FIELD NOTES
NOTE 02 · AUTHORITY AND WEATHER
The server decides. The weather does not.
Why combat resolution lives on the server, why weather is confined to the presentation layer, and the firewall between the two.
What follows is a developer's personal record, including internal milestones, technical decisions and failures. It describes the current state — not a final form, and not a release commitment.
The one-line version
The server decides combat. Weather is just the picture. There is a wall between the two that I do not let anything cross.
Here is why, and what that wall actually looks like.
1. Why authority lives on the server
Phase 1 uses Nakama 3.39.0 for server authority. Server logic runs inside Nakama's TypeScript
sandbox, which has no Node fs and no crypto. I thought that was a nuisance; it turned out to
help. It forces every deterministic routine into its plainest possible form — which is exactly
what you want when the client (GDScript) has to compute a bit-identical result.
Authoritative simulation runs on integer XZ coordinates, with y_mm derived from the locked
heightfield. Why integers? Because floating point cannot be made bit-identical across two
runtimes, and I need state hashes to match. Formulas, RNG, protocol serialisation and state
hashes all have golden vectors on both the TypeScript and GDScript side, and both sides must
agree exactly.
P1-WP1 was the lab for all of this: an isolated slice doing nothing but authoritative movement,
a 30 Hz movement protocol, a single realtime session/socket owner, client prediction and
reconciliation. When it finished, it was sealed as immutable evidence.
The numbers (all from the sealed evidence, none estimated afterwards):
- formal profile join RTT median / P95:
252.124 / 465.155 ms; - degraded profile (
200 msdelay,50 msjitter,3%loss):3554.517 / 7935.412 ms; - Snapshot ceiling
5,120bytes, with no invented smaller figure to make it look neater; - the three WSS profiles each completed
610seconds of measurement and passed CA-validating WSS cross-binding, tick, bandwidth, exact release, cleanup, and strict zero checks on StableError, hash, protocol, socket and sequence staleness.
The degraded numbers are ugly. They stay because they are real.
2. Failures stay in the evidence too
The first v2 degraded attempt, 20260716T222636Z_degraded_200_50_3_67129, ran the full 610
seconds — and both clients received zero Events. The finaliser rejected and scrubbed the run
without inventing a StableError or a Match-level cause, because at the time none could be
inferred.
The same-source retry passed, but it belongs to an older source snapshot, so it is excluded from the final aggregate.
The stranger one: formal profile 20260716T231707Z_formal_120_20_1_21821 completed both
measurements with two zero-exit clients, then got rejected at closure over one generically
unreadable release ACK.
Read-only local evidence gave me high confidence that a delayed macOS com.apple.provenance
attachment changed the ACK's ctime so it landed after the payload's mtime.
But — the generic failure itself cannot prove that low-level cause. So I hardened only the two closed ACKs, minimally: at most three seconds per file, two complete strict reads at least 250 ms apart, comparing document, bytes, mode, hash, dev, inode, size, mtime and ctime for equality. Missing, unsafe, partial or continuously changing ACKs still fail closed. Every other JSON keeps a single strict read.
And because the source changed, all five formal profiles were rerun.
I write this down because "I think this was the cause so I fixed it this way" and "I proved the cause" are different claims, and conflating them is the most dangerous thing I can do here.
3. What weather can and cannot do
There are three weather presentations that can be switched at runtime: warm_day (the default),
rainy_day and windy_day.
They are configuration-driven presentation profiles — one JSON each, with a declared status
of additive_presentation_only.
Rain changes:
- sky, sun colour and energy (
sun_energydrops to0.42from the warm day), ambient light, fog colour and density; - volumetric fog, exposure, and brightness/contrast/saturation adjustment (saturation to
0.8); 440near rain streaks,220far streaks and64ground splashes, all on fixed seed104729;- a wet ground look: normal offset, tint, roughness
0.18, specular0.72, fresnel strength0.16.
Wind changes:
- world-space wind trails (
72near,36far, fixed seed130363); - canopy sway:
12tree MultiMeshes, wind direction XZ(1.0, 0.28), strength0.18 m, speed1.35, gust frequency0.55, flutter frequency2.8.
The constraints sit in the same JSON, machine-readable:
"constraints": {
"default_weather_remains": "warm_day",
"gameplay_authority_affected": false,
"terrain_collision_or_navigation_affected": false,
"town_or_title_affected": false,
"tree_trunks_animated": false,
"tree_shadow_casters_animated": false
}
So, stated plainly:
- weather does not change terrain, collision, navigation, AI perception, damage, tactical rules or online authority;
- there is no automatic weather cycle, no random weather event, and no Nakama authoritative weather sync;
- trunks do not move, and shadow-casting trees do not move — only canopy leaves sway;
- the current switch lives in a developer-build debug drawer (it has complete
zh_CN/ja/enstrings and its own tests). The normal player UI does not expose it.
The clear/rain/wind switcher on this site is a presentation-layer preview. It is honestly what it looks like: the same field, the same camera, the same character layout, compared under three weather presentations.
"Dynamic weather reshapes terrain and tactics" would read better. It is not the current implementation, so I am not going to write it.
4. Performance: what is met, and what is not
P0B lighting V3 at desktop_high:
- density 20: GPU median / P95
18.000 / 22.420 ms; - Boss + 19:
19.670 / 24.612 ms; - the Metal GPU P95 ceiling for Gate-C visuals is
33.3334 ms(1280×720,desktop_high) — passed.
But Phase 1 carries a stricter target: <16.67 ms. That one is still open.
It has not been passed and it has not been waived — it is deferred to P1-B, with final current-package / mixed-load proof due no later than P1-D. It gets its own line in the status document specifically so it cannot be absorbed into a general "performance is fine."
I also make no stable-frame-rate claim. A GPU sample is a GPU sample; it is not the smoothness a player feels.
5. What combat actually is right now
- Player actions: movement, auto slash, dash slash, whirlwind, dodge;
- Enemies: deterministic Goblin and ordinary Minotaur AI with bounded attack slots and separation;
- Density:
20at formal peak,30under stress; - Boss: three phases with telegraphs, shared attacker slots, poise and bounded reinforcements;
- Feedback: damage, stagger, knockback, death, damage numbers, VFX, audio, HUD;
- Pools: fixed enemy/effect/audio pools with zero exhaustion, zero fallback and zero node or orphan growth across a five-minute soak.
The online scope (P1-WP2) authorised is: private Hub party code / leader / Ready / party chat; authoritative online solo and duo forest combat; server-side AI, collision, skills, damage, upgrades, Boss, death, spectator and outcomes; spectator-only reconnect within 60 seconds with no control takeover and no mid-run join; and all-or-zero locked-roster settlement with receipt and restart recovery.
And it must stop at P1-B. That is a line I drew for myself.
Combat is still iterating. The four-player side, the settlement details and the boss feel are all still moving; this note describes the current state.