Sync working project files - hub layout, boss roster, doc updates
This commit is contained in:
+51
-79
@@ -1,93 +1,95 @@
|
||||
extends CharacterBody2D
|
||||
extends EnemyBase
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ANCHOR — Structural integrity corruption
|
||||
# Pathfinds to nearest wall, locks in. Fires slow homing projectiles.
|
||||
# Does not move once anchored.
|
||||
# Drifts to a random floor point, anchors for a random duration while firing
|
||||
# homing projectiles, then picks a new point. Invincible while drifting.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const ENEMY_PROJECTILE = preload("res://scenes/enemies/enemy_projectile.tscn")
|
||||
|
||||
enum State { SEEKING, ANCHORED }
|
||||
enum State { DRIFTING, ANCHORED }
|
||||
|
||||
var max_health: float = 40.0
|
||||
var current_health: float = 40.0
|
||||
var speed: float = 95.0
|
||||
var contact_damage: float = 0.5
|
||||
var contact_cooldown: float = 0.8
|
||||
|
||||
const SHOT_COOLDOWN := 2.2
|
||||
# Room interior half-extents (960×540 room, 32px walls, 16px gap buffer)
|
||||
const INNER_HALF_X := 432.0
|
||||
const INNER_HALF_Y := 222.0
|
||||
# Margin from walls so it stays on the floor interior
|
||||
const FLOOR_MARGIN := 80.0
|
||||
|
||||
var _player: Node2D = null
|
||||
var _state: State = State.SEEKING
|
||||
var _target_wall: Vector2
|
||||
var _shot_timer: float = 1.0 # initial fire delay
|
||||
var _contact_timer: float = 0.0
|
||||
var _flash_timer: float = 0.0
|
||||
const SHOT_COOLDOWN := 2.2
|
||||
const ANCHOR_TIME_MIN := 3.0
|
||||
const ANCHOR_TIME_MAX := 6.0
|
||||
|
||||
@onready var visual: Node2D = $Visual
|
||||
var _state: State = State.DRIFTING
|
||||
var _target_pos: Vector2
|
||||
var _shot_timer: float = 1.0
|
||||
var _anchor_timer: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("enemies")
|
||||
$ContactArea.body_entered.connect(_on_contact_entered)
|
||||
_compute_wall_target()
|
||||
max_health = 40.0
|
||||
current_health = 40.0
|
||||
speed = 95.0
|
||||
contact_damage = 0.5
|
||||
super._ready()
|
||||
_pick_floor_point()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Find the nearest wall face and set it as the target
|
||||
# Pick a random point within the room interior
|
||||
# ---------------------------------------------------------------------------
|
||||
func _compute_wall_target() -> void:
|
||||
func _pick_floor_point() -> void:
|
||||
var room_center: Vector2 = (get_parent() as Node2D).global_position
|
||||
var lp: Vector2 = global_position - room_center
|
||||
|
||||
var d_left: float = lp.x - (-INNER_HALF_X)
|
||||
var d_right: float = INNER_HALF_X - lp.x
|
||||
var d_up: float = lp.y - (-INNER_HALF_Y)
|
||||
var d_down: float = INNER_HALF_Y - lp.y
|
||||
var md: float = minf(minf(d_left, d_right), minf(d_up, d_down))
|
||||
|
||||
if md == d_left:
|
||||
_target_wall = room_center + Vector2(-INNER_HALF_X, lp.y)
|
||||
elif md == d_right:
|
||||
_target_wall = room_center + Vector2( INNER_HALF_X, lp.y)
|
||||
elif md == d_up:
|
||||
_target_wall = room_center + Vector2(lp.x, -INNER_HALF_Y)
|
||||
else:
|
||||
_target_wall = room_center + Vector2(lp.x, INNER_HALF_Y)
|
||||
var rx: float = randf_range(-INNER_HALF_X + FLOOR_MARGIN, INNER_HALF_X - FLOOR_MARGIN)
|
||||
var ry: float = randf_range(-INNER_HALF_Y + FLOOR_MARGIN, INNER_HALF_Y - FLOOR_MARGIN)
|
||||
_target_pos = room_center + Vector2(rx, ry)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_find_player()
|
||||
_tick_contact(delta)
|
||||
_tick_flash(delta)
|
||||
_tick_slow(delta)
|
||||
|
||||
match _state:
|
||||
State.SEEKING:
|
||||
State.DRIFTING:
|
||||
_do_seek()
|
||||
State.ANCHORED:
|
||||
_do_anchored(delta)
|
||||
|
||||
velocity *= _slow_factor
|
||||
move_and_slide()
|
||||
|
||||
func _find_player() -> void:
|
||||
if _player == null or not is_instance_valid(_player):
|
||||
_player = get_tree().get_first_node_in_group("player")
|
||||
# ---------------------------------------------------------------------------
|
||||
# Invincible while drifting
|
||||
# ---------------------------------------------------------------------------
|
||||
func take_damage(amount: float) -> void:
|
||||
if _state == State.DRIFTING:
|
||||
return
|
||||
super.take_damage(amount)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Move toward the nearest wall, then lock in
|
||||
# Drift toward target floor point, then anchor
|
||||
# ---------------------------------------------------------------------------
|
||||
func _do_seek() -> void:
|
||||
var diff := _target_wall - global_position
|
||||
var diff := _target_pos - global_position
|
||||
if diff.length() < 12.0:
|
||||
velocity = Vector2.ZERO
|
||||
_state = State.ANCHORED
|
||||
velocity = Vector2.ZERO
|
||||
_state = State.ANCHORED
|
||||
_shot_timer = SHOT_COOLDOWN * 0.5
|
||||
_anchor_timer = randf_range(ANCHOR_TIME_MIN, ANCHOR_TIME_MAX)
|
||||
else:
|
||||
velocity = diff.normalized() * speed
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stationary — fire slow homing projectiles at the player
|
||||
# Anchored — fire homing projectiles; leave when anchor_timer expires
|
||||
# ---------------------------------------------------------------------------
|
||||
func _do_anchored(delta: float) -> void:
|
||||
velocity = Vector2.ZERO
|
||||
|
||||
_anchor_timer -= delta
|
||||
if _anchor_timer <= 0.0:
|
||||
_state = State.DRIFTING
|
||||
_pick_floor_point()
|
||||
return
|
||||
|
||||
_shot_timer -= delta
|
||||
if _shot_timer <= 0.0 and _player != null:
|
||||
_fire_at_player()
|
||||
@@ -103,34 +105,4 @@ func _fire_at_player() -> void:
|
||||
proj.max_range = 720.0
|
||||
proj.homing = true
|
||||
proj.homing_target = _player
|
||||
get_parent().call_deferred("add_child", proj)
|
||||
|
||||
func take_damage(amount: float) -> void:
|
||||
current_health -= amount
|
||||
_flash_timer = 0.1
|
||||
if current_health <= 0.0:
|
||||
_die()
|
||||
|
||||
func _die() -> void:
|
||||
_spawn_drop()
|
||||
queue_free()
|
||||
|
||||
func _spawn_drop() -> void:
|
||||
var orb = preload("res://scenes/enemies/currency_orb.tscn").instantiate()
|
||||
orb.position = (get_parent() as Node2D).to_local(global_position)
|
||||
get_parent().call_deferred("add_child", orb)
|
||||
|
||||
func _on_contact_entered(body: Node) -> void:
|
||||
if body.is_in_group("player") and _contact_timer == 0.0:
|
||||
body.take_damage(contact_damage)
|
||||
_contact_timer = contact_cooldown
|
||||
|
||||
func _tick_contact(delta: float) -> void:
|
||||
_contact_timer = maxf(_contact_timer - delta, 0.0)
|
||||
|
||||
func _tick_flash(delta: float) -> void:
|
||||
if _flash_timer > 0.0:
|
||||
_flash_timer -= delta
|
||||
visual.modulate = Color(2.5, 2.5, 2.5)
|
||||
else:
|
||||
visual.modulate = Color(1.0, 1.0, 1.0)
|
||||
get_parent().call_deferred("add_child", proj)
|
||||
@@ -10,8 +10,19 @@ func _draw() -> void:
|
||||
draw_circle(Vector2.ZERO, 5.0, Color(1.0, 0.8, 0.1))
|
||||
draw_arc(Vector2.ZERO, 5.0, 0, TAU, 16, Color(1.0, 1.0, 0.4), 1.0)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
var player: Node2D = get_tree().get_first_node_in_group("player") as Node2D
|
||||
if player == null:
|
||||
return
|
||||
var pstats: Node = player.get_node_or_null("Stats")
|
||||
if pstats == null or not pstats.get("scrap_magnet"):
|
||||
return
|
||||
var dist: float = global_position.distance_to(player.global_position)
|
||||
if dist <= float(pstats.get("magnet_radius")) and dist > 1.0:
|
||||
var dir: Vector2 = (player.global_position - global_position).normalized()
|
||||
global_position += dir * 240.0 * delta
|
||||
|
||||
func _on_body_entered(body: Node) -> void:
|
||||
if body.is_in_group("player"):
|
||||
UpgradeManager.hub_credits += value
|
||||
EventBus.credits_changed.emit(UpgradeManager.hub_credits)
|
||||
queue_free()
|
||||
RunManager.earn_credits(value)
|
||||
queue_free()
|
||||
+11
-51
@@ -1,40 +1,30 @@
|
||||
extends CharacterBody2D
|
||||
extends EnemyBase
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DRIFTER — Locomotion corruption
|
||||
# Moves in slow arcs and wide curves. Never charges straight. Contact only.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var max_health: float = 16.0
|
||||
var current_health: float = 16.0
|
||||
var speed: float = 80.0
|
||||
var contact_damage: float = 1.0
|
||||
var contact_cooldown: float = 0.8
|
||||
|
||||
var _player: Node2D = null
|
||||
var _contact_timer: float = 0.0
|
||||
var _flash_timer: float = 0.0
|
||||
var _wobble_time: float = 0.0
|
||||
var _wobble_phase: float = 0.0 # random start so groups don't sync
|
||||
|
||||
@onready var visual: Node2D = $Visual
|
||||
var _wobble_time: float = 0.0
|
||||
var _wobble_phase: float = 0.0 # random start so groups don't sync
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("enemies")
|
||||
$ContactArea.body_entered.connect(_on_contact_entered)
|
||||
max_health = 24.0
|
||||
current_health = 24.0
|
||||
speed = 100.0
|
||||
contact_damage = 1.0
|
||||
super._ready()
|
||||
_wobble_phase = randf() * TAU
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_find_player()
|
||||
_tick_contact(delta)
|
||||
_tick_flash(delta)
|
||||
_tick_slow(delta)
|
||||
_do_arc_chase(delta)
|
||||
velocity *= _slow_factor
|
||||
move_and_slide()
|
||||
|
||||
func _find_player() -> void:
|
||||
if _player == null or not is_instance_valid(_player):
|
||||
_player = get_tree().get_first_node_in_group("player")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Arcing movement — sinusoidal perpendicular drift applied to toward-player dir
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -46,34 +36,4 @@ func _do_arc_chase(delta: float) -> void:
|
||||
var perp := toward.rotated(PI * 0.5)
|
||||
_wobble_time += delta
|
||||
var wobble := sin(_wobble_time * 1.8 + _wobble_phase) * 0.75
|
||||
velocity = (toward + perp * wobble).normalized() * speed
|
||||
|
||||
func take_damage(amount: float) -> void:
|
||||
current_health -= amount
|
||||
_flash_timer = 0.1
|
||||
if current_health <= 0.0:
|
||||
_die()
|
||||
|
||||
func _die() -> void:
|
||||
_spawn_drop()
|
||||
queue_free()
|
||||
|
||||
func _spawn_drop() -> void:
|
||||
var orb = preload("res://scenes/enemies/currency_orb.tscn").instantiate()
|
||||
orb.position = (get_parent() as Node2D).to_local(global_position)
|
||||
get_parent().call_deferred("add_child", orb)
|
||||
|
||||
func _on_contact_entered(body: Node) -> void:
|
||||
if body.is_in_group("player") and _contact_timer == 0.0:
|
||||
body.take_damage(contact_damage)
|
||||
_contact_timer = contact_cooldown
|
||||
|
||||
func _tick_contact(delta: float) -> void:
|
||||
_contact_timer = maxf(_contact_timer - delta, 0.0)
|
||||
|
||||
func _tick_flash(delta: float) -> void:
|
||||
if _flash_timer > 0.0:
|
||||
_flash_timer -= delta
|
||||
visual.modulate = Color(2.5, 2.5, 2.5)
|
||||
else:
|
||||
visual.modulate = Color(1.0, 1.0, 1.0)
|
||||
velocity = (toward + perp * wobble).normalized() * speed
|
||||
@@ -1,4 +1,5 @@
|
||||
extends CharacterBody2D
|
||||
class_name EnemyBase
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stats — override in derived enemy types
|
||||
@@ -10,11 +11,13 @@ var contact_damage: float = 1.0
|
||||
var contact_cooldown: float = 0.8
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal state
|
||||
# Internal state — shared across all enemies
|
||||
# ---------------------------------------------------------------------------
|
||||
var _contact_timer: float = 0.0
|
||||
var _flash_timer: float = 0.0
|
||||
var _player: Node2D = null
|
||||
var _contact_timer: float = 0.0
|
||||
var _flash_timer: float = 0.0
|
||||
var _slow_factor: float = 1.0
|
||||
var _slow_timer: float = 0.0
|
||||
var _player: Node2D = null
|
||||
|
||||
@onready var visual: Node2D = $Visual
|
||||
|
||||
@@ -22,22 +25,41 @@ func _ready() -> void:
|
||||
add_to_group("enemies")
|
||||
$ContactArea.body_entered.connect(_on_contact_entered)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_contact_timer = maxf(_contact_timer - delta, 0.0)
|
||||
_tick_flash(delta)
|
||||
_chase_player()
|
||||
move_and_slide()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AI — simple chaser
|
||||
# Shared helpers — call from derived _physics_process as needed
|
||||
# ---------------------------------------------------------------------------
|
||||
func _chase_player() -> void:
|
||||
func _find_player() -> void:
|
||||
if _player == null or not is_instance_valid(_player):
|
||||
_player = get_tree().get_first_node_in_group("player")
|
||||
if _player != null:
|
||||
velocity = (_player.global_position - global_position).normalized() * speed
|
||||
|
||||
func _tick_contact(delta: float) -> void:
|
||||
if _contact_timer > 0.0:
|
||||
_contact_timer -= delta
|
||||
if _contact_timer <= 0.0:
|
||||
_contact_timer = 0.0
|
||||
# Re-apply damage if player is still overlapping after cooldown
|
||||
for body in $ContactArea.get_overlapping_bodies():
|
||||
if body.is_in_group("player"):
|
||||
body.take_damage(contact_damage)
|
||||
_contact_timer = contact_cooldown
|
||||
break
|
||||
|
||||
func _tick_flash(delta: float) -> void:
|
||||
if _flash_timer > 0.0:
|
||||
_flash_timer -= delta
|
||||
visual.modulate = Color(2.5, 2.5, 2.5)
|
||||
else:
|
||||
velocity = Vector2.ZERO
|
||||
visual.modulate = Color(1.0, 1.0, 1.0)
|
||||
|
||||
func apply_slow(duration: float, factor: float) -> void:
|
||||
_slow_factor = factor
|
||||
_slow_timer = duration
|
||||
|
||||
func _tick_slow(delta: float) -> void:
|
||||
if _slow_timer > 0.0:
|
||||
_slow_timer -= delta
|
||||
if _slow_timer <= 0.0:
|
||||
_slow_factor = 1.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Damage / death
|
||||
@@ -63,14 +85,4 @@ func _spawn_drop() -> void:
|
||||
func _on_contact_entered(body: Node) -> void:
|
||||
if body.is_in_group("player") and _contact_timer == 0.0:
|
||||
body.take_damage(contact_damage)
|
||||
_contact_timer = contact_cooldown
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hit flash
|
||||
# ---------------------------------------------------------------------------
|
||||
func _tick_flash(delta: float) -> void:
|
||||
if _flash_timer > 0.0:
|
||||
_flash_timer -= delta
|
||||
visual.modulate = Color(2.5, 2.5, 2.5)
|
||||
else:
|
||||
visual.modulate = Color(1.0, 1.0, 1.0)
|
||||
_contact_timer = contact_cooldown
|
||||
+47
-91
@@ -1,87 +1,74 @@
|
||||
extends CharacterBody2D
|
||||
extends EnemyBase
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# REPEATER — Task-loop corruption
|
||||
# Drifts to last known player position, stops, fires a rotating burst loop.
|
||||
# Does not re-track the player — stuck completing a subroutine.
|
||||
# Snaps to the nearest wall on spawn and stays there forever.
|
||||
# Fires a rotating burst loop indefinitely — a stationary turret.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const ENEMY_PROJECTILE = preload("res://scenes/enemies/enemy_projectile.tscn")
|
||||
|
||||
enum State { DRIFTING, FIRING }
|
||||
|
||||
var max_health: float = 24.0
|
||||
var current_health: float = 24.0
|
||||
var speed: float = 60.0
|
||||
var contact_damage: float = 0.5
|
||||
var contact_cooldown: float = 0.8
|
||||
# Room interior half-extents (960×540 room, 32px walls, 16px gap buffer)
|
||||
const INNER_HALF_X := 432.0
|
||||
const INNER_HALF_Y := 222.0
|
||||
|
||||
const BURST_SIZE := 3 # shots per burst
|
||||
const SHOT_INTERVAL := 0.18 # seconds between shots within a burst
|
||||
const BURST_PAUSE := 1.8 # seconds after burst completes before rotating
|
||||
const BURST_PAUSE := 1.8 # seconds after burst before rotating
|
||||
const ROTATE_STEP := 0.3491 # 20° in radians
|
||||
|
||||
var _player: Node2D = null
|
||||
var _state: State = State.DRIFTING
|
||||
var _target_pos: Vector2
|
||||
var _target_set: bool = false
|
||||
var _fire_angle: float = 0.0
|
||||
var _shot_count: int = 0
|
||||
var _shot_timer: float = 0.0
|
||||
var _contact_timer: float = 0.0
|
||||
var _flash_timer: float = 0.0
|
||||
|
||||
@onready var visual: Node2D = $Visual
|
||||
var _fire_angle: float = 0.0
|
||||
var _shot_count: int = 0
|
||||
var _shot_timer: float = 1.0 # initial delay before first burst
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("enemies")
|
||||
$ContactArea.body_entered.connect(_on_contact_entered)
|
||||
_fire_angle = randf() * TAU # random initial burst direction
|
||||
max_health = 30.0
|
||||
current_health = 30.0
|
||||
speed = 0.0
|
||||
contact_damage = 0.5
|
||||
super._ready()
|
||||
_fire_angle = randf() * TAU
|
||||
_snap_to_wall()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Snap to the nearest wall face at spawn
|
||||
# ---------------------------------------------------------------------------
|
||||
func _snap_to_wall() -> void:
|
||||
var room_center: Vector2 = (get_parent() as Node2D).global_position
|
||||
var lp: Vector2 = global_position - room_center
|
||||
|
||||
var d_left: float = lp.x - (-INNER_HALF_X)
|
||||
var d_right: float = INNER_HALF_X - lp.x
|
||||
var d_up: float = lp.y - (-INNER_HALF_Y)
|
||||
var d_down: float = INNER_HALF_Y - lp.y
|
||||
var md: float = minf(minf(d_left, d_right), minf(d_up, d_down))
|
||||
|
||||
var wall_pos: Vector2
|
||||
if md == d_left:
|
||||
wall_pos = room_center + Vector2(-INNER_HALF_X, lp.y)
|
||||
elif md == d_right:
|
||||
wall_pos = room_center + Vector2( INNER_HALF_X, lp.y)
|
||||
elif md == d_up:
|
||||
wall_pos = room_center + Vector2(lp.x, -INNER_HALF_Y)
|
||||
else:
|
||||
wall_pos = room_center + Vector2(lp.x, INNER_HALF_Y)
|
||||
|
||||
global_position = wall_pos
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_find_player()
|
||||
_tick_contact(delta)
|
||||
_tick_flash(delta)
|
||||
match _state:
|
||||
State.DRIFTING:
|
||||
_do_drift()
|
||||
State.FIRING:
|
||||
_do_fire(delta)
|
||||
_tick_slow(delta)
|
||||
velocity = Vector2.ZERO
|
||||
_do_fire(delta)
|
||||
velocity *= _slow_factor
|
||||
move_and_slide()
|
||||
|
||||
func _find_player() -> void:
|
||||
if _player == null or not is_instance_valid(_player):
|
||||
_player = get_tree().get_first_node_in_group("player")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Drift toward last known position, then lock and fire
|
||||
# ---------------------------------------------------------------------------
|
||||
func _do_drift() -> void:
|
||||
if _player == null:
|
||||
velocity = Vector2.ZERO
|
||||
return
|
||||
# Capture target position once
|
||||
if not _target_set:
|
||||
_target_pos = _player.global_position
|
||||
_target_set = true
|
||||
|
||||
var diff := _target_pos - global_position
|
||||
if diff.length() < 12.0:
|
||||
velocity = Vector2.ZERO
|
||||
# Aim first burst toward player
|
||||
if _player != null:
|
||||
_fire_angle = (_player.global_position - global_position).angle()
|
||||
_state = State.FIRING
|
||||
_shot_count = 0
|
||||
_shot_timer = 0.0
|
||||
else:
|
||||
velocity = diff.normalized() * speed
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stationary burst loop — fire 3, pause, rotate 20° CW, repeat
|
||||
# ---------------------------------------------------------------------------
|
||||
func _do_fire(delta: float) -> void:
|
||||
velocity = Vector2.ZERO
|
||||
_shot_timer -= delta
|
||||
if _shot_timer > 0.0:
|
||||
return
|
||||
@@ -91,7 +78,6 @@ func _do_fire(delta: float) -> void:
|
||||
_shot_count += 1
|
||||
_shot_timer = SHOT_INTERVAL
|
||||
else:
|
||||
# Burst done — rotate 20° clockwise and reset
|
||||
_fire_angle += ROTATE_STEP
|
||||
_shot_count = 0
|
||||
_shot_timer = BURST_PAUSE
|
||||
@@ -103,34 +89,4 @@ func _fire_projectile() -> void:
|
||||
proj.speed = 280.0
|
||||
proj.damage = 1.0
|
||||
proj.max_range = 520.0
|
||||
get_parent().call_deferred("add_child", proj)
|
||||
|
||||
func take_damage(amount: float) -> void:
|
||||
current_health -= amount
|
||||
_flash_timer = 0.1
|
||||
if current_health <= 0.0:
|
||||
_die()
|
||||
|
||||
func _die() -> void:
|
||||
_spawn_drop()
|
||||
queue_free()
|
||||
|
||||
func _spawn_drop() -> void:
|
||||
var orb = preload("res://scenes/enemies/currency_orb.tscn").instantiate()
|
||||
orb.position = (get_parent() as Node2D).to_local(global_position)
|
||||
get_parent().call_deferred("add_child", orb)
|
||||
|
||||
func _on_contact_entered(body: Node) -> void:
|
||||
if body.is_in_group("player") and _contact_timer == 0.0:
|
||||
body.take_damage(contact_damage)
|
||||
_contact_timer = contact_cooldown
|
||||
|
||||
func _tick_contact(delta: float) -> void:
|
||||
_contact_timer = maxf(_contact_timer - delta, 0.0)
|
||||
|
||||
func _tick_flash(delta: float) -> void:
|
||||
if _flash_timer > 0.0:
|
||||
_flash_timer -= delta
|
||||
visual.modulate = Color(2.5, 2.5, 2.5)
|
||||
else:
|
||||
visual.modulate = Color(1.0, 1.0, 1.0)
|
||||
get_parent().call_deferred("add_child", proj)
|
||||
@@ -1,4 +1,4 @@
|
||||
extends CharacterBody2D
|
||||
extends EnemyBase
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The Supervisor — Phase 1/2/Climax boss
|
||||
@@ -9,14 +9,6 @@ enum Phase { ONE, TWO, CLIMAX }
|
||||
|
||||
const DRIFTER_SCENE = preload("res://scenes/enemies/drifter.tscn")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stats
|
||||
# ---------------------------------------------------------------------------
|
||||
var max_health: float = 200.0
|
||||
var current_health: float = 200.0
|
||||
var contact_damage: float = 1.0
|
||||
var contact_cooldown: float = 0.8
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Patrol (Phase 1)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -46,18 +38,20 @@ const WIND_UP_DURATION := 0.65 # seconds of warning before beam fires
|
||||
# ---------------------------------------------------------------------------
|
||||
const FREEZE_DURATION := 2.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2 Drifter spawning — one at a time, over a fixed window
|
||||
# ---------------------------------------------------------------------------
|
||||
const DRIFTER_SPAWN_COUNT := 4 # total drifters spawned in phase 2
|
||||
const DRIFTER_SPAWN_INTERVAL := 8.0 # seconds between each spawn
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal state
|
||||
# ---------------------------------------------------------------------------
|
||||
var _phase: Phase = Phase.ONE
|
||||
var _player: Node2D = null
|
||||
|
||||
var _patrol_points: Array[Vector2] = []
|
||||
var _patrol_index: int = 0
|
||||
|
||||
var _contact_timer: float = 0.0
|
||||
var _flash_timer: float = 0.0
|
||||
|
||||
# Beam state
|
||||
var _beam_timer: float = 1.5 # initial delay before first beam check
|
||||
var _beam_active: bool = false
|
||||
@@ -65,21 +59,28 @@ var _beam_node: Node2D = null
|
||||
var _beam_dur_timer: float = 0.0
|
||||
|
||||
# Wind-up state
|
||||
var _winding_up: bool = false
|
||||
var _wind_up_timer: float = 0.0
|
||||
var _winding_up: bool = false
|
||||
var _wind_up_timer: float = 0.0
|
||||
var _wind_up_dir: Vector2 = Vector2.RIGHT # locked direction when wind-up starts
|
||||
|
||||
var _drifter_spawned: bool = false
|
||||
var _freeze_timer: float = 0.0
|
||||
# Phase 2 Drifter spawning
|
||||
var _drifters_spawned: int = 0
|
||||
var _drifter_timer: float = DRIFTER_SPAWN_INTERVAL
|
||||
|
||||
@onready var visual: Node2D = $Visual
|
||||
var _freeze_timer: float = 0.0
|
||||
|
||||
# Facing indicator — Line2D child added to visual at runtime
|
||||
var _facing_indicator: Line2D = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ready
|
||||
# ---------------------------------------------------------------------------
|
||||
func _ready() -> void:
|
||||
add_to_group("enemies")
|
||||
$ContactArea.body_entered.connect(_on_contact_entered)
|
||||
max_health = 200.0
|
||||
current_health = 200.0
|
||||
contact_damage = 1.0
|
||||
contact_cooldown = 0.8
|
||||
super._ready()
|
||||
var c := global_position
|
||||
_patrol_points = [
|
||||
c + Vector2(-PATROL_W, -PATROL_H),
|
||||
@@ -87,6 +88,23 @@ func _ready() -> void:
|
||||
c + Vector2( PATROL_W, PATROL_H),
|
||||
c + Vector2(-PATROL_W, PATROL_H),
|
||||
]
|
||||
_build_facing_indicator()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Facing indicator — arrow line attached to the visual node
|
||||
# ---------------------------------------------------------------------------
|
||||
func _build_facing_indicator() -> void:
|
||||
var arrow := Line2D.new()
|
||||
arrow.width = 3.0
|
||||
arrow.default_color = Color(1.0, 0.9, 0.3, 0.85)
|
||||
arrow.add_point(Vector2(0.0, 0.0))
|
||||
arrow.add_point(Vector2(22.0, 0.0))
|
||||
visual.add_child(arrow)
|
||||
_facing_indicator = arrow
|
||||
|
||||
func _update_facing(dir: Vector2) -> void:
|
||||
if _facing_indicator != null:
|
||||
_facing_indicator.rotation = dir.angle()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Physics loop
|
||||
@@ -100,12 +118,11 @@ func _physics_process(delta: float) -> void:
|
||||
var pct := current_health / max_health
|
||||
if _phase == Phase.ONE and pct <= 0.55:
|
||||
_enter_phase_two()
|
||||
elif _phase == Phase.TWO and pct <= 0.20:
|
||||
elif _phase == Phase.TWO and current_health <= 5.0:
|
||||
_enter_climax()
|
||||
|
||||
match _phase:
|
||||
Phase.ONE:
|
||||
# Stop and lock on during wind-up
|
||||
if _winding_up:
|
||||
velocity = Vector2.ZERO
|
||||
else:
|
||||
@@ -115,25 +132,38 @@ func _physics_process(delta: float) -> void:
|
||||
velocity = Vector2.ZERO
|
||||
else:
|
||||
_do_chase(PATROL_SPEED_P2)
|
||||
_maybe_spawn_drifter()
|
||||
_tick_drifter_spawns(delta)
|
||||
Phase.CLIMAX:
|
||||
_do_climax(delta)
|
||||
|
||||
move_and_slide()
|
||||
# Update facing indicator (hidden during climax)
|
||||
if _phase != Phase.CLIMAX:
|
||||
if _facing_indicator != null:
|
||||
_facing_indicator.visible = true
|
||||
var facing: Vector2
|
||||
if _winding_up:
|
||||
facing = _wind_up_dir
|
||||
elif velocity.length() > 1.0:
|
||||
facing = velocity.normalized()
|
||||
elif _player != null:
|
||||
facing = (_player.global_position - global_position).normalized()
|
||||
else:
|
||||
facing = Vector2.RIGHT
|
||||
_update_facing(facing)
|
||||
else:
|
||||
if _facing_indicator != null:
|
||||
_facing_indicator.visible = false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Player lookup
|
||||
# ---------------------------------------------------------------------------
|
||||
func _find_player() -> void:
|
||||
if _player == null or not is_instance_valid(_player):
|
||||
_player = get_tree().get_first_node_in_group("player")
|
||||
move_and_slide()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase transitions
|
||||
# ---------------------------------------------------------------------------
|
||||
func _enter_phase_two() -> void:
|
||||
_phase = Phase.TWO
|
||||
_winding_up = false
|
||||
_phase = Phase.TWO
|
||||
_winding_up = false
|
||||
_drifters_spawned = 0
|
||||
_drifter_timer = DRIFTER_SPAWN_INTERVAL * 0.5 # first spawn sooner
|
||||
|
||||
func _enter_climax() -> void:
|
||||
_phase = Phase.CLIMAX
|
||||
@@ -168,15 +198,18 @@ func _do_chase(spd: float) -> void:
|
||||
velocity = (_player.global_position - global_position).normalized() * spd
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2 — spawn one Drifter
|
||||
# Phase 2 — spawn Drifters one at a time on a fixed interval
|
||||
# ---------------------------------------------------------------------------
|
||||
func _maybe_spawn_drifter() -> void:
|
||||
if _drifter_spawned:
|
||||
func _tick_drifter_spawns(delta: float) -> void:
|
||||
if _drifters_spawned >= DRIFTER_SPAWN_COUNT:
|
||||
return
|
||||
_drifter_spawned = true
|
||||
var drifter = DRIFTER_SCENE.instantiate()
|
||||
drifter.position = (get_parent() as Node2D).to_local(global_position + Vector2(0, 130))
|
||||
get_parent().call_deferred("add_child", drifter)
|
||||
_drifter_timer -= delta
|
||||
if _drifter_timer <= 0.0:
|
||||
var drifter = DRIFTER_SCENE.instantiate()
|
||||
drifter.position = (get_parent() as Node2D).to_local(global_position + Vector2(0, 130))
|
||||
get_parent().call_deferred("add_child", drifter)
|
||||
_drifters_spawned += 1
|
||||
_drifter_timer = DRIFTER_SPAWN_INTERVAL
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Climax — frozen distress signal, then die
|
||||
@@ -196,7 +229,6 @@ func _tick_beam(delta: float) -> void:
|
||||
if _phase == Phase.CLIMAX:
|
||||
return
|
||||
|
||||
# Active beam — count down duration
|
||||
if _beam_active:
|
||||
_beam_dur_timer -= delta
|
||||
if _beam_dur_timer <= 0.0:
|
||||
@@ -206,7 +238,6 @@ func _tick_beam(delta: float) -> void:
|
||||
_beam_node = null
|
||||
return
|
||||
|
||||
# Wind-up — boss is stopped and flashing; fires when timer expires
|
||||
if _winding_up:
|
||||
_wind_up_timer -= delta
|
||||
if _wind_up_timer <= 0.0:
|
||||
@@ -216,19 +247,16 @@ func _tick_beam(delta: float) -> void:
|
||||
_beam_timer = cd
|
||||
return
|
||||
|
||||
# Cooldown between beams
|
||||
if _beam_timer > 0.0:
|
||||
_beam_timer -= delta
|
||||
return
|
||||
|
||||
# Check sensor cone — start wind-up if player is in range
|
||||
if _player_in_sensor_cone():
|
||||
_winding_up = true
|
||||
_wind_up_timer = WIND_UP_DURATION
|
||||
# Lock the fire direction to current heading at the moment of detection
|
||||
_wind_up_dir = velocity.normalized() if velocity.length() > 1.0 else Vector2.RIGHT
|
||||
else:
|
||||
_beam_timer = 0.3 # recheck soon
|
||||
_beam_timer = 0.3
|
||||
|
||||
func _player_in_sensor_cone() -> bool:
|
||||
if _player == null:
|
||||
@@ -243,7 +271,7 @@ func _fire_beam() -> void:
|
||||
_beam_active = true
|
||||
_beam_dur_timer = BEAM_DURATION
|
||||
|
||||
var forward := _wind_up_dir # use direction locked at wind-up start
|
||||
var forward := _wind_up_dir
|
||||
var beam_len := BEAM_RANGE
|
||||
var beam_w := 30.0
|
||||
var offset := 24.0
|
||||
@@ -280,7 +308,7 @@ func _fire_beam() -> void:
|
||||
_beam_node = beam
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Damage / death
|
||||
# Damage / death — override base to emit boss signals; no drop (room handles it)
|
||||
# ---------------------------------------------------------------------------
|
||||
func take_damage(amount: float) -> void:
|
||||
current_health -= amount
|
||||
@@ -293,47 +321,27 @@ func _die() -> void:
|
||||
EventBus.boss_died.emit()
|
||||
if is_instance_valid(_beam_node):
|
||||
_beam_node.queue_free()
|
||||
_spawn_drop()
|
||||
queue_free()
|
||||
|
||||
func _spawn_drop() -> void:
|
||||
var orb = preload("res://scenes/enemies/currency_orb.tscn").instantiate()
|
||||
orb.position = (get_parent() as Node2D).to_local(global_position)
|
||||
get_parent().call_deferred("add_child", orb)
|
||||
queue_free() # no drop — room.gd spawns the guaranteed body part
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Contact damage
|
||||
# ---------------------------------------------------------------------------
|
||||
func _on_contact_entered(body: Node) -> void:
|
||||
if body.is_in_group("player") and _contact_timer == 0.0:
|
||||
body.take_damage(contact_damage)
|
||||
_contact_timer = contact_cooldown
|
||||
|
||||
func _tick_contact(delta: float) -> void:
|
||||
_contact_timer = maxf(_contact_timer - delta, 0.0)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Visual — flash on hit, wind-up warning pulse, phase tints, climax strobe
|
||||
# Visual — overrides base _tick_flash with phase-aware tints
|
||||
# ---------------------------------------------------------------------------
|
||||
func _tick_flash(delta: float) -> void:
|
||||
if _phase == Phase.CLIMAX:
|
||||
return # climax handles its own visual in _do_climax
|
||||
|
||||
# Wind-up: rapid yellow-white warning pulse — overrides everything else
|
||||
if _winding_up:
|
||||
var pulse := fmod(_wind_up_timer, 0.18) < 0.09
|
||||
var base := Color(1.0, 0.65, 0.2) if _phase == Phase.TWO else Color(1.0, 1.0, 1.0)
|
||||
visual.modulate = Color(2.4, 2.1, 0.3) if pulse else base
|
||||
return
|
||||
|
||||
# Hit flash
|
||||
if _flash_timer > 0.0:
|
||||
_flash_timer -= delta
|
||||
visual.modulate = Color(2.5, 2.5, 2.5)
|
||||
return
|
||||
|
||||
# Resting tint per phase
|
||||
if _phase == Phase.TWO:
|
||||
visual.modulate = Color(1.0, 0.65, 0.2)
|
||||
else:
|
||||
visual.modulate = Color(1.0, 1.0, 1.0)
|
||||
visual.modulate = Color(1.0, 1.0, 1.0)
|
||||
Reference in New Issue
Block a user