diff --git a/assets/sprites/tileset_v1.png b/assets/sprites/tileset_v1.png new file mode 100644 index 0000000..01938cc Binary files /dev/null and b/assets/sprites/tileset_v1.png differ 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)