import { ALL_STATS, Skill, Stat, SuccessorOption, Wish } from "./datatypes.ts"; import { getSkills } from "./skills.ts"; import { getThralls, ItemStage, LifeStage, Thrall } from "./thralls.ts"; export class PlayerProgress { #name: string; #thrallTemplate: number; #nImprovements: number; #stats: Record; #talents: Record; #isInPenance: boolean; #wish: Wish | null; #exp: number; #blood: number; #itemsPurloined: number; #skillsLearned: number[]; // use the raw ID representation for indexOf #untrimmedSkillsAvailable: Skill[]; #thrallsUnlocked: number[]; #thrallDamage: Record; #thrallsObtainedItem: number[]; #thrallsDeliveredItem: number[]; constructor(asSuccessor: SuccessorOption, withWish: Wish | null) { this.#name = asSuccessor.name; this.#thrallTemplate = asSuccessor.template.id; this.#nImprovements = asSuccessor.nImprovements; this.#stats = { ...asSuccessor.stats }; this.#talents = { ...asSuccessor.talents }; this.#isInPenance = asSuccessor.inPenance; this.#wish = withWish; this.#exp = 0; this.#blood = 0; this.#itemsPurloined = 0; this.#skillsLearned = []; this.#untrimmedSkillsAvailable = []; this.#thrallsUnlocked = []; this.#thrallDamage = {}; this.#thrallsObtainedItem = []; this.#thrallsDeliveredItem = []; this.refill(); } applyEndOfTurn() { for (let stat of ALL_STATS.values()) { this.add(stat, this.#talents[stat]); } } get name(): string { return this.#name; } get template(): Thrall { return { id: this.#thrallTemplate }; } get nImprovements(): number { return this.#nImprovements; } get isInPenance(): boolean { return this.#isInPenance; } refill() { this.#blood = 1000; let learnableSkills = []; // TODO: Also include costing info for (let skill of getSkills() .getAvailableSkills(this.#isInPenance) .values()) { if (this.#canBeAvailable(skill)) { learnableSkills.push(skill); } } for (let thrall of getThralls().getAll()) { let stage = this.getThrallLifeStage(thrall); if (stage == LifeStage.Vampirized || stage == LifeStage.Dead) { continue; } this.#thrallDamage[thrall.id] = Math.max( this.#thrallDamage[thrall.id] ?? 0 - 0.2, 0.0, ); } this.#untrimmedSkillsAvailable = learnableSkills; } hasLearned(skill: Skill) { return this.#skillsLearned.indexOf(skill.id) !== -1; } learnSkill(skill: Skill) { if (this.#skillsLearned.indexOf(skill.id) != -1) { return; } this.#skillsLearned.push(skill.id); // remove entries for that skill let skills2 = []; for (let entry of this.#untrimmedSkillsAvailable.values()) { if (entry.id == skill.id) { continue; } skills2.push(entry); } this.#untrimmedSkillsAvailable = skills2; } #canBeAvailable(skill: Skill) { // make sure we haven't learned this skill already if (this.hasLearned(skill)) { return false; } let data = getSkills().get(skill); // make sure the prereqs are met for (let prereq of data.prereqs.values()) { if (!this.hasLearned(prereq)) { return false; } } // ok, we're good!! return true; } purloinItem() { this.#itemsPurloined += 1; } getItemsPurloined() { return this.#itemsPurloined; } add(stat: Stat, amount: number) { if (amount != Math.floor(amount)) { throw `stat increment must be integer: ${amount}`; } this.#stats[stat] += amount; this.#stats[stat] = Math.min(Math.max(this.#stats[stat], -99), 999); } addExperience(amt: number) { this.#exp += amt; } getExperience(): number { return this.#exp; } spendExperience(cost: number) { if (this.#exp < cost) { throw `can't spend ${cost}`; } this.#exp -= cost; } getStat(stat: Stat): number { return this.#stats[stat]; } getTalent(stat: Stat): number { return this.#talents[stat]; } getBlood(): number { return Math.floor(Math.max(this.#blood, 0)); } addBlood(amt: number) { this.#blood += amt; this.#blood = Math.min(this.#blood, 5000); } spendBlood(amt: number) { this.#blood -= amt; } getWish(): Wish | null { return this.#wish; } getAvailableSkills(): Skill[] { // Sort by cost, then by name, then trim down to first 6 let skillsAvailable = [...this.#untrimmedSkillsAvailable]; skillsAvailable.sort((a, b) => { let name1 = getSkills().get(a).profile.name; let name2 = getSkills().get(b).profile.name; if (name1 < name2) { return -1; } if (name1 > name2) { return 1; } return 0; }); skillsAvailable.sort((a, b) => { return getSkills().computeCost(a) - getSkills().computeCost(b); }); return skillsAvailable.slice(0, 6); } getUntrimmedAvailableSkillIds(): number[] { return this.#untrimmedSkillsAvailable.map((s) => s.id); } getLearnedSkills() : Skill[] { let learnedSkills = []; for (let s of this.#skillsLearned.values()) { learnedSkills.push({ id: s }); } return learnedSkills; } getRawLearnedSkills() : number[] { return [...this.#skillsLearned]; } getStats() { return { ...this.#stats }; } getTalents() { return { ...this.#talents }; } unlockThrall(thrall: Thrall) { let { id } = thrall; if (this.#thrallsUnlocked.indexOf(id) != -1) { return; } this.#thrallsUnlocked.push(id); } isThrallUnlocked(thrall: Thrall) { return this.#thrallsUnlocked.indexOf(thrall.id) != -1; } getUnlockedThrallIds() : number[] { return [...this.#thrallsUnlocked]; } damageThrall(thrall: Thrall, amount: number) { if (amount <= 0.0) { throw new Error(`damage must be some positive amount, not ${amount}`); } let stage = this.getThrallLifeStage(thrall); if (stage == LifeStage.Vampirized) { this.#thrallDamage[thrall.id] = 4.0; } this.#thrallDamage[thrall.id] = (this.#thrallDamage[thrall.id] ?? 0.0) + amount; } getThrallDamage(thrall: Thrall): number { return this.#thrallDamage[thrall.id] ?? 0.0; } getThrallLifeStage(thrall: Thrall): LifeStage { let damage = this.#thrallDamage[thrall.id] ?? 0; if (damage < 0.5) { return LifeStage.Fresh; } if (damage < 1.75) { return LifeStage.Average; } if (damage < 3.0) { return LifeStage.Poor; } if (damage < 4.0) { return LifeStage.Vampirized; } return LifeStage.Dead; } obtainThrallItem(thrall: Thrall) { if (this.#thrallsObtainedItem.indexOf(thrall.id) != -1) { return; } this.#thrallsObtainedItem.push(thrall.id); } getThrallObtainedItemIds() : number[] { return [...this.#thrallsObtainedItem]; } deliverThrallItem(thrall: Thrall) { if (this.#thrallsDeliveredItem.indexOf(thrall.id) != -1) { return; } this.#thrallsDeliveredItem.push(thrall.id); } getThrallDeliveredItemIds() : number[] { return [...this.#thrallsDeliveredItem]; } getThrallItemStage(thrall: Thrall): ItemStage { if (this.#thrallsDeliveredItem.indexOf(thrall.id) != -1) { return ItemStage.Delivered; } if (this.#thrallsObtainedItem.indexOf(thrall.id) != -1) { return ItemStage.Obtained; } return ItemStage.Untouched; } anyAffordableSkillsAtMinimum() { let skills = this.getAvailableSkills(); for (let skill of skills.values()) { if (getSkills().isAtMinimum(skill)) { if ( getPlayerProgress().getExperience() > getSkills().computeCost(skill) ) { return true; } } } return false; } } let active: PlayerProgress | null = null; export function initPlayerProgress( asSuccessor: SuccessorOption, withWish: Wish | null, ) { active = new PlayerProgress(asSuccessor, withWish); } export function getPlayerProgress(): PlayerProgress { if (active == null) { throw new Error( `trying to get player progress before it has been initialized`, ); } return active; }