Combat
The server owns every player’s health, stamina and injuries. A player’s game only reports what its swing or its arrow
hit - the game’s own hit record - and the server decides what that did, tells the mode, and tells every client what to show.
The numbers come from [combat] in server.toml (Server configuration); the game’s
own weapon and armour tables give the damage when the server has the tables export ([combat] tables).
From a hit to damage
Section titled “From a hit to damage”- The attacker’s client reports the hit: the target, the attack zone, the body part struck, and - for an arrow, a bolt or a ball - the shot it belongs to.
- The server checks it is plausible: the two are within
max_reach, the claim follows a swing the attacker announced withinclaim_window_ms, a shot was actually fired (shot_interval_ms, the ammunition in the shooter’s inventory), the two are allowed to fight (pvp, not teammates, the same virtual world). - The damage is computed. Melee: the attacker’s equipped weapon’s
Attack× the type’s modifier (stab, slash or smash) ×attack_scale, reduced by the armour the target wears on the struck part against that type (d / (d + armour_pivot), 85 % at most), × the part’s coefficient (head 1.5, torso 1, limbs 0.7), less when the swing was thrown tired, ×damage_scale. Ranged: the ammunition’s attack × the launch speed (the draw - a snap shot at half speed does a quarter) ×ranged_scale, then the same armour and part rules; every arrow is a stab. Bare hands dofist_damage. An attacker whose weapon is sheathed hits with fists whatever the claim says. Without the tables the server takes the attacker’s own number, clamped tomax_claim_damage. - The mode hears
OnPlayerDamage(pid, attacker, damage, zone, part, weapon, dtype)and may change the number, zero it or cancel the hit. - The damage comes off the health; the victim’s stamina takes
hit_stamina_damage; a hit of at leastinjury_thresholdinjures the part (OnPlayerInjury); a stab or a slash opens a bleed ofbleed_per_damage × damagehealth per second (max_bleedat most) forbleed_seconds; at0health the player dies (OnPlayerDeath) and every client shows it.
The server log names every hit: Henry#0 hit Hans#1 for 27.2 (shortswordBroad slash, raw 30.5, armour 6; zone 3, torso): health 72.8, torso injured, bleeding 0.41/s.
Stamina
Section titled “Stamina”A swing costs attack_stamina_cost (× 1.2 for a slash or a smash, × 0.5 bare-handed, × 1.25 with an injured arm); a swing
thrown with less than it costs lands at 35-100 % of its damage. A hit taken costs hit_stamina_damage, a swing blocked
block_stamina_cost (a perfect block costs the blocker nothing and the attacker
perfect_block_stamina), a shot shot_stamina, a sprint sprint_stamina_cost per second, a jump jump_stamina_cost. It
regenerates at stamina_regen per second after 1.5 s without any of those - three quarters of it while walking or running,
as the game does, and halved with an injured head or torso. The bar
the player sees is this number: GetPlayerStamina / SetPlayerStamina.
Perfect blocks and master strikes
Section titled “Perfect blocks and master strikes”The game’s own special moves work between players, and each one is the defender’s game’s call: the window the attacker’s swing opens and the key pressed inside it decide the move on the defender’s screen, the server hears of it, and every other screen plays the same move against the same swing.
- A perfect block - the block timed to the blow - makes that swing harmless: it never comes to
OnPlayerDamage, and its attacker paysperfect_block_stamina. - A master strike - a counter into the swing with the guard turned to meet it - parries it the same way and cuts back.
The counter is the defender’s hit on the attacker, on the torso and ×
master_strike_scale, and comes to the mode as an ordinaryOnPlayerDamagewith the defender asattacker. It always plays the plain animation, never the killing one: whether it kills is the damage’s call, as with any other hit. Withmaster_strike = falsethe players’ games offer no master strike (nor the disarm) and only the perfect block is left. - A clinch - two players whose blades lock - pairs them on every screen at once: the one who leads it strikes inside the
clinch (a pommel, a shield bash, a throw) and the other may counter a blow with a block timed to it, in the game’s own slow
motion. A clinch blow is the striker’s hit on the victim’s torso, ×
clinch_scale, and comes to the mode as an ordinaryOnPlayerDamage; a countered one never comes. The server pairs two players only withpvpon, while they fight each other in the same virtual world, stand within 3 metres and neither rides, and not within a second of either one’s last clinch;clinch = falsenever pairs them, and they fight on. - A counter after a perfect block, or any other special attack, is an ordinary swing and an ordinary hit, and the one it is aimed at can perfect-block it in turn - parry, counter, parry, counter. Every screen shows the attack the attacker’s game chose.
- A plain block is the defender’s call as well (
victim_blocks): a blow their own game blocked does no harm and never comes toOnPlayerDamage, whatever the attacker’s screen showed a moment later. Armour is no block: a blow that heavy armour takes whole in either player’s game still lands, and step 3 prices it. - In fact every melee blow between two players is the defender’s game’s call (
victim_blocks): it lands when their game shows it land - at the body part it struck there - is blocked when their guard met it, and misses when their game never shows the blade reach them. The attacker’s game sees its swing a moment earlier, on its own copy of the defender, and that copy never stands exactly where the defender does; the defender’s screen is the one where blocking happens. A blow the attacker saw land waits up toverdict_window_msfor the defender’s game, then counts as a miss (unanswered_blow = "land"takes the attacker’s word instead). The price is always the server’s: step 3, never a number either game sent.
A player whose game has judged no blow yet this session (a bot, a client that cannot) is hit as the attacker saw it: the blow
waits for that defender’s word - their round trip, at most parry_window_ms, or counter_parry_window_ms for the first
swing after the attacker’s own perfect block (a counter, which its victim sees a moment later) - and lands when none comes. A
clinch blow waits up to clinch_parry_window_ms for the victim’s counter. Either way OnPlayerDamage comes a moment after
the blow. Against an
NPC actor’s swing the player’s game settles the block itself - with [validation] strict the server also lands a
swing the player’s game never answers, unless its own record shows the player perfectly blocked it, blocked it, stepped away or
was out of the actor’s reach (the validation settings) - and a master strike
on an actor comes as OnActorDamage. [combat] perfect_block = false switches the moves off on the server: a
blow counts as the attacker’s game saw it land.
Fights and the lock-on
Section titled “Fights and the lock-on”Two players are in a fight - and only then may either one’s game lock on to the other’s body (the direction indicators,
the combat stance) - once a swing of one reaches the other’s body, after /fight <name>, or when the mode says so. Without
a fight two players walking up to each other stay at peace. A fight ends after fight_timeout seconds without a blow
between the two (0 = never by time), on a death, on /peace (ends every fight of the caller - its fights with NPC actors
too, which stand down), on a disconnect, or when the
mode ends it. Both players read a chat line at each change; the mode hears OnFightStart / OnFightEnd.
The mode’s side of it: StartFight (on a running fight it restarts the timeout clock - a duel mode
calls it every second so two circling duelists never lose the lock), EndFight,
AreFighting, GetPlayerOpponents,
GetPlayerWeapon.
[combat] pvp = false keeps a co-op party from locking on to or hurting each other: no fight can start and every hit
between players is refused.
SetPlayerTeam(pid, team) puts players in teams; a hit between two players
of one team is refused before OnPlayerDamage and starts no fight. NO_TEAM (-1) is no team. A mode that wants
friendly fire on writes its rule in OnPlayerDamage instead of using teams.
Ranged weapons
Section titled “Ranged weapons”A bow, a crossbow or a gun works through the server like a sword: the shooter’s client reports the shot as it leaves the
weapon (the ammunition, the speed - which is the draw), the server admits it (a bow among the equipment, the ammunition in
the inventory, shot_interval_ms between shots, shot_stamina), every client in range flies a look-alike, and the hit is
the shooter’s claim within the flight time plus shot_window_seconds. OnPlayerDamage names the weapon as
"bow_c + arrow_normal" and the type as "stab". The archer’s strength limits the draw as in the game: the server reads the
level the archer’s game reports, which a mode sets exactly - lower too - with SetPlayerStat
([spawn] stat_level only ever raises it).
NPC actors
Section titled “NPC actors”A player’s hit on an NPC actor goes through the same model (no armour on the actor) and comes to the mode as
OnActorDamage; an actor’s blow on a player is priced by the victim’s own game (the actor’s weapon through their armour)
× [actors] attack_scale and comes as OnActorAttack. An armed actor raises its guard against a swing part of the time ([actors] block_chance,
block_hold) and swings where the opponent’s guard is not; its wounds and bleeding follow the same [combat] keys as a player’s
(OnActorInjury, HealActor).
Deciding a hit
Section titled “Deciding a hit”Everything the mode needs is in the callback: cancel, scale, redirect, log.
-- no damage in the lobby zone, double damage to the head, a log line for every killfunction OnPlayerDamage(pid, attacker, damage, zone, part, weapon, dtype) if IsPlayerInZone(pid, lobby) then return false end if part == BODY_PART_HEAD then return damage * 2 endend
function OnPlayerDeath(pid, attacker) if attacker >= 0 then Log(GetPlayerName(attacker) .. " killed " .. GetPlayerName(pid) .. " with " .. GetPlayerWeapon(attacker)) endendHealPlayer(pid) makes a player whole; SetPlayerHealth(pid, 0) kills one; [combat] respawn_seconds = 0 leaves the
respawn to the mode’s SpawnPlayer.
