import type { ClassId } from "./heroes.ts";
export {
criticalChance,
levelUpCost,
maxEnergy,
maxVigor,
progressionPoints,
SKILLS,
} from "./progression.ts";
import strikeArt from "../assets/icons/ffffff/transparent/1x1/lorc/broadsword.svg";
import defendArt from "../assets/icons/ffffff/transparent/1x1/lorc/riot-shield.svg";
import shieldBashArt from "../assets/icons/ffffff/transparent/1x1/delapouite/shield-bash.svg";
import bulwarkArt from "../assets/icons/ffffff/transparent/1x1/lorc/bordered-shield.svg";
import riposteArt from "../assets/icons/ffffff/transparent/1x1/lorc/shield-reflect.svg";
import sentinelOathArt from "../assets/icons/ffffff/transparent/1x1/lorc/winged-shield.svg";
import hexBoltArt from "../assets/icons/ffffff/transparent/1x1/lorc/magic-swirl.svg";
import creepingDreadArt from "../assets/icons/ffffff/transparent/1x1/lorc/poison-gas.svg";
import siphonArt from "../assets/icons/ffffff/transparent/1x1/lorc/life-tap.svg";
import moonRitualArt from "../assets/icons/ffffff/transparent/1x1/lorc/moon-claws.svg";
import backstabArt from "../assets/icons/ffffff/transparent/1x1/lorc/backstab.svg";
import vanishArt from "../assets/icons/ffffff/transparent/1x1/lorc/cloak-dagger.svg";
import fanOfKnivesArt from "../assets/icons/ffffff/transparent/1x1/lorc/all-for-one.svg";
import gildedFlourishArt from "../assets/icons/ffffff/transparent/1x1/lorc/sword-spin.svg";
import maidenArt from "../assets/icons/ffffff/transparent/1x1/delapouite/viking-shield.svg";
import witchArt from "../assets/icons/ffffff/transparent/1x1/delapouite/woman-elf-face.svg";
import assassinArt from "../assets/icons/ffffff/transparent/1x1/darkzaitzev/hooded-assassin.svg";
import brassSentinelArt from "../assets/icons/ffffff/transparent/1x1/lorc/robot-golem.svg";
import clockworkDivaArt from "../assets/icons/ffffff/transparent/1x1/lorc/sing.svg";
import gildedThugArt from "../assets/icons/ffffff/transparent/1x1/lorc/mailed-fist.svg";
import moonHagArt from "../assets/icons/ffffff/transparent/1x1/cathelineau/witch-face.svg";
import obsidianMatronArt from "../assets/icons/ffffff/transparent/1x1/lorc/reaper-scythe.svg";
import sunburstSovereignArt from "../assets/icons/ffffff/transparent/1x1/lorc/queen-crown.svg";
import velvetFencerArt from "../assets/icons/ffffff/transparent/1x1/cathelineau/swordwoman.svg";
export type { ClassId };
export const CLASSES: Record<
ClassId,
{
id: ClassId;
name: string;
title: string;
art: string;
maxHp: number;
energy: number;
handSize: number;
blurb: string;
signature: string[];
}
> = {
maiden: {
id: "maiden",
name: "Shield Maiden",
title: "The Immovable Protector",
art: maidenArt,
maxHp: 80,
energy: 3,
handSize: 5,
blurb:
"A living bulwark of ivory and brass. She turns the storm aside and answers it in kind.",
signature: ["shield_bash", "bulwark", "riposte"],
},
witch: {
id: "witch",
name: "Witch",
title: "Weaver of Hexes",
art: witchArt,
maxHp: 64,
energy: 3,
handSize: 5,
blurb:
"She does not strike — she lingers. Curses bloom slowly, then all at once.",
signature: ["hex_bolt", "creeping_dread", "siphon"],
},
assassin: {
id: "assassin",
name: "Assassin",
title: "Whisper of the Blade",
art: assassinArt,
maxHp: 58,
energy: 3,
handSize: 5,
blurb:
"Seen once, in the moment the knife is already sheathed. Burst, vanish, repeat.",
signature: ["backstab", "vanish", "fan_of_knives"],
},
};
export interface CardEffect {
damage?: number;
hits?: number;
block?: number;
heal?: number;
draw?: number;
energyGain?: number;
poison?: number;
weak?: number;
strength?: number;
evade?: number;
retaliate?: number;
anima?: number;
aoe?: boolean;
}
export interface CardDef {
id: string;
name: string;
art: string;
klass: ClassId | "any";
type: "physical" | "defense" | "poison" | "arcane";
cost: number;
text: string;
effects: CardEffect;
rare?: boolean;
}
export const CARDS: Record<string, CardDef> = {
strike: {
id: "strike",
name: "Strike",
art: strikeArt,
klass: "any",
type: "physical",
cost: 1,
text: "Deal 6 damage.",
effects: { damage: 6 },
},
defend: {
id: "defend",
name: "Defend",
art: defendArt,
klass: "any",
type: "defense",
cost: 1,
text: "Gain 5 block.",
effects: { block: 5 },
},
shield_bash: {
id: "shield_bash",
name: "Shield Bash",
art: shieldBashArt,
klass: "maiden",
type: "physical",
cost: 1,
text: "Deal 4 damage. Gain 4 block.",
effects: { damage: 4, block: 4 },
},
bulwark: {
id: "bulwark",
name: "Bulwark",
art: bulwarkArt,
klass: "maiden",
type: "defense",
cost: 2,
text: "Gain 12 block. Gain 1 strength.",
effects: { block: 12, strength: 1 },
},
riposte: {
id: "riposte",
name: "Riposte",
art: riposteArt,
klass: "maiden",
type: "defense",
cost: 1,
text: "Gain 5 block. Deal 4 damage to any attacker this turn.",
effects: { block: 5, retaliate: 4 },
rare: true,
},
sentinel_oath: {
id: "sentinel_oath",
name: "Sentinel's Oath",
art: sentinelOathArt,
klass: "maiden",
type: "defense",
cost: 1,
text: "Gain 7 block. Draw 1 card.",
effects: { block: 7, draw: 1 },
},
hex_bolt: {
id: "hex_bolt",
name: "Hex Bolt",
art: hexBoltArt,
klass: "witch",
type: "poison",
cost: 1,
text: "Deal 3 damage. Apply 3 poison.",
effects: { damage: 3, poison: 3 },
},
creeping_dread: {
id: "creeping_dread",
name: "Creeping Dread",
art: creepingDreadArt,
klass: "witch",
type: "poison",
cost: 1,
text: "Apply 4 poison. Apply 1 weak.",
effects: { poison: 4, weak: 1 },
},
siphon: {
id: "siphon",
name: "Siphon",
art: siphonArt,
klass: "witch",
type: "arcane",
cost: 2,
text: "Deal 8 damage. Heal 4.",
effects: { damage: 8, heal: 4 },
},
moon_ritual: {
id: "moon_ritual",
name: "Moon Ritual",
art: moonRitualArt,
klass: "witch",
type: "arcane",
cost: 0,
text: "Draw 1 card. Gain 1 anima.",
effects: { draw: 1, anima: 2 },
rare: true,
},
backstab: {
id: "backstab",
name: "Backstab",
art: backstabArt,
klass: "assassin",
type: "physical",
cost: 0,
text: "Deal 9 damage. Exhausting speed — costs nothing, draws nothing.",
effects: { damage: 9 },
},
vanish: {
id: "vanish",
name: "Vanish",
art: vanishArt,
klass: "assassin",
type: "defense",
cost: 1,
text: "Gain 4 block. Evade the next 2 attacks.",
effects: { block: 4, evade: 2 },
},
fan_of_knives: {
id: "fan_of_knives",
name: "Fan of Knives",
art: fanOfKnivesArt,
klass: "assassin",
type: "physical",
cost: 1,
text: "Deal 3 damage twice to ALL enemies.",
effects: { damage: 3, hits: 2, aoe: true },
},
gilded_flourish: {
id: "gilded_flourish",
name: "Gilded Flourish",
art: gildedFlourishArt,
klass: "assassin",
type: "physical",
cost: 1,
text: "Deal 5 damage. Gain 1 strength.",
effects: { damage: 5, strength: 1 },
rare: true,
},
};
export interface CardInstance {
uid: number;
def: CardDef;
upgraded: boolean;
}
let uidCounter = 0;
export function makeCard(id: string): CardInstance | null {
const def = CARDS[id];
if (!def) return null;
return { uid: ++uidCounter, def, upgraded: false };
}
const UPGRADE_MULT = 1.5;
function round(n: number): number {
return Math.max(1, Math.round(n));
}
export function upgradeEffect(effects: CardEffect): CardEffect {
const out: CardEffect = { ...effects };
for (
const key of [
"damage",
"block",
"heal",
"poison",
"weak",
"strength",
"evade",
"retaliate",
"anima",
] as const
) {
const value = out[key];
if (typeof value === "number") {
// deno-lint-ignore no-explicit-any
out[key] = round(value * UPGRADE_MULT) as any;
}
}
return out;
}
/** Starting deck per design: small, consistent, class-flavored. */
export function startingDeck(klass: ClassId): string[] {
return [
...Array(4).fill("strike"),
...Array(4).fill("defend"),
...CLASSES[klass].signature.slice(0, 2),
];
}
// ---------------------------------------------------------------------------
// Enemies
// ---------------------------------------------------------------------------
export interface EnemyMoveDef {
kind: "attack" | "block" | "buff" | "hex";
damage?: number;
hits?: number;
block?: number;
strength?: number;
poison?: number;
label: string;
}
export interface EnemyTemplate {
name: string;
art: string;
hp: number;
moves: EnemyMoveDef[];
}
export const ENEMY_TEMPLATES: Record<string, EnemyTemplate> = {
gilded_thug: {
name: "Gilded Thug",
art: gildedThugArt,
hp: 22,
moves: [
{ kind: "attack", damage: 7, label: "Cudgel" },
{ kind: "attack", damage: 4, hits: 2, label: "Flurry" },
],
},
brass_sentinel: {
name: "Brass Sentinel",
art: brassSentinelArt,
hp: 30,
moves: [
{ kind: "block", block: 8, label: "Harden" },
{ kind: "attack", damage: 10, label: "Gavel Fist" },
],
},
moon_hag: {
name: "Moon Hag",
art: moonHagArt,
hp: 20,
moves: [
{ kind: "hex", poison: 4, label: "Bitter Draught" },
{ kind: "attack", damage: 6, label: "Claw" },
{ kind: "buff", strength: 2, label: "Work Up" },
],
},
velvet_fencer: {
name: "Velvet Fencer",
art: velvetFencerArt,
hp: 24,
moves: [
{ kind: "attack", damage: 5, hits: 3, label: "Triple Lunge" },
{ kind: "block", block: 6, label: "Parry Stance" },
],
},
};
export const ELITE_TEMPLATES: EnemyTemplate[] = [
{
name: "Obsidian Matron",
art: obsidianMatronArt,
hp: 46,
moves: [
{ kind: "attack", damage: 9, hits: 2, label: "Twin Scythes" },
{ kind: "hex", poison: 5, label: "Withering Gaze" },
{ kind: "block", block: 10, label: "Umbral Veil" },
],
},
{
name: "Clockwork Diva",
art: clockworkDivaArt,
hp: 52,
moves: [
{ kind: "block", block: 12, label: "Gilded Aria" },
{ kind: "attack", damage: 14, label: "Fortissimo" },
{ kind: "buff", strength: 3, label: "Crescendo" },
],
},
];
export const BOSS_TEMPLATES: EnemyTemplate[] = [
{
name: "The Sunburst Sovereign",
art: sunburstSovereignArt,
hp: 90,
moves: [
{ kind: "attack", damage: 12, hits: 2, label: "Radiant Verdict" },
{ kind: "block", block: 15, label: "Solar Court" },
{ kind: "hex", poison: 6, label: "Golden Rot" },
{ kind: "buff", strength: 3, label: "Coronation" },
],
},
];
export function scaleHp(base: number, depth: number): number {
return Math.round(base * (1 + 0.28 * depth));
}
export function scaleDamage(base: number, depth: number): number {
return Math.round(base * (1 + 0.18 * depth));
}
// ---------------------------------------------------------------------------
// Dungeon map
// ---------------------------------------------------------------------------
export type NodeKind =
| "fight"
| "elite"
| "treasure"
| "rest"
| "boss";
export interface MapNode {
kind: NodeKind;
done: boolean;
}
export const MAP_FLOORS = 6;