Initial commit — Station 0 vertical slice in progress

Includes full design documentation (ARCHITECTURE, DESIGN_NOTES, CONTENT_SPEC, PRODUCTION_NOTES)
and active Godot 4 / GDScript codebase. Core systems built: player, floor generation,
room system, enemy base, body part upgrades, run manager, save system, HUD, EventBus.
This commit is contained in:
Station 0 Dev
2026-03-04 12:33:05 -05:00
commit 342175f4c3
66 changed files with 4683 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
extends CharacterBody2D
# ---------------------------------------------------------------------------
# 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.
# ---------------------------------------------------------------------------
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
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 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
func _ready() -> void:
add_to_group("enemies")
$ContactArea.body_entered.connect(_on_contact_entered)
_fire_angle = randf() * TAU # random initial burst direction
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)
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
if _shot_count < BURST_SIZE:
_fire_projectile()
_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
func _fire_projectile() -> void:
var proj = ENEMY_PROJECTILE.instantiate()
proj.position = (get_parent() as Node2D).to_local(global_position)
proj.direction = Vector2.from_angle(_fire_angle)
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)