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; }