Skip to content

Commit acfc27a

Browse files
committed
quests
1 parent fe049ac commit acfc27a

23 files changed

Lines changed: 3103 additions & 830 deletions

File tree

apps/api/src/__tests__/quest-data-integrity.spec.ts

Lines changed: 616 additions & 0 deletions
Large diffs are not rendered by default.

apps/api/src/abilities/abilities.input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export class CreateAbilitySavingThrowInput {
238238
@Field(() => Int)
239239
abilityId: number;
240240

241-
@Field(() => SaveType, { defaultValue: SaveType.SPELL })
241+
@Field(() => SaveType, { defaultValue: SaveType.WILL })
242242
saveType: SaveType;
243243

244244
@Field()
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/**
2+
* Combat dice formulas ported from FieryMUD legacy C++ code.
3+
* These calculate default HP and damage dice based on level, race, and class.
4+
*
5+
* The legacy MUD stores dice MODIFIERS in mob files, not absolute values.
6+
* Base dice are calculated from level with race/class factors applied.
7+
*/
8+
9+
/**
10+
* Race dice factors - determines base damage scaling by race.
11+
* Higher values = more physical damage focus.
12+
* Values from legacy/src/races.cpp
13+
*/
14+
export const RACE_DICE_FACTORS: Record<string, number> = {
15+
// Player races
16+
HUMAN: 100,
17+
ELF: 80,
18+
DWARF: 100,
19+
HALFLING: 80,
20+
GNOME: 75,
21+
TROLL: 120,
22+
DROW: 80,
23+
OGRE: 120,
24+
ORC: 100,
25+
HALF_ELF: 90,
26+
HALF_ORC: 100,
27+
BARBARIAN: 110,
28+
DUERGAR: 100,
29+
SVERFNEBLIN: 90,
30+
NYMPH: 70,
31+
DRAGONBORN: 100,
32+
DRAGONBORN_FIRE: 100,
33+
DRAGONBORN_FROST: 100,
34+
DRAGONBORN_ACID: 100,
35+
DRAGONBORN_LIGHTNING: 100,
36+
DRAGONBORN_GAS: 100,
37+
// Monster races
38+
HUMANOID: 100,
39+
ANIMAL: 100,
40+
UNDEAD: 100,
41+
DRAGON: 120,
42+
DRAGON_GENERAL: 120,
43+
DRAGON_FIRE: 120,
44+
DRAGON_FROST: 120,
45+
DRAGON_ACID: 120,
46+
DRAGON_LIGHTNING: 120,
47+
DRAGON_GAS: 120,
48+
GIANT: 120,
49+
DEMON: 110,
50+
ELEMENTAL: 100,
51+
PLANT: 80,
52+
FAERIE: 70,
53+
CELESTIAL: 100,
54+
ABERRATION: 100,
55+
GOLEM: 120,
56+
GOBLIN: 80,
57+
INSECT: 80,
58+
FISH: 60,
59+
BROWNIE: 70,
60+
OTHER: 100,
61+
};
62+
63+
/**
64+
* Class dice factors - determines base damage scaling by class.
65+
* Higher values = more physical damage focus.
66+
* Values from legacy/src/class.cpp
67+
*/
68+
export const CLASS_DICE_FACTORS: Record<string, number> = {
69+
// Melee classes (high damage)
70+
WARRIOR: 120,
71+
PALADIN: 120,
72+
ANTI_PALADIN: 120,
73+
MERCENARY: 120,
74+
BERSERKER: 120,
75+
// Hybrid classes
76+
RANGER: 100,
77+
MONK: 100,
78+
ROGUE: 100,
79+
THIEF: 100,
80+
ASSASSIN: 100,
81+
HUNTER: 100,
82+
// Caster classes (low physical damage)
83+
SORCERER: 80,
84+
CLERIC: 80,
85+
DRUID: 80,
86+
SHAMAN: 80,
87+
NECROMANCER: 80,
88+
CONJURER: 80,
89+
BARD: 80,
90+
PYROMANCER: 80,
91+
CRYOMANCER: 80,
92+
ILLUSIONIST: 80,
93+
PRIEST: 80,
94+
DIABOLIST: 80,
95+
MYSTIC: 80,
96+
// Default
97+
LAYMAN: 100,
98+
};
99+
100+
/**
101+
* Get the dice factor for a race.
102+
* @param race - The race name (case-insensitive)
103+
* @returns The dice factor (default 100 if unknown)
104+
*/
105+
export function getRaceDiceFactor(race: string): number {
106+
return RACE_DICE_FACTORS[race.toUpperCase()] ?? 100;
107+
}
108+
109+
/**
110+
* Get the dice factor for a class.
111+
* @param className - The class name (case-insensitive)
112+
* @returns The dice factor (default 100 if unknown)
113+
*/
114+
export function getClassDiceFactor(className: string): number {
115+
return CLASS_DICE_FACTORS[className.toUpperCase()] ?? 100;
116+
}
117+
118+
/**
119+
* Calculate the combined dice factor from race and class.
120+
* Uses the average of race and class factors.
121+
* @param raceFactor - The race dice factor (50-150)
122+
* @param classFactor - The class dice factor (50-150)
123+
* @returns Combined factor (50-150)
124+
*/
125+
export function getCombinedDiceFactor(
126+
raceFactor: number,
127+
classFactor: number
128+
): number {
129+
return Math.floor((raceFactor + classFactor) / 2);
130+
}
131+
132+
/**
133+
* Calculate base HP dice (hit points) based on level.
134+
* Formula from legacy get_set_hp() in magic.cpp
135+
*
136+
* @param level - Mob level (1-99)
137+
* @returns Object with { num, size, bonus } for HP dice
138+
*/
139+
export function calculateHpDice(level: number): {
140+
num: number;
141+
size: number;
142+
bonus: number;
143+
} {
144+
const clampedLevel = Math.max(1, Math.min(99, level));
145+
146+
// Base HP formula: level determines dice count, size is fixed
147+
// Low levels: fewer dice, higher levels: many dice
148+
const num = Math.floor(clampedLevel / 2.5 + 0.5);
149+
const size = 8; // Standard d8 for HP
150+
151+
// Bonus scales with level for consistent HP totals
152+
const bonus = Math.floor(clampedLevel * 2);
153+
154+
return { num: Math.max(1, num), size, bonus };
155+
}
156+
157+
/**
158+
* Calculate base damage dice based on level and race/class factors.
159+
* Formula from legacy get_set_dice() in magic.cpp
160+
*
161+
* @param level - Mob level (1-99)
162+
* @param raceFactor - Race dice factor (default 100)
163+
* @param classFactor - Class dice factor (default 100)
164+
* @returns Object with { num, size } for damage dice
165+
*/
166+
export function calculateDamageDice(
167+
level: number,
168+
raceFactor: number = 100,
169+
classFactor: number = 100
170+
): { num: number; size: number } {
171+
const clampedLevel = Math.max(1, Math.min(99, level));
172+
const combinedFactor = getCombinedDiceFactor(raceFactor, classFactor);
173+
174+
// Base dice calculation from level
175+
// Formula: (level / 2.5 + 0.5) scaled by combined factor
176+
const baseDice = Math.floor(clampedLevel / 2.5 + 0.5);
177+
const scaledDice = Math.floor((baseDice * combinedFactor) / 100);
178+
179+
// Dice size scales with level tiers
180+
let size: number;
181+
if (clampedLevel <= 10) {
182+
size = 6;
183+
} else if (clampedLevel <= 30) {
184+
size = 8;
185+
} else if (clampedLevel <= 60) {
186+
size = 10;
187+
} else {
188+
size = 12;
189+
}
190+
191+
return { num: Math.max(1, scaledDice), size };
192+
}
193+
194+
/**
195+
* Calculate base damage bonus (damroll) based on level and race/class factors.
196+
* Formula from legacy get_set_hd() in magic.cpp
197+
*
198+
* @param level - Mob level (1-99)
199+
* @param raceFactor - Race dice factor (default 100)
200+
* @param classFactor - Class dice factor (default 100)
201+
* @returns The damage bonus value
202+
*/
203+
export function calculateDamageBonus(
204+
level: number,
205+
raceFactor: number = 100,
206+
classFactor: number = 100
207+
): number {
208+
const clampedLevel = Math.max(1, Math.min(99, level));
209+
const combinedFactor = getCombinedDiceFactor(raceFactor, classFactor);
210+
211+
// Base damroll formula from level
212+
const baseDamroll = Math.floor(clampedLevel / 2.5 + 0.5);
213+
return Math.floor((baseDamroll * combinedFactor) / 100);
214+
}
215+
216+
/**
217+
* Calculate all default combat stats for a mob based on level, race, and class.
218+
* This is the main entry point for calculating mob defaults.
219+
*
220+
* @param level - Mob level (1-99)
221+
* @param race - Race name (e.g., "HUMANOID", "DRAGON")
222+
* @param className - Class name (e.g., "WARRIOR", "SORCERER"), optional
223+
* @returns Object with hpDice and damageDice formatted as strings
224+
*/
225+
export function calculateMobCombatDefaults(
226+
level: number,
227+
race: string,
228+
className?: string
229+
): {
230+
hpDice: string;
231+
damageDice: string;
232+
damageDiceNum: number;
233+
damageDiceSize: number;
234+
damageDiceBonus: number;
235+
hpDiceNum: number;
236+
hpDiceSize: number;
237+
hpDiceBonus: number;
238+
} {
239+
const raceFactor = getRaceDiceFactor(race);
240+
const classFactor = className ? getClassDiceFactor(className) : 100;
241+
242+
const hp = calculateHpDice(level);
243+
const dmg = calculateDamageDice(level, raceFactor, classFactor);
244+
const bonus = calculateDamageBonus(level, raceFactor, classFactor);
245+
246+
// Format dice strings
247+
const hpBonus = hp.bonus >= 0 ? `+${hp.bonus}` : `${hp.bonus}`;
248+
const dmgBonus = bonus >= 0 ? `+${bonus}` : `${bonus}`;
249+
250+
return {
251+
hpDice: `${hp.num}d${hp.size}${hpBonus}`,
252+
damageDice: `${dmg.num}d${dmg.size}${dmgBonus}`,
253+
hpDiceNum: hp.num,
254+
hpDiceSize: hp.size,
255+
hpDiceBonus: hp.bonus,
256+
damageDiceNum: dmg.num,
257+
damageDiceSize: dmg.size,
258+
damageDiceBonus: bonus,
259+
};
260+
}

apps/api/src/common/mappers/__tests__/object.mapper.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ function base(): ObjectMapperSource {
1010
keywords: ['a', 'b'],
1111
name: 'Obj',
1212
plainName: 'Obj',
13+
baseName: 'Obj',
14+
plainBaseName: 'Obj',
15+
article: 'a',
1316
roomDescription: 'Room desc',
1417
plainRoomDescription: 'Room desc',
1518
examineDescription: 'Exam desc',

apps/api/src/mobs/mob.dto.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -336,13 +336,23 @@ export class CreateMobInput {
336336
@IsNumber()
337337
resistancePoison?: number;
338338

339-
@Field()
339+
@Field({
340+
nullable: true,
341+
description:
342+
'HP dice (e.g., "10d8+50"). If not provided, calculated from level/race/class.',
343+
})
344+
@IsOptional()
340345
@IsString()
341-
hpDice: string;
346+
hpDice?: string;
342347

343-
@Field()
348+
@Field({
349+
nullable: true,
350+
description:
351+
'Damage dice (e.g., "5d10+20"). If not provided, calculated from level/race/class.',
352+
})
353+
@IsOptional()
344354
@IsString()
345-
damageDice: string;
355+
damageDice?: string;
346356

347357
@Field(() => DamageType, { defaultValue: DamageType.HIT })
348358
@IsOptional()
@@ -447,6 +457,33 @@ export class CreateMobInput {
447457
classId?: number;
448458
}
449459

460+
@ObjectType()
461+
export class MobCombatDefaultsDto {
462+
@Field()
463+
hpDice: string;
464+
465+
@Field()
466+
damageDice: string;
467+
468+
@Field(() => Int)
469+
hpDiceNum: number;
470+
471+
@Field(() => Int)
472+
hpDiceSize: number;
473+
474+
@Field(() => Int)
475+
hpDiceBonus: number;
476+
477+
@Field(() => Int)
478+
damageDiceNum: number;
479+
480+
@Field(() => Int)
481+
damageDiceSize: number;
482+
483+
@Field(() => Int)
484+
damageDiceBonus: number;
485+
}
486+
450487
@InputType()
451488
export class UpdateMobInput {
452489
@Field(() => [String], { nullable: true })

0 commit comments

Comments
 (0)