Make ladders work

This commit is contained in:
Pyrex 2025-02-02 22:26:56 -08:00
parent c23a7b6d75
commit 047248adb6
10 changed files with 172 additions and 122 deletions

BIN
src/art/pickups/ladder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 216 B

View File

@ -1,7 +1,7 @@
import {BG_OUTER} from "./colors.ts"; import {BG_OUTER} from "./colors.ts";
import {D, I} from "./engine/public.ts"; import {D, I} from "./engine/public.ts";
import {IGame, Point, Size} from "./engine/datatypes.ts"; import {IGame, Point, Size} from "./engine/datatypes.ts";
import {HuntMode} from "./huntmode.ts"; import {getHuntMode} from "./huntmode.ts";
import {getPageLocation, Page, withCamera} from "./layout.ts"; import {getPageLocation, Page, withCamera} from "./layout.ts";
import {getHud} from "./hud.ts"; import {getHud} from "./hud.ts";
import {getHotbar, Hotbar} from "./hotbar.ts"; import {getHotbar, Hotbar} from "./hotbar.ts";
@ -32,7 +32,6 @@ class MenuCamera {
export class Game implements IGame { export class Game implements IGame {
camera: MenuCamera; camera: MenuCamera;
page: Page; page: Page;
huntMode: HuntMode;
#bottomThing: SkillsModal | SleepModal | Hotbar | null; #bottomThing: SkillsModal | SleepModal | Hotbar | null;
constructor() { constructor() {
@ -42,7 +41,6 @@ export class Game implements IGame {
}); });
this.page = "Gameplay"; this.page = "Gameplay";
this.huntMode = HuntMode.generate({depth: 1});
this.#bottomThing = null; this.#bottomThing = null;
} }
@ -86,7 +84,7 @@ export class Game implements IGame {
this.#chooseBottomThing(); this.#chooseBottomThing();
withCamera("Gameplay", () => { withCamera("Gameplay", () => {
this.huntMode.update(); getHuntMode().update();
}); });
withCamera("HUD", () => { getHud().update() }) withCamera("HUD", () => { getHud().update() })
this.#bottomThing?.update(); this.#bottomThing?.update();
@ -94,7 +92,7 @@ export class Game implements IGame {
drawGameplay() { drawGameplay() {
withCamera("Gameplay", () => { withCamera("Gameplay", () => {
this.huntMode.draw(); getHuntMode().draw();
}); });
withCamera("HUD", () => { getHud().draw() }) withCamera("HUD", () => { getHud().draw() })
this.#bottomThing?.draw() this.#bottomThing?.draw()

View File

@ -3,6 +3,7 @@ import {Point, Size} from "./engine/datatypes.ts";
import {FG_BOLD, FG_TEXT} from "./colors.ts"; import {FG_BOLD, FG_TEXT} from "./colors.ts";
import {ALL_STATS} from "./datatypes.ts"; import {ALL_STATS} from "./datatypes.ts";
import {getPlayerProgress} from "./playerprogress.ts"; import {getPlayerProgress} from "./playerprogress.ts";
import {getHuntMode} from "./huntmode.ts";
export class Hud { export class Hud {
get size(): Size { get size(): Size {
@ -14,7 +15,7 @@ export class Hud {
draw() { draw() {
// D.fillRect(new Point(-4, -4), this.size.add(new Size(8, 8)), BG_INSET) // D.fillRect(new Point(-4, -4), this.size.add(new Size(8, 8)), BG_INSET)
D.drawText("Pyrex", new Point(0, 0), FG_BOLD) D.drawText("Pyrex", new Point(0, 0), FG_BOLD)
D.drawText("Level 1", new Point(0, 16), FG_TEXT) D.drawText(`Level ${getHuntMode().getDepth()}`, new Point(0, 16), FG_TEXT)
let y = 48; let y = 48;
let prog = getPlayerProgress(); let prog = getPlayerProgress();

View File

@ -1,11 +1,11 @@
import {Grid, Point, Rect, Size} from "./engine/datatypes.ts"; import {Grid, Point, Rect, Size} from "./engine/datatypes.ts";
import {ConceptualCell, maps} from "./maps.ts";
import {ALL_STATS, Resource, Stat} from "./datatypes.ts"; import {ALL_STATS, Resource, Stat} from "./datatypes.ts";
import {DrawPile} from "./drawpile.ts"; import {DrawPile} from "./drawpile.ts";
import {D} from "./engine/public.ts"; import {D} from "./engine/public.ts";
import {sprDrips, sprRaccoonWalking, sprResourcePickup, sprStatPickup} from "./sprites.ts"; import {sprDrips, sprLadder, sprRaccoonWalking, sprResourcePickup, sprStatPickup} from "./sprites.ts";
import {BG_INSET, FG_TEXT} from "./colors.ts"; import {BG_INSET, FG_TEXT} from "./colors.ts";
import {getPlayerProgress} from "./playerprogress.ts"; import {getPlayerProgress} from "./playerprogress.ts";
import {generate} from "./mapgen.ts";
export type MapCellContent = export type MapCellContent =
{type: "statPickup", stat: Stat} | {type: "statPickup", stat: Stat} |
@ -21,122 +21,53 @@ export type MapCell = {
nextMoveAccessible: boolean, nextMoveAccessible: boolean,
} }
export class HuntMode { export type LoadedMap = {
depth: number cells: Grid<MapCell>,
cells: Grid<MapCell>
player: Point player: Point
}
export class HuntMode {
map: LoadedMap
drawpile: DrawPile drawpile: DrawPile
frame: number frame: number
depth: number
constructor({depth, cells, player}: {depth: number, cells: Grid<MapCell>, player: Point }) { constructor() {
this.depth = depth; this.map = null!; // initialized in replaceMap
this.cells = cells;
this.player = player;
this.drawpile = new DrawPile(); this.drawpile = new DrawPile();
this.frame = 0; this.frame = 0;
this.depth = 1;
this.replaceMap();
} }
// == map generator == replaceMap(deeper?: boolean) {
static generate({depth}: {depth: number}) { this.map = generate();
let mapNames: Array<string> = Object.keys(maps); this.#updateVisibilityAndPossibleMoves();
let mapName = mapNames[Math.floor(Math.random() * mapNames.length)];
let map = maps[mapName];
let baseCells = map.map((ccell, _xy) => { if (deeper) {
return this.#generateCell(ccell); this.depth += 1;
})
let cells = new Grid(
new Size(baseCells.size.w + 2, baseCells.size.h + 2), (xy) => {
let offset = xy.offset(new Point(-1, -1));
if (offset.x == -1 || offset.y == -1 || offset.x == baseCells.size.w || offset.y == baseCells.size.h) {
return this.#generateBoundaryCell();
}
return baseCells.get(offset)
}
)
let validSpawns = [];
for (let x = 0; x < cells.size.w; x++) {
for (let y = 0; y < cells.size.h; y++) {
let position = new Point(x, y);
if (cells.get(position).isValidSpawn) {
validSpawns.push(position);
}
}
}
let player = choose(validSpawns);
cells.get(player).content = {type: "empty"};
if (Math.random() < 0.75) {
while (true) {
let x = Math.floor(Math.random() * cells.size.w);
let y = Math.floor(Math.random() * cells.size.h);
let xy = new Point(x, y);
let item = cells.get(new Point(x, y));
if (player.equals(xy)) {
continue;
}
if (item.content.type == "block") {
continue;
}
item.content = {type: "stairs"}
break;
} }
} }
let hm = new HuntMode({depth, cells, player}) getDepth() {
hm.#updateVisibilityAndPossibleMoves(); return this.depth;
return hm;
}
static #generateCell(conceptual: ConceptualCell): MapCell {
switch (conceptual) {
case "X":
return { content: {type: "block"}, revealed: false, isValidSpawn: false, nextMoveAccessible: false};
case " ":
return { content: HuntMode.#generateContent(), revealed: false, isValidSpawn: false, nextMoveAccessible: false };
case ".":
return { content: HuntMode.#generateContent(), revealed: false, isValidSpawn: true, nextMoveAccessible: false };
}
}
static #generateBoundaryCell() {
return this.#generateCell("X");
}
static #generateContent(): MapCellContent {
// stat pickup
let gsp = (): MapCellContent => {
return {type: "statPickup", stat: choose(ALL_STATS)}
};
let exp = (): MapCellContent => {
return {type: "resourcePickup", resource: "EXP"}
}
// TODO: Other objects?
return choose([
gsp, gsp, gsp, gsp,
exp,
])();
} }
// == update logic == // == update logic ==
#updateVisibilityAndPossibleMoves() { #updateVisibilityAndPossibleMoves() {
for (let x = 0; x < this.cells.size.w; x++) { for (let x = 0; x < this.map.cells.size.w; x++) {
for (let y = 0; y < this.cells.size.h; y++) { for (let y = 0; y < this.map.cells.size.h; y++) {
let position = new Point(x, y); let position = new Point(x, y);
let data = this.cells.get(position); let data = this.map.cells.get(position);
data.nextMoveAccessible = false; data.nextMoveAccessible = false;
if ( if (
Math.abs(x - this.player.x) <= 1 && Math.abs(x - this.map.player.x) <= 1 &&
Math.abs(y - this.player.y) <= 1 Math.abs(y - this.map.player.y) <= 1
) { ) {
data.revealed = true; data.revealed = true;
if (!this.player.equals(position)) { if (!this.map.player.equals(position)) {
data.nextMoveAccessible = true; data.nextMoveAccessible = true;
} }
} }
@ -146,7 +77,12 @@ export class HuntMode {
} }
#collectResources() { #collectResources() {
let present = this.cells.get(this.player); let present = this.map.cells.get(this.map.player);
if (present.content.type == "stairs") {
getPlayerProgress().addBlood(1000);
this.replaceMap(true);
}
if (present.content.type == "statPickup") { if (present.content.type == "statPickup") {
let stat = present.content.stat; let stat = present.content.stat;
@ -170,10 +106,13 @@ export class HuntMode {
} }
#computeCostToMoveTo(mapPosition: Point): number | null { #computeCostToMoveTo(mapPosition: Point): number | null {
let present = this.cells.get(mapPosition); let present = this.map.cells.get(mapPosition);
if (present.content.type == "statPickup" || present.content.type == "resourcePickup") { if (present.content.type == "statPickup" || present.content.type == "resourcePickup") {
return 100; return 100;
} }
if (present.content.type == "stairs") {
return 0;
}
if (present.content.type == "empty") { if (present.content.type == "empty") {
return 10; return 10;
} }
@ -181,7 +120,7 @@ export class HuntMode {
} }
movePlayerTo(newPosition: Point) { movePlayerTo(newPosition: Point) {
this.player = newPosition; this.map.player = newPosition;
this.#updateVisibilityAndPossibleMoves(); this.#updateVisibilityAndPossibleMoves();
this.#collectResources(); this.#collectResources();
} }
@ -192,18 +131,18 @@ export class HuntMode {
this.drawpile.clear(); this.drawpile.clear();
let globalOffset = let globalOffset =
new Point(this.player.x * MAP_CELL_ONSCREEN_SIZE.w, this.player.y * MAP_CELL_ONSCREEN_SIZE.h).offset( new Point(this.map.player.x * MAP_CELL_ONSCREEN_SIZE.w, this.map.player.y * MAP_CELL_ONSCREEN_SIZE.h).offset(
new Point(-192, -192) new Point(-192, -192)
) )
let map = this.cells; let map = this.map.cells;
for (let y = 0; y < map.size.h; y += 1) { for (let y = 0; y < map.size.h; y += 1) {
for (let x = 0; x < map.size.w; x += 1) { for (let x = 0; x < map.size.w; x += 1) {
let cellOffset = new Point(x * MAP_CELL_ONSCREEN_SIZE.w, y * MAP_CELL_ONSCREEN_SIZE.h).offset(globalOffset.negate()); let cellOffset = new Point(x * MAP_CELL_ONSCREEN_SIZE.w, y * MAP_CELL_ONSCREEN_SIZE.h).offset(globalOffset.negate());
let cellData = this.cells.get(new Point(x, y)) let cellData = this.map.cells.get(new Point(x, y))
let belowIsBlock = true; let belowIsBlock = true;
if (y < map.size.h - 1) { if (y < map.size.h - 1) {
let below = this.cells.get(new Point(x, y + 1)); let below = this.map.cells.get(new Point(x, y + 1));
belowIsBlock = !below.revealed || below.content.type == "block"; belowIsBlock = !below.revealed || below.content.type == "block";
} }
@ -241,7 +180,7 @@ export class HuntMode {
if (cellData.content.type == "block") { if (cellData.content.type == "block") {
if (!belowIsBlock) { if (!belowIsBlock) {
this.drawpile.add(inAir, () => { this.drawpile.add(inAir, () => {
D.drawSprite(sprDrips, cellOffset.offset(new Point(0, -cellSize.h)), 1, {xScale: 3, yScale: 3}) D.drawSprite(sprDrips, cellOffset.offset(new Point(0, -cellSize.h / 2)), 1, {xScale: 3, yScale: 3})
}) })
} }
return; return;
@ -252,6 +191,11 @@ export class HuntMode {
this.drawpile.addClickable(onFloor, this.drawpile.addClickable(onFloor,
(hover: boolean) => { (hover: boolean) => {
D.fillRect(cellTopLeft, cellSize, hover ? FG_TEXT : BG_INSET) D.fillRect(cellTopLeft, cellSize, hover ? FG_TEXT : BG_INSET)
if (cellData.content.type == "stairs") {
// draw ladder if applicable
D.drawSprite(sprLadder, cellTopLeft, 0, {xScale: 3, yScale: 3});
}
}, },
new Rect(cellTopLeft, cellSize), new Rect(cellTopLeft, cellSize),
cellData.nextMoveAccessible && cost != null && cost <= getPlayerProgress().getBlood(), cellData.nextMoveAccessible && cost != null && cost <= getPlayerProgress().getBlood(),
@ -263,10 +207,11 @@ export class HuntMode {
} }
); );
if (belowIsBlock) { if (belowIsBlock) {
// draw the underhang // draw the underhang
this.drawpile.add(onFloor, () => { this.drawpile.add(onFloor, () => {
D.drawSprite(sprDrips, cellOffset.offset(new Point(0, cellSize.h/2)), 0, {xScale: 3, yScale: 3}) D.drawSprite(sprDrips, cellOffset.offset(new Point(0, cellSize.h / 2)), 0, {xScale: 3, yScale: 3})
}) })
} }
@ -304,10 +249,10 @@ export class HuntMode {
#drawPlayer(globalOffset: Point) { #drawPlayer(globalOffset: Point) {
let cellOffset = new Point( let cellOffset = new Point(
this.player.x * MAP_CELL_ONSCREEN_SIZE.w, this.map.player.x * MAP_CELL_ONSCREEN_SIZE.w,
this.player.y * MAP_CELL_ONSCREEN_SIZE.h this.map.player.y * MAP_CELL_ONSCREEN_SIZE.h
).offset(globalOffset.negate()) ).offset(globalOffset.negate())
this.drawpile.add(this.player.y, () => { this.drawpile.add(this.map.player.y, () => {
D.drawSprite( D.drawSprite(
sprRaccoonWalking, sprRaccoonWalking,
cellOffset.offset(new Point(0, 22)), cellOffset.offset(new Point(0, 22)),
@ -322,9 +267,7 @@ export class HuntMode {
const MAP_CELL_ONSCREEN_SIZE: Size = new Size(96, 48) const MAP_CELL_ONSCREEN_SIZE: Size = new Size(96, 48)
function choose<T>(array: Array<T>): T { let active = new HuntMode();
if (array.length == 0) { export function getHuntMode() {
throw `array cannot have length 0 for choose` return active;
}
return array[Math.floor(Math.random() * array.length)]
} }

97
src/mapgen.ts Normal file
View File

@ -0,0 +1,97 @@
import {ConceptualCell, maps} from "./maps.ts";
import {Grid, Point, Size} from "./engine/datatypes.ts";
import {ALL_STATS} from "./datatypes.ts";
import {LoadedMap, MapCell, MapCellContent} from "./huntmode.ts";
export function generate(): LoadedMap {
let mapNames: Array<string> = Object.keys(maps);
let mapName = mapNames[Math.floor(Math.random() * mapNames.length)];
let map = maps[mapName];
let baseCells = map.map((ccell, _xy) => {
return generateCell(ccell);
})
let cells = new Grid(
new Size(baseCells.size.w + 2, baseCells.size.h + 2), (xy) => {
let offset = xy.offset(new Point(-1, -1));
if (offset.x == -1 || offset.y == -1 || offset.x == baseCells.size.w || offset.y == baseCells.size.h) {
return generateBoundaryCell();
}
return baseCells.get(offset)
}
)
let validSpawns = [];
for (let x = 0; x < cells.size.w; x++) {
for (let y = 0; y < cells.size.h; y++) {
let position = new Point(x, y);
if (cells.get(position).isValidSpawn) {
validSpawns.push(position);
}
}
}
let player = choose(validSpawns);
cells.get(player).content = {type: "empty"};
let nStairs = choose([1, 1, 1, 0]);
for (let i = 0; i < nStairs; i++) {
while (true) {
let x = Math.floor(Math.random() * cells.size.w);
let y = Math.floor(Math.random() * cells.size.h);
let xy = new Point(x, y);
let item = cells.get(new Point(x, y));
if (player.equals(xy)) {
continue;
}
if (item.content.type == "block" || item.content.type == "stairs") {
continue;
}
item.content = {type: "stairs"}
break;
}
}
return {
cells,
player,
}
}
function generateCell(conceptual: ConceptualCell): MapCell {
switch (conceptual) {
case "X":
return { content: {type: "block"}, revealed: false, isValidSpawn: false, nextMoveAccessible: false};
case " ":
return { content: generateContent(), revealed: false, isValidSpawn: false, nextMoveAccessible: false };
case ".":
return { content: generateContent(), revealed: false, isValidSpawn: true, nextMoveAccessible: false };
}
}
function generateBoundaryCell() {
return generateCell("X");
}
function generateContent(): MapCellContent {
// stat pickup
let gsp = (): MapCellContent => {
return {type: "statPickup", stat: choose(ALL_STATS)}
};
let exp = (): MapCellContent => {
return {type: "resourcePickup", resource: "EXP"}
}
// TODO: Other objects?
return choose([
gsp, gsp, gsp, gsp,
exp,
])();
}
function choose<T>(array: Array<T>): T {
if (array.length == 0) {
throw `array cannot have length 0 for choose`
}
return array[Math.floor(Math.random() * array.length)]
}

View File

@ -107,6 +107,11 @@ export class PlayerProgress {
return Math.floor(Math.max(this.#blood, 0)); return Math.floor(Math.max(this.#blood, 0));
} }
addBlood(amt: number) {
this.#blood += amt;
this.#blood = Math.min(this.#blood, 5000)
}
spendBlood(amt: number) { spendBlood(amt: number) {
this.#blood -= amt; this.#blood -= amt;
} }
@ -127,7 +132,6 @@ export class PlayerProgress {
}); });
return skillsAvailable.slice(0, 6) return skillsAvailable.slice(0, 6)
} }
} }
let active: PlayerProgress = new PlayerProgress(); let active: PlayerProgress = new PlayerProgress();

View File

@ -79,7 +79,7 @@ function governing(track: Track, difficulty: Difficulty): SkillGoverning {
let cost: number let cost: number
switch(difficulty) { switch(difficulty) {
case 0: underTarget = 5; target = 15; cost = 50; break; case 0: underTarget = 5; target = 15; cost = 50; break;
case 1: underTarget = 50; target = 100; cost = 100; break; case 1: underTarget = 15; target = 40; cost = 100; break;
case 2: underTarget = 100; target = 150; cost = 250; break; case 2: underTarget = 100; target = 150; cost = 250; break;
case 3: underTarget = 175; target = 250; cost = 500; break; case 3: underTarget = 175; target = 250; cost = 500; break;
} }

View File

@ -6,6 +6,7 @@ import {D} from "./engine/public.ts";
import {BG_INSET} from "./colors.ts"; import {BG_INSET} from "./colors.ts";
import {getSkillsModal} from "./skillsmodal.ts"; import {getSkillsModal} from "./skillsmodal.ts";
import {getPlayerProgress} from "./playerprogress.ts"; import {getPlayerProgress} from "./playerprogress.ts";
import {getHuntMode} from "./huntmode.ts";
export class SleepModal { export class SleepModal {
#drawpile: DrawPile; #drawpile: DrawPile;
@ -60,10 +61,10 @@ export class SleepModal {
let remainingWidth = size.w - 160; let remainingWidth = size.w - 160;
let nextRect = new Rect(new Point(160, 96), new Size(remainingWidth, 32)); let nextRect = new Rect(new Point(160, 96), new Size(remainingWidth, 32));
addButton(this.#drawpile, "Sleep All Day", nextRect, true, () => { addButton(this.#drawpile, "Sleep (Next Day)", nextRect, true, () => {
getPlayerProgress().refill(); getPlayerProgress().refill();
getHuntMode().replaceMap();
getSleepModal().setShown(false); getSleepModal().setShown(false);
// TODO: Advance huntmode
}); });
this.#drawpile.executeOnClick(); this.#drawpile.executeOnClick();

View File

@ -9,6 +9,7 @@ import imgRaccoon from "./art/characters/raccoon.png";
import imgRaccoonWalking from "./art/characters/raccoon_walking.png"; import imgRaccoonWalking from "./art/characters/raccoon_walking.png";
import imgResourcePickup from "./art/pickups/resources.png"; import imgResourcePickup from "./art/pickups/resources.png";
import imgStatPickup from "./art/pickups/stats.png"; import imgStatPickup from "./art/pickups/stats.png";
import imgLadder from "./art/pickups/ladder.png";
import imgDrips from "./art/tilesets/drips.png"; import imgDrips from "./art/tilesets/drips.png";
import {Point, Size} from "./engine/datatypes.ts"; import {Point, Size} from "./engine/datatypes.ts";
@ -40,6 +41,11 @@ export let sprStatPickup = new Sprite(
); );
export let sprDrips = new Sprite( export let sprDrips = new Sprite(
imgDrips, new Size(32, 24), new Point(16, 0), imgDrips, new Size(32, 16), new Point(16, 0),
new Size(2, 1), 2 new Size(2, 1), 2
); );
export let sprLadder = new Sprite(
imgLadder, new Size(32, 24), new Point(0, 0),
new Size(1, 1), 1
);