Raccoon walks around badly

This commit is contained in:
Pyrex 2025-02-01 23:33:41 -08:00
parent 46a249352d
commit dfae5b2405
9 changed files with 302 additions and 160 deletions

BIN
src/art/tilesets/drips.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

3
src/datatypes.ts Normal file
View File

@ -0,0 +1,3 @@
export type Stat = "AGI" | "INT" | "CHA" | "PSI";
export const ALL_STATS: Array<Stat> = ["AGI", "INT", "CHA", "PSI"];

View File

@ -1,15 +1,47 @@
import {D, I} from "./engine/public.ts";
import {Rect} from "./engine/datatypes.ts";
export class DrawPile { export class DrawPile {
readonly #draws: {depth: number, op: () => void}[] readonly #draws: {depth: number, op: () => void, onClick?: () => void}[]
#hoveredIndex: number | null;
constructor() { constructor() {
this.#draws = [] this.#draws = []
this.#hoveredIndex = null;
} }
add(depth: number, op: () => void) { add(depth: number, op: () => void) {
this.#draws.push({depth, op}); this.#draws.push({depth, op});
} }
execute() { addClickable(depth: number, op: (hover: boolean) => void, rect: Rect, enabled: boolean, onClick: () => void) {
let position = I.mousePosition?.offset(D.camera);
let hovered = false;
if (position != null) {
hovered = rect.contains(position);
}
if (!enabled) {
hovered = false;
}
if (hovered) {
this.#hoveredIndex = this.#draws.length;
}
this.#draws.push({depth, op: (() => op(hovered)), onClick: onClick})
}
executeOnClick() {
if (I.isMouseClicked("leftMouse")) {
let hi = this.#hoveredIndex;
if (hi != null) {
let cb = this.#draws[hi]?.onClick;
if (cb != null) {
cb();
}
}
}
}
draw() {
let draws = [...this.#draws]; let draws = [...this.#draws];
draws.sort( draws.sort(
(d0, d1) => d0.depth - d1.depth (d0, d1) => d0.depth - d1.depth

View File

@ -82,6 +82,20 @@ export class Size {
} }
} }
export class Rect {
readonly top: Point;
readonly size: Size;
constructor(top: Point, size: Size) {
this.top = top;
this.size = size;
}
contains(other: Point) {
return (other.x >= this.top.x && other.y >= this.top.y && other.x < this.top.x + this.size.w && other.y < this.top.y + this.size.h);
}
}
export class Grid<T> { export class Grid<T> {
readonly size: Size; readonly size: Size;
#data: T[][]; #data: T[][];

View File

@ -71,7 +71,7 @@ class Drawing {
return mainFont.measureText({text, forceWidth}) return mainFont.measureText({text, forceWidth})
} }
drawSprite(sprite: Sprite, position: Point, ix?: number, options?: {xScale?: number, yScale: number, angle: number}) { drawSprite(sprite: Sprite, position: Point, ix?: number, options?: {xScale?: number, yScale: number, angle?: number}) {
position = this.camera.negate().offset(position); position = this.camera.negate().offset(position);
let ctx = getScreen().unsafeMakeContext(); let ctx = getScreen().unsafeMakeContext();

View File

@ -1,10 +1,11 @@
import {desiredHeight, desiredWidth, getScreen} from "./engine/internal/screen.ts"; import {desiredHeight, desiredWidth, getScreen} from "./engine/internal/screen.ts";
import {BG_INSET, BG_OUTER, FG_TEXT} from "./colors.ts"; import {BG_INSET, BG_OUTER, FG_TEXT} from "./colors.ts";
import {checkGrid, ConceptualCell, maps, mapSzX, mapSzY} from "./maps.ts";
import {D, I} from "./engine/public.ts"; import {D, I} from "./engine/public.ts";
import {Grid, IGame, Point, Size} from "./engine/datatypes.ts"; import {IGame, Point, Rect, Size} from "./engine/datatypes.ts";
import {sprRaccoonWalking, sprStatPickup} from "./sprites.ts"; import {sprDrips, sprRaccoonWalking, sprStatPickup} from "./sprites.ts";
import {DrawPile} from "./drawpile.ts"; import {DrawPile} from "./drawpile.ts";
import {HuntMode, MapCell} from "./huntmode.ts";
import {ALL_STATS} from "./datatypes.ts";
class MenuCamera { class MenuCamera {
// measured in whole screens // measured in whole screens
@ -45,6 +46,7 @@ export class Game implements IGame {
camera: MenuCamera; camera: MenuCamera;
state: GameState; state: GameState;
huntMode: HuntMode; huntMode: HuntMode;
gameplayDrawPile: DrawPile | null;
frame: number; frame: number;
constructor() { constructor() {
@ -55,6 +57,7 @@ export class Game implements IGame {
this.state = "Gameplay"; this.state = "Gameplay";
this.huntMode = HuntMode.generate({depth: 1}); this.huntMode = HuntMode.generate({depth: 1});
this.gameplayDrawPile = null;
this.frame = 0; this.frame = 0;
} }
@ -120,199 +123,142 @@ export class Game implements IGame {
size: new Size(smallPaneW, smallPaneH), size: new Size(smallPaneW, smallPaneH),
} }
} }
} }
updateGameplay() { #moveCameraForGameplayDrawpile(cb: () => void) {
}
drawGameplay() {
let region = this.getPaneRegionForGameState("Gameplay") let region = this.getPaneRegionForGameState("Gameplay")
// TODO: Draw
let oldCamera = D.camera; let oldCamera = D.camera;
D.camera = D.camera.offset(region.small.position.negate()); D.camera = D.camera.offset(region.small.position.negate());
let drawpile = new DrawPile(); cb();
let globalOffset =
new Point(this.huntMode.player.x * 32, this.huntMode.player.y * 32).offset(
new Point(-192, -128)
)
for (let y = 0; y < mapSzY; y += 1) {
for (let x = 0; x < mapSzX; x += 1) {
let cellOffset = new Point(x * 32, y * 32).offset(globalOffset.negate());
let cellData = this.huntMode.cells.get(new Point(x, y))
this.#drawMapCell(drawpile, cellOffset, new Point(x, y), cellData);
}
}
this.#drawPlayer(drawpile, globalOffset);
drawpile.execute();
D.drawText("hello", new Point(0, 0), FG_TEXT);
D.camera = oldCamera; D.camera = oldCamera;
} }
updateGameplay() {
var drawpile = new DrawPile();
this.#moveCameraForGameplayDrawpile(() => {
let globalOffset =
new Point(this.huntMode.player.x * MAP_CELL_ONSCREEN_SIZE.w, this.huntMode.player.y * MAP_CELL_ONSCREEN_SIZE.h).offset(
new Point(-192, -192)
)
let map = this.huntMode.cells;
for (let y = 0; y < map.size.h; y += 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 cellData = this.huntMode.cells.get(new Point(x, y))
let belowIsBlock = true;
if (y < map.size.h - 1) {
let below = this.huntMode.cells.get(new Point(x, y + 1));
belowIsBlock = !below.revealed || below.content.type == "block";
}
this.#drawMapCell(drawpile, cellOffset, new Point(x, y), cellData, belowIsBlock);
}
}
this.#drawPlayer(drawpile, globalOffset);
drawpile.executeOnClick();
});
this.gameplayDrawPile = drawpile;
}
drawGameplay() {
// TODO: Draw
this.#moveCameraForGameplayDrawpile(() => {
this.gameplayDrawPile?.draw();
// D.drawText("shapes", new Point(0, 0), FG_TEXT);
});
}
#drawMapCell( #drawMapCell(
drawpile: DrawPile, drawpile: DrawPile,
cellOffset: Point, cellOffset: Point,
mapPosition: Point, mapPosition: Point,
cellData: MapCell, cellData: MapCell,
belowIsBlock: boolean
) { ) {
const OFFSET_FLOOR = -256; const OFFSET_FLOOR = -256;
const OFFSET_AIR = 0; const OFFSET_AIR = 0;
const depth = cellOffset.y; const depth = mapPosition.y;
const onFloor = OFFSET_FLOOR + depth; const onFloor = OFFSET_FLOOR + depth;
const inAir = OFFSET_AIR + depth; const inAir = OFFSET_AIR + depth;
if (cellData.content.type == "block") {
return;
}
/*
if (!cellData.revealed) { if (!cellData.revealed) {
return; return;
} }
*/
let cellTopLeft = cellOffset.offset(new Size(-MAP_CELL_ONSCREEN_SIZE.w / 2, -MAP_CELL_ONSCREEN_SIZE.h / 2));
let cellSize = MAP_CELL_ONSCREEN_SIZE;
if (cellData.content.type == "block") {
if (!belowIsBlock) {
drawpile.add(inAir, () => {
D.drawSprite(sprDrips, cellOffset.offset(new Point(0, -cellSize.h)), 1, {xScale: 3, yScale: 3})
})
}
return;
}
// draw inset zone // draw inset zone
drawpile.add(onFloor, () => drawpile.addClickable(onFloor,
D.fillRect(cellOffset.offset(new Size(-16, -26)), new Size(32, 32), BG_INSET) (hover: boolean) => {
D.fillRect(cellTopLeft, cellSize, hover ? FG_TEXT : BG_INSET)
},
new Rect(cellTopLeft, cellSize),
cellData.nextMoveAccessible,
() => {
this.huntMode.movePlayerTo(mapPosition)
}
); );
/* if (belowIsBlock) {
if (!cellData.revealed) { // draw the underhang
// TODO: draw some kind of question mark drawpile.add(onFloor, () => {
D.drawText("?", cellOffset.offset(new Point(12, 8)), FG_TEXT); D.drawSprite(sprDrips, cellOffset.offset(new Point(0, cellSize.h/2)), 0, {xScale: 3, yScale: 3})
return })
} }
*/
if (cellData.content.type == "statPickup") { if (cellData.content.type == "statPickup") {
let content = cellData.content; let content = cellData.content;
let extraXOffset = 0; // Math.cos(this.frame / 80 + mapPosition.x + mapPosition.y) * 1; let extraXOffset = 0; // Math.cos(this.frame / 80 + mapPosition.x + mapPosition.y) * 1;
let extraYOffset = 0; // Math.sin(this.frame / 50 + mapPosition.x * 2+ mapPosition.y * 0.75) * 6 - 3; let extraYOffset = Math.sin(this.frame / 50 + mapPosition.x * 2+ mapPosition.y * 0.75) * 6 - 18;
drawpile.add(inAir, () => { drawpile.add(inAir, () => {
D.drawSprite( D.drawSprite(
sprStatPickup, sprStatPickup,
cellOffset.offset(new Point(extraXOffset, extraYOffset)), cellOffset.offset(new Point(extraXOffset, extraYOffset)),
ALL_STATS.indexOf(content.stat) ALL_STATS.indexOf(content.stat),
{
xScale: 3,
yScale: 3,
}
) )
}); });
} }
} }
#drawPlayer(drawpile: DrawPile, globalOffset: Point) { #drawPlayer(drawpile: DrawPile, globalOffset: Point) {
let cellOffset = new Point(this.huntMode.player.x * 32, this.huntMode.player.y * 32).offset(globalOffset.negate()) let cellOffset = new Point(this.huntMode.player.x * MAP_CELL_ONSCREEN_SIZE.w, this.huntMode.player.y * MAP_CELL_ONSCREEN_SIZE.h).offset(globalOffset.negate())
drawpile.add(this.huntMode.player.y, () => { drawpile.add(this.huntMode.player.y, () => {
D.drawSprite( D.drawSprite(
sprRaccoonWalking, sprRaccoonWalking,
cellOffset cellOffset.offset(new Point(0, 22)),
0, {
xScale: 3,
yScale: 3
}
) )
}); });
} }
} }
type Stat = "AGI" | "INT" | "CHA" | "PSI"; const MAP_CELL_ONSCREEN_SIZE: Size = new Size(96, 48)
const ALL_STATS: Array<Stat> = ["AGI", "INT", "CHA", "PSI"];
type MapCellContent =
{type: "statPickup", stat: Stat} |
{type: "stairs"} |
{type: "empty"} |
{type: "block"}
type MapCell = {
content: MapCellContent,
isValidSpawn: boolean,
revealed: boolean
}
class HuntMode {
depth: number
cells: Grid<MapCell>
player: Point
constructor({depth, cells, player}: {depth: number, cells: Grid<MapCell>, player: Point }) {
this.depth = depth;
this.cells = cells;
this.player = player;
checkGrid(this.cells);
}
static generate({depth}: {depth: number}) {
let mapNames: Array<string> = Object.keys(maps);
let mapName = mapNames[Math.floor(Math.random() * mapNames.length)];
let map = maps[mapName];
let cells = map.map((ccell, _xy) => {
return this.#generateCell(ccell);
})
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() * mapSzX);
let y = Math.floor(Math.random() * mapSzY);
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;
}
}
return new HuntMode({depth, cells, player})
}
static #generateCell(conceptual: ConceptualCell): MapCell {
switch (conceptual) {
case "X":
return { content: {type: "block"}, revealed: true, isValidSpawn: false};
case " ":
return { content: HuntMode.#generateContent(), revealed: false, isValidSpawn: false };
case ".":
return { content: HuntMode.#generateContent(), revealed: false, isValidSpawn: true };
}
}
static #generateContent(): MapCellContent {
// stat pickup
let gsp = (): MapCellContent => {
return {type: "statPickup", stat: choose(ALL_STATS)}
};
// TODO: Other objects?
return choose([
gsp, gsp, gsp, gsp
])();
}
}
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)]
}
export let game = new Game(); export let game = new Game();

151
src/huntmode.ts Normal file
View File

@ -0,0 +1,151 @@
import {Grid, Point, Size} from "./engine/datatypes.ts";
import {ConceptualCell, maps} from "./maps.ts";
import {ALL_STATS, Stat} from "./datatypes.ts";
export type MapCellContent =
{type: "statPickup", stat: Stat} |
{type: "stairs"} |
{type: "empty"} |
{type: "block"}
export type MapCell = {
content: MapCellContent,
isValidSpawn: boolean,
revealed: boolean,
nextMoveAccessible: boolean,
}
export class HuntMode {
depth: number
cells: Grid<MapCell>
player: Point
constructor({depth, cells, player}: {depth: number, cells: Grid<MapCell>, player: Point }) {
this.depth = depth;
this.cells = cells;
this.player = player;
}
// == map generator ==
static generate({depth}: {depth: number}) {
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 this.#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 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})
hm.#updateVisibilityAndPossibleMoves();
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)}
};
// TODO: Other objects?
return choose([
gsp, gsp, gsp, gsp
])();
}
// == update logic ==
#updateVisibilityAndPossibleMoves() {
for (let x = 0; x < this.cells.size.w; x++) {
for (let y = 0; y < this.cells.size.h; y++) {
let position = new Point(x, y);
let data = this.cells.get(position);
data.nextMoveAccessible = false;
if (
Math.abs(x - this.player.x) <= 1 &&
Math.abs(y - this.player.y) <= 1
) {
data.revealed = true;
if (!this.player.equals(position)) {
data.nextMoveAccessible = true;
}
}
}
}
}
#collectResources() {
let present = this.cells.get(this.player);
if (present.content.type == "statPickup") {
present.content = {type: "empty"};
}
}
movePlayerTo(newPosition: Point) {
this.player = newPosition;
this.#updateVisibilityAndPossibleMoves();
this.#collectResources();
}
}
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

@ -1,15 +1,5 @@
import {Grid} from "./engine/datatypes.ts"; import {Grid} from "./engine/datatypes.ts";
export const mapSzX = 12;
export const mapSzY= 9;
export function checkGrid<T>(grid: Grid<T>): Grid<T> {
if (grid.size.w != mapSzX || grid.size.h != mapSzY) {
throw `map must be ${mapSzX}x${mapSzY}, not ${grid.size}`
}
return grid;
}
export type ConceptualCell = "X" | "." | " "; export type ConceptualCell = "X" | "." | " ";
function loadMap(map: Array<string>): Grid<ConceptualCell> { function loadMap(map: Array<string>): Grid<ConceptualCell> {

View File

@ -8,6 +8,7 @@ import imgSnake from "./art/characters/snake.png";
import imgRaccoon from "./art/characters/raccoon.png"; import imgRaccoon from "./art/characters/raccoon.png";
import imgRaccoonWalking from "./art/characters/raccoon_walking.png"; import imgRaccoonWalking from "./art/characters/raccoon_walking.png";
import imgStatPickup from "./art/pickups/stats.png"; import imgStatPickup from "./art/pickups/stats.png";
import imgDrips from "./art/tilesets/drips.png";
import {Point, Size} from "./engine/datatypes.ts"; import {Point, Size} from "./engine/datatypes.ts";
/* /*
@ -29,6 +30,11 @@ export let sprRaccoonWalking = new Sprite(
); );
export let sprStatPickup = new Sprite( export let sprStatPickup = new Sprite(
imgStatPickup, new Size(32, 32), new Point(16, 26), imgStatPickup, new Size(32, 32), new Point(16, 16),
new Size(4, 1), 4 new Size(4, 1), 4
); );
export let sprDrips = new Sprite(
imgDrips, new Size(32, 24), new Point(16, 0),
new Size(2, 1), 2
);