Initial commit — Station 0 vertical slice in progress
Includes full design documentation (ARCHITECTURE, DESIGN_NOTES, CONTENT_SPEC, PRODUCTION_NOTES) and active Godot 4 / GDScript codebase. Core systems built: player, floor generation, room system, enemy base, body part upgrades, run manager, save system, HUD, EventBus.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
extends CanvasLayer
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Layout constants (screen space: 1920 x 1080)
|
||||
# ---------------------------------------------------------------------------
|
||||
const SCREEN_W := 1920.0
|
||||
const SCREEN_H := 1080.0
|
||||
const PANEL_W := 620.0
|
||||
const PANEL_H := 500.0
|
||||
const LINE_H := 36.0
|
||||
const LIST_PAD_X := 24.0
|
||||
const LIST_PAD_Y := 60.0
|
||||
|
||||
const BG_COLOR := Color(0.06, 0.07, 0.12, 0.94)
|
||||
const CURSOR_COLOR := Color(0.9, 0.85, 0.3, 1)
|
||||
const NORMAL_COLOR := Color(0.75, 0.75, 0.85, 1)
|
||||
const EQUIPPED_COLOR := Color(0.4, 0.9, 0.6, 1)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
var _items: Array = [] # Array[BodyPartData] — sorted by slot
|
||||
var _cursor: int = 0
|
||||
var _item_labels: Array = [] # Array[Label]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UI nodes (built once in _ready)
|
||||
# ---------------------------------------------------------------------------
|
||||
var _bg: Polygon2D
|
||||
var _title_lbl: Label
|
||||
var _hint_lbl: Label
|
||||
var _list_root: Node2D # parent for item labels (positioned in screen space)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Boot
|
||||
# ---------------------------------------------------------------------------
|
||||
func _ready() -> void:
|
||||
_build_static_ui()
|
||||
hide()
|
||||
|
||||
|
||||
func _build_static_ui() -> void:
|
||||
var cx := SCREEN_W / 2.0
|
||||
var cy := SCREEN_H / 2.0
|
||||
var hw := PANEL_W / 2.0
|
||||
var hh := PANEL_H / 2.0
|
||||
|
||||
# Background panel
|
||||
_bg = Polygon2D.new()
|
||||
_bg.polygon = PackedVector2Array([
|
||||
Vector2(-hw, -hh), Vector2(hw, -hh),
|
||||
Vector2( hw, hh), Vector2(-hw, hh),
|
||||
])
|
||||
_bg.color = BG_COLOR
|
||||
_bg.position = Vector2(cx, cy)
|
||||
add_child(_bg)
|
||||
|
||||
# Title
|
||||
_title_lbl = Label.new()
|
||||
_title_lbl.text = "— ARMORY —"
|
||||
_title_lbl.position = Vector2(cx - 60, cy - hh + 14)
|
||||
add_child(_title_lbl)
|
||||
|
||||
# Hint bar
|
||||
_hint_lbl = Label.new()
|
||||
_hint_lbl.text = "[↑↓] Navigate [Enter] Equip [Esc] Close"
|
||||
_hint_lbl.position = Vector2(cx - hw + LIST_PAD_X, cy + hh - 30)
|
||||
add_child(_hint_lbl)
|
||||
|
||||
# Container node for item labels
|
||||
_list_root = Node2D.new()
|
||||
_list_root.position = Vector2(cx - hw + LIST_PAD_X, cy - hh + LIST_PAD_Y)
|
||||
add_child(_list_root)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input
|
||||
# ---------------------------------------------------------------------------
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not visible:
|
||||
return
|
||||
|
||||
if event.is_action_pressed("ui_up"):
|
||||
_cursor = wrapi(_cursor - 1, 0, maxi(1, _items.size()))
|
||||
_refresh_labels()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.is_action_pressed("ui_down"):
|
||||
_cursor = wrapi(_cursor + 1, 0, maxi(1, _items.size()))
|
||||
_refresh_labels()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.is_action_pressed("ui_accept"):
|
||||
_equip_selected()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.is_action_pressed("ui_cancel"):
|
||||
hide()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Open — call from hub.gd
|
||||
# ---------------------------------------------------------------------------
|
||||
func open() -> void:
|
||||
_rebuild_items()
|
||||
_cursor = 0
|
||||
_refresh_labels()
|
||||
show()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rebuild item list from UpgradeManager
|
||||
# ---------------------------------------------------------------------------
|
||||
func _rebuild_items() -> void:
|
||||
_items.clear()
|
||||
|
||||
for path: String in UpgradeManager.acquired_part_paths:
|
||||
if path == "":
|
||||
continue
|
||||
var part := load(path) as BodyPartData
|
||||
if part:
|
||||
_items.append(part)
|
||||
|
||||
# Sort by slot order so the list groups naturally
|
||||
var slot_order := UpgradeManager.SLOTS
|
||||
_items.sort_custom(func(a, b): return slot_order.find(a.slot) < slot_order.find(b.slot))
|
||||
|
||||
# Destroy old labels
|
||||
for lbl: Label in _item_labels:
|
||||
lbl.queue_free()
|
||||
_item_labels.clear()
|
||||
|
||||
# Create one label per item
|
||||
for i in _items.size():
|
||||
var lbl := Label.new()
|
||||
lbl.position = Vector2(0, i * LINE_H)
|
||||
_list_root.add_child(lbl)
|
||||
_item_labels.append(lbl)
|
||||
|
||||
|
||||
func _refresh_labels() -> void:
|
||||
for i in _item_labels.size():
|
||||
var part: BodyPartData = _items[i]
|
||||
var equipped_path: String = UpgradeManager.equipped_parts.get(part.slot, "")
|
||||
var is_equipped := equipped_path == part.resource_path
|
||||
var is_cursor := i == _cursor
|
||||
|
||||
var slot_tag := "[%s]" % part.slot.to_upper()
|
||||
var equip_tag := " ✓" if is_equipped else ""
|
||||
var lbl: Label = _item_labels[i]
|
||||
lbl.text = ("%s %s%s" % [slot_tag, part.display_name, equip_tag]).lstrip("")
|
||||
|
||||
if is_cursor:
|
||||
lbl.add_theme_color_override("font_color", CURSOR_COLOR)
|
||||
elif is_equipped:
|
||||
lbl.add_theme_color_override("font_color", EQUIPPED_COLOR)
|
||||
else:
|
||||
lbl.add_theme_color_override("font_color", NORMAL_COLOR)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Equip the part under the cursor
|
||||
# ---------------------------------------------------------------------------
|
||||
func _equip_selected() -> void:
|
||||
if _items.is_empty():
|
||||
return
|
||||
var part: BodyPartData = _items[_cursor]
|
||||
UpgradeManager.equip_part(part)
|
||||
_refresh_labels()
|
||||
@@ -0,0 +1,195 @@
|
||||
extends CanvasLayer
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Heart geometry — two polygons per slot: background (dark) + fill (red)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Full heart, centered at origin, ~40 wide x 36 tall
|
||||
var HEART_FULL := PackedVector2Array([
|
||||
Vector2( 0, 18), Vector2(-15, 3), Vector2(-20, -6),
|
||||
Vector2(-15, -15), Vector2(-6, -18), Vector2(-2, -14),
|
||||
Vector2( 0, -11),
|
||||
Vector2( 2, -14), Vector2( 6, -18), Vector2(15, -15),
|
||||
Vector2( 20, -6), Vector2(15, 3),
|
||||
])
|
||||
|
||||
# Left half only — used for the half-heart fill
|
||||
var HEART_HALF := PackedVector2Array([
|
||||
Vector2( 0, 18), Vector2(-15, 3), Vector2(-20, -6),
|
||||
Vector2(-15, -15), Vector2(-6, -18), Vector2(-2, -14),
|
||||
Vector2( 0, -11),
|
||||
])
|
||||
|
||||
const FULL_COLOR := Color(0.90, 0.10, 0.10) # bright red
|
||||
const EMPTY_COLOR := Color(0.18, 0.04, 0.04) # dark maroon (background)
|
||||
|
||||
const HEART_SPACING := 52 # px between heart centers
|
||||
const ORIGIN := Vector2(32, 32) # center of first heart (screen px)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Boss bar
|
||||
# ---------------------------------------------------------------------------
|
||||
const BOSS_BAR_W := 400.0
|
||||
const BOSS_BAR_H := 18.0
|
||||
const BOSS_BAR_POS := Vector2(280.0, 490.0) # top-left corner (960×540 viewport)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
# Each entry: { "bg": Polygon2D, "fill": Polygon2D }
|
||||
var _slots: Array = []
|
||||
var _credits_label: Label
|
||||
|
||||
# Boss bar nodes (null when not active)
|
||||
var _boss_bar_root: Node2D = null
|
||||
var _boss_bar_fill: Polygon2D = null
|
||||
var _boss_bar_label: Label = null
|
||||
var _boss_max_hp: float = 1.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Boot
|
||||
# ---------------------------------------------------------------------------
|
||||
func _ready() -> void:
|
||||
EventBus.player_health_changed.connect(_on_health_changed)
|
||||
EventBus.credits_changed.connect(_on_credits_changed)
|
||||
EventBus.boss_health_changed.connect(_on_boss_health_changed)
|
||||
EventBus.boss_died.connect(_on_boss_died)
|
||||
_build_credits_label()
|
||||
# Initialise once the scene is fully set up
|
||||
call_deferred("_init_from_player")
|
||||
|
||||
|
||||
func _init_from_player() -> void:
|
||||
var p = get_tree().get_first_node_in_group("player")
|
||||
if p:
|
||||
_rebuild(p.stats.max_health, p.stats.current_health)
|
||||
else:
|
||||
_rebuild(6.0, 6.0)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build / rebuild all heart slots
|
||||
# ---------------------------------------------------------------------------
|
||||
func _rebuild(max_hp: float, current_hp: float) -> void:
|
||||
for slot in _slots:
|
||||
slot["bg"].queue_free()
|
||||
slot["fill"].queue_free()
|
||||
_slots.clear()
|
||||
|
||||
var num_hearts := int(max_hp / 2.0)
|
||||
|
||||
for i in num_hearts:
|
||||
var center := ORIGIN + Vector2(i * HEART_SPACING, 0)
|
||||
|
||||
# Background — always visible, shows the "empty" container
|
||||
var bg := Polygon2D.new()
|
||||
bg.polygon = HEART_FULL
|
||||
bg.color = EMPTY_COLOR
|
||||
bg.position = center
|
||||
add_child(bg)
|
||||
|
||||
# Fill — overlaid on top; polygon shape changes per fill level
|
||||
var fill := Polygon2D.new()
|
||||
fill.polygon = HEART_FULL
|
||||
fill.color = FULL_COLOR
|
||||
fill.position = center
|
||||
add_child(fill)
|
||||
|
||||
_slots.append({"bg": bg, "fill": fill})
|
||||
|
||||
_update_display(current_hp)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Update fill polygons to reflect current health
|
||||
# ---------------------------------------------------------------------------
|
||||
func _update_display(current_hp: float) -> void:
|
||||
for i in _slots.size():
|
||||
var fill: Polygon2D = _slots[i]["fill"]
|
||||
var hp_in_slot := current_hp - i * 2.0
|
||||
|
||||
if hp_in_slot >= 2.0:
|
||||
fill.polygon = HEART_FULL
|
||||
fill.visible = true
|
||||
elif hp_in_slot >= 1.0:
|
||||
fill.polygon = HEART_HALF
|
||||
fill.visible = true
|
||||
else:
|
||||
fill.visible = false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Credits label
|
||||
# ---------------------------------------------------------------------------
|
||||
func _build_credits_label() -> void:
|
||||
_credits_label = Label.new()
|
||||
_credits_label.position = Vector2(16, 72)
|
||||
_credits_label.text = "SCRAP: %d" % UpgradeManager.hub_credits
|
||||
add_child(_credits_label)
|
||||
|
||||
func _on_credits_changed(new_total: int) -> void:
|
||||
_credits_label.text = "SCRAP: %d" % new_total
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signal handler
|
||||
# ---------------------------------------------------------------------------
|
||||
func _on_health_changed(current_hp: float, max_hp: float) -> void:
|
||||
var num_hearts := int(max_hp / 2.0)
|
||||
if num_hearts != _slots.size():
|
||||
_rebuild(max_hp, current_hp)
|
||||
else:
|
||||
_update_display(current_hp)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Boss health bar
|
||||
# ---------------------------------------------------------------------------
|
||||
func _on_boss_health_changed(current_hp: float, max_hp: float) -> void:
|
||||
if _boss_bar_root == null:
|
||||
_build_boss_bar(max_hp)
|
||||
_boss_max_hp = max_hp
|
||||
_update_boss_bar(current_hp)
|
||||
|
||||
func _on_boss_died() -> void:
|
||||
if _boss_bar_root != null:
|
||||
_boss_bar_root.queue_free()
|
||||
_boss_bar_root = null
|
||||
_boss_bar_fill = null
|
||||
_boss_bar_label = null
|
||||
|
||||
func _build_boss_bar(max_hp: float) -> void:
|
||||
_boss_max_hp = max_hp
|
||||
|
||||
_boss_bar_root = Node2D.new()
|
||||
_boss_bar_root.position = BOSS_BAR_POS
|
||||
add_child(_boss_bar_root)
|
||||
|
||||
# Name label
|
||||
_boss_bar_label = Label.new()
|
||||
_boss_bar_label.text = "THE SUPERVISOR"
|
||||
_boss_bar_label.position = Vector2(0, -22)
|
||||
_boss_bar_root.add_child(_boss_bar_label)
|
||||
|
||||
# Background
|
||||
var bg := Polygon2D.new()
|
||||
bg.polygon = PackedVector2Array([
|
||||
Vector2(0, 0), Vector2(BOSS_BAR_W, 0),
|
||||
Vector2(BOSS_BAR_W, BOSS_BAR_H), Vector2(0, BOSS_BAR_H),
|
||||
])
|
||||
bg.color = Color(0.15, 0.05, 0.05)
|
||||
_boss_bar_root.add_child(bg)
|
||||
|
||||
# Fill
|
||||
_boss_bar_fill = Polygon2D.new()
|
||||
_boss_bar_fill.polygon = PackedVector2Array([
|
||||
Vector2(0, 0), Vector2(BOSS_BAR_W, 0),
|
||||
Vector2(BOSS_BAR_W, BOSS_BAR_H), Vector2(0, BOSS_BAR_H),
|
||||
])
|
||||
_boss_bar_fill.color = Color(0.85, 0.1, 0.1)
|
||||
_boss_bar_root.add_child(_boss_bar_fill)
|
||||
|
||||
func _update_boss_bar(current_hp: float) -> void:
|
||||
if _boss_bar_fill == null:
|
||||
return
|
||||
var pct := clampf(current_hp / _boss_max_hp, 0.0, 1.0)
|
||||
var w := BOSS_BAR_W * pct
|
||||
_boss_bar_fill.polygon = PackedVector2Array([
|
||||
Vector2(0, 0), Vector2(w, 0),
|
||||
Vector2(w, BOSS_BAR_H), Vector2(0, BOSS_BAR_H),
|
||||
])
|
||||
Reference in New Issue
Block a user