---
name: basic-lightweight-combat
title: Basic Lightweight Combat
description: Experimental, optional damage and initiative rules for games that need a starting point. No status effects, equipment effects, or persistent combat state.
homepage: https://set.world
spec: https://set.world/api/basic-lightweight-combat/spec
license: MIT
version: 1
experimental: true
rules_version: basic-lightweight-combat-1
methods: [GET]
auth: none
cors: open
content_type: text/markdown
canonical_paths:
  - https://set.world/basic-lightweight-combat
  - https://set.world/basic-lightweight-combat.md
  - https://set.world/api/basic-lightweight-combat/skill
---

# Basic lightweight combat

**Experimental and optional.** Use this if your game has no combat rules yet.
Action games based on player timing, aiming, or collisions can ignore it.
This is a small interpretation of Set's attributes, not a balance guarantee.

The endpoint resolves one attack or schedules action opportunities. Your game
keeps health, stamina, positions, cooldowns, and event history. Requests do not
save characters or award rewards.

| Read or calculate | Endpoint |
| --- | --- |
| This skill | `GET /api/basic-lightweight-combat/skill` |
| Formulas, limits, and current versions | `GET /api/basic-lightweight-combat/spec` |
| One attack | `GET /api/basic-lightweight-combat?mode=damage&…` |
| Who acts first and how many moves | `GET /api/basic-lightweight-combat?mode=initiative&…` |

## Supply reconciled stats

Use `finalStats` from a character response, or your own already-reconciled block.
Do not apply the same traits or equipment bonuses again. The order from
`/api/stats` is:

```text
strength, dexterity, intelligence, wisdom, agility, vitality, perception, resolve, luck
```

All nine inputs must be integers in 8–24. Five primaries feed the existing
attribute formulas. Wisdom, perception, resolve, and luck remain accepted but
unused here. `level` does not scale damage or grant moves.

## Resolve an attack

```sh
curl 'https://set.world/api/basic-lightweight-combat?mode=damage&attacker=16-16-16-16-16-16-16-16-16&defender=16-16-16-16-16-16-16-16-16&health=100&stamina=40&seed=42'
```

| Input | Meaning |
| --- | --- |
| `attacker`, `defender` | Required nine dash-joined stats |
| `kind` | `physical` (default), `magical`, or `action` |
| `health` | Defender's current health; defaults to derived maximum |
| `stamina` | Attacker's current stamina before recovery; defaults to derived maximum |
| `elapsedMs` | Time to recover attacker stamina, integer 0–30,000; default 0 |
| `seed` | Optional uint32; omit for an unseeded draw |
| `rulesVersion`, `contentVersion` | Optional exact-version preconditions |

Health and stamina accept finite fractional values from zero through their
respective derived maxima. Out-of-range values fail; they are not silently
clamped into a different request. This profile does not price or model a custom
10,000-HP boss from a stat-24 block.

### Damage channels

| Kind | Base damage | Defender reduction | Optional real-time cadence |
| --- | --- | --- | --- |
| physical | `physicalDamage` | `physicalDamageReduction` | `1000 / attackSpeed` ms |
| magical | `magicalDamage` | `magicalDamageReduction` | `1000 / castSpeed` ms |
| action | `actionDamage` | `physicalDamageReduction` | `1000 / chargeSpeedAction` ms |

```text
dodgeChance = clamp(0, 1, defender.dodgeRate * (1 - clamp(0, 1, attacker.clarity)))
criticalChance = clamp(0, 1, attacker.criticalHitChance)
rolledDamage = max(1, round(baseDamage * (1 - reduction) * (1 - globalResistance)
                           * (critical ? 2 : 1)))
damage = min(defenderHealth, rolledDamage)
```

Draw dodge first. If it misses, damage is zero and no critical draw is consumed.
Otherwise draw critical; it doubles damage before rounding. Physical, magical,
and action attacks all use this hit/critical rule. With every stat at 16, an
unclipped physical hit is 21 damage, or 41 on a critical; a miss is zero. These
are formula values, not a claim about the outcome of the example seed.

### Stamina and results

Recover `10 * elapsedMs / 1000` stamina, capped at maximum. An attack attempt
costs 20, including a miss. Below 20 there is no attempt and no random draw.
A defender at zero health also prevents an attempt and costs no stamina.

The response includes:

- `experimental`, `rulesVersion`, `contentVersion`, `mode`, `seed`, and effective `inputs`.
- Both stat blocks and derived attributes.
- `outcome`: `hit`, `miss`, `insufficient-stamina`, or `defender-defeated`.
- `attempted`, `hit`, `critical`, and actual `damage`.
- `health`: before, after, maximum.
- `stamina`: before, recovered, ready, spent, after, maximum.
- `roll`: raw damage, probabilities, mitigation, and random draws; null for no attempt.
- `timing`: attack, movement, and initiative intervals in milliseconds.

Apply the returned resource changes once in your game. Supply the resulting
stamina to the next request; account for elapsed recovery time once per actor.
Only call for a living attacker eligible under your range and cooldown rules.
The endpoint cannot infer those facts or battle completion from a stat block.
`health` belongs to the defender; `elapsedMs` asserts eligible recovery time for
the attacker, even if the defender is already defeated.

## Resolve initiative and moves

Here a **move means an action opportunity**, not a grid cell or a guaranteed
attack. Every character's readiness clock starts at time zero:

```text
actionIntervalMs = 1000 / initiative
action n occurs at n * actionIntervalMs, starting at n = 1
```

```sh
curl --get 'https://set.world/api/basic-lightweight-combat' \
  --data-urlencode 'mode=initiative' \
  --data-urlencode 'actors=[{"id":"steady","stats":[8,8,8,8,8,8,8,8,8]},{"id":"quick","stats":[24,24,24,24,24,24,24,24,24]}]' \
  --data-urlencode 'startMs=0' \
  --data-urlencode 'durationMs=5000'
```

`quick` has initiative 1.288: its first opportunity is at about 776.3975 ms
and it gets 6 moves. `steady` has initiative 1: first at 1,000 ms, 5 moves.
The formula therefore yields `quick` first and eleven total opportunities.
Exact-time ties retain actor input order; this is a deterministic tie policy,
not a claim of fairness between equal actors.

| Input | Domain |
| --- | --- |
| `actors` | JSON array of 1–128 `{ id, stats }` entries; unique nonblank string IDs, at most 64 characters |
| `startMs` | Nonnegative integer, default 0 |
| `durationMs` | Integer 1–30,000, default 5,000 |
| Time boundary | `startMs + durationMs <= 1,000,000,000` |
| JSON query length | At most 65,536 characters; transport limits may be smaller |

The result returns `first`, chronological `order`, and `actors` with `moves`,
initiative, first/next opportunity times, and timing values. Every event carries
actor ID, input index, action ordinal, and `atMs`.

Windows are **open at the start and closed at the end**: `(startMs, endMs]`.
For the next window, use the previous `endMs` as `startMs`. An event exactly on
that boundary belongs to the earlier window only. Fixed stats and actor order
produce the same schedule whether queried in one window or adjacent windows.
Changing stats restarts that actor's mathematical clock from the same epoch;
preserving accumulated initiative across stat changes belongs to your game.

Scheduling uses integer rate units, `round(initiative * 10000)`, to determine
membership and ties. Returned timestamps are rounded to four decimals for
display; advance windows with `window.endMs`, not a rounded event timestamp.
This mode has no RNG and accepts no `seed`.

Use each opportunity to choose an action. Remove defeated actors from your
game's event queue. Stamina and targeting can prevent an attack. The endpoint
does not spend resources while planning the schedule.

## Two ways to use the timing

For a basic turn game, use the initiative queue as the action cadence. Treat
its `moves` as opportunities and resolve an eligible attack on a chosen turn.

For a real-time game, use the reported attack cadence and
`movementIntervalMs = 400 / walkSpeed` if useful, with your own action gating.
These are alternative timing interpretations; do not add movement and attack
rates together as a free action budget.

## See initiative in a dungeon

The home page's fourth column, Adventure mode with party, is a consumer
of this initiative interpretation. Roll a party in column three; ordinary seeded
enemies then wait around corners of a first-person maze. Their Anima bodies use
the dungeon's Bayer palette and their locations appear on the minimap. Each holds
its actual generated mainhand and offhand with independent anatomical grips.
These held gestures add no weapon effects to the physical turn math. The party
is represented by damage counters and live resource cards, with no party models.
The visible status is only Turn N; the acting member's blank portrait border
lights blue for its action.

The consumer calls `resolveCombatInitiative` locally in consecutive five-second
windows and plays the resulting opportunities one at a time. Ordering retains
exact ordinal/rate comparisons and input-order ties, while a 1.1-second visual
beat makes each action readable. Its visible animation clock does not define
initiative or stamina income. It recovers living participants from the exact
elapsed initiative time once, spends 20 on a physical attempt, and rests below
20. Physical hits use this profile's dodge, critical, mitigation, and health cap.
Defeated actors lose future opportunities; health persists between encounters.

The game chooses corner positions, adjacent engagement, targeting, travel stamina
recovery, and when to finish. Enemy turns rotate through surviving party members;
party turns target the one encountered enemy. Play/Pause and Next turn expose
the schedule; Replay restores the same local cast and resources. These are optional
consumer choices, not additional endpoint behavior or a battle-session API. There
are no equipment/status effects, automatic healing, rewards, or birth updates.
The source's ALGORITHMS §14 records `dungeon-initiative-1` and its replay boundary.

## Build a spatial interpretation

The home army demo shares the **hit and stamina helpers** with this API and runs
the actual Anima scene with an overhead starting camera and optional orbit controls. Its 60Hz local loop adds
generated weapons, physical contacts, range, hand-specific reload, healing,
recoil, and ragdolls. It ends on army elimination and retains the result. It
makes no per-hit HTTP requests and does not use the initiative queue. The API
still resolves only one attack or initiative window per request.

[Anima scene](https://set.world/anima-scene) exposes that interactive field;
[All weapons](https://set.world/anima-all-weapons) demonstrates its complete
weapon vocabulary without combat. These show how one set of generated objects
supports several experiences. Exploring or adopting this approach is optional.
It is one clear starting point for your own system, including systems that
replace this fallback entirely.

The scene adds three useful concepts above the API:

- **Size:** authored weapon dimensions determine grip placement, melee contact
  reach, bounded launcher scaling, and the shape visible to a player.
- **Weight:** surface geometry and mass distribution around the grip produce a
  relative mass/inertia estimate. It adds stamina cost and slows handling against
  that same character's unarmed baseline. This is not kilograms or a grade bonus.
- **Range:** the local delivery type, character stats, geometric burden, release
  height, speed, and gravity determine reach. A fixed aim marker records intent;
  actual contact decides whether the shot reaches an opponent.

Your game can supply these definitions independently. Read `finalStats` and
attributes once; map the canonical gear identity to a weapon definition; check
team, range, cooldown, stamina, and contact before resolving an eligible hit.
Keep current resources and effect lifetimes outside the immutable birth object.
The scene also chooses local projectile/Repulse damage multipliers and all-color
healing rings; those choices do not change this endpoint's published formulas.
The exact Anima profiles are documented in the repository's `ALGORITHMS.md` §13.
No item budget is silently converted into a stat bonus.

## What stays yours

Status effects and weapon/equipment effects are excluded. There is no automatic
budget allocation, trait reapplication, range check, pathfinding, target choice,
healing, reward issuance, or progression. Stamina recovery is not health recovery.

`ailmentResistance`, `chargeSpeedCast`, `carrySpeed`, `carryCapacity`, `jumpHeight`,
`visionRange`, and `reflexes` receive no mechanics in this fallback. The skill
does not force a formula onto every attribute just because it exists.

## Replay and failures

Keep the mode, all effective inputs, seed for damage, rules version, and content
version. Use `deriveSeed(base, attackIndex)` for successive seeded attacks;
reusing the same seed repeats the same draws. A new `elapsedMs` or current
resource value is a different request. Recovery and results are arithmetic,
not evidence that a game event happened or permission to redeem it twice.

Malformed inputs and unsupported or mode-incompatible parameters return HTTP
400 with `{ error: { code, message, field } }`. Repeated query parameters use
the first value. Empty optional `seed=` is unseeded; other empty numeric fields
are invalid. Omitted resources default to full; explicit zero remains zero.
Version preconditions that differ from the server return 409. Methods other
than GET return 405 on the calculation endpoint; OPTIONS uses the shared CORS middleware.

Rules are currently `basic-lightweight-combat-1`. Content revision 3 introduces
this profile. Experimental changes must advance the rules version and follow
Set's content revision policy. Historical rules/content are not hosted; retain
the matching source release or complete results when you need them.
