diff --git a/CONTENT_SPEC.md b/CONTENT_SPEC.md index 0dbc54f..e05b5e8 100644 --- a/CONTENT_SPEC.md +++ b/CONTENT_SPEC.md @@ -43,7 +43,7 @@ All enemies are corrupted Adaptive Maintenance Units — same chassis class as t A containment unit whose crowd-control protocol has turned inward — it is now attempting to contain the entire room, including the player. Slow and methodical. The room itself becomes the threat. -**Room**: Large. No environmental hazards on entry. Preceded by a charging station (full HP restore before entry). +**Room**: Large. No environmental hazards on entry. ### Movement Patrols the room perimeter in a slow, continuous circuit. Never crosses the center. Fully predictable path — the danger comes from what it leaves behind, not from the boss itself. @@ -64,7 +64,7 @@ Drops a stationary proximity mine at its current patrol position each cycle. Min A communications hub unit whose broadcast function corrupted into something aggressive. Erratic, unpredictable positioning. Forces the player to stay mobile and read the room constantly. -**Room**: Large. No environmental hazards on entry. Preceded by a charging station (full HP restore before entry). +**Room**: Large. No environmental hazards on entry. ### Movement Anchors near room center but teleports to a random position every 8–10 seconds. No warning before teleport — position resets without notice. Punishes clustering near the boss. @@ -85,7 +85,7 @@ Periodically broadcasts a signal that summons 2–3 Drifters. Not full waves — What the corruption becomes when it fully realizes itself. Individual units lose their original purpose and merge into a single machine that was never meant to exist. -**Room**: Large. Largest boss room in the run. Preceded by a charging station (full HP restore before entry). +**Room**: Large. Largest boss room in the run. ### Phase 1 — Assembly No central target yet. Corrupted units flood the room in waves — Drifters, Repeaters, Anchors. As each one dies, it is pulled toward the center and physically joins the assembling structure (visual indicator builds up). Player must clear everything they've learned across floors 1 and 2 simultaneously, under pressure. The Hivemind core cannot be damaged during this phase. @@ -205,7 +205,7 @@ One item on a pedestal. No enemies. Safe. 20% chance to also contain a lore term Unmanned. No NPC keeper. Sells 2 items + 1 consumable at fixed scrap prices. No haggling. Purely transactional — all NPC personality lives in the hub. **Boss room** -One boss per floor. Each preceded by a charging station room (no enemies, full HP restore). Boss rooms are the largest rooms in the run. +One boss per floor. Boss rooms are the largest rooms in the run. No boss health bar — player reads boss state through visual phase transitions and the climax (blinking/dying) phase. Keeps tension high and encourages reading the fight, not the UI. | Floor | Boss | |---|---| @@ -237,7 +237,7 @@ Both hazard types included in VS. |---|---|---| | Floor 1 | 4–5 rooms + boss | Learn the rhythm. One item. Maybe find the shop. First Repeater feels like a puzzle. Warden fight — room fills with mines, learn to manage space. | | Floor 2 | 5–6 rooms + boss | Mixed threats. Find a body part. Build takes shape. One tense near-death moment. Relay fight — erratic, keeps moving, adds split focus. | -| Floor 3 | 2–3 rooms + boss | Room composition tightens. Anchors in tighter spaces. Charging station. Hivemind — three phases, everything converges. Victory. | +| Floor 3 | 2–3 rooms + boss | Room composition tightens. Anchors in tighter spaces. Hivemind — three phases, everything converges. Victory. | **Target run time**: 20–30 minutes. diff --git a/scenes/enemies/hivemind.tscn b/scenes/enemies/hivemind.tscn index 05136d0..cdf613e 100644 --- a/scenes/enemies/hivemind.tscn +++ b/scenes/enemies/hivemind.tscn @@ -10,7 +10,7 @@ radius = 40.0 [node name="Hivemind" type="CharacterBody2D"] collision_layer = 8 -collision_mask = 1 +collision_mask = 3 script = ExtResource("1") [node name="Collision" type="CollisionShape2D" parent="."] diff --git a/scenes/player/player.tscn b/scenes/player/player.tscn index 7057196..ad69967 100644 --- a/scenes/player/player.tscn +++ b/scenes/player/player.tscn @@ -8,7 +8,7 @@ radius = 10.0 [node name="Player" type="CharacterBody2D"] collision_layer = 2 -collision_mask = 1 +collision_mask = 9 script = ExtResource("1") [node name="Collision" type="CollisionShape2D" parent="."] diff --git a/scripts/enemies/hivemind.gd b/scripts/enemies/hivemind.gd index 5dfb433..b86af6b 100644 --- a/scripts/enemies/hivemind.gd +++ b/scripts/enemies/hivemind.gd @@ -138,13 +138,13 @@ func _ready() -> void: _core_visual.visible = false # hidden during assembly _core_visual.scale = Vector2(_core_scale, _core_scale) - # Disable and scale down collision during assembly + # Scale down collision during assembly (contact area disabled, but hitbox active for projectiles) if _contact_area: _contact_area.monitoring = false _contact_area.monitorable = false _contact_area.scale = Vector2(_core_scale, _core_scale) if _collision_shape: - _collision_shape.disabled = true + # Keep hitbox enabled but scaled - boss is invulnerable during assembly via take_damage() _collision_shape.scale = Vector2(_core_scale, _core_scale) @@ -310,13 +310,12 @@ func _enter_active() -> void: _core_visual.visible = true _core_visual.scale = Vector2(1.0, 1.0) - # Enable and scale up collision + # Enable contact area and scale up collision if _contact_area: _contact_area.monitoring = true _contact_area.monitorable = true _contact_area.scale = Vector2(1.0, 1.0) if _collision_shape: - _collision_shape.disabled = false _collision_shape.scale = Vector2(1.0, 1.0) _teleport_timer = 2.0 diff --git a/scripts/ui/hud.gd b/scripts/ui/hud.gd index cd62421..c886cac 100644 --- a/scripts/ui/hud.gd +++ b/scripts/ui/hud.gd @@ -27,11 +27,11 @@ const HEART_SPACING := 52 # px between heart centers const ORIGIN := Vector2(32, 32) # center of first heart (screen px) # --------------------------------------------------------------------------- -# Boss bar +# DEPRECATED — Boss bar (commented out) # --------------------------------------------------------------------------- -const BOSS_BAR_W := 400.0 -const BOSS_BAR_H := 18.0 -const BOSS_BAR_POS := Vector2(280.0, 510.0) # bottom center (960×540 viewport) +#const BOSS_BAR_W := 400.0 +#const BOSS_BAR_H := 18.0 +#const BOSS_BAR_POS := Vector2(760.0, 1040.0) # bottom center (1920×1080 viewport) # --------------------------------------------------------------------------- # State @@ -40,11 +40,11 @@ const BOSS_BAR_POS := Vector2(280.0, 510.0) # bottom center (960×540 viewport 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 +# DEPRECATED — Boss bar nodes (commented out) +#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 @@ -52,8 +52,9 @@ var _boss_max_hp: float = 1.0 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) + # DEPRECATED — Boss bar signal connections (commented out) + #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") @@ -139,67 +140,67 @@ func _on_health_changed(current_hp: float, max_hp: float) -> void: _update_display(current_hp) # --------------------------------------------------------------------------- -# Boss health bar +# DEPRECATED — Boss health bar (commented out) # --------------------------------------------------------------------------- -var _boss_name: String = "" - -func _on_boss_health_changed(current_hp: float, max_hp: float, boss_name: String) -> void: - if _boss_bar_root == null: - _build_boss_bar(max_hp, boss_name) - elif _boss_name != boss_name: - # Different boss, update label - _boss_name = boss_name - if _boss_bar_label != null: - _boss_bar_label.text = boss_name - _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 - _boss_name = "" - -func _build_boss_bar(max_hp: float, boss_name: String) -> void: - _boss_max_hp = max_hp - _boss_name = boss_name - - _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 = boss_name - _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), - ]) +#var _boss_name: String = "" +# +#func _on_boss_health_changed(current_hp: float, max_hp: float, boss_name: String) -> void: +# if _boss_bar_root == null: +# _build_boss_bar(max_hp, boss_name) +# elif _boss_name != boss_name: +# # Different boss, update label +# _boss_name = boss_name +# if _boss_bar_label != null: +# _boss_bar_label.text = boss_name +# _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 +# _boss_name = "" +# +#func _build_boss_bar(max_hp: float, boss_name: String) -> void: +# _boss_max_hp = max_hp +# _boss_name = boss_name +# +# _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 = boss_name +# _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), +# ])