481 lines
12 KiB
TypeScript
481 lines
12 KiB
TypeScript
import { Point, Rect, Size } from "./engine/datatypes.ts";
|
|
import { DrawPile } from "./drawpile.ts";
|
|
import { D, I } from "./engine/public.ts";
|
|
import { sprThrallLore } from "./sprites.ts";
|
|
import {
|
|
BG_INSET,
|
|
FG_TEXT,
|
|
FG_TEXT_ENDORSED,
|
|
FG_TOO_EXPENSIVE,
|
|
} from "./colors.ts";
|
|
import { getPlayerProgress } from "./playerprogress.ts";
|
|
import { Architecture, LoadedNewMap } from "./newmap.ts";
|
|
import { FLOOR_CELL_SIZE, GridArt } from "./gridart.ts";
|
|
import { shadowcast } from "./shadowcast.ts";
|
|
import { withCamera } from "./layout.ts";
|
|
import { getCheckModal } from "./checkmodal.ts";
|
|
import { CARDINAL_DIRECTIONS } from "./mapgen.ts";
|
|
import { Block3D, Floor3D, World3D } from "./world3d.ts";
|
|
|
|
export class HuntMode {
|
|
map: LoadedNewMap;
|
|
floatingPlayer: Point;
|
|
velocity: Point;
|
|
faceLeft: boolean;
|
|
|
|
drawpile: DrawPile;
|
|
frame: number;
|
|
depth: number;
|
|
|
|
constructor(depth: number, map: LoadedNewMap) {
|
|
this.map = map;
|
|
this.floatingPlayer = map.entrance.offset(new Point(0.5, 0.5));
|
|
this.velocity = new Point(0, 0);
|
|
this.faceLeft = false;
|
|
|
|
this.drawpile = new DrawPile();
|
|
this.frame = 0;
|
|
this.depth = depth;
|
|
|
|
// getCheckModal().show(standardVaultTemplates[5].checks[1], null)
|
|
}
|
|
|
|
get gridifiedPlayer(): Point {
|
|
return new Point(
|
|
Math.floor(this.floatingPlayer.x),
|
|
Math.floor(this.floatingPlayer.y),
|
|
);
|
|
}
|
|
|
|
get pixelPlayer(): Point {
|
|
return new Point(
|
|
Math.floor(
|
|
this.floatingPlayer.x * FLOOR_CELL_SIZE.w - FLOOR_CELL_SIZE.w / 2,
|
|
),
|
|
Math.floor(
|
|
this.floatingPlayer.y * FLOOR_CELL_SIZE.h - FLOOR_CELL_SIZE.h / 2,
|
|
),
|
|
);
|
|
}
|
|
|
|
getDepth() {
|
|
return this.depth;
|
|
}
|
|
|
|
#computeCostToClick(mapPosition: Point): number | null {
|
|
let present = this.map.get(mapPosition);
|
|
|
|
if (present.architecture != Architecture.Floor) {
|
|
return null;
|
|
}
|
|
|
|
let dist = Math.max(
|
|
Math.abs(mapPosition.x - this.gridifiedPlayer.x),
|
|
Math.abs(mapPosition.y - this.gridifiedPlayer.y),
|
|
);
|
|
|
|
if (dist > 1) {
|
|
return null;
|
|
}
|
|
|
|
let pickup = present.pickup;
|
|
if (pickup == null) {
|
|
return 10;
|
|
}
|
|
return pickup.computeCostToClick();
|
|
}
|
|
|
|
getZoneLabel(): string | null {
|
|
return this.map.get(this.gridifiedPlayer).zoneLabel;
|
|
}
|
|
|
|
// draw
|
|
update() {
|
|
withCamera("Gameplay", () => {
|
|
this.#update();
|
|
});
|
|
}
|
|
draw() {
|
|
withCamera("Gameplay", () => {
|
|
this.#draw();
|
|
});
|
|
}
|
|
|
|
#update() {
|
|
this.frame += 1;
|
|
this.drawpile.clear();
|
|
|
|
let globalOffset = this.pixelPlayer.offset(new Point(-192, -192));
|
|
|
|
this.#updatePlayer();
|
|
this.#updateFov();
|
|
this.#updatePickups();
|
|
|
|
let world3d = new World3D(this.map.size);
|
|
for (let y = 0; y < this.map.size.h; y += 1) {
|
|
for (let x = 0; x < this.map.size.w; x += 1) {
|
|
this.#writeMapCellToWorld(world3d, new Point(x, y));
|
|
}
|
|
}
|
|
|
|
for (let y = 0; y < this.map.size.h; y += 1) {
|
|
for (let x = 0; x < this.map.size.w; x += 1) {
|
|
let offsetInPixels = new Point(x, y)
|
|
.scale(FLOOR_CELL_SIZE)
|
|
.offset(this.pixelPlayer.negate());
|
|
this.#drawMapCell(offsetInPixels, world3d, new Point(x, y));
|
|
}
|
|
}
|
|
|
|
this.#drawPlayer(globalOffset);
|
|
|
|
this.#drawBadges(globalOffset);
|
|
|
|
this.drawpile.executeOnClick();
|
|
}
|
|
|
|
#updatePlayer() {
|
|
let dx = this.velocity.x;
|
|
let dy = this.velocity.y;
|
|
|
|
let touched = false;
|
|
let amt = 0.006;
|
|
|
|
if (getPlayerProgress().getBlood() <= 0) {
|
|
amt = 0;
|
|
}
|
|
|
|
let mvdx = 0;
|
|
let mvdy = 0;
|
|
if (I.isKeyDown("w")) {
|
|
touched = true;
|
|
mvdy -= amt;
|
|
}
|
|
if (I.isKeyDown("s")) {
|
|
touched = true;
|
|
mvdy += amt;
|
|
}
|
|
if (I.isKeyDown("a")) {
|
|
touched = true;
|
|
mvdx -= amt;
|
|
}
|
|
if (I.isKeyDown("d")) {
|
|
touched = true;
|
|
mvdx += amt;
|
|
}
|
|
|
|
if (touched) {
|
|
getCheckModal().show(null, null);
|
|
}
|
|
|
|
dx += mvdx;
|
|
dy += mvdy;
|
|
dx *= 0.87;
|
|
dy *= 0.87;
|
|
|
|
if (Math.abs(dx) < 0.0001) {
|
|
dx = 0;
|
|
}
|
|
if (Math.abs(dy) < 0.0001) {
|
|
dy = 0;
|
|
}
|
|
if (mvdx < 0) {
|
|
this.faceLeft = true;
|
|
}
|
|
if (mvdx > 0) {
|
|
this.faceLeft = false;
|
|
}
|
|
|
|
let nSteps = 40;
|
|
let szX = 0.5;
|
|
let szY = 0.5;
|
|
|
|
this.velocity = new Point(dx, dy);
|
|
|
|
// try to push us away from walls if we're close
|
|
for (let offset of CARDINAL_DIRECTIONS.values()) {
|
|
let bigBbox = new Rect(
|
|
this.floatingPlayer
|
|
.offset(offset.scale(new Size(0.12, 0.12)))
|
|
.offset(new Point(-szX / 2, -szY / 2)),
|
|
new Size(szX, szY),
|
|
);
|
|
|
|
let hitsWall = false;
|
|
for (let cell of bigBbox.overlappedCells(new Size(1, 1)).values()) {
|
|
if (this.#blocksMovement(cell.top)) {
|
|
hitsWall = true;
|
|
break;
|
|
}
|
|
}
|
|
if (hitsWall) {
|
|
this.velocity = this.velocity.offset(
|
|
offset.scale(new Point(0.005, 0.005)).negate(),
|
|
);
|
|
}
|
|
}
|
|
|
|
let initialXy = this.floatingPlayer;
|
|
for (let i = 0; i < nSteps; i++) {
|
|
let oldXy = this.floatingPlayer;
|
|
let newXy = oldXy.offset(new Point(this.velocity.x / nSteps, 0));
|
|
|
|
let bbox = new Rect(
|
|
newXy.offset(new Point(-szX / 2, -szY / 2)),
|
|
new Size(szX, szY),
|
|
);
|
|
|
|
for (let cell of bbox.overlappedCells(new Size(1, 1)).values()) {
|
|
if (this.#blocksMovement(cell.top)) {
|
|
this.velocity = new Point(0, this.velocity.y);
|
|
newXy = oldXy;
|
|
}
|
|
}
|
|
|
|
oldXy = newXy;
|
|
newXy = oldXy.offset(new Point(0, this.velocity.y / nSteps));
|
|
|
|
bbox = new Rect(
|
|
newXy.offset(new Point(-szX / 2, -szY / 2)),
|
|
new Size(szX, szY),
|
|
);
|
|
|
|
for (let cell of bbox.overlappedCells(new Size(1, 1)).values()) {
|
|
if (this.#blocksMovement(cell.top)) {
|
|
this.velocity = new Point(this.velocity.x, 0);
|
|
newXy = oldXy;
|
|
}
|
|
}
|
|
|
|
this.floatingPlayer = newXy;
|
|
}
|
|
let finalXy = this.floatingPlayer;
|
|
getPlayerProgress().spendBlood(finalXy.distance(initialXy) * 10);
|
|
}
|
|
|
|
#updateFov() {
|
|
for (let y = 0; y < this.map.size.h; y += 1) {
|
|
for (let x = 0; x < this.map.size.w; x += 1) {
|
|
this.map.get(new Point(x, y)).revealed = false;
|
|
}
|
|
}
|
|
|
|
this.map.get(this.gridifiedPlayer).revealed = true;
|
|
shadowcast(
|
|
[this.gridifiedPlayer.x, this.gridifiedPlayer.y],
|
|
([x, y]: [number, number]): boolean => {
|
|
let cell = this.map.get(new Point(x, y));
|
|
let pickup = cell.pickup;
|
|
return (
|
|
cell.architecture == Architecture.Wall ||
|
|
(pickup != null && pickup.obstructsVision())
|
|
);
|
|
},
|
|
([x, y]: [number, number]) => {
|
|
if (!this.#inVisibilityRange(x, y)) {
|
|
return;
|
|
}
|
|
this.map.get(new Point(x, y)).revealed = true;
|
|
},
|
|
);
|
|
}
|
|
|
|
#updatePickups() {
|
|
for (let y = 0; y < this.map.size.h; y++) {
|
|
for (let x = 0; x < this.map.size.w; x++) {
|
|
let cell = this.map.get(new Point(x, y));
|
|
cell.pickup?.update(cell);
|
|
}
|
|
}
|
|
}
|
|
|
|
#inVisibilityRange(x: number, y: number): boolean {
|
|
let dx = x - this.gridifiedPlayer.x;
|
|
let dy = y - this.gridifiedPlayer.y;
|
|
return dx * dx + dy * dy < 13;
|
|
}
|
|
|
|
#draw() {
|
|
this.drawpile.draw();
|
|
}
|
|
|
|
#drawMapCell(offsetInPixels: Point, world3d: World3D, mapPosition: Point) {
|
|
const OFFSET_FLOOR = -256 + mapPosition.y;
|
|
|
|
const gridArt = new GridArt(offsetInPixels);
|
|
|
|
world3d.drawCell(this.drawpile, gridArt, mapPosition);
|
|
|
|
let cellData = this.map.get(mapPosition);
|
|
|
|
if (!cellData.revealed) {
|
|
return;
|
|
}
|
|
|
|
let pickup = cellData.pickup;
|
|
|
|
// draw inset zone
|
|
let cost = this.#computeCostToClick(mapPosition);
|
|
let tooExpensive = cost != null && cost > getPlayerProgress().getBlood();
|
|
this.drawpile.addClickable(
|
|
OFFSET_FLOOR,
|
|
(hover: boolean) => {
|
|
let highlighted = hover;
|
|
if (cost == null) {
|
|
highlighted = false;
|
|
}
|
|
if (!pickup?.advertisesClickable()) {
|
|
highlighted = false;
|
|
}
|
|
|
|
let color = BG_INSET;
|
|
if (highlighted) {
|
|
color = FG_TEXT;
|
|
if (tooExpensive) {
|
|
color = FG_TOO_EXPENSIVE;
|
|
}
|
|
}
|
|
|
|
gridArt.drawFloor(color);
|
|
},
|
|
gridArt.floorRect,
|
|
true,
|
|
{
|
|
onClick: () => {
|
|
if (cost == null || tooExpensive) {
|
|
return;
|
|
}
|
|
if (pickup?.onClick(cellData)) {
|
|
return;
|
|
}
|
|
},
|
|
onSqueeze: () => {
|
|
// the cost _gates_ squeezes
|
|
// but onSqueeze must pay the cost manually if a cost
|
|
// is to be paid
|
|
if (cost == null || tooExpensive) {
|
|
return;
|
|
}
|
|
pickup?.onSqueeze(cellData);
|
|
},
|
|
},
|
|
);
|
|
|
|
if (pickup != null) {
|
|
pickup.draw(this.drawpile, gridArt);
|
|
}
|
|
}
|
|
|
|
#writeMapCellToWorld(world3d: World3D, mapPosition: Point) {
|
|
let cellData = this.map.get(mapPosition);
|
|
if (!cellData.revealed) {
|
|
return;
|
|
}
|
|
|
|
let pickupBlock = cellData.pickup?.getBlock();
|
|
if (pickupBlock) {
|
|
world3d.set(mapPosition, pickupBlock);
|
|
return;
|
|
}
|
|
|
|
if (cellData.architecture == Architecture.Wall) {
|
|
world3d.set(mapPosition, Block3D.standardWall());
|
|
return;
|
|
}
|
|
|
|
world3d.set(mapPosition, new Floor3D());
|
|
}
|
|
|
|
#drawPlayer(_globalOffset: Point) {
|
|
/*
|
|
let cellOffset = this.pixelPlayer.offset(globalOffset.negate());
|
|
this.drawpile.add(1024, () => {
|
|
D.drawSprite(sprThrallLore, cellOffset, 1, {
|
|
xScale: this.faceLeft ? -2 : 2,
|
|
yScale: 2,
|
|
});
|
|
});
|
|
*/
|
|
this.drawpile.add(1024, () => {
|
|
D.drawSprite(sprThrallLore, new Point(192, 192), 1, {
|
|
xScale: this.faceLeft ? -2 : 2,
|
|
yScale: 2,
|
|
});
|
|
});
|
|
}
|
|
|
|
#drawBadges(globalOffset: Point) {
|
|
for (let y = 0; y < this.map.size.h; y += 1) {
|
|
for (let x = 0; x < this.map.size.w; x += 1) {
|
|
this.#drawBadge(globalOffset, new Point(x, y));
|
|
}
|
|
}
|
|
}
|
|
|
|
#drawBadge(globalOffset: Point, cell: Point) {
|
|
if (!this.map.get(cell).pickup?.advertisesBadge()) {
|
|
return;
|
|
}
|
|
|
|
// NOTE: This doesn't think of visibility at all
|
|
let badgePosition = cell.offset(new Size(-0.25, -0.25));
|
|
badgePosition = badgePosition.offset(
|
|
new Point(
|
|
Math.cos(cell.x * 2 + (this.frame / 720) * 2 * Math.PI) * 0.05,
|
|
Math.sin(cell.y + (this.frame / 480) * 2 * Math.PI) * 0.1,
|
|
),
|
|
);
|
|
let cellOffset = new Point(
|
|
badgePosition.x * FLOOR_CELL_SIZE.w,
|
|
badgePosition.y * FLOOR_CELL_SIZE.h,
|
|
).offset(globalOffset.negate());
|
|
|
|
let center = new Point(192, 192);
|
|
cellOffset = cellOffset.offset(center.negate());
|
|
|
|
let dist = Math.sqrt(
|
|
cellOffset.x * cellOffset.x + cellOffset.y * cellOffset.y,
|
|
);
|
|
let ang = Math.atan2(cellOffset.y, cellOffset.x);
|
|
// console.log(dist, ang);
|
|
dist = Math.min(dist, 128);
|
|
cellOffset = new Point(Math.cos(ang) * dist, Math.sin(ang) * dist);
|
|
cellOffset = cellOffset.offset(center);
|
|
|
|
this.drawpile.add(1024, () => {
|
|
// draw badge
|
|
D.fillRect(
|
|
cellOffset.offset(new Point(-4, -4)),
|
|
new Size(8, 8),
|
|
FG_TEXT_ENDORSED,
|
|
);
|
|
});
|
|
}
|
|
|
|
#blocksMovement(xy: Point) {
|
|
let cell = this.map.get(xy);
|
|
if (cell.architecture == Architecture.Wall) {
|
|
return true;
|
|
}
|
|
|
|
if (cell.pickup?.blocksMovement()) {
|
|
return true;
|
|
}
|
|
|
|
// TODO: Other cases
|
|
return false;
|
|
}
|
|
}
|
|
|
|
let active: HuntMode | null = null;
|
|
export function initHuntMode(huntMode: HuntMode) {
|
|
active = huntMode;
|
|
}
|
|
|
|
export function getHuntMode() {
|
|
if (active == null) {
|
|
throw new Error(`trying to get hunt mode before it has been initialized`);
|
|
}
|
|
return active;
|
|
}
|