Guard source gear namespaces and retain recoverable legacy records (#9)
This commit is contained in:
1 parent
ab37006932
commit
e567910c04
23 files changed
+1203
-66
No files matched your search
+49
-18
@@ -64,11 +64,13 @@ import { useDungeonSimulationStore } from "./dungeonSimulationStore";
|
||||
import { useGameStore } from "./store";
|
||||
import {
|
||||
baseClassFor,
|
||||
contentCategoryForClass,
|
||||
isRomClassId,
|
||||
type ClassId,
|
||||
type GenderId,
|
||||
type RaceId,
|
||||
} from "../app/characterCatalog";
|
||||
import type { ContentCategoryId } from "../app/contentCategories";
|
||||
import { normalizeCharacterStatRuleset, type CharacterStatRuleset } from "./characterStatRuleset";
|
||||
import { applyRomNativeStatModifiers, type RomNativeAdditiveChannel, type RomNativeAttributeKey,
|
||||
type RomNativeDerivedStats, type RomNativeRateChannel, type RomNativeStatModifiers } from "./romNativeStats";
|
||||
@@ -455,7 +457,7 @@ import { bossCardForSource } from "./bossCardCatalog";
|
||||
import { collectBossCardDrop, emptyBossCardCollection, normalizeBossCardCollection, rollBossCards, type BossCardCollection, type BossCardDrop } from "./bossCards";
|
||||
import { adaptRunewakerEnemyDamage } from "./runewakerCombatFormula";
|
||||
import { channelAmountEffects, channelPulseAt, channelPulseCount } from "./combatChannelAmounts";
|
||||
import { normalizeInventory, type InventoryItem } from "./lootTypes";
|
||||
import { inventoryItemUsableForCategory, normalizeInventory, type InventoryItem } from "./lootTypes";
|
||||
import {
|
||||
PLAYER_EQUIPMENT_OWNER_ID,
|
||||
activeEquipmentStatsForClass,
|
||||
@@ -1405,6 +1407,7 @@ export interface ActorHealingRequest {
|
||||
|
||||
export interface CharacterCombatSeed {
|
||||
readonly classId: ClassId;
|
||||
readonly categoryId?: ContentCategoryId;
|
||||
readonly statRuleset?: CharacterStatRuleset;
|
||||
readonly hunterPetFamily?: Wow335HunterPetFamily;
|
||||
readonly specializationId?: string | null;
|
||||
@@ -1570,6 +1573,7 @@ export interface EnemyPeriodicCombatEffect {
|
||||
export interface CombatState {
|
||||
classId: ClassId;
|
||||
statRuleset: CharacterStatRuleset;
|
||||
contentCategoryId: ContentCategoryId;
|
||||
readonly romNativeRecoveryClock?: RomNativeRecoveryClock;
|
||||
hunterPetFamily: Wow335HunterPetFamily;
|
||||
specializationId: string | null;
|
||||
@@ -7799,6 +7803,7 @@ function damageMobInWork(
|
||||
const testing = useGameStore.getState().gmTestMode;
|
||||
const canAward = killed && !mob.experienceGranted && !testing;
|
||||
const beforeLevel = work.progression.level;
|
||||
const ownerCategory = useCombatStore.getState().contentCategoryId;
|
||||
const hasTreasureHunter = work.auras.some((aura) => (
|
||||
aura.targetId === PLAYER_AURA_ENTITY_ID
|
||||
&& (aura.expiresAt === null || aura.expiresAt > now)
|
||||
@@ -7807,9 +7812,10 @@ function damageMobInWork(
|
||||
const pendingLoot = testing ? [] : killed && mob.lootSourceId
|
||||
? rollBossLoot(mob.lootSourceId, beforeLevel, {
|
||||
now,
|
||||
ownerCategory,
|
||||
bonusItemChance: romRogueTreasureHunterLootChance(mob.bonusLootChance, hasTreasureHunter),
|
||||
})
|
||||
: mob.pendingLoot;
|
||||
: mob.pendingLoot.filter((item) => inventoryItemUsableForCategory(item, ownerCategory));
|
||||
const card = killed && mob.boss && !testing
|
||||
? bossCardForSource(mob.lootSourceId ?? mob.id, useGameStore.getState().activeDungeonId, mob.name)
|
||||
: null;
|
||||
@@ -11786,6 +11792,7 @@ function combatActorSnapshots(state: CombatState): Readonly<Record<string, Comba
|
||||
function initialState() {
|
||||
const classId: ClassId = "priest";
|
||||
const statRuleset: CharacterStatRuleset = "wow";
|
||||
const contentCategoryId: ContentCategoryId = "wow";
|
||||
const raceId: RaceId = "human";
|
||||
const gender: GenderId = "male";
|
||||
const level = 1;
|
||||
@@ -11805,6 +11812,7 @@ function initialState() {
|
||||
return {
|
||||
classId,
|
||||
statRuleset,
|
||||
contentCategoryId,
|
||||
hunterPetFamily: "wolf" as const,
|
||||
specializationId: null,
|
||||
gearRole: "healer" as PartyRole,
|
||||
@@ -11893,9 +11901,13 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
: [];
|
||||
const talentModifiers = characterStatModifiers(classId, progression.level, talentRanks);
|
||||
const abilities = abilitiesForCharacter(classId, secondaryClassId);
|
||||
const inventory = normalizeInventory(profile.inventory);
|
||||
const equipment = normalizeEquipment(profile.equipment, inventory);
|
||||
const gearStats = specializationId !== null || classId.startsWith("rom-") ? resolvedEquipmentStats(equipment, inventory, {
|
||||
// The selected source class owns the character namespace; dungeon and
|
||||
// untrusted seed metadata cannot change which game's items are usable.
|
||||
const contentCategoryId = contentCategoryForClass(classId);
|
||||
const inventory = normalizeInventory(profile.inventory, { ownerCategory: contentCategoryId });
|
||||
const categoryInventory = inventory.filter((item) => inventoryItemUsableForCategory(item, contentCategoryId));
|
||||
const equipment = normalizeEquipment(profile.equipment, inventory, contentCategoryId);
|
||||
const gearStats = specializationId !== null || classId.startsWith("rom-") ? resolvedEquipmentStats(equipment, categoryInventory, {
|
||||
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
|
||||
ownerKind: "player",
|
||||
classId,
|
||||
@@ -11903,7 +11915,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
specializationId,
|
||||
role: gearRole,
|
||||
level: progression.level,
|
||||
}) : activeEquipmentStatsForClass(equipment, inventory, PLAYER_EQUIPMENT_OWNER_ID, classId);
|
||||
}) : activeEquipmentStatsForClass(equipment, inventory, PLAYER_EQUIPMENT_OWNER_ID, classId, contentCategoryId);
|
||||
const maxHealth = talentAdjustedMaxHealth(
|
||||
classId,
|
||||
progression.level,
|
||||
@@ -11952,6 +11964,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
raceId,
|
||||
gender,
|
||||
statRuleset,
|
||||
contentCategoryId,
|
||||
romNativeRecoveryClock: undefined,
|
||||
level: progression.level,
|
||||
xp: progression.xp,
|
||||
@@ -11978,7 +11991,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
abilities,
|
||||
talentRanks,
|
||||
),
|
||||
inventory,
|
||||
inventory: categoryInventory,
|
||||
bossCards: normalizeBossCardCollection(profile.bossCards),
|
||||
equipment,
|
||||
gearStats,
|
||||
@@ -11988,6 +12001,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
},
|
||||
|
||||
refreshResolvedGear: () => set((state) => {
|
||||
const categoryInventory = state.inventory.filter((item) => inventoryItemUsableForCategory(item, state.contentCategoryId));
|
||||
const context: HeirloomContext = {
|
||||
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
|
||||
ownerKind: "player",
|
||||
@@ -11998,8 +12012,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
level: state.level,
|
||||
};
|
||||
const gearStats = state.specializationId !== null || state.classId.startsWith("rom-")
|
||||
? resolvedEquipmentStats(state.equipment, state.inventory, context)
|
||||
: activeEquipmentStatsForClass(state.equipment, state.inventory, PLAYER_EQUIPMENT_OWNER_ID, state.classId);
|
||||
? resolvedEquipmentStats(state.equipment, categoryInventory, context)
|
||||
: activeEquipmentStatsForClass(state.equipment, state.inventory, PLAYER_EQUIPMENT_OWNER_ID, state.classId, state.contentCategoryId);
|
||||
const maxHealth = auraAdjustedPlayerMaxHealth(
|
||||
state.classId,
|
||||
state.level,
|
||||
@@ -12259,9 +12273,11 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
lootMob: (id, now = combatNow()) => {
|
||||
const mob = get().mobs[id];
|
||||
if (!mob || !mob.dead || !mob.hasLoot || mob.looted || mob.despawned) return false;
|
||||
const ownerCategory = get().contentCategoryId;
|
||||
const acceptedLoot = mob.pendingLoot.filter((item) => inventoryItemUsableForCategory(item, ownerCategory));
|
||||
const corpseDespawnAt = Math.max(now, (mob.diedAt ?? now) + MINIMUM_CORPSE_VISIBLE_MS);
|
||||
const lootMessage = mob.pendingLoot.length
|
||||
? `Looted ${mob.pendingLoot.map((item) => `${item.name}: ${itemStatSummary(item.stats)}`).join(", ")}.`
|
||||
const lootMessage = acceptedLoot.length
|
||||
? `Looted ${acceptedLoot.map((item) => `${item.name}: ${itemStatSummary(item.stats)}`).join(", ")}.`
|
||||
: `Looted ${mob.name}.`;
|
||||
set((state) => {
|
||||
const bossReward = mob.boss
|
||||
@@ -12269,13 +12285,13 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
id: ++bossLootRewardSequence,
|
||||
bossId: id,
|
||||
bossName: mob.name,
|
||||
items: mob.pendingLoot,
|
||||
items: acceptedLoot,
|
||||
...(mob.bossCardDrop ? { cards: mob.bossCardDrop } : {}),
|
||||
acquiredAt: now,
|
||||
} satisfies BossLootReward
|
||||
: null;
|
||||
return {
|
||||
inventory: [...state.inventory, ...mob.pendingLoot],
|
||||
inventory: [...state.inventory, ...acceptedLoot],
|
||||
bossCards: mob.bossCardDrop ? collectBossCardDrop(state.bossCards, mob.bossCardDrop) : state.bossCards,
|
||||
mobs: {
|
||||
...state.mobs,
|
||||
@@ -12332,6 +12348,9 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
{
|
||||
secondaryClassId: ownerId === PLAYER_EQUIPMENT_OWNER_ID ? state.secondaryClassId : ownerMember?.secondaryClassId,
|
||||
level: ownerId === PLAYER_EQUIPMENT_OWNER_ID ? state.level : ownerMember?.level,
|
||||
ownerCategory: ownerId === PLAYER_EQUIPMENT_OWNER_ID
|
||||
? state.contentCategoryId
|
||||
: usePartyStore.getState().categoryId ?? contentCategoryForClass(ownerClassId),
|
||||
},
|
||||
);
|
||||
if (!change.ok) {
|
||||
@@ -12342,7 +12361,11 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
const ownerName = ownerId === PLAYER_EQUIPMENT_OWNER_ID
|
||||
? "You"
|
||||
: ownerMember?.name ?? "Companion";
|
||||
const gearStats = state.specializationId !== null || state.classId.startsWith("rom-") ? resolvedEquipmentStats(change.assignments, state.inventory, {
|
||||
const categoryInventory = state.inventory.filter((candidate) => inventoryItemUsableForCategory(
|
||||
candidate,
|
||||
state.contentCategoryId,
|
||||
));
|
||||
const gearStats = state.specializationId !== null || state.classId.startsWith("rom-") ? resolvedEquipmentStats(change.assignments, categoryInventory, {
|
||||
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
|
||||
ownerKind: "player",
|
||||
classId: state.classId,
|
||||
@@ -12350,7 +12373,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
specializationId: state.specializationId,
|
||||
role: state.gearRole,
|
||||
level: state.level,
|
||||
}) : activeEquipmentStatsForClass(change.assignments, state.inventory, PLAYER_EQUIPMENT_OWNER_ID, state.classId);
|
||||
}) : activeEquipmentStatsForClass(change.assignments, state.inventory, PLAYER_EQUIPMENT_OWNER_ID, state.classId, state.contentCategoryId);
|
||||
const maxHealth = auraAdjustedPlayerMaxHealth(
|
||||
state.classId,
|
||||
state.level,
|
||||
@@ -12403,7 +12426,11 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
),
|
||||
feedback: nextFeedback("success", `${ownerName} equipped ${item?.name ?? "the item"}.`),
|
||||
});
|
||||
usePartyStore.getState().syncEquipment(change.assignments, state.inventory);
|
||||
const partyCategory = usePartyStore.getState().categoryId;
|
||||
usePartyStore.getState().syncEquipment(
|
||||
change.assignments,
|
||||
state.inventory.filter((candidate) => inventoryItemUsableForCategory(candidate, partyCategory)),
|
||||
);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -12411,7 +12438,11 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
const state = get();
|
||||
const next = unequipInventorySlot(state.equipment, ownerId, slot);
|
||||
if (next === state.equipment) return false;
|
||||
const gearStats = state.specializationId !== null || state.classId.startsWith("rom-") ? resolvedEquipmentStats(next, state.inventory, {
|
||||
const categoryInventory = state.inventory.filter((candidate) => inventoryItemUsableForCategory(
|
||||
candidate,
|
||||
state.contentCategoryId,
|
||||
));
|
||||
const gearStats = state.specializationId !== null || state.classId.startsWith("rom-") ? resolvedEquipmentStats(next, categoryInventory, {
|
||||
ownerId: PLAYER_EQUIPMENT_OWNER_ID,
|
||||
ownerKind: "player",
|
||||
classId: state.classId,
|
||||
@@ -12419,7 +12450,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
specializationId: state.specializationId,
|
||||
role: state.gearRole,
|
||||
level: state.level,
|
||||
}) : activeEquipmentStatsForClass(next, state.inventory, PLAYER_EQUIPMENT_OWNER_ID, state.classId);
|
||||
}) : activeEquipmentStatsForClass(next, state.inventory, PLAYER_EQUIPMENT_OWNER_ID, state.classId, state.contentCategoryId);
|
||||
const maxHealth = auraAdjustedPlayerMaxHealth(
|
||||
state.classId,
|
||||
state.level,
|
||||
|
||||
Reference in new issue
Block a user