Release Healer Man 0.1.14
This commit is contained in:
1 parent
67d7a8ebe7
commit
7b41b102fa
147 files changed
+169892
-1342
No files matched your search
@@ -0,0 +1,99 @@
|
||||
import type { MobCombatState } from "./combatStore";
|
||||
import type { EnemyDamageSchool, EnemyTelegraphShape } from "./enemyAttacks";
|
||||
|
||||
export type EncounterTimelineKind = "casting" | "timer" | "health";
|
||||
|
||||
export interface EncounterTimelineRow {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly kind: EncounterTimelineKind;
|
||||
readonly value: string;
|
||||
readonly progress: number;
|
||||
readonly sortAt: number;
|
||||
readonly school: EnemyDamageSchool;
|
||||
readonly telegraphShape?: EnemyTelegraphShape;
|
||||
}
|
||||
|
||||
export interface EncounterTimelineModel {
|
||||
readonly bossId: string;
|
||||
readonly bossName: string;
|
||||
readonly rows: readonly EncounterTimelineRow[];
|
||||
}
|
||||
|
||||
function clampedProgress(value: number): number {
|
||||
return Math.min(1, Math.max(0, value));
|
||||
}
|
||||
|
||||
export function encounterTimeline(
|
||||
mobs: Readonly<Record<string, MobCombatState>>,
|
||||
now: number,
|
||||
limit = 4,
|
||||
): EncounterTimelineModel | null {
|
||||
const boss = Object.values(mobs)
|
||||
.filter((mob) => mob.boss && mob.engaged && !mob.dead && !mob.despawned)
|
||||
.sort((left, right) => (
|
||||
Number(Boolean(right.activeCast)) - Number(Boolean(left.activeCast))
|
||||
|| left.id.localeCompare(right.id)
|
||||
))[0];
|
||||
if (!boss) return null;
|
||||
|
||||
const healthPercent = (boss.health / Math.max(1, boss.maxHealth)) * 100;
|
||||
const rows = boss.attacks.flatMap<EncounterTimelineRow>((attack) => {
|
||||
if (attack.id === "basic-attack") return [];
|
||||
const castCount = boss.abilityCastCounts[attack.id] ?? 0;
|
||||
if (attack.maximumCasts && castCount >= attack.maximumCasts) return [];
|
||||
const common = {
|
||||
id: attack.id,
|
||||
name: attack.name,
|
||||
school: attack.school,
|
||||
...(attack.telegraph ? { telegraphShape: attack.telegraph.shape } : {}),
|
||||
};
|
||||
if (boss.activeCast?.attackId === attack.id) {
|
||||
const duration = Math.max(1, boss.activeCast.completesAt - boss.activeCast.startedAt);
|
||||
const remaining = Math.max(0, boss.activeCast.completesAt - now);
|
||||
return [{
|
||||
...common,
|
||||
kind: "casting",
|
||||
value: `${(remaining / 1_000).toFixed(1)}s`,
|
||||
progress: clampedProgress(remaining / duration),
|
||||
sortAt: -1,
|
||||
}];
|
||||
}
|
||||
if (attack.condition?.kind === "self-health-percent") {
|
||||
const threshold = attack.condition.maximum;
|
||||
return [{
|
||||
...common,
|
||||
kind: "health",
|
||||
value: `${Math.round(threshold)}% HP`,
|
||||
progress: clampedProgress(healthPercent / Math.max(1, threshold)),
|
||||
sortAt: 1_000_000 + (100 - threshold) * 1_000,
|
||||
}];
|
||||
}
|
||||
const readyAt = boss.abilityReadyAt[attack.id];
|
||||
if (!Number.isFinite(readyAt)) return [];
|
||||
const remaining = Math.max(0, (readyAt ?? now) - now);
|
||||
const cooldown = Math.max(
|
||||
1,
|
||||
attack.repeatCooldownMs?.[1] ?? attack.initialCooldownMs?.[1] ?? attack.cooldownMs,
|
||||
);
|
||||
return [{
|
||||
...common,
|
||||
kind: "timer",
|
||||
value: `${(remaining / 1_000).toFixed(1)}s`,
|
||||
progress: clampedProgress(remaining / cooldown),
|
||||
sortAt: remaining,
|
||||
}];
|
||||
}).sort((left, right) => left.sortAt - right.sortAt || left.id.localeCompare(right.id));
|
||||
|
||||
const alwaysVisibleHealthRows = rows.filter((row) => row.kind === "health");
|
||||
const countdownRows = rows
|
||||
.filter((row) => row.kind !== "health")
|
||||
.slice(0, Math.max(1, limit));
|
||||
const visibleRows = [...countdownRows, ...alwaysVisibleHealthRows];
|
||||
|
||||
return visibleRows.length ? {
|
||||
bossId: boss.id,
|
||||
bossName: boss.name,
|
||||
rows: visibleRows,
|
||||
} : null;
|
||||
}
|
||||
Reference in new issue
Block a user