Files
spacel/scripts/shop_ui.gd
T
alpacaman edc40f9008 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>
2026-04-21 14:38:09 +02:00

606 lines
26 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)