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:
@@ -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")
|
||||
Reference in New Issue
Block a user