WOS Sim
← Back to Runs

Run Detail

0fcffb9d-9749-4928-83f3-22aec0b251a5

Started27/06/2026, 03:34:30
Git SHAbef5a413
Statedirty
Avg Error %0.20%
BH Sig Count3
Total Cases201
Passing179
Run Report

Code Changes Since Previous Run

Index: simulator/config/hero_definitions/Gwen.json
===================================================================
--- simulator/config/hero_definitions/Gwen.json prev run
+++ simulator/config/hero_definitions/Gwen.json this run
@@ -5,10 +5,10 @@
"skills": {
"EagleVision": {
"description": "Increasing target's damage taken by X%",
"trigger": {
- "type": "attack",
- "every": 2
+ "type": "turn",
+ "source": "self.any"
},
"effects": {
"EagleVision/1": {
"type": "active.hero.damageTaken.up",
@@ -19,9 +19,10 @@
20,
25
],
"units": {
- "applies_to": "enemy"
+ "applies_to": "trigger.target",
+ "applies_vs": "trigger.source"
},
"duration": {
"type": "attack",
"value": 1
@@ -58,9 +59,9 @@
"source": "use.source",
"target": "effect.applies_vs"
}
]
- },
+ },
"AirDominance/2": {
"type": "active.hero.damageTaken.up",
"value": [
5,
Index: simulator/src/damage.ts
===================================================================
--- simulator/src/damage.ts prev run
+++ simulator/src/damage.ts this run
@@ -61,11 +61,8 @@
// AttackOutcome is assembled by the recorder, not here, so fast mode allocates nothing extra.
export interface DamageResult {
kills: number;
consumedEffectIds: string[];
- consumedEffectUseKey?: string;
- consumedEffectUseId?: string;
- consumedEffectUseIds?: string[];
appliedEffectIds?: string[];
appliedEffects?: DamageEquationTrace["appliedEffects"];
trace?: DamageEquationTrace;
}
@@ -186,17 +183,13 @@
finalKills: kills
}
: undefined;
- const returnedConsumedEffectIds =
- consumedEffectIds.size === 0 ? job.consumedEffectIds ?? EMPTY_CONSUMED_EFFECT_IDS : [...consumedEffectIds, ...(job.consumedEffectIds ?? [])];
+ const returnedConsumedEffectIds = consumedEffectIds.size === 0 ? EMPTY_CONSUMED_EFFECT_IDS : [...consumedEffectIds];
return {
kills,
consumedEffectIds: returnedConsumedEffectIds,
- consumedEffectUseKey: job.consumedEffectUseKey,
- consumedEffectUseId: job.consumedEffectUseId,
- consumedEffectUseIds: job.consumedEffectUseIds,
appliedEffectIds: traceEnabled ? appliedEffects.map((effect) => effect.effectId) : undefined,
appliedEffects: traceEnabled ? appliedEffects : undefined,
trace
};
Index: simulator/src/effectIndex.ts
===================================================================
--- simulator/src/effectIndex.ts prev run
+++ simulator/src/effectIndex.ts this run
@@ -57,11 +57,11 @@
const slot =
definition.role === "attacker"
? damageJobShapeSlot(jobKind, effect.appliesTo.side, appliesToUnit, effect.appliesVs.side, appliesVsUnit)
: damageJobShapeSlot(jobKind, effect.appliesVs.side, appliesVsUnit, effect.appliesTo.side, appliesToUnit);
- const effects = index.damageByJobShape[slot];
+ const arr = index.damageByJobShape[slot];
const candidate = { effect, bucket: definition.path };
- if (effects) effects.push(candidate);
+ if (arr) arr.push(candidate);
else index.damageByJobShape[slot] = [candidate];
}
}
}
@@ -72,22 +72,26 @@
compactEffects(index.controls, isActive);
compactEffects(index.extraAttacks, isActive);
compactEffects(index.battleOrder, isActive);
for (let slot = 0; slot < index.damageByJobShape.length; slot += 1) {
- const candidates = index.damageByJobShape[slot];
- if (!candidates) continue;
- compactCandidates(candidates, isActive);
- if (candidates.length === 0) index.damageByJobShape[slot] = undefined;
+ const arr = index.damageByJobShape[slot];
+ if (!arr) continue;
+ compactCandidates(arr, isActive);
+ if (arr.length === 0) index.damageByJobShape[slot] = undefined;
}
}
export function removeStaticProfileBucketEffects(index: EffectIndex): void {
compactEffects(index.all, (effect) => !isStaticProfileEffect(effect));
for (let slot = 0; slot < index.damageByJobShape.length; slot += 1) {
- const candidates = index.damageByJobShape[slot];
- if (!candidates) continue;
- compactCandidates(candidates, (_effect, candidate) => !isStaticProfileBucket(candidate.bucket));
- if (candidates.length === 0) index.damageByJobShape[slot] = undefined;
+ const arr = index.damageByJobShape[slot];
+ if (!arr) continue;
+ let write = 0;
+ for (let read = 0; read < arr.length; read += 1) {
+ if (!isStaticProfileBucket(arr[read].bucket)) { arr[write] = arr[read]; write += 1; }
+ }
+ arr.length = write;
+ if (arr.length === 0) index.damageByJobShape[slot] = undefined;
}
}
export function bucketCandidatesForJob(index: EffectIndex, job: DamageJob): IndexedBucketEffect[] {
@@ -103,22 +107,21 @@
): number {
return (((kindIndex(jobKind) * 2 + sideIndex(attackerSide)) * 3 + unitIndex(attackerUnit)) * 2 + sideIndex(defenderSide)) * 3 + unitIndex(defenderUnit);
}
-function kindIndex(kind: DamageKind): number {
- return kind === "normal" ? 0 : 1;
-}
-
-function sideIndex(side: SideId): number {
- return side === "attacker" ? 0 : 1;
-}
-
+function kindIndex(kind: DamageKind): number { return kind === "normal" ? 0 : 1; }
+function sideIndex(side: SideId): number { return side === "attacker" ? 0 : 1; }
function unitIndex(unit: UnitType): number {
if (unit === "infantry") return 0;
if (unit === "lancer") return 1;
return 2;
}
+function isStaticProfileEffect(effect: ActiveEffect): boolean {
+ const definition = bucketDefinition(effect.intent.type);
+ return isStaticProfileBucket(effect.intent.type) || (definition !== undefined && isStaticProfileBucket(definition.path));
+}
+
function compactEffects(effects: ActiveEffect[], isActive: (effect: ActiveEffect) => boolean): void {
let write = 0;
for (let read = 0; read < effects.length; read += 1) {
const effect = effects[read];
@@ -128,18 +131,13 @@
}
effects.length = write;
}
-function isStaticProfileEffect(effect: ActiveEffect): boolean {
- const definition = bucketDefinition(effect.intent.type);
- return isStaticProfileBucket(effect.intent.type) || (definition !== undefined && isStaticProfileBucket(definition.path));
-}
-
-function compactCandidates(candidates: IndexedBucketEffect[], isActive: (effect: ActiveEffect, candidate: IndexedBucketEffect) => boolean): void {
+function compactCandidates(candidates: IndexedBucketEffect[], isActive: (effect: ActiveEffect) => boolean): void {
let write = 0;
for (let read = 0; read < candidates.length; read += 1) {
const candidate = candidates[read];
- if (!isActive(candidate.effect, candidate)) continue;
+ if (!isActive(candidate.effect)) continue;
candidates[write] = candidate;
write += 1;
}
candidates.length = write;
Index: simulator/src/recorder.ts
===================================================================
--- simulator/src/recorder.ts prev run
+++ simulator/src/recorder.ts this run
@@ -105,11 +105,8 @@
],
appliedEffectIds: result.appliedEffectIds ?? [],
appliedEffects: result.appliedEffects ?? [],
consumedEffectIds: result.consumedEffectIds,
- consumedEffectUseKey: result.consumedEffectUseKey,
- consumedEffectUseId: result.consumedEffectUseId,
- consumedEffectUseIds: result.consumedEffectUseIds,
trace: result.trace
});
}
Index: simulator/src/simulator.test.ts
===================================================================
--- simulator/src/simulator.test.ts prev run
+++ simulator/src/simulator.test.ts this run
@@ -646,12 +646,15 @@
}),
{ mode: "trace" }
);
+ const roundTwoNormalOutcome = result.attacks.find((attack) => attack.kind === "normal" && attack.jobId.startsWith("r2:attacker:infantry"));
const roundTwoSkillOutcome = result.attacks.find((attack) => attack.kind === "skill" && attack.jobId.startsWith("r2:attacker:infantry"));
+ assert.notEqual(roundTwoNormalOutcome, undefined);
+ assert.equal(roundTwoNormalOutcome!.consumedEffectIds.length, 2);
assert.notEqual(roundTwoSkillOutcome, undefined);
- assert.equal(roundTwoSkillOutcome!.consumedEffectIds.length, 2);
+ assert.equal(roundTwoSkillOutcome!.consumedEffectIds.length, 0);
});
test("requires_effect is ignored by native simulator effect activation", () => {
const result = simulateBattle(
Index: simulator/src/simulator.ts
===================================================================
--- simulator/src/simulator.ts prev run
+++ simulator/src/simulator.ts this run
@@ -34,9 +34,9 @@
skillMatchesTrigger,
type Rng
} from "./effects";
import { classifyEffectForJob } from "./classifier";
-import { createEffectIndex, indexEffect, pruneEffectIndex, removeStaticProfileBucketEffects, type EffectIndex } from "./effectIndex";
+import { bucketCandidatesForJob, createEffectIndex, indexEffect, pruneEffectIndex, removeStaticProfileBucketEffects, type EffectIndex } from "./effectIndex";
import { normalizeUnitType } from "./normalize";
import { emptyTroops, resolveFighter } from "./resolve";
import { buildStaticDamageProfile, type StaticDamageProfile } from "./staticDamageProfile";
@@ -48,17 +48,17 @@
interface Runtime {
activeEffects: ActiveEffect[];
effectIndex: EffectIndex;
+ effectById: Map<string, ActiveEffect>;
staticDamageProfile?: StaticDamageProfile;
damageScratch: DamageScratch;
rng: Rng;
skills: RuntimeSkills;
skillReports: Record<SideId, Map<string, SkillReportEntry>>;
effectActivationCounts: Record<SideId, number>;
extraSkillAttackJobsByEffect: Record<string, number>;
attackControlCounts: { dodge: number; no_attack: number };
- consumedEffectUseKeys: Set<string>;
counters: {
attacks: Record<SideId, Record<UnitType, number>>;
received: Record<SideId, Record<UnitType, number>>;
};
@@ -77,8 +77,13 @@
selected: ActiveEffect;
effects: ActiveEffect[];
}
+interface ExtraSkillJobsResult {
+ jobs: DamageJob[];
+ consumedEffectIds: string[];
+}
+
interface BattleRun {
fighters: Record<SideId, ResolvedFighter>;
runtime: Runtime;
winner: SideId | "draw";
@@ -216,17 +221,17 @@
removeStaticProfileBucketEffects(effectIndex);
return {
activeEffects,
effectIndex,
+ effectById: new Map(activeEffects.map((e) => [e.id, e])),
staticDamageProfile: template.staticDamageProfile,
damageScratch: createFastDamageScratch(),
rng,
skills: template.skills,
skillReports: cloneSkillReports(template.skillReports),
effectActivationCounts: { ...template.effectActivationCounts },
extraSkillAttackJobsByEffect: { ...template.extraSkillAttackJobsByEffect },
attackControlCounts: { ...template.attackControlCounts },
- consumedEffectUseKeys: new Set(),
counters: {
attacks: { attacker: { ...template.counters.attacks.attacker }, defender: { ...template.counters.attacks.defender } },
received: { attacker: { ...template.counters.received.attacker }, defender: { ...template.counters.received.defender } }
}
@@ -304,54 +309,79 @@
expireInactive(runtime, round);
triggerRoundStartSkills(round, runtime, roundStartTroops);
const intents = resolveAttackIntents(round, runtime, roundStartTroops);
- const jobs: DamageJob[] = [];
+ const allJobs: DamageJob[] = []; // for recorder
+ const results: DamageJobResult[] = [];
const cancelled: CancelledAttack[] = [];
+ // Phase 1: fire all attack_declared triggers — all ActiveEffects are
+ // resolved before any damage is calculated, per the battle-core spec.
+ const pendingNormalJobs: DamageJob[] = [];
for (const intent of intents) {
triggerSkills("attack_declared", round, runtime.skills.attackDeclared, runtime, intent);
const job = normalJob(intent, roundStartTroops);
const controls = applicableControls(job, round, runtime);
if (controls.no_attack || controls.dodge) {
const control = controls.no_attack ?? controls.dodge!;
runtime.attackControlCounts[control.reason] += 1;
- const consumedEffectIds = attackDurationEffectIdsForJob(job, round, runtime.activeEffects);
+ const consumedEffectIds = attackDurationEffectIdsForJob(job, runtime.effectIndex);
cancelled.push({ intent, effectId: control.effect.id, reason: control.reason, consumedEffectIds });
consumeEffects(runtime, consumedEffectIds);
} else {
- jobs.push(job);
- jobs.push(...extraSkillJobs(job, round, runtime, roundStartTroops));
+ pendingNormalJobs.push(job);
}
}
- const results: DamageJobResult[] = [];
- for (const job of jobs) {
- const result = calculateDamageJob(job, fighters, runtime.activeEffects, {
+ // Phase 2: calculate each normal job; any extra_skill_attack effect it
+ // uses spawns extra DamageJobs that are calculated immediately after.
+ // consumeEffects runs after each job so subsequent normal jobs see the
+ // correct uses count on any shared extra_skill_attack effects.
+ for (const job of pendingNormalJobs) {
+ allJobs.push(job);
+ const normalResult = calculateDamageJob(job, fighters, runtime.activeEffects, {
trace: recorder.capturesTrace,
effectIndex: runtime.effectIndex,
staticDamageProfile: runtime.staticDamageProfile,
scratch: recorder.capturesTrace ? undefined : runtime.damageScratch,
capToDefenderTroops: loopOptions.capJobKills
});
- results.push({ job, result });
- if (
- loopOptions.scoreSide &&
- job.attackerSide === loopOptions.scoreSide.attackerSide &&
- job.defenderSide === loopOptions.scoreSide.defenderSide
- ) {
- score += result.kills;
+ results.push({ job, result: normalResult });
+ if (loopOptions.scoreSide && job.attackerSide === loopOptions.scoreSide.attackerSide && job.defenderSide === loopOptions.scoreSide.defenderSide) {
+ score += normalResult.kills;
}
- consumeEffects(runtime, result.consumedEffectIds, result.consumedEffectUseKey, result.consumedEffectUseId, result.consumedEffectUseIds);
+ consumeEffects(runtime, normalResult.consumedEffectIds);
+
+ const extraSkill = extraSkillJobs(job, round, runtime, roundStartTroops);
+ if (extraSkill.consumedEffectIds.length > 0) {
+ normalResult.consumedEffectIds = [...normalResult.consumedEffectIds, ...extraSkill.consumedEffectIds];
+ consumeEffects(runtime, extraSkill.consumedEffectIds);
+ }
+
+ for (const extraJob of extraSkill.jobs) {
+ allJobs.push(extraJob);
+ const extraResult = calculateDamageJob(extraJob, fighters, runtime.activeEffects, {
+ trace: recorder.capturesTrace,
+ effectIndex: runtime.effectIndex,
+ staticDamageProfile: runtime.staticDamageProfile,
+ scratch: recorder.capturesTrace ? undefined : runtime.damageScratch,
+ capToDefenderTroops: loopOptions.capJobKills
+ });
+ results.push({ job: extraJob, result: extraResult });
+ if (loopOptions.scoreSide && extraJob.attackerSide === loopOptions.scoreSide.attackerSide && extraJob.defenderSide === loopOptions.scoreSide.defenderSide) {
+ score += extraResult.kills;
+ }
+ consumeEffects(runtime, extraResult.consumedEffectIds);
+ }
}
if (loopOptions.capRoundKills) capRoundKills(results, roundStartTroops);
if (loopOptions.commitLosses) commitRound(cancelled, results, fighters, runtime);
else commitRoundCounters(cancelled, results, runtime);
for (const entry of cancelled) recorder.recordCancelled(entry.intent, entry.effectId, entry.reason, entry.consumedEffectIds);
for (const entry of results) recorder.recordDamageJob(entry.job, entry.result);
- recorder.recordRound(round, roundStartTroops, intents, jobs);
+ recorder.recordRound(round, roundStartTroops, intents, allJobs);
}
const winner = winnerFor(fighters) ?? "draw";
return {
@@ -498,17 +528,17 @@
}
return {
activeEffects: [],
effectIndex: createEffectIndex(),
+ effectById: new Map(),
staticDamageProfile: undefined,
damageScratch: createFastDamageScratch(),
rng,
skills,
skillReports: reports,
effectActivationCounts: { attacker: 0, defender: 0 },
extraSkillAttackJobsByEffect: {},
attackControlCounts: { dodge: 0, no_attack: 0 },
- consumedEffectUseKeys: new Set(),
counters: {
attacks: { attacker: emptyTroops(), defender: emptyTroops() },
received: { attacker: emptyTroops(), defender: emptyTroops() }
}
@@ -531,11 +561,25 @@
}
function addActiveEffect(runtime: Runtime, effect: ActiveEffect): void {
runtime.activeEffects.push(effect);
+ runtime.effectById.set(effect.id, effect);
indexEffect(runtime.effectIndex, effect);
}
+function expireInactive(runtime: Runtime, round: number): void {
+ let write = 0;
+ for (let read = 0; read < runtime.activeEffects.length; read += 1) {
+ const effect = runtime.activeEffects[read];
+ if (isEffectActive(effect, round)) {
+ runtime.activeEffects[write] = effect;
+ write += 1;
+ }
+ }
+ runtime.activeEffects.length = write;
+ pruneEffectIndex(runtime.effectIndex, (effect) => isEffectActive(effect, round));
+}
+
function addInputPassiveEffects(runtime: Runtime, passive: FighterInput["passive"], side: SideId): void {
if (!passive) return;
for (const stat of ["attack", "defense", "lethality", "health"] as const) {
for (const direction of ["up", "down"] as const) {
@@ -696,19 +740,18 @@
normalAttack: DamageJob,
round: number,
runtime: Runtime,
roundStartTroops: DamageJob["roundStartTroops"]
-): DamageJob[] {
+): ExtraSkillJobsResult {
const jobs: DamageJob[] = [];
+ const consumedEffectIds: string[] = [];
const effectGroups = selectStackedExtraAttackEffectGroups(
runtime.effectIndex.extraAttacks.filter((effect) => isEffectActive(effect, round) && extraAttackEffectAppliesToNormalAttack(effect, normalAttack)),
round
);
for (const effectGroup of effectGroups) {
const effect = effectGroup.selected;
const definitions = effect.triggerDamageJobs ?? [];
- const consumedEffectIds = effectGroup.effects.map((groupEffect) => groupEffect.id);
- const consumedEffectUseKey = `${normalAttack.sourceIntentId}:${effect.stackingKey ?? effect.id}`;
const firstJobIndex = jobs.length;
let definitionIndex = 0;
for (const definition of definitions) {
const sources = resolveTriggerJobSelector(definition.source, "source", effect, normalAttack, roundStartTroops);
@@ -732,24 +775,20 @@
defenderSide: target.side,
defenderUnit: target.unit,
sourceEffectId,
sourceSkillReportKey,
- sourceMultiplier: multiplier,
- consumedEffectIds,
- consumedEffectUseKey,
- consumedEffectUseId: effect.id,
- consumedEffectUseIds: consumedEffectIds
+ sourceMultiplier: multiplier
});
runtime.extraSkillAttackJobsByEffect[sourceEffectId] = (runtime.extraSkillAttackJobsByEffect[sourceEffectId] ?? 0) + 1;
}
}
definitionIndex += 1;
}
if (jobs.length > firstJobIndex) {
- for (const consumedEffectId of consumedEffectIds) reserveConsumedEffectUse(runtime, consumedEffectUseKey, consumedEffectId);
+ consumedEffectIds.push(...effectGroup.effects.map((groupEffect) => groupEffect.id));
}
}
- return jobs;
+ return { jobs, consumedEffectIds };
}
function selectStackedExtraAttackEffectGroups(effects: ActiveEffect[], round: number): ExtraAttackEffectGroup[] {
const selected: ExtraAttackEffectGroup[] = [];
@@ -886,60 +925,28 @@
runtime.counters.received[job.defenderSide][job.defenderUnit] += 1;
}
}
-function expireInactive(runtime: Runtime, round: number): void {
- let write = 0;
- for (let read = 0; read < runtime.activeEffects.length; read += 1) {
- const effect = runtime.activeEffects[read];
- if (isEffectActive(effect, round)) {
- runtime.activeEffects[write] = effect;
- write += 1;
- }
+function attackDurationEffectIdsForJob(job: DamageJob, index: EffectIndex): string[] {
+ const ids: string[] = [];
+ for (const candidate of bucketCandidatesForJob(index, job)) {
+ if (candidate.effect.duration.type === "attack") ids.push(candidate.effect.id);
}
- runtime.activeEffects.length = write;
- pruneEffectIndex(runtime.effectIndex, (effect) => isEffectActive(effect, round));
+ for (const effect of index.controls) {
+ if (effect.duration.type !== "attack") continue;
+ const classification = classifyEffectForJob(effect, job);
+ if (classification?.kind === "control") ids.push(effect.id);
+ }
+ return ids;
}
-function attackDurationEffectIdsForJob(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" || classification?.kind === "control";
- })
- .map((effect) => effect.id);
-}
-
-function consumeEffects(
- runtime: Runtime,
- consumedEffectIds: string[],
- consumedEffectUseKey?: string,
- consumedEffectUseId?: string,
- consumedEffectUseIds?: string[]
-): void {
- const dedupeIds = consumedEffectUseIds ?? (consumedEffectUseId ? [consumedEffectUseId] : undefined);
+function consumeEffects(runtime: Runtime, consumedEffectIds: string[]): void {
for (const consumed of consumedEffectIds) {
- if (consumedEffectUseKey && dedupeIds?.includes(consumed)) {
- const useKey = `${consumedEffectUseKey}:${consumed}`;
- if (runtime.consumedEffectUseKeys.has(useKey)) continue;
- runtime.consumedEffectUseKeys.add(useKey);
- }
- for (const effect of runtime.activeEffects) {
- if (effect.id === consumed) effect.uses += 1;
- }
+ const effect = runtime.effectById.get(consumed);
+ if (effect) effect.uses += 1;
}
}
-function reserveConsumedEffectUse(runtime: Runtime, consumedEffectUseKey: string, consumedEffectId: string): void {
- const useKey = `${consumedEffectUseKey}:${consumedEffectId}`;
- if (runtime.consumedEffectUseKeys.has(useKey)) return;
- runtime.consumedEffectUseKeys.add(useKey);
- for (const effect of runtime.activeEffects) {
- if (effect.id === consumedEffectId) effect.uses += 1;
- }
-}
-
function winnerFor(fighters: Record<SideId, ResolvedFighter>): SideId | undefined {
const attackerAlive = total(fighters.attacker.troops) > 0;
const defenderAlive = total(fighters.defender.troops) > 0;
if (attackerAlive && !defenderAlive) return "attacker";
Index: simulator/src/types.ts
===================================================================
--- simulator/src/types.ts prev run
+++ simulator/src/types.ts this run
@@ -277,12 +277,8 @@
defenderUnit: UnitType;
sourceEffectId?: string;
sourceSkillReportKey?: string;
sourceMultiplier?: number;
- consumedEffectIds?: string[];
- consumedEffectUseKey?: string;
- consumedEffectUseId?: string;
- consumedEffectUseIds?: string[];
}
export interface CounterDelta {
side: SideId;
@@ -343,11 +339,8 @@
counterDeltas: CounterDelta[];
appliedEffectIds: string[];
appliedEffects: AppliedEffectTrace[];
consumedEffectIds: string[];
- consumedEffectUseKey?: string;
- consumedEffectUseId?: string;
- consumedEffectUseIds?: string[];
cancelledBy?: string;
cancelReason?: "dodge" | "no_attack";
trace?: DamageEquationTrace;
}
Show Raw Dirty State Patch (vs Clean Baseline)
diff --git a/simulator/config/hero_definitions/Gwen.json b/simulator/config/hero_definitions/Gwen.json
--- a/simulator/config/hero_definitions/Gwen.json
+++ b/simulator/config/hero_definitions/Gwen.json
@@ -6,8 +6,8 @@
"EagleVision": {
"description": "Increasing target's damage taken by X%",
"trigger": {
- "type": "attack",
- "every": 2
+ "type": "turn",
+ "source": "self.any"
},
"effects": {
"EagleVision/1": {
@@ -20,7 +20,8 @@
25
],
"units": {
- "applies_to": "trigger.target"
+ "applies_to": "trigger.target",
+ "applies_vs": "trigger.source"
},
"duration": {
"type": "attack",
diff --git a/simulator/src/damage.ts b/simulator/src/damage.ts
--- a/simulator/src/damage.ts
+++ b/simulator/src/damage.ts
@@ -62,9 +62,6 @@
export interface DamageResult {
kills: number;
consumedEffectIds: string[];
- consumedEffectUseKey?: string;
- consumedEffectUseId?: string;
- consumedEffectUseIds?: string[];
appliedEffectIds?: string[];
appliedEffects?: DamageEquationTrace["appliedEffects"];
trace?: DamageEquationTrace;
@@ -187,15 +184,11 @@
}
: undefined;
- const returnedConsumedEffectIds =
- consumedEffectIds.size === 0 ? job.consumedEffectIds ?? EMPTY_CONSUMED_EFFECT_IDS : [...consumedEffectIds, ...(job.consumedEffectIds ?? [])];
+ const returnedConsumedEffectIds = consumedEffectIds.size === 0 ? EMPTY_CONSUMED_EFFECT_IDS : [...consumedEffectIds];
return {
kills,
consumedEffectIds: returnedConsumedEffectIds,
- consumedEffectUseKey: job.consumedEffectUseKey,
- consumedEffectUseId: job.consumedEffectUseId,
- consumedEffectUseIds: job.consumedEffectUseIds,
appliedEffectIds: traceEnabled ? appliedEffects.map((effect) => effect.effectId) : undefined,
appliedEffects: traceEnabled ? appliedEffects : undefined,
trace
diff --git a/simulator/src/effectIndex.ts b/simulator/src/effectIndex.ts
--- a/simulator/src/effectIndex.ts
+++ b/simulator/src/effectIndex.ts
@@ -58,9 +58,9 @@
definition.role === "attacker"
? damageJobShapeSlot(jobKind, effect.appliesTo.side, appliesToUnit, effect.appliesVs.side, appliesVsUnit)
: damageJobShapeSlot(jobKind, effect.appliesVs.side, appliesVsUnit, effect.appliesTo.side, appliesToUnit);
- const effects = index.damageByJobShape[slot];
+ const arr = index.damageByJobShape[slot];
const candidate = { effect, bucket: definition.path };
- if (effects) effects.push(candidate);
+ if (arr) arr.push(candidate);
else index.damageByJobShape[slot] = [candidate];
}
}
@@ -73,20 +73,24 @@
compactEffects(index.extraAttacks, isActive);
compactEffects(index.battleOrder, isActive);
for (let slot = 0; slot < index.damageByJobShape.length; slot += 1) {
- const candidates = index.damageByJobShape[slot];
- if (!candidates) continue;
- compactCandidates(candidates, isActive);
- if (candidates.length === 0) index.damageByJobShape[slot] = undefined;
+ const arr = index.damageByJobShape[slot];
+ if (!arr) continue;
+ compactCandidates(arr, isActive);
+ if (arr.length === 0) index.damageByJobShape[slot] = undefined;
}
}
export function removeStaticProfileBucketEffects(index: EffectIndex): void {
compactEffects(index.all, (effect) => !isStaticProfileEffect(effect));
for (let slot = 0; slot < index.damageByJobShape.length; slot += 1) {
- const candidates = index.damageByJobShape[slot];
- if (!candidates) continue;
- compactCandidates(candidates, (_effect, candidate) => !isStaticProfileBucket(candidate.bucket));
- if (candidates.length === 0) index.damageByJobShape[slot] = undefined;
+ const arr = index.damageByJobShape[slot];
+ if (!arr) continue;
+ let write = 0;
+ for (let read = 0; read < arr.length; read += 1) {
+ if (!isStaticProfileBucket(arr[read].bucket)) { arr[write] = arr[read]; write += 1; }
+ }
+ arr.length = write;
+ if (arr.length === 0) index.damageByJobShape[slot] = undefined;
}
}
@@ -104,20 +108,19 @@
return (((kindIndex(jobKind) * 2 + sideIndex(attackerSide)) * 3 + unitIndex(attackerUnit)) * 2 + sideIndex(defenderSide)) * 3 + unitIndex(defenderUnit);
}
-function kindIndex(kind: DamageKind): number {
- return kind === "normal" ? 0 : 1;
-}
-
-function sideIndex(side: SideId): number {
- return side === "attacker" ? 0 : 1;
-}
-
+function kindIndex(kind: DamageKind): number { return kind === "normal" ? 0 : 1; }
+function sideIndex(side: SideId): number { return side === "attacker" ? 0 : 1; }
function unitIndex(unit: UnitType): number {
if (unit === "infantry") return 0;
if (unit === "lancer") return 1;
return 2;
}
+function isStaticProfileEffect(effect: ActiveEffect): boolean {
+ const definition = bucketDefinition(effect.intent.type);
+ return isStaticProfileBucket(effect.intent.type) || (definition !== undefined && isStaticProfileBucket(definition.path));
+}
+
function compactEffects(effects: ActiveEffect[], isActive: (effect: ActiveEffect) => boolean): void {
let write = 0;
for (let read = 0; read < effects.length; read += 1) {
@@ -129,16 +132,11 @@
effects.length = write;
}
-function isStaticProfileEffect(effect: ActiveEffect): boolean {
- const definition = bucketDefinition(effect.intent.type);
- return isStaticProfileBucket(effect.intent.type) || (definition !== undefined && isStaticProfileBucket(definition.path));
-}
-
-function compactCandidates(candidates: IndexedBucketEffect[], isActive: (effect: ActiveEffect, candidate: IndexedBucketEffect) => boolean): void {
+function compactCandidates(candidates: IndexedBucketEffect[], isActive: (effect: ActiveEffect) => boolean): void {
let write = 0;
for (let read = 0; read < candidates.length; read += 1) {
const candidate = candidates[read];
- if (!isActive(candidate.effect, candidate)) continue;
+ if (!isActive(candidate.effect)) continue;
candidates[write] = candidate;
write += 1;
}
diff --git a/simulator/src/recorder.ts b/simulator/src/recorder.ts
--- a/simulator/src/recorder.ts
+++ b/simulator/src/recorder.ts
@@ -106,9 +106,6 @@
appliedEffectIds: result.appliedEffectIds ?? [],
appliedEffects: result.appliedEffects ?? [],
consumedEffectIds: result.consumedEffectIds,
- consumedEffectUseKey: result.consumedEffectUseKey,
- consumedEffectUseId: result.consumedEffectUseId,
- consumedEffectUseIds: result.consumedEffectUseIds,
trace: result.trace
});
}
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
@@ -647,10 +647,13 @@
{ mode: "trace" }
);
+ const roundTwoNormalOutcome = result.attacks.find((attack) => attack.kind === "normal" && attack.jobId.startsWith("r2:attacker:infantry"));
const roundTwoSkillOutcome = result.attacks.find((attack) => attack.kind === "skill" && attack.jobId.startsWith("r2:attacker:infantry"));
+ assert.notEqual(roundTwoNormalOutcome, undefined);
+ assert.equal(roundTwoNormalOutcome!.consumedEffectIds.length, 2);
assert.notEqual(roundTwoSkillOutcome, undefined);
- assert.equal(roundTwoSkillOutcome!.consumedEffectIds.length, 2);
+ assert.equal(roundTwoSkillOutcome!.consumedEffectIds.length, 0);
});
test("requires_effect is ignored by native simulator effect activation", () => {
diff --git a/simulator/src/simulator.ts b/simulator/src/simulator.ts
--- a/simulator/src/simulator.ts
+++ b/simulator/src/simulator.ts
@@ -35,7 +35,7 @@
type Rng
} from "./effects";
import { classifyEffectForJob } from "./classifier";
-import { createEffectIndex, indexEffect, pruneEffectIndex, removeStaticProfileBucketEffects, type EffectIndex } from "./effectIndex";
+import { bucketCandidatesForJob, createEffectIndex, indexEffect, pruneEffectIndex, removeStaticProfileBucketEffects, type EffectIndex } from "./effectIndex";
import { normalizeUnitType } from "./normalize";
import { emptyTroops, resolveFighter } from "./resolve";
import { buildStaticDamageProfile, type StaticDamageProfile } from "./staticDamageProfile";
@@ -49,6 +49,7 @@
interface Runtime {
activeEffects: ActiveEffect[];
effectIndex: EffectIndex;
+ effectById: Map<string, ActiveEffect>;
staticDamageProfile?: StaticDamageProfile;
damageScratch: DamageScratch;
rng: Rng;
@@ -57,7 +58,6 @@
effectActivationCounts: Record<SideId, number>;
extraSkillAttackJobsByEffect: Record<string, number>;
attackControlCounts: { dodge: number; no_attack: number };
- consumedEffectUseKeys: Set<string>;
counters: {
attacks: Record<SideId, Record<UnitType, number>>;
received: Record<SideId, Record<UnitType, number>>;
@@ -78,6 +78,11 @@
effects: ActiveEffect[];
}
+interface ExtraSkillJobsResult {
+ jobs: DamageJob[];
+ consumedEffectIds: string[];
+}
+
interface BattleRun {
fighters: Record<SideId, ResolvedFighter>;
runtime: Runtime;
@@ -217,6 +222,7 @@
return {
activeEffects,
effectIndex,
+ effectById: new Map(activeEffects.map((e) => [e.id, e])),
staticDamageProfile: template.staticDamageProfile,
damageScratch: createFastDamageScratch(),
rng,
@@ -225,7 +231,6 @@
effectActivationCounts: { ...template.effectActivationCounts },
extraSkillAttackJobsByEffect: { ...template.extraSkillAttackJobsByEffect },
attackControlCounts: { ...template.attackControlCounts },
- consumedEffectUseKeys: new Set(),
counters: {
attacks: { attacker: { ...template.counters.attacks.attacker }, defender: { ...template.counters.attacks.defender } },
received: { attacker: { ...template.counters.received.attacker }, defender: { ...template.counters.received.defender } }
@@ -305,9 +310,13 @@
triggerRoundStartSkills(round, runtime, roundStartTroops);
const intents = resolveAttackIntents(round, runtime, roundStartTroops);
- const jobs: DamageJob[] = [];
+ const allJobs: DamageJob[] = []; // for recorder
+ const results: DamageJobResult[] = [];
const cancelled: CancelledAttack[] = [];
+ // Phase 1: fire all attack_declared triggers — all ActiveEffects are
+ // resolved before any damage is calculated, per the battle-core spec.
+ const pendingNormalJobs: DamageJob[] = [];
for (const intent of intents) {
triggerSkills("attack_declared", round, runtime.skills.attackDeclared, runtime, intent);
const job = normalJob(intent, roundStartTroops);
@@ -315,33 +324,54 @@
if (controls.no_attack || controls.dodge) {
const control = controls.no_attack ?? controls.dodge!;
runtime.attackControlCounts[control.reason] += 1;
- const consumedEffectIds = attackDurationEffectIdsForJob(job, round, runtime.activeEffects);
+ const consumedEffectIds = attackDurationEffectIdsForJob(job, runtime.effectIndex);
cancelled.push({ intent, effectId: control.effect.id, reason: control.reason, consumedEffectIds });
consumeEffects(runtime, consumedEffectIds);
} else {
- jobs.push(job);
- jobs.push(...extraSkillJobs(job, round, runtime, roundStartTroops));
+ pendingNormalJobs.push(job);
}
}
- const results: DamageJobResult[] = [];
- for (const job of jobs) {
- const result = calculateDamageJob(job, fighters, runtime.activeEffects, {
+ // Phase 2: calculate each normal job; any extra_skill_attack effect it
+ // uses spawns extra DamageJobs that are calculated immediately after.
+ // consumeEffects runs after each job so subsequent normal jobs see the
+ // correct uses count on any shared extra_skill_attack effects.
+ for (const job of pendingNormalJobs) {
+ allJobs.push(job);
+ const normalResult = calculateDamageJob(job, fighters, runtime.activeEffects, {
trace: recorder.capturesTrace,
effectIndex: runtime.effectIndex,
staticDamageProfile: runtime.staticDamageProfile,
scratch: recorder.capturesTrace ? undefined : runtime.damageScratch,
capToDefenderTroops: loopOptions.capJobKills
});
- results.push({ job, result });
- if (
- loopOptions.scoreSide &&
- job.attackerSide === loopOptions.scoreSide.attackerSide &&
- job.defenderSide === loopOptions.scoreSide.defenderSide
- ) {
- score += result.kills;
+ results.push({ job, result: normalResult });
+ if (loopOptions.scoreSide && job.attackerSide === loopOptions.scoreSide.attackerSide && job.defenderSide === loopOptions.scoreSide.defenderSide) {
+ score += normalResult.kills;
+ }
+ consumeEffects(runtime, normalResult.consumedEffectIds);
+
+ const extraSkill = extraSkillJobs(job, round, runtime, roundStartTroops);
+ if (extraSkill.consumedEffectIds.length > 0) {
+ normalResult.consumedEffectIds = [...normalResult.consumedEffectIds, ...extraSkill.consumedEffectIds];
+ consumeEffects(runtime, extraSkill.consumedEffectIds);
+ }
+
+ for (const extraJob of extraSkill.jobs) {
+ allJobs.push(extraJob);
+ const extraResult = calculateDamageJob(extraJob, fighters, runtime.activeEffects, {
+ trace: recorder.capturesTrace,
+ effectIndex: runtime.effectIndex,
+ staticDamageProfile: runtime.staticDamageProfile,
+ scratch: recorder.capturesTrace ? undefined : runtime.damageScratch,
+ capToDefenderTroops: loopOptions.capJobKills
+ });
+ results.push({ job: extraJob, result: extraResult });
+ if (loopOptions.scoreSide && extraJob.attackerSide === loopOptions.scoreSide.attackerSide && extraJob.defenderSide === loopOptions.scoreSide.defenderSide) {
+ score += extraResult.kills;
+ }
+ consumeEffects(runtime, extraResult.consumedEffectIds);
}
- consumeEffects(runtime, result.consumedEffectIds, result.consumedEffectUseKey, result.consumedEffectUseId, result.consumedEffectUseIds);
}
if (loopOptions.capRoundKills) capRoundKills(results, roundStartTroops);
@@ -350,7 +380,7 @@
for (const entry of cancelled) recorder.recordCancelled(entry.intent, entry.effectId, entry.reason, entry.consumedEffectIds);
for (const entry of results) recorder.recordDamageJob(entry.job, entry.result);
- recorder.recordRound(round, roundStartTroops, intents, jobs);
+ recorder.recordRound(round, roundStartTroops, intents, allJobs);
}
const winner = winnerFor(fighters) ?? "draw";
@@ -499,6 +529,7 @@
return {
activeEffects: [],
effectIndex: createEffectIndex(),
+ effectById: new Map(),
staticDamageProfile: undefined,
damageScratch: createFastDamageScratch(),
rng,
@@ -507,7 +538,6 @@
effectActivationCounts: { attacker: 0, defender: 0 },
extraSkillAttackJobsByEffect: {},
attackControlCounts: { dodge: 0, no_attack: 0 },
- consumedEffectUseKeys: new Set(),
counters: {
attacks: { attacker: emptyTroops(), defender: emptyTroops() },
received: { attacker: emptyTroops(), defender: emptyTroops() }
@@ -532,9 +562,23 @@
function addActiveEffect(runtime: Runtime, effect: ActiveEffect): void {
runtime.activeEffects.push(effect);
+ runtime.effectById.set(effect.id, effect);
indexEffect(runtime.effectIndex, effect);
}
+function expireInactive(runtime: Runtime, round: number): void {
+ let write = 0;
+ for (let read = 0; read < runtime.activeEffects.length; read += 1) {
+ const effect = runtime.activeEffects[read];
+ if (isEffectActive(effect, round)) {
+ runtime.activeEffects[write] = effect;
+ write += 1;
+ }
+ }
+ runtime.activeEffects.length = write;
+ pruneEffectIndex(runtime.effectIndex, (effect) => isEffectActive(effect, round));
+}
+
function addInputPassiveEffects(runtime: Runtime, passive: FighterInput["passive"], side: SideId): void {
if (!passive) return;
for (const stat of ["attack", "defense", "lethality", "health"] as const) {
@@ -697,8 +741,9 @@
round: number,
runtime: Runtime,
roundStartTroops: DamageJob["roundStartTroops"]
-): DamageJob[] {
+): ExtraSkillJobsResult {
const jobs: DamageJob[] = [];
+ const consumedEffectIds: string[] = [];
const effectGroups = selectStackedExtraAttackEffectGroups(
runtime.effectIndex.extraAttacks.filter((effect) => isEffectActive(effect, round) && extraAttackEffectAppliesToNormalAttack(effect, normalAttack)),
round
@@ -706,8 +751,6 @@
for (const effectGroup of effectGroups) {
const effect = effectGroup.selected;
const definitions = effect.triggerDamageJobs ?? [];
- const consumedEffectIds = effectGroup.effects.map((groupEffect) => groupEffect.id);
- const consumedEffectUseKey = `${normalAttack.sourceIntentId}:${effect.stackingKey ?? effect.id}`;
const firstJobIndex = jobs.length;
let definitionIndex = 0;
for (const definition of definitions) {
@@ -733,11 +776,7 @@
defenderUnit: target.unit,
sourceEffectId,
sourceSkillReportKey,
- sourceMultiplier: multiplier,
- consumedEffectIds,
- consumedEffectUseKey,
- consumedEffectUseId: effect.id,
- consumedEffectUseIds: consumedEffectIds
+ sourceMultiplier: multiplier
});
runtime.extraSkillAttackJobsByEffect[sourceEffectId] = (runtime.extraSkillAttackJobsByEffect[sourceEffectId] ?? 0) + 1;
}
@@ -745,10 +784,10 @@
definitionIndex += 1;
}
if (jobs.length > firstJobIndex) {
- for (const consumedEffectId of consumedEffectIds) reserveConsumedEffectUse(runtime, consumedEffectUseKey, consumedEffectId);
+ consumedEffectIds.push(...effectGroup.effects.map((groupEffect) => groupEffect.id));
}
}
- return jobs;
+ return { jobs, consumedEffectIds };
}
function selectStackedExtraAttackEffectGroups(effects: ActiveEffect[], round: number): ExtraAttackEffectGroup[] {
@@ -887,55 +926,23 @@
}
}
-function expireInactive(runtime: Runtime, round: number): void {
- let write = 0;
- for (let read = 0; read < runtime.activeEffects.length; read += 1) {
- const effect = runtime.activeEffects[read];
- if (isEffectActive(effect, round)) {
- runtime.activeEffects[write] = effect;
- write += 1;
- }
+function attackDurationEffectIdsForJob(job: DamageJob, index: EffectIndex): string[] {
+ const ids: string[] = [];
+ for (const candidate of bucketCandidatesForJob(index, job)) {
+ if (candidate.effect.duration.type === "attack") ids.push(candidate.effect.id);
}
- runtime.activeEffects.length = write;
- pruneEffectIndex(runtime.effectIndex, (effect) => isEffectActive(effect, round));
-}
-
-function attackDurationEffectIdsForJob(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" || classification?.kind === "control";
- })
- .map((effect) => effect.id);
-}
-
-function consumeEffects(
- runtime: Runtime,
- consumedEffectIds: string[],
- consumedEffectUseKey?: string,
- consumedEffectUseId?: string,
- consumedEffectUseIds?: string[]
-): void {
- const dedupeIds = consumedEffectUseIds ?? (consumedEffectUseId ? [consumedEffectUseId] : undefined);
- for (const consumed of consumedEffectIds) {
- if (consumedEffectUseKey && dedupeIds?.includes(consumed)) {
- const useKey = `${consumedEffectUseKey}:${consumed}`;
- if (runtime.consumedEffectUseKeys.has(useKey)) continue;
- runtime.consumedEffectUseKeys.add(useKey);
- }
- for (const effect of runtime.activeEffects) {
- if (effect.id === consumed) effect.uses += 1;
- }
+ for (const effect of index.controls) {
+ if (effect.duration.type !== "attack") continue;
+ const classification = classifyEffectForJob(effect, job);
+ if (classification?.kind === "control") ids.push(effect.id);
}
+ return ids;
}
-function reserveConsumedEffectUse(runtime: Runtime, consumedEffectUseKey: string, consumedEffectId: string): void {
- const useKey = `${consumedEffectUseKey}:${consumedEffectId}`;
- if (runtime.consumedEffectUseKeys.has(useKey)) return;
- runtime.consumedEffectUseKeys.add(useKey);
- for (const effect of runtime.activeEffects) {
- if (effect.id === consumedEffectId) effect.uses += 1;
+function consumeEffects(runtime: Runtime, consumedEffectIds: string[]): void {
+ for (const consumed of consumedEffectIds) {
+ const effect = runtime.effectById.get(consumed);
+ if (effect) effect.uses += 1;
}
}
diff --git a/simulator/src/types.ts b/simulator/src/types.ts
--- a/simulator/src/types.ts
+++ b/simulator/src/types.ts
@@ -278,10 +278,6 @@
sourceEffectId?: string;
sourceSkillReportKey?: string;
sourceMultiplier?: number;
- consumedEffectIds?: string[];
- consumedEffectUseKey?: string;
- consumedEffectUseId?: string;
- consumedEffectUseIds?: string[];
}
export interface CounterDelta {
@@ -344,9 +340,6 @@
appliedEffectIds: string[];
appliedEffects: AppliedEffectTrace[];
consumedEffectIds: string[];
- consumedEffectUseKey?: string;
- consumedEffectUseId?: string;
- consumedEffectUseIds?: string[];
cancelledBy?: string;
cancelReason?: "dodge" | "no_attack";
trace?: DamageEquationTrace;

Accuracy Results

201 / 201
FileCase#AdjS nG nS μG μBias%tqPW
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_90115302.05302.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_10111-1618.0-1618.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_11211-376.0-376.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_12311-6.0-6.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_134111462.01462.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_14511525.0525.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_156+0.013%11-254.0-254.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_167+0.008%111425.01425.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_178-0.003%11986.0986.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_189+0.017%11-177.0-177.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_191011-489.0-489.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_2011-0.006%11712.0712.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_2112+0.013%11-193.0-193.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_2213111309.01309.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_231411-277.0-277.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_2415-0.008%11585.0585.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_251611-45.0-45.00.00%P
testcases/1-testcases_no-heroes_t6_single-type_nc.jsondaut_viper_2617-0.002%11798.0798.00.00%P
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_10+0.05%11735.0741.0-0.32%F
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_21+0.05%11-62.0-61.0-0.06%P
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_32+0.05%11771.0778.0-0.38%F
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_43+0.05%11345.0353.0-0.45%F
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_54+0.043%11264.0264.00.00%P
testcases/2-testcases_no-heroes_t6_mixed_nc.jsondaut_viper_65+0.05%11-90.0-89.0-0.07%P
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_10+0.05%11345.0342.00.19%F
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_211001-3729.0-3729.00.00%P
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_321001-2912.3-2823.0-0.42%-3.470.015P
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_431001-516.1-519.00.02%0.400.89P
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_5410019362.09282.00.64%1.860.53P
testcases/3-testcases_mixed-heroes-not-verified.jsondaut_viper_751001-2273.1-2295.00.16%2.400.21P
testcases/4-testcases_no-heroes_infantry_fc5.jsonNitro_Beast_30_1010013-45545.1-45251.0-0.22%-0.580.88P
testcases/emulator_verified/ahmose_solo_nc.jsonahmose_solo0-0.05%112636.02633.00.06%F
testcases/emulator_verified/alonso_attacker_600_all.jsonalonso_attacker_600_all01006-256.4-253.0-0.14%-0.750.88P
testcases/emulator_verified/alonso_attacker_900_inf_lancer.jsonalonso_attacker_900_inf_lancer01004-463.5-464.50.04%1.820.53P
testcases/emulator_verified/alonso_attacker_900_inf_lancer.jsonalonso_attacker_900_inf_lancer11002-459.6-460.00.02%0.480.88P
testcases/emulator_verified/alonso_minxxx_defender.jsonalonso_minxxx_defender010081453.41450.40.14%0.460.88P
testcases/emulator_verified/alonso_norah_combo.jsonalonso_norah_combo01002-393.4-393.0-0.02%-0.190.97P
testcases/emulator_verified/alonso_norah_combo.jsonalonso_norah_combo11001-353.4-349.0-0.26%-1.650.53P
testcases/emulator_verified/alonso_norah_combo.jsonalonso_norah_combo21002-393.1-390.0-0.19%-1.450.58P
testcases/emulator_verified/alonso_solo.jsonalonso_solo0100101243.41104.82.97%1.200.70P
testcases/emulator_verified/alonso_solo_v2.jsonalonso_solo_v201006-493.2-513.71.58%0.870.84P
testcases/emulator_verified/alonso_solo_v2.jsonalonso_solo_v2110019-10.010.6-1.59%-1.060.78P
testcases/emulator_verified/alonso_vs_alonso.jsonalonso_vs_alonso010081551.01554.6-0.17%-0.660.88P
testcases/emulator_verified/alonso_vs_alonso_inf_lancer.jsonalonso_vs_alonso_inf_lancer01008-751.5-749.0-0.04%-0.470.88P
testcases/emulator_verified/alonso_wip_attacker.jsonalonso_wip_attacker010081768.21768.9-0.03%-0.400.89P
testcases/emulator_verified/ambusher_no_marksmen.jsonambusher_no_marksmen01001-101271.4-105686.01.45%1.580.53P
testcases/emulator_verified/attdef_jasser_patrick_minxxx_attacks_nc.jsonattdef_jasser_patrick_minxxx_attacks0+0.05%13-807.0-807.00.00%P
testcases/emulator_verified/attdef_jasser_patrick_nc.jsonattdef_jasser_patrick013-1138.0-1138.00.00%P
testcases/emulator_verified/attdef_jasser_patrick_swap_nc.jsonattdef_jasser_patrick_swap0131088.01088.00.00%P
testcases/emulator_verified/attdef_jasser_patrick_wip_attacks_nc.jsonattdef_jasser_patrick_wip_attacks0-0.05%11807.0807.00.00%P
testcases/emulator_verified/attdef_jasser_patrick_wip_attacks_nc.jsonattdef_jasser_patrick_wip_attacks1-0.05%12807.0807.00.00%P
testcases/emulator_verified/attdef_test_minxxx_attacks_nc.jsonattdef_test_minxxx_attacks_nc0-0.05%111133.01131.00.08%F
testcases/emulator_verified/attdef_test_wip_attacks_nc.jsonattdef_test_wip_attacks_nc012-1138.0-1138.00.00%P
testcases/emulator_verified/bahiti_norah_combo_nc.jsonbahiti_norah_combo_nc01001193.4269.0-3.15%-0.550.88P
testcases/emulator_verified/bahiti_solo.jsonbahiti_solo010022221.12206.00.32%0.390.89P
testcases/emulator_verified/bahiti_solo_nc.jsonbahiti_solo_nc010013677.03675.00.04%0.380.89P
testcases/emulator_verified/determinism_test_normal_nc.jsondeterminism_test_normal014852.0852.00.00%P
testcases/emulator_verified/determinism_test_swapped_nc.jsondeterminism_test_swapped011-852.0-852.00.00%P
testcases/emulator_verified/flint_reina_zinman_combo.jsonflint_reina_zinman_combo01001-445.1-439.0-0.15%-0.990.80P
testcases/emulator_verified/flint_solo_nc.jsonflint_solo0+0.05%112861.02862.0-0.02%P
testcases/emulator_verified/flint_zinman_combo_nc.jsonflint_zinman_combo_nc0111005.01005.00.00%P
testcases/emulator_verified/greg_mia_combo.jsongreg_mia_combo010042416.22508.3-1.97%-1.610.53P
testcases/emulator_verified/greg_mia_combo.jsongreg_mia_combo1-0.05%10053482.43432.81.06%5.570.0F
testcases/emulator_verified/greg_mia_defender_current.jsongreg_mia_defender_current0100183347.63351.6-0.09%-0.700.88P
testcases/emulator_verified/greg_mia_nohero_control_current.jsongreg_mia_nohero_control_current0133752.03752.00.00%P
testcases/emulator_verified/greg_mia_nohero_control_current.jsongreg_mia_nohero_control_current1+0.05%133652.03652.00.00%P
testcases/emulator_verified/greg_only_defender_current.jsongreg_only_defender_current010063685.93685.80.00%0.030.99P
testcases/emulator_verified/greg_only_defender_current.jsongreg_only_defender_current110023585.43585.00.01%0.120.97P
testcases/emulator_verified/greg_only_defender_current.jsongreg_only_defender_current2100203593.83594.6-0.02%-0.630.88P
testcases/emulator_verified/greg_solo.jsongreg_solo010012704.62697.00.16%0.590.88P
testcases/emulator_verified/gwen_norah_combo_nc.jsongwen_norah_combo_nc010012639.92668.0-0.60%-0.460.88P
testcases/emulator_verified/gwen_solo_nc.jsongwen_solo0+0.05%152969.03006.0-0.79%F
testcases/emulator_verified/hector_patrick_combo.jsonhector_patrick_combo01001-590.7-590.0-0.05%-0.970.80P
testcases/emulator_verified/hector_renee_wayne.jsonhector_renee_wayne010011587.91597.0-0.45%-2.940.072P
testcases/emulator_verified/hector_solo.jsonhector_solo01005-388.8-394.40.12%0.510.88P
testcases/emulator_verified/jasser_solo_nc.jsonjasser_solo0+0.05%112740.02740.00.00%P
testcases/emulator_verified/jessie_solo_nc.jsonjessie_solo0-0.05%1379.079.00.00%P
testcases/emulator_verified/ling_xue_solo_nc.jsonling_xue_solo0+0.05%112822.02822.00.00%P
testcases/emulator_verified/ling_xue_solo_nc.jsonling_xue_solo1+0.05%113214.03215.0-0.02%P
testcases/emulator_verified/logan_bahiti_combo_nc.jsonlogan_bahiti_combo_nc01001-507.1-517.00.55%0.800.88P
testcases/emulator_verified/logan_gwen_combo_nc.jsonlogan_gwen_combo_nc0+0.05%11-409.0-403.0-0.25%F
testcases/emulator_verified/logan_lynn_combo.jsonlogan_lynn_combo01001-551.1-553.00.08%1.130.73P
testcases/emulator_verified/logan_lynn_combo.jsonlogan_lynn_combo11002551.1550.50.03%0.510.88P
testcases/emulator_verified/logan_lynn_combo.jsonlogan_lynn_combo21002551.0551.00.00%0.010.99P
testcases/emulator_verified/logan_reina_bahiti_combo_nc.jsonlogan_reina_bahiti_combo_nc01001-495.2-482.0-0.60%-0.890.84P
testcases/emulator_verified/logan_solo_minxxx_nc.jsonlogan_solo_minxxx011-1995.0-1995.00.00%P
testcases/emulator_verified/logan_solo_v2_nc.jsonlogan_solo_v2011-1134.0-1134.00.00%P
testcases/emulator_verified/lumak_bokan_solo_nc.jsonlumak_bokan_solo0+0.05%112797.02797.00.00%P
testcases/emulator_verified/lumak_bokan_solo_nc.jsonlumak_bokan_solo1+0.05%113127.03127.00.00%P
testcases/emulator_verified/lynn_solo.jsonlynn_solo01005-185.1-181.2-0.08%-0.440.88P
testcases/emulator_verified/mia_only_defender_current.jsonmia_only_defender_current0100103540.83543.9-0.07%-0.630.88P
testcases/emulator_verified/mia_only_defender_current.jsonmia_only_defender_current1100203439.73449.2-0.21%-2.460.21P
testcases/emulator_verified/mia_solo.jsonmia_solo01001-269.3-260.0-0.27%-0.970.80P
testcases/emulator_verified/mia_solo.jsonmia_solo110011297.91293.00.22%0.590.88P
testcases/emulator_verified/mia_solo.jsonmia_solo21001-185.0-180.0-0.32%-1.010.80P
testcases/emulator_verified/mia_solo.jsonmia_solo31001879.6879.00.04%0.140.97P
testcases/emulator_verified/mia_solo.jsonmia_solo41001969.9967.00.21%0.650.88P
testcases/emulator_verified/molly_lynn_combo.jsonmolly_lynn_combo01005-402.8-402.80.00%0.040.99P
testcases/emulator_verified/molly_solo.jsonmolly_solo01005244.0244.8-0.07%-1.670.53P
testcases/emulator_verified/natalia_solo.jsonnatalia_solo0+0.05%10012614.52664.0-1.06%-2.390.21F
testcases/emulator_verified/norah_greg_combo.jsonnorah_greg_combo01005-366.7-366.0-0.04%-0.700.88P
testcases/emulator_verified/norah_hector_zinman_combo.jsonnorah_hector_zinman_combo01005-269.3-270.40.07%1.500.58P
testcases/emulator_verified/norah_s2_inf_only_A.jsonnorah_s2_inf_only_A0+0.05%100492.798.5-0.35%-1.190.70P
testcases/emulator_verified/norah_s2_inf_only_B_nc.jsonnorah_s2_inf_only_B0+0.05%13507.0507.00.00%P
testcases/emulator_verified/norah_s2_splash_A.jsonnorah_s2_splash_A010011785.21780.00.23%0.350.90P
testcases/emulator_verified/norah_s2_splash_B_nc.jsonnorah_s2_splash_B0131900.01900.00.00%P
testcases/emulator_verified/norah_s2_splash_C_nc.jsonnorah_s2_splash_C0131153.01153.00.00%P
testcases/emulator_verified/norah_s2_splash_D.jsonnorah_s2_splash_D01003829.5827.30.12%0.720.88P
testcases/emulator_verified/norah_solo.jsonnorah_solo010072740.12753.1-0.28%-0.710.88P
testcases/emulator_verified/norah_zinman_combo_nc.jsonnorah_zinman_combo_nc01001676.2670.00.26%0.100.97P
testcases/emulator_verified/patrick_solo_nc.jsonpatrick_solo0+0.001%112581.02582.0-0.02%P
testcases/emulator_verified/philly_bahiti_combo.jsonphilly_bahiti_combo01001-558.5-556.0-0.14%-1.310.68P
testcases/emulator_verified/philly_bahiti_combo.jsonphilly_bahiti_combo11004-454.3-458.30.22%1.600.53P
testcases/emulator_verified/philly_solo.jsonphilly_solo010022262.32253.00.20%0.160.97P
testcases/emulator_verified/philly_solo.jsonphilly_solo110033620.83619.70.02%0.150.97P
testcases/emulator_verified/reina_attacker_wip.jsonreina_attacker_wip01001-1991.7-1992.00.01%0.520.88P
testcases/emulator_verified/reina_bahiti_combo_nc.jsonreina_bahiti_combo_nc01001-483.1-502.01.05%1.160.71P
testcases/emulator_verified/reina_logan_combo_simulator.jsonreina_logan_combo_simulator01001-549.5-553.00.14%1.280.68P
testcases/emulator_verified/reina_solo_wip.jsonreina_solo_wip010011991.61992.0-0.02%-0.590.88P
testcases/emulator_verified/renee_solo_nc.jsonrenee_solo_nc015-554.0-554.00.00%P
testcases/emulator_verified/seo_yoon_solo_nc.jsonseo_yoon_solo0-0.05%13-619.0-620.00.10%P
testcases/emulator_verified/sergey_norah_zinman_combo_nc.jsonsergey_norah_zinman_combo_nc010011324.51372.0-1.58%-0.710.88P
testcases/emulator_verified/sergey_reina_combo_nc.jsonsergey_reina_combo_nc01001-303.3-295.0-0.35%-0.290.93P
testcases/emulator_verified/sergey_solo_nc.jsonsergey_solo0-0.05%121348.01348.00.00%P
testcases/emulator_verified/simple_001_nc.jsonsimple_00101001-186.0-186.00.00%P
testcases/emulator_verified/troop_check_nc.jsontroop_check011-279.0-279.00.00%P
testcases/emulator_verified/type_triangle_A_nc.jsontype_triangle_A_nc0+0.05%11-895.0-895.00.00%P
testcases/emulator_verified/type_triangle_B_nc.jsontype_triangle_B_nc011-929.0-929.00.00%P
testcases/emulator_verified/wayne_mixed_solo.jsonwayne_mixed_solo01004232.9237.0-0.23%-0.110.97P
testcases/emulator_verified/wayne_mixed_solo.jsonwayne_mixed_solo11007310.4346.6-2.01%-1.770.53P
testcases/emulator_verified/wayne_reina_combo.jsonwayne_reina_combo010081550.41559.4-0.37%-1.710.53P
testcases/emulator_verified/wayne_reina_wuming_combo.jsonwayne_reina_wuming_combo010081698.21694.00.18%1.990.45P
testcases/emulator_verified/wayne_s1_solo.jsonwayne_s1_solo01005254.0251.80.36%1.300.68P
testcases/emulator_verified/wayne_s2_solo.jsonwayne_s2_solo01002363.7361.50.43%1.250.68P
testcases/emulator_verified/wos425_wip_nohero_control_nc.jsonwos425_wip_nohero_control_nc011774.0774.00.00%P
testcases/emulator_verified/wos425_wip_patrick_health_nc.jsonwos425_wip_patrick_health_nc011762.0762.00.00%P
testcases/emulator_verified/wos425_wip_philly_defense.jsonwos425_wip_philly_defense01005768.4768.6-0.02%-0.590.88P
testcases/emulator_verified/wos425_wip_sergey_damage_taken_nc.jsonwos425_wip_sergey_damage_taken_nc011749.0749.00.00%P
testcases/emulator_verified/wos425_wip_sergey_patrick_combo_nc.jsonwos425_wip_sergey_patrick_combo_nc011726.0726.00.00%P
testcases/emulator_verified/wos425_wip_sergey_philly_combo.jsonwos425_wip_sergey_philly_combo01005737.8738.2-0.04%-0.880.84P
testcases/emulator_verified/wos427_wip_sergey_philly_840.jsonwos427_wip_sergey_philly_840010048.522.5-0.85%-0.920.84P
testcases/emulator_verified/wos444_bradley_s3_all_troops_damage_control_nc.jsonwos444_bradley_s3_all_troops_damage_control_nc0+0.05%115764.05848.0-0.82%F
testcases/emulator_verified/wos444_bradley_s3_all_troops_damage_control_nc.jsonwos444_bradley_s3_all_troops_damage_control_nc1115848.05848.00.00%P
testcases/emulator_verified/wos444_bradley_s3_all_troops_damage_control_nc.jsonwos444_bradley_s3_all_troops_damage_control_nc2117010.07010.00.00%P
testcases/emulator_verified/wos444_bradley_s3_all_troops_damage_hero_nc.jsonwos444_bradley_s3_all_troops_damage_hero_nc0118117.08117.00.00%P
testcases/emulator_verified/wos444_edith_s11_mark_damage_taken_control_nc.jsonwos444_edith_s11_mark_damage_taken_control_nc0122578.02578.00.00%P
testcases/emulator_verified/wos444_edith_s11_mark_damage_taken_control_nc.jsonwos444_edith_s11_mark_damage_taken_control_nc1112106.02106.00.00%P
testcases/emulator_verified/wos444_edith_s11_mark_damage_taken_hero_nc.jsonwos444_edith_s11_mark_damage_taken_hero_nc0111863.01863.00.00%P
testcases/emulator_verified/wos444_edith_s12_lancer_damage_dealt_control_nc.jsonwos444_edith_s12_lancer_damage_dealt_control_nc0112332.02332.00.00%P
testcases/emulator_verified/wos444_edith_s12_lancer_damage_dealt_hero_nc.jsonwos444_edith_s12_lancer_damage_dealt_hero_nc0111909.01909.00.00%P
testcases/emulator_verified/wos444_gordon_s22_all_enemy_damage_dealt_control_nc.jsonwos444_gordon_s22_all_enemy_damage_dealt_control_nc0117656.07656.00.00%P
testcases/emulator_verified/wos444_gordon_s22_all_enemy_damage_dealt_control_nc.jsonwos444_gordon_s22_all_enemy_damage_dealt_control_nc1+0.05%117612.07656.0-0.41%F
testcases/emulator_verified/wos444_gordon_s22_all_enemy_damage_dealt_control_nc.jsonwos444_gordon_s22_all_enemy_damage_dealt_control_nc2127656.07656.00.00%P
testcases/emulator_verified/wos444_gordon_s22_all_enemy_damage_dealt_hero_nc.jsonwos444_gordon_s22_all_enemy_damage_dealt_hero_nc010055076.85065.40.11%0.130.97P
testcases/emulator_verified/wos451_bradley_s21_lancer_damage_up_bucket.jsonwos451_bradley_s21_lancer_damage_up_bucket0112736.02736.00.00%P
testcases/emulator_verified/wos451_bradley_s21_lancer_damage_up_control.jsonwos451_bradley_s21_lancer_damage_up_control0-0.025%111504.01504.00.00%P
testcases/emulator_verified/wos451_bradley_s22_infantry_damage_up_bucket.jsonwos451_bradley_s22_infantry_damage_up_bucket0-0.05%112747.02746.00.03%P
testcases/emulator_verified/wos451_bradley_s22_infantry_damage_up_control.jsonwos451_bradley_s22_infantry_damage_up_control0111623.01623.00.00%P
testcases/emulator_verified/wos451_bradley_s3_all_damage_up_bucket.jsonwos451_bradley_s3_all_damage_up_bucket0116151.06151.00.00%P
testcases/emulator_verified/wos451_bradley_s3_all_damage_up_control.jsonwos451_bradley_s3_all_damage_up_control0114903.04903.00.00%P
testcases/emulator_verified/wos451_gordon_s11_lancer_damage_up_bucket.jsonwos451_gordon_s11_lancer_damage_up_bucket0+0.05%11-1540.0-1517.0-0.66%F
testcases/emulator_verified/wos451_gordon_s11_lancer_damage_up_control.jsonwos451_gordon_s11_lancer_damage_up_control0+0.05%11-2739.0-2739.00.00%P
testcases/emulator_verified/wos451_gordon_s21_lancer_damage_up_bucket.jsonwos451_gordon_s21_lancer_damage_up_bucket0+0.05%11-1034.0-1017.0-0.69%F
testcases/emulator_verified/wos451_gordon_s21_lancer_damage_up_control.jsonwos451_gordon_s21_lancer_damage_up_control011-1917.0-1917.00.00%P
testcases/emulator_verified/wu_ming_solo_nc.jsonwu_ming_solo0111392.01392.00.00%P
testcases/emulator_verified/zinman_solo_nc.jsonzinman_solo0+0.05%113177.03177.00.00%P
testcases/heroes_unittests/Flint_tc.jsondaut_viper_10+0.05%15-151.0-149.8-0.05%F
testcases/heroes_unittests/Flint_tc.jsondaut_viper_21-0.05%165752.05732.70.27%F
testcases/heroes_unittests/Hector_tc.jsondaut_viper_101001-441.6-441.00.00%-0.130.97P
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_10+0.05%11-62.0-61.0-0.06%P
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_21+0.05%11-90.0-89.0-0.07%P
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_32-0.025%11481.0481.00.00%P
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_43-0.017%11217.0217.00.00%P
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_5411578.0578.00.00%P
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_65+0.05%11-97.0-96.0-0.06%P
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_76+0.05%11-75.0-74.0-0.05%P
testcases/heroes_unittests/Jessie_tc_nc.jsondaut_viper_8711-988.0-988.00.00%P
testcases/heroes_unittests/Mia_tc.jsondaut_viper_101001-173.5-181.00.17%1.570.53P
testcases/heroes_unittests/Mia_tc.jsondaut_viper_21+0.05%10071591.81625.1-0.77%-2.250.27F
testcases/heroes_unittests/Mia_tc.jsondaut_viper_3210061122.01124.2-0.10%-1.480.58P
testcases/heroes_unittests/Mia_tc.jsondaut_viper_431001668.1671.0-0.13%-0.310.93P
testcases/heroes_unittests/Mia_tc.jsondaut_viper_541004-124.3-121.8-0.11%-1.250.68P
testcases/heroes_unittests/Molly_tc.jsondaut_viper_10+0.05%11-62.0-61.0-0.06%P
testcases/heroes_unittests/Molly_tc.jsondaut_viper_21+0.05%11-90.0-89.0-0.07%P
testcases/heroes_unittests/Molly_tc.jsondaut_viper_32-0.025%11481.0481.00.00%P
testcases/heroes_unittests/Molly_tc.jsondaut_viper_431001-63.5-68.00.33%3.730.0084P
testcases/heroes_unittests/Molly_tc.jsondaut_viper_541001-190.3-190.0-0.02%-0.210.97P
testcases/heroes_unittests/Reina_tc.jsondaut_viper_10+0.05%11-66.0-62.0-0.18%F
testcases/heroes_unittests/Reina_tc.jsondaut_viper_2111-40.0-40.00.00%P
testcases/heroes_unittests/Reina_tc.jsondaut_viper_4210061413.41428.3-0.46%-0.280.93P
testcases/heroes_unittests/Reina_tc.jsondaut_viper_531001-201.9-202.00.01%0.060.99P
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_10+0.05%11-62.0-61.0-0.06%P
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_61+0.05%11-90.0-89.0-0.07%P
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_32-0.025%11481.0481.00.00%P
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_43+0.05%114339.04341.0-0.04%F
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_54+0.05%113802.03805.0-0.06%F
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_65-0.006%11917.0915.00.04%F
testcases/heroes_unittests/Seo-yoon_tc_nc.jsondaut_viper_76-0.004%112130.02128.00.04%F
testcases/heroes_unittests/Zinman_tc.jsondaut_viper_10-0.05%11-122.0-122.00.00%P
testcases/heroes_unittests/Zinman_tc.jsondaut_viper_21-0.025%114018.04018.00.00%P
testcases/heroes_unittests/Zinman_tc.jsondaut_viper_3211-130.0-130.00.00%P