npm package: @federicomorando/sword-engine
Description: Pure game rules engine for Il Tempo della Spada (SwORD system). Zero platform dependencies.
Author: Federico Morando
Latest version: 1.3.0

# sword-engine

Pure game rules engine for **Il Tempo della Spada** (SwORD system). Zero platform dependencies.

This package contains all game mechanics as pure functions: they take plain objects and return plain objects, with no side effects. It can be used by any platform adapter (Foundry VTT, Discord bots, CLI tools, etc.).

Used by the [SwORD Foundry VTT System](https://github.com/federicomorando/TDS4Foundry). Adapters build an intent from UI choices, roll dice, call a use-case function, then apply the returned patches to the platform's data layer.

## Modules

| Module | Description |
|--------|-------------|
| `src/check.mjs` | 9-step check pipeline + valore activation |
| `src/combat.mjs` | Damage, wounds, misura, reactions, grapple, mounted combat, talent effects |
| `src/sfida.mjs` | Extended tests: presets, approach modifiers, turn successes, resources |
| `src/rest.mjs` | Rest, meditation, fattucchiere: condition penalties, recovery |
| `src/constants.mjs` | Shared constants: skills, valori, approach mods, characteristic helpers |
| `src/data/talents.mjs` | 72 talent definitions + unlock/progress logic |
| `src/data/cultures.mjs` | 12 culture traits |
| `src/data/equipment.mjs` | 25 weapons, 3 shields, 3 armor, 29 gear, pregi, currency, encumbrance |
| `src/data/bestiary.mjs` | 39 creature stat blocks |
| `src/use-cases/resolve-check.mjs` | High-level skill check: valore, fama, combined maneuvers, penalties, resource patches |
| `src/use-cases/resolve-attack.mjs` | Attack resolution: approach, range, special moves, talents, mounted, resource patches |
| `src/use-cases/resolve-defense.mjs` | Defense check + outcome: damage, wound distribution, special moves, forza reactions |
| `src/use-cases/resolve-reaction.mjs` | All 7 reaction types: fear, cavalcare, fatigue, wounds, resource patches |
| `src/ars-oratoria.mjs` | Ars oratoria: risk-reward, outcome tiers, drain, senza parole |
| `src/contacts.mjs` | Contacts: ceto/region penalties, settlement limits, talent bonuses |
| `src/interlude.mjs` | Interlude: work earnings, bond costs, currency conversion |
| `src/use-cases/resolve-sfida-turn.mjs` | Sfida turn: resolveCheck + approach + accumulation + drain + deductions |
| `src/use-cases/resolve-ars-oratoria-turn.mjs` | Ars oratoria turn: opposed check + risk-reward + drain + combined + senza parole |
| `src/use-cases/resolve-contacts.mjs` | Contact creation: skill check with ceto/region penalties |
| `src/use-cases/resolve-interlude.mjs` | Interlude actions: study (PE), work (earnings), bond (cost check) |

## Usage

### Low-level: check pipeline

```javascript
import { swordCheckResolve } from "sword-engine";

const result = swordCheckResolve({
  characteristicScore: 10,
  diceCount: 2,
  grade: 4,
  extraDice: 0,
  successBonus: 0,
  successPenalty: 0,
  difficultyThreshold: null,
  opposedSuccesses: null,
  diceRolled: [6, 5],
  discardIndices: null
});
// result.finalSuccesses === 2
```

### High-level: full skill check with resources

```javascript
import { resolveCheck } from "sword-engine";

const { engineOutput, valoreUsed, patches } = resolveCheck(
  {
    characteristicScore: 10,
    diceCount: 2,
    grade: 3,
    extraDice: 0,
    focusDice: 1,
    valoreSelected: "honor",
    valoreScore: 2,
    spiritoCancelPenalty: 1
  },
  {
    fatiguePenalty: 1, woundPenalty: 0,
    spirito: 8, fama: 2, fatica: 10, riflessi: 10
  },
  [4, 2, 1] // pre-rolled dice
);
// engineOutput.finalSuccesses includes valore bonus
// patches = { "resources.spirito": 4 } (3 valore + 1 cancel)
```

### Combat: attack → defense → outcome

```javascript
import { resolveAttack, resolveDefenseCheck, resolveDefenseOutcome } from "sword-engine";

// 1. Attacker rolls
const attack = resolveAttack(
  {
    characteristicScore: 10, diceCount: 3, grade: 3,
    approach: "carica", specialMove: "targetedAttack",
    specialMoveTarget: "weaponArm_chest"
  },
  { fatiguePenalty: 0, woundPenalty: 0, spirito: 8, fama: 2, fatica: 12, riflessi: 10 },
  [6, 4, 1] // pre-rolled dice
);
// attack.effectiveAttackHit, attack.patches

// 2. Defender rolls (character)
const defense = resolveDefenseCheck(attack, {
  characteristicScore: 9, diceCount: 2, grade: 2,
  defenseMode: "schivata"
}, { fatiguePenalty: 0, woundPenalty: 0, spirito: 5, riflessi: 8 }, [5, 3]);

// 3. Resolve outcome (shared by all 4 defense flows)
const outcome = resolveDefenseOutcome(
  { ...attack, damageValue: 3, damageType: "T" },
  { defenseSucceeded: defense.defenseSucceeded, netSuccesses: defense.engineOutput.netSuccesses },
  { woundLevels: {graffi:0,leggere:0,gravi:0,critiche:0,mortali:0},
    woundCapacities: {graffi:3,leggere:2,gravi:2,critiche:1,mortali:1},
    fatica: 10, riflessi: 8 }
);
// outcome.damageResult, outcome.forzaReactions, outcome.patches
```

### Reactions

```javascript
import { resolveReaction, REACTION_SKILL_MAP } from "sword-engine";

const result = resolveReaction(
  { reactionType: "paura", threatSuccesses: 3, characteristicScore: 8,
    diceCount: 2, grade: 2, inCombat: true },
  { fatiguePenalty: 1, woundPenalty: 0, spirito: 5, fatica: 10, riflessi: 8 },
  [4, 3]
);
// result.passed, result.fearOutcome, result.fearFatigaLost, result.patches
```

## Tests

```bash
npm test
```

Runs 4,894 tests across 18 test suites (check, combat, talent, equipment, bestiary, sfida, rest, resolve-check, resolve-attack, resolve-defense, resolve-reaction, ars-oratoria, contacts, interlude, resolve-sfida-turn, resolve-ars-oratoria-turn, resolve-contacts, resolve-interlude).

## Changelog

### 1.3.0

**Features:**
- `combat.mjs`: `resolveDamage` now accepts `attackerSizeCategory` parameter. Grande/Enorme creatures use **multiplicative** damage (`netSuccesses × weaponDamage`). Default "media" preserves additive behavior.
- `resolve-attack.mjs`: `targetSizeCategory` parameter for target size bonuses (Piccola -1, Grande +1 ranged, Enorme +2 ranged / +1 melee). `targetMounted` parameter for mounted vs unmounted +1 bonus.
- All 5 resolve functions: Spirito overflow to Fatica when spending takes Spirito below 0.
- `combat.mjs`: `hitShield` special move difficulty (2).

### 1.2.0

**Bug fix:**
- `combat.mjs`: Grapple difficulty now uses targeted attack difficulty **minus 1** (PDF line 2666). Previous values matched targeted attack exactly. New values: head/vitals 3 (was 4), legs/shieldArm 2 (was 3), weaponArm/chest 1 (was 2).

### 1.1.0

**Bug fix:**
- `rest.mjs`: Rest failure Fatica loss now uses **effective** condition penalty (after Spirito cancellation) instead of raw condition penalty. Per PDF line 1600: "1 Fatica per uncontested success." Previously, spending Spirito to cancel penalty still resulted in the full penalty Fatica loss on failure.

**Feature: Encumbrance penalty in all resolve functions:**
- All 5 use-case resolve functions now include `state.encumbrancePenalty` (default 0) in the `basePenalty` computation. Per PDF §12.3: encumbrance penalty applies to **all checks**.
- Affected files: `resolve-check.mjs`, `resolve-attack.mjs`, `resolve-defense.mjs`, `resolve-reaction.mjs`, `resolve-ars-oratoria-turn.mjs`.
- Backward compatible: `encumbrancePenalty` defaults to 0 when not provided.

### 1.0.0

Initial release. Full implementation of the SwORD check engine, combat mechanics, extended tests, social challenges, contacts, interludes, rest/meditation, 72 talents, 12 cultures, 60 equipment items, 39 creatures.

## License

MIT