Skip to content

ResolveBattleRound

Resolve one battle round: one declaration per living, able party member.

Full documentation: ResolveBattleRound. Wire type: resolve_battle_round.

Legal session modes: battle

JSON Schema

{
  "$defs": {
    "BattleDeclaration": {
      "description": "One party member's declared action for a battle round.\n\nYou build these yourself, one per member the round expects, and hand the whole\nset to `ResolveBattleRound`. Which\nmembers a round expects is\n`EncounterView.declarers`. The\nthree id tuples beside it (`front_rank`, `immobile`, and `reloading`) say which\ndeclarations the engine will take, so a front end that reads all four offers only\nlegal choices.\n\n`action` decides which other fields matter, and the rest stay `None`. `attack`\nnames a target group and, optionally, a wielded weapon, with `None` meaning bare\nhands. `cast` names the spell, its mode, its form, and its targets. `move` is a\nrange-track intent for the whole formation. `use_item` covers a wand, staff, or\nrod, and a flask thrown at a group. `turn_undead` needs nothing else: turning\nresolves in the magic phase but is never disrupted, because it's a class ability\nrather than a spell. `hold` does nothing, which is how a member with no legal\naction still fills their slot on the roster.",
      "properties": {
        "character_id": {
          "title": "Character Id",
          "type": "string",
          "description": "Whose declaration this is, by\n`MemberView.id`. It has to be one of the ids in\n`EncounterView.declarers`, and every one of\nthose needs a declaration of its own in the same round."
        },
        "action": {
          "enum": [
            "attack",
            "cast",
            "turn_undead",
            "move",
            "use_item",
            "hold"
          ],
          "title": "Action",
          "type": "string",
          "description": "What this member does. `attack` swings or shoots at a group, `cast` casts a spell,\n`turn_undead` presents a holy symbol, `move` changes the distance to the monsters,\n`use_item` throws or triggers something, and `hold` does nothing. Which other fields matter\nfollows from this one, and `hold` needs none of them."
        },
        "target_group_id": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Target Group Id",
          "description": "Which monster group the action is aimed at, by\n`EncounterGroupView.id` off\n`EncounterView.groups`. An `attack` needs it, so\ndoes a `use_item` with a thrown item, and so does a `close` move. A group that has fled is\nrefused with `battle.declaration.unknown_group`."
        },
        "weapon_id": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Weapon Id",
          "description": "Which wielded weapon to attack with: a mundane weapon's catalog id, a magic weapon's\nper-instance id, or `None` to strike unarmed. The weapon has to be wielded already, so\nequip it with `EquipItem` before the fight. A weapon\nmerely carried is refused with `battle.declaration.weapon_not_wielded`. Whether the\ndeclaration counts as melee or missile follows from the weapon's own qualities and the\ngroup's distance."
        },
        "spell_id": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Spell Id",
          "description": "Which spell a `cast` declaration casts, from\n`load_spells` (see the spell id index), or,\nfor a `use_item` scroll read, which spell to read off the scroll. A `cast` with no spell\nnamed is refused with `battle.declaration.missing_spell`."
        },
        "spell_mode": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Spell Mode",
          "description": "Which of the spell's usages to cast, by that usage's\n`SpellMode.key`. A `cast` declaration has to name one. A\nscroll read may leave it `None` and take the spell's first usage."
        },
        "reversed": {
          "default": false,
          "title": "Reversed",
          "type": "boolean",
          "description": "Cast the reversed form of the spell, on the same terms as\n`CastSpell.reversed`: an arcane caster needs a\nreversed copy memorized, and a divine caster chooses here."
        },
        "targets": {
          "default": [],
          "items": {
            "type": "string"
          },
          "title": "Targets",
          "type": "array",
          "description": "The spell's targets, named as in\n`CastSpell.targets`: member ids, monster ids, or\na `cell:` reference. A target the party can't see is refused with\n`battle.declaration.invisible_target`."
        },
        "move": {
          "anyOf": [
            {
              "enum": [
                "close",
                "fighting_withdrawal",
                "retreat"
              ],
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Move",
          "description": "Which movement a `move` declaration makes. `close` advances the whole formation on\n`target_group_id` at the party's slowest encounter rate, stopping at melee range.\n`fighting_withdrawal` backs the formation off at half that rate. `retreat` breaks off at\nfull rate, and a round in which every member retreats ends the battle and turns it into a\npursuit, or into a clean escape when nothing can chase. The party moves as one formation\nand a single member can't leave it, so `fighting_withdrawal` and `retreat` are legal only\nwhen every member the round expects to declare names the same one. A round some declare one\nin and the rest don't is refused whole with `battle.declaration.formation_split`, one\nrejection per move the round split on, naming who chose it and who didn't. `close` is not a\ndefensive move and needs no agreement, so the first one in marching order advances the whole\nformation whenever the round is accepted, and a `close` declared beside a `fighting_withdrawal`\nor a `retreat` is one of the others that split the round. A fighting withdrawal is a move on\nits own: the member who declares it makes no attack that round, because the formation moves\ntogether and a member declares one thing per round.\n[The adaptations register](https://mmacy.github.io/osrlib-python/adaptations/)\nstates that reading in full."
        },
        "item_id": {
          "anyOf": [
            {
              "type": "string"
            },
            {
              "type": "null"
            }
          ],
          "default": null,
          "title": "Item Id",
          "description": "Which item a `use_item` declaration uses: a magic item's per-instance id for a wand, staff,\nor rod, or a mundane item's catalog id for something thrown at a group, like a flask of\noil. An item with no combat use of its own is refused with\n`battle.declaration.item_unusable`."
        }
      },
      "required": [
        "character_id",
        "action"
      ],
      "title": "BattleDeclaration",
      "type": "object"
    }
  },
  "description": "Resolve one battle round: one declaration per living, able party member.\n\nA battle must be underway (see\n`EngageBattle`). Validation is the pure\npre-phase: every declaration validates or the whole command rejects listing\nevery rejection, because partial acceptance would tangle the replay contract.\n\nModes:\n    `battle`\n\nRejections:\n    - `session.command.wrong_mode` - no battle is underway.\n    - `battle.none_active` - a second check behind the mode gate, not\n      reachable through normal play.\n    - `battle.declaration.roster_mismatch` - the declarations do not name\n      exactly the living, able members.\n    - `battle.declaration.unknown_action` - an unrecognized `action`.\n    - Move declarations: `battle.declaration.missing_move`,\n      `battle.declaration.unknown_group`, `battle.declaration.cannot_move`, and\n      `battle.declaration.formation_split` when a `fighting_withdrawal` or a\n      `retreat` is not the whole formation's. There is one per defensive move the\n      round split on, and its `others` names every other declarer of the round, the\n      one who chose the other defensive move included, so a round that splits on\n      both moves comes back with two, the fighting withdrawal's first and the\n      retreat's second, each naming the other's declarers.\n    - Attack declarations: `battle.declaration.unknown_group`,\n      `battle.declaration.no_target`, `battle.declaration.weapon_not_wielded`,\n      `battle.declaration.not_in_front_rank`, and the kernel attack checks\n      `combat.attack.out_of_reach`, `combat.attack.out_of_range`,\n      `combat.attack.reload`, `combat.attack.attacker_incapacitated`,\n      `combat.attack.attacker_blind`.\n    - Cast declarations: `battle.declaration.missing_spell`,\n      `battle.declaration.unknown_group`,\n      `battle.declaration.invisible_target`, and the cast checks\n      `magic.cast.unknown_spell`, `magic.cast.silenced_area`,\n      `magic.cast.unknown_mode`, `magic.cast.unknown_target`,\n      `magic.cast.not_memorized`, `magic.cast.caster_incapacitated`,\n      `magic.cast.caster_restrained`, `magic.cast.anti_magic_shell`,\n      `magic.cast.not_reversible`, `magic.cast.target_count`,\n      `magic.cast.out_of_range`.\n    - Turn-undead declarations: `magic.turning.not_a_turner`,\n      `magic.turning.caster_incapacitated`.\n    - Item declarations: `battle.declaration.item_unusable`,\n      `battle.declaration.unknown_group`, `battle.declaration.no_target`,\n      `items.use.not_usable`, `items.device.inert`, `items.scroll.spent`,\n      `items.scroll.no_such_spell`, `items.scroll.wrong_caster`,\n      `exploration.action.requires_light`, `combat.attack.out_of_reach`.\n\nEvents:\n    Opening the round:\n    `BattleRoundEvent`, a\n    `SpellDeclaredEvent` per declared\n    cast, and\n    `InitiativeRolledEvent` for the\n    side order.\n\n    Movement: `GroupMovedEvent` as the\n    gap changes.\n\n    Missiles and melee:\n    `AttackRolledEvent`,\n    `DamageDealtEvent`,\n    `DamageAbsorbedEvent`,\n    `HitPointsReportedEvent`,\n    `SavingThrowRolledEvent`,\n    `EquipmentDestroyedEvent`,\n    `LevelDrainedEvent` with\n    `SpellForgottenEvent` when a drain\n    costs a caster prepared spells, and\n    `DeathEvent`.\n\n    Magic: `SpellCastEvent`,\n    `TargetsSelectedEvent` when the\n    spell picks its own targets,\n    `SpellDisruptedEvent`,\n    `MagicDispelledEvent`,\n    `UndeadTurnedEvent`, and the effects\n    a spell leaves behind\n    (`EffectAttachedEvent`,\n    `EffectReleasedEvent`,\n    `ConditionGainedEvent`,\n    `ConditionRemovedEvent`,\n    `HealingAppliedEvent`).\n\n    Items: `ItemUsedEvent`, with\n    `ItemIdentifiedEvent` and\n    `CurseRevealedEvent` at first\n    meaningful use.\n\n    Morale: `MoraleCheckedEvent`,\n    `MonsterFledEvent` for a group that\n    breaks, and\n    `MonstersLeftBehindEvent`\n    when the runners leave members who cannot move behind them.\n\n    Closing the round, the clock's own bookkeeping:\n    `EffectExpiredEvent`,\n    `EffectTickedEvent`,\n    `MonsterRevivedEvent`,\n    `LightEvent`, and\n    `ProvisionsEvent`.\n\n    A terminal round appends\n    `BattleEndedEvent` and the\n    encounter's conclusion\n    (`EncounterEndedEvent`,\n    `MonsterDefeatedEvent`s, and,\n    under the immediate XP timing,\n    `XpAwardedEvent` and\n    `CharacterLeveledUpEvent`),\n    or `GameOverEvent` on a party wipe.",
  "properties": {
    "command_type": {
      "const": "resolve_battle_round",
      "default": "resolve_battle_round",
      "title": "Command Type",
      "type": "string",
      "description": "The wire discriminator. Each subclass fixes it to its own snake_case literal, like\n`\"move_party\"` or `\"open_door\"`, so you never set it yourself: constructing the subclass\ndoes. It's what `parse_command` and the\n`AnyCommand` union read to rebuild the right class\nfrom a serialized mapping, and it stays the same across releases."
    },
    "source": {
      "anyOf": [
        {
          "minLength": 1,
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "title": "Source",
      "description": "An annotation naming the authored object (a trigger or quest id) or the game\nsystem on whose behalf the command was issued. Execution never reads it: a stamped\ncommand does exactly what the same command unstamped does. It is logged and replayed\nwith the command, so the log alone answers \"why did this happen\". Absent is `None`,\nand the empty string is not a value."
    },
    "declarations": {
      "default": [],
      "items": {
        "$ref": "#/$defs/BattleDeclaration"
      },
      "title": "Declarations",
      "type": "array",
      "description": "One `BattleDeclaration` per member listed in\n`EncounterView.declarers`, and none for\nanyone else. Any other roster is refused with `battle.declaration.roster_mismatch`, and a\nsingle bad declaration rejects the whole command rather than half the round. Order doesn't\ndecide who acts, since initiative and the phase order do that, but when more than one\nmember declares `close` on different groups the first entry is the one the formation\nfollows."
    }
  },
  "title": "ResolveBattleRound",
  "type": "object"
}