import { define } from "../../../utils.ts";
import { CARDS } from "../../../lib/game.ts";
import { getHero, saveHeroProgress } from "../../../lib/heroes.ts";
import {
isValidProgression,
levelUpCost,
parseProgression,
progressionChange,
SKILL_IDS,
skillRankCost,
} from "../../../lib/progression.ts";
export const handler = define.handlers({
async POST(ctx) {
const user = ctx.state.user;
if (!user) return new Response("Unauthorized", { status: 401 });
const hero = await getHero(user.id);
if (!hero) return Response.json({ error: "No hero." }, { status: 404 });
let body: unknown;
try {
body = await ctx.req.json();
} catch {
return Response.json({ error: "Invalid JSON." }, { status: 400 });
}
const {
anima,
level,
progression,
deck,
expectedAnima,
expectedLevel,
expectedProgression,
expectedDeck,
} = (body ?? {}) as {
anima?: unknown;
level?: unknown;
progression?: unknown;
deck?: unknown;
expectedAnima?: unknown;
expectedLevel?: unknown;
expectedProgression?: unknown;
expectedDeck?: unknown;
};
if (
typeof anima !== "number" || typeof level !== "number" ||
!Array.isArray(deck) || typeof expectedAnima !== "number" ||
typeof expectedLevel !== "number" || !Array.isArray(expectedDeck) ||
!progression || typeof progression !== "object" ||
!expectedProgression || typeof expectedProgression !== "object"
) {
return Response.json({ error: "Invalid payload." }, { status: 400 });
}
const validCard = (id: string) =>
CARDS[id.startsWith("+") ? id.slice(1) : id];
if (
[...deck, ...expectedDeck].some((c) =>
typeof c !== "string" || !validCard(c)
)
) {
return Response.json({ error: "Unknown card in deck." }, { status: 400 });
}
const nextDeck = deck as string[];
const previousDeck = expectedDeck as string[];
if (
expectedAnima !== hero.anima || expectedLevel !== hero.level ||
JSON.stringify(previousDeck) !== JSON.stringify(hero.deck)
) {
return Response.json(
{ error: "Game state changed. Reload before saving again." },
{ status: 409 },
);
}
const previousProgression = parseProgression(
expectedProgression,
expectedLevel,
);
const nextProgression = parseProgression(progression, level);
if (
JSON.stringify(previousProgression) !==
JSON.stringify(hero.progression) ||
!isValidProgression(level, nextProgression)
) {
return Response.json({ error: "Invalid progression." }, { status: 400 });
}
const progressionTransition = progressionChange(
previousProgression,
nextProgression,
);
const changed = nextDeck.flatMap((id, index) =>
id === previousDeck[index] ? [] : [index]
);
const appended = nextDeck.length === previousDeck.length + 1 &&
nextDeck.slice(0, -1).every((id, index) => id === previousDeck[index]);
const upgraded = nextDeck.length === previousDeck.length &&
changed.length === 1 &&
nextDeck[changed[0]] === `+${previousDeck[changed[0]]}`;
const unchanged = nextDeck.length === previousDeck.length &&
changed.length === 0;
const addedId = appended ? nextDeck.at(-1)! : null;
const addedCard = addedId && !addedId.startsWith("+")
? CARDS[addedId]
: null;
const classAllowed = Boolean(
addedCard &&
(addedCard.klass === "any" || addedCard.klass === hero.class),
);
const sameProgression = progressionTransition === "unchanged";
const learnedSkill = progressionTransition === "skill"
? SKILL_IDS.find((id) =>
nextProgression.skills[id] !== previousProgression.skills[id]
)
: undefined;
const levelUp = expectedLevel < 999 && level === expectedLevel + 1 &&
anima === expectedAnima - levelUpCost(expectedLevel) && unchanged &&
sameProgression;
const cardUpgrade = level === expectedLevel && upgraded &&
sameProgression &&
anima === expectedAnima - (20 + 10 * expectedLevel);
const reward = level === expectedLevel && appended && classAllowed &&
sameProgression &&
anima === expectedAnima;
const progressOnly = level === expectedLevel && unchanged &&
sameProgression &&
anima >= expectedAnima;
const attributeSpent = level === expectedLevel && unchanged &&
anima === expectedAnima && progressionTransition === "attribute";
const learnedSkillCost = learnedSkill === undefined
? undefined
: skillRankCost(previousProgression.skills[learnedSkill]);
const skillPurchased = level === expectedLevel && unchanged &&
learnedSkillCost !== undefined && expectedAnima >= learnedSkillCost &&
anima === expectedAnima - learnedSkillCost;
if (
!levelUp && !cardUpgrade && !reward && !progressOnly &&
!attributeSpent && !skillPurchased
) {
return Response.json({ error: "Invalid progress transition." }, {
status: 400,
});
}
const saved = await saveHeroProgress(user.id, {
anima,
level,
progression: nextProgression,
deck: nextDeck,
expectedAnima,
expectedLevel,
expectedProgression: previousProgression,
expectedDeck: expectedDeck as string[],
});
if (!saved) {
return Response.json(
{ error: "Game state changed. Reload before saving again." },
{ status: 409 },
);
}
return Response.json({ ok: true });
},
});