forked from phenom/healer-man
Release Healer Man 0.1.17
This commit is contained in:
1 parent
b57d2b2839
commit
fc8a7a722a
13 files changed
+235
-41
No files matched your search
@@ -55,9 +55,15 @@ export function createArenaPractice(options: PracticeOptions) {
|
||||
onSnapshot: (snapshot) => options.onSnapshot({ ...snapshot, controlledActorId: "practice-player" }),
|
||||
onResult: (result) => options.onResult({ ...result, ratingUpdates: [] }),
|
||||
});
|
||||
simulation.start();
|
||||
options.onSnapshot({ ...simulation.snapshot(), controlledActorId: "practice-player" });
|
||||
let started = false;
|
||||
return {
|
||||
start() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
simulation.resetPreparationClock();
|
||||
simulation.start();
|
||||
},
|
||||
intent(intent: ArenaCombatIntent) {
|
||||
return simulation.receiveIntent("practice-player", intent);
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@ interface ArenaStoreState {
|
||||
deleteLoadout: (session: PlayerSession, characterId: string, slot: number) => Promise<boolean>;
|
||||
queueMatch: (session: PlayerSession | null, character: CharacterProfile) => Promise<boolean>;
|
||||
ready: (session: PlayerSession) => Promise<void>;
|
||||
startPractice: () => void;
|
||||
leave: (session: PlayerSession | null) => Promise<void>;
|
||||
intent: (intent: Omit<ArenaCombatIntent, "sequence">) => void;
|
||||
resetResult: () => void;
|
||||
@@ -221,6 +222,8 @@ export const useArenaStore = create<ArenaStoreState>((set, get) => ({
|
||||
catch (error) { set({ error: messageFor(error) }); }
|
||||
},
|
||||
|
||||
startPractice: () => get().practice?.start(),
|
||||
|
||||
leave: async (session) => {
|
||||
get().practice?.stop();
|
||||
get().socket?.close();
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
MINIMUM_CORPSE_VISIBLE_MS,
|
||||
MOB_WOUND_ANIMATION_COOLDOWN_MS,
|
||||
PLAYER_AURA_ENTITY_ID,
|
||||
resolvedAbilityCosts,
|
||||
selectedMob,
|
||||
useCombatStore,
|
||||
} from "./combatStore";
|
||||
@@ -97,6 +98,32 @@ describe("combat store", () => {
|
||||
expect(useCombatStore.getState().mobs.ravager.engaged).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the displayed primary mana and resolved percentage cost for affordability", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1 });
|
||||
const initial = useCombatStore.getState();
|
||||
const frostbolt = initial.abilities.find((ability) => ability.id === "mage-frostbolt");
|
||||
expect(frostbolt).toBeDefined();
|
||||
const [cost] = resolvedAbilityCosts(frostbolt!, initial.talentModifiers, initial);
|
||||
expect(cost).toEqual({ resource: "mana", amount: Math.round(initial.maxResource * 0.14) });
|
||||
registerTarget("mana-target", [10, 0, 0]);
|
||||
|
||||
useCombatStore.setState({
|
||||
resource: cost.amount,
|
||||
resourcePools: { ...initial.resourcePools, mana: 0 },
|
||||
});
|
||||
expect(useCombatStore.getState().castAbility(frostbolt!.id, [0, 0, 0], 1_000)).toMatchObject({ ok: true });
|
||||
useCombatStore.getState().cancelCast();
|
||||
|
||||
useCombatStore.setState({
|
||||
resource: cost.amount - 1,
|
||||
resourcePools: { ...initial.resourcePools, mana: initial.maxResource },
|
||||
});
|
||||
expect(useCombatStore.getState().castAbility(frostbolt!.id, [0, 0, 0], 3_000)).toMatchObject({
|
||||
ok: false,
|
||||
reason: "not-enough-resource",
|
||||
});
|
||||
});
|
||||
|
||||
it("emits exactly one cast and release event and cancels presentation with the cast", () => {
|
||||
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 1 });
|
||||
registerTarget("presentation-target", [10, 0, 0]);
|
||||
|
||||
+15
-7
@@ -1504,16 +1504,19 @@ function initialResourcePools(
|
||||
return { pools, maximums };
|
||||
}
|
||||
|
||||
function abilityCosts(
|
||||
export function resolvedAbilityCosts(
|
||||
ability: AbilityDefinition,
|
||||
modifiers: readonly TalentModifier[],
|
||||
state: Pick<CombatState, "maxHealth" | "maxResourcePools"> & Partial<Pick<CombatState, "auras">>,
|
||||
state: Pick<CombatState, "classId" | "maxHealth" | "maxResource" | "maxResourcePools"> & Partial<Pick<CombatState, "auras">>,
|
||||
): readonly { resource: ResourceType; amount: number }[] {
|
||||
const aggregated = new Map<ResourceType, number>();
|
||||
const activeResourceType = resourceProfileForClass(state.classId).type;
|
||||
for (const cost of ability.costs?.length ? ability.costs : [ability.cost]) {
|
||||
const maximum = cost.resource === "health"
|
||||
? state.maxHealth
|
||||
: state.maxResourcePools[cost.resource] ?? defaultResourceMaximum(cost.resource);
|
||||
: cost.resource === activeResourceType
|
||||
? state.maxResource
|
||||
: state.maxResourcePools[cost.resource] ?? defaultResourceMaximum(cost.resource);
|
||||
const baseAmount = cost.percentage ? maximum * cost.amount / 100 : cost.amount;
|
||||
const rawAmount = Math.max(0, Math.round(applyTalentModifierValue(
|
||||
baseAmount,
|
||||
@@ -3866,7 +3869,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
: ability?.target;
|
||||
const modifiers = state.talentModifiers;
|
||||
const resourceCost = ability ? talentAdjustedResourceCost(ability, modifiers) : 0;
|
||||
const costs = ability ? abilityCosts(ability, modifiers, state) : [];
|
||||
const costs = ability ? resolvedAbilityCosts(ability, modifiers, state) : [];
|
||||
const costsToSpend = completingCast?.mode === "channel" ? [] : costs;
|
||||
const targeting = ability ? targetingRulesForAbility(ability) : null;
|
||||
const spellSchool = ability ? ability.castRules?.school ?? abilityDamageSchool(state.classId, ability) : "physical";
|
||||
@@ -3895,7 +3898,9 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
else if (ability.id === "rom-mage-primary-56" && !hasRuneWakerState("rom-static-field-charge")) failure = "condition-not-met";
|
||||
else if (costs.some((cost) => cost.resource === "health"
|
||||
? state.health <= cost.amount
|
||||
: (state.resourcePools[cost.resource] ?? (cost.resource === resourceProfileForClass(state.classId).type ? state.resource : 0)) < cost.amount)) failure = "not-enough-resource";
|
||||
: (cost.resource === resourceProfileForClass(state.classId).type
|
||||
? state.resource
|
||||
: state.resourcePools[cost.resource] ?? 0) < cost.amount)) failure = "not-enough-resource";
|
||||
else if (runeCost && !spendDeathKnightRunes(state.deathKnightRunes, runeCost, now)) failure = "not-enough-runes";
|
||||
|
||||
const playerOrigin = currentOrigin(state, origin);
|
||||
@@ -4060,7 +4065,11 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
state.classId,
|
||||
);
|
||||
const cooldownMs = talentAdjustedCooldown(ability, modifiers);
|
||||
const spentPools = { ...state.resourcePools };
|
||||
const activeResourceType = resourceProfileForClass(state.classId).type;
|
||||
const spentPools: Partial<Record<ResourceType, number>> = {
|
||||
...state.resourcePools,
|
||||
[activeResourceType]: state.resource,
|
||||
};
|
||||
const lastResourceSpentAt = { ...state.lastResourceSpentAt };
|
||||
let healthAfterCosts = state.health;
|
||||
for (const cost of costsToSpend) {
|
||||
@@ -4073,7 +4082,6 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
const spentRunes = runeCost && completingCast?.mode !== "channel"
|
||||
? spendDeathKnightRunes(state.deathKnightRunes, runeCost, now)
|
||||
: state.deathKnightRunes;
|
||||
const activeResourceType = resourceProfileForClass(state.classId).type;
|
||||
const activeResourceAfterCosts = spentPools[activeResourceType] ?? state.resource - resourceCost;
|
||||
if (!completingCast && castTimeMs > 0) {
|
||||
castSequence += 1;
|
||||
|
||||
Reference in new issue
Block a user