1. Current gameplay model
The client implements a small server-authoritative multiplayer arena presentation:
- the player selects one of four chicken skins and a nickname;
-
every client joins the same Colyseus room type,
my_room; - the backend publishes players and seeds;
- the client sends directional intent rather than positions;
- server positions drive the default local and remote presentation;
- mass changes resize the player and its collision radius;
- players can exchange chat messages;
-
a
player_eatenevent can play feedback for the local predator.
The client contains no local rules for seed pickup, consuming another player, spawning entities, victory, defeat, score, or mass calculation. Those outcomes can only appear through replicated state or server messages.
2. Controls and input behavior
| Action | Input |
|---|---|
| Move left | A or Left Arrow |
| Move right | D or Right Arrow |
| Move up | W or Up Arrow |
| Move down | S or Down Arrow |
| Send chat | Enter while the message field is focused, or SEND |
| Release chat focus | Escape, or left-click outside the focused message field |
| Settings | OPTIONS in the menu or the global gear icon |
| Mute | Global sound icon |
| Leave room | Leave game in settings while connected |
MovementInput returns zero when any
LineEdit or TextEdit has focus, preventing
keyboard movement while typing. There are no touch controls, virtual
joystick, controller mappings, action/remapping UI, or accessibility
alternatives in the current project settings.
Input.get_vector() is reduced to the sign of each axis.
This produces eight possible non-zero directions and discards analog
magnitude.
3. Join and room contract
Endpoint selection
NetConfig.COLYSEUS_URL is selected at script
initialization:
const COLYSEUS_URL = (
"http://172.22.111.106:8080"
if Config.CLIENT_ENV == Config.ClientEnvEnum.DEVELOPMENT
else "https://chickensmustdie.com"
)
The current CLIENT_ENV is DEVELOPMENT.
Join operation
The client calls:
client.join_or_create(
"my_room",
{
"nickname": nickname,
"playerColor": color,
}
)
Supported playerColor values from the current UI are
chicken_1, chicken_2,
chicken_3, and chicken_4. The client trims
the nickname and rejects only an empty result. The server must
enforce all other validation.
Connection state machine
DISCONNECTED
| join_game()
v
CONNECTING
| room.joined | creation/error/close during join
v v
CONNECTED --------------------------> DISCONNECTED
| room.dropped | leave_game() ^
v v |
RECONNECTING LEAVING --------------+
| room.reconnected room.left or local disconnected check
v
CONNECTED
The state machine also allows room.left from
CONNECTED or RECONNECTING to finish the
session and return to DISCONNECTED.
On a join error, the room/callback references are cleared before the join screen is re-enabled. The current source therefore supports another join attempt.
4. Expected room state
The client reads the state as a Godot Dictionary and
expects two collections:
{
"players": {
"session-id-1": {
"nickname": "Player One",
"playerColor": "chicken_1",
"x": 120.0,
"y": 240.0,
"mass": 5.0
}
},
"seeds": {
"seed-id-1": {
"x": 360.0,
"y": 180.0
}
}
}
Player fields
| Field | Client expectation | Use |
|---|---|---|
| Collection key | Session ID string | Chooses local versus remote scene and provides stable identity. |
nickname |
String/property accessible as
player_state.nickname
|
Label above the character. |
playerColor |
String/property | Animation prefix. |
x, y |
Numeric/property | Initial and authoritative target position. |
mass |
Positive numeric/property | Collision radius, sprite scale, label offset, and camera signal. |
BasePlayer.setup() accesses fields through property
syntax, while seed code uses dictionary get(). The
backend schema/SDK representation must support the player access
pattern.
The client does not validate finite positions, a recognized skin
key, or positive mass. A malformed skin produces missing-animation
warnings; invalid mass can reach sqrt().
Seed fields
| Field | Client expectation | Use |
|---|---|---|
| Collection key | Stable seed ID | Instance lookup and removal. |
x, y |
Numeric dictionary fields |
Visual position; absent values default to 0.0.
|
Seeds are presentation-only Node2D sprites. They have
no collider or local pickup callback.
5. Replication and data flow
Player creation/removal
After room.joined, NetworkManager creates
Colyseus.Callbacks.of(room). When gameplay loads,
PlayerManager subscribes to
players collection add/remove callbacks.
players entry added
-> identify local session ID
-> instantiate local_player.tscn or remote_player.tscn
-> add child
-> store by session ID
-> BasePlayer.setup(state, session ID)
players entry removed
-> queue_free player node
-> erase dictionary entry
At every room.state_changed, Game also
provides the full players dictionary to
PlayerManager.apply_server_state(). The manager updates
present entities, creates missing ones, and removes stale local
ones. Callback and snapshot paths are intentionally redundant.
Seed reconciliation
Seeds use only a full dictionary diff from
room.state_changed:
for each server seed:
missing locally -> instantiate and setup
present locally -> apply x/y
for each local seed:
absent on server -> queue_free and erase
There is no client-side seed prediction. A seed disappears only when the server removes it from state.
Initial state handling
Game._ready() connects to future
state_changed events and also defers one
_on_state_changed() call. This covers a state snapshot
that arrived between the room join and completion of the gameplay
scene change.
6. Outgoing messages
move
NetworkInputSender emits input on a direction change
and then at least once every 0.05 seconds while the
component is active and the client remains CONNECTED.
{
"seq": 42,
"clientTime": 12345678,
"input": {
"left": false,
"right": true,
"up": false,
"down": true
}
}
Semantics:
-
seqstarts at 1 and increases per local-player instance; -
clientTimecomes fromTime.get_ticks_msec()and is not wall-clock/Unix time; - input describes intent, not a trusted position;
-
opposing directions cannot both be true from the current
Vector2representation; - diagonal input may contain one horizontal and one vertical flag;
-
sends are silently dropped by
NetworkManagerunless its state isCONNECTED.
The client does not use seq for acknowledgements or
replay. If the backend includes an acknowledged sequence in its
schema, the current client ignores it.
chat_send
{
"text": "Hello!"
}
The chat panel trims leading/trailing whitespace, rejects empty
text, and limits the UI field to 200 characters. It does not add an
optimistic local message; display waits for a
chat_message response/broadcast.
The backend remains responsible for rate limiting, authorization, sanitization, moderation, and enforcing its own length limit.
7. Incoming messages
| Message type | Client access pattern | Current behavior | Status |
|---|---|---|---|
chat_message |
sentAt, nickname,
text via get()
|
Adds a message row and scrolls down. | Implemented |
chat_error |
payload.code |
Shows the code for three seconds. | Implemented, assumes property exists |
player_eaten |
payload.get("predatorSessionId", "") |
Matching local predator plays crunch sound. | Implemented |
game_config |
Opaque payload | Emits a signal. | Partial: no consumer |
tick_sync |
Opaque payload | Emits a signal. | Partial: no consumer |
__playground_message_types |
Opaque payload | Debug log only. | Development hook |
| Any other type | Type only | Warning log. | Implemented fallback |
A chat timestamp is expected to be Unix milliseconds.
ChatMessage converts it to the system's local time-zone
bias and displays [HH:MM].
8. Movement modes
Default behavior: server-position interpolation
RECONCILIATION_ENABLED is false. In this
mode, the local player does not use
MovementController to move itself. Both local and
remote instances receive authoritative positions and interpolate
toward them.
Differences:
| Property | Local | Remote |
|---|---|---|
| Interpolation speed | 18.0 |
12.0 |
| Hard snap distance | Default controller value, 100.0 px |
Default controller value, 100.0 px |
| Animation direction | Current input direction | Direction of interpolated visual movement |
| Collision during interpolation | None | None |
Exponential interpolation uses:
var interpolation_weight := 1.0 - exp(-interpolation_speed * delta)
body.global_position = body.global_position.lerp(
target_position,
interpolation_weight
)
This is stable across varying frame durations. At 100 pixels of error or more, the controller snaps. At a tiny residual, it also snaps to avoid endless sub-pixel movement.
Because interpolation assigns global_position, local
map collisions are not enforced in the default mode. The server must
enforce collision and bounds and publish valid positions.
Optional free local simulation
When reconciliation is manually enabled,
LocalPlayer runs MovementController. The
configured movement mode is FREE:
clamp each direction component to its sign;
normalize diagonal direction;
-
multiply by
100 * delta; -
move X with
move_and_collide(); -
move Y with
move_and_collide().
Axis-separated collision movement permits sliding along obstacles. The backend must use compatible speed, diagonal normalization, collision geometry, and update cadence if this client path is enabled.
Optional grid simulation
Setting Config.MOVEMENT_TYPE to
GRID selects a 16-pixel step system. It preserves a
target until a step finishes and can consume enough frame movement
to complete multiple steps. Before starting,
test_move() checks the target; diagonal movement also
checks horizontal and vertical cardinal paths.
This code is partial: it is disabled by current configuration and carries a source TODO to check the grid simulation. It should not be considered production-verified.
9. Prediction and reconciliation
The project calls the enabled local-simulation path reconciliation, but its exact capabilities matter.
What it does
- moves the local player immediately from current input;
- continuously stores the latest server position;
- waits until the player has no input;
- at most once per configured interval, compares local and server positions;
- ignores errors up to 0.5 pixels;
- smooths moderate error over 0.3 seconds through collision-aware axis moves;
- hard-snaps error of 32 pixels or more.
What it does not do
- no input history;
- no server acknowledgement of
seq; - no mapping between server tick and local simulation frame;
- no rewind to authoritative state;
- no replay of inputs after the acknowledged sequence;
- no correction while input remains non-zero;
-
no server-provided movement constants, despite routed
game_config/tick_syncmessages.
The feature is therefore partial and disabled by default. It is basic client-side movement plus delayed idle correction, not full deterministic client prediction/reconciliation.
Correction lifecycle
new server position -> store only
each physics frame:
moving?
yes -> reset idle timer and cancel active correction
no -> increase idle timer
active correction? -> apply a proportional part via move_and_collide
interval reached AND idle >= 0.15 s?
error <= 0.5 -> do nothing
error >= 32 -> hard snap
otherwise -> begin 0.3 s correction
10. Collision, map, and camera
Map geometry
The runtime map is 800 x 608 pixels. Nine rectangles
from the compiled map become StaticBody2D collision
shapes. Four additional walls, each 64 pixels thick, are centered
just outside the left, right, top, and bottom boundaries.
The local camera limits match 0,0 to
800,608. There is no dynamic link from loaded map
dimensions to camera limits, so changing map size requires updating
the local player scene as well.
Player mass and collision
The server's mass value affects presentation as
follows:
| Output | Formula |
|---|---|
| Collision radius | sqrt(mass) * 2.3 |
| Sprite scale | 0.5 * sqrt(mass / 5.0) |
| Name-label Y | 7.5 * sqrt(mass / 5.0) - 4.1 |
mass_changed is emitted only when the new value differs
approximately from current_mass.
Camera zoom
For the local player, camera zoom maps linearly from mass 5 at zoom 3 to mass 200 at zoom 2, with clamping outside the range. It then approaches the target exponentially at speed 2.
The mass signal is connected before base setup, so the initial server mass sets the camera target as well. The serialized camera still starts at zoom 3 and approaches that target using the existing smoothing.
11. Animation behavior
Each of the four skins defines these exact animation names:
chicken_1_walk chicken_1_stand chicken_1_idle
chicken_2_walk chicken_2_stand chicken_2_idle
chicken_3_walk chicken_3_stand chicken_3_idle
chicken_4_walk chicken_4_stand chicken_4_idle
Player animation rules:
- non-zero direction ->
walk; - zero for less than three seconds ->
stand; - zero for at least three seconds ->
idle; - moving right flips the sprite horizontally; moving left clears the flip;
- vertical-only movement preserves the last horizontal orientation.
For the local player, animation is driven by input even in default server-interpolation mode. A blocked or rejected move may therefore still show walking. Remote animation uses actual interpolated movement direction.
The join preview cycles all three states on timers and validates animation existence before playing.
12. UI flow
Main menu
START -> Join screen
OPTIONS -> local SettingsMenu
Logo click -> Splash -> fade -> Main menu
EXIT -> quit
LEADERBOARD -> no handler
CREDITS -> no handler
Join screen
Previous/Next -> change skin
Join/Enter -> validate and connect
Back -> Main menu
Gameplay
Chat -> server round trip
Global settings -> volumes / leave
Global mute -> runtime Master mute
GlobalUi remains present across all scenes and has
process mode Always. The main menu additionally owns
another settings menu instance for its OPTIONS button. Both panels
use the same SettingsManager, but they are separate
scene instances.
13. Chat behavior
The chat panel is a CanvasLayer at layer 10 in the
lower-left corner. It has no history cap: every received message
creates another node for the lifetime of the gameplay scene.
Visibility behavior:
- starts at opacity 0.3;
- focus, send, message, or error changes it to 1.0;
- after eight seconds of inactivity it fades to 0.3 over 0.4 seconds;
- message receipt resets the timer;
- errors remain visible for three seconds, independently of the opacity timer.
The client does not escape or transform message text before
assigning it to a Godot Label. Godot labels do not
interpret BBCode unless using a RichTextLabel, but the
backend must still sanitize content appropriate to its storage and
other clients.
14. Audio behavior
Music
Only one track, time_for_adventure.mp3, is registered
as menu. It begins in the main menu and persists across
scene changes because MusicManager is an autoload and
no join/game code stops it.
Effects
-
UI button click:
SFXbus,-15 dB, often started at 0.11 seconds. -
Eating feedback:
crunch.wav, defaultMasterbus, randomized pitch. -
Splash word/explosion clips: played by
AudioStreamPlayer2Don defaultMasterbus.
Settings and mute
Master, Music, and SFX sliders apply instantly and are saved when a
settings panel closes. The global mute button uses
SettingsManager, whose signal also updates the button
after Master slider changes. Existing persistence and routing
remain:
- mute is not persisted;
- the stored Master slider value does not change when mute toggles;
- applying a positive Master slider value unmutes the bus;
- the SFX slider affects the UI click but not the current eating/splash sounds.
15. Reconnection and failure behavior
The Colyseus room is configured to reconnect automatically up to eight times with bounded delay. Reconnect signals retain unsent chat input and refresh the world snapshot on recovery; there is no dedicated reconnect status overlay.
During RECONNECTING:
is_in_roomis false;-
all
NetworkManager.send_message()calls return without sending; - the gameplay scene remains visible;
- there is no pause or input lock;
- default interpolation retains its last target;
- if optional prediction is enabled, local simulation may continue visually while messages are discarded.
When the SDK restores the room, state returns to
CONNECTED. When the room ultimately emits
left, cleanup returns to the main menu. There is no
dedicated terminal reconnect-failure message.
Intentional leave works while connected or reconnecting and has a
1.5-second check. If disconnected without left, local
cleanup completes and detaches old room signals. If still connected,
the manager retains the room, restores CONNECTED and
reconnection options, logs a critical error, and permits another
leave attempt. There is no force-close API in the wrapper.
16. Feature status matrix
| Area | Status | Evidence/limit |
|---|---|---|
| Menu, join, optional splash | Implemented | Connected scene handlers and active main scene. |
| Four skins and three animations each | Implemented | SpriteFrames and runtime naming match. |
| Single-room Colyseus join | Implemented | Hard-coded my_room. |
| Join retry after error | Implemented | Join-time errors clean room/state. |
| Player/seed state replication | Implemented | Callback plus full-snapshot reconciliation. |
| Default position smoothing | Implemented | Local/remote interpolation active. |
| Client prediction/reconciliation | Partial, disabled | No ack/replay; idle correction only. |
| Free movement simulation | Implemented but inactive by default | Used only when reconciliation is enabled. |
| Grid movement | Partial, inactive | Source TODO requests verification. |
| Chat | Implemented | Server round trip, 200-char UI limit, error display. |
| Mass growth presentation | Implemented | Scale, circle radius, label, signal. |
| Camera response to mass changes | Implemented | Initial and later mass use the same signal connection. |
| Basic SDK reconnect | Partial | Config/signals exist; no UX or gameplay freeze. |
| Normal room leave | Implemented | room.left completes cleanup. |
| Stuck connected leave timeout | Partial | Retains the connected room and allows retry; no native force-close API. |
| Audio settings persistence | Implemented for volumes | Direct mute is not persisted. |
| Runtime map | Implemented for one map | No runtime selection; camera bounds are hard-coded. |
| Leaderboard / credits | Not implemented | Buttons have no handlers. |
| Server config / tick sync use | Not implemented beyond routing | Signals have no consumers. |
| Automated gameplay tests | Not implemented | No application test suite found. |