Update class mechanics, stat rulesets, and CoA reference catalogs
This commit is contained in:
1 parent
b16821e92e
commit
39458d5de6
221 files changed
+2584962
-568
No files matched your search
+99
-26
@@ -39,6 +39,7 @@ import {
|
||||
} from "./companionSpecReadiness";
|
||||
import type { RuntimeTalentRanks as TalentRanks } from "./talentRuntimeCatalog";
|
||||
import type { EnemyControlMechanic } from "./enemyAttacks";
|
||||
import { normalizeCharacterStatRuleset, type CharacterStatRuleset } from "./characterStatRuleset";
|
||||
import type { ContentCategoryId } from "../app/contentCategories";
|
||||
import { isValidRomClassPair, ROM_SECONDARY_UNLOCK_LEVEL } from "../app/romDualClass";
|
||||
import type { PartyRole, PartySize } from "./partyRoles";
|
||||
@@ -89,6 +90,7 @@ export interface PartyMember {
|
||||
readonly gender: GenderId;
|
||||
readonly appearance: CharacterAppearance;
|
||||
readonly classId: ClassId;
|
||||
readonly statRuleset?: CharacterStatRuleset;
|
||||
readonly secondaryClassId?: ClassId | null;
|
||||
readonly specialization: string;
|
||||
readonly specializationId: string | null;
|
||||
@@ -106,6 +108,7 @@ export interface PartyMember {
|
||||
readonly resource: number;
|
||||
readonly maxResource: number;
|
||||
readonly resourcePools: Readonly<Partial<Record<ResourceType, number>>>;
|
||||
readonly resourceMaximums?: Readonly<Partial<Record<ResourceType, number>>>;
|
||||
readonly lastResourceSpentAt: Readonly<Partial<Record<ResourceType, number>>>;
|
||||
readonly warriorAngerManagementRageRemainderMs?: number;
|
||||
readonly comboPoints: number;
|
||||
@@ -127,8 +130,10 @@ export interface PartyMember {
|
||||
interface PartyTemplate {
|
||||
readonly name: string;
|
||||
readonly classId: ClassId;
|
||||
readonly statRuleset?: CharacterStatRuleset;
|
||||
readonly secondaryClassId?: ClassId | null;
|
||||
readonly raceId?: RaceId;
|
||||
readonly gender?: GenderId;
|
||||
readonly specialization: string;
|
||||
readonly talentTreeId: string;
|
||||
readonly talentProfileId?: string | null;
|
||||
@@ -137,8 +142,10 @@ interface PartyTemplate {
|
||||
export interface ConfiguredPartyMemberSeed {
|
||||
readonly name: string;
|
||||
readonly classId: ClassId;
|
||||
readonly statRuleset?: CharacterStatRuleset;
|
||||
readonly secondaryClassId?: ClassId | null;
|
||||
readonly raceId?: RaceId;
|
||||
readonly gender?: GenderId;
|
||||
readonly specialization: string;
|
||||
readonly specializationId?: string | null;
|
||||
readonly talentTreeId?: string;
|
||||
@@ -195,20 +202,20 @@ function wowTemplates(
|
||||
}
|
||||
|
||||
const ROM_TANK_TEMPLATES: readonly PartyTemplate[] = Object.freeze([
|
||||
{ name: "Aldric", classId: "rom-warrior", specialization: "Primary", talentTreeId: "" },
|
||||
{ ...romFiveIdentity("rom-knight", "tank")!, specialization: "Primary", talentTreeId: "" },
|
||||
{ name: "Maerwen", classId: "rom-warden", specialization: "Primary", talentTreeId: "" },
|
||||
{ name: "Bronn", classId: "rom-champion", specialization: "Primary", talentTreeId: "" },
|
||||
{ name: "Aldric", classId: "rom-warrior", statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
|
||||
{ ...romFiveIdentity("rom-knight", "tank")!, statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
|
||||
{ name: "Maerwen", classId: "rom-warden", statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
|
||||
{ name: "Bronn", classId: "rom-champion", statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
|
||||
]);
|
||||
const ROM_HEALER_TEMPLATES: readonly PartyTemplate[] = Object.freeze([
|
||||
{ ...romFiveIdentity("rom-priest", "healer")!, specialization: "Primary", talentTreeId: "" },
|
||||
{ name: "Sylva", classId: "rom-druid", specialization: "Primary", talentTreeId: "" },
|
||||
{ ...romFiveIdentity("rom-priest", "healer")!, statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
|
||||
{ name: "Sylva", classId: "rom-druid", statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
|
||||
]);
|
||||
const ROM_DAMAGE_TEMPLATES: readonly PartyTemplate[] = Object.freeze([
|
||||
{ ...romFiveIdentity("rom-scout", "damage")!, specialization: "Primary", talentTreeId: "" },
|
||||
{ ...romFiveIdentity("rom-rogue", "damage")!, specialization: "Primary", talentTreeId: "" },
|
||||
{ ...romFiveIdentity("rom-mage", "damage")!, specialization: "Primary", talentTreeId: "" },
|
||||
{ name: "Morgha", classId: "rom-warlock", specialization: "Primary", talentTreeId: "" },
|
||||
{ ...romFiveIdentity("rom-scout", "damage")!, statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
|
||||
{ ...romFiveIdentity("rom-rogue", "damage")!, statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
|
||||
{ ...romFiveIdentity("rom-mage", "damage")!, statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
|
||||
{ name: "Morgha", classId: "rom-warlock", statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
|
||||
]);
|
||||
|
||||
export const TANK_SPECIALIZATIONS = Object.freeze(new Set(
|
||||
@@ -251,10 +258,16 @@ function memberMaxHealth(
|
||||
level: number,
|
||||
role: PartyRole,
|
||||
gearStats: ItemStats = EMPTY_ITEM_STATS,
|
||||
_classId: ClassId = "warrior",
|
||||
_raceId: RaceId = "human",
|
||||
classId: ClassId = "warrior",
|
||||
raceId: RaceId = "human",
|
||||
statRuleset: CharacterStatRuleset = "wow",
|
||||
secondaryClassId?: ClassId | null,
|
||||
gender: GenderId = "male",
|
||||
): number {
|
||||
const safeLevel = Math.max(1, Math.trunc(level));
|
||||
if (statRuleset === "rom") {
|
||||
return deriveCharacterStats(classId, safeLevel, gearStats, raceId, {}, secondaryClassId, gender, statRuleset).health;
|
||||
}
|
||||
const base = role === "tank"
|
||||
? 185 + safeLevel * 32
|
||||
: role === "healer"
|
||||
@@ -268,10 +281,13 @@ function memberMaxResource(
|
||||
level: number,
|
||||
raceId: RaceId,
|
||||
gearStats: ItemStats = EMPTY_ITEM_STATS,
|
||||
statRuleset: CharacterStatRuleset = "wow",
|
||||
secondaryClassId?: ClassId | null,
|
||||
gender: GenderId = "male",
|
||||
): number {
|
||||
const profile = resourceProfileForClass(classId);
|
||||
return profile.type === "mana"
|
||||
? deriveCharacterStats(classId, level, gearStats, raceId).mana
|
||||
? deriveCharacterStats(classId, level, gearStats, raceId, {}, secondaryClassId, gender, statRuleset).mana
|
||||
: profile.maximum;
|
||||
}
|
||||
|
||||
@@ -280,6 +296,12 @@ function secondaryResourceMaximum(type: ResourceType, member: PartyMember): numb
|
||||
return naturePowerMaximumForCharacter(member.classId, member.secondaryClassId, member.level);
|
||||
}
|
||||
if (type === "psi") return 6;
|
||||
if (type === "mana" && member.statRuleset === "rom") {
|
||||
return deriveCharacterStats(
|
||||
member.classId, member.level, member.gearStats, member.raceId, {}, member.secondaryClassId,
|
||||
member.gender, "rom",
|
||||
).mana;
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
|
||||
@@ -292,6 +314,19 @@ function initialSecondaryResource(type: ResourceType, maximum: number): number {
|
||||
/** Keeps every resource used by the member's learned kit available to companion AI. */
|
||||
function withMemberAbilityResourcePools(member: PartyMember): PartyMember {
|
||||
const types = new Set<ResourceType>([member.resourceType]);
|
||||
if (member.statRuleset === "rom" && isRomClassId(member.classId)
|
||||
&& deriveCharacterStats(
|
||||
member.classId,
|
||||
member.level,
|
||||
member.gearStats,
|
||||
member.raceId,
|
||||
{},
|
||||
member.secondaryClassId,
|
||||
member.gender,
|
||||
"rom",
|
||||
).romNative?.usesMana) {
|
||||
types.add("mana");
|
||||
}
|
||||
for (const ability of abilitiesForCharacter(member.classId, member.secondaryClassId)) {
|
||||
const learned = learnedAbilityForBuild(ability, member.level, member.talentRanks);
|
||||
if (!learned) continue;
|
||||
@@ -300,18 +335,26 @@ function withMemberAbilityResourcePools(member: PartyMember): PartyMember {
|
||||
}
|
||||
}
|
||||
const resourcePools: Partial<Record<ResourceType, number>> = {};
|
||||
const resourceMaximums: Partial<Record<ResourceType, number>> = {};
|
||||
for (const type of types) {
|
||||
const maximum = type === member.resourceType
|
||||
? member.maxResource
|
||||
: member.resourceMaximums?.[type] ?? secondaryResourceMaximum(type, member);
|
||||
resourceMaximums[type] = maximum;
|
||||
if (type === member.resourceType) {
|
||||
resourcePools[type] = member.resource;
|
||||
continue;
|
||||
}
|
||||
const maximum = secondaryResourceMaximum(type, member);
|
||||
resourcePools[type] = Math.max(0, Math.min(
|
||||
maximum,
|
||||
member.resourcePools[type] ?? initialSecondaryResource(type, maximum),
|
||||
));
|
||||
}
|
||||
return { ...member, resourcePools: Object.freeze(resourcePools) };
|
||||
return {
|
||||
...member,
|
||||
resourcePools: Object.freeze(resourcePools),
|
||||
resourceMaximums: Object.freeze(resourceMaximums),
|
||||
};
|
||||
}
|
||||
|
||||
function withMemberGearStats(member: PartyMember, gearStats: ItemStats): PartyMember {
|
||||
@@ -321,16 +364,22 @@ function withMemberGearStats(member: PartyMember, gearStats: ItemStats): PartyMe
|
||||
gearStats,
|
||||
member.classId,
|
||||
member.raceId,
|
||||
member.statRuleset,
|
||||
member.secondaryClassId,
|
||||
member.gender,
|
||||
);
|
||||
const health = member.health <= 0
|
||||
? 0
|
||||
: Math.max(1, Math.round(member.health / Math.max(1, member.maxHealth) * maxHealth));
|
||||
const maxResource = memberMaxResource(member.classId, member.level, member.raceId, gearStats);
|
||||
const maxResource = memberMaxResource(
|
||||
member.classId, member.level, member.raceId, gearStats, member.statRuleset, member.secondaryClassId, member.gender,
|
||||
);
|
||||
const resource = Math.max(0, Math.round(
|
||||
member.resource / Math.max(1, member.maxResource) * maxResource,
|
||||
));
|
||||
return withMemberAbilityResourcePools({
|
||||
...member,
|
||||
resourceMaximums: undefined,
|
||||
gearStats,
|
||||
health,
|
||||
maxHealth,
|
||||
@@ -367,9 +416,16 @@ function createMember(
|
||||
const race = racePool.find((candidate) => candidate.id === template.raceId)
|
||||
?? racePool[Math.floor(nextRandom(identityRandom) * racePool.length)] ?? racePool[0];
|
||||
const genders = supportedGenders(race.id);
|
||||
const gender: GenderId = genders[Math.floor(nextRandom(identityRandom) * genders.length)] ?? genders[0] ?? "male";
|
||||
const gender: GenderId = template.gender
|
||||
?? genders[Math.floor(nextRandom(identityRandom) * genders.length)]
|
||||
?? genders[0]
|
||||
?? "male";
|
||||
const appearance = randomAppearance(race.id, gender, () => nextRandom(identityRandom));
|
||||
const id = `party-${role}-${index}-${template.classId}`;
|
||||
const statRuleset = normalizeCharacterStatRuleset(
|
||||
template.statRuleset ?? "wow",
|
||||
template.classId,
|
||||
);
|
||||
const specializationId = specializationsForClass(template.classId)
|
||||
.find((definition) => definition.displayName === template.specialization)?.id ?? null;
|
||||
const gearStats = resolvedEquipmentStats({}, [], {
|
||||
@@ -381,9 +437,14 @@ function createMember(
|
||||
role,
|
||||
level,
|
||||
});
|
||||
const maxHealth = memberMaxHealth(level, role, gearStats, template.classId, race.id);
|
||||
const secondaryClassId = level >= ROM_SECONDARY_UNLOCK_LEVEL ? template.secondaryClassId ?? null : null;
|
||||
const maxHealth = memberMaxHealth(
|
||||
level, role, gearStats, template.classId, race.id, statRuleset, secondaryClassId, gender,
|
||||
);
|
||||
const resourceProfile = resourceProfileForClass(template.classId);
|
||||
const maxResource = memberMaxResource(template.classId, level, race.id, gearStats);
|
||||
const maxResource = memberMaxResource(
|
||||
template.classId, level, race.id, gearStats, statRuleset, secondaryClassId, gender,
|
||||
);
|
||||
const resource = Math.round(
|
||||
maxResource * (resourceProfile.maximum > 0 ? resourceProfile.initial / resourceProfile.maximum : 0),
|
||||
);
|
||||
@@ -394,7 +455,8 @@ function createMember(
|
||||
gender,
|
||||
appearance,
|
||||
classId: template.classId,
|
||||
secondaryClassId: level >= ROM_SECONDARY_UNLOCK_LEVEL ? template.secondaryClassId ?? null : null,
|
||||
statRuleset,
|
||||
secondaryClassId,
|
||||
specialization: template.specialization,
|
||||
specializationId,
|
||||
talentTreeId: template.talentTreeId,
|
||||
@@ -439,8 +501,10 @@ export function createConfiguredPartyMember(
|
||||
const member = createMember({
|
||||
name: configuration.name,
|
||||
classId: configuration.classId,
|
||||
statRuleset: configuration.statRuleset ?? "wow",
|
||||
secondaryClassId: configuration.secondaryClassId,
|
||||
raceId: configuration.raceId,
|
||||
gender: configuration.gender,
|
||||
specialization: configuration.specialization,
|
||||
talentTreeId: configuration.talentTreeId ?? configuration.specializationId ?? "",
|
||||
talentProfileId: configuration.talentProfileId ?? null,
|
||||
@@ -583,6 +647,7 @@ export interface PartyState {
|
||||
| "resource"
|
||||
| "maxResource"
|
||||
| "resourcePools"
|
||||
| "resourceMaximums"
|
||||
| "lastResourceSpentAt"
|
||||
| "cooldowns"
|
||||
| "globalCooldownEndsAt"
|
||||
@@ -678,12 +743,21 @@ export const usePartyStore = create<PartyState>((set, get) => ({
|
||||
if (state.members.every((member) => member.level === safeLevel)) return state;
|
||||
return {
|
||||
members: state.members.map((member) => {
|
||||
const nextSecondaryClassId = isRomClassId(member.classId)
|
||||
&& romFiveIdentity(member.classId, member.role)?.name === member.name
|
||||
? (safeLevel >= ROM_SECONDARY_UNLOCK_LEVEL
|
||||
? romFiveIdentity(member.classId, member.role)?.secondaryClassId ?? null
|
||||
: null)
|
||||
: member.secondaryClassId;
|
||||
const maxHealth = memberMaxHealth(
|
||||
safeLevel,
|
||||
member.role,
|
||||
member.gearStats,
|
||||
member.classId,
|
||||
member.raceId,
|
||||
member.statRuleset,
|
||||
nextSecondaryClassId,
|
||||
member.gender,
|
||||
);
|
||||
const health = member.health <= 0
|
||||
? 0
|
||||
@@ -693,19 +767,18 @@ export const usePartyStore = create<PartyState>((set, get) => ({
|
||||
safeLevel,
|
||||
member.raceId,
|
||||
member.gearStats,
|
||||
member.statRuleset,
|
||||
nextSecondaryClassId,
|
||||
member.gender,
|
||||
);
|
||||
const resource = Math.max(0, Math.round(
|
||||
member.resource / Math.max(1, member.maxResource) * maxResource,
|
||||
));
|
||||
return withMemberAbilityResourcePools({
|
||||
...member,
|
||||
resourceMaximums: undefined,
|
||||
level: safeLevel,
|
||||
secondaryClassId: isRomClassId(member.classId)
|
||||
&& romFiveIdentity(member.classId, member.role)?.name === member.name
|
||||
? (safeLevel >= ROM_SECONDARY_UNLOCK_LEVEL
|
||||
? romFiveIdentity(member.classId, member.role)?.secondaryClassId ?? null
|
||||
: null)
|
||||
: member.secondaryClassId,
|
||||
secondaryClassId: nextSecondaryClassId,
|
||||
health,
|
||||
maxHealth,
|
||||
resource,
|
||||
|
||||
Reference in new issue
Block a user