From 75403e7c1db81c80a6534bb5c9c4c2f33ff67762 Mon Sep 17 00:00:00 2001 From: Mike McGhen Date: Wed, 11 Mar 2026 12:23:53 -0400 Subject: [PATCH] Tileset, Audio Changes, and Portal changes --- assets/sprites/tileset_v1.png | Bin 0 -> 10069 bytes scripts/enemies/enemy_base.gd | 3 + scripts/player/player.gd | 2 +- scripts/rooms/room.gd | 71 +++++----- scripts/rooms/station_door.gd | 251 ++++++++++++++++++++++++++++++++++ 5 files changed, 293 insertions(+), 34 deletions(-) create mode 100644 assets/sprites/tileset_v1.png create mode 100644 scripts/rooms/station_door.gd diff --git a/assets/sprites/tileset_v1.png b/assets/sprites/tileset_v1.png new file mode 100644 index 0000000000000000000000000000000000000000..01938cccfa166274e86ee1cce5509072a5c5cc02 GIT binary patch literal 10069 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 f?EjBHc;7XyPir{0XO literal 0 HcmV?d00001 diff --git a/scripts/enemies/enemy_base.gd b/scripts/enemies/enemy_base.gd index 35a59a0..b371685 100644 --- a/scripts/enemies/enemy_base.gd +++ b/scripts/enemies/enemy_base.gd @@ -64,6 +64,9 @@ func apply_slow(duration: float, factor: float) -> void: # Stack slows by adding new effect - factors multiply together _slow_effects.append({"factor": factor, "timer": duration}) +func clear_slows() -> void: + _slow_effects.clear() + func _tick_slow(delta: float) -> void: # Tick down all slow effects and remove expired ones var i := _slow_effects.size() - 1 diff --git a/scripts/player/player.gd b/scripts/player/player.gd index 8cc2353..74be5d2 100644 --- a/scripts/player/player.gd +++ b/scripts/player/player.gd @@ -114,7 +114,7 @@ func _fire(direction: Vector2) -> void: func _spawn_projectile(dir: Vector2, zero_damage: bool = false) -> void: var proj: Area2D = PROJECTILE.instantiate() - proj.global_position = shoot_origin.global_position + proj.global_position = global_position # Spawn at player center to avoid wall clipping proj.direction = dir proj.speed = stats.proj_speed proj.max_range = stats.range_ diff --git a/scripts/rooms/room.gd b/scripts/rooms/room.gd index bb0c7cf..2b0feb8 100644 --- a/scripts/rooms/room.gd +++ b/scripts/rooms/room.gd @@ -25,6 +25,7 @@ const HIVEMIND_SCENE = preload("res://scenes/enemies/hivemind.tscn") const BODY_PART_PICKUP = preload("res://scenes/upgrades/body_part_pickup.tscn") const RUN_ITEM_PICKUP = preload("res://scenes/items/run_item_pickup.tscn") const SHOP_PEDESTAL_SCR = preload("res://scripts/items/shop_item_pedestal.gd") +const STATION_DOOR_SCR = preload("res://scripts/rooms/station_door.gd") const ALL_PART_PATHS: Array[String] = [ "res://data/body_parts/head_wide_angle_lens.tres", @@ -56,6 +57,7 @@ var _floor: Node = null # reference to floor.gd node var _enemy_count: int = 0 var _doors: Dictionary = {} # direction -> Area2D trigger var _physical_doors: Dictionary = {} # direction -> RoomDoor +var _floor_exit_door: StationDoor = null # boss room exit door # --------------------------------------------------------------------------- # Signal tracking for explicit cleanup (safer than relying on auto-cleanup) @@ -104,6 +106,9 @@ func activate() -> void: RoomData.RoomType.SHOP: _spawn_shop() data.cleared = true + RoomData.RoomType.START: + _spawn_entry_door() + data.cleared = true _: data.cleared = true @@ -339,39 +344,39 @@ func _on_enemy_died() -> void: func _spawn_floor_exit() -> void: var is_final_floor := RunManager.current_floor >= 3 - # Visual — bright teal portal offset from body part drop (which is at center) - var poly := Polygon2D.new() - poly.polygon = PackedVector2Array([-35, -50, 35, -50, 35, 50, -35, 50]) - poly.color = Color(0.2, 0.85, 0.9) - poly.position = Vector2(120, 0) - contents.add_child(poly) + # Station door for floor transition - positioned to the right of body part drop + _floor_exit_door = StationDoor.new() + _floor_exit_door.position = Vector2(150, 0) - var lbl := Label.new() - lbl.text = "EXTRACT" if is_final_floor else "NEXT FLOOR" - lbl.position = Vector2(85, -75) - contents.add_child(lbl) + if is_final_floor: + _floor_exit_door.setup_evacuation_exit() + else: + # Next sector: floor 1 -> SECTOR 2, floor 2 -> SECTOR 3 + _floor_exit_door.setup_sector_exit(RunManager.current_floor + 1) - var area := Area2D.new() - area.collision_layer = 0 - area.collision_mask = 2 + _floor_exit_door.door_entered.connect(_on_floor_exit_entered) + contents.add_child(_floor_exit_door) - var cs := CollisionShape2D.new() - var rs := RectangleShape2D.new() - rs.size = Vector2(70, 100) - cs.shape = rs - area.add_child(cs) - area.position = Vector2(120, 0) + # Open the door now that boss is dead + _floor_exit_door.open() - area.body_entered.connect(func(body: Node) -> void: - if body.is_in_group("player"): - RunManager.player_health_carry = body.stats.current_health - if RunManager.current_floor >= 3: - RunManager.end_run(true) - else: - RunManager.advance_floor() - get_tree().call_deferred("change_scene_to_file", "res://scenes/run/floor.tscn") - ) - contents.add_child(area) + +func _on_floor_exit_entered(body: Node) -> void: + RunManager.player_health_carry = body.stats.current_health + if RunManager.current_floor >= 3: + RunManager.end_run(true) + else: + RunManager.advance_floor() + get_tree().call_deferred("change_scene_to_file", "res://scenes/run/floor.tscn") + + +func _spawn_entry_door() -> void: + # Decorative open door behind spawn point - shows "you entered from here" + # Positioned at bottom of room (player spawns in center, door behind them) + var entry_door := StationDoor.new() + entry_door.position = Vector2(0, ROOM_H / 2.0 - 80) # near bottom wall + entry_door.setup_entry_door() + contents.add_child(entry_door) func _spawn_body_part_drop() -> void: @@ -424,13 +429,13 @@ func _make_oil_slick(pos: Vector2) -> void: cs.shape = sh slick.add_child(cs) - # Visual — flattened dark ellipse (puddle) + # Visual — circle matching collision shape var poly := Polygon2D.new() var pts := PackedVector2Array() var segs := 14 for i in segs: var a := i * TAU / segs - pts.append(Vector2(cos(a) * radius, sin(a) * radius * 0.55)) + pts.append(Vector2(cos(a) * radius, sin(a) * radius)) poly.polygon = pts poly.color = Color(0.04, 0.07, 0.14, 0.78) slick.add_child(poly) @@ -443,8 +448,8 @@ func _make_oil_slick(pos: Vector2) -> void: slick.body_exited.connect(func(b: Node) -> void: if b.is_in_group("player") and b.has_method("exit_slick"): b.exit_slick() - elif b.is_in_group("enemies") and b.has_method("apply_slow"): - b.apply_slow(0.0, 1.0)) # clear slow immediately + elif b.is_in_group("enemies") and b.has_method("clear_slows"): + b.clear_slows()) contents.add_child(slick) diff --git a/scripts/rooms/station_door.gd b/scripts/rooms/station_door.gd new file mode 100644 index 0000000..d049ff3 --- /dev/null +++ b/scripts/rooms/station_door.gd @@ -0,0 +1,251 @@ +extends Node2D +class_name StationDoor + +# Station door for floor transitions - bulkhead/airlock style visuals +# Can be decorative (entry doors) or functional (exit doors with triggers) + +signal door_entered(body: Node) + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +const DOOR_WIDTH := 100.0 +const DOOR_HEIGHT := 120.0 +const FRAME_THICKNESS := 16.0 +const PANEL_GAP := 4.0 # gap between door panels when open + +# Colors +const FRAME_COLOR := Color(0.25, 0.25, 0.32) # dark metal frame +const PANEL_COLOR := Color(0.35, 0.32, 0.28) # door panel (closed) +const PANEL_OPEN_COLOR := Color(0.2, 0.2, 0.25) # recessed panel (open) +const LIGHT_OFF := Color(0.3, 0.15, 0.15) # indicator off +const LIGHT_LOCKED := Color(0.9, 0.2, 0.2) # red locked +const LIGHT_UNLOCKED := Color(0.2, 0.9, 0.3) # green unlocked +const EXIT_LIGHT := Color(0.2, 0.95, 0.4) # bright green exit + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- +enum DoorType { ENTRY, SECTOR_EXIT, EVACUATION_EXIT } + +var door_type: DoorType = DoorType.ENTRY +var sector_label: String = "" # "SECTOR 2", "SECTOR 3", or "EXIT" +var is_functional: bool = false # if true, has trigger area + +# --------------------------------------------------------------------------- +# State +# --------------------------------------------------------------------------- +var _is_open: bool = false +var _left_panel: Polygon2D = null +var _right_panel: Polygon2D = null +var _indicator_light: Polygon2D = null +var _label: Label = null +var _trigger_area: Area2D = null + +# --------------------------------------------------------------------------- +# Setup +# --------------------------------------------------------------------------- +func setup_entry_door() -> void: + # Decorative open door - shows "you came from here" + door_type = DoorType.ENTRY + is_functional = false + _build_frame() + _build_panels() + _build_indicator() + open() # entry doors are always open + +func setup_sector_exit(sector_num: int) -> void: + # Sector transition door - "SECTOR 2" or "SECTOR 3" + door_type = DoorType.SECTOR_EXIT + sector_label = "SECTOR %d" % sector_num + is_functional = true + _build_frame() + _build_panels() + _build_indicator() + _build_label() + _build_trigger() + close() # starts closed until boss dies + +func setup_evacuation_exit() -> void: + # Final exit after Hivemind - green EXIT lighting + door_type = DoorType.EVACUATION_EXIT + sector_label = "EXIT" + is_functional = true + _build_frame() + _build_panels() + _build_indicator() + _build_label() + _build_trigger() + close() # starts closed until boss dies + +# --------------------------------------------------------------------------- +# Build components +# --------------------------------------------------------------------------- +func _build_frame() -> void: + # Outer frame - bulkhead style + var frame := Polygon2D.new() + var hw := DOOR_WIDTH / 2.0 + FRAME_THICKNESS + var hh := DOOR_HEIGHT / 2.0 + FRAME_THICKNESS + var inner_hw := DOOR_WIDTH / 2.0 + var inner_hh := DOOR_HEIGHT / 2.0 + + # Outer rectangle with inner cutout (using polygon) + # Top frame + var top := Polygon2D.new() + top.polygon = PackedVector2Array([ + Vector2(-hw, -hh), Vector2(hw, -hh), + Vector2(hw, -inner_hh), Vector2(-hw, -inner_hh) + ]) + top.color = FRAME_COLOR + add_child(top) + + # Bottom frame + var bottom := Polygon2D.new() + bottom.polygon = PackedVector2Array([ + Vector2(-hw, inner_hh), Vector2(hw, inner_hh), + Vector2(hw, hh), Vector2(-hw, hh) + ]) + bottom.color = FRAME_COLOR + add_child(bottom) + + # Left frame + var left := Polygon2D.new() + left.polygon = PackedVector2Array([ + Vector2(-hw, -inner_hh), Vector2(-inner_hw, -inner_hh), + Vector2(-inner_hw, inner_hh), Vector2(-hw, inner_hh) + ]) + left.color = FRAME_COLOR + add_child(left) + + # Right frame + var right := Polygon2D.new() + right.polygon = PackedVector2Array([ + Vector2(inner_hw, -inner_hh), Vector2(hw, -inner_hh), + Vector2(hw, inner_hh), Vector2(inner_hw, inner_hh) + ]) + right.color = FRAME_COLOR + add_child(right) + +func _build_panels() -> void: + # Two sliding door panels + var panel_w := DOOR_WIDTH / 2.0 - PANEL_GAP + var panel_h := DOOR_HEIGHT + var hh := panel_h / 2.0 + + _left_panel = Polygon2D.new() + _left_panel.polygon = PackedVector2Array([ + Vector2(-panel_w, -hh), Vector2(0, -hh), + Vector2(0, hh), Vector2(-panel_w, hh) + ]) + _left_panel.color = PANEL_COLOR + add_child(_left_panel) + + _right_panel = Polygon2D.new() + _right_panel.polygon = PackedVector2Array([ + Vector2(0, -hh), Vector2(panel_w, -hh), + Vector2(panel_w, hh), Vector2(0, hh) + ]) + _right_panel.color = PANEL_COLOR + add_child(_right_panel) + +func _build_indicator() -> void: + # Small indicator light above the door + _indicator_light = Polygon2D.new() + var light_size := 8.0 + _indicator_light.polygon = PackedVector2Array([ + Vector2(-light_size, -light_size / 2.0), + Vector2(light_size, -light_size / 2.0), + Vector2(light_size, light_size / 2.0), + Vector2(-light_size, light_size / 2.0) + ]) + _indicator_light.position = Vector2(0, -DOOR_HEIGHT / 2.0 - FRAME_THICKNESS - 6) + _indicator_light.color = LIGHT_OFF + add_child(_indicator_light) + +func _build_label() -> void: + _label = Label.new() + _label.text = sector_label + _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _label.position = Vector2(-40, -DOOR_HEIGHT / 2.0 - FRAME_THICKNESS - 30) + + # Style the label + if door_type == DoorType.EVACUATION_EXIT: + _label.add_theme_color_override("font_color", EXIT_LIGHT) + else: + _label.add_theme_color_override("font_color", Color(0.8, 0.8, 0.9)) + + add_child(_label) + +func _build_trigger() -> void: + _trigger_area = Area2D.new() + _trigger_area.collision_layer = 0 + _trigger_area.collision_mask = 2 # player layer + _trigger_area.monitoring = false # disabled until door opens + + var cs := CollisionShape2D.new() + var rs := RectangleShape2D.new() + rs.size = Vector2(DOOR_WIDTH - 20, DOOR_HEIGHT - 20) + cs.shape = rs + _trigger_area.add_child(cs) + + _trigger_area.body_entered.connect(_on_body_entered) + add_child(_trigger_area) + +# --------------------------------------------------------------------------- +# Open / Close +# --------------------------------------------------------------------------- +func open() -> void: + if _is_open: + return + _is_open = true + + # Slide panels outward + var slide_dist := DOOR_WIDTH / 2.0 + PANEL_GAP + if _left_panel: + _left_panel.position.x = -slide_dist + _left_panel.color = PANEL_OPEN_COLOR + if _right_panel: + _right_panel.position.x = slide_dist + _right_panel.color = PANEL_OPEN_COLOR + + # Update indicator + if _indicator_light: + if door_type == DoorType.EVACUATION_EXIT: + _indicator_light.color = EXIT_LIGHT + else: + _indicator_light.color = LIGHT_UNLOCKED + + # Enable trigger + if _trigger_area: + _trigger_area.monitoring = true + +func close() -> void: + if not _is_open and _left_panel != null: + return + _is_open = false + + # Center panels + if _left_panel: + _left_panel.position.x = 0 + _left_panel.color = PANEL_COLOR + if _right_panel: + _right_panel.position.x = 0 + _right_panel.color = PANEL_COLOR + + # Update indicator + if _indicator_light: + _indicator_light.color = LIGHT_LOCKED + + # Disable trigger + if _trigger_area: + _trigger_area.monitoring = false + +func is_open() -> bool: + return _is_open + +# --------------------------------------------------------------------------- +# Trigger callback +# --------------------------------------------------------------------------- +func _on_body_entered(body: Node) -> void: + if body.is_in_group("player"): + door_entered.emit(body)