Skip to content

Battles ​

Every fight in Valeron, whether your army storms a bandit camp, wolves fall on your village or a rival lord attacks your castle, is run by the server as a small simulation on the defender's castle grid. It runs in fixed steps from a seed, so the same setup and seed always give the same result. The server fights it once, keeps the result, and stores a replay that both sides can watch.

Times on this page are battle seconds. A battle runs in one go on the server when the army arrives, so world speed does not affect anything inside it.

The basics ​

  • The attacker wins only if a living commander (captain, marshal, bandit chief or pack leader) stands still on a tile next to the keep for 5 seconds.
  • The defender wins if every attacking commander dies, if the attackers are wiped out, or if 10 minutes pass.
  • An army with no commander does not fight at all; it loses at once.
  • The keep cannot be damaged. Walls, towers, gates and oil pots can be broken.
  • Attackers look for the cheapest way to the keep. Strong walls count as expensive ground, so troops walk round them if there is a gap and break through if there is not.
  • Archers and crossbowmen never attack walls. Rams, catapults and trebuchets attack only structures.
  • Defenders on walls and towers take 40% less damage from arrows and bolts, and only attackers with a bow or crossbow can reach them. Defending archers up there shoot 2 tiles further.
  • Peasant levies fill moats. Nobody else can cross a real moat until they do.
  • Traps are hidden from scouts and spring once.
  • Kills earn Renown. Losing to a raid, or having your village taken by a lord, costs Renown.

Setting up the field ​

The map ​

The field is the defender's castle plot: 32 by 32 tiles for a player village. A bandit or outlaw camp is 24 by 24, a wolf den 20 by 20 and an old-realm castle 36 by 36. Only pieces with status built take part. Pieces still queued or under construction, and destroyed rubble, are ignored.

Piece hit points at the start of the battle are

battle hp     = max(1, stored hp) x (1 + wallStrength)
battle max hp = piece's full hp x (1 + wallStrength)

for every piece that has hp above 0 and is not a trap. Moats, ditches and traps have no hit points. The keep has hit points listed but is never damaged.

The goal tiles are the open tiles in the one-tile ring around the keep, worked out once at the start. For the 4 by 4 keep that is up to 20 tiles. A wall built hard against the keep removes that tile from the goal for the whole battle, even if the wall is later destroyed.

Defenders ​

Each garrison placement is turned into soldiers one by one:

  • If the tile holds a blocking piece with troop slots (wall, tower, gatehouse, keep) and the unit is allowed on walls, the soldier stands on that piece. He is "elevated".
  • Otherwise he goes to the nearest tile without a blocking piece that has fewer than 3 soldiers on it, searching outwards ring by ring.

Units that may not man walls (militia, knights, commanders, siege) are moved to the ground even when the castle screen let you put them on the keep. Troops at home but not stationed are added as one extra placement at the tile up and left of the keep's corner, then spread by the same three-per-tile rule.

Attackers ​

Each deployment group is spread around its anchor tile, three to a tile, using only tiles within 4 tiles of the map edge that have no blocking piece. A tile is in the zone when min(x, y, size - 1 - x, size - 1 - y) < 4.

The default formation, used for raids and when a player does not deploy in time, lines up along the edge facing the attacker's home (whichever of the horizontal or vertical difference is bigger; a tie picks east or west). Rows count inwards from the edge:

Row from edgeUnitsGroup size
0siege engines1 each
1archers, crossbowmen, outlawsup to 6
2commanders1 each
3peasants, infantry, cavalryup to 6

Groups in a row are spaced 3 tiles apart and centred on the middle of the edge. Units are taken in alphabetical order of their id.

The clock ​

ConstantValue
Tick length0.25 s
Time limit600 s (2400 ticks), then the defender wins
Capture5 s (20 ticks) on a goal tile
Moat fill3 s (12 ticks) per tile

Every tick runs in this order:

  1. If a piece fell or a moat tile was filled since the last tick, rebuild the path-to-keep field.
  2. On even ticks, rebuild the two chase fields.
  3. Land every projectile due this tick.
  4. Burning ground damages attackers standing in it.
  5. Towers and oil pots with an attack fire if ready.
  6. Measure the attacking army's front.
  7. Every unit acts, in creation order. Defenders were created first, so they act before attackers in the same tick.
  8. Check capture: any commander who has stood still on a goal tile for 20 ticks in a row wins it.
  9. If no attacking commander is alive, the battle ends.

A unit that is walking does nothing until it arrives. A unit that just attacked waits round(attackSeconds / 0.25) ticks. That rounding changes real attack speeds:

Listed seconds per attackTicksReal seconds
1.041.0
1.251.25
1.3 (swordsman)51.25
1.4 (pikeman)61.5
1.5 (archer)61.5
1.6 (outlaw)61.5
3 (crossbowman)123
4 (ram, ballista tower)164
6 (catapult)246
10 (trebuchet)4010
15 (oil pot)6015

So swordsmen swing slightly faster than listed and pikemen slightly slower.

Movement and pathing ​

The path to the keep ​

Attackers follow a cost field computed outwards from the goal tiles (Dijkstra over 8 directions, diagonals cost sqrt(2) times more, and no cutting a corner past a blocking tile). Entering a tile costs:

TileCost to enter
Open ground1
Ditch2
Shallow moat (old-realm castles)4 x (1 + moatSlow)
Moat (player castles)6 x (1 + moatSlow)
Wall, tower, gate, oil pot4 + hp / 60
Keepimpassable

The hp in that formula is the piece's battle hp, wall strength included, as of the last field rebuild. The field is only rebuilt when a piece falls or a moat tile is filled, so a half-broken wall still costs its full price until something somewhere gives way.

Examples, with no wall strength bonus:

Palisade (180 hp)          4 + 180/60  = 7
Stone Wall (700 hp)        4 + 700/60  = 15.7
Stone Wall, +60% strength  4 + 1120/60 = 22.7
Stone Gatehouse (3000 hp)  4 + 3000/60 = 54 per tile, 3 tiles deep
Great Tower (3500 hp)      4 + 3500/60 = 62.3

A stone wall therefore counts like about 16 tiles of walking. If a gap in your walls is fewer extra tiles away than that, attackers walk to the gap. A gatehouse counts as three of its tiles in a row (3 x 54 = 162), so attackers usually prefer a plain wall section to the gate.

When the next step down the field is a blocking piece, an attacker first tries to line up square to it rather than diagonally. Then he hits it, if he can damage structures (see below). Archers and crossbowmen just stand there.

Chase fields and aggro ​

Two breadth-first fields measure, in steps, how far each tile is from the nearest enemy on the ground (units on walls are not chase targets). They only reach a few steps:

SideWill leave its path to chase an enemy within
Attacking commanders2 steps
Other attackers5 steps
Defenders on the ground9 steps

Step time ​

A step takes max(1, round(length / speed / 0.25)) ticks, where length is 1 or sqrt(2). For attackers only:

  • a moat tile cuts speed to speed x 0.35 / (1 + moatSlow);
  • a ditch tile cuts speed to speed x 0.6.

Defenders are never slowed. Examples: a swordsman (0.56 tiles/s) takes 7 ticks (1.75 s) per straight step; a knight (1.36) takes 3 ticks straight and 4 diagonally; a peasant (0.87) wading a moat takes 13 ticks.

What each unit does ​

Attacking infantry, cavalry and peasants ​

In order, each time it may act:

  1. Swing at the nearest defender on the ground within 1.5 tiles. Defenders on walls cannot be reached.
  2. A peasant standing in a moat fills it instead of moving: after 12 ticks of filling the tile becomes dry ground.
  3. Commanders only: hold back if the front is more than 6 away from the keep and the commander is already within front + 4 of it (see "The front").
  4. If a ground defender is within the aggro range, step towards him.
  5. Otherwise step down the path to the keep. Nobody but a peasant steps into a player-castle moat; the others wait at its edge until peasants fill it. If the next tile is a blocking piece, hit it.

The front ​

The front is the keep-field cost at the 30th percentile of the attacking troops, counting everyone except commanders and siege engines (sorted from nearest to farthest, the entry at index floor(n x 0.3)). With no such troops left, the front is 0 and commanders go straight in.

Attacking archers and crossbowmen ​

Shoot the nearest defender within range (wall-top defenders included). If none, walk down the path to the keep. They never hit walls.

Siege engines ​

Rams walk the path to the keep and batter whatever blocks it. They ignore soldiers entirely.

Catapults and trebuchets look for a target among all standing pieces that have hp, except the keep, within range. They pick the lowest distance x 0.5 for a piece that shoots back (a ballista tower or an oil pot) and distance x 1 for anything else. So a ballista tower at 12 tiles beats a wall at 7. If nothing is in range they move down the path to the keep, and when blocked they wait.

Defenders ​

  • Ranged: shoot the nearest attacker within range, plus 2 tiles if elevated. An elevated archer with no target stays put.
  • Melee, on a wall or on the ground: swing at the nearest attacker within 1.5 tiles. If elevated and nothing is adjacent, stay put. On the ground, chase any attacker within 9 steps, else walk back to the starting tile.

Defenders walk freely through their own gatehouses. Attackers must break them.

Targeting and ties ​

"Nearest" uses straight-line distance from the unit's tile. Among units at exactly the same distance, the one created first wins (defenders in garrison order, then attackers in deployment order). There is no focus fire, no threat table and no preference for low-HP targets.

Damage ​

Attack value ​

attack = base attack
  x (1 + peasantAttack)       if role is peasant
  x (1 + archerAttack)        if role is ranged (archers, crossbowmen, outlaws)
  x (1 + swordsmanAttack)     if the unit is a swordsman
  x (1 + cavalryAttack)       if role is cavalry
  x (1 + catapultDamage)      if role is siege (rams and trebuchets too)
  x (1 + towerArcherAttack)   if a defending ranged unit stands on a piece
  x (1 + allAttack)           battle boost cards
  x vsRole[target role]       pikemen 2.5 vs cavalry; crossbowmen 1.6 vs cavalry, 1.3 vs commanders

Each side uses its own bonuses. Attackers on player armies get research plus battle boost cards. Defenders of a player village get research, wall strength cards and battle boost cards. Camps, dens, old-realm castles and raiding parties get no bonuses.

Damage taken by a unit ​

armour = base armour
armour = min(0.85, armour + pikemanDefence)   pikemen only
armour = min(0.9,  armour + allArmour)        if a battle boost gives armour
damage = attack x (1 - armour)
damage = damage x 0.6                         if hit by an arrow or bolt while on a wall or tower

A unit dies when its hp reaches 0. There is no healing.

Ranged hits ​

Arrows, bolts, stones and boulders fly. The damage is worked out when the shot is fired and applied when it lands, max(1, round(distance / projectileSpeed / 0.25)) ticks later. A hit follows its target wherever it has walked. If the target died in the meantime, the shot does nothing.

ProjectileTiles per second
Bolt15
Ballista14
Arrow12
Stone (catapult)8
Boulder (trebuchet)7
Pitch (oil pot)7
ShooterChance to hit
Archer or crossbowman, target on the ground85%
Archer or crossbowman, target on a wall or tower65%
Ballista tower or oil pot85%
Catapult or trebuchet80%

A miss lands up to 1.2 tiles off in each direction. Misses by archers and ballistas do nothing. Misses by splash weapons still splash where they land.

Structures ​

A melee attacker hitting a piece deals attack x vsStructures. Armour does not apply to pieces, and nor does vsRole. Units with vsStructures 0 (archers, crossbowmen, outlaws) cannot hurt pieces at all.

A catapult or trebuchet shot that hits deals attack x vsStructures to the piece. Wherever it lands, hit or miss, it also deals attack x 0.5 to every defender within the splash radius (1 tile for catapults, 1.5 for trebuchets), wall-top defenders included and with no cover reduction.

When a piece falls, its tiles open, everyone standing on it dies, and the path field is rebuilt.

Worked example: breaking a 700 hp stone wall (Stone Walls research alone gives +15%, so 805 hp).

swordsman: 20 x 1 = 20 per 1.25 s  -> 805 / 20  = 40.25 -> 41 swings, 50 s from first to last
ram:       110 x 3 = 330 per 4 s   -> 805 / 330 = 2.44  -> 3 blows, 2 waits of 4 s between them
ram with Siegecraft + Military Engineers (+30%): 110 x 1.3 x 3 = 429 -> 2 blows
catapult:  90 x 3 = 270 per hit, 80% to hit, one throw every 6 s
trebuchet: 200 x 4 = 800 per hit; 805 hp needs 2 hits

Towers and oil pots that fire on their own ​

Pieces with an attack fire at the nearest attacker within range of the piece's centre, from the very first tick.

PieceDamageRangeEverySplashBurns
Ballista Tower (player)45104 snoneno
Boiling Oil (player)90315 s1.5yes
Ballista Tower (old realm)70144 s0.6no
Boiling Oil (old realm)2248 s1.3yes

A shot without splash damages its target. A shot with splash damages every attacker within the splash radius of where it lands instead. Bonuses do not change these numbers, except that Boiling Oil and Pitch Ditches research raise the fire left behind (below).

Fire ​

A burning shot leaves fire where it lands: radius max(1, splash), for 4 seconds, dealing damage x 0.5 x (1 + oilDamage) per second to every attacker inside. A burning trap leaves fire of radius 1.2 for 5 seconds, dealing trap damage x (1 + oilDamage) per second. Armour reduces fire damage. Fires hurt attackers only, and several fires stack.

Worked example: one player oil pot on a cluster of swordsmen (armour 0.4), with Boiling Oil and Pitch Ditches researched (oilDamage 0.4):

splash:  90 x 0.6                    = 54 each, everyone within 1.5 tiles
fire:    90 x 0.5 x 1.4 = 63 per s    -> 63 x 0.6 = 37.8 per s
         over 16 ticks of 0.25 s      = 151.2 more each if they stand in it

The oilDamage research does not raise the pot's direct hit, only its fire.

Moats, ditches and traps ​

  • A player-castle moat stops everyone but peasant levies. A peasant who steps in fills the tile in 3 seconds (12 of his own turns) unless an enemy is next to him. Old-realm moats are shallow: everyone wades, slowly, and peasants still fill them.
  • A ditch costs 2 to enter and slows attackers to 60% speed.
  • A trap springs on the first attacker to arrive on its tile and is used up. The victim takes the trap's damage (armour applies). A burning trap also leaves fire.
TrapDamageFire
Pitfall60none
Pitch Trap4545 x (1 + oilDamage) per s for 5 s, radius 1.2
Killing Pit (camps)70none
Pitch Ditch (camps)1515 x (1 + oilDamage) per s for 5 s, radius 1.2

The slows value listed on pitch traps and pitch ditches is not used by the battle code. Traps never hurt defenders.

Capture ​

After all units have acted, each living attacking commander gains one point of holding if he is not walking and stands on a goal tile, and drops back to 0 otherwise. Fighting does not reset it. The first commander, in creation order, to reach 20 (5 seconds) takes the keep and the battle ends at once. If none is alive at that point, the battle also ends: "Every commander fell" if other attackers live, "The attackers were wiped out" if none do.

Outcomes ​

Attacking a camp, den or old-realm castle ​

Attacker winsDefender wins
Lootthe site's whole hoard, scaled to what survivors can carrynone
Sitecleared from the mapstays, with its garrison reduced by the dead
Attacker Renownkills plus a clearing bonuskills

The camp's own leader (bandit chief or pack leader) is always there again next time. Hoards are fixed by the site's id, so rescouting will not change them:

SiteHoardClearing bonus
Wolf den60 to 160 meat15
Bandit camp100 to 300 gold, 10 to 40 iron, 20 to 80 cheese25
Outlaw camp80 to 220 gold, 30 to 90 meat, 40 to 120 wood30
Old-realm castle400 to 1200 gold, 100 to 300 stone, 40 to 120 iron, 0 to 60 wine120

Attacking a lord's village ​

The assault comes at the end of a siege; see Sieges and strongholds. A battle at a siege camp is fought on an open 24 by 24 field round the besiegers' command tent, with the same rules.

Attacker winsDefender wins
Loot30% of each good (pillage) or 10% (raze), scaled to carry; none on a capturenone
Buildingsraze: 2 destroyed, 4 damaged; capture: the village changes hands with 3 damaged, or a last stronghold's lord is evictednone
Attacker Renownkills plus the defender's losskills
Defender Renownloses round(20 + 0.03 x Renown)kills

A raid on your village ​

Raiders winYou win
Lootthe raid's share (wolves: food only), scaled to carrynone
Buildingscastle troops: 1 destroyed, 3 damagednone
Your Renownlose round(flat + share x Renown)kills

Raid flat and share values are in Military. In every case the dead are gone and damage to your castle stays until you repair it.

Loot ​

want[res] = floor(stored[res] x share)          for every good with want > 0
capacity  = sum over survivors of carry x count
k         = min(1, capacity / sum(want))
take[res] = floor(want[res] x k)

A village's gold is its lord's treasury, so stored[gold] is the defending lord's whole balance and the share is taken from that. Loot gold you bring home goes to your own treasury.

Worked example: you pillage a village holding 1000 wood and 500 stone, whose lord has 300 gold, and 20 swordsmen and your captain survive.

want      = 300 wood, 150 stone, 90 gold   (540 total)
capacity  = 20 x 10 + 1 x 0 = 200
k         = 200 / 540 = 0.370
take      = floor(111.1) = 111 wood, floor(55.6) = 55 stone, floor(33.3) = 33 gold  (199)

Weapons in the armoury are resources too and are looted like anything else. Camp hoards use the same formula with share 1. On the way home the loot is added to your stores up to capacity, and the rest is lost.

Razing ​

Destroyed and damaged buildings are picked by a seeded shuffle from buildings that are active or idle and not founding buildings (hall, stockpile, granary). Destroyed ones vanish. Damaged ones go back into construction for max(60 s, buildSeconds x 0.5 / worldSpeed) in real time and stop working until done.

Renown ​

kill Renown = round(sum of unit renown x count killed)
loss        = min(floor(Renown), round(flat + Renown x share))

Kill values are in Military. A peasant is worth 0.5, so two dead peasants are worth 1. Renown never goes below 0. The winner of a lord-versus-lord battle gains exactly what the loser lost, on top of kills. A defender who loses gets the loss only; their kills earn nothing. Raiders never earn Renown.

Example: you beat a bandit raid, killing 20 bandits, 5 outlaws and the chief: 20 x 1 + 5 x 1.5 + 8 = 35.5, rounded to 36. A lord with 5000 Renown whose village is taken loses round(20 + 150) = 170, and the attacker gains 170 plus kills.

After the battle ​

  • Every piece is written back to the castle with hp = max(1, round(battle hp / (1 + wallStrength))) if it still stands, or hp 0, status destroyed if it fell. Sprung traps and filled moat tiles also come back as destroyed and must be repaired.
  • Dead defenders come off the village's troops. Dead stationed soldiers also come off their castle placements.
  • Both sides get a battle report (30 kept per lord), with units, losses, Renown, loot, razed and damaged buildings, and counts of structures destroyed and damaged.

How units look ​

Every unit is drawn from a rendered sprite sheet: 8 facings, each with idle, walk, attack and death animations. A soldier stands about 11 pixels tall at zoom 1.

  • The sides are told apart by a ring of colour under each unit's feet: madder red for the attacker, woad blue for the defender. The art itself is undyed. The ring goes when the unit dies.
  • Death plays once and the body stays down.
  • Peasants filling a moat use their attack swing.
  • Bowmen (archers, crossbowmen, outlaws) shoot; everyone else on foot swings.
  • The Pack Leader is the wolf drawn a quarter bigger and darker.
  • The knight, the ram, the catapult and the trebuchet move as one piece: the knight bobs at a trot, the ram drives forward, the engines rock with each throw and tip over when destroyed.
  • A unit without a sheet, and any unit while its sheet is still loading, is drawn as a simple figure in its side's colour instead.

Replays ​

Each battle is stored as a replay: the starting position of every unit and piece, then a list of events (moves, swings, wall hits, shots, deaths, piece damage, pieces falling, traps springing, fires, moat filling). You can watch any battle that is still in your reports. The server keeps the last 300 replays across the whole world; older ones are cleared and show "That replay has been cleared away." In the replay, traps and hidden pieces are flagged hidden until they spring.

The seed is a hash of the world seed and the battle id. Replays are exact, but you cannot predict the seed before a battle, so you cannot rehearse one.

Interactions ​

For maintainers ​

Source files:

  • server/src/features/military/battle.ts: the whole simulation (simulateBattle).
  • server/src/features/military/rules.ts: carryCapacity, takeLoot, raze, killRenown, raidRenownLoss, CLEAR_RENOWN, campHoard.
  • server/src/features/military/pieces.ts: old-realm and camp pieces and layouts (pveLayout), the fallback hall keep.
  • server/src/features/military/deploy.ts: deploy zone and default formation.
  • server/src/features/military/index.ts: armyBattle, raidBattle, villageDefence, applyDefenderLosses, replay storage.
  • server/src/features/military/military.test.ts: determinism, capture, traps, walls, moats, loot, razing, Renown.
  • clients/web/src/features/military/skins.ts: SheetSkin (sheets from clients/web/public/sprites/units/, indexed by units.json) and the drawn fallback figures.
  • Unit art: concepts from nano-banana-2, models, rigs and animations from Tripo (rig v3.0 biped with the idle, walk, slash or shoot, and fall presets; the wolf on a quadruped rig with the quadruped walk). tools/art/blender/units.txt lists each unit's kind, height and facing correction, render_units.sh renders through blender.sh (one Blender at a time, CPU) with render_unit.py, and pack_units.py writes the sheets. The Tripo files live outside the repo in ~/Code/valeron-assets/3d/units. /units.html on the dev server shows every sheet in every facing.

Key constants in battle.ts: ATTACKER_AGGRO = 5, DEFENDER_AGGRO = 9, commander aggro 2, BUCKET = 4, PROJECTILE_SPEED, cover factor 0.6, hit chances 0.85 / 0.65 / 0.8, miss scatter 2.4, piece entry cost 4 + hp / 60, moat costs 6 and 4, ditch 2, moat speed 0.35, ditch speed 0.6, armour caps 0.85 and 0.9, siege splash damage 0.5, fire 4 s at 0.5 dps share, trap fire 5 s radius 1.2, front percentile 0.3, commander holds back while front > 6. Rules from militaryContent.battle: tickSeconds 0.25, maxSeconds 600, deployDepth 4, moatFillSeconds 3, captureSeconds 5.

Oddities found while writing this page:

  • trap.slows (pitch traps, pitch ditches, the castle ditch) is never read by the battle code.
  • The castle Ditch says "half speed", but the battle slows attackers to 0.6.
  • Siegecraft and Military Engineers say "catapult damage" but catapultDamage multiplies every siege unit, rams and trebuchets included.
  • Goal tiles are fixed at the start, so a wall touching the keep removes that goal tile even after it falls.
  • The path field ignores partial damage until something falls or a moat fills.