Sync working project files - hub layout, boss roster, doc updates
This commit is contained in:
+141
-8
@@ -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()
|
||||
Reference in New Issue
Block a user