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:
@@ -0,0 +1,110 @@
|
||||
extends CharacterBody2D
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Node refs
|
||||
# ---------------------------------------------------------------------------
|
||||
@onready var stats: Node = $Stats
|
||||
@onready var body: Node2D = $Body
|
||||
@onready var shoot_origin: Marker2D = $ShootOrigin
|
||||
|
||||
const PROJECTILE = preload("res://scenes/player/projectile.tscn")
|
||||
const IFRAME_DURATION := 0.8
|
||||
|
||||
var _shoot_timer: float = 0.0
|
||||
var _iframe_timer: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("player")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Physics
|
||||
# ---------------------------------------------------------------------------
|
||||
func _physics_process(delta: float) -> void:
|
||||
_handle_movement()
|
||||
_handle_shooting(delta)
|
||||
_handle_iframes(delta)
|
||||
move_and_slide()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Movement — WASD, 8-directional (direct key checks, no input map needed)
|
||||
# ---------------------------------------------------------------------------
|
||||
func _handle_movement() -> void:
|
||||
var input_dir := Vector2.ZERO
|
||||
if Input.is_physical_key_pressed(KEY_W): input_dir.y -= 1
|
||||
if Input.is_physical_key_pressed(KEY_S): input_dir.y += 1
|
||||
if Input.is_physical_key_pressed(KEY_A): input_dir.x -= 1
|
||||
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 input_dir.x != 0.0:
|
||||
body.scale.x = sign(input_dir.x)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shooting — Arrow keys via built-in ui_ actions (always available in Godot)
|
||||
# ---------------------------------------------------------------------------
|
||||
func _handle_shooting(delta: float) -> void:
|
||||
_shoot_timer = maxf(_shoot_timer - delta, 0.0)
|
||||
|
||||
var shoot_dir := Vector2.ZERO
|
||||
if Input.is_action_pressed("ui_up"):
|
||||
shoot_dir = Vector2.UP
|
||||
elif Input.is_action_pressed("ui_down"):
|
||||
shoot_dir = Vector2.DOWN
|
||||
elif Input.is_action_pressed("ui_left"):
|
||||
shoot_dir = Vector2.LEFT
|
||||
elif Input.is_action_pressed("ui_right"):
|
||||
shoot_dir = Vector2.RIGHT
|
||||
|
||||
if shoot_dir != Vector2.ZERO and _shoot_timer == 0.0:
|
||||
_fire(shoot_dir)
|
||||
_shoot_timer = 1.0 / stats.fire_rate
|
||||
|
||||
func _fire(direction: Vector2) -> void:
|
||||
if stats.scatter_count <= 1:
|
||||
_spawn_projectile(direction)
|
||||
else:
|
||||
var spread := deg_to_rad(stats.scatter_spread_deg)
|
||||
var step := spread / float(stats.scatter_count - 1)
|
||||
var start := direction.angle() - spread / 2.0
|
||||
for i in stats.scatter_count:
|
||||
_spawn_projectile(Vector2.from_angle(start + step * i))
|
||||
|
||||
func _spawn_projectile(dir: Vector2) -> void:
|
||||
var proj: Area2D = PROJECTILE.instantiate()
|
||||
proj.global_position = shoot_origin.global_position
|
||||
proj.direction = dir
|
||||
proj.damage = stats.damage
|
||||
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
|
||||
get_parent().add_child(proj)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Iframes — flash body during invincibility
|
||||
# ---------------------------------------------------------------------------
|
||||
func _handle_iframes(delta: float) -> void:
|
||||
if _iframe_timer <= 0.0:
|
||||
body.modulate = Color(1, 1, 1, 1)
|
||||
return
|
||||
_iframe_timer -= delta
|
||||
# Visible flash every 0.1s
|
||||
body.modulate = Color(1, 1, 1, 0.25) if fmod(_iframe_timer, 0.2) < 0.1 else Color(1, 1, 1, 1)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Damage / death
|
||||
# ---------------------------------------------------------------------------
|
||||
func take_damage(amount: float) -> void:
|
||||
if _iframe_timer > 0.0:
|
||||
return
|
||||
stats.current_health -= amount
|
||||
_iframe_timer = IFRAME_DURATION
|
||||
EventBus.player_health_changed.emit(stats.current_health, stats.max_health)
|
||||
if stats.current_health <= 0.0:
|
||||
_die()
|
||||
|
||||
func _die() -> void:
|
||||
RunManager.end_run(false)
|
||||
Reference in New Issue
Block a user