Initial commit — Godot space roguelite source
- Touch controls: direct InputEventScreenTouch in shop_ui (bypass relay) - ItemDB: static preload list instead of DirAccess scan (export fix) - All 18 items with EN localisation (name_en, desc_en, category_en) - Ship playstyles: NOVA-1 shield, INFERNO ram, AURORA agile/tank - Quasar: SMBH visual, jet boost, merge, push, BH-eating - Atlas & UI text updated EN+DE Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,605 @@
|
||||
extends Node2D
|
||||
|
||||
# ─── Werkstatt UI ─────────────────────────────────────────────────────────────
|
||||
# Zwei Phasen:
|
||||
# PHASE_ATTR — 3 kostenlose Attribut-Upgrades (Pflicht, eins auswählen)
|
||||
# PHASE_SHOP — Werkstatt: beliebig viele Items kaufen, SPACE beendet
|
||||
#
|
||||
# Items kommen aus ItemDB.roll_werkstatt() (Plugin-System unter res://items/).
|
||||
# Gekaufte Items werden via stats.apply_item_def(def) angewendet und erscheinen
|
||||
# visuell am Schiff-Preview im Zentrum.
|
||||
# Emits: shop_closed(remaining_credits, updated_stats, owned_item_ids)
|
||||
|
||||
signal shop_closed(remaining_credits: int, new_stats: ShipStats, items: Array, reroll_count: int)
|
||||
|
||||
# ─── Konstanten ───────────────────────────────────────────────────────────────
|
||||
const REROLL_BASE_COST := 60
|
||||
const REROLL_STEP_COST := 42
|
||||
const SHOP_CARD_COUNT := 4
|
||||
const ATTR_CARD_COUNT := 3
|
||||
|
||||
# Cockpit palette
|
||||
const COL_BG := Color(0.0, 0.0, 0.04, 0.92)
|
||||
const COL_PRIMARY := Color(0.0, 1.0, 0.533, 1.0)
|
||||
const COL_ACCENT := Color(0.27, 1.0, 0.8, 1.0)
|
||||
const COL_DIM := Color(0.0, 0.27, 0.13, 0.55)
|
||||
const COL_WHITE := Color(1.0, 1.0, 1.0, 0.90)
|
||||
const COL_WARN := Color(1.0, 0.67, 0.0, 0.90)
|
||||
const COL_POS := Color(0.3, 1.0, 0.4, 1.0)
|
||||
const COL_NEG := Color(1.0, 0.35, 0.35, 1.0)
|
||||
|
||||
# ─── Attribut-Pool (Phase 1 — gratis, reine Buffs) ────────────────────────────
|
||||
# Jedes Attribut ist ein Dictionary wie ein ItemDef-Effect.
|
||||
const ATTR_POOL: Array = [
|
||||
{ "id": "a_speed", "name": "Schubverstärker", "name_en": "Thruster", "desc": "+8% Geschwindigkeit", "desc_en": "+8% Speed", "effects": { "speed_mult": 1.08 } },
|
||||
{ "id": "a_turn", "name": "Agilität", "name_en": "Agility", "desc": "+12% Wendigkeit", "desc_en": "+12% Handling", "effects": { "turn_mult": 1.12 } },
|
||||
{ "id": "a_fire", "name": "Schnellfeuer", "name_en": "Rapid Fire", "desc": "+12% Feuerrate", "desc_en": "+12% Fire Rate", "effects": { "fire_rate_mult": 1.12 } },
|
||||
{ "id": "a_damage", "name": "Schwere Ladung", "name_en": "Heavy Load", "desc": "+12% Schaden", "desc_en": "+12% Damage", "effects": { "damage_mult": 1.12 } },
|
||||
{ "id": "a_proj", "name": "Lineare Ballistik","name_en": "Linear Ballistics", "desc": "+12% Projektilgeschw.", "desc_en": "+12% Projectile Speed", "effects": { "bullet_speed_mult": 1.12 } },
|
||||
{ "id": "a_shield", "name": "Panzerplatte", "name_en": "Armor Plate", "desc": "+1 Schutzladung", "desc_en": "+1 Shield Charge", "effects": { "shield_charges": 1 } },
|
||||
{ "id": "a_invuln", "name": "Schildgenerator", "name_en": "Shield Generator", "desc": "+18% Unverwundbarkeit", "desc_en": "+18% Invulnerability", "effects": { "invuln_mult": 1.18 } },
|
||||
{ "id": "a_credits", "name": "Profitoptimierer", "name_en": "Profit Optimizer", "desc": "+8% Kreditgewinn", "desc_en": "+8% Credit Gain", "effects": { "credit_bonus": 1.08 } },
|
||||
]
|
||||
|
||||
# ─── Phasen-State ─────────────────────────────────────────────────────────────
|
||||
enum Phase { ATTR, SHOP }
|
||||
var _phase: int = Phase.ATTR
|
||||
|
||||
# ─── Session-State ────────────────────────────────────────────────────────────
|
||||
var _lives: int = 3
|
||||
var _credits: int = 0
|
||||
var _stats: ShipStats = null
|
||||
var _owned_ids: Array = []
|
||||
var _player_label: String = ""
|
||||
var _ship_palette: Dictionary = {}
|
||||
var _wave: int = 1
|
||||
var _player_idx: int = 1 # 1 = P1 (Pfeiltasten/Enter/Space), 2 = P2 (ADWEF/Q)
|
||||
|
||||
# Phase 1
|
||||
var _attr_choices: Array = [] # 3 zufällige Attribute
|
||||
var _attr_cursor: int = 0
|
||||
|
||||
# Phase 2
|
||||
var _shop_cards: Array = [] # aktuelle 4 ItemDef-Objekte
|
||||
var _shop_cursor: int = 0
|
||||
var _reroll_count: int = 0
|
||||
|
||||
# UI
|
||||
var _blink: float = 0.0
|
||||
var W: float = 960.0
|
||||
var H: float = 600.0
|
||||
|
||||
# Touch state (direct InputEventScreenTouch handling)
|
||||
var _tc_id: int = -1
|
||||
var _tc_start: Vector2 = Vector2.ZERO
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
func open(lives: int, credits: int, stats: ShipStats, owned: Array,
|
||||
player_label: String = "", ship_palette: Dictionary = {},
|
||||
wave: int = 1, reroll_count: int = 0, player_idx: int = 1) -> void:
|
||||
_lives = lives
|
||||
_credits = credits
|
||||
_stats = stats if stats != null else ShipStats.new()
|
||||
_owned_ids = owned.duplicate()
|
||||
_player_label = player_label
|
||||
_ship_palette = ship_palette
|
||||
_wave = wave
|
||||
# Attribut-Phase nur in ungeraden Wellen (1, 3, 5, ...). Gerade Wellen
|
||||
# starten direkt im Shop — langsamere Power-Kurve.
|
||||
_phase = Phase.ATTR if (wave % 2) == 1 else Phase.SHOP
|
||||
_attr_cursor = 0
|
||||
_shop_cursor = 0
|
||||
# Reroll-Counter persistiert über Wellen hinweg (von main.gd durchgereicht).
|
||||
_reroll_count = reroll_count
|
||||
_player_idx = player_idx
|
||||
_blink = 0.0
|
||||
if _phase == Phase.ATTR:
|
||||
_roll_attr()
|
||||
_roll_shop()
|
||||
visible = true
|
||||
|
||||
func _roll_attr() -> void:
|
||||
var pool := ATTR_POOL.duplicate()
|
||||
pool.shuffle()
|
||||
_attr_choices = pool.slice(0, ATTR_CARD_COUNT)
|
||||
|
||||
func _roll_shop() -> void:
|
||||
# Exclude already-owned item IDs so you don't see the same thing twice in one roll
|
||||
_shop_cards = ItemDB.roll_werkstatt(SHOP_CARD_COUNT, _owned_ids)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not visible: return
|
||||
_blink += delta
|
||||
queue_redraw()
|
||||
|
||||
# ─── Input ────────────────────────────────────────────────────────────────────
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not visible: return
|
||||
if not event is InputEventScreenTouch: return
|
||||
if event.pressed:
|
||||
if _tc_id == -1:
|
||||
_tc_id = event.index
|
||||
_tc_start = event.position
|
||||
elif event.index == _tc_id:
|
||||
_tc_id = -1
|
||||
var d: Vector2 = event.position - _tc_start
|
||||
if d.length() < 32.0:
|
||||
_touch_tap(event.position)
|
||||
elif abs(d.x) >= abs(d.y):
|
||||
_touch_nav("ui_left" if d.x < 0 else "ui_right")
|
||||
else:
|
||||
_touch_nav("ui_up" if d.y < 0 else "ui_down")
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _touch_tap(p: Vector2) -> void:
|
||||
if _phase == Phase.ATTR:
|
||||
var card_w := 230.0; var card_h := 140.0
|
||||
var n := _attr_choices.size()
|
||||
var total_w: float = card_w * n + 20.0 * (n - 1)
|
||||
var start_x: float = (W - total_w) * 0.5
|
||||
var cy: float = H * 0.5 - 30.0 - card_h * 0.5 + 40.0
|
||||
for i in n:
|
||||
var cx: float = start_x + i * (card_w + 20.0)
|
||||
if Rect2(cx, cy, card_w, card_h).has_point(p):
|
||||
_attr_cursor = i
|
||||
_confirm_attr()
|
||||
return
|
||||
else:
|
||||
var card_w := 250.0; var card_h := 140.0
|
||||
var card_pos: Array = [
|
||||
Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - card_h - 50.0),
|
||||
Vector2(W * 0.5 + 120.0, H * 0.5 - card_h - 50.0),
|
||||
Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - 10.0),
|
||||
Vector2(W * 0.5 + 120.0, H * 0.5 - 10.0),
|
||||
]
|
||||
for i in min(_shop_cards.size(), card_pos.size()):
|
||||
if Rect2(card_pos[i].x, card_pos[i].y, card_w, card_h).has_point(p):
|
||||
_shop_cursor = i
|
||||
_try_buy(i)
|
||||
return
|
||||
if p.y >= H - 52.0:
|
||||
if p.x < W * 0.5:
|
||||
_try_reroll()
|
||||
else:
|
||||
_close()
|
||||
|
||||
func _touch_nav(action: String) -> void:
|
||||
if _phase == Phase.ATTR:
|
||||
match action:
|
||||
"ui_left": _attr_cursor = (_attr_cursor - 1 + _attr_choices.size()) % _attr_choices.size()
|
||||
"ui_right": _attr_cursor = (_attr_cursor + 1) % _attr_choices.size()
|
||||
else:
|
||||
match action:
|
||||
"ui_left": _move_shop_cursor(-1)
|
||||
"ui_right": _move_shop_cursor(1)
|
||||
"ui_up": _move_shop_cursor(-2)
|
||||
"ui_down": _move_shop_cursor(2)
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not visible: return
|
||||
if _phase == Phase.ATTR:
|
||||
_handle_attr_input(event)
|
||||
else:
|
||||
_handle_shop_input(event)
|
||||
|
||||
func _handle_attr_input(event: InputEvent) -> void:
|
||||
if _player_idx == 2:
|
||||
if event.is_action_pressed("p2_left"):
|
||||
_attr_cursor = (_attr_cursor - 1 + _attr_choices.size()) % _attr_choices.size()
|
||||
elif event.is_action_pressed("p2_right"):
|
||||
_attr_cursor = (_attr_cursor + 1) % _attr_choices.size()
|
||||
elif event.is_action_pressed("p2_shoot"):
|
||||
_confirm_attr()
|
||||
else:
|
||||
if event.is_action_pressed("ui_left"):
|
||||
_attr_cursor = (_attr_cursor - 1 + _attr_choices.size()) % _attr_choices.size()
|
||||
elif event.is_action_pressed("ui_right"):
|
||||
_attr_cursor = (_attr_cursor + 1) % _attr_choices.size()
|
||||
elif event is InputEventKey and event.pressed and event.keycode == KEY_SPACE:
|
||||
_confirm_attr()
|
||||
elif event.is_action_pressed("ui_accept"):
|
||||
_confirm_attr()
|
||||
|
||||
func _handle_shop_input(event: InputEvent) -> void:
|
||||
if _player_idx == 2:
|
||||
if event.is_action_pressed("p2_wipe") or event.is_action_pressed("ui_cancel"):
|
||||
_close()
|
||||
elif event.is_action_pressed("p2_left"):
|
||||
_move_shop_cursor(-1)
|
||||
elif event.is_action_pressed("p2_right"):
|
||||
_move_shop_cursor(1)
|
||||
elif event.is_action_pressed("p2_thrust"):
|
||||
_move_shop_cursor(-2)
|
||||
elif event.is_action_pressed("p2_boost"):
|
||||
_try_reroll()
|
||||
elif event.is_action_pressed("p2_shoot"):
|
||||
_try_buy(_shop_cursor)
|
||||
else:
|
||||
if event is InputEventKey and event.pressed:
|
||||
if event.keycode == KEY_SPACE or event.keycode == KEY_ESCAPE:
|
||||
_close()
|
||||
return
|
||||
elif event.keycode == KEY_ENTER:
|
||||
_try_buy(_shop_cursor)
|
||||
return
|
||||
elif event.keycode == KEY_R:
|
||||
_try_reroll()
|
||||
return
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_close()
|
||||
elif event.is_action_pressed("ui_left"):
|
||||
_move_shop_cursor(-1)
|
||||
elif event.is_action_pressed("ui_right"):
|
||||
_move_shop_cursor(1)
|
||||
elif event.is_action_pressed("ui_up"):
|
||||
_move_shop_cursor(-2)
|
||||
elif event.is_action_pressed("ui_down"):
|
||||
_move_shop_cursor(2)
|
||||
elif event.is_action_pressed("ui_accept"):
|
||||
_try_buy(_shop_cursor)
|
||||
|
||||
func _move_shop_cursor(delta: int) -> void:
|
||||
if _shop_cards.is_empty(): return
|
||||
_shop_cursor = (_shop_cursor + delta + _shop_cards.size()) % _shop_cards.size()
|
||||
|
||||
func _confirm_attr() -> void:
|
||||
if _attr_cursor >= _attr_choices.size(): return
|
||||
var choice: Dictionary = _attr_choices[_attr_cursor]
|
||||
_stats.apply_item(choice["effects"])
|
||||
_phase = Phase.SHOP
|
||||
|
||||
func _try_buy(idx: int) -> void:
|
||||
if idx < 0 or idx >= _shop_cards.size(): return
|
||||
var def: ItemDef = _shop_cards[idx]
|
||||
if _credits < def.cost: return
|
||||
_credits -= def.cost
|
||||
_stats.apply_item_def(def)
|
||||
_owned_ids.append(def.id)
|
||||
_shop_cards.remove_at(idx)
|
||||
if _shop_cards.is_empty():
|
||||
_shop_cursor = 0
|
||||
else:
|
||||
_shop_cursor = min(_shop_cursor, _shop_cards.size() - 1)
|
||||
|
||||
func _try_reroll() -> void:
|
||||
var cost: int = REROLL_BASE_COST + _reroll_count * REROLL_STEP_COST
|
||||
if _credits < cost: return
|
||||
_credits -= cost
|
||||
_reroll_count += 1
|
||||
_roll_shop()
|
||||
_shop_cursor = 0
|
||||
|
||||
func _close() -> void:
|
||||
visible = false
|
||||
shop_closed.emit(_credits, _stats, _owned_ids, _reroll_count)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Drawing
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
func _draw() -> void:
|
||||
var vs: Vector2 = get_viewport_rect().size
|
||||
W = vs.x; H = vs.y
|
||||
draw_rect(get_viewport_rect(), COL_BG)
|
||||
|
||||
_draw_header()
|
||||
_draw_brackets(8, 8, W - 8, H - 8, COL_DIM, 16)
|
||||
|
||||
if _phase == Phase.ATTR:
|
||||
_draw_attr_phase()
|
||||
else:
|
||||
_draw_shop_phase()
|
||||
|
||||
_draw_footer()
|
||||
|
||||
# ── Header ────────────────────────────────────────────────────────────────────
|
||||
func _draw_header() -> void:
|
||||
draw_rect(Rect2(0, 0, W, 50.0), Color(0.0, 0.04, 0.02, 0.95))
|
||||
draw_line(Vector2(0, 50), Vector2(W, 50), COL_DIM, 1.0)
|
||||
|
||||
var phase_key: String = "werk_phase_attr" if _phase == Phase.ATTR else "werk_phase_shop"
|
||||
var wave_str := Tr.t("werk_wave") % _wave
|
||||
if _player_label != "":
|
||||
_draw_text_c(_player_label, W * 0.5, 10.0, 9, COL_DIM)
|
||||
_draw_text_c(Tr.t(phase_key) + wave_str, W * 0.5, 26.0, 13, COL_PRIMARY)
|
||||
|
||||
# Lives + credits oben rechts
|
||||
var lives_str := ""
|
||||
for li in 3:
|
||||
lives_str += ("●" if li < _lives else "○")
|
||||
_draw_text(lives_str + " CR: %d" % _credits, W - 190.0, 14.0, 11, COL_PRIMARY)
|
||||
|
||||
# ── Attribut-Phase ────────────────────────────────────────────────────────────
|
||||
func _draw_attr_phase() -> void:
|
||||
var y_mid := H * 0.5 - 30.0
|
||||
_draw_text_c(Tr.t("werk_attr_prompt"), W * 0.5, 76.0, 11, COL_ACCENT)
|
||||
|
||||
var card_w := 230.0; var card_h := 140.0
|
||||
var n: int = _attr_choices.size()
|
||||
var total_w: float = card_w * n + 20.0 * (n - 1)
|
||||
var start_x: float = (W - total_w) * 0.5
|
||||
|
||||
for i in n:
|
||||
var choice: Dictionary = _attr_choices[i]
|
||||
var cx: float = start_x + i * (card_w + 20.0)
|
||||
var cy: float = y_mid - card_h * 0.5 + 40.0
|
||||
var is_sel: bool = i == _attr_cursor
|
||||
var pulse: float = 0.5 + 0.5 * sin(_blink * 3.0)
|
||||
|
||||
draw_rect(Rect2(cx, cy, card_w, card_h),
|
||||
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.10))
|
||||
var bc: Color = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.55 + 0.45 * pulse) if is_sel else COL_DIM
|
||||
_draw_brackets(cx, cy, cx + card_w, cy + card_h, bc, 14)
|
||||
|
||||
_draw_text_c(Tr.t("werk_attr_tag"), cx + card_w * 0.5, cy + 10.0, 8,
|
||||
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.6))
|
||||
var _en := Settings.language == "en"
|
||||
_draw_text_c(choice["name_en"] if _en else choice["name"], cx + card_w * 0.5, cy + 32.0, 13, COL_WHITE)
|
||||
_draw_text_c(choice["desc_en"] if _en else choice["desc"], cx + card_w * 0.5, cy + 62.0, 10, COL_POS)
|
||||
|
||||
if is_sel:
|
||||
var hint_col := Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.6 + 0.4 * pulse)
|
||||
var pick_key: String = "werk_pick_p2" if _player_idx == 2 else "werk_pick"
|
||||
_draw_text_c(Tr.hint(pick_key), cx + card_w * 0.5, cy + card_h - 24.0, 10, hint_col)
|
||||
|
||||
# ── Werkstatt-Phase ───────────────────────────────────────────────────────────
|
||||
func _draw_shop_phase() -> void:
|
||||
# Schiff-Preview in der Mitte (leicht nach oben verschoben)
|
||||
_draw_ship_preview(W * 0.5, H * 0.5 - 15.0)
|
||||
|
||||
# 2×2 Karten um das Schiff herum — nach oben geschoben, damit Vorschau-Panel Platz hat
|
||||
var card_w := 250.0; var card_h := 140.0
|
||||
var positions: Array = [
|
||||
Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - card_h - 50.0), # Top-left y=110
|
||||
Vector2(W * 0.5 + 120.0, H * 0.5 - card_h - 50.0), # Top-right y=110
|
||||
Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - 10.0), # Bottom-left y=290
|
||||
Vector2(W * 0.5 + 120.0, H * 0.5 - 10.0), # Bottom-right y=290
|
||||
]
|
||||
|
||||
for i in _shop_cards.size():
|
||||
var def: ItemDef = _shop_cards[i]
|
||||
var p: Vector2 = positions[i] if i < positions.size() else Vector2(20, 80 + i * 40)
|
||||
_draw_shop_card(def, p.x, p.y, card_w, card_h, i == _shop_cursor)
|
||||
|
||||
# Vorschau-Panel — unterhalb der unteren Kartenreihe (y=430), mit 8px Abstand
|
||||
if not _shop_cards.is_empty() and _shop_cursor < _shop_cards.size():
|
||||
_draw_preview_panel(_shop_cards[_shop_cursor], W * 0.5, H - 119.0)
|
||||
|
||||
# Owned items Banner (oben unter dem Header)
|
||||
if _owned_ids.size() > 0:
|
||||
_draw_owned_banner(W * 0.5, 60.0)
|
||||
|
||||
func _draw_shop_card(def: ItemDef, x: float, y: float, w: float, h: float, is_sel: bool) -> void:
|
||||
var pulse: float = 0.5 + 0.5 * sin(_blink * 3.0)
|
||||
var can_afford: bool = _credits >= def.cost
|
||||
var rarity_col: Color = ItemDB.RARITY_COLORS.get(def.rarity, COL_PRIMARY)
|
||||
|
||||
var bg_a: float = 0.18 if can_afford else 0.08
|
||||
draw_rect(Rect2(x, y, w, h), Color(rarity_col.r, rarity_col.g, rarity_col.b, bg_a))
|
||||
|
||||
var border_col: Color
|
||||
if is_sel:
|
||||
border_col = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.55 + 0.45 * pulse)
|
||||
else:
|
||||
border_col = Color(rarity_col.r, rarity_col.g, rarity_col.b, 0.35 if can_afford else 0.15)
|
||||
_draw_brackets(x, y, x + w, y + h, border_col, 12)
|
||||
|
||||
# Icon + Kategorie
|
||||
var _item_en := Settings.language == "en"
|
||||
var disp_cat := def.category_en if _item_en and def.category_en != "" else def.category
|
||||
var disp_name := def.name_en if _item_en and def.name_en != "" else def.name
|
||||
_draw_text(def.icon + " " + disp_cat, x + 12.0, y + 8.0, 8,
|
||||
Color(rarity_col.r, rarity_col.g, rarity_col.b, 0.7 if can_afford else 0.3))
|
||||
|
||||
# Name
|
||||
var name_a: float = 0.9 if can_afford else 0.35
|
||||
_draw_text(disp_name, x + 12.0, y + 26.0, 13, Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b, name_a))
|
||||
|
||||
# Effekte (bis zu 4 Zeilen, + grün / − rot)
|
||||
var line_y: float = y + 58.0
|
||||
var count := 0
|
||||
for key: String in def.effects:
|
||||
if count >= 4: break
|
||||
var val = def.effects[key]
|
||||
var is_pos: bool = ItemDB.is_positive_effect(key, val)
|
||||
var sign_str: String = "+ " if is_pos else "− "
|
||||
var col: Color = COL_POS if is_pos else COL_NEG
|
||||
if not can_afford:
|
||||
col = Color(col.r, col.g, col.b, 0.35)
|
||||
var line: String = "%s%s %s" % [sign_str, ItemDB.stat_display_name(key), _fmt_val(key, val)]
|
||||
_draw_text(line, x + 16.0, line_y, 9, col)
|
||||
line_y += 13.0
|
||||
count += 1
|
||||
|
||||
# Cost unten rechts
|
||||
var cost_col: Color = COL_WARN if not can_afford else Color(1.0, 1.0, 0.6, 0.95)
|
||||
_draw_text("%d CR" % def.cost, x + w - 60.0, y + h - 20.0, 12, cost_col)
|
||||
|
||||
# Hint auf aktiver Karte
|
||||
if is_sel:
|
||||
var hint_col: Color = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.6 + 0.4 * pulse)
|
||||
var hint: String = Tr.hint("werk_buy_hint_p2" if _player_idx == 2 else "werk_buy_hint_p1") if can_afford else Tr.t("shop_no_credits")
|
||||
_draw_text(hint, x + 12.0, y + h - 20.0, 9, hint_col)
|
||||
|
||||
# ── Preview-Panel: Vorher/Nachher-Diff ────────────────────────────────────────
|
||||
func _draw_preview_panel(def: ItemDef, cx: float, cy: float) -> void:
|
||||
var pw := 420.0; var ph := 86.0
|
||||
var px := cx - pw * 0.5; var py := cy - ph * 0.5
|
||||
draw_rect(Rect2(px, py, pw, ph), Color(0.0, 0.04, 0.02, 0.9))
|
||||
_draw_brackets(px, py, px + pw, py + ph, COL_DIM, 10)
|
||||
|
||||
_draw_text_c(Tr.t("werk_preview") + " · " + def.name,
|
||||
cx, py + 6.0, 9, COL_ACCENT)
|
||||
|
||||
# Liste: key: before → after
|
||||
var col_x: float = px + 14.0
|
||||
var line_y: float = py + 26.0
|
||||
var count := 0
|
||||
for key: String in def.effects:
|
||||
if count >= 3: break
|
||||
var val = def.effects[key]
|
||||
var before: float = _stat_value(key)
|
||||
var after: float = _project_stat(key, val)
|
||||
var pos: bool = ItemDB.is_positive_effect(key, val)
|
||||
var col: Color = COL_POS if pos else COL_NEG
|
||||
var line := "%s: %.2f → %.2f" % [ItemDB.stat_display_name(key), before, after]
|
||||
_draw_text(line, col_x, line_y, 10, col)
|
||||
line_y += 14.0
|
||||
count += 1
|
||||
|
||||
# Hull-Size Warnung
|
||||
if def.hull_size_bonus > 0.0:
|
||||
_draw_text(Tr.t("werk_grows"), px + pw - 150.0, py + ph - 22.0, 9, COL_WARN)
|
||||
|
||||
func _stat_value(key: String) -> float:
|
||||
match key:
|
||||
"speed_mult": return _stats.speed_mult
|
||||
"turn_mult": return _stats.turn_mult
|
||||
"fire_rate_mult": return _stats.fire_rate_mult
|
||||
"damage_mult": return _stats.damage_mult
|
||||
"bullet_speed_mult": return _stats.bullet_speed_mult
|
||||
"bullet_count": return float(_stats.bullet_count)
|
||||
"shield_charges": return float(_stats.shield_charges)
|
||||
"invuln_mult": return _stats.invuln_mult
|
||||
"bh_resist": return _stats.bh_resist
|
||||
"wipe_mult": return _stats.wipe_mult
|
||||
"credit_bonus": return _stats.credit_bonus
|
||||
return 0.0
|
||||
|
||||
func _project_stat(key: String, val) -> float:
|
||||
match key:
|
||||
"speed_mult","turn_mult","fire_rate_mult","damage_mult", \
|
||||
"bullet_speed_mult","invuln_mult","wipe_mult","credit_bonus":
|
||||
return _stat_value(key) * float(val)
|
||||
"bullet_count","shield_charges":
|
||||
return _stat_value(key) + float(val)
|
||||
"bh_resist":
|
||||
return min(0.9, _stat_value(key) + float(val))
|
||||
return _stat_value(key)
|
||||
|
||||
func _fmt_val(key: String, val) -> String:
|
||||
match key:
|
||||
"bullet_count": return "+%d" % int(val)
|
||||
"shield_charges": return ("+%d" % int(val)) if int(val) >= 0 else "%d" % int(val)
|
||||
"bh_resist": return "+%.0f%%" % (float(val) * 100.0)
|
||||
_:
|
||||
var f: float = float(val)
|
||||
var pct: float = (f - 1.0) * 100.0
|
||||
return "%+.0f%%" % pct
|
||||
|
||||
# ── Ship preview im Zentrum ───────────────────────────────────────────────────
|
||||
func _draw_ship_preview(cx: float, cy: float) -> void:
|
||||
var bw := 180.0; var bh := 180.0
|
||||
draw_rect(Rect2(cx - bw * 0.5, cy - bh * 0.5, bw, bh),
|
||||
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.05))
|
||||
_draw_brackets(cx - bw * 0.5, cy - bh * 0.5, cx + bw * 0.5, cy + bh * 0.5,
|
||||
COL_DIM, 14)
|
||||
|
||||
_draw_text_c(Tr.t("werk_ship_preview"), cx, cy - bh * 0.5 + 6.0, 8,
|
||||
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.55))
|
||||
|
||||
# Schiff zeichnen — gleich wie Spaceship.HULL_PIXELS, skaliert
|
||||
var hs: float = _stats.hull_scale if _stats != null else 3.0
|
||||
var preview_scale: float = 3.5 # Extra-Zoom für Preview
|
||||
var off_mul: float = hs / 3.0 * preview_scale
|
||||
var rect_sz: float = hs * preview_scale * 0.9
|
||||
var rect_half: float = rect_sz * 0.5
|
||||
var heading: float = -PI * 0.5 # Schiff zeigt nach oben
|
||||
var cos_h: float = cos(heading); var sin_h: float = sin(heading)
|
||||
|
||||
for px_data in Spaceship.HULL_PIXELS:
|
||||
var lx: float = px_data[0] * off_mul
|
||||
var ly: float = px_data[1] * off_mul
|
||||
var col: Color = _ship_palette.get(px_data[2], COL_WHITE)
|
||||
var rx: float = cx + lx * cos_h - ly * sin_h
|
||||
var ry: float = cy + lx * sin_h + ly * cos_h
|
||||
draw_rect(Rect2(rx - rect_half, ry - rect_half, rect_sz, rect_sz), col)
|
||||
|
||||
# Attachments aus owned_items
|
||||
var seen: Dictionary = {}
|
||||
for id: String in _owned_ids:
|
||||
var def: ItemDef = ItemDB.get_def_by_id(id)
|
||||
if def == null or def.visual_pixels.is_empty(): continue
|
||||
var stack: int = int(seen.get(id, 0))
|
||||
seen[id] = stack + 1
|
||||
var stack_off: float = float(stack) * 0.6
|
||||
for vpx in def.visual_pixels:
|
||||
var lx: float = (float(vpx[0]) + stack_off) * off_mul
|
||||
var ly: float = float(vpx[1]) * off_mul
|
||||
var col: Color = _ship_palette.get(vpx[2], COL_WHITE)
|
||||
var rx: float = cx + lx * cos_h - ly * sin_h
|
||||
var ry: float = cy + lx * sin_h + ly * cos_h
|
||||
draw_rect(Rect2(rx - rect_half, ry - rect_half, rect_sz, rect_sz), col)
|
||||
|
||||
func _draw_owned_banner(cx: float, cy: float) -> void:
|
||||
var names: Array = []
|
||||
for id: String in _owned_ids:
|
||||
var def: ItemDef = ItemDB.get_def_by_id(id)
|
||||
if def != null:
|
||||
names.append(def.name)
|
||||
if names.is_empty(): return
|
||||
var line := Tr.t("shop_owned") + " " + " · ".join(names)
|
||||
if line.length() > 100:
|
||||
line = line.substr(0, 97) + "…"
|
||||
_draw_text_c(line, cx, cy, 9, Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.5))
|
||||
|
||||
# ── Footer ────────────────────────────────────────────────────────────────────
|
||||
func _draw_footer() -> void:
|
||||
draw_rect(Rect2(0, H - 52.0, W, 52.0), Color(0.0, 0.04, 0.02, 0.95))
|
||||
draw_line(Vector2(0, H - 52.0), Vector2(W, H - 52.0), COL_DIM, 1.0)
|
||||
|
||||
var p2: bool = _player_idx == 2
|
||||
var is_touch: bool = Settings.last_input_device == "touch"
|
||||
var pulse: float = 0.5 + 0.4 * sin(_blink * 2.5)
|
||||
if _phase == Phase.ATTR:
|
||||
if is_touch:
|
||||
_draw_text_c("● TAP", W * 0.5, H - 26.0, 11,
|
||||
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse))
|
||||
else:
|
||||
_draw_text_c(Tr.hint("werk_attr_footer_p2" if p2 else "werk_attr_footer"), W * 0.5, H - 48.0, 9, COL_DIM)
|
||||
_draw_text_c(Tr.hint("werk_confirm_p2" if p2 else "werk_confirm"), W * 0.5, H - 26.0, 11,
|
||||
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse))
|
||||
else:
|
||||
var reroll_cost: int = REROLL_BASE_COST + _reroll_count * REROLL_STEP_COST
|
||||
var reroll_col: Color = COL_DIM if _credits < reroll_cost else COL_PRIMARY
|
||||
if is_touch:
|
||||
# Touch: left half = REROLL tap zone, right half = SKIP tap zone
|
||||
draw_rect(Rect2(0, H - 52.0, W * 0.5 - 1, 52.0),
|
||||
Color(reroll_col.r, reroll_col.g, reroll_col.b, 0.06))
|
||||
draw_rect(Rect2(W * 0.5 + 1, H - 52.0, W * 0.5 - 1, 52.0),
|
||||
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.06))
|
||||
var reroll_str: String = ("REROLL %d CR" if Settings.language == "en" else "REROLL %d CR") % reroll_cost
|
||||
_draw_text_c(reroll_str, W * 0.25, H - 26.0, 10, reroll_col)
|
||||
var skip_str: String = "SKIP ►" if Settings.language == "en" else "WEITER ►"
|
||||
_draw_text_c(skip_str, W * 0.75, H - 26.0, 11,
|
||||
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse))
|
||||
else:
|
||||
_draw_text_c(Tr.hint("werk_shop_footer_p2" if p2 else "werk_shop_footer"), W * 0.5, H - 48.0, 9, COL_DIM)
|
||||
var reroll_key: String = "Q" if p2 else "R"
|
||||
_draw_text_c(Tr.t("werk_reroll") % [reroll_key, reroll_cost], W * 0.30, H - 26.0, 10, reroll_col)
|
||||
_draw_text_c(Tr.hint("werk_continue_p2" if p2 else "shop_continue"), W * 0.72, H - 26.0, 11,
|
||||
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse))
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Text + Shape Helpers
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
func _draw_text(text: String, x: float, y: float, sz: int, col: Color) -> void:
|
||||
var font := ThemeDB.fallback_font
|
||||
draw_string(font, Vector2(x, y + sz), text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col)
|
||||
|
||||
func _draw_text_c(text: String, x: float, y: float, sz: int, col: Color) -> void:
|
||||
var font := ThemeDB.fallback_font
|
||||
var tw: float = font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
|
||||
draw_string(font, Vector2(x - tw * 0.5, y + sz), text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col)
|
||||
|
||||
func _draw_brackets(x1: float, y1: float, x2: float, y2: float, col: Color, arm: float) -> void:
|
||||
var bw: float = 1.5
|
||||
draw_line(Vector2(x1, y1), Vector2(x1 + arm, y1), col, bw)
|
||||
draw_line(Vector2(x1, y1), Vector2(x1, y1 + arm), col, bw)
|
||||
draw_line(Vector2(x2, y1), Vector2(x2 - arm, y1), col, bw)
|
||||
draw_line(Vector2(x2, y1), Vector2(x2, y1 + arm), col, bw)
|
||||
draw_line(Vector2(x1, y2), Vector2(x1 + arm, y2), col, bw)
|
||||
draw_line(Vector2(x1, y2), Vector2(x1, y2 - arm), col, bw)
|
||||
draw_line(Vector2(x2, y2), Vector2(x2 - arm, y2), col, bw)
|
||||
draw_line(Vector2(x2, y2), Vector2(x2, y2 - arm), col, bw)
|
||||
Reference in New Issue
Block a user