Initial commit: Transcience MVP
ci / verify (push) Successful in 1m57s

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:
Adyrem
2026-09-03 16:03:57 +02:00
commit 651c4ad94a
385 changed files with 28725 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
extends GutTest
## The escape button is the player's safety valve, and it is also the one input
## with a real consequence attached, so its timing is server-owned end to end.
var world: SimWorld
const PEER := 3
func before_each() -> void:
world = SimWorld.new(1)
world.add_player(PEER, "tester")
world.players[PEER].iframes = 0
func _hold_escape(ticks: int) -> void:
for _i in ticks:
var frames: Array[InputFrame] = [
InputFrame.make(world.tick + 1, Vector2.ZERO, 0.0, InputFrame.BTN_ESCAPE)]
world.queue_input(PEER, frames)
world.step()
func _events_of(type: int) -> Array:
return world.events.filter(func(e: Dictionary) -> bool: return int(e["t"]) == type)
func test_escape_takes_the_full_channel_time() -> void:
_hold_escape(SimConfig.ESCAPE_CHANNEL_TICKS - 1)
assert_eq(_events_of(SimEvent.Type.ESCAPE_COMPLETED).size(), 0,
"the escape must not complete early")
_hold_escape(1)
assert_eq(_events_of(SimEvent.Type.ESCAPE_COMPLETED).size(), 1)
func test_releasing_the_button_cancels_the_channel() -> void:
_hold_escape(60)
assert_gt(world.players[PEER].escape_ticks, 0)
var frames: Array[InputFrame] = [InputFrame.make(world.tick + 1, Vector2.ZERO, 0.0, 0)]
world.queue_input(PEER, frames)
world.step()
assert_eq(world.players[PEER].escape_ticks, 0)
assert_eq(_events_of(SimEvent.Type.ESCAPE_CANCELLED).size(), 1)
func test_taking_damage_cancels_the_channel() -> void:
_hold_escape(60)
world.pool.spawn(world.players[PEER].pos, Vector2.ZERO, 6.0, 60, 10,
SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB)
world.step()
assert_eq(world.players[PEER].escape_ticks, 0,
"escaping under fire has to be a real risk, not a free exit")
assert_gt(_events_of(SimEvent.Type.ESCAPE_CANCELLED).size(), 0)
func test_a_cancelled_channel_restarts_from_zero() -> void:
_hold_escape(120)
var frames: Array[InputFrame] = [InputFrame.make(world.tick + 1, Vector2.ZERO, 0.0, 0)]
world.queue_input(PEER, frames)
world.step()
_hold_escape(SimConfig.ESCAPE_CHANNEL_TICKS - 1)
assert_eq(_events_of(SimEvent.Type.ESCAPE_COMPLETED).size(), 0,
"progress must not carry over from an interrupted channel")
func test_escape_completion_never_reaches_the_client_directly() -> void:
_hold_escape(SimConfig.ESCAPE_CHANNEL_TICKS)
var packet := NetCodec.decode_events(NetCodec.encode_events(world.tick, world.events))
for ev: Dictionary in packet["events"]:
assert_ne(int(ev["t"]), SimEvent.Type.ESCAPE_COMPLETED,
"the transfer is the server's decision; clients only see the outcome")