Skill costing 1
This commit is contained in:
		| @@ -3,7 +3,7 @@ export type Stat = "AGI" | "INT" | "CHA" | "PSI"; | ||||
| export const ALL_STATS: Array<Stat> = ["AGI", "INT", "CHA", "PSI"]; | ||||
|  | ||||
| export type SkillGoverning = { | ||||
|   stats: Stat[], target: number, cost: number, note: string | ||||
|   stats: Stat[], underTarget: number, target: number, cost: number, note: string | ||||
| }; | ||||
| export type SkillProfile = { | ||||
|   name: string, | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/game.ts
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/game.ts
									
									
									
									
									
								
							| @@ -6,6 +6,7 @@ import {getPageLocation, Page, withCamera} from "./layout.ts"; | ||||
| import {getHud} from "./hud.ts"; | ||||
| import {getHotbar, Hotbar} from "./hotbar.ts"; | ||||
| import {getSkillsModal, SkillsModal} from "./skillsmodal.ts"; | ||||
| import {getSleepModal, SleepModal} from "./sleepmodal.ts"; | ||||
|  | ||||
| class MenuCamera { | ||||
|   // measured in whole screens | ||||
| @@ -32,7 +33,7 @@ export class Game implements IGame { | ||||
|   camera: MenuCamera; | ||||
|   page: Page; | ||||
|   huntMode: HuntMode; | ||||
|   #bottomThing: SkillsModal | Hotbar | null; | ||||
|   #bottomThing: SkillsModal | SleepModal | Hotbar | null; | ||||
|  | ||||
|   constructor() { | ||||
|     this.camera = new MenuCamera({ | ||||
| @@ -111,6 +112,15 @@ export class Game implements IGame { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // between skillsModal and sleepModal, skillsModal wins | ||||
|     // this is important because skillsModal can be | ||||
|     // activated from sleepModal | ||||
|     let sleepModal = getSleepModal(); | ||||
|     if (sleepModal.isShown) { | ||||
|       this.#bottomThing = sleepModal; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // use the hotbar only as a matter of last resort | ||||
|     this.#bottomThing = getHotbar(); | ||||
|   } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import {DrawPile} from "./drawpile.ts"; | ||||
| import {withCamera} from "./layout.ts"; | ||||
| import {getSkillsModal} from "./skillsmodal.ts"; | ||||
| import {addButton} from "./button.ts"; | ||||
| import {getSleepModal} from "./sleepmodal.ts"; | ||||
|  | ||||
| type Button = { | ||||
|   label: string, | ||||
| @@ -38,6 +39,12 @@ export class Hotbar { | ||||
|       label: "Thralls" | ||||
|     }) | ||||
|      */ | ||||
|     buttons.push({ | ||||
|       label: "Sleep", | ||||
|       cbClick: () => { | ||||
|         getSleepModal().setShown(true) | ||||
|       } | ||||
|     }) | ||||
|     return buttons; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -146,14 +146,23 @@ export class HuntMode { | ||||
|       let stat = present.content.stat; | ||||
|       let amount = 1; | ||||
|       present.content = {type: "empty"}; | ||||
|       getPlayerProgress().spendBlood(90); | ||||
|       getPlayerProgress().add(stat, amount); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   #computeCostToMoveTo(mapPosition: Point): number | null { | ||||
|     let present = this.cells.get(mapPosition); | ||||
|     if (present.content.type == "statPickup") { | ||||
|       return 100; | ||||
|     } | ||||
|     if (present.content.type == "empty") { | ||||
|       return 10; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   movePlayerTo(newPosition: Point) { | ||||
|     this.player = newPosition; | ||||
|     getPlayerProgress().spendBlood(10); | ||||
|     this.#updateVisibilityAndPossibleMoves(); | ||||
|     this.#collectResources(); | ||||
|   } | ||||
| @@ -220,14 +229,18 @@ export class HuntMode { | ||||
|     } | ||||
|  | ||||
|     // draw inset zone | ||||
|     let cost = this.#computeCostToMoveTo(mapPosition); | ||||
|     this.drawpile.addClickable(onFloor, | ||||
|       (hover: boolean) => { | ||||
|         D.fillRect(cellTopLeft, cellSize, hover ? FG_TEXT : BG_INSET) | ||||
|       }, | ||||
|       new Rect(cellTopLeft, cellSize), | ||||
|       cellData.nextMoveAccessible, | ||||
|       cellData.nextMoveAccessible && cost != null && cost <= getPlayerProgress().getBlood(), | ||||
|       () => { | ||||
|         this.movePlayerTo(mapPosition) | ||||
|         if (cost != null) { | ||||
|           getPlayerProgress().spendBlood(cost); | ||||
|           this.movePlayerTo(mapPosition) | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|  | ||||
|   | ||||
| @@ -95,8 +95,20 @@ export class PlayerProgress { | ||||
|   } | ||||
|  | ||||
|   getAvailableSkills(): Skill[] { | ||||
|     // TODO: Sort by cost, then by name, then trim down to first 8 | ||||
|     return this.#untrimmedSkillsAvailable | ||||
|     // Sort by cost, then by name, then trim down to first 6 | ||||
|     let skillsAvailable = [...this.#untrimmedSkillsAvailable]; | ||||
|     skillsAvailable.sort((a, b) => { | ||||
|       return getSkills().computeCost(a) - getSkills().computeCost(b) | ||||
|     }); | ||||
|     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; | ||||
|     }); | ||||
|     return skillsAvailable.slice(0, 6) | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import {Skill, SkillData, SkillGoverning, Stat} from "./datatypes.ts"; | ||||
| import {getPlayerProgress} from "./playerprogress.ts"; | ||||
|  | ||||
| class SkillsTable { | ||||
|   #skills: SkillData[] | ||||
| @@ -24,6 +25,36 @@ class SkillsTable { | ||||
|     } | ||||
|     return skills; | ||||
|   } | ||||
|  | ||||
|   computeCost(skill: Skill) { | ||||
|     let data = this.get(skill); | ||||
|  | ||||
|     let governingStatValue = 0; | ||||
|     for (let stat of data.governing.stats.values()) { | ||||
|       governingStatValue += getPlayerProgress().getStat(stat) / data.governing.stats.length; | ||||
|     } | ||||
|  | ||||
|     return Math.floor(geomInterpolate( | ||||
|       governingStatValue, | ||||
|       data.governing.underTarget, data.governing.target, | ||||
|       data.governing.cost, 999 | ||||
|     )) | ||||
|   } | ||||
| } | ||||
|  | ||||
| function geomInterpolate( | ||||
|   x: number, | ||||
|   lowIn: number, | ||||
|   highIn: number, | ||||
|   lowOut: number, | ||||
|   highOut: number, | ||||
| ) { | ||||
|   if (x < lowIn) { return highOut; } | ||||
|   if (x >= highIn) { return lowOut; } | ||||
|  | ||||
|   const proportion = 1.0 - (x - lowIn) / (highIn - lowIn); | ||||
|   console.log(`proportion: ${x} ${proportion}`) | ||||
|   return lowOut * Math.pow(highOut / lowOut, proportion) | ||||
| } | ||||
|  | ||||
| type Difficulty = 0 | 1 | 2 | 3 | ||||
| @@ -44,16 +75,18 @@ let templates: Record<Track, GoverningTemplate> = { | ||||
|  | ||||
| function governing(track: Track, difficulty: Difficulty): SkillGoverning { | ||||
|   let template = templates[track]; | ||||
|   let underTarget: number | ||||
|   let target: number | ||||
|   let cost: number | ||||
|   switch(difficulty) { | ||||
|     case 0: target = 30; cost = 50; break; | ||||
|     case 1: target = 100; cost = 100; break; | ||||
|     case 2: target = 150; cost = 250; break; | ||||
|     case 3: target = 250; cost = 500; break; | ||||
|     case 0: underTarget = 5; target = 15; cost = 50; break; | ||||
|     case 1: underTarget = 50; target = 100; cost = 100; break; | ||||
|     case 2: underTarget = 100; target = 150; cost = 250; break; | ||||
|     case 3: underTarget = 175; target = 250; cost = 500; break; | ||||
|   } | ||||
|   return { | ||||
|     stats: template.stats, | ||||
|     underTarget: underTarget, | ||||
|     target: target, | ||||
|     cost: cost, | ||||
|     note: template.note, | ||||
|   | ||||
| @@ -58,6 +58,7 @@ export class SkillsModal { | ||||
|     let y = 0; | ||||
|     for (let skill of availableSkills) { | ||||
|       let data = getSkills().get(skill); | ||||
|       let cost = getSkills().computeCost(skill); | ||||
|       let y_ = y; | ||||
|       let selected = this.#skillSelection?.id == skill.id; | ||||
|       let skillRect = new Rect(new Point(0,  y_), new Size(160 + 4, 16)); | ||||
| @@ -73,7 +74,7 @@ export class SkillsModal { | ||||
|           } | ||||
|           D.fillRect(skillRect.top, skillRect.size, bg); | ||||
|           D.drawText(data.profile.name, new Point(4, y_), fg); | ||||
|           D.drawText("100", new Point(160 - 4, y_), fg, {alignX: AlignX.Right}); | ||||
|           D.drawText("" + cost, new Point(160 - 4, y_), fg, {alignX: AlignX.Right}); | ||||
|         }, | ||||
|         skillRect, | ||||
|         enabled, | ||||
|   | ||||
							
								
								
									
										80
									
								
								src/sleepmodal.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/sleepmodal.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| import {DrawPile} from "./drawpile.ts"; | ||||
| import {Point, Rect, Size} from "./engine/datatypes.ts"; | ||||
| import {getPartLocation, withCamera} from "./layout.ts"; | ||||
| import {addButton} from "./button.ts"; | ||||
| import {D} from "./engine/public.ts"; | ||||
| import {BG_INSET} from "./colors.ts"; | ||||
| import {getSkillsModal} from "./skillsmodal.ts"; | ||||
| import {getPlayerProgress} from "./playerprogress.ts"; | ||||
|  | ||||
| export class SleepModal { | ||||
|   #drawpile: DrawPile; | ||||
|   #shown: boolean; | ||||
|  | ||||
|   constructor() { | ||||
|     this.#drawpile = new DrawPile(); | ||||
|     this.#shown = false; | ||||
|   } | ||||
|  | ||||
|   get #size(): Size { | ||||
|     // We share this logic with SkillModal: | ||||
|     // Instead of calculating this here, compute it from outside | ||||
|     // as it has to be the same for every bottom modal | ||||
|     return getPartLocation("BottomModal").size | ||||
|   } | ||||
|  | ||||
|   get isShown(): boolean { | ||||
|     return this.#shown; | ||||
|   } | ||||
|  | ||||
|   setShown(shown: boolean) { | ||||
|     this.#shown = shown | ||||
|   } | ||||
|  | ||||
|  | ||||
|   update() { | ||||
|     withCamera("BottomModal", () => this.#update()) | ||||
|   } | ||||
|  | ||||
|   draw() { | ||||
|     withCamera("BottomModal", () => this.#draw()) | ||||
|   } | ||||
|  | ||||
|   #update() { | ||||
|     this.#drawpile.clear(); | ||||
|     let size = this.#size | ||||
|     this.#drawpile.add(0, () => { | ||||
|       D.fillRect(new Point(-4, -4), size.add(new Size(8, 8)), BG_INSET) | ||||
|     }) | ||||
|  | ||||
|     // add close button | ||||
|     let closeRect = new Rect(new Point(0, 96), new Size(80, 32)) | ||||
|     addButton(this.#drawpile, "Back", closeRect, true, () => { | ||||
|       this.setShown(false); | ||||
|     }) | ||||
|  | ||||
|     let skillsRect = new Rect(new Point(80, 96), new Size(80, 32)); | ||||
|     addButton(this.#drawpile, "Skills", skillsRect, true, () => { | ||||
|       getSkillsModal().setShown(true); | ||||
|     }) | ||||
|  | ||||
|     let remainingWidth = size.w - 160; | ||||
|     let nextRect = new Rect(new Point(160, 96), new Size(remainingWidth, 32)); | ||||
|     addButton(this.#drawpile, "Sleep All Day", nextRect, true, () => { | ||||
|       getPlayerProgress().refill(); | ||||
|       getSleepModal().setShown(false); | ||||
|       // TODO: Advance huntmode | ||||
|     }); | ||||
|  | ||||
|     this.#drawpile.executeOnClick(); | ||||
|   } | ||||
|  | ||||
|   #draw() { | ||||
|     this.#drawpile.draw(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| let active = new SleepModal(); | ||||
| export function getSleepModal(): SleepModal { | ||||
|   return active; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user