Skip to content

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).

  1. 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.
  2. The server checks it is plausible: the two are within max_reach, the claim follows a swing the attacker announced within claim_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).
  3. 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 do fist_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 to max_claim_damage.
  4. The mode hears OnPlayerDamage(pid, attacker, damage, zone, part, weapon, dtype) and may change the number, zero it or cancel the hit.
  5. The damage comes off the health; the victim’s stamina takes hit_stamina_damage; a hit of at least injury_threshold injures the part (OnPlayerInjury); a stab or a slash opens a bleed of bleed_per_damage × damage health per second (max_bleed at most) for bleed_seconds; at 0 health 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.

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 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 - halved with an injured head or torso. The bar the player sees is this number: GetPlayerStamina / SetPlayerStamina.

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), 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.

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 (SetPlayerStat).

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).

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 kill
function 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 end
end
function OnPlayerDeath(pid, attacker)
if attacker >= 0 then
Log(GetPlayerName(attacker) .. " killed " .. GetPlayerName(pid) .. " with " .. GetPlayerWeapon(attacker))
end
end

HealPlayer(pid) makes a player whole; SetPlayerHealth(pid, 0) kills one; [combat] respawn_seconds = 0 leaves the respawn to the mode’s SpawnPlayer.