import { PMF } from "../pmf/pmf.js"; import { DiceQuery } from "../pmf/query.js"; import type { Attack, Rider, RiderDamage, RiderOptions, Source, TurnSpec } from "./types.js"; /** * A turn of attacks plus conditional damage riders, resolved to one **exact** * joint distribution. * * Riders are correlated with the attacks that trigger them, so a rider cannot be * a separate `DiceQuery` single convolved in afterwards: that preserves the mean * but corrupts the distribution (two daggers + Sneak Attack report P(0 damage) of * 0.015 instead of the true 0.1225). `Turn` owns the sources and enumerates the * joint outcome space instead, carrying one byte of state per trigger group. * * @example * const dagger = d20.plus(8).ac(16).onHit(d4.plus(4)); * const rogue = turn([dagger, dagger]).rider({ damage: roll(3, d6), on: "first-hit" }); * rogue.mean(); // 18.6225 */ export declare class Turn { private readonly eps; private readonly declaredAttacks; private readonly riders; private readonly plan; private resolved?; private constructor(); /** * Builds a turn from plain data, throwing {@link TurnSpecError} if it is * malformed. Use this from a UI, where `error.code` maps to the field state to * show. */ static from(spec: TurnSpec, eps?: number): Turn; /** * Appends an attack, throwing {@link TurnSpecError} if that makes the turn * invalid. * * A rider with no explicit `of` watches every declared attack *including ones * appended after it*, because `of` is resolved when the plan is built rather * than when the rider is added. Pass an explicit `of` to pin a rider to the * attacks it already saw. One attack must exist before a rider with a default * `of` is added, or the build fails `unknown-id`. */ attack(source: Source, id?: string): Turn; /** * Appends `count` copies of the same attack — the Extra Attack case, which is * most of 5e. Argument order mirrors `roll(count, die)`. * * ```ts * turn().attacks(4, greatsword).onEveryHit(d6); // fighter 20 + hunter's mark * ``` * * @throws {RangeError} if `count` is not a positive integer. */ attacks(count: number, source: Source): Turn; /** * Appends a rider, throwing {@link TurnSpecError} if that makes the turn * invalid. The `onX` methods below are the readable way to call this. */ rider(rider: Rider): Turn; /** * Fires once, on the first source that lands, in that source's mode — so a * crit on the first landing attack doubles the rider's dice. Sneak Attack. */ onFirstHit(damage: RiderDamage, options?: RiderOptions): Turn; /** * Fires once if any source crit, always in crit mode. Divine Smite: nothing is * lost by holding it for a crit, so this is "any", not "first". */ onAnyCrit(damage: RiderDamage, options?: RiderOptions): Turn; /** * Fires once if any source missed. The reroll gate: a reroll is a fresh attack, * so pass one as the damage. Kensei's Unerring Accuracy, Lucky. */ onAnyMiss(damage: RiderDamage, options?: RiderOptions): Turn; /** * Fires once per source that lands, in that hit's mode — so it can fire several * times in a turn. Hunter's Mark, Hex, Rage. */ onEveryHit(damage: RiderDamage, options?: RiderOptions): Turn; /** * Damage for the turns where the rider added just before this one did *not* * fire: "flurry of blows if I didn't smite". * * ```ts * turn([dagger, dagger]) * .onAnyCrit(roll(2, d8)) // smite * .otherwise([flurry, flurry]) // ... or two more attacks * ``` * * Always binds to the *immediately* preceding rider, so the two are branches of * one decision and can never both land. Note that chaining it therefore * alternates rather than laddering: `a.otherwise(b).otherwise(c)` makes `c` * fire whenever `b` did not, which is exactly when `a` did. For a genuine * three-way priority chain, name the riders and use explicit `not-fired` * triggers against the right one. */ otherwise(damage: RiderDamage, options?: Omit): Turn; /** * The exact joint distribution: mass 1, outcome-labelled. Resolved once and * cached. * * There is no `toPMF(eps)` to match the builders: a turn's epsilon is fixed * when it is constructed, because the plan is validated and its sources are * resolved at that point. */ get pmf(): PMF; /** Mean damage for the turn. */ mean(): number; /** * A query whose `singles` are the **declared attacks** and whose combined * distribution is the exact turn PMF. * * Riders are inside the combined PMF, not in `singles`, so singles-based * helpers (`probAtLeastOne`, `countSinglesWith`, `outcomeStats`) describe the * attacks only. Read rider-inclusive statistics off the combined PMF — * `outcomeTotals`, `outcomeDamageRanges`, `damageAttributionChartModel`. */ toQuery(): DiceQuery; /** * Attack ids in declaration order, including the `attack 1`, `attack 2`, … * defaults given to bare sources. These are the names `of` accepts. */ get attackIds(): readonly string[]; /** * Rider ids in declaration order, including the `rider 1`, `rider 2`, … * defaults. These are the names {@link Turn.fireProbability} accepts. */ get riderIds(): readonly string[]; /** * P(this rider fired). For an `every-hit` rider it is P(at least one source * hit), since that rider can fire more than once in a turn. * * @throws {TurnSpecError} `unknown-id` if `id` is not a rider — attack ids * included, since attacks always happen and have no firing probability. */ fireProbability(id: string): number; private resolve; } /** * Starts a {@link Turn}. Takes one attack or a list of them, so the two common * shapes both read straight: * * ```ts * turn(greatsword).onAnyCrit(roll(4, d8)); // one attack * turn([dagger, dagger]).onFirstHit(roll(3, d6)); // two * turn().attacks(4, greatsword); // four * ``` */ export declare function turn(attacks?: Attack | readonly Attack[], eps?: number): Turn; //# sourceMappingURL=turn.d.ts.map