generated from nhcarrigan/template
fix: runestone formula, prestige/transcendence rebalance, exploration fixes, and comprehensive balance audit #135
@@ -147,17 +147,30 @@ prestigeRouter.post("/", async(context) => {
|
|||||||
|
|
||||||
const prestigeCount = prestigeData.count;
|
const prestigeCount = prestigeData.count;
|
||||||
void logger.metric("prestige", 1, { discordId, prestigeCount });
|
void logger.metric("prestige", 1, { discordId, prestigeCount });
|
||||||
void postMilestoneWebhook(discordId, "prestige", {
|
|
||||||
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
|
||||||
/* v8 ignore next -- @preserve */
|
|
||||||
apotheosis: prestigeState.apotheosis?.count ?? 0,
|
|
||||||
|
|
||||||
prestige: prestigeData.count,
|
const playerRecord = await prisma.player.findUnique({
|
||||||
|
select: { profileSettings: true },
|
||||||
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
where: { discordId },
|
||||||
/* v8 ignore next 2 -- @preserve */
|
|
||||||
transcendence: prestigeState.transcendence?.count ?? 0,
|
|
||||||
});
|
});
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Runtime shape check for JSON field */
|
||||||
|
const playerSettings = playerRecord?.profileSettings as
|
||||||
|
Record<string, unknown> | null | undefined;
|
||||||
|
const announcementsEnabled
|
||||||
|
= playerSettings?.enablePrestigeAnnouncements !== false;
|
||||||
|
|
||||||
|
if (announcementsEnabled) {
|
||||||
|
void postMilestoneWebhook(discordId, "prestige", {
|
||||||
|
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
||||||
|
/* v8 ignore next -- @preserve */
|
||||||
|
apotheosis: prestigeState.apotheosis?.count ?? 0,
|
||||||
|
|
||||||
|
prestige: prestigeData.count,
|
||||||
|
|
||||||
|
// eslint-disable-next-line capitalized-comments -- v8 ignore
|
||||||
|
/* v8 ignore next 2 -- @preserve */
|
||||||
|
transcendence: prestigeState.transcendence?.count ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return context.json({
|
return context.json({
|
||||||
milestoneRunestones: milestoneRunestones,
|
milestoneRunestones: milestoneRunestones,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const parseProfileSettings = (raw: unknown): ProfileSettings => {
|
|||||||
: "suffix";
|
: "suffix";
|
||||||
return {
|
return {
|
||||||
enableNotifications: rawObject.enableNotifications === true,
|
enableNotifications: rawObject.enableNotifications === true,
|
||||||
|
enablePrestigeAnnouncements: rawObject.enablePrestigeAnnouncements !== false,
|
||||||
enableSounds: rawObject.enableSounds === true,
|
enableSounds: rawObject.enableSounds === true,
|
||||||
numberFormat: numberFormat,
|
numberFormat: numberFormat,
|
||||||
showAchievementsUnlocked: rawObject.showAchievementsUnlocked !== false,
|
showAchievementsUnlocked: rawObject.showAchievementsUnlocked !== false,
|
||||||
@@ -222,6 +223,7 @@ profileRouter.put("/", authMiddleware, async(context) => {
|
|||||||
: "suffix";
|
: "suffix";
|
||||||
const profileSettings: ProfileSettings = {
|
const profileSettings: ProfileSettings = {
|
||||||
enableNotifications: body.profileSettings.enableNotifications ?? false,
|
enableNotifications: body.profileSettings.enableNotifications ?? false,
|
||||||
|
enablePrestigeAnnouncements: body.profileSettings.enablePrestigeAnnouncements ?? true,
|
||||||
enableSounds: body.profileSettings.enableSounds ?? false,
|
enableSounds: body.profileSettings.enableSounds ?? false,
|
||||||
numberFormat: numberFormat,
|
numberFormat: numberFormat,
|
||||||
showAchievementsUnlocked: body.profileSettings.showAchievementsUnlocked ?? true,
|
showAchievementsUnlocked: body.profileSettings.showAchievementsUnlocked ?? true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { GameState } from "@elysium/types";
|
|||||||
|
|
||||||
vi.mock("../../src/db/client.js", () => ({
|
vi.mock("../../src/db/client.js", () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
player: { update: vi.fn() },
|
player: { findUnique: vi.fn(), update: vi.fn() },
|
||||||
gameState: { findUnique: vi.fn(), update: vi.fn(), updateMany: vi.fn() },
|
gameState: { findUnique: vi.fn(), update: vi.fn(), updateMany: vi.fn() },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -47,7 +47,7 @@ const makeState = (overrides: Partial<GameState> = {}): GameState => ({
|
|||||||
describe("prestige route", () => {
|
describe("prestige route", () => {
|
||||||
let app: Hono;
|
let app: Hono;
|
||||||
let prisma: {
|
let prisma: {
|
||||||
player: { update: ReturnType<typeof vi.fn> };
|
player: { findUnique: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn> };
|
||||||
gameState: { findUnique: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn>; updateMany: ReturnType<typeof vi.fn> };
|
gameState: { findUnique: ReturnType<typeof vi.fn>; update: ReturnType<typeof vi.fn>; updateMany: ReturnType<typeof vi.fn> };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,6 +128,18 @@ describe("prestige route", () => {
|
|||||||
const body = await res.json() as { runestones: number; newPrestigeCount: number };
|
const body = await res.json() as { runestones: number; newPrestigeCount: number };
|
||||||
expect(body.newPrestigeCount).toBe(1);
|
expect(body.newPrestigeCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips webhook when enablePrestigeAnnouncements is false", async () => {
|
||||||
|
const { postMilestoneWebhook } = await import("../../src/services/webhook.js");
|
||||||
|
const state = makeState();
|
||||||
|
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state, updatedAt: 0 } as never);
|
||||||
|
vi.mocked(prisma.gameState.updateMany).mockResolvedValueOnce({ count: 1 } as never);
|
||||||
|
vi.mocked(prisma.player.update).mockResolvedValueOnce({} as never);
|
||||||
|
vi.mocked(prisma.player.findUnique).mockResolvedValueOnce({ profileSettings: { enablePrestigeAnnouncements: false } } as never);
|
||||||
|
const res = await post("");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(postMilestoneWebhook).not.toHaveBeenCalledWith(expect.anything(), "prestige", expect.anything());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("POST /buy-upgrade", () => {
|
describe("POST /buy-upgrade", () => {
|
||||||
|
|||||||
@@ -225,6 +225,10 @@ const EditProfileModal = ({
|
|||||||
void handleNotificationsEnable();
|
void handleNotificationsEnable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handlePrestigeAnnouncementsToggle(): void {
|
||||||
|
toggleSetting("enablePrestigeAnnouncements");
|
||||||
|
}
|
||||||
|
|
||||||
const isSaveDisabled = saving || characterName.trim() === "";
|
const isSaveDisabled = saving || characterName.trim() === "";
|
||||||
|
|
||||||
let saveLabel = "Save Profile";
|
let saveLabel = "Save Profile";
|
||||||
@@ -417,6 +421,23 @@ const EditProfileModal = ({
|
|||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={`stat-toggle-btn ${
|
||||||
|
profileSettings.enablePrestigeAnnouncements
|
||||||
|
? "stat-toggle-on"
|
||||||
|
: "stat-toggle-off"
|
||||||
|
}`}
|
||||||
|
onClick={handlePrestigeAnnouncementsToggle}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span>{"⭐ Prestige Bot Announcements"}</span>
|
||||||
|
<span className="stat-toggle-indicator">
|
||||||
|
{profileSettings.enablePrestigeAnnouncements
|
||||||
|
? "✓ On"
|
||||||
|
: "Off"
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="edit-profile-section">
|
<div className="edit-profile-section">
|
||||||
|
|||||||
@@ -48,11 +48,17 @@ interface ProfileSettings {
|
|||||||
* Whether browser system notifications are enabled.
|
* Whether browser system notifications are enabled.
|
||||||
*/
|
*/
|
||||||
enableNotifications: boolean;
|
enableNotifications: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether prestige milestones are announced in the Discord server.
|
||||||
|
*/
|
||||||
|
enablePrestigeAnnouncements: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- intentional constant name
|
// eslint-disable-next-line @typescript-eslint/naming-convention -- intentional constant name
|
||||||
const DEFAULT_PROFILE_SETTINGS: ProfileSettings = {
|
const DEFAULT_PROFILE_SETTINGS: ProfileSettings = {
|
||||||
enableNotifications: false,
|
enableNotifications: false,
|
||||||
|
enablePrestigeAnnouncements: true,
|
||||||
enableSounds: false,
|
enableSounds: false,
|
||||||
numberFormat: "suffix",
|
numberFormat: "suffix",
|
||||||
showAchievementsUnlocked: true,
|
showAchievementsUnlocked: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user