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
+106
-2
@@ -17,6 +17,21 @@ export type EnemyDamageSchool =
|
||||
|
||||
export type EnemyControlMechanic = "fear" | "sleep" | "root" | "stun" | "silence";
|
||||
|
||||
export type EnemyTelegraphShape = "circle" | "cone" | "line";
|
||||
export type EnemyTelegraphAnchor = "source" | "target";
|
||||
|
||||
/** World-space warning geometry shared by simulation and Three presentation. */
|
||||
export interface EnemyAttackTelegraph {
|
||||
readonly shape: EnemyTelegraphShape;
|
||||
readonly anchor: EnemyTelegraphAnchor;
|
||||
/** Minimum warning time before the mechanic resolves. */
|
||||
readonly warningMs: number;
|
||||
readonly radius?: number;
|
||||
readonly length?: number;
|
||||
readonly width?: number;
|
||||
readonly angleDegrees?: number;
|
||||
}
|
||||
|
||||
export type EnemyAbilityEffect =
|
||||
| { readonly kind: "damage"; readonly multiplier: number }
|
||||
| { readonly kind: "heal"; readonly multiplier: number }
|
||||
@@ -26,6 +41,11 @@ export type EnemyAbilityEffect =
|
||||
readonly durationMs: number;
|
||||
readonly magnitude?: number;
|
||||
}
|
||||
| {
|
||||
readonly kind: "target-aura";
|
||||
readonly aura: string;
|
||||
readonly durationMs: number;
|
||||
}
|
||||
| {
|
||||
readonly kind: "control";
|
||||
readonly mechanic: EnemyControlMechanic;
|
||||
@@ -83,6 +103,7 @@ export interface EnemyAttackDefinition {
|
||||
readonly maximumCasts?: number;
|
||||
readonly damageMultiplier: number;
|
||||
readonly radius?: number;
|
||||
readonly telegraph?: EnemyAttackTelegraph;
|
||||
readonly effect?: EnemyAbilityEffect;
|
||||
readonly condition?: EnemyAbilityCondition;
|
||||
readonly source?: EnemyAbilitySource;
|
||||
@@ -96,6 +117,8 @@ export interface EnemyCombatDefinition {
|
||||
readonly moveSpeed?: number;
|
||||
readonly aggroRange?: number;
|
||||
readonly leashRange?: number;
|
||||
/** Stable special-attack ids available for future encounter loadout randomization. */
|
||||
readonly mechanicIds?: readonly string[];
|
||||
readonly attacks: readonly EnemyAttackDefinition[];
|
||||
}
|
||||
|
||||
@@ -125,14 +148,94 @@ function normalizeCooldownRange(
|
||||
return Object.freeze([Math.round(lower), Math.round(upper)]);
|
||||
}
|
||||
|
||||
function harmfulEnemyAttack(value: EnemyAttackDefinition): boolean {
|
||||
return !value.effect
|
||||
|| value.effect.kind === "damage"
|
||||
|| value.effect.kind === "control"
|
||||
|| value.effect.kind === "target-aura";
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy/imported encounters receive safe defaults while authored mechanics can
|
||||
* override every dimension in the modular mechanic catalog.
|
||||
*/
|
||||
export function inferEnemyAttackTelegraph(
|
||||
value: EnemyAttackDefinition,
|
||||
): EnemyAttackTelegraph | undefined {
|
||||
if (value.id === "basic-attack" || !harmfulEnemyAttack(value)) return undefined;
|
||||
const radius = value.radius ?? Math.max(3, value.range * 0.35);
|
||||
if (/\b(?:breath|cleave|frontal|sweep|cone)\b/i.test(value.name)) {
|
||||
return {
|
||||
shape: "cone",
|
||||
anchor: "source",
|
||||
warningMs: 1_650,
|
||||
length: Math.max(5.5, value.range),
|
||||
angleDegrees: /breath/i.test(value.name) ? 70 : 95,
|
||||
};
|
||||
}
|
||||
if (/\b(?:beam|charge|line|lance)\b/i.test(value.name)) {
|
||||
return {
|
||||
shape: "line",
|
||||
anchor: "source",
|
||||
warningMs: 1_650,
|
||||
length: Math.max(7, value.range),
|
||||
width: Math.max(2, radius * 0.45),
|
||||
};
|
||||
}
|
||||
if (value.delivery === "area" || value.radius !== undefined) {
|
||||
const targetAnchored = /\b(?:blizzard|rain|barrage|volley|eruption|ground|meteor)\b/i
|
||||
.test(value.name) && value.target !== "self";
|
||||
return {
|
||||
shape: "circle",
|
||||
anchor: targetAnchored ? "target" : "source",
|
||||
warningMs: 1_650,
|
||||
radius,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeTelegraph(value: EnemyAttackTelegraph): EnemyAttackTelegraph {
|
||||
const warningMs = Math.round(finiteAtLeast(value.warningMs, 1_650, 250));
|
||||
if (value.shape === "circle") {
|
||||
return Object.freeze({
|
||||
shape: value.shape,
|
||||
anchor: value.anchor,
|
||||
warningMs,
|
||||
radius: finiteAtLeast(value.radius, 4, 0.5),
|
||||
});
|
||||
}
|
||||
if (value.shape === "cone") {
|
||||
return Object.freeze({
|
||||
shape: value.shape,
|
||||
anchor: "source",
|
||||
warningMs,
|
||||
length: finiteAtLeast(value.length, 8, 1),
|
||||
angleDegrees: Math.min(180, finiteAtLeast(value.angleDegrees, 90, 5)),
|
||||
});
|
||||
}
|
||||
return Object.freeze({
|
||||
shape: value.shape,
|
||||
anchor: "source",
|
||||
warningMs,
|
||||
length: finiteAtLeast(value.length, 8, 1),
|
||||
width: finiteAtLeast(value.width, 3, 0.5),
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeEnemyAttack(
|
||||
value: EnemyAttackDefinition,
|
||||
fallback: EnemyAttackDefinition = DEFAULT_ENEMY_ATTACK,
|
||||
): EnemyAttackDefinition {
|
||||
const cooldownMs = Math.round(finiteAtLeast(value.cooldownMs, fallback.cooldownMs, 1));
|
||||
const telegraph = value.telegraph ?? inferEnemyAttackTelegraph(value);
|
||||
const inferredCastTimeMs = value.castTimeMs === undefined && value.animation === "cast"
|
||||
? (fallback.castTimeMs ?? 1_500)
|
||||
: (fallback.castTimeMs ?? 0);
|
||||
: telegraph?.warningMs ?? fallback.castTimeMs ?? 0;
|
||||
const castTimeMs = Math.max(
|
||||
Math.round(finiteAtLeast(value.castTimeMs, inferredCastTimeMs, 0)),
|
||||
telegraph?.warningMs ?? 0,
|
||||
);
|
||||
return Object.freeze({
|
||||
id: value.id.trim() || fallback.id,
|
||||
name: value.name.trim() || fallback.name,
|
||||
@@ -143,7 +246,7 @@ export function normalizeEnemyAttack(
|
||||
target: value.target,
|
||||
school: value.school,
|
||||
animation: value.animation,
|
||||
castTimeMs: Math.round(finiteAtLeast(value.castTimeMs, inferredCastTimeMs, 0)),
|
||||
castTimeMs,
|
||||
interruptible: value.interruptible ?? fallback.interruptible ?? true,
|
||||
range: finiteAtLeast(value.range, fallback.range, 0),
|
||||
cooldownMs,
|
||||
@@ -160,6 +263,7 @@ export function normalizeEnemyAttack(
|
||||
...(value.radius !== undefined
|
||||
? { radius: finiteAtLeast(value.radius, 0, 0) }
|
||||
: {}),
|
||||
...(telegraph ? { telegraph: normalizeTelegraph(telegraph) } : {}),
|
||||
...(value.effect ? { effect: value.effect } : {}),
|
||||
...(value.condition ? { condition: value.condition } : {}),
|
||||
...(value.source ? { source: value.source } : {}),
|
||||
|
||||
Reference in new issue
Block a user