DEVELOPER'S JOURNAL

NOTE 02 · ONE EXPEDITION

Online combat pitfalls — server authority, the card pause and the week in Tokyo

Why everything is decided on the server, how “you stop, the world doesn't” got settled, the warlord's four moves, weather that only changes the light, and the absurd failures of restarting a server in Tokyo.

These are my own notes. They mention failures, hesitation and things that are not finished. They describe the present — not a final form, and not a release commitment of any kind.

Preface

An expedition leaves through the north gate and comes back through the north gate, and in between four people have to see the same fight. That sentence is the first rule of the whole combat system and the most expensive one: damage, enemies, hits, upgrades, death, settlement — all decided by the server; the client only displays. Your own movement may run ahead; everything else waits for the server's word.

In practice it looks like this: the authoritative simulation runs on integer XZ coordinates, height is looked up from a locked heightfield; floats can never agree bit-for-bit between the TypeScript and GDScript runtimes and the state hashes have to match, so — integers. Protocol at 30 Hz, snapshots at 15 Hz, the client predicts only its own movement and facing. The lazy option was considered — everyone simulates locally, reconcile at the end — but then "the same fight" becomes four roughly similar fights, and who dropped, who reconnected, who saw the warlord raise its arm first all turn into arguments nobody can settle. Four people on one screen means "who decides" cannot be ambiguous from day one.

You stop, the world doesn't

Levelling is simple: enemies drop experience, level two through eight, three cards per level, take one. The card screen is titled CHOOSE A MUTATION rather than "upgrade", because what grows in one run does not carry into the next. Technique Upgrades are the small ones; Legendary Upgrades at four and eight lay three cards out to consider.

"Consider" is the problem. In single-player the world pauses while you pick; with four people, whose world pauses? The rule that stuck: the person choosing is locked — cannot move, attack, or be hurt — while everyone else keeps fighting and the clock keeps running; only when every player who can still act is choosing does the clock stop. It favours nobody, and it is a little cruel — a companion may be getting surrounded during your few seconds with the cards. So the minor upgrades were later made non-blocking, a corner prompt saying press 1 to 3, pick while fighting; only a Legendary Upgrade actually unrolls the scroll.

Four moves behind the gate

At the end of the road, on the far side of the ruined gate, stands the Evil Minotaur. On entry it swaps whatever is left on the field for exactly four goblins, no more, no fewer, never replenished. Then the four moves: melee, Axe Cleave, Ground Stomp, Gore Charge. Each is drawn on the ground before it lands — a wedge, a circle, a long lane — outline first, then filling in. At thirty percent health it roars, and after that it is faster and its recovery shorter.

Far more time went into those warnings than into damage numbers. Numbers can be tuned; if a player cannot read "what happens next", tuning is pointless. Making a game alone means testing it alone, and the easiest place to fool yourself is "I know what its next move is" — of course you do, you wrote it. So the warnings have to read to someone seeing them for the first time.

In-engine capture: the forest battlefield in rain, a red enraged warlord raising its axe, minotaurs and goblins closing in, the swordsman and a dark placeholder figure standing on blue ringsIN-ENGINE CAPTURE · IN-DEVELOPMENT BUILD
Rain, before the warlord's gate. The red one is the warlord after its roar; the white lines are rain; the dark figure in front is the vampire — his final look is still under review, so a placeholder stands in. In-engine capture, in-development build.

Rain changes the light

The field has three weathers: clear, rain and wind. Rain lowers and cools the light, rain lines fall, the ground begins to reflect; in wind, pale streaks cross the field and the canopy leans one way — the trunks stay still, only the leaves move.

To be exact about it: weather changes the picture only. Not terrain, not collision, not enemy decisions, not damage, and it is not synchronised to the server. The switch currently lives in a developer debug drawer; the player UI does not have it. The clear / rain / wind switcher on this site is what it looks like: same field, same camera, three presentations side by side. "Dynamic weather changes your tactics" would sell better; it is not what is implemented, so it is not written. Every sentence on this site has to have something in the repository behind it — that is a rule this project set for itself.

The week in Tokyo

The hardest stretch of the online work was not code. It was proving that if the server restarts mid-fight, settlement is voided correctly and nobody gets paid — which meant restarting the server in Tokyo again and again, with each failure more absurd than the last. An expired credential. A command given both stdin and input, which Python refuses outright, so the child process never even started. A validator that treated (None, [], []) and (None, tuple(), tuple()) as different things and therefore could never say yes. And finally a thirty-second stop-grace window: the simulation kept running through it, ran past "voided" and ended in an ordinary "failure"; thirty seconds to five, and it held.

The rule was that this kind of operation is never simply re-run and each attempt is approved separately; a dozen approvals in, patience ran out and a line went to the agent — in Chinese, "don't make me approve, I approve anything this work package does" — which then really did become a formal authorisation. At the end of that week the online phase closed with one metric short by one: a thirty-minute mixed-load test whose longest run of consecutive overruns was three against a contract of fewer than three. It went forward with that gap, and the gap is written down as a gap, not dressed up as a pass.

The props had no collision

On 6 August, a bit of actual playing turned up two things: the arena props had no collision — you could walk straight through rubble and plinths — and the canopy occlusion fade was gone; leaves were supposed to turn transparent when a character walked behind a tree, and they didn't.

The first could not be fixed by "add a collider to the props", because position is computed by the server and a client-side collider does not exist as far as play is concerned. Blocking had to go into the authority-side arena data, which changed the arena's identity, which meant the already-packaged client could no longer join new matches. One sentence — "the props have no collision" — cost a content migration. The second was less dramatic: the fade code had never been deleted; the new arena simply lacked the node it looks for, so it quietly stopped working. No error, just nothing.

Both were found by playing, not by testing. Among several thousand checks, not one asked whether a person could walk through an altar.

Afterword

On 10 August an attempt to restore the Tokyo server to its previous stable version failed — the certificate chain would not verify; the automatic rollback succeeded and the service stayed up. A successful rollback is not a successful restore, and the record says exactly that, in two sentences. From that day the LAN became this project's "affordable development deployment": same connection profile, same protocol and identities, just not in Tokyo.

Combat is still moving: four players run, the warlord's four moves are in, weather sits in the drawer, settlement is all-or-nothing, and feel, numbers and spectating details are all still changing. This entry records how it is now, and why it looks like this.