Sync working project files - hub layout, boss roster, doc updates

This commit is contained in:
2026-03-05 15:50:37 -05:00
parent 6ce4531b76
commit f1b4820307
47 changed files with 1476 additions and 402 deletions
+141 -8
View File
@@ -9,12 +9,29 @@ extends CharacterBody2D
const PROJECTILE = preload("res://scenes/player/projectile.tscn")
const IFRAME_DURATION := 0.8
const SHIELD_COOLDOWN := 3.0
var _shoot_timer: float = 0.0
var _shoot_timer: float = 0.0
var _iframe_timer: float = 0.0
# Overclock — counts shots fired; every 3rd deals 0 damage
var _overclock_counter: int = 0
# Memory Spike — first projectile per room deals 3x damage
var _memory_spike_available: bool = true
# Shield Projector — active block, absorbs one hit
var _shield_ready: bool = true
var _shield_active: bool = false
var _shield_cd_timer: float = 0.0
# Oil slick — count of slick zones currently overlapping player
var _slick_count: int = 0
func _ready() -> void:
add_to_group("player")
EventBus.room_entered.connect(_on_room_entered)
EventBus.room_cleared.connect(_on_room_cleared)
# ---------------------------------------------------------------------------
# Physics
@@ -23,6 +40,7 @@ func _physics_process(delta: float) -> void:
_handle_movement()
_handle_shooting(delta)
_handle_iframes(delta)
_handle_shield(delta)
move_and_slide()
# ---------------------------------------------------------------------------
@@ -36,7 +54,12 @@ func _handle_movement() -> void:
if Input.is_physical_key_pressed(KEY_D): input_dir.x += 1
if input_dir.length() > 1.0:
input_dir = input_dir.normalized()
velocity = input_dir * stats.speed
if _slick_count > 0:
# Oil slick — momentum-based: velocity slowly drifts toward desired direction
velocity = velocity.lerp(input_dir * stats.speed, 0.08)
else:
velocity = input_dir * stats.speed
if input_dir.x != 0.0:
body.scale.x = sign(input_dir.x)
@@ -62,27 +85,62 @@ func _handle_shooting(delta: float) -> void:
_shoot_timer = 1.0 / stats.fire_rate
func _fire(direction: Vector2) -> void:
# Overclock: every 3rd shot deals 0 damage
var is_zeroed_shot := false
if stats.overclock:
_overclock_counter += 1
if _overclock_counter >= 3:
_overclock_counter = 0
is_zeroed_shot = true
if stats.scatter_count <= 1:
_spawn_projectile(direction)
_spawn_projectile(direction, is_zeroed_shot)
else:
var spread := deg_to_rad(stats.scatter_spread_deg)
var step := spread / float(stats.scatter_count - 1)
var step := spread / float(stats.scatter_count - 1) if stats.scatter_count > 1 else 0.0
var start := direction.angle() - spread / 2.0
for i in stats.scatter_count:
_spawn_projectile(Vector2.from_angle(start + step * i))
_spawn_projectile(Vector2.from_angle(start + step * i), is_zeroed_shot)
func _spawn_projectile(dir: 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.direction = dir
proj.damage = stats.damage
proj.speed = stats.proj_speed
proj.max_range = stats.range_
proj.ricochet = stats.ricochet
proj.explosive = stats.explosive
proj.explosion_radius = stats.explosion_radius
proj.explosion_damage = stats.explosion_damage
if zero_damage:
proj.damage = 0.0
elif stats.memory_spike and _memory_spike_available:
proj.damage = stats.damage * 3.0
_memory_spike_available = false
else:
proj.damage = stats.damage
get_parent().add_child(proj)
# ---------------------------------------------------------------------------
# Shield Projector — Space to activate; absorbs one hit, SHIELD_COOLDOWN
# ---------------------------------------------------------------------------
func _handle_shield(delta: float) -> void:
if not stats.shield_projector:
_shield_active = false
_shield_ready = true
_shield_cd_timer = 0.0
return
if _shield_cd_timer > 0.0:
_shield_cd_timer -= delta
if _shield_cd_timer <= 0.0:
_shield_ready = true
if _shield_ready and Input.is_physical_key_pressed(KEY_SPACE):
_shield_active = true
# ---------------------------------------------------------------------------
# Iframes — flash body during invincibility
# ---------------------------------------------------------------------------
@@ -100,11 +158,86 @@ func _handle_iframes(delta: float) -> void:
func take_damage(amount: float) -> void:
if _iframe_timer > 0.0:
return
stats.current_health -= amount
# Shield Projector — absorb one hit
if _shield_active:
_shield_active = false
_shield_ready = false
_shield_cd_timer = SHIELD_COOLDOWN
return
# Rust Coat — reduce damage (min 1)
var actual := amount
if stats.rust_coat:
actual = maxf(actual - stats.damage_reduction, 1.0)
stats.current_health -= actual
_iframe_timer = IFRAME_DURATION
EventBus.player_health_changed.emit(stats.current_health, stats.max_health)
# Reactive effects on taking damage
if stats.static_discharge:
_trigger_static_discharge()
if stats.coolant_leak:
_spawn_coolant_zone()
if stats.current_health <= 0.0:
_die()
func _die() -> void:
RunManager.end_run(false)
# ---------------------------------------------------------------------------
# Static Discharge — instant AoE damage to enemies in radius
# ---------------------------------------------------------------------------
func _trigger_static_discharge() -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
if not is_instance_valid(enemy):
continue
if global_position.distance_to(enemy.global_position) <= stats.discharge_radius:
if enemy.has_method("take_damage"):
enemy.take_damage(stats.discharge_damage)
# ---------------------------------------------------------------------------
# Coolant Leak — spawn a temporary slow zone at player position (1s)
# ---------------------------------------------------------------------------
func _spawn_coolant_zone() -> void:
var zone := Area2D.new()
zone.collision_layer = 0
zone.collision_mask = 8 # enemy CharacterBody2D layer
var cs := CollisionShape2D.new()
var sh := CircleShape2D.new()
sh.radius = 24.0
cs.shape = sh
zone.add_child(cs)
zone.position = (get_parent() as Node2D).to_local(global_position)
zone.body_entered.connect(func(b: Node) -> void:
if b.has_method("apply_slow"):
b.apply_slow(2.0, 0.4))
get_parent().call_deferred("add_child", zone)
get_tree().create_timer(1.0).timeout.connect(func() -> void:
if is_instance_valid(zone):
zone.queue_free())
# ---------------------------------------------------------------------------
# Oil slick — called by hazard Area2D nodes
# ---------------------------------------------------------------------------
func enter_slick() -> void:
_slick_count += 1
func exit_slick() -> void:
_slick_count = maxi(_slick_count - 1, 0)
# ---------------------------------------------------------------------------
# Room event handlers
# ---------------------------------------------------------------------------
func _on_room_entered(_room_id: String) -> void:
_memory_spike_available = true
_overclock_counter = 0
func _on_room_cleared(_room_id: String) -> void:
if stats.fragmented_map:
EventBus.room_revealed.emit()