Skip to content
export const VIGOR_PER_RANK = 8;
export const BASE_SKILL_COST = 20;
export const SKILL_RANK_COST = 10;

export type SkillId = "energy" | "strength" | "critical" | "poison";

export interface HeroProgression {
  version: 1;
  vigor: number;
  skills: Record<SkillId, number>;
}

export const SKILLS: Record<
  SkillId,
  {
    name: string;
    description: string;
    effect: string;
    maxRank: number;
    glyph: string;
  }
> = {
  energy: {
    name: "Aether Well",
    description: "Begin every turn with more energy.",
    effect: "+1 energy per rank",
    maxRank: 2,
    glyph: "✦",
  },
  strength: {
    name: "Tempered Might",
    description: "Enter each battle with enhanced Strength.",
    effect: "+1 starting Strength per rank",
    maxRank: 5,
    glyph: "↑",
  },
  critical: {
    name: "Fateful Edge",
    description: "Attacks can deal double damage.",
    effect: "+5% critical chance per rank",
    maxRank: 5,
    glyph: "✧",
  },
  poison: {
    name: "Venomcraft",
    description: "Apply additional poison with every poison card.",
    effect: "+1 poison applied per rank",
    maxRank: 5,
    glyph: "☠",
  },
};

export const SKILL_IDS = Object.keys(SKILLS) as SkillId[];

export function initialProgression(): HeroProgression {
  return {
    version: 1,
    vigor: 0,
    skills: { energy: 0, strength: 0, critical: 0, poison: 0 },
  };
}

function rank(value: unknown, max = 999): number {
  return typeof value === "number" && Number.isFinite(value)
    ? Math.max(0, Math.min(max, Math.floor(value)))
    : 0;
}

export function parseProgression(
  value: unknown,
  level: number,
): HeroProgression {
  let parsed: unknown = value;
  if (typeof value === "string") {
    try {
      parsed = JSON.parse(value);
    } catch {
      parsed = null;
    }
  }
  if (!parsed || typeof parsed !== "object" || !("version" in parsed)) {
    return {
      ...initialProgression(),
      // Before attributes existed, every ascension granted one Vigor rank.
      vigor: Math.max(0, Math.floor(level) - 1),
    };
  }

  const candidate = parsed as {
    version?: unknown;
    vigor?: unknown;
    skills?: Record<string, unknown>;
  };
  if (candidate.version !== 1) return initialProgression();
  return {
    version: 1,
    vigor: rank(candidate.vigor),
    skills: Object.fromEntries(
      SKILL_IDS.map((id) => [
        id,
        rank(candidate.skills?.[id], SKILLS[id].maxRank),
      ]),
    ) as Record<SkillId, number>,
  };
}

export function progressionPoints(
  level: number,
  progression: HeroProgression,
): { attribute: number } {
  const earned = Math.max(0, Math.floor(level) - 1);
  return {
    attribute: Math.max(0, earned - progression.vigor),
  };
}

export function skillRankCost(currentRank: number): number {
  return BASE_SKILL_COST +
    SKILL_RANK_COST * Math.max(0, Math.floor(currentRank));
}

export function levelUpCost(level: number): number {
  return 25 + 15 * (Math.max(1, Math.floor(level)) - 1);
}

export function maxVigor(baseHp: number, progression: HeroProgression): number {
  return baseHp + progression.vigor * VIGOR_PER_RANK;
}

export function maxEnergy(baseEnergy: number, progression: HeroProgression) {
  return baseEnergy + progression.skills.energy;
}

export function criticalChance(progression: HeroProgression): number {
  return progression.skills.critical * 5 / 100;
}

export function progressionChange(
  previous: HeroProgression,
  next: HeroProgression,
): "unchanged" | "attribute" | "skill" | "invalid" {
  const vigorDelta = next.vigor - previous.vigor;
  const changedSkills = SKILL_IDS.filter((id) =>
    next.skills[id] !== previous.skills[id]
  );
  if (vigorDelta === 0 && changedSkills.length === 0) return "unchanged";
  if (vigorDelta === 1 && changedSkills.length === 0) return "attribute";
  if (
    vigorDelta === 0 && changedSkills.length === 1 &&
    next.skills[changedSkills[0]] === previous.skills[changedSkills[0]] + 1
  ) {
    return "skill";
  }
  return "invalid";
}

export function isValidProgression(
  level: number,
  progression: HeroProgression,
): boolean {
  const earned = Math.max(0, Math.floor(level) - 1);
  return progression.vigor <= earned &&
    SKILL_IDS.every((id) =>
      progression.skills[id] >= 0 &&
      progression.skills[id] <= SKILLS[id].maxRank
    );
}

export function serializeProgression(progression: HeroProgression): string {
  return JSON.stringify(progression);
}