← Back to Runs
Run Detail
5212cdc6-ce3e-42bb-8f6e-93a1106829a9
Started11/06/2026, 04:33:39
Git SHA40ddd336
Statedirty
Avg Error %0.20%
BH Sig Count4
Total Cases178
Passing162
Run Report—
Code Changes Since Previous Run
No code changes between this run and the previous run.
Show Raw Dirty State Patch (vs Clean Baseline)
diff --git a/simulator/src/damage.ts b/simulator/src/damage.ts--- a/simulator/src/damage.ts+++ b/simulator/src/damage.ts@@ -38,6 +38,7 @@effect: ActiveEffect;bucket: AtomicBucket;valuePct: number;+ carriedInactive?: boolean;}interface MaxBucketCandidateGroup {@@ -123,13 +124,17 @@const consumedEffectIds = new Set<string>();const candidates: BucketCandidate[] = [];const handledCandidateEffectIds = traceEnabled ? new Set<string>() : undefined;+ const carriedAttackDurationEffectIds = job.carriedAttackDurationEffectIds ? new Set(job.carriedAttackDurationEffectIds) : undefined;for (const candidate of bucketCandidatesForJob(options.effectIndex, job)) {- if (!isEffectActive(candidate.effect, job.round)) continue;+ const active = isEffectActive(candidate.effect, job.round);+ const carriedInactive = !active && carriedAttackDurationEffectIds?.has(candidate.effect.id) === true;+ if (!active && !carriedInactive) continue;handledCandidateEffectIds?.add(candidate.effect.id);candidates.push({effect: candidate.effect,bucket: candidate.bucket,- valuePct: currentEffectValuePct(candidate.effect, job.round)+ valuePct: currentEffectValuePct(candidate.effect, job.round),+ carriedInactive});}@@ -267,7 +272,7 @@appliedEffects.push(appliedEffect);}for (const candidate of candidates) {- if (candidate.effect.duration.type === "attack") consumedEffectIds.add(candidate.effect.id);+ if (candidate.effect.duration.type === "attack" && !candidate.carriedInactive) consumedEffectIds.add(candidate.effect.id);if (detail === "full" && candidate !== selected) {rejectedEffects.push({ effectId: candidate.effect.source.effectId ?? candidate.effect.id, reason: "same_effect_max_suppressed" });}diff --git a/simulator/src/simulator.test.ts b/simulator/src/simulator.test.ts--- a/simulator/src/simulator.test.ts+++ b/simulator/src/simulator.test.ts@@ -1323,6 +1323,87 @@assert.deepEqual(result.extraSkillAttackJobsByEffect, { hitLiving: 4 });});+test("attack-duration normal damage effects do not carry to triggered extra skill damage by default", () => {+ const result = simulateBattle(+ {+ maxRounds: 1,+ trace: true,+ attacker: {+ troops: { marksman_t1: 100 },+ heroes: { FollowUp: { skill_1: 1, skill_2: 1 } }+ },+ defender: {+ troops: { infantry_t1: 100 },+ heroes: {}+ }+ },+ attackDurationCarryConfig()+ );++ const normalBoosts = result.attacks+ .filter((attack) => attack.kind === "normal" && attack.attackerSide === "attacker")+ .map((attack) => attack.trace?.atomicBuckets["active.hero.attack.up"].totalPct ?? 0);+ const skillBoosts = result.attacks+ .filter((attack) => attack.kind === "skill")+ .map((attack) => attack.trace?.atomicBuckets["active.hero.attack.up"].totalPct ?? 0);+ assert.deepEqual(normalBoosts, [100]);+ assert.deepEqual(skillBoosts, [0]);+});++test("mechanics flag carries consumed attack-duration effects to matching triggered extra skill damage only", () => {+ const result = simulateBattle(+ {+ maxRounds: 1,+ trace: true,+ mechanics: { carryAttackDurationEffectsToTriggeredExtraSkillDamage: true },+ attacker: {+ troops: { marksman_t1: 100 },+ heroes: { FollowUp: { skill_1: 1, skill_2: 1 } }+ },+ defender: {+ troops: { infantry_t1: 100, lancer_t1: 100, marksman_t1: 100 },+ heroes: {}+ }+ },+ attackDurationCarryConfig({ buffAppliesVs: "infantry", extraTargets: "enemy.living" })+ );++ const normalBoosts = result.attacks+ .filter((attack) => attack.kind === "normal" && attack.attackerSide === "attacker")+ .map((attack) => attack.trace?.atomicBuckets["active.hero.attack.up"].totalPct ?? 0);+ const skillBoostsByTarget = result.attacks+ .filter((attack) => attack.kind === "skill")+ .map((attack) => `${attack.defenderUnit}:${attack.trace?.atomicBuckets["active.hero.attack.up"].totalPct ?? 0}`)+ .sort();+ assert.deepEqual(normalBoosts, [100]);+ assert.deepEqual(skillBoostsByTarget, ["infantry:100", "lancer:0", "marksman:0"]);+});++test("mechanics flag carries consumed attack-duration effects with any target scope to all matching triggered extra skill damage", () => {+ const result = simulateBattle(+ {+ maxRounds: 1,+ trace: true,+ mechanics: { carryAttackDurationEffectsToTriggeredExtraSkillDamage: true },+ attacker: {+ troops: { marksman_t1: 100 },+ heroes: { FollowUp: { skill_1: 1, skill_2: 1 } }+ },+ defender: {+ troops: { infantry_t1: 100, lancer_t1: 100, marksman_t1: 100 },+ heroes: {}+ }+ },+ attackDurationCarryConfig({ buffAppliesVs: "any", extraTargets: "enemy.living" })+ );++ const skillBoosts = result.attacks+ .filter((attack) => attack.kind === "skill")+ .map((attack) => attack.trace?.atomicBuckets["active.hero.attack.up"].totalPct ?? 0)+ .sort((left, right) => left - right);+ assert.deepEqual(skillBoosts, [100, 100, 100]);+});+test("attack-triggered source and target selectors resolve to concrete active scopes", () => {const result = simulateBattle({@@ -1801,3 +1882,40 @@diagnostics: { legacyFields: [], effectTypes: {}, unsupportedEffects: [], ambiguousTurnTriggerSelectors: [] }};}++function attackDurationCarryConfig(+ options: { buffAppliesVs?: "any" | "infantry"; extraTargets?: "use.target" | "enemy.living" } = {}+): SimulatorConfig {+ const buffAppliesVs = options.buffAppliesVs ?? "any";+ const extraTargets = options.extraTargets ?? "use.target";+ return minimalConfig({+ FollowUp: {+ name: "FollowUp",+ skills: {+ OneUseNormalBoost: {+ trigger: { type: "battle_start" },+ effects: {+ boost: {+ type: "active.hero.attack.up",+ value: 100,+ units: { applies_to: "marksman", applies_vs: buffAppliesVs },+ duration: { type: "attack", value: 1 }+ }+ }+ },+ TriggeredExtraDamage: {+ trigger: { type: "attack", probability: 100, source: "marksman" },+ effects: {+ extra: {+ type: "extra_skill_attack",+ value: 100,+ units: { applies_to: "trigger.source", applies_vs: "any" },+ trigger_damage_jobs: [{ source: "use.source", target: extraTargets }],+ duration: { type: "attack", value: 1 }+ }+ }+ }+ }+ }+ });+}diff --git a/simulator/src/simulator.ts b/simulator/src/simulator.ts--- a/simulator/src/simulator.ts+++ b/simulator/src/simulator.ts@@ -53,6 +53,7 @@extraSkillAttackJobsByEffect: Record<string, number>;attackControlCounts: { dodge: number; no_attack: number };consumedEffectUseKeys: Set<string>;+ carryAttackDurationEffectsToTriggeredExtraSkillDamage: boolean;counters: {attacks: Record<SideId, Record<UnitType, number>>;received: Record<SideId, Record<UnitType, number>>;@@ -112,7 +113,7 @@const attacker = resolveFighter(input.attacker, "attacker", config, input.mechanics);const defender = resolveFighter(input.defender, "defender", config, input.mechanics);const fighters: Record<SideId, ResolvedFighter> = { attacker, defender };- const runtime = createRuntime([attacker, defender], createSeededRng(input.seed ?? "simulator-default"));+ const runtime = createRuntime([attacker, defender], createSeededRng(input.seed ?? "simulator-default"), input.mechanics);const detail = options.detail ?? "full";const traceEnabled = detail === "full" && input.trace;const trace: BattleTrace | undefined = traceEnabled ? { resolved: buildResolved(attacker, defender), rounds: [] } : undefined;@@ -289,7 +290,7 @@return Number.isFinite(value) && value > 0 && value < 100;}-function createRuntime(fighters: ResolvedFighter[], rng: Rng): Runtime {+function createRuntime(fighters: ResolvedFighter[], rng: Rng, mechanics?: BattleInput["mechanics"]): Runtime {const reports: Record<SideId, Map<string, SkillReportEntry>> = { attacker: new Map(), defender: new Map() };const skills = buildRuntimeSkills(fighters);for (const fighter of fighters) {@@ -321,6 +322,7 @@extraSkillAttackJobsByEffect: {},attackControlCounts: { dodge: 0, no_attack: 0 },consumedEffectUseKeys: new Set(),+ carryAttackDurationEffectsToTriggeredExtraSkillDamage: mechanics?.carryAttackDurationEffectsToTriggeredExtraSkillDamage === true,counters: {attacks: { attacker: emptyTroops(), defender: emptyTroops() },received: { attacker: emptyTroops(), defender: emptyTroops() }@@ -513,6 +515,9 @@roundStartTroops: DamageJob["roundStartTroops"]): DamageJob[] {const jobs: DamageJob[] = [];+ const carriedAttackDurationEffectIds = runtime.carryAttackDurationEffectsToTriggeredExtraSkillDamage+ ? attackDurationBucketEffectIdsForJob(normalAttack, round, runtime.activeEffects)+ : undefined;const effectGroups = selectStackedExtraAttackEffectGroups(runtime.effectIndex.extraAttacks.filter((effect) => isEffectActive(effect, round) && extraAttackEffectAppliesToNormalAttack(effect, normalAttack)),round@@ -548,6 +553,7 @@sourceEffectId,sourceSkillReportKey,sourceMultiplier: multiplier,+ carriedAttackDurationEffectIds,consumedEffectIds,consumedEffectUseKey,consumedEffectUseId: effect.id,@@ -745,6 +751,16 @@.map((effect) => effect.id);}+function attackDurationBucketEffectIdsForJob(job: DamageJob, round: number, effects: ActiveEffect[]): string[] {+ return effects+ .filter((effect) => {+ if (effect.kind === "extra_attack" || effect.duration.type !== "attack" || !isEffectActive(effect, round)) return false;+ const classification = classifyEffectForJob(effect, job);+ return classification?.kind === "bucket";+ })+ .map((effect) => effect.id);+}+function consumeEffects(runtime: Runtime,consumedEffectIds: string[],diff --git a/simulator/src/testcases.test.ts b/simulator/src/testcases.test.ts--- a/simulator/src/testcases.test.ts+++ b/simulator/src/testcases.test.ts@@ -285,6 +285,25 @@assert.deepEqual(input.mechanics, { weather: "clear", engagement_type: "rally" });});+test("adaptTestcaseEntry merges option mechanics without replacing testcase mechanics", () => {+ const input = adaptTestcaseEntry(+ {+ test_id: "mechanics_case",+ engagement_type: "rally",+ mechanics: { weather: "clear" },+ attacker: { troops: { infantry_t1: 1 } },+ defender: { troops: { infantry_t1: 1 } }+ },+ { mechanics: { carryAttackDurationEffectsToTriggeredExtraSkillDamage: true } }+ );++ assert.deepEqual(input.mechanics, {+ weather: "clear",+ carryAttackDurationEffectsToTriggeredExtraSkillDamage: true,+ engagement_type: "rally"+ });+});+test("compareOutcomeDistribution matches deterministic zero-bias shape", () => {const metrics = compareOutcomeDistribution({candidate: { n: 1, mu: -186, sigma: 0 },diff --git a/simulator/src/testcases.ts b/simulator/src/testcases.ts--- a/simulator/src/testcases.ts+++ b/simulator/src/testcases.ts@@ -27,6 +27,7 @@seed?: string | number;trace?: boolean;workers?: number;+ mechanics?: Record<string, unknown>;}export interface TestcaseCaseReport {@@ -207,7 +208,7 @@const detail = emptyCaseReport(reportFile, testcaseId, index, diagnostics);const preparedCase: PreparedTestcaseCase = { file, reportFile, entry, testcaseId, index, detail };try {- preparedCase.input = adaptTestcaseEntry(entry, { seed: options.seed, trace: options.trace }, diagnostics);+ preparedCase.input = adaptTestcaseEntry(entry, { seed: options.seed, trace: options.trace, mechanics: options.mechanics }, diagnostics);preparedCase.key = snapshotKey(reportFile, index);} catch (error) {detail.error = errorMessage(error);@@ -613,7 +614,11 @@return summary;}-export function adaptTestcaseEntry(entry: unknown, options: { seed?: string | number; trace?: boolean } = {}, diagnostics: string[] = []): BattleInput {+export function adaptTestcaseEntry(+ entry: unknown,+ options: { seed?: string | number; trace?: boolean; mechanics?: Record<string, unknown> } = {},+ diagnostics: string[] = []+): BattleInput {const object = entry as {attacker?: FighterInput;defender?: FighterInput;@@ -626,7 +631,7 @@};if (!object.attacker || !object.defender) throw new Error(`Testcase ${object.test_id ?? "(unknown)"} is missing attacker or defender`);diagnostics.push(...diagnoseFighterShape("attacker", object.attacker), ...diagnoseFighterShape("defender", object.defender));- const mechanics = testcaseMechanics(object);+ const mechanics = testcaseMechanics(object, options.mechanics);const maxRounds = optionalNumber(object.maxRounds ?? object.max_rounds);return {attacker: object.attacker,@@ -702,8 +707,12 @@return diagnostics;}-function testcaseMechanics(entry: { mechanics?: Record<string, unknown>; engagement_type?: unknown; engagementType?: unknown }): Record<string, unknown> | undefined {+function testcaseMechanics(+ entry: { mechanics?: Record<string, unknown>; engagement_type?: unknown; engagementType?: unknown },+ optionMechanics?: Record<string, unknown>+): Record<string, unknown> | undefined {const mechanics = entry.mechanics && typeof entry.mechanics === "object" ? { ...entry.mechanics } : {};+ if (optionMechanics && typeof optionMechanics === "object") Object.assign(mechanics, optionMechanics);if (entry.engagement_type !== undefined) mechanics.engagement_type = entry.engagement_type;if (entry.engagementType !== undefined) mechanics.engagementType = entry.engagementType;return Object.keys(mechanics).length > 0 ? mechanics : undefined;diff --git a/simulator/src/types.ts b/simulator/src/types.ts--- a/simulator/src/types.ts+++ b/simulator/src/types.ts@@ -277,6 +277,7 @@sourceEffectId?: string;sourceSkillReportKey?: string;sourceMultiplier?: number;+ carriedAttackDurationEffectIds?: string[];consumedEffectIds?: string[];consumedEffectUseKey?: string;consumedEffectUseId?: string;
Accuracy Results
178 / 178