Merge branch 'main' into fix-mapgen
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								src/art/pickups/collectibles.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/art/pickups/collectibles.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 605 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/art/pickups/collectibles_silhouettes.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/art/pickups/collectibles_silhouettes.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 471 B  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 455 B  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 431 B  | 
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import { Color } from "./engine/datatypes.ts";
 | 
					import { Color } from "./engine/datatypes.ts";
 | 
				
			||||||
 | 
					import { Stat } from "./datatypes.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const BG_OUTER = Color.parseHexCode("#143464");
 | 
					export const BG_OUTER = Color.parseHexCode("#143464");
 | 
				
			||||||
export const BG_WALL_OR_UNREVEALED = Color.parseHexCode("#143464");
 | 
					export const BG_WALL_OR_UNREVEALED = Color.parseHexCode("#143464");
 | 
				
			||||||
@@ -10,3 +11,36 @@ export const FG_TEXT_ENDORSED = Color.parseHexCode("#80ff80");
 | 
				
			|||||||
export const FG_BOLD = Color.parseHexCode("#ffffff");
 | 
					export const FG_BOLD = Color.parseHexCode("#ffffff");
 | 
				
			||||||
export const BG_CEILING = Color.parseHexCode("#143464");
 | 
					export const BG_CEILING = Color.parseHexCode("#143464");
 | 
				
			||||||
export const FG_MOULDING = FG_TEXT;
 | 
					export const FG_MOULDING = FG_TEXT;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// stat colors
 | 
				
			||||||
 | 
					export const SWATCH_EXP: [Color, Color] = [
 | 
				
			||||||
 | 
					  Color.parseHexCode("#b9bffb"),
 | 
				
			||||||
 | 
					  Color.parseHexCode("#e3e6ff"),
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SWATCH_AGI: [Color, Color] = [
 | 
				
			||||||
 | 
					  Color.parseHexCode("#df3e23"),
 | 
				
			||||||
 | 
					  Color.parseHexCode("#fa6a0a"),
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SWATCH_INT: [Color, Color] = [
 | 
				
			||||||
 | 
					  Color.parseHexCode("#285cc4"),
 | 
				
			||||||
 | 
					  Color.parseHexCode("#249fde"),
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SWATCH_CHA: [Color, Color] = [
 | 
				
			||||||
 | 
					  Color.parseHexCode("#793a80"),
 | 
				
			||||||
 | 
					  Color.parseHexCode("#bc4a9b"),
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SWATCH_PSI: [Color, Color] = [
 | 
				
			||||||
 | 
					  Color.parseHexCode("#9cdb43"),
 | 
				
			||||||
 | 
					  Color.parseHexCode("#d6f264"),
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SWATCH_STAT: Record<Stat, [Color, Color]> = {
 | 
				
			||||||
 | 
					  AGI: SWATCH_AGI,
 | 
				
			||||||
 | 
					  INT: SWATCH_INT,
 | 
				
			||||||
 | 
					  CHA: SWATCH_CHA,
 | 
				
			||||||
 | 
					  PSI: SWATCH_PSI,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										134
									
								
								src/floater.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/floater.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
				
			|||||||
 | 
					import { BreakableBlockPickupCallbacks } from "./pickups.ts";
 | 
				
			||||||
 | 
					import { Point, Rect, Size } from "./engine/datatypes.ts";
 | 
				
			||||||
 | 
					import { displace } from "./physics.ts";
 | 
				
			||||||
 | 
					import { getHuntMode } from "./huntmode.ts";
 | 
				
			||||||
 | 
					import { DrawPile } from "./drawpile.ts";
 | 
				
			||||||
 | 
					import { FLOOR_CELL_SIZE } from "./gridart.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Floater {
 | 
				
			||||||
 | 
					  xy: Point;
 | 
				
			||||||
 | 
					  velocity: Point;
 | 
				
			||||||
 | 
					  z: number;
 | 
				
			||||||
 | 
					  velZ: number;
 | 
				
			||||||
 | 
					  frame: number;
 | 
				
			||||||
 | 
					  spin: number;
 | 
				
			||||||
 | 
					  collected: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #callbacks: BreakableBlockPickupCallbacks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(xy: Point, z: number, callbacks: BreakableBlockPickupCallbacks) {
 | 
				
			||||||
 | 
					    this.xy = xy;
 | 
				
			||||||
 | 
					    this.velocity = new Point(0, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.z = z;
 | 
				
			||||||
 | 
					    this.velZ = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.frame = 0;
 | 
				
			||||||
 | 
					    this.spin = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.collected = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.#callbacks = callbacks;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  update() {
 | 
				
			||||||
 | 
					    let bbox = this.bbox;
 | 
				
			||||||
 | 
					    let { displacement, dxy } = displace(
 | 
				
			||||||
 | 
					      bbox,
 | 
				
			||||||
 | 
					      this.velocity,
 | 
				
			||||||
 | 
					      (r) => getHuntMode().isBlocked(r),
 | 
				
			||||||
 | 
					      { bounce: 0.6 },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.xy = this.xy.offset(displacement);
 | 
				
			||||||
 | 
					    this.velocity = dxy;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.velocity = new Point(this.velocity.x * 0.99, this.velocity.y * 0.99);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.velZ -= 0.04;
 | 
				
			||||||
 | 
					    this.z += this.velZ;
 | 
				
			||||||
 | 
					    if (this.z < 0) {
 | 
				
			||||||
 | 
					      this.z = 0;
 | 
				
			||||||
 | 
					      this.velZ = -this.velZ * 0.8; // minor bounce
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.frame += 1;
 | 
				
			||||||
 | 
					    this.spin += this.velocity.distance(new Point(0, 0)) * 4;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let playerPos = getHuntMode().floatingPlayer;
 | 
				
			||||||
 | 
					    let dist = playerPos.distance(this.xy);
 | 
				
			||||||
 | 
					    let dir = Math.atan2(this.xy.y - playerPos.y, this.xy.x - playerPos.x);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this.frame >= 60) {
 | 
				
			||||||
 | 
					      if (dist < 0.5) {
 | 
				
			||||||
 | 
					        this.collect();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      let gravityAmt = (0.6 - dist) / 0.6;
 | 
				
			||||||
 | 
					      gravityAmt = Math.pow(gravityAmt, 0.8);
 | 
				
			||||||
 | 
					      if (gravityAmt > 0) {
 | 
				
			||||||
 | 
					        let dx = -Math.cos(dir) * 0.005;
 | 
				
			||||||
 | 
					        let dy = -Math.sin(dir) * 0.005;
 | 
				
			||||||
 | 
					        this.velocity = this.velocity.offset(
 | 
				
			||||||
 | 
					          new Point(gravityAmt * dx, gravityAmt * dy),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  collect() {
 | 
				
			||||||
 | 
					    if (this.collected) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.collected = true;
 | 
				
			||||||
 | 
					    this.#callbacks.obtain();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  draw(drawpile: DrawPile, globalOffset: Point) {
 | 
				
			||||||
 | 
					    // TODO: Use some kind of global projection type
 | 
				
			||||||
 | 
					    let xyLow = this.xy
 | 
				
			||||||
 | 
					      .offset(new Point(-0.5, -0.5))
 | 
				
			||||||
 | 
					      .scale(FLOOR_CELL_SIZE)
 | 
				
			||||||
 | 
					      .offset(globalOffset.negate());
 | 
				
			||||||
 | 
					    let z = this.z / 100.0;
 | 
				
			||||||
 | 
					    let xy = new Point(
 | 
				
			||||||
 | 
					      xyLow.x,
 | 
				
			||||||
 | 
					      xyLow.y - z * 16, // not perspectivally accurate, but convincing
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    drawpile.add(0, () => {
 | 
				
			||||||
 | 
					      this.drawParticle(xyLow.offset(new Point(0, 2)), true);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (this.frame >= 1200) {
 | 
				
			||||||
 | 
					      if (this.frame % 30 < 15) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else if (this.frame >= 960) {
 | 
				
			||||||
 | 
					      if (this.frame % 60 < 30) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else if (this.frame >= 720) {
 | 
				
			||||||
 | 
					      if (this.frame % 120 < 60) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    drawpile.add(128, () => {
 | 
				
			||||||
 | 
					      this.drawParticle(xy, false);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get alive(): boolean {
 | 
				
			||||||
 | 
					    return !this.collected && this.frame < 1440;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get bbox(): Rect {
 | 
				
			||||||
 | 
					    let w = 0.25;
 | 
				
			||||||
 | 
					    let h = 0.25;
 | 
				
			||||||
 | 
					    return new Rect(this.xy.offset(new Point(-w / 2, -h / 2)), new Size(w, h));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  drawParticle(projected: Point, isShadow: boolean): any {
 | 
				
			||||||
 | 
					    this.#callbacks.drawParticle(
 | 
				
			||||||
 | 
					      projected,
 | 
				
			||||||
 | 
					      isShadow,
 | 
				
			||||||
 | 
					      ((0 * Math.PI) / 4) * Math.cos(this.spin),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										215
									
								
								src/huntmode.ts
									
									
									
									
									
								
							
							
						
						
									
										215
									
								
								src/huntmode.ts
									
									
									
									
									
								
							@@ -4,9 +4,6 @@ import { D, I } from "./engine/public.ts";
 | 
				
			|||||||
import { sprThrallLore } from "./sprites.ts";
 | 
					import { sprThrallLore } from "./sprites.ts";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  BG_INSET,
 | 
					  BG_INSET,
 | 
				
			||||||
  BG_WALL_OR_UNREVEALED,
 | 
					 | 
				
			||||||
  FG_BOLD,
 | 
					 | 
				
			||||||
  FG_MOULDING,
 | 
					 | 
				
			||||||
  FG_TEXT,
 | 
					  FG_TEXT,
 | 
				
			||||||
  FG_TEXT_ENDORSED,
 | 
					  FG_TEXT_ENDORSED,
 | 
				
			||||||
  FG_TOO_EXPENSIVE,
 | 
					  FG_TOO_EXPENSIVE,
 | 
				
			||||||
@@ -18,9 +15,13 @@ import { shadowcast } from "./shadowcast.ts";
 | 
				
			|||||||
import { withCamera } from "./layout.ts";
 | 
					import { withCamera } from "./layout.ts";
 | 
				
			||||||
import { getCheckModal } from "./checkmodal.ts";
 | 
					import { getCheckModal } from "./checkmodal.ts";
 | 
				
			||||||
import { CARDINAL_DIRECTIONS } from "./mapgen.ts";
 | 
					import { CARDINAL_DIRECTIONS } from "./mapgen.ts";
 | 
				
			||||||
 | 
					import { Block3D, Floor3D, World3D } from "./world3d.ts";
 | 
				
			||||||
 | 
					import { Floater } from "./floater.ts";
 | 
				
			||||||
 | 
					import { displace } from "./physics.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class HuntMode {
 | 
					export class HuntMode {
 | 
				
			||||||
  map: LoadedNewMap;
 | 
					  map: LoadedNewMap;
 | 
				
			||||||
 | 
					  floaters: Floater[];
 | 
				
			||||||
  floatingPlayer: Point;
 | 
					  floatingPlayer: Point;
 | 
				
			||||||
  velocity: Point;
 | 
					  velocity: Point;
 | 
				
			||||||
  faceLeft: boolean;
 | 
					  faceLeft: boolean;
 | 
				
			||||||
@@ -31,6 +32,7 @@ export class HuntMode {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  constructor(depth: number, map: LoadedNewMap) {
 | 
					  constructor(depth: number, map: LoadedNewMap) {
 | 
				
			||||||
    this.map = map;
 | 
					    this.map = map;
 | 
				
			||||||
 | 
					    this.floaters = [];
 | 
				
			||||||
    this.floatingPlayer = map.entrance.offset(new Point(0.5, 0.5));
 | 
					    this.floatingPlayer = map.entrance.offset(new Point(0.5, 0.5));
 | 
				
			||||||
    this.velocity = new Point(0, 0);
 | 
					    this.velocity = new Point(0, 0);
 | 
				
			||||||
    this.faceLeft = false;
 | 
					    this.faceLeft = false;
 | 
				
			||||||
@@ -111,16 +113,26 @@ export class HuntMode {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    this.#updatePlayer();
 | 
					    this.#updatePlayer();
 | 
				
			||||||
    this.#updateFov();
 | 
					    this.#updateFov();
 | 
				
			||||||
 | 
					    this.#updateFloaters();
 | 
				
			||||||
    this.#updatePickups();
 | 
					    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 y = 0; y < this.map.size.h; y += 1) {
 | 
				
			||||||
      for (let x = 0; x < this.map.size.w; x += 1) {
 | 
					      for (let x = 0; x < this.map.size.w; x += 1) {
 | 
				
			||||||
        let offsetInPixels = new Point(x, y)
 | 
					        let offsetInPixels = new Point(x, y)
 | 
				
			||||||
          .scale(FLOOR_CELL_SIZE)
 | 
					          .scale(FLOOR_CELL_SIZE)
 | 
				
			||||||
          .offset(this.pixelPlayer.negate());
 | 
					          .offset(this.pixelPlayer.negate());
 | 
				
			||||||
        this.#drawMapCell(offsetInPixels, new Point(x, y));
 | 
					        this.#drawMapCell(offsetInPixels, world3d, new Point(x, y));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.#drawFloaters(globalOffset);
 | 
				
			||||||
    this.#drawPlayer(globalOffset);
 | 
					    this.#drawPlayer(globalOffset);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.#drawBadges(globalOffset);
 | 
					    this.#drawBadges(globalOffset);
 | 
				
			||||||
@@ -128,6 +140,10 @@ export class HuntMode {
 | 
				
			|||||||
    this.drawpile.executeOnClick();
 | 
					    this.drawpile.executeOnClick();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  spawnFloater(floater: Floater) {
 | 
				
			||||||
 | 
					    this.floaters.push(floater);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  #updatePlayer() {
 | 
					  #updatePlayer() {
 | 
				
			||||||
    let dx = this.velocity.x;
 | 
					    let dx = this.velocity.x;
 | 
				
			||||||
    let dy = this.velocity.y;
 | 
					    let dy = this.velocity.y;
 | 
				
			||||||
@@ -180,9 +196,8 @@ export class HuntMode {
 | 
				
			|||||||
      this.faceLeft = false;
 | 
					      this.faceLeft = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let nSteps = 40;
 | 
					    let szX = 0.5;
 | 
				
			||||||
    let szX = 0.75;
 | 
					    let szY = 0.5;
 | 
				
			||||||
    let szY = 0.75;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.velocity = new Point(dx, dy);
 | 
					    this.velocity = new Point(dx, dy);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -190,7 +205,7 @@ export class HuntMode {
 | 
				
			|||||||
    for (let offset of CARDINAL_DIRECTIONS.values()) {
 | 
					    for (let offset of CARDINAL_DIRECTIONS.values()) {
 | 
				
			||||||
      let bigBbox = new Rect(
 | 
					      let bigBbox = new Rect(
 | 
				
			||||||
        this.floatingPlayer
 | 
					        this.floatingPlayer
 | 
				
			||||||
          .offset(offset.scale(new Size(0.02, 0.02)))
 | 
					          .offset(offset.scale(new Size(0.12, 0.12)))
 | 
				
			||||||
          .offset(new Point(-szX / 2, -szY / 2)),
 | 
					          .offset(new Point(-szX / 2, -szY / 2)),
 | 
				
			||||||
        new Size(szX, szY),
 | 
					        new Size(szX, szY),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -209,42 +224,17 @@ export class HuntMode {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let initialXy = this.floatingPlayer;
 | 
					    let origin = new Point(szX / 2, szY / 2);
 | 
				
			||||||
    for (let i = 0; i < nSteps; i++) {
 | 
					    let bbox = new Rect(
 | 
				
			||||||
      let oldXy = this.floatingPlayer;
 | 
					      this.floatingPlayer.offset(origin.negate()),
 | 
				
			||||||
      let newXy = oldXy.offset(new Point(this.velocity.x / nSteps, 0));
 | 
					      new Size(szX, szY),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
      let bbox = new Rect(
 | 
					    let { displacement, dxy } = displace(bbox, this.velocity, (b: Rect) =>
 | 
				
			||||||
        newXy.offset(new Point(-szX / 2, -szY / 2)),
 | 
					      this.isBlocked(b),
 | 
				
			||||||
        new Size(szX, szY),
 | 
					    );
 | 
				
			||||||
      );
 | 
					    this.floatingPlayer = this.floatingPlayer.offset(displacement);
 | 
				
			||||||
 | 
					    this.velocity = dxy;
 | 
				
			||||||
      for (let cell of bbox.overlappedCells(new Size(1, 1)).values()) {
 | 
					    getPlayerProgress().spendBlood(displacement.distance(new Point(0, 0)) * 10);
 | 
				
			||||||
        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() {
 | 
					  #updateFov() {
 | 
				
			||||||
@@ -274,6 +264,18 @@ export class HuntMode {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #updateFloaters() {
 | 
				
			||||||
 | 
					    let newFloaters = [];
 | 
				
			||||||
 | 
					    for (let f of this.floaters.values()) {
 | 
				
			||||||
 | 
					      f.update();
 | 
				
			||||||
 | 
					      if (f.alive) {
 | 
				
			||||||
 | 
					        newFloaters.push(f);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.floaters = newFloaters;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  #updatePickups() {
 | 
					  #updatePickups() {
 | 
				
			||||||
    for (let y = 0; y < this.map.size.h; y++) {
 | 
					    for (let y = 0; y < this.map.size.h; y++) {
 | 
				
			||||||
      for (let x = 0; x < this.map.size.w; x++) {
 | 
					      for (let x = 0; x < this.map.size.w; x++) {
 | 
				
			||||||
@@ -293,26 +295,14 @@ export class HuntMode {
 | 
				
			|||||||
    this.drawpile.draw();
 | 
					    this.drawpile.draw();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  #drawMapCell(offsetInPixels: Point, mapPosition: Point) {
 | 
					  #drawMapCell(offsetInPixels: Point, world3d: World3D, mapPosition: Point) {
 | 
				
			||||||
    const OFFSET_UNDER_FLOOR = -512 + mapPosition.y;
 | 
					 | 
				
			||||||
    const OFFSET_FLOOR = -256 + mapPosition.y;
 | 
					    const OFFSET_FLOOR = -256 + mapPosition.y;
 | 
				
			||||||
    const OFFSET_AIR = 0 + mapPosition.y;
 | 
					 | 
				
			||||||
    const OFFSET_TOP = 256 + mapPosition.y;
 | 
					 | 
				
			||||||
    const OFFSET_TOP_OF_TOP = 512 + mapPosition.y;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const gridArt = new GridArt(offsetInPixels);
 | 
					    const gridArt = new GridArt(offsetInPixels);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let cellData = this.map.get(mapPosition);
 | 
					    world3d.drawCell(this.drawpile, gridArt, mapPosition);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.drawpile.add(OFFSET_UNDER_FLOOR, () => {
 | 
					    let cellData = this.map.get(mapPosition);
 | 
				
			||||||
      gridArt.drawCeiling(BG_WALL_OR_UNREVEALED);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    if (cellData.architecture == Architecture.Wall || !cellData.revealed) {
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_TOP, () => {
 | 
					 | 
				
			||||||
        gridArt.drawCeiling(BG_WALL_OR_UNREVEALED);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!cellData.revealed) {
 | 
					    if (!cellData.revealed) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
@@ -343,7 +333,6 @@ export class HuntMode {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        gridArt.drawFloor(color);
 | 
					        gridArt.drawFloor(color);
 | 
				
			||||||
        pickup?.drawFloor(gridArt);
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      gridArt.floorRect,
 | 
					      gridArt.floorRect,
 | 
				
			||||||
      true,
 | 
					      true,
 | 
				
			||||||
@@ -369,70 +358,38 @@ export class HuntMode {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (pickup != null) {
 | 
					    if (pickup != null) {
 | 
				
			||||||
      this.drawpile.add(OFFSET_AIR, () => {
 | 
					      pickup.draw(this.drawpile, gridArt);
 | 
				
			||||||
        pickup.drawInAir(gridArt);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const isRevealedBlock = (dx: number, dy: number) => {
 | 
					 | 
				
			||||||
      let other = this.map.get(mapPosition.offset(new Point(dx, dy)));
 | 
					 | 
				
			||||||
      return other.revealed && other.architecture == Architecture.Wall;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    if (isRevealedBlock(0, -1) && isRevealedBlock(-1, 0)) {
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
					 | 
				
			||||||
        gridArt.drawMouldingTopLeft(FG_MOULDING);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (isRevealedBlock(0, -1)) {
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_AIR, () => {
 | 
					 | 
				
			||||||
        gridArt.drawWallTop(FG_TEXT);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
					 | 
				
			||||||
        gridArt.drawMouldingTop(FG_MOULDING);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (isRevealedBlock(0, -1) && isRevealedBlock(1, 0)) {
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
					 | 
				
			||||||
        gridArt.drawMouldingTopRight(FG_MOULDING);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (isRevealedBlock(-1, 0)) {
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_AIR, () => {
 | 
					 | 
				
			||||||
        gridArt.drawWallLeft(FG_TEXT);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
					 | 
				
			||||||
        gridArt.drawMouldingLeft(FG_MOULDING);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (isRevealedBlock(0, 1) && isRevealedBlock(-1, 0)) {
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
					 | 
				
			||||||
        gridArt.drawMouldingBottomLeft(FG_MOULDING);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (isRevealedBlock(0, 1)) {
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_AIR, () => {
 | 
					 | 
				
			||||||
        gridArt.drawWallBottom(FG_BOLD);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
					 | 
				
			||||||
        gridArt.drawMouldingBottom(FG_MOULDING);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (isRevealedBlock(0, 1) && isRevealedBlock(1, 0)) {
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
					 | 
				
			||||||
        gridArt.drawMouldingBottomRight(FG_MOULDING);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (isRevealedBlock(1, 0)) {
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_AIR, () => {
 | 
					 | 
				
			||||||
        gridArt.drawWallRight(FG_BOLD);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      this.drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
					 | 
				
			||||||
        gridArt.drawMouldingRight(FG_MOULDING);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  #drawPlayer(globalOffset: Point) {
 | 
					  #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());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #drawFloaters(globalOffset: Point) {
 | 
				
			||||||
 | 
					    for (let f of this.floaters.values()) {
 | 
				
			||||||
 | 
					      f.draw(this.drawpile, globalOffset);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  #drawPlayer(_globalOffset: Point) {
 | 
				
			||||||
 | 
					    /*
 | 
				
			||||||
    let cellOffset = this.pixelPlayer.offset(globalOffset.negate());
 | 
					    let cellOffset = this.pixelPlayer.offset(globalOffset.negate());
 | 
				
			||||||
    this.drawpile.add(1024, () => {
 | 
					    this.drawpile.add(1024, () => {
 | 
				
			||||||
      D.drawSprite(sprThrallLore, cellOffset, 1, {
 | 
					      D.drawSprite(sprThrallLore, cellOffset, 1, {
 | 
				
			||||||
@@ -440,6 +397,13 @@ export class HuntMode {
 | 
				
			|||||||
        yScale: 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) {
 | 
					  #drawBadges(globalOffset: Point) {
 | 
				
			||||||
@@ -490,6 +454,15 @@ export class HuntMode {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isBlocked(bbox: Rect): boolean {
 | 
				
			||||||
 | 
					    for (let cell of bbox.overlappedCells(new Size(1, 1)).values()) {
 | 
				
			||||||
 | 
					      if (this.#blocksMovement(cell.top)) {
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  #blocksMovement(xy: Point) {
 | 
					  #blocksMovement(xy: Point) {
 | 
				
			||||||
    let cell = this.map.get(xy);
 | 
					    let cell = this.map.get(xy);
 | 
				
			||||||
    if (cell.architecture == Architecture.Wall) {
 | 
					    if (cell.architecture == Architecture.Wall) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -167,6 +167,10 @@ export class CellView {
 | 
				
			|||||||
    this.#point = point;
 | 
					    this.#point = point;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get xy(): Point {
 | 
				
			||||||
 | 
					    return this.#point;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  set architecture(value: Architecture) {
 | 
					  set architecture(value: Architecture) {
 | 
				
			||||||
    this.#map.setArchitecture(this.#point, value);
 | 
					    this.#map.setArchitecture(this.#point, value);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										36
									
								
								src/physics.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/physics.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					import { Point, Rect } from "./engine/datatypes.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function displace(
 | 
				
			||||||
 | 
					  bbox: Rect,
 | 
				
			||||||
 | 
					  dxy: Point,
 | 
				
			||||||
 | 
					  blocked: (where: Rect) => boolean,
 | 
				
			||||||
 | 
					  options?: { bounce?: number },
 | 
				
			||||||
 | 
					): { bbox: Rect; displacement: Point; dxy: Point } {
 | 
				
			||||||
 | 
					  let nSteps = 40;
 | 
				
			||||||
 | 
					  let bounce = options?.bounce ?? 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let xy = bbox.top;
 | 
				
			||||||
 | 
					  for (let i = 0; i < nSteps; i++) {
 | 
				
			||||||
 | 
					    let trialXy = xy.offset(new Point(dxy.x / nSteps, 0));
 | 
				
			||||||
 | 
					    let trialBbox = new Rect(trialXy, bbox.size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (blocked(trialBbox)) {
 | 
				
			||||||
 | 
					      dxy = new Point(bounce * -dxy.x, dxy.y);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      xy = trialXy;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    trialXy = xy.offset(new Point(0, dxy.y / nSteps));
 | 
				
			||||||
 | 
					    trialBbox = new Rect(trialXy, bbox.size);
 | 
				
			||||||
 | 
					    if (blocked(trialBbox)) {
 | 
				
			||||||
 | 
					      dxy = new Point(dxy.x, bounce * -dxy.y);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      xy = trialXy;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    bbox: new Rect(xy, bbox.size),
 | 
				
			||||||
 | 
					    displacement: xy.offset(bbox.top.negate()),
 | 
				
			||||||
 | 
					    dxy,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										308
									
								
								src/pickups.ts
									
									
									
									
									
								
							
							
						
						
									
										308
									
								
								src/pickups.ts
									
									
									
									
									
								
							@@ -6,16 +6,19 @@ import { generateMap } from "./mapgen.ts";
 | 
				
			|||||||
import { ALL_STATS, Stat } from "./datatypes.ts";
 | 
					import { ALL_STATS, Stat } from "./datatypes.ts";
 | 
				
			||||||
import { D } from "./engine/public.ts";
 | 
					import { D } from "./engine/public.ts";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  sprCollectibles,
 | 
				
			||||||
 | 
					  sprCollectiblesSilhouettes,
 | 
				
			||||||
  sprLadder,
 | 
					  sprLadder,
 | 
				
			||||||
  sprLock,
 | 
					  sprLock,
 | 
				
			||||||
  sprResourcePickup,
 | 
					 | 
				
			||||||
  sprStatPickup,
 | 
					 | 
				
			||||||
} from "./sprites.ts";
 | 
					} from "./sprites.ts";
 | 
				
			||||||
import { GridArt } from "./gridart.ts";
 | 
					import { GridArt } from "./gridart.ts";
 | 
				
			||||||
import { getCheckModal } from "./checkmodal.ts";
 | 
					import { getCheckModal } from "./checkmodal.ts";
 | 
				
			||||||
import { Point, Size } from "./engine/datatypes.ts";
 | 
					import { Point, Size } from "./engine/datatypes.ts";
 | 
				
			||||||
import { choose } from "./utils.ts";
 | 
					import { choose } from "./utils.ts";
 | 
				
			||||||
import { FG_TEXT } from "./colors.ts";
 | 
					import { FG_BOLD, FG_TEXT, SWATCH_EXP, SWATCH_STAT } from "./colors.ts";
 | 
				
			||||||
 | 
					import { Block3D } from "./world3d.ts";
 | 
				
			||||||
 | 
					import { DrawPile } from "./drawpile.ts";
 | 
				
			||||||
 | 
					import { Floater } from "./floater.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Pickup =
 | 
					export type Pickup =
 | 
				
			||||||
  | LockPickup
 | 
					  | LockPickup
 | 
				
			||||||
@@ -52,14 +55,19 @@ export class LockPickup {
 | 
				
			|||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  drawFloor() {}
 | 
					  draw(drawpile: DrawPile, gridArt: GridArt) {
 | 
				
			||||||
  drawInAir(gridArt: GridArt) {
 | 
					    drawpile.add(0, () => {
 | 
				
			||||||
    for (let z = 0; z < 5; z += 0.25) {
 | 
					      for (let z = 0; z < 5; z += 0.25) {
 | 
				
			||||||
      D.drawSprite(sprLock, gridArt.project(z), 0, {
 | 
					        D.drawSprite(sprLock, gridArt.project(z), 0, {
 | 
				
			||||||
        xScale: 2.0,
 | 
					          xScale: 2.0,
 | 
				
			||||||
        yScale: 2.0,
 | 
					          yScale: 2.0,
 | 
				
			||||||
      });
 | 
					        });
 | 
				
			||||||
    }
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBlock(): Block3D | null {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update() {}
 | 
					  update() {}
 | 
				
			||||||
@@ -72,12 +80,16 @@ export class LockPickup {
 | 
				
			|||||||
  onSqueeze() {}
 | 
					  onSqueeze() {}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type BreakableBlockPickupCallbacks =
 | 
				
			||||||
 | 
					  | StatPickupCallbacks
 | 
				
			||||||
 | 
					  | ExperiencePickupCallbacks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const RECOVERY_PER_TICK: number = 0.1;
 | 
					const RECOVERY_PER_TICK: number = 0.1;
 | 
				
			||||||
export class BreakableBlockPickup {
 | 
					export class BreakableBlockPickup {
 | 
				
			||||||
  callbacks: StatPickupCallbacks | ExperiencePickupCallbacks;
 | 
					  callbacks: BreakableBlockPickupCallbacks;
 | 
				
			||||||
  breakProgress: number;
 | 
					  breakProgress: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(callbacks: StatPickupCallbacks | ExperiencePickupCallbacks) {
 | 
					  constructor(callbacks: BreakableBlockPickupCallbacks) {
 | 
				
			||||||
    this.callbacks = callbacks;
 | 
					    this.callbacks = callbacks;
 | 
				
			||||||
    this.breakProgress = 0.0;
 | 
					    this.breakProgress = 0.0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -102,28 +114,53 @@ export class BreakableBlockPickup {
 | 
				
			|||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  drawFloor() {}
 | 
					  get #adjustedProgress(): number {
 | 
				
			||||||
  drawInAir(gridArt: GridArt) {
 | 
					    return Math.pow(this.breakProgress, 2.15);
 | 
				
			||||||
    let progress = Math.pow(this.breakProgress, 2.15);
 | 
					  }
 | 
				
			||||||
    let extraMult = 1.0;
 | 
					 | 
				
			||||||
    let angleRange = 0;
 | 
					 | 
				
			||||||
    if (progress != 0) {
 | 
					 | 
				
			||||||
      extraMult = 1.2;
 | 
					 | 
				
			||||||
      angleRange = 10;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.callbacks.draw(gridArt.project(5), {
 | 
					  draw(drawpile: DrawPile, gridArt: GridArt) {
 | 
				
			||||||
      xScale: 2 * (1.0 - progress * 0.7) * extraMult,
 | 
					    drawpile.add(384, () => {
 | 
				
			||||||
      yScale: 2 * (1.0 - progress * 0.7) * extraMult,
 | 
					      let progress = this.#adjustedProgress;
 | 
				
			||||||
      angle: (2 * progress - 1) * angleRange,
 | 
					      let extraMult = 1.0;
 | 
				
			||||||
 | 
					      let angleRange = 0;
 | 
				
			||||||
 | 
					      if (progress != 0) {
 | 
				
			||||||
 | 
					        extraMult = 1.2;
 | 
				
			||||||
 | 
					        angleRange = 10;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.callbacks.draw(gridArt, {
 | 
				
			||||||
 | 
					        xScale: 2 * (1.0 - progress * 0.7) * extraMult,
 | 
				
			||||||
 | 
					        yScale: 2 * (1.0 - progress * 0.7) * extraMult,
 | 
				
			||||||
 | 
					        angle: (2 * progress - 1) * angleRange,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBlock(): Block3D | null {
 | 
				
			||||||
 | 
					    return this.callbacks.getBlock(this.breakProgress);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update(cellData: CellView) {
 | 
					  update(cellData: CellView) {
 | 
				
			||||||
    if (this.breakProgress >= 1.0) {
 | 
					    if (this.breakProgress >= 1.0) {
 | 
				
			||||||
      getPlayerProgress().spendBlood(this.callbacks.cost);
 | 
					      getPlayerProgress().spendBlood(this.callbacks.cost);
 | 
				
			||||||
      cellData.pickup = null;
 | 
					      cellData.pickup = null;
 | 
				
			||||||
      this.callbacks.obtain();
 | 
					
 | 
				
			||||||
 | 
					      let n = choose([1, 1, 1, 1, 1, 2, 3]);
 | 
				
			||||||
 | 
					      for (let i = 0; i < n; i++) {
 | 
				
			||||||
 | 
					        let floater = new Floater(
 | 
				
			||||||
 | 
					          cellData.xy.offset(new Point(0.5, 0.5)),
 | 
				
			||||||
 | 
					          50,
 | 
				
			||||||
 | 
					          this.callbacks,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        let speed = 0.015;
 | 
				
			||||||
 | 
					        let direction = Math.random() * Math.PI * 2;
 | 
				
			||||||
 | 
					        floater.velocity = new Point(
 | 
				
			||||||
 | 
					          Math.cos(direction) * speed,
 | 
				
			||||||
 | 
					          Math.sin(direction) * speed * 0.1,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        floater.velZ = 0.8;
 | 
				
			||||||
 | 
					        getHuntMode().spawnFloater(floater);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.breakProgress = Math.max(0.0, this.breakProgress - RECOVERY_PER_TICK);
 | 
					    this.breakProgress = Math.max(0.0, this.breakProgress - RECOVERY_PER_TICK);
 | 
				
			||||||
@@ -157,8 +194,16 @@ export class StatPickupCallbacks {
 | 
				
			|||||||
    getPlayerProgress().purloinItem();
 | 
					    getPlayerProgress().purloinItem();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBlock(progress: number) {
 | 
				
			||||||
 | 
					    return new Block3D(
 | 
				
			||||||
 | 
					      progress > 0.6 ? FG_BOLD : SWATCH_STAT[this.#stat][1],
 | 
				
			||||||
 | 
					      progress > 0.6 ? FG_TEXT : SWATCH_STAT[this.#stat][0],
 | 
				
			||||||
 | 
					      progress > 0.6 ? FG_BOLD : SWATCH_STAT[this.#stat][1],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  draw(
 | 
					  draw(
 | 
				
			||||||
    at: Point,
 | 
					    gridArt: GridArt,
 | 
				
			||||||
    options: { xScale?: number; yScale?: number; angle?: number },
 | 
					    options: { xScale?: number; yScale?: number; angle?: number },
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    let statIndex = ALL_STATS.indexOf(this.#stat);
 | 
					    let statIndex = ALL_STATS.indexOf(this.#stat);
 | 
				
			||||||
@@ -166,7 +211,21 @@ export class StatPickupCallbacks {
 | 
				
			|||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    D.drawSprite(sprStatPickup, at, statIndex, options);
 | 
					    let at = gridArt.project(100);
 | 
				
			||||||
 | 
					    D.drawSprite(sprCollectiblesSilhouettes, at, statIndex, options);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  drawParticle(at: Point, isShadow: boolean, rotation: number) {
 | 
				
			||||||
 | 
					    let statIndex = ALL_STATS.indexOf(this.#stat);
 | 
				
			||||||
 | 
					    if (statIndex == -1) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    D.drawSprite(
 | 
				
			||||||
 | 
					      isShadow ? sprCollectiblesSilhouettes : sprCollectibles,
 | 
				
			||||||
 | 
					      at,
 | 
				
			||||||
 | 
					      statIndex,
 | 
				
			||||||
 | 
					      { xScale: 2 * Math.cos(rotation), yScale: 2 },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -182,11 +241,29 @@ export class ExperiencePickupCallbacks {
 | 
				
			|||||||
    getPlayerProgress().purloinItem();
 | 
					    getPlayerProgress().purloinItem();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBlock(progress: number) {
 | 
				
			||||||
 | 
					    return new Block3D(
 | 
				
			||||||
 | 
					      progress > 0.6 ? FG_BOLD : SWATCH_EXP[1],
 | 
				
			||||||
 | 
					      progress > 0.6 ? FG_TEXT : SWATCH_EXP[0],
 | 
				
			||||||
 | 
					      progress > 0.6 ? FG_BOLD : SWATCH_EXP[1],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  draw(
 | 
					  draw(
 | 
				
			||||||
    at: Point,
 | 
					    gridArt: GridArt,
 | 
				
			||||||
    options: { xScale?: number; yScale?: number; angle?: number },
 | 
					    options: { xScale?: number; yScale?: number; angle?: number },
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    D.drawSprite(sprResourcePickup, at, 0, options);
 | 
					    let at = gridArt.project(100);
 | 
				
			||||||
 | 
					    D.drawSprite(sprCollectiblesSilhouettes, at, 4, options);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  drawParticle(at: Point, isShadow: boolean, rotation: number) {
 | 
				
			||||||
 | 
					    D.drawSprite(
 | 
				
			||||||
 | 
					      isShadow ? sprCollectiblesSilhouettes : sprCollectibles,
 | 
				
			||||||
 | 
					      at,
 | 
				
			||||||
 | 
					      4,
 | 
				
			||||||
 | 
					      { xScale: 2 * Math.cos(rotation), yScale: 2 },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -211,13 +288,18 @@ export class LadderPickup {
 | 
				
			|||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  drawFloor(gridArt: GridArt) {
 | 
					  draw(drawpile: DrawPile, gridArt: GridArt) {
 | 
				
			||||||
    D.drawSprite(sprLadder, gridArt.project(0.0), 0, {
 | 
					    drawpile.add(-128, () => {
 | 
				
			||||||
      xScale: 2.0,
 | 
					      D.drawSprite(sprLadder, gridArt.project(0.0), 0, {
 | 
				
			||||||
      yScale: 2.0,
 | 
					        xScale: 2.0,
 | 
				
			||||||
 | 
					        yScale: 2.0,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  drawInAir() {}
 | 
					
 | 
				
			||||||
 | 
					  getBlock(): Block3D | null {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update() {}
 | 
					  update() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -257,15 +339,20 @@ export class ThrallPickup {
 | 
				
			|||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  drawFloor() {}
 | 
					  draw(drawpile: DrawPile, gridArt: GridArt) {
 | 
				
			||||||
  drawInAir(gridArt: GridArt) {
 | 
					    drawpile.add(0, () => {
 | 
				
			||||||
    let data = getThralls().get(this.thrall);
 | 
					      let data = getThralls().get(this.thrall);
 | 
				
			||||||
    D.drawSprite(data.sprite, gridArt.project(0.0), 0, {
 | 
					      D.drawSprite(data.sprite, gridArt.project(0.0), 0, {
 | 
				
			||||||
      xScale: 2.0,
 | 
					        xScale: 2.0,
 | 
				
			||||||
      yScale: 2.0,
 | 
					        yScale: 2.0,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBlock(): Block3D | null {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update() {}
 | 
					  update() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onClick(cell: CellView): boolean {
 | 
					  onClick(cell: CellView): boolean {
 | 
				
			||||||
@@ -307,15 +394,20 @@ export class ThrallPosterPickup {
 | 
				
			|||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  drawFloor() {}
 | 
					  draw(drawpile: DrawPile, gridArt: GridArt) {
 | 
				
			||||||
  drawInAir(gridArt: GridArt) {
 | 
					    drawpile.add(0, () => {
 | 
				
			||||||
    let data = getThralls().get(this.thrall);
 | 
					      let data = getThralls().get(this.thrall);
 | 
				
			||||||
    D.drawSprite(data.sprite, gridArt.project(0.0), 2, {
 | 
					      D.drawSprite(data.sprite, gridArt.project(0.0), 2, {
 | 
				
			||||||
      xScale: 2.0,
 | 
					        xScale: 2.0,
 | 
				
			||||||
      yScale: 2.0,
 | 
					        yScale: 2.0,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBlock(): Block3D | null {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update() {}
 | 
					  update() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onClick(cell: CellView): boolean {
 | 
					  onClick(cell: CellView): boolean {
 | 
				
			||||||
@@ -358,27 +450,32 @@ export class ThrallRecruitedPickup {
 | 
				
			|||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  drawFloor() {}
 | 
					  draw(drawpile: DrawPile, gridArt: GridArt) {
 | 
				
			||||||
  drawInAir(gridArt: GridArt) {
 | 
					    drawpile.add(0, () => {
 | 
				
			||||||
    let data = getThralls().get(this.thrall);
 | 
					      let data = getThralls().get(this.thrall);
 | 
				
			||||||
    let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall);
 | 
					      let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall);
 | 
				
			||||||
    let ix = 0;
 | 
					      let ix = 0;
 | 
				
			||||||
    let rot = 0;
 | 
					      let rot = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (lifeStage == LifeStage.Vampirized) {
 | 
					      if (lifeStage == LifeStage.Vampirized) {
 | 
				
			||||||
      ix = 1;
 | 
					        ix = 1;
 | 
				
			||||||
    }
 | 
					      }
 | 
				
			||||||
    if (lifeStage == LifeStage.Dead) {
 | 
					      if (lifeStage == LifeStage.Dead) {
 | 
				
			||||||
      ix = 1;
 | 
					        ix = 1;
 | 
				
			||||||
      rot = 270;
 | 
					        rot = 270;
 | 
				
			||||||
    }
 | 
					      }
 | 
				
			||||||
    D.drawSprite(data.sprite, gridArt.project(0.0), ix, {
 | 
					      D.drawSprite(data.sprite, gridArt.project(0.0), ix, {
 | 
				
			||||||
      xScale: 2.0,
 | 
					        xScale: 2.0,
 | 
				
			||||||
      yScale: 2.0,
 | 
					        yScale: 2.0,
 | 
				
			||||||
      angle: rot,
 | 
					        angle: rot,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBlock(): Block3D | null {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update() {}
 | 
					  update() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onClick(_cell: CellView): boolean {
 | 
					  onClick(_cell: CellView): boolean {
 | 
				
			||||||
@@ -477,28 +574,33 @@ export class ThrallCollectionPlatePickup {
 | 
				
			|||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  drawFloor() {}
 | 
					  draw(drawpile: DrawPile, gridArt: GridArt) {
 | 
				
			||||||
  drawInAir(gridArt: GridArt) {
 | 
					    drawpile.add(0, () => {
 | 
				
			||||||
    let itemStage = getPlayerProgress().getThrallItemStage(this.thrall);
 | 
					      let itemStage = getPlayerProgress().getThrallItemStage(this.thrall);
 | 
				
			||||||
    let data = getThralls().get(this.thrall);
 | 
					      let data = getThralls().get(this.thrall);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (itemStage != ItemStage.Delivered) {
 | 
					      if (itemStage != ItemStage.Delivered) {
 | 
				
			||||||
      D.drawRect(
 | 
					        D.drawRect(
 | 
				
			||||||
        gridArt.project(0).offset(new Point(-18, -18)),
 | 
					          gridArt.project(0).offset(new Point(-18, -18)),
 | 
				
			||||||
        new Size(36, 36),
 | 
					          new Size(36, 36),
 | 
				
			||||||
        FG_TEXT,
 | 
					          FG_TEXT,
 | 
				
			||||||
      );
 | 
					        );
 | 
				
			||||||
    } else {
 | 
					      } else {
 | 
				
			||||||
      D.drawSprite(data.sprite, gridArt.project(2), 3, {
 | 
					        D.drawSprite(data.sprite, gridArt.project(2), 3, {
 | 
				
			||||||
        xScale: 2.0,
 | 
					          xScale: 2.0,
 | 
				
			||||||
        yScale: 2.0,
 | 
					          yScale: 2.0,
 | 
				
			||||||
      });
 | 
					        });
 | 
				
			||||||
    }
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBlock(): Block3D | null {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update() {}
 | 
					  update() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onClick(_cell: CellView): boolean {
 | 
					  onClick(cell: CellView): boolean {
 | 
				
			||||||
    let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall);
 | 
					    let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall);
 | 
				
			||||||
    let itemStage = getPlayerProgress().getThrallItemStage(this.thrall);
 | 
					    let itemStage = getPlayerProgress().getThrallItemStage(this.thrall);
 | 
				
			||||||
    let data = getThralls().get(this.thrall);
 | 
					    let data = getThralls().get(this.thrall);
 | 
				
			||||||
@@ -548,7 +650,7 @@ export class ThrallCollectionPlatePickup {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
          null,
 | 
					          null,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        data.rewardCallback();
 | 
					        data.rewardCallback((what) => this.spawn(cell.xy, what));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -556,6 +658,29 @@ export class ThrallCollectionPlatePickup {
 | 
				
			|||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  spawn(xy: Point, what: Stat | "EXP") {
 | 
				
			||||||
 | 
					    let callbacks = null;
 | 
				
			||||||
 | 
					    if (what == "EXP") {
 | 
				
			||||||
 | 
					      callbacks = new ExperiencePickupCallbacks();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      callbacks = new StatPickupCallbacks(what);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (callbacks == null) { return; }
 | 
				
			||||||
 | 
					    let floater = new Floater(
 | 
				
			||||||
 | 
					      xy.offset(new Point(0.5, 0.5)),
 | 
				
			||||||
 | 
					      50,
 | 
				
			||||||
 | 
					      callbacks,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    let speed = 0.015;
 | 
				
			||||||
 | 
					    let direction = Math.random() * Math.PI * 2;
 | 
				
			||||||
 | 
					    floater.velocity = new Point(
 | 
				
			||||||
 | 
					      Math.cos(direction) * speed,
 | 
				
			||||||
 | 
					      Math.sin(direction) * speed * 0.1,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    floater.velZ = 0.8;
 | 
				
			||||||
 | 
					    getHuntMode().spawnFloater(floater);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onSqueeze() {}
 | 
					  onSqueeze() {}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -585,16 +710,21 @@ export class ThrallItemPickup {
 | 
				
			|||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  drawFloor() {}
 | 
					  draw(drawpile: DrawPile, gridArt: GridArt) {
 | 
				
			||||||
  drawInAir(gridArt: GridArt) {
 | 
					    drawpile.add(0, () => {
 | 
				
			||||||
    let data = getThralls().get(this.thrall);
 | 
					      let data = getThralls().get(this.thrall);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    D.drawSprite(data.sprite, gridArt.project(2), 3, {
 | 
					      D.drawSprite(data.sprite, gridArt.project(2), 3, {
 | 
				
			||||||
      xScale: 2.0,
 | 
					        xScale: 2.0,
 | 
				
			||||||
      yScale: 2.0,
 | 
					        yScale: 2.0,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBlock(): Block3D | null {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  update() {}
 | 
					  update() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onClick(cell: CellView): boolean {
 | 
					  onClick(cell: CellView): boolean {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import { Sprite } from "./engine/internal/sprite.ts";
 | 
					import { Sprite } from "./engine/internal/sprite.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import imgResourcePickup from "./art/pickups/resources.png";
 | 
					import imgCollectibles from "./art/pickups/collectibles.png";
 | 
				
			||||||
import imgStatPickup from "./art/pickups/stats.png";
 | 
					import imgCollectiblesSilhouettes from "./art/pickups/collectibles_silhouettes.png";
 | 
				
			||||||
import imgLadder from "./art/pickups/ladder.png";
 | 
					import imgLadder from "./art/pickups/ladder.png";
 | 
				
			||||||
import imgLock from "./art/pickups/lock.png";
 | 
					import imgLock from "./art/pickups/lock.png";
 | 
				
			||||||
import { Point, Size } from "./engine/datatypes.ts";
 | 
					import { Point, Size } from "./engine/datatypes.ts";
 | 
				
			||||||
@@ -13,20 +13,20 @@ import imgThrallParty from "./art/thralls/thrall_party.png";
 | 
				
			|||||||
import imgThrallStare from "./art/thralls/thrall_stare.png";
 | 
					import imgThrallStare from "./art/thralls/thrall_stare.png";
 | 
				
			||||||
import imgThrallStealth from "./art/thralls/thrall_stealth.png";
 | 
					import imgThrallStealth from "./art/thralls/thrall_stealth.png";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export let sprResourcePickup = new Sprite(
 | 
					export let sprCollectibles = new Sprite(
 | 
				
			||||||
  imgResourcePickup,
 | 
					  imgCollectibles,
 | 
				
			||||||
  new Size(32, 32),
 | 
					  new Size(32, 32),
 | 
				
			||||||
  new Point(16, 16),
 | 
					  new Point(16, 16),
 | 
				
			||||||
  new Size(1, 1),
 | 
					  new Size(5, 1),
 | 
				
			||||||
  1,
 | 
					  5,
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export let sprStatPickup = new Sprite(
 | 
					export let sprCollectiblesSilhouettes = new Sprite(
 | 
				
			||||||
  imgStatPickup,
 | 
					  imgCollectiblesSilhouettes,
 | 
				
			||||||
  new Size(32, 32),
 | 
					  new Size(32, 32),
 | 
				
			||||||
  new Point(16, 16),
 | 
					  new Point(16, 16),
 | 
				
			||||||
  new Size(4, 1),
 | 
					  new Size(5, 1),
 | 
				
			||||||
  4,
 | 
					  5,
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export let sprLadder = new Sprite(
 | 
					export let sprLadder = new Sprite(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ import {
 | 
				
			|||||||
  sprThrallStealth,
 | 
					  sprThrallStealth,
 | 
				
			||||||
} from "./sprites.ts";
 | 
					} from "./sprites.ts";
 | 
				
			||||||
import { Sprite } from "./engine/internal/sprite.ts";
 | 
					import { Sprite } from "./engine/internal/sprite.ts";
 | 
				
			||||||
import { getPlayerProgress } from "./playerprogress.ts";
 | 
					import {Stat} from "./datatypes.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Thrall = {
 | 
					export type Thrall = {
 | 
				
			||||||
  id: number;
 | 
					  id: number;
 | 
				
			||||||
@@ -62,7 +62,7 @@ export type ThrallData = {
 | 
				
			|||||||
  itemPickupMessage: string;
 | 
					  itemPickupMessage: string;
 | 
				
			||||||
  deliveryMessage: string;
 | 
					  deliveryMessage: string;
 | 
				
			||||||
  rewardMessage: string;
 | 
					  rewardMessage: string;
 | 
				
			||||||
  rewardCallback: () => void;
 | 
					  rewardCallback: (spawn: (what: Stat | "EXP") => void) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  lifeStageText: Record<LifeStage, LifeStageText>;
 | 
					  lifeStageText: Record<LifeStage, LifeStageText>;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -132,8 +132,8 @@ export let thrallParty = table.add({
 | 
				
			|||||||
  deliveryMessage:
 | 
					  deliveryMessage:
 | 
				
			||||||
    '"Oh, that? Yeah, I won it." And then lost it, apparently.\n\nHe\'s elated. He will never leave.',
 | 
					    '"Oh, that? Yeah, I won it." And then lost it, apparently.\n\nHe\'s elated. He will never leave.',
 | 
				
			||||||
  rewardMessage: "Garrett showers you with INT!",
 | 
					  rewardMessage: "Garrett showers you with INT!",
 | 
				
			||||||
  rewardCallback: () => {
 | 
					  rewardCallback: (spawn) => {
 | 
				
			||||||
    getPlayerProgress().add("INT", 10);
 | 
					    for (let i = 0; i < 30; i++) { spawn("INT"); }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  lifeStageText: {
 | 
					  lifeStageText: {
 | 
				
			||||||
    fresh: {
 | 
					    fresh: {
 | 
				
			||||||
@@ -204,8 +204,8 @@ export let thrallLore = table.add({
 | 
				
			|||||||
  deliveryMessage:
 | 
					  deliveryMessage:
 | 
				
			||||||
    "Lupin looks at his own reflection -- with interest, confusion, dismissal, and then deep satisfaction. He loves it. He will never leave.",
 | 
					    "Lupin looks at his own reflection -- with interest, confusion, dismissal, and then deep satisfaction. He loves it. He will never leave.",
 | 
				
			||||||
  rewardMessage: "Lupin showers you with AGI!",
 | 
					  rewardMessage: "Lupin showers you with AGI!",
 | 
				
			||||||
  rewardCallback: () => {
 | 
					  rewardCallback: (spawn) => {
 | 
				
			||||||
    getPlayerProgress().add("AGI", 10);
 | 
					    for (let i = 0; i < 30; i++) { spawn("AGI"); }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  lifeStageText: {
 | 
					  lifeStageText: {
 | 
				
			||||||
    fresh: {
 | 
					    fresh: {
 | 
				
			||||||
@@ -273,9 +273,9 @@ export let thrallBat = table.add({
 | 
				
			|||||||
  deliveryMessage:
 | 
					  deliveryMessage:
 | 
				
			||||||
    'Monica salivates. "This is... this is... simply exquisite!"\n\nShe is happy. She will never leave.',
 | 
					    'Monica salivates. "This is... this is... simply exquisite!"\n\nShe is happy. She will never leave.',
 | 
				
			||||||
  rewardMessage: "Monica showers you with CHA and INT!",
 | 
					  rewardMessage: "Monica showers you with CHA and INT!",
 | 
				
			||||||
  rewardCallback: () => {
 | 
					  rewardCallback: (spawn) => {
 | 
				
			||||||
    getPlayerProgress().add("CHA", 5);
 | 
					    for (let i = 0; i < 15; i++) { spawn("CHA"); }
 | 
				
			||||||
    getPlayerProgress().add("INT", 5);
 | 
					    for (let i = 0; i < 15; i++) { spawn("INT"); }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  lifeStageText: {
 | 
					  lifeStageText: {
 | 
				
			||||||
    fresh: {
 | 
					    fresh: {
 | 
				
			||||||
@@ -343,8 +343,8 @@ export let thrallCharm = table.add({
 | 
				
			|||||||
  deliveryMessage:
 | 
					  deliveryMessage:
 | 
				
			||||||
    "Renfield inhales sharply and widens his stance, trying to hide his physical reaction to your face. He is elated and will never leave.",
 | 
					    "Renfield inhales sharply and widens his stance, trying to hide his physical reaction to your face. He is elated and will never leave.",
 | 
				
			||||||
  rewardMessage: "Renfield showers you with PSI!",
 | 
					  rewardMessage: "Renfield showers you with PSI!",
 | 
				
			||||||
  rewardCallback: () => {
 | 
					  rewardCallback: (spawn) => {
 | 
				
			||||||
    getPlayerProgress().add("PSI", 10);
 | 
					    for (let i = 0; i < 24; i++) { spawn("PSI"); }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  lifeStageText: {
 | 
					  lifeStageText: {
 | 
				
			||||||
    fresh: {
 | 
					    fresh: {
 | 
				
			||||||
@@ -410,9 +410,9 @@ export let thrallStealth = table.add({
 | 
				
			|||||||
  deliveryMessage:
 | 
					  deliveryMessage:
 | 
				
			||||||
    "\"That? That's not mine.\" But she wants it. Now it's hers. She will never leave.",
 | 
					    "\"That? That's not mine.\" But she wants it. Now it's hers. She will never leave.",
 | 
				
			||||||
  rewardMessage: "Narthyss showers you with CHA and AGI!",
 | 
					  rewardMessage: "Narthyss showers you with CHA and AGI!",
 | 
				
			||||||
  rewardCallback: () => {
 | 
					  rewardCallback: (spawn) => {
 | 
				
			||||||
    getPlayerProgress().add("CHA", 5);
 | 
					    for (let i = 0; i < 15; i++) { spawn("CHA"); }
 | 
				
			||||||
    getPlayerProgress().add("AGI", 5);
 | 
					    for (let i = 0; i < 15; i++) { spawn("AGI"); }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  lifeStageText: {
 | 
					  lifeStageText: {
 | 
				
			||||||
    fresh: {
 | 
					    fresh: {
 | 
				
			||||||
@@ -479,8 +479,8 @@ export let thrallStare = table.add({
 | 
				
			|||||||
  deliveryMessage:
 | 
					  deliveryMessage:
 | 
				
			||||||
    "Ridley admires the gear but -- to your surprise -- refuses to jam it into its brain.\n\nThe pup is elated and will never leave.",
 | 
					    "Ridley admires the gear but -- to your surprise -- refuses to jam it into its brain.\n\nThe pup is elated and will never leave.",
 | 
				
			||||||
  rewardMessage: "Ridley showers you with EXP!",
 | 
					  rewardMessage: "Ridley showers you with EXP!",
 | 
				
			||||||
  rewardCallback: () => {
 | 
					  rewardCallback: (spawn) => {
 | 
				
			||||||
    getPlayerProgress().addExperience(100);
 | 
					    for (let i = 0; i < 6; i++) { spawn("EXP"); }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  lifeStageText: {
 | 
					  lifeStageText: {
 | 
				
			||||||
    fresh: {
 | 
					    fresh: {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										140
									
								
								src/world3d.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/world3d.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,140 @@
 | 
				
			|||||||
 | 
					import { Color, Grid, Point, Rect, Size } from "./engine/datatypes.ts";
 | 
				
			||||||
 | 
					import { DrawPile } from "./drawpile.ts";
 | 
				
			||||||
 | 
					import { GridArt } from "./gridart.ts";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  BG_CEILING,
 | 
				
			||||||
 | 
					  BG_WALL_OR_UNREVEALED,
 | 
				
			||||||
 | 
					  FG_BOLD,
 | 
				
			||||||
 | 
					  FG_TEXT,
 | 
				
			||||||
 | 
					} from "./colors.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class World3D {
 | 
				
			||||||
 | 
					  #grid: Grid<Element3D>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(size: Size) {
 | 
				
			||||||
 | 
					    this.#grid = new Grid<Element3D>(size, () => null);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set(at: Point, value: Element3D) {
 | 
				
			||||||
 | 
					    this.#grid.set(at, value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  drawCell(drawpile: DrawPile, gridArt: GridArt, xy: Point) {
 | 
				
			||||||
 | 
					    const OFFSET_AIR = 0;
 | 
				
			||||||
 | 
					    const OFFSET_TOP = 256;
 | 
				
			||||||
 | 
					    const OFFSET_TOP_OF_TOP = 512;
 | 
				
			||||||
 | 
					    let here = this.#grid.get(xy);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (here == null) {
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_TOP, () => {
 | 
				
			||||||
 | 
					        gridArt.drawCeiling(BG_CEILING);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getRevealedBlock = (dx: number, dy: number): Block3D | null => {
 | 
				
			||||||
 | 
					      let xy2 = xy.offset(new Point(dx, dy));
 | 
				
			||||||
 | 
					      if (!new Rect(new Point(0, 0), this.#grid.size).contains(xy2)) {
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let other = this.#grid.get(xy.offset(new Point(dx, dy)));
 | 
				
			||||||
 | 
					      if (other instanceof Block3D) {
 | 
				
			||||||
 | 
					        return other;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let center = getRevealedBlock(0, 0);
 | 
				
			||||||
 | 
					    if (center) {
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_TOP, () => {
 | 
				
			||||||
 | 
					        gridArt.drawCeiling(center.ceiling);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let west = getRevealedBlock(-1, 0);
 | 
				
			||||||
 | 
					    let east = getRevealedBlock(1, 0);
 | 
				
			||||||
 | 
					    let north = getRevealedBlock(0, -1);
 | 
				
			||||||
 | 
					    let south = getRevealedBlock(0, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (north && west) {
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
				
			||||||
 | 
					        gridArt.drawMouldingTopLeft(north.moulding);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (north) {
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_AIR, () => {
 | 
				
			||||||
 | 
					        gridArt.drawWallTop(north.dark);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
				
			||||||
 | 
					        gridArt.drawMouldingTop(north.moulding);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (north && east) {
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
				
			||||||
 | 
					        gridArt.drawMouldingTopRight(north.moulding);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (west) {
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_AIR, () => {
 | 
				
			||||||
 | 
					        gridArt.drawWallLeft(west.dark);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
				
			||||||
 | 
					        gridArt.drawMouldingLeft(west.moulding);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (south && west) {
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
				
			||||||
 | 
					        gridArt.drawMouldingBottomLeft(south.moulding);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (south) {
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_AIR, () => {
 | 
				
			||||||
 | 
					        gridArt.drawWallBottom(south.bright);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
				
			||||||
 | 
					        gridArt.drawMouldingBottom(south.moulding);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (south && east) {
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
				
			||||||
 | 
					        gridArt.drawMouldingBottomRight(south.moulding);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (east) {
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_AIR, () => {
 | 
				
			||||||
 | 
					        gridArt.drawWallRight(east.bright);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      drawpile.add(OFFSET_TOP_OF_TOP, () => {
 | 
				
			||||||
 | 
					        gridArt.drawMouldingRight(east.moulding);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Element3D = Floor3D | Block3D | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Floor3D {
 | 
				
			||||||
 | 
					  constructor() {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Block3D {
 | 
				
			||||||
 | 
					  readonly bright: Color;
 | 
				
			||||||
 | 
					  readonly dark: Color;
 | 
				
			||||||
 | 
					  readonly ceiling: Color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get moulding(): Color {
 | 
				
			||||||
 | 
					    return this.dark;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(bright: Color, dark: Color, ceiling: Color) {
 | 
				
			||||||
 | 
					    this.bright = bright;
 | 
				
			||||||
 | 
					    this.dark = dark;
 | 
				
			||||||
 | 
					    this.ceiling = ceiling;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static standardWall(): Block3D {
 | 
				
			||||||
 | 
					    return new Block3D(FG_BOLD, FG_TEXT, BG_WALL_OR_UNREVEALED);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user