Update Healer Man server
This commit is contained in:
1 parent
39458d5de6
commit
6cc1b9217f
203 files changed
+133929
-31271
No files matched your search
@@ -0,0 +1,266 @@
|
||||
import catalogJson from "./generated/romNativeNpcEquipment.generated.json";
|
||||
|
||||
export interface RomNativeNpcEquipmentItem {
|
||||
readonly sourceItemId: number;
|
||||
readonly sourceCatalog: "armorobject.db" | "weaponobject.db";
|
||||
readonly sourceRecordIndex: number;
|
||||
readonly objectKind: "equipment-item";
|
||||
readonly objectType: number;
|
||||
readonly quality: number;
|
||||
readonly itemLevel: number;
|
||||
readonly gearType: number;
|
||||
readonly gearPosition: number;
|
||||
readonly eqTypeValuePairs: readonly { readonly slot: number; readonly eqType: number; readonly value: number }[];
|
||||
/** GameObjectItemStruct::DropItemAbility rows; these are drop metadata, not equipment affixes. */
|
||||
readonly dropAbilityIdRatePairs: readonly { readonly slot: number; readonly attributeId: number; readonly rate: number }[];
|
||||
readonly durable: number;
|
||||
readonly refineTableId: number;
|
||||
readonly attackRange: number | null;
|
||||
readonly attackSpeed: number | null;
|
||||
readonly refineLevel: number;
|
||||
readonly addPowerQuality: number;
|
||||
readonly collectionNpcId: number;
|
||||
}
|
||||
|
||||
export interface RomNativeNpcEquipmentCandidate {
|
||||
readonly candidateIndex: number;
|
||||
readonly sourceItemId: number;
|
||||
readonly selectionWeightPercent: number;
|
||||
}
|
||||
|
||||
export interface RomNativeNpcEquipmentRecord {
|
||||
readonly npcId: number;
|
||||
readonly sourceRecordIndex: number;
|
||||
readonly mainHandCandidates: readonly RomNativeNpcEquipmentCandidate[];
|
||||
readonly offHandCandidates: readonly RomNativeNpcEquipmentCandidate[];
|
||||
readonly bowEquipmentId: number;
|
||||
readonly ornamentEquipmentId: number;
|
||||
}
|
||||
|
||||
export interface RomNativeNpcEquipmentSelection {
|
||||
readonly mainHandCandidateIndex?: number;
|
||||
readonly offHandCandidateIndex?: number;
|
||||
/** Stable seed for the simulator's weighted candidate choice; defaults to npcId. */
|
||||
readonly selectionSeed?: string | number;
|
||||
}
|
||||
|
||||
export interface RomNativeNpcRefineAbility {
|
||||
readonly refineObjectId: number;
|
||||
readonly sourceRecordIndex: number;
|
||||
readonly refineType: number;
|
||||
readonly refineLevel: number;
|
||||
readonly eqTypeValuePairs: readonly { readonly slot: number; readonly eqType: number; readonly value: number }[];
|
||||
readonly exEqPowerRate: number;
|
||||
}
|
||||
|
||||
export interface RomNativeNpcWornItem {
|
||||
readonly slot: "mainHand" | "offHand" | "bow" | "ornament";
|
||||
readonly item: RomNativeNpcEquipmentItem;
|
||||
readonly fieldQuality: number;
|
||||
readonly fieldDurability: number;
|
||||
readonly fieldPowerQuality: number;
|
||||
readonly fieldRare: number;
|
||||
readonly effectiveRefineLevel: number;
|
||||
readonly durabilityPowerRate: number;
|
||||
readonly wearPower: number;
|
||||
readonly refineAbilities: readonly RomNativeNpcRefineAbility[];
|
||||
readonly inherentAttributeIds: readonly number[];
|
||||
readonly runeIds: readonly number[];
|
||||
}
|
||||
|
||||
export interface RomNativeResolvedNpcEquipment {
|
||||
readonly mainHand: RomNativeNpcEquipmentItem | null;
|
||||
readonly offHand: RomNativeNpcEquipmentItem | null;
|
||||
readonly bow: RomNativeNpcEquipmentItem | null;
|
||||
readonly ornament: RomNativeNpcEquipmentItem | null;
|
||||
readonly wornItems: readonly RomNativeNpcWornItem[];
|
||||
readonly selectedCandidateIndices: {
|
||||
readonly mainHand: number | null;
|
||||
readonly offHand: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export type RomNativeNpcEquipmentResult =
|
||||
| { readonly status: "supported"; readonly value: RomNativeResolvedNpcEquipment }
|
||||
| { readonly status: "unsupported"; readonly reason: string };
|
||||
|
||||
interface GeneratedCatalog {
|
||||
readonly npcs: readonly RomNativeNpcEquipmentRecord[];
|
||||
readonly itemsById: Readonly<Record<string, RomNativeNpcEquipmentItem | null>>;
|
||||
readonly refineItemsById: Readonly<Record<string, RomNativeNpcRefineAbility | null>>;
|
||||
readonly source: {
|
||||
readonly npcWearDefaults: {
|
||||
readonly fieldDurable: number;
|
||||
readonly fieldQuality: number;
|
||||
readonly fieldPowerQuality: number;
|
||||
readonly fieldRare: number;
|
||||
readonly inherentIds: readonly number[];
|
||||
readonly runeIds: readonly number[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const catalog = catalogJson as unknown as GeneratedCatalog;
|
||||
const npcById = new Map(catalog.npcs.map((npc) => [npc.npcId, npc]));
|
||||
|
||||
export function romNativeNpcEquipmentRecord(npcId: number): RomNativeNpcEquipmentRecord | undefined {
|
||||
return npcById.get(npcId);
|
||||
}
|
||||
|
||||
/** All exported source NPC equipment rows, in generated source order. */
|
||||
export function romNativeNpcEquipmentRecords(): readonly RomNativeNpcEquipmentRecord[] {
|
||||
return catalog.npcs;
|
||||
}
|
||||
|
||||
function deterministicBucket(seed: string | number, npcId: number, slot: "mainHand" | "offHand"): number {
|
||||
const key = `rom-native-npc-equipment/v1:${npcId}:${slot}:${String(seed)}`;
|
||||
let hash = 0x811c9dc5;
|
||||
for (let index = 0; index < key.length; index += 1) {
|
||||
hash ^= key.charCodeAt(index);
|
||||
hash = Math.imul(hash, 0x01000193);
|
||||
}
|
||||
return Math.floor((hash >>> 0) * 100 / 0x1_0000_0000);
|
||||
}
|
||||
|
||||
function deterministicCandidate(
|
||||
npcId: number,
|
||||
slot: "mainHand" | "offHand",
|
||||
candidates: readonly RomNativeNpcEquipmentCandidate[],
|
||||
seed: string | number,
|
||||
): RomNativeNpcEquipmentCandidate | undefined {
|
||||
const weights = candidates.map((candidate) => candidate.selectionWeightPercent);
|
||||
if (weights.some((weight) => !Number.isFinite(weight) || weight < 0)
|
||||
|| Math.abs(weights.reduce((sum, weight) => sum + weight, 0) - 100) > 0.00001) return undefined;
|
||||
const bucket = deterministicBucket(seed, npcId, slot);
|
||||
let cumulative = 0;
|
||||
for (const candidate of candidates) {
|
||||
cumulative += candidate.selectionWeightPercent;
|
||||
if (bucket < cumulative) return candidate;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveCandidate(
|
||||
npcId: number,
|
||||
slot: "mainHand" | "offHand",
|
||||
candidates: readonly RomNativeNpcEquipmentCandidate[],
|
||||
requestedIndex: number | undefined,
|
||||
seed: string | number,
|
||||
): { readonly status: "supported"; readonly item: RomNativeNpcEquipmentItem | null; readonly index: number }
|
||||
| { readonly status: "unsupported"; readonly reason: string } {
|
||||
if (!candidates.length) return { status: "unsupported", reason: `missing-${slot}-candidates` };
|
||||
let candidate: RomNativeNpcEquipmentCandidate | undefined;
|
||||
if (requestedIndex !== undefined) {
|
||||
if (!Number.isInteger(requestedIndex) || requestedIndex < 0) {
|
||||
return { status: "unsupported", reason: `invalid-${slot}-candidate-index` };
|
||||
}
|
||||
candidate = candidates.find((entry) => entry.candidateIndex === requestedIndex);
|
||||
if (!candidate) return { status: "unsupported", reason: `missing-${slot}-candidate` };
|
||||
} else {
|
||||
candidate = deterministicCandidate(npcId, slot, candidates, seed);
|
||||
if (!candidate) return { status: "unsupported", reason: `invalid-${slot}-candidate-weights` };
|
||||
}
|
||||
if (candidate.sourceItemId === 0) return { status: "supported", item: null, index: candidate.candidateIndex };
|
||||
const item = catalog.itemsById[String(candidate.sourceItemId)];
|
||||
if (!item) return { status: "unsupported", reason: `missing-${slot}-source-item-${candidate.sourceItemId}` };
|
||||
return { status: "supported", item, index: candidate.candidateIndex };
|
||||
}
|
||||
|
||||
function resolveFixedItem(
|
||||
slot: "bow" | "ornament",
|
||||
itemId: number,
|
||||
): { readonly status: "supported"; readonly item: RomNativeNpcEquipmentItem | null }
|
||||
| { readonly status: "unsupported"; readonly reason: string } {
|
||||
if (itemId === 0) return { status: "supported", item: null };
|
||||
const item = catalog.itemsById[String(itemId)];
|
||||
if (!item) return { status: "unsupported", reason: `missing-${slot}-source-item-${itemId}` };
|
||||
return { status: "supported", item };
|
||||
}
|
||||
|
||||
function sourceDurabilityPowerRate(item: RomNativeNpcEquipmentItem, fieldQuality: number, fieldDurability: number): number {
|
||||
const orgDurability = Math.trunc(item.durable * fieldQuality / 100);
|
||||
const fieldDurabilityHundreds = Math.trunc(fieldDurability / 100);
|
||||
if (fieldDurabilityHundreds > 100 || fieldDurabilityHundreds > orgDurability) return 1.2;
|
||||
if (fieldDurabilityHundreds > Math.trunc(orgDurability / 2)) return 1;
|
||||
if (fieldDurabilityHundreds > Math.trunc(orgDurability / 5)) return 0.8;
|
||||
return 0.2;
|
||||
}
|
||||
|
||||
function wornItem(
|
||||
slot: RomNativeNpcWornItem["slot"],
|
||||
item: RomNativeNpcEquipmentItem,
|
||||
): RomNativeNpcWornItem | { readonly reason: string } {
|
||||
const defaults = catalog.source.npcWearDefaults;
|
||||
const refineLevel = Math.min(item.refineLevel, 20);
|
||||
const refineAbilities: RomNativeNpcRefineAbility[] = [];
|
||||
if (refineLevel > 0) {
|
||||
if (!Number.isSafeInteger(item.refineTableId) || item.refineTableId <= 0) {
|
||||
return { reason: `missing-${slot}-refine-table-id-${item.sourceItemId}` };
|
||||
}
|
||||
for (let level = 1; level <= refineLevel; level += 1) {
|
||||
const refineId = item.refineTableId + level - 1;
|
||||
const refine = catalog.refineItemsById[String(refineId)];
|
||||
if (!refine) return { reason: `missing-${slot}-refine-ability-${refineId}` };
|
||||
refineAbilities.push(refine);
|
||||
}
|
||||
}
|
||||
const durabilityPowerRate = sourceDurabilityPowerRate(item, defaults.fieldQuality, defaults.fieldDurable);
|
||||
return Object.freeze({
|
||||
slot,
|
||||
item,
|
||||
fieldQuality: defaults.fieldQuality,
|
||||
fieldDurability: defaults.fieldDurable,
|
||||
fieldPowerQuality: defaults.fieldPowerQuality,
|
||||
fieldRare: defaults.fieldRare,
|
||||
effectiveRefineLevel: refineLevel,
|
||||
durabilityPowerRate,
|
||||
wearPower: defaults.fieldPowerQuality / 10 * durabilityPowerRate,
|
||||
refineAbilities: Object.freeze(refineAbilities),
|
||||
inherentAttributeIds: defaults.inherentIds,
|
||||
runeIds: defaults.runeIds,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves one stable weighted NPC equipment outcome. The source's five
|
||||
* candidate slots and 20-percent weights stay intact; a caller seed selects
|
||||
* a reproducible outcome without averaging candidate stats.
|
||||
*/
|
||||
export function resolveRomNativeNpcEquipment(
|
||||
npcId: number,
|
||||
selection: RomNativeNpcEquipmentSelection = {},
|
||||
): RomNativeNpcEquipmentResult {
|
||||
const npc = npcById.get(npcId);
|
||||
if (!npc) return { status: "unsupported", reason: "missing-source-npc" };
|
||||
const seed = selection.selectionSeed ?? npcId;
|
||||
const mainHand = resolveCandidate(npcId, "mainHand", npc.mainHandCandidates, selection.mainHandCandidateIndex, seed);
|
||||
if (mainHand.status !== "supported") return mainHand;
|
||||
const offHand = resolveCandidate(npcId, "offHand", npc.offHandCandidates, selection.offHandCandidateIndex, seed);
|
||||
if (offHand.status !== "supported") return offHand;
|
||||
const bow = resolveFixedItem("bow", npc.bowEquipmentId);
|
||||
if (bow.status !== "supported") return bow;
|
||||
const ornament = resolveFixedItem("ornament", npc.ornamentEquipmentId);
|
||||
if (ornament.status !== "supported") return ornament;
|
||||
|
||||
const items = [
|
||||
["mainHand", mainHand.item], ["offHand", offHand.item], ["bow", bow.item], ["ornament", ornament.item],
|
||||
] as const;
|
||||
const wornItems: RomNativeNpcWornItem[] = [];
|
||||
for (const [slot, item] of items) {
|
||||
if (!item) continue;
|
||||
const worn = wornItem(slot, item);
|
||||
if ("reason" in worn) return { status: "unsupported", reason: worn.reason };
|
||||
wornItems.push(worn);
|
||||
}
|
||||
return {
|
||||
status: "supported",
|
||||
value: {
|
||||
mainHand: mainHand.item,
|
||||
offHand: offHand.item,
|
||||
bow: bow.item,
|
||||
ornament: ornament.item,
|
||||
wornItems: Object.freeze(wornItems),
|
||||
selectedCandidateIndices: { mainHand: mainHand.index, offHand: offHand.index },
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user