Commit graph

17 commits

Author SHA1 Message Date
WiseDev
7a0ba1b652 give the server its own battle clock and end condition
LogicBattle::isEndConditionMatched, transcribed: the battle is over when
end_counter is positive, when either king is dead, when tick/20 seconds
reach MatchLength + OvertimeSeconds, or - once past MatchLength - when
the crowns differ. the divisor is the 0x66666667/2^35 multiply in the
client, which is a divide by twenty, so the battle runs at 20 ticks a
second.

crowns come from LogicSummoner::getStars: three when the enemy king is
down, otherwise two minus the enemy princess towers still standing,
which is exactly what leader_towers holds.

BattleRegistry keeps the LogicGameMode we built for each account and
advances it on the tick the client reports in its turn message, so the
server now tracks the clock, the crowns and whether the battle is over,
and drops the session when the player goes home. the two isSummoner
guards in the overtime branch are left out: they only fire for an object
that is not a summoner, which a king tower always is.
2026-08-23 12:01:26 +03:00
WiseDev
a8dcd683b2 resume the home logic when the player comes back
BattleScreen::createBattleEndHUD takes a null result message and builds
the end screen from the local battle, then BattleScreen::sendGoHomeMessage
fires - so in this mode the client decides the battle is over and asks to
go home on its own. we now clear the stopped flag on that request instead
of leaving the session wedged until the next login.

battle turns are logged with their tick, checksum and command types while
the flag is up, so the traffic at the end of a battle is visible.
2026-08-23 11:56:04 +03:00
WiseDev
3cba764841 deal the summoner a starting hand
the card bar was empty because the hand lives on the king tower, not on
the battle. LogicSummoner::decode gates a whole block behind one boolean
- setEncodeDeckDataEnabled on the client side - and we always wrote it
false, so the client kept the hand and the draw queue it was born with,
which is nothing.

the block holds four deck indices for the hand, then two int lists: the
draw queue getNextSpell walks and the used pile reshuffleDeck folds back
into it. we now hand out the first four slots and queue the rest, leave
the used pile empty and keep last used at -1, which is what
getLastUsedSpell reads as "none".

starting mana comes from the START_MANA global instead of zero, and the
field after last-used is named after clearSpellCooldowns, which is the
only thing that writes it.
2026-08-23 11:39:49 +03:00
WiseDev
27f99ee898 match against a bot player instead of an npc
the decks were the visible half: LogicBattle carries one LogicSpellDeck
per player and we wrote both as absent, so the client cleared them and
the card bar came up empty. both sides get a real deck now - the player
from their profile, the bot from the fullest row of predefined_decks.

matchmaking no longer picks a row out of npcs.csv. it takes the location
from the arena's PvpLocation column and builds an opponent avatar with
its own name, arena and trophies, so the battle reads as a player match
rather than a trainer one. StartMissionMessage still goes through the
npc path unchanged.

the battle type stays 1 on purpose. LogicGameMode::isImmediateMessageExecution
is (type - 1) < 3, so 1, 2 and 3 let the client simulate locally while 0
makes it wait for the server to drive the sector - which needs the real
tick loop we do not have yet.
2026-08-23 11:28:17 +03:00
WiseDev
4477b12f0a stop the home logic once the battle starts
the client sends EndClientTurnMessage during the battle too, but its
tick and checksum belong to the battle, not to the home. we kept
comparing them against the home checksum and answered with
OutOfSyncMessage, which is the "Client and server are out of sync!"
dialog on tick 60.

HomeMode carries the stopped flag now and returns an empty turn result
while it is set, and sector_state_for raises it, so both the mission and
the matchmaking entry points are covered.
2026-08-23 11:19:26 +03:00
WiseDev
6c5295bc07 fix the LogicGameObject field mapping
the base object writes owner index, component mask, position and z, in
that order. we had the first field unnamed and were writing the owner
into the z slot, so every object belonged to player 0, and the mask slot
carried a zero.

the mask matters: getHitpointComponent tests bit 2 of it before touching
components[2], so a zero mask made every tower report no hitpoints,
LogicCharacter::isAlive fell through to the z field, shouldDestruct went
true and the towers were destroyed on the first tick - which then hit
"cant find summoner tower" in LogicBattle::removeGameObjectReferences.

entries are built through LogicGameObjectEntry::new now so the mask is
derived from the component array and cannot drift from it.

also rename the first character flag after what sets it: kamikaze
death and morph both raise it right before the object is removed.
2026-08-23 11:17:10 +03:00
WiseDev
bc04651a73 write the compression flag byte in SectorStateMessage
SectorManager::receiveSectorState reads one byte off the front of the
message body and branches on it: 1 goes to receiveCompressedSectorState,
anything else falls through to the plain decode. we never wrote that
byte, so the client ate the first byte of the snapshot as the flag and
then read every field one byte early - server tick 0, discard 11, and
the section sentinel landed on the LogicTime tick instead of 11.

drop the two bisect switches, they served their purpose: both settings
aborted identically, which is what ruled out the object payload.
2026-08-23 11:09:27 +03:00
WiseDev
e984aa7d15 verify the snapshot before sending it, and add two bisect switches
the client aborts in Debugger::error("Full update stream is corrupted!"), tail
called out of LogicGameMode::decode, which is why the stack blames
receiveSectorState. only one error path exists there so it is a sentinel.

verify_snapshot reads the snapshot back following the client decoders and
refuses to send anything that does not land both sentinels with no trailing
bytes. it passes today, so the layout matches what i believe the client reads.

checked every reader against the binary: LogicGameMode, LogicBattle,
LogicGameObjectManager, LogicGameObject, LogicCharacter, LogicSummoner, all
three components, LogicClientAvatar, readGlobalID, readDataReference,
readConstantSizeIntArray, readGameObjectReference, decodeComponent, and
setLevelIndex for the component set. all match.

SCROLL_BATTLE_SUMMONER=0 encodes the king towers as plain characters,
SCROLL_BATTLE_PRINCESS=0 drops the princess towers. one client run with each
splits the remaining hypotheses.
2026-08-23 10:50:03 +03:00
WiseDev
fcb07bba64 fix the buff component array size, it desynced the snapshot
the client reads getTable(8)->getItemCount() entries there, table 8 is
damage_types with 5 rows. i was writing character_buffs, 12 rows, so every
snapshot carried 7 extra vints and 7 extra booleans and the stream slid.

that is what tripped the sentinel and aborted the client in
Debugger::error("Full update stream is corrupted!").

verified the rest field by field against the client decoders: LogicGameMode,
LogicBattle, LogicGameObjectManager, LogicGameObject, LogicCharacter,
LogicSummoner and LogicCombatComponent all match. the conditional character
fields stay off because ReloadAfterHits and ManaGenerateLimit are empty for both
tower rows.
2026-08-23 10:35:26 +03:00
WiseDev
6dc4caede9 matchmake against a bot instead of searching forever
537 only tells the client's ui to start searching, the handshake after it was
missing: server sends 24106 StopHomeLogic, client answers 14105, server sends
21903. that last step now builds the same snapshot the npc mission does, using
npcs row 0 as the opponent.

battle type stays 1 so it runs on the client's offline path. a real pvp battle
is type 0 and needs the udp sector channel, which does not exist here.

14107 CancelMatchmake now answers 24125 instead of being ignored.
2026-08-23 10:28:28 +03:00
WiseDev
774e7c60cf build and send the battle sector state
npc missions used to get a ServerErrorMessage back. now StartMission builds a
LogicGameMode snapshot off the arena tilemap and answers 21903.

towers come from assets/locations/*.csv the way initDefaultSector does it: tile
coordinates times 500, leader index decided by which half of the map the tower
sits in. the two king towers must be there, the client dereferences them without
a null check. they live in buildings.csv, not characters.csv.

LogicCharacter puts the base object fields fourth, not first. the buff component
writes a fixed array sized by the character_buffs row count even with no buffs.

training_arena parses to 2 kings, 4 princess towers, 36x64 subtiles. snapshot is
602 bytes over 6 objects. the real client has not seen it yet.
2026-08-23 10:24:20 +03:00
WiseDev
ca8e41c2c5 roll chest loot from the csv instead of a fixed config
RandomSpells / DifferentSpells / RareChance / EpicChance / MinGold / MaxGold all
come off the chest row now, gold is scaled by the player's arena. magic chest is
30 cards over 8 cards with 1 epic and 6 rares, like the client says it is.

arena chest rows inherit everything through BaseChest, so Free_Arena1 reads Free.
2026-08-23 09:30:38 +03:00
WiseDev
f218a45ed2 fix chest shop prices and gate the free chest
rounding is banded and the 5s band biases +3, so 22 goes to 25 not 20. numbers match the csv for arena 1 and 2.
free chest end timestamp needs migration 0002.
2026-08-23 09:02:49 +03:00
WiseDev
a2bf58a627 stop minting a new account on every login
if the client presents an id we don't know, create it under that id and bump the sequence past it.
also chest offers match through BaseChest, so one silver row covers every arena.
2026-08-23 08:54:43 +03:00
WiseDev
8a745d634c generate the daily card shop server side
prices come from the csv globals now, a json cost is just an override.
DATABASE_URL defaults to postgres:///scroll when unset.
2026-08-23 08:47:55 +03:00
WiseDev
ad5a1613e3 move players and accounts into postgres
json stores are gone, they nulled out every data ref on load and wrote the null straight back on save.
shop json and the purchase commands ended up in here too.
2026-08-23 08:40:13 +03:00
WiseDev
d25a6de423 move all crates into crates/, glob members
pure move, no code touched. readme and protocol.md paths fixed up.
2026-08-23 08:15:45 +03:00