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,133 @@
|
||||
extends Node2D
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
const HUB_ROOM_SCENE = preload("res://scenes/hub/rooms/hub_room_base.tscn")
|
||||
const ROOM_W := 960.0
|
||||
const ROOM_H := 540.0
|
||||
const TRANSITION_TIME := 0.25
|
||||
const ENTRY_INSET := 60.0
|
||||
|
||||
# Fixed hub layout — 6 rooms arranged as:
|
||||
#
|
||||
# [Control (0,-1)]
|
||||
# |
|
||||
# [Cafe(-1,0)]-[Staging(0,0)]-[Armory(1,0)]
|
||||
# | |
|
||||
# [Shop(0,1)]----[Trophy(1,1)]
|
||||
#
|
||||
# HubRoomType: STAGING=0, ARMORY=1, CAFETERIA=2, SHOP=3, CONTROL_ROOM=4, TROPHY_ROOM=5
|
||||
const LAYOUT := [
|
||||
{"type": 0, "grid": Vector2i( 0, 0)}, # STAGING
|
||||
{"type": 4, "grid": Vector2i( 0, -1)}, # CONTROL_ROOM
|
||||
{"type": 2, "grid": Vector2i(-1, 0)}, # CAFETERIA
|
||||
{"type": 1, "grid": Vector2i( 1, 0)}, # ARMORY
|
||||
{"type": 3, "grid": Vector2i( 0, 1)}, # SHOP
|
||||
{"type": 5, "grid": Vector2i( 1, 1)}, # TROPHY_ROOM
|
||||
]
|
||||
|
||||
const DIRS := {
|
||||
"up": Vector2i( 0, -1),
|
||||
"down": Vector2i( 0, 1),
|
||||
"left": Vector2i(-1, 0),
|
||||
"right": Vector2i( 1, 0),
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Node refs
|
||||
# ---------------------------------------------------------------------------
|
||||
@onready var rooms_container: Node2D = $Rooms
|
||||
@onready var camera: Camera2D = $Camera2D
|
||||
@onready var player: Node2D = $Player
|
||||
@onready var armory_ui: CanvasLayer = $ArmoryUI
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
var _rooms: Dictionary = {} # Vector2i -> HubRoom
|
||||
var _current_grid: Vector2i
|
||||
var _transitioning: bool = false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Boot
|
||||
# ---------------------------------------------------------------------------
|
||||
func _ready() -> void:
|
||||
_build_hub()
|
||||
|
||||
|
||||
func _build_hub() -> void:
|
||||
var connections := _compute_connections()
|
||||
|
||||
for entry in LAYOUT:
|
||||
var grid_pos: Vector2i = entry["grid"]
|
||||
var room_type: int = entry["type"]
|
||||
var conn: Dictionary = connections.get(grid_pos, {})
|
||||
|
||||
var room_node: HubRoom = HUB_ROOM_SCENE.instantiate()
|
||||
room_node.position = _grid_to_world(grid_pos)
|
||||
rooms_container.add_child(room_node)
|
||||
room_node.setup(room_type, conn, self)
|
||||
_rooms[grid_pos] = room_node
|
||||
|
||||
# Start in Staging Area; player spawns below center so the portal is visible
|
||||
var staging_world := _grid_to_world(Vector2i(0, 0))
|
||||
camera.position = staging_world
|
||||
player.global_position = staging_world + Vector2(0, 80)
|
||||
_current_grid = Vector2i(0, 0)
|
||||
|
||||
|
||||
func _compute_connections() -> Dictionary:
|
||||
var grid := {}
|
||||
for entry in LAYOUT:
|
||||
grid[entry["grid"]] = true
|
||||
|
||||
var result := {}
|
||||
for entry in LAYOUT:
|
||||
var pos: Vector2i = entry["grid"]
|
||||
var conn := {}
|
||||
for dir_name in DIRS:
|
||||
var nb: Vector2i = pos + DIRS[dir_name]
|
||||
if grid.has(nb):
|
||||
conn[dir_name] = nb
|
||||
result[pos] = conn
|
||||
|
||||
return result
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transitions (identical pattern to floor.gd)
|
||||
# ---------------------------------------------------------------------------
|
||||
func transition_to(target_grid: Vector2i, from_direction: String) -> void:
|
||||
if _transitioning:
|
||||
return
|
||||
_transitioning = true
|
||||
_current_grid = target_grid
|
||||
|
||||
var new_world := _grid_to_world(target_grid)
|
||||
player.global_position = new_world + _entry_offset(from_direction)
|
||||
|
||||
var tween := create_tween()
|
||||
tween.tween_property(camera, "position", new_world, TRANSITION_TIME)
|
||||
tween.tween_callback(func(): _transitioning = false)
|
||||
|
||||
|
||||
func _entry_offset(from_direction: String) -> Vector2:
|
||||
var i := ENTRY_INSET
|
||||
match from_direction:
|
||||
"up": return Vector2(0, ROOM_H / 2.0 - i)
|
||||
"down": return Vector2(0, -ROOM_H / 2.0 + i)
|
||||
"left": return Vector2( ROOM_W / 2.0 - i, 0)
|
||||
"right": return Vector2(-ROOM_W / 2.0 + i, 0)
|
||||
return Vector2.ZERO
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
func _grid_to_world(grid_pos: Vector2i) -> Vector2:
|
||||
return Vector2(grid_pos.x * ROOM_W, grid_pos.y * ROOM_H)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Armory
|
||||
# ---------------------------------------------------------------------------
|
||||
func open_armory() -> void:
|
||||
armory_ui.open()
|
||||
@@ -0,0 +1,258 @@
|
||||
extends Node2D
|
||||
class_name HubRoom
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Room types
|
||||
# ---------------------------------------------------------------------------
|
||||
enum HubRoomType {
|
||||
STAGING = 0,
|
||||
ARMORY = 1,
|
||||
CAFETERIA = 2,
|
||||
SHOP = 3,
|
||||
CONTROL_ROOM = 4,
|
||||
TROPHY_ROOM = 5,
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants — wall geometry identical to run rooms
|
||||
# ---------------------------------------------------------------------------
|
||||
const ROOM_W := 960.0
|
||||
const ROOM_H := 540.0
|
||||
const WALL_T := 32.0
|
||||
const DOOR_W := 80.0
|
||||
const DOOR_H := 80.0
|
||||
|
||||
const WALL_COLOR := Color(0.18, 0.18, 0.25)
|
||||
|
||||
const ROOM_NAMES := [
|
||||
"STAGING AREA", # 0
|
||||
"ARMORY", # 1
|
||||
"CAFETERIA", # 2
|
||||
"SHOP", # 3
|
||||
"CONTROL ROOM", # 4
|
||||
"TROPHY ROOM", # 5
|
||||
]
|
||||
|
||||
const FLOOR_COLORS := [
|
||||
Color(0.14, 0.14, 0.20), # STAGING — neutral dark
|
||||
Color(0.14, 0.12, 0.10), # ARMORY — warm metal
|
||||
Color(0.10, 0.14, 0.10), # CAFETERIA — green tint
|
||||
Color(0.14, 0.10, 0.14), # SHOP — purple tint
|
||||
Color(0.08, 0.12, 0.18), # CONTROL_ROOM — blue tint
|
||||
Color(0.16, 0.14, 0.08), # TROPHY_ROOM — warm gold tint
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
var _type: int = 0 # HubRoomType value
|
||||
var _connections: Dictionary = {} # "up"/"down"/"left"/"right" -> Vector2i
|
||||
var _hub: Node = null
|
||||
var _doors: Dictionary = {} # direction -> Area2D
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Node refs
|
||||
# ---------------------------------------------------------------------------
|
||||
@onready var floor_poly: Polygon2D = $Floor
|
||||
@onready var walls_node: Node2D = $Walls
|
||||
@onready var door_triggers: Node2D = $DoorTriggers
|
||||
@onready var contents: Node2D = $Contents
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
func setup(room_type: int, connections: Dictionary, hub_ref: Node) -> void:
|
||||
_type = room_type
|
||||
_connections = connections
|
||||
_hub = hub_ref
|
||||
|
||||
floor_poly.color = FLOOR_COLORS[_type]
|
||||
_build_walls()
|
||||
_build_door_triggers()
|
||||
_build_room_label()
|
||||
|
||||
if _type == HubRoomType.STAGING:
|
||||
_build_run_portal()
|
||||
elif _type == HubRoomType.ARMORY:
|
||||
_build_armory_workbench()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wall generation (same logic as room.gd)
|
||||
# ---------------------------------------------------------------------------
|
||||
func _build_walls() -> void:
|
||||
var hw := ROOM_W / 2.0
|
||||
var hh := ROOM_H / 2.0
|
||||
var hdw := DOOR_W / 2.0
|
||||
var hdh := DOOR_H / 2.0
|
||||
|
||||
var has_up := _connections.has("up")
|
||||
var has_down := _connections.has("down")
|
||||
var has_left := _connections.has("left")
|
||||
var has_right := _connections.has("right")
|
||||
|
||||
if has_up:
|
||||
_make_wall(Rect2(-hw, -hh, hw - hdw, WALL_T))
|
||||
_make_wall(Rect2( hdw, -hh, hw - hdw, WALL_T))
|
||||
else:
|
||||
_make_wall(Rect2(-hw, -hh, ROOM_W, WALL_T))
|
||||
|
||||
if has_down:
|
||||
_make_wall(Rect2(-hw, hh - WALL_T, hw - hdw, WALL_T))
|
||||
_make_wall(Rect2( hdw, hh - WALL_T, hw - hdw, WALL_T))
|
||||
else:
|
||||
_make_wall(Rect2(-hw, hh - WALL_T, ROOM_W, WALL_T))
|
||||
|
||||
var inner_top := -hh + WALL_T
|
||||
var inner_bot := hh - WALL_T
|
||||
var top_seg_h := -hdh - inner_top
|
||||
var bot_seg_h := inner_bot - hdh
|
||||
var full_side := inner_bot - inner_top
|
||||
|
||||
if has_left:
|
||||
_make_wall(Rect2(-hw, inner_top, WALL_T, top_seg_h))
|
||||
_make_wall(Rect2(-hw, hdh, WALL_T, bot_seg_h))
|
||||
else:
|
||||
_make_wall(Rect2(-hw, inner_top, WALL_T, full_side))
|
||||
|
||||
if has_right:
|
||||
_make_wall(Rect2(hw - WALL_T, inner_top, WALL_T, top_seg_h))
|
||||
_make_wall(Rect2(hw - WALL_T, hdh, WALL_T, bot_seg_h))
|
||||
else:
|
||||
_make_wall(Rect2(hw - WALL_T, inner_top, WALL_T, full_side))
|
||||
|
||||
|
||||
func _make_wall(rect: Rect2) -> void:
|
||||
var body := StaticBody2D.new()
|
||||
body.collision_layer = 1
|
||||
body.collision_mask = 0
|
||||
body.position = rect.get_center()
|
||||
|
||||
var cs := CollisionShape2D.new()
|
||||
var rs := RectangleShape2D.new()
|
||||
rs.size = rect.size
|
||||
cs.shape = rs
|
||||
body.add_child(cs)
|
||||
|
||||
var poly := Polygon2D.new()
|
||||
var hx := rect.size.x / 2.0
|
||||
var hy := rect.size.y / 2.0
|
||||
poly.polygon = PackedVector2Array([
|
||||
Vector2(-hx, -hy), Vector2(hx, -hy),
|
||||
Vector2( hx, hy), Vector2(-hx, hy),
|
||||
])
|
||||
poly.color = WALL_COLOR
|
||||
body.add_child(poly)
|
||||
|
||||
walls_node.add_child(body)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Door triggers
|
||||
# ---------------------------------------------------------------------------
|
||||
func _build_door_triggers() -> void:
|
||||
var hw := ROOM_W / 2.0
|
||||
var hh := ROOM_H / 2.0
|
||||
|
||||
var configs := {
|
||||
"up": {"pos": Vector2(0, -hh + WALL_T * 0.5), "size": Vector2(DOOR_W * 0.9, WALL_T * 1.5)},
|
||||
"down": {"pos": Vector2(0, hh - WALL_T * 0.5), "size": Vector2(DOOR_W * 0.9, WALL_T * 1.5)},
|
||||
"left": {"pos": Vector2(-hw + WALL_T * 0.5, 0), "size": Vector2(WALL_T * 1.5, DOOR_H * 0.9)},
|
||||
"right": {"pos": Vector2( hw - WALL_T * 0.5, 0), "size": Vector2(WALL_T * 1.5, DOOR_H * 0.9)},
|
||||
}
|
||||
|
||||
for dir in _connections.keys():
|
||||
var cfg = configs[dir]
|
||||
var area := Area2D.new()
|
||||
area.collision_layer = 0
|
||||
area.collision_mask = 2
|
||||
area.name = "Door_" + dir
|
||||
|
||||
var cs := CollisionShape2D.new()
|
||||
var rs := RectangleShape2D.new()
|
||||
rs.size = cfg["size"]
|
||||
cs.shape = rs
|
||||
area.add_child(cs)
|
||||
area.position = cfg["pos"]
|
||||
|
||||
area.body_entered.connect(_on_door_entered.bind(dir))
|
||||
door_triggers.add_child(area)
|
||||
_doors[dir] = area
|
||||
|
||||
|
||||
func _on_door_entered(body: Node, direction: String) -> void:
|
||||
if body.is_in_group("player"):
|
||||
_hub.call_deferred("transition_to", _connections[direction], direction)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Room label
|
||||
# ---------------------------------------------------------------------------
|
||||
func _build_room_label() -> void:
|
||||
var lbl := Label.new()
|
||||
lbl.text = ROOM_NAMES[_type]
|
||||
lbl.position = Vector2(-60, -235)
|
||||
contents.add_child(lbl)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Armory workbench
|
||||
# ---------------------------------------------------------------------------
|
||||
func _build_armory_workbench() -> void:
|
||||
# Visual — blue-grey bench
|
||||
var bench := Polygon2D.new()
|
||||
bench.polygon = PackedVector2Array([-50, -20, 50, -20, 50, 20, -50, 20])
|
||||
bench.color = Color(0.30, 0.40, 0.60)
|
||||
bench.position = Vector2(0, -100)
|
||||
contents.add_child(bench)
|
||||
|
||||
var lbl := Label.new()
|
||||
lbl.text = "UPGRADE"
|
||||
lbl.position = Vector2(-30, -140)
|
||||
contents.add_child(lbl)
|
||||
|
||||
var area := Area2D.new()
|
||||
area.collision_layer = 0
|
||||
area.collision_mask = 2
|
||||
|
||||
var cs := CollisionShape2D.new()
|
||||
var rs := RectangleShape2D.new()
|
||||
rs.size = Vector2(100, 40)
|
||||
cs.shape = rs
|
||||
area.add_child(cs)
|
||||
area.position = Vector2(0, -100)
|
||||
|
||||
area.body_entered.connect(func(b: Node) -> void:
|
||||
if b.is_in_group("player"):
|
||||
_hub.call_deferred("open_armory"))
|
||||
contents.add_child(area)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Staging Area run portal
|
||||
# ---------------------------------------------------------------------------
|
||||
func _build_run_portal() -> void:
|
||||
# Visual — bright green rectangle
|
||||
var poly := Polygon2D.new()
|
||||
poly.polygon = PackedVector2Array([-40, -30, 40, -30, 40, 30, -40, 30])
|
||||
poly.color = Color(0.2, 0.9, 0.4)
|
||||
poly.position = Vector2(0, -150)
|
||||
contents.add_child(poly)
|
||||
|
||||
# Label above portal
|
||||
var lbl := Label.new()
|
||||
lbl.text = "START RUN"
|
||||
lbl.position = Vector2(-36, -195)
|
||||
contents.add_child(lbl)
|
||||
|
||||
# Trigger zone
|
||||
var area := Area2D.new()
|
||||
area.collision_layer = 0
|
||||
area.collision_mask = 2
|
||||
|
||||
var cs := CollisionShape2D.new()
|
||||
var rs := RectangleShape2D.new()
|
||||
rs.size = Vector2(80, 60)
|
||||
cs.shape = rs
|
||||
area.add_child(cs)
|
||||
area.position = Vector2(0, -150)
|
||||
|
||||
area.body_entered.connect(func(b: Node) -> void:
|
||||
if b.is_in_group("player"):
|
||||
GameManager.call_deferred("start_run"))
|
||||
contents.add_child(area)
|
||||
Reference in New Issue
Block a user