Commit graph

8 commits

Author SHA1 Message Date
WiseDev
64a457545d name the collision body instead of a five-tuple
fixes the previous commit, which named the type but never defined it and
did not build.
2026-08-23 14:16:38 +03:00
WiseDev
8a4a81057b push units apart when they overlap
collisions, from checkCollisions and checkCollision in the client.

a pair is considered when both are on the same plane - air with air,
ground with ground, decided by z. the radius is the unit's own
CollisionRadius, capped at 500 when the other side has no movement
component, which is what lets a unit squeeze past a building instead of
being shoved by it, plus the other's radius. the axis test comes before
the squared one, tangency counts as a hit, and two units standing exactly
on top of each other are separated along y by the owner's facing rather
than dividing by zero.

the push is clamp(sum - distance, 0, 300) scaled by the other's mass over
mine, plus one, capped at 300, spread along dx and dy over the distance.
Mass is clamped to one through twenty and a building counts as twenty.
the accumulator is drained the same tick it is filled, as it is in the
client.

what is still short of the client: the push is averaged over the pairs
rather than run through updateMovementTowards, avoidance steering is not
modelled, and LogicMath::sqrt is our exact root rather than the client's
table - which differs from the true root above 2147441940 and will have
to be reproduced bug for bug before checksums can agree.
2026-08-23 14:15:32 +03:00
WiseDev
4239680f53 stop the integer square root from spinning forever
newton's method in integers can settle into a two-value cycle rather
than a fixed point, and the loop guard was "the value changed", which
such a cycle satisfies for ever. it ran under the session lock inside a
spawned task, so the worker never reached a yield point and the runtime
could not shut down - which is why ctrl-c printed "shutdown requested"
and then hung.

the guard is now "stopped decreasing", which is the converging form.
checked against the exact integer square root across the small range and
the boundaries, including the 46340 saturation edge and INT_MAX.
2026-08-23 14:07:03 +03:00
WiseDev
9230a212ef keep the path instead of finding it again every tick
the tick was running A* over the whole 36x64 grid for every unit, every
tick. catching up ten seconds meant thousands of searches inside the
session lock, so the tick loop never finished, snapshots never went out,
and the client - which refuses to send a command while
isFullUpdatePending is true - sat there showing the connection icon and
would not spawn anything. ctrl-c looked like a hang for the same reason:
a task stuck in that loop.

the client does not do this either. LogicMovementComponent carries a
path array precisely so the route is found once and walked. we keep the
route and the goal it was found for, drop a node once we are within
250 units of it, and only search again when the goal moves or the route
runs out.

advance_to also refuses to simulate more than forty ticks in one call,
so a late tick can never turn into an unbounded loop under the lock.
2026-08-23 14:03:26 +03:00
WiseDev
dbbd162bed regenerate mana on the server
at type 0 the client stopped working the elixir bar out for itself and
started reading it from the summoner in our snapshot - which never
moved, so the bar sat where the opening state left it and no card could
be afforded.

the rule from LogicSummoner::tick: an accumulator gains five thousand a
tick and one mana is granted for every MANA_REGEN_MS * 100 / MAX_MANA it
holds, the remainder carried rather than dropped. that works out at
2.8 seconds a mana with the shipped globals, and halves in the last
sixty seconds through MANA_REGEN_MS_END, which is the speed-up the game
has always had.
2026-08-23 13:53:41 +03:00
WiseDev
d1cc829623 send units at the nearest tower when nothing is in sight
units stood still all battle. a unit only moved if it had a combat
target, and targets are only found inside SightRange - six thousand
units for a knight - while the enemy tower sits twenty thousand away. so
nothing ever walked, nothing was ever hit, no tower fell, crowns stayed
at nothing and the battle could only end on the clock.

a unit with no target now walks at the nearest enemy building, which is
the default target the client falls back to. once the tower comes inside
sight the ordinary search picks it up and the attack timer takes over.

the battle turn log carries the standing tower count per side so damage
is visible before it becomes a crown.
2026-08-23 13:28:23 +03:00
WiseDev
9084163f71 walk units around the river instead of through it
units now path. A* over the tilemap grid, water costing 800 against 1
for ground so the route runs to a bridge, or 20 when the unit flies and
crosses anywhere. bit 5 of a map cell is the water flag, which is what
makes 48 water and leaves the bridge cells - carrying the lane ids 1 and
2 - dry. the unit walks to the next node rather than at its target, and
still stops at Range.

also stops pushing snapshots while the battle type is 1. the client is
simulating the same battle itself there, and our state does not match it
yet, so every push yanked the units back to where the server thought
they were. the snapshots are still built and verified each second, ready
for the switch to type 0 once the two simulations agree; they are simply
not sent.
2026-08-23 13:23:58 +03:00
WiseDev
054321ad50 simulate movement, targeting and damage on the server
crowns now move because the server finally hurts things. each tick it
retargets, moves, resolves attacks and buries the dead, in that order.

the numbers are the client's own. Speed is position units per tick, so a
knight at 60 covers 1.2 tiles a second; SightRange and Range are in the
same units with 1000 to a game tile, and the target's CollisionRadius is
added on the far side of both. distances compare squared with the
client's saturation rule - beyond 46340 on either axis, or on overflow,
the distance is INT_MAX rather than a wrapped negative. the attack timer
counts milliseconds fifty at a time and fires once LoadTime + HitSpeed
have passed, then rewinds to LoadTime. tower damage comes from
projectiles.csv, not from the Damage column of buildings.csv, which is
empty for them.

a princess tower leaving the board is struck from leader_towers, which is
what getStars reads, so crowns follow from the same list the client
keeps.

what is deliberately not modelled yet: pathfinding, so units walk
straight at their target instead of along the roads and over the bridges;
collision, pushback and avoidance; projectiles as travelling objects,
since the damage lands the moment the attack fires; and buffs. field_48
on the combat component is renamed hit_timer after what it holds.
2026-08-23 13:10:30 +03:00