Initial commit: Transcience MVP
Top-down twin-stick bullet-hell, Godot 4.7, server-authoritative dedicated server with client-side prediction. Clients send input only; the server resolves every hit for both players and enemies (no PvP). - SimWorld: whole simulation as plain RefCounted objects (no nodes, no physics server), ~0.24ms/tick at peak load -- runs headless for free and drives 78 tests in under a second - BulletPool: struct-of-arrays bullet storage, replicated as spawn/despawn events rather than per-tick state - Emitter framework (Ring/AimedSpread/WallGap/ArcSweep) shared by trash enemies and bosses -- a new boss is data in src/content/content.gd, no simulation changes - The Warden of the Fold: stationary 4-phase boss built entirely on that format - Lobby hub with a portal into on-demand dungeon instances; one process hosts the hub plus every concurrent dungeon - Emergency escape: 3s server-owned channel, cancelled by damage - tools/check.sh, test.sh (GUT), smoke.sh (real server + bot clients over ENet), bench.gd; git hooks wired to the same scripts - docs/ARCHITECTURE.md, NETCODE.md, WORKFLOW.md, ROADMAP.md
This commit is contained in:
+114
@@ -0,0 +1,114 @@
|
||||
extends CanvasLayer
|
||||
## Heads-up display. Built in code rather than as a scene because every element
|
||||
## is data-driven -- there is no layout here a designer would want to drag.
|
||||
|
||||
const MARGIN := 24.0
|
||||
const BAR_W := 260.0
|
||||
const BAR_H := 16.0
|
||||
|
||||
var _canvas: Control
|
||||
var _status: Label
|
||||
var _hint: Label
|
||||
var _hit_flash: float = 0.0
|
||||
var client: ClientRuntime = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
layer = 10
|
||||
_canvas = Control.new()
|
||||
_canvas.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_canvas.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_canvas.draw.connect(_draw_hud)
|
||||
add_child(_canvas)
|
||||
|
||||
_status = _make_label(Vector2(MARGIN, MARGIN))
|
||||
_hint = _make_label(Vector2(MARGIN, 0.0))
|
||||
_hint.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
|
||||
_hint.position = Vector2(MARGIN, -56.0)
|
||||
|
||||
|
||||
func _make_label(pos: Vector2) -> Label:
|
||||
var l := Label.new()
|
||||
l.position = pos
|
||||
l.add_theme_font_size_override("font_size", 14)
|
||||
l.add_theme_color_override("font_color", Color(0.8, 0.85, 0.95))
|
||||
_canvas.add_child(l)
|
||||
return l
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
client = Net.client
|
||||
_hit_flash = maxf(_hit_flash - delta * 2.5, 0.0)
|
||||
_status.text = _status_text()
|
||||
_hint.text = _hint_text()
|
||||
_canvas.queue_redraw()
|
||||
|
||||
|
||||
func _status_text() -> String:
|
||||
if client == null:
|
||||
return "connecting..."
|
||||
var where := "LOBBY" if client.instance_kind == Protocol.InstanceKind.LOBBY else "DUNGEON"
|
||||
return "%s instance %d hp %d/%d %d fps" % [
|
||||
where, client.instance_id, client.my_hp, SimConfig.PLAYER_MAX_HP,
|
||||
Engine.get_frames_per_second()]
|
||||
|
||||
|
||||
func _hint_text() -> String:
|
||||
if client == null:
|
||||
return ""
|
||||
if client.instance_kind == Protocol.InstanceKind.LOBBY:
|
||||
return "WASD move mouse aim LMB fire E on the ring to enter a dungeon"
|
||||
return "WASD move mouse aim LMB fire hold F to escape to the lobby"
|
||||
|
||||
|
||||
func _draw_hud() -> void:
|
||||
if client == null:
|
||||
return
|
||||
var origin := Vector2(MARGIN, MARGIN + 28.0)
|
||||
_bar(origin, float(client.my_hp) / float(SimConfig.PLAYER_MAX_HP),
|
||||
Color(0.35, 0.9, 0.6), Color(0.1, 0.15, 0.18))
|
||||
|
||||
if client.my_escaping:
|
||||
_bar(origin + Vector2(0.0, BAR_H + 8.0), client.my_escape,
|
||||
Color(0.5, 0.85, 1.0), Color(0.1, 0.15, 0.2))
|
||||
_canvas.draw_string(ThemeDB.fallback_font,
|
||||
origin + Vector2(BAR_W + 12.0, BAR_H + 8.0 + BAR_H),
|
||||
"ESCAPING", HORIZONTAL_ALIGNMENT_LEFT, -1, 14, Color(0.5, 0.85, 1.0))
|
||||
|
||||
_draw_boss_bar()
|
||||
|
||||
if not client.my_alive:
|
||||
var size := _canvas.size
|
||||
_canvas.draw_string(ThemeDB.fallback_font, size * 0.5 - Vector2(70.0, 0.0),
|
||||
"DOWN -- respawning", HORIZONTAL_ALIGNMENT_LEFT, -1, 22, Color(1.0, 0.4, 0.4))
|
||||
|
||||
if _hit_flash > 0.0:
|
||||
_canvas.draw_rect(Rect2(Vector2.ZERO, _canvas.size),
|
||||
Color(1.0, 0.2, 0.25, 0.18 * _hit_flash))
|
||||
|
||||
|
||||
func _draw_boss_bar() -> void:
|
||||
var b := client.boss_state()
|
||||
if b.is_empty() or client.boss_def == null:
|
||||
return
|
||||
var w := 560.0
|
||||
var pos := Vector2((_canvas.size.x - w) * 0.5, MARGIN)
|
||||
var frac := clampf(float(b["hp"]) / float(client.boss_def.max_hp), 0.0, 1.0)
|
||||
_canvas.draw_rect(Rect2(pos, Vector2(w, BAR_H)), Color(0.12, 0.08, 0.1))
|
||||
_canvas.draw_rect(Rect2(pos, Vector2(w * frac, BAR_H)), Color(0.95, 0.35, 0.45))
|
||||
var phase_index := int(b["phase"])
|
||||
var phase_name := ""
|
||||
if phase_index < client.boss_def.phases.size():
|
||||
phase_name = client.boss_def.phases[phase_index].name
|
||||
_canvas.draw_string(ThemeDB.fallback_font, pos + Vector2(0.0, -6.0),
|
||||
"%s -- %s" % [client.boss_def.display_name, phase_name],
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 14, Color(1.0, 0.75, 0.8))
|
||||
|
||||
|
||||
func _bar(pos: Vector2, frac: float, fill: Color, back: Color) -> void:
|
||||
_canvas.draw_rect(Rect2(pos, Vector2(BAR_W, BAR_H)), back)
|
||||
_canvas.draw_rect(Rect2(pos, Vector2(BAR_W * clampf(frac, 0.0, 1.0), BAR_H)), fill)
|
||||
|
||||
|
||||
func flash_hit() -> void:
|
||||
_hit_flash = 1.0
|
||||
Reference in New Issue
Block a user