feat(renown): add the Renown power-curve table and reputation-rank resolver
This commit is contained in:
29
apps/api/src/renown/renown-base-stats.spec.ts
Normal file
29
apps/api/src/renown/renown-base-stats.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { RENOWN_BASE_STATS, RENOWN_MAX, RENOWN_MIN } from './renown-base-stats';
|
||||||
|
|
||||||
|
describe('RENOWN_BASE_STATS', () => {
|
||||||
|
it('spans exactly Renown 1 through 15', () => {
|
||||||
|
expect(RENOWN_MIN).toBe(1);
|
||||||
|
expect(RENOWN_MAX).toBe(15);
|
||||||
|
expect(Object.keys(RENOWN_BASE_STATS).map(Number).sort((a, b) => a - b)).toEqual(
|
||||||
|
Array.from({ length: 15 }, (_, i) => i + 1),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches the exact V1 reference table from spec §4', () => {
|
||||||
|
expect(RENOWN_BASE_STATS[1]).toEqual({ baseHp: 100, baseAttack: 6 });
|
||||||
|
expect(RENOWN_BASE_STATS[5]).toEqual({ baseHp: 114, baseAttack: 8 });
|
||||||
|
expect(RENOWN_BASE_STATS[10]).toEqual({ baseHp: 132, baseAttack: 10 });
|
||||||
|
expect(RENOWN_BASE_STATS[15]).toEqual({ baseHp: 148, baseAttack: 12 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is monotonically non-decreasing in both stats as Renown increases', () => {
|
||||||
|
for (let renown = 2; renown <= 15; renown += 1) {
|
||||||
|
expect(RENOWN_BASE_STATS[renown].baseHp).toBeGreaterThanOrEqual(
|
||||||
|
RENOWN_BASE_STATS[renown - 1].baseHp,
|
||||||
|
);
|
||||||
|
expect(RENOWN_BASE_STATS[renown].baseAttack).toBeGreaterThanOrEqual(
|
||||||
|
RENOWN_BASE_STATS[renown - 1].baseAttack,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
24
apps/api/src/renown/renown-base-stats.ts
Normal file
24
apps/api/src/renown/renown-base-stats.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export const RENOWN_MIN = 1;
|
||||||
|
export const RENOWN_MAX = 15;
|
||||||
|
|
||||||
|
// Verbatim from Playable Slice 0.6.5 spec §4. Distributes the old Level 1-7
|
||||||
|
// total base-stat progression (100 HP / 6 Attack -> 148 HP / 12 Attack)
|
||||||
|
// across Renown 1-15. RenownService looks this up on every milestone
|
||||||
|
// completion; it is never incremented, only reassigned by rank (design R2).
|
||||||
|
export const RENOWN_BASE_STATS: Record<number, { baseHp: number; baseAttack: number }> = {
|
||||||
|
1: { baseHp: 100, baseAttack: 6 },
|
||||||
|
2: { baseHp: 104, baseAttack: 6 },
|
||||||
|
3: { baseHp: 107, baseAttack: 7 },
|
||||||
|
4: { baseHp: 111, baseAttack: 7 },
|
||||||
|
5: { baseHp: 114, baseAttack: 8 },
|
||||||
|
6: { baseHp: 118, baseAttack: 8 },
|
||||||
|
7: { baseHp: 121, baseAttack: 9 },
|
||||||
|
8: { baseHp: 125, baseAttack: 9 },
|
||||||
|
9: { baseHp: 128, baseAttack: 9 },
|
||||||
|
10: { baseHp: 132, baseAttack: 10 },
|
||||||
|
11: { baseHp: 135, baseAttack: 10 },
|
||||||
|
12: { baseHp: 139, baseAttack: 11 },
|
||||||
|
13: { baseHp: 142, baseAttack: 11 },
|
||||||
|
14: { baseHp: 145, baseAttack: 11 },
|
||||||
|
15: { baseHp: 148, baseAttack: 12 },
|
||||||
|
};
|
||||||
33
apps/api/src/reputation/reputation-rank.spec.ts
Normal file
33
apps/api/src/reputation/reputation-rank.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { resolveReputationRank } from './reputation-rank';
|
||||||
|
|
||||||
|
describe('resolveReputationRank', () => {
|
||||||
|
it('resolves 0 reputation to Stranger with a next threshold of 100', () => {
|
||||||
|
expect(resolveReputationRank(0)).toEqual({
|
||||||
|
key: 'STRANGER',
|
||||||
|
label: 'Fremder',
|
||||||
|
threshold: 0,
|
||||||
|
nextThreshold: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves exactly at a threshold to that rank, not the one below', () => {
|
||||||
|
expect(resolveReputationRank(100).key).toBe('TOLERATED');
|
||||||
|
expect(resolveReputationRank(99).key).toBe('STRANGER');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves every documented threshold to its exact rank', () => {
|
||||||
|
expect(resolveReputationRank(250).key).toBe('KNOWN');
|
||||||
|
expect(resolveReputationRank(500).key).toBe('RECOGNIZED');
|
||||||
|
expect(resolveReputationRank(800).key).toBe('TRUSTED');
|
||||||
|
expect(resolveReputationRank(1200).key).toBe('ESTEEMED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has no next threshold once at the top rank', () => {
|
||||||
|
expect(resolveReputationRank(1200).nextThreshold).toBeNull();
|
||||||
|
expect(resolveReputationRank(50_000).nextThreshold).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports the correct nextThreshold mid-range', () => {
|
||||||
|
expect(resolveReputationRank(300).nextThreshold).toBe(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
30
apps/api/src/reputation/reputation-rank.ts
Normal file
30
apps/api/src/reputation/reputation-rank.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export interface ReputationRankInfo {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
threshold: number;
|
||||||
|
nextThreshold: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verbatim thresholds from spec §10, German labels for the German-language UI.
|
||||||
|
// Sorted descending: the first entry whose threshold <= reputation wins.
|
||||||
|
const REPUTATION_RANKS: ReadonlyArray<{ threshold: number; key: string; label: string }> = [
|
||||||
|
{ threshold: 1200, key: 'ESTEEMED', label: 'Geachtet' },
|
||||||
|
{ threshold: 800, key: 'TRUSTED', label: 'Vertraut' },
|
||||||
|
{ threshold: 500, key: 'RECOGNIZED', label: 'Anerkannt' },
|
||||||
|
{ threshold: 250, key: 'KNOWN', label: 'Bekannt' },
|
||||||
|
{ threshold: 100, key: 'TOLERATED', label: 'Geduldet' },
|
||||||
|
{ threshold: 0, key: 'STRANGER', label: 'Fremder' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function resolveReputationRank(reputation: number): ReputationRankInfo {
|
||||||
|
const index = REPUTATION_RANKS.findIndex((rank) => reputation >= rank.threshold);
|
||||||
|
const rank = REPUTATION_RANKS[index];
|
||||||
|
const nextRank = index > 0 ? REPUTATION_RANKS[index - 1] : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: rank.key,
|
||||||
|
label: rank.label,
|
||||||
|
threshold: rank.threshold,
|
||||||
|
nextThreshold: nextRank ? nextRank.threshold : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user