4 Commits

Author SHA1 Message Date
hikari bd8ae930a5 balance: add crafting daily challenge type to unblock progression-stuck players (closes #167)
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m1s
CI / Lint, Build & Test (pull_request) Successful in 1m10s
2026-04-06 18:54:39 -07:00
hikari 55a521a759 balance: increase runestone yield and reduce income_10/11 costs (closes #166, #170) 2026-04-06 18:44:44 -07:00
hikari 4e2bc2cb98 balance: crystal economy improvements (closes #165, #173, #215)
Early game: first_steps and goblin_camp quests now award a small
crystal bonus so the crystal economy is visible from turn one.

Mid-game income: click_deity (1M clicks) 5k→15k, prestige_master
(P10) 5k→15k, prestige_legend (P25) 25k→75k to close the gap
before the first large crystal-cost upgrade.

Crystal sinks: add crystal_pulse (3k→×1.5), crystal_surge
(20k→×2), and crystal_tempest (150k→×3) global upgrades to fill
the dead zone between crystal_mastery (600 crystals) and the
existing 2M+ adventurer upgrades.

Closes #165
Closes #173
Closes #215
2026-04-06 18:38:30 -07:00
hikari b5eff7de31 balance: cap quest failure rates at 15% (closes #172)
Proportionally scaled all zoneFailureChance values from the old
4%-40% range down to 4%-15%, preserving the relative gradient
across zones. A 7-hour quest failing 40% of the time was too
punishing; 15% max keeps risk meaningful without being cruel.

Closes #172
2026-04-06 18:28:05 -07:00
19 changed files with 32 additions and 197 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@elysium/api", "name": "@elysium/api",
"version": "0.5.0", "version": "0.4.0",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./prod/src/index.js", "main": "./prod/src/index.js",
+1 -1
View File
@@ -21,7 +21,7 @@ export const defaultAdventurers: Array<Adventurer> = [
unlocked: true, unlocked: true,
}, },
{ {
baseCost: 65, baseCost: 100,
class: "warrior", class: "warrior",
combatPower: 3, combatPower: 3,
count: 0, count: 0,
+1 -1
View File
@@ -269,7 +269,7 @@ export const defaultEquipment: Array<Equipment> = [
type: "trinket", type: "trinket",
}, },
{ {
bonus: { clickMultiplier: 1.9, goldMultiplier: 1.3 }, bonus: { clickMultiplier: 1.65, goldMultiplier: 1.2 },
description: description:
"A fragment of the Mud Kraken's crystallised essence. Focuses raw power into devastating strikes.", "A fragment of the Mud Kraken's crystallised essence. Focuses raw power into devastating strikes.",
equipped: false, equipped: false,
+4 -4
View File
@@ -323,7 +323,7 @@ export const defaultRecipes: Array<CraftingRecipe> = [
// Zone 13: primordial_chaos // Zone 13: primordial_chaos
{ {
bonus: { type: "click_power", value: 1.22 }, bonus: { type: "click_power", value: 1.2 },
description: description:
"Chaos fragments and creation shards arranged into a lens that hasn't decided what it wants to focus on yet, which somehow makes every click land harder than it should.", "Chaos fragments and creation shards arranged into a lens that hasn't decided what it wants to focus on yet, which somehow makes every click land harder than it should.",
id: "chaos_lens", id: "chaos_lens",
@@ -387,7 +387,7 @@ export const defaultRecipes: Array<CraftingRecipe> = [
zoneId: "reality_forge", zoneId: "reality_forge",
}, },
{ {
bonus: { type: "click_power", value: 1.25 }, bonus: { type: "click_power", value: 1.22 },
description: description:
"A reality shard carefully shaped with creation tools into something that could, theoretically, become a universe. Instead it makes your clicks unreasonably effective.", "A reality shard carefully shaped with creation tools into something that could, theoretically, become a universe. Instead it makes your clicks unreasonably effective.",
id: "universe_seed", id: "universe_seed",
@@ -439,7 +439,7 @@ export const defaultRecipes: Array<CraftingRecipe> = [
zoneId: "primeval_sanctum", zoneId: "primeval_sanctum",
}, },
{ {
bonus: { type: "click_power", value: 1.28 }, bonus: { type: "click_power", value: 1.25 },
description: description:
"The primeval relic, set into a memory shard framework. What function it originally served is unknowable. In your guild's hands, it makes every action more deliberate and more powerful.", "The primeval relic, set into a memory shard framework. What function it originally served is unknowable. In your guild's hands, it makes every action more deliberate and more powerful.",
id: "first_artefact", id: "first_artefact",
@@ -522,7 +522,7 @@ export const defaultRecipes: Array<CraftingRecipe> = [
// Zone 18: the_absolute // Zone 18: the_absolute
{ {
bonus: { type: "click_power", value: 1.3 }, bonus: { type: "click_power", value: 1.28 },
description: description:
"Absolute fragments ground and set in an omega crystal lattice — an instrument of pure finality. Every action your guild takes through it carries the weight of an ending. It does not miss.", "Absolute fragments ground and set in an omega crystal lattice — an instrument of pure finality. Every action your guild takes through it carries the weight of an ending. It does not miss.",
id: "absolute_focus", id: "absolute_focus",
+6 -10
View File
@@ -35,16 +35,12 @@ export const authMiddleware: MiddlewareHandler<HonoEnvironment> = async(
const payload = verifyToken(token); const payload = verifyToken(token);
context.set("discordId", payload.discordId); context.set("discordId", payload.discordId);
} catch (error) { } catch (error) {
const isExpiredToken void logger.error(
= error instanceof Error && error.message === "Token has expired"; "auth_middleware",
if (!isExpiredToken) { error instanceof Error
void logger.error( ? error
"auth_middleware", : new Error(String(error)),
error instanceof Error );
? error
: new Error(String(error)),
);
}
return context.json({ error: "Invalid or expired token" }, 401); return context.json({ error: "Invalid or expired token" }, 401);
} }
-20
View File
@@ -9,7 +9,6 @@
/* eslint-disable complexity -- Boss handler has inherent complexity */ /* eslint-disable complexity -- Boss handler has inherent complexity */
/* eslint-disable stylistic/max-len -- Long lines in combat logic */ /* eslint-disable stylistic/max-len -- Long lines in combat logic */
/* eslint-disable max-lines -- Boss route with full combat logic and helpers exceeds line limit */ /* eslint-disable max-lines -- Boss route with full combat logic and helpers exceeds line limit */
import { createHmac } from "node:crypto";
import { import {
computeSetBonuses, computeSetBonuses,
getActiveCompanionBonus, getActiveCompanionBonus,
@@ -26,17 +25,6 @@ import { updateChallengeProgress } from "../services/dailyChallenges.js";
import { logger } from "../services/logger.js"; import { logger } from "../services/logger.js";
import type { HonoEnvironment } from "../types/hono.js"; import type { HonoEnvironment } from "../types/hono.js";
/**
* Computes the HMAC-SHA256 of data using the given secret.
* @param data - The data string to sign.
* @param secret - The HMAC secret key.
* @returns The hex-encoded HMAC digest.
*/
const computeHmac = (data: string, secret: string): string => {
return createHmac("sha256", secret).update(data).
digest("hex");
};
/** /**
* Exponential base for the prestige combat multiplier: Math.pow(base, prestigeCount). * Exponential base for the prestige combat multiplier: Math.pow(base, prestigeCount).
* Replaces the former linear formula (1 + count * 0.1) to enable late-game zone progression. * Replaces the former linear formula (1 + count * 0.1) to enable late-game zone progression.
@@ -391,11 +379,6 @@ bossRouter.post("/challenge", async(context) => {
where: { discordId }, where: { discordId },
}); });
const secret = process.env.ANTI_CHEAT_SECRET;
const updatedSignature = secret === undefined
? undefined
: computeHmac(JSON.stringify(state), secret);
const { bossId } = body; const { bossId } = body;
void logger.metric("boss_challenge", 1, { bossId, discordId, won }); void logger.metric("boss_challenge", 1, { bossId, discordId, won });
@@ -418,9 +401,6 @@ bossRouter.post("/challenge", async(context) => {
if (casualties !== undefined) { if (casualties !== undefined) {
response.casualties = casualties; response.casualties = casualties;
} }
if (updatedSignature !== undefined) {
response.signature = updatedSignature;
}
return context.json(response); return context.json(response);
} catch (error) { } catch (error) {
+1 -42
View File
@@ -681,45 +681,6 @@ const validateAndSanitize = (
storySpread = { story: previous.story }; storySpread = { story: previous.story };
} }
/*
* Merge daily challenge progress: take the maximum progress for each
* challenge so a stale auto-save arriving after a craft/boss/etc. update
* cannot silently roll back server-side challenge completions.
*/
// eslint-disable-next-line capitalized-comments -- v8 ignore
/* v8 ignore next 35 -- @preserve */
let dailyChallengesSpread: object = {};
// eslint-disable-next-line stylistic/max-len -- Long condition; splitting would reduce readability
if (incoming.dailyChallenges !== undefined && previous.dailyChallenges !== undefined) {
const previousChallengeMap = new Map(
previous.dailyChallenges.challenges.map((challenge) => {
return [ challenge.id, challenge ];
}),
);
// eslint-disable-next-line stylistic/max-len -- Long chain; splitting would reduce readability
const mergedChallenges = incoming.dailyChallenges.challenges.map((challenge) => {
const serverChallenge = previousChallengeMap.get(challenge.id);
if (serverChallenge === undefined) {
return challenge;
}
// eslint-disable-next-line stylistic/max-len -- Long expression; splitting would reduce readability
const bestProgress = Math.max(challenge.progress, serverChallenge.progress);
return {
...challenge,
completed: bestProgress >= challenge.target,
progress: bestProgress,
};
});
dailyChallengesSpread = {
dailyChallenges: {
...incoming.dailyChallenges,
challenges: mergedChallenges,
},
};
} else if (previous.dailyChallenges !== undefined) {
dailyChallengesSpread = { dailyChallenges: previous.dailyChallenges };
}
return { return {
...incoming, ...incoming,
achievements, achievements,
@@ -732,7 +693,6 @@ const validateAndSanitize = (
...apotheosisSpread, ...apotheosisSpread,
...explorationSpread, ...explorationSpread,
...storySpread, ...storySpread,
...dailyChallengesSpread,
}; };
}; };
@@ -1064,8 +1024,7 @@ gameRouter.post("/save", async(context) => {
const companionUnlocks = computeUnlockedCompanionIds({ const companionUnlocks = computeUnlockedCompanionIds({
apotheosisCount: stateToSave.apotheosis?.count ?? 0, apotheosisCount: stateToSave.apotheosis?.count ?? 0,
lifetimeBossesDefeated: playerRecord?.lifetimeBossesDefeated ?? 0, lifetimeBossesDefeated: playerRecord?.lifetimeBossesDefeated ?? 0,
// eslint-disable-next-line stylistic/max-len -- Long property; splitting would reduce readability lifetimeGoldEarned: playerRecord?.lifetimeGoldEarned ?? 0,
lifetimeGoldEarned: (playerRecord?.lifetimeGoldEarned ?? 0) + stateToSave.player.totalGoldEarned,
lifetimeQuestsCompleted: playerRecord?.lifetimeQuestsCompleted ?? 0, lifetimeQuestsCompleted: playerRecord?.lifetimeQuestsCompleted ?? 0,
prestigeCount: stateToSave.prestige.count, prestigeCount: stateToSave.prestige.count,
transcendenceCount: stateToSave.transcendence?.count ?? 0, transcendenceCount: stateToSave.transcendence?.count ?? 0,
+5 -36
View File
@@ -6,26 +6,18 @@ vi.mock("../../src/services/jwt.js", () => ({
verifyToken: vi.fn(), verifyToken: vi.fn(),
})); }));
vi.mock("../../src/services/logger.js", () => ({
logger: {
error: vi.fn().mockResolvedValue(undefined),
},
}));
describe("authMiddleware", () => { describe("authMiddleware", () => {
beforeEach(() => { beforeEach(() => {
vi.resetModules(); vi.resetModules();
vi.clearAllMocks();
}); });
const makeApp = async () => { const makeApp = async () => {
const { authMiddleware } = await import("../../src/middleware/auth.js"); const { authMiddleware } = await import("../../src/middleware/auth.js");
const { verifyToken } = await import("../../src/services/jwt.js"); const { verifyToken } = await import("../../src/services/jwt.js");
const { logger } = await import("../../src/services/logger.js");
const app = new Hono<{ Variables: { discordId: string } }>(); const app = new Hono<{ Variables: { discordId: string } }>();
app.use("*", authMiddleware); app.use("*", authMiddleware);
app.get("/test", (c) => c.json({ discordId: c.get("discordId") })); app.get("/test", (c) => c.json({ discordId: c.get("discordId") }));
return { app, logger, verifyToken }; return { app, verifyToken };
}; };
it("returns 401 when Authorization header is missing", async () => { it("returns 401 when Authorization header is missing", async () => {
@@ -53,8 +45,8 @@ describe("authMiddleware", () => {
expect(body.discordId).toBe("user_123"); expect(body.discordId).toBe("user_123");
}); });
it("returns 401 and logs when verifyToken throws a non-expiry error", async () => { it("returns 401 when verifyToken throws", async () => {
const { app, logger, verifyToken } = await makeApp(); const { app, verifyToken } = await makeApp();
vi.mocked(verifyToken).mockImplementationOnce(() => { vi.mocked(verifyToken).mockImplementationOnce(() => {
throw new Error("Invalid token"); throw new Error("Invalid token");
}); });
@@ -62,15 +54,10 @@ describe("authMiddleware", () => {
headers: { Authorization: "Bearer bad_token" }, headers: { Authorization: "Bearer bad_token" },
})); }));
expect(res.status).toBe(401); expect(res.status).toBe(401);
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- logger mock requires cast */
expect((logger.error as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith(
"auth_middleware",
expect.any(Error),
);
}); });
it("returns 401 and logs when verifyToken throws a non-Error value", async () => { it("returns 401 when verifyToken throws a non-Error value", async () => {
const { app, logger, verifyToken } = await makeApp(); const { app, verifyToken } = await makeApp();
vi.mocked(verifyToken).mockImplementationOnce(() => { vi.mocked(verifyToken).mockImplementationOnce(() => {
throw "raw string error"; throw "raw string error";
}); });
@@ -78,23 +65,5 @@ describe("authMiddleware", () => {
headers: { Authorization: "Bearer bad_token" }, headers: { Authorization: "Bearer bad_token" },
})); }));
expect(res.status).toBe(401); expect(res.status).toBe(401);
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- logger mock requires cast */
expect((logger.error as ReturnType<typeof vi.fn>)).toHaveBeenCalledWith(
"auth_middleware",
expect.any(Error),
);
});
it("returns 401 without logging when token has expired", async () => {
const { app, logger, verifyToken } = await makeApp();
vi.mocked(verifyToken).mockImplementationOnce(() => {
throw new Error("Token has expired");
});
const res = await app.fetch(new Request("http://localhost/test", {
headers: { Authorization: "Bearer expired_token" },
}));
expect(res.status).toBe(401);
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- logger mock requires cast */
expect((logger.error as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
}); });
}); });
-31
View File
@@ -340,37 +340,6 @@ describe("boss route", () => {
expect(area?.status).toBe("locked"); expect(area?.status).toBe("locked");
}); });
it("includes HMAC signature in response when ANTI_CHEAT_SECRET is set", async () => {
process.env.ANTI_CHEAT_SECRET = "test_secret";
const state = makeState({
bosses: [makeBoss()] as GameState["bosses"],
adventurers: [makeAdventurer()] as GameState["adventurers"],
zones: [],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await challenge({ bossId: "test_boss" });
expect(res.status).toBe(200);
const body = await res.json() as { signature: string | undefined };
expect(body.signature).toBeDefined();
delete process.env.ANTI_CHEAT_SECRET;
});
it("omits signature in response when ANTI_CHEAT_SECRET is not set", async () => {
delete process.env.ANTI_CHEAT_SECRET;
const state = makeState({
bosses: [makeBoss()] as GameState["bosses"],
adventurers: [makeAdventurer()] as GameState["adventurers"],
zones: [],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await challenge({ bossId: "test_boss" });
expect(res.status).toBe(200);
const body = await res.json() as { signature: string | undefined };
expect(body.signature).toBeUndefined();
});
it("returns 500 when the database throws", async () => { it("returns 500 when the database throws", async () => {
vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error")); vi.mocked(prisma.gameState.findUnique).mockRejectedValueOnce(new Error("DB error"));
const res = await challenge({ bossId: "test_boss" }); const res = await challenge({ bossId: "test_boss" });
+1 -1
View File
@@ -597,7 +597,7 @@ describe("debug route", () => {
it("patches adventurer stats when only name has changed (exercises all earlier OR conditions)", async () => { it("patches adventurer stats when only name has changed (exercises all earlier OR conditions)", async () => {
const state = makeState({ const state = makeState({
adventurers: [{ id: "militia", count: 5, unlocked: true, baseCost: 65, goldPerSecond: 0.7, essencePerSecond: 0, combatPower: 3, level: 2, name: "Old Name", class: "warrior" }] as GameState["adventurers"], adventurers: [{ id: "militia", count: 5, unlocked: true, baseCost: 100, goldPerSecond: 0.7, essencePerSecond: 0, combatPower: 3, level: 2, name: "Old Name", class: "warrior" }] as GameState["adventurers"],
}); });
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never); vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never); vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@elysium/web", "name": "@elysium/web",
"version": "0.5.0", "version": "0.4.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
-5
View File
@@ -92,11 +92,6 @@ const fetchJson = async <T>(
= typeof errorBody.error === "string" = typeof errorBody.error === "string"
? errorBody.error ? errorBody.error
: "Unknown error"; : "Unknown error";
if (response.status === 401) {
globalThis.localStorage.removeItem("elysium_token");
globalThis.localStorage.removeItem("elysium_save_signature");
globalThis.location.href = "/";
}
if (response.status >= 400 && response.status < 500) { if (response.status >= 400 && response.status < 500) {
throw new ValidationError(message, response.status); throw new ValidationError(message, response.status);
} }
@@ -163,8 +163,7 @@ const CompanionPanel = (): JSX.Element => {
const progressByUnlockType: Record<string, number> = { const progressByUnlockType: Record<string, number> = {
apotheosis: state.apotheosis?.count ?? 0, apotheosis: state.apotheosis?.count ?? 0,
lifetimeBosses: state.player.lifetimeBossesDefeated, lifetimeBosses: state.player.lifetimeBossesDefeated,
// eslint-disable-next-line stylistic/max-len -- Long expression; splitting would reduce readability lifetimeGold: state.player.lifetimeGoldEarned,
lifetimeGold: state.player.lifetimeGoldEarned + state.player.totalGoldEarned,
lifetimeQuests: state.player.lifetimeQuestsCompleted, lifetimeQuests: state.player.lifetimeQuestsCompleted,
prestige: state.prestige.count, prestige: state.prestige.count,
transcendence: state.transcendence?.count ?? 0, transcendence: state.transcendence?.count ?? 0,
+1 -3
View File
@@ -114,9 +114,6 @@ const QuestCard = ({
} }
<div className="quest-rewards"> <div className="quest-rewards">
{quest.rewards.map((reward, rewardIndex) => { {quest.rewards.map((reward, rewardIndex) => {
if (reward.type === "crystals" && (reward.amount ?? 0) === 0) {
return null;
}
return ( return (
<span className="reward-tag" key={`${reward.type}-${reward.targetId ?? String(reward.amount ?? rewardIndex)}`}> <span className="reward-tag" key={`${reward.type}-${reward.targetId ?? String(reward.amount ?? rewardIndex)}`}>
{reward.type === "gold" {reward.type === "gold"
@@ -124,6 +121,7 @@ const QuestCard = ({
{reward.type === "essence" {reward.type === "essence"
&& `${formatNumber(reward.amount ?? 0)}`} && `${formatNumber(reward.amount ?? 0)}`}
{reward.type === "crystals" {reward.type === "crystals"
&& (reward.amount ?? 0) > 0
&& `💎 ${formatNumber(reward.amount ?? 0)}`} && `💎 ${formatNumber(reward.amount ?? 0)}`}
{reward.type === "upgrade" && "🔓 Upgrade"} {reward.type === "upgrade" && "🔓 Upgrade"}
{reward.type === "adventurer" && "👥 New Adventurer"} {reward.type === "adventurer" && "👥 New Adventurer"}
+1 -1
View File
@@ -218,7 +218,7 @@ const ResourceBar = ({
: ""}`}> : ""}`}>
<span className="resource-icon">{"💎"}</span> <span className="resource-icon">{"💎"}</span>
<span className="resource-value"> <span className="resource-value">
{formatNumber(crystals)} {formatInteger(crystals)}
</span> </span>
<span className="resource-label">{"Crystals"}</span> <span className="resource-label">{"Crystals"}</span>
{crystalsFull {crystalsFull
+6 -31
View File
@@ -1356,11 +1356,10 @@ export const GameProvider = ({
newlyFailedQuestsReference.current = []; newlyFailedQuestsReference.current = [];
} }
// Auto-save every 30 seconds (skip if a force sync or auto-prestige is in-flight to avoid signature collisions) // Auto-save every 30 seconds (skip if a force sync is in-flight to avoid signature collisions)
if (Date.now() - lastSaveReference.current >= autoSaveIntervalMs) { if (Date.now() - lastSaveReference.current >= autoSaveIntervalMs) {
lastSaveReference.current = Date.now(); lastSaveReference.current = Date.now();
// eslint-disable-next-line stylistic/max-len -- Long condition; splitting would reduce readability if (stateReference.current !== null && !isSyncingReference.current) {
if (stateReference.current !== null && !isSyncingReference.current && !isAutoPrestigingReference.current) {
void saveGame({ void saveGame({
state: stateReference.current, state: stateReference.current,
...signatureReference.current === null ...signatureReference.current === null
@@ -1497,20 +1496,11 @@ export const GameProvider = ({
}); });
/* /*
* Boss fight modifies server state; update signature chain so * Boss fight modifies server state; clear stale signature so
* the next pre-save or auto-save sends the correct token. * the next pre-save or auto-save does not send a mismatched one.
*/ */
if (result.signature === undefined) { signatureReference.current = null;
signatureReference.current = null; localStorage.removeItem("elysium_save_signature");
localStorage.removeItem("elysium_save_signature");
} else {
signatureReference.current = result.signature;
localStorage.setItem(
"elysium_save_signature",
result.signature,
);
}
lastSaveReference.current = Date.now();
setAutoBossLastResult({ setAutoBossLastResult({
at: Date.now(), at: Date.now(),
bossName: bossName, bossName: bossName,
@@ -1857,13 +1847,6 @@ export const GameProvider = ({
}, },
}; };
}); });
/*
* Buying a prestige upgrade mutates DB state; clear the cached signature
* so the next auto-save doesn't collide with a stale one.
*/
signatureReference.current = null;
localStorage.removeItem("elysium_save_signature");
} catch (error_: unknown) { } catch (error_: unknown) {
logError("buy_prestige_upgrade", error_); logError("buy_prestige_upgrade", error_);
// Silently ignore — server errors shouldn't crash the UI // Silently ignore — server errors shouldn't crash the UI
@@ -2194,14 +2177,6 @@ export const GameProvider = ({
} }
return applyBossResult(previous, bossId, result); return applyBossResult(previous, bossId, result);
}); });
if (result.signature === undefined) {
signatureReference.current = null;
localStorage.removeItem("elysium_save_signature");
} else {
signatureReference.current = result.signature;
localStorage.setItem("elysium_save_signature", result.signature);
}
lastSaveReference.current = Date.now();
setBattleResult({ bossName: boss.name, result: result }); setBattleResult({ bossName: boss.name, result: result });
} catch (error_: unknown) { } catch (error_: unknown) {
const bossErrorMessage const bossErrorMessage
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "elysium", "name": "elysium",
"version": "0.5.0", "version": "0.4.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@elysium/types", "name": "@elysium/types",
"version": "0.5.0", "version": "0.4.0",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./prod/src/index.js", "main": "./prod/src/index.js",
-5
View File
@@ -170,11 +170,6 @@ interface BossChallengeResponse {
adventurerId: string; adventurerId: string;
killed: number; killed: number;
}>; }>;
/**
* HMAC-SHA256 signature of the updated state for anti-cheat chain continuity.
*/
signature?: string;
} }
type PrestigeRequest = Record<string, never>; type PrestigeRequest = Record<string, never>;