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

The army demo shares the **physical hit formula and stamina helpers** with this
API. Its spatial loop still uses 100ms ticks, shortest paths, rotating movement
priority, and attacks declared together. It does not use the initiative queue.
The API does not resolve an entire army battle in one request.

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