Ceremonial PR: fix map gen #39
							
								
								
									
										
											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 { Stat } from "./datatypes.ts"; | ||||
|  | ||||
| export const BG_OUTER = 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 BG_CEILING = Color.parseHexCode("#143464"); | ||||
| 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 { | ||||
|   BG_INSET, | ||||
|   BG_WALL_OR_UNREVEALED, | ||||
|   FG_BOLD, | ||||
|   FG_MOULDING, | ||||
|   FG_TEXT, | ||||
|   FG_TEXT_ENDORSED, | ||||
|   FG_TOO_EXPENSIVE, | ||||
| @@ -18,9 +15,13 @@ 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"; | ||||
| import { Floater } from "./floater.ts"; | ||||
| import { displace } from "./physics.ts"; | ||||
|  | ||||
| export class HuntMode { | ||||
|   map: LoadedNewMap; | ||||
|   floaters: Floater[]; | ||||
|   floatingPlayer: Point; | ||||
|   velocity: Point; | ||||
|   faceLeft: boolean; | ||||
| @@ -31,6 +32,7 @@ export class HuntMode { | ||||
|  | ||||
|   constructor(depth: number, map: LoadedNewMap) { | ||||
|     this.map = map; | ||||
|     this.floaters = []; | ||||
|     this.floatingPlayer = map.entrance.offset(new Point(0.5, 0.5)); | ||||
|     this.velocity = new Point(0, 0); | ||||
|     this.faceLeft = false; | ||||
| @@ -111,16 +113,26 @@ export class HuntMode { | ||||
|  | ||||
|     this.#updatePlayer(); | ||||
|     this.#updateFov(); | ||||
|     this.#updateFloaters(); | ||||
|     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, new Point(x, y)); | ||||
|         this.#drawMapCell(offsetInPixels, world3d, new Point(x, y)); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     this.#drawFloaters(globalOffset); | ||||
|     this.#drawPlayer(globalOffset); | ||||
|  | ||||
|     this.#drawBadges(globalOffset); | ||||
| @@ -128,6 +140,10 @@ export class HuntMode { | ||||
|     this.drawpile.executeOnClick(); | ||||
|   } | ||||
|  | ||||
|   spawnFloater(floater: Floater) { | ||||
|     this.floaters.push(floater); | ||||
|   } | ||||
|  | ||||
|   #updatePlayer() { | ||||
|     let dx = this.velocity.x; | ||||
|     let dy = this.velocity.y; | ||||
| @@ -180,9 +196,8 @@ export class HuntMode { | ||||
|       this.faceLeft = false; | ||||
|     } | ||||
|  | ||||
|     let nSteps = 40; | ||||
|     let szX = 0.75; | ||||
|     let szY = 0.75; | ||||
|     let szX = 0.5; | ||||
|     let szY = 0.5; | ||||
|  | ||||
|     this.velocity = new Point(dx, dy); | ||||
|  | ||||
| @@ -190,7 +205,7 @@ export class HuntMode { | ||||
|     for (let offset of CARDINAL_DIRECTIONS.values()) { | ||||
|       let bigBbox = new Rect( | ||||
|         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)), | ||||
|         new Size(szX, szY), | ||||
|       ); | ||||
| @@ -209,42 +224,17 @@ export class HuntMode { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     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); | ||||
|     let origin = new Point(szX / 2, szY / 2); | ||||
|     let bbox = new Rect( | ||||
|       this.floatingPlayer.offset(origin.negate()), | ||||
|       new Size(szX, szY), | ||||
|     ); | ||||
|     let { displacement, dxy } = displace(bbox, this.velocity, (b: Rect) => | ||||
|       this.isBlocked(b), | ||||
|     ); | ||||
|     this.floatingPlayer = this.floatingPlayer.offset(displacement); | ||||
|     this.velocity = dxy; | ||||
|     getPlayerProgress().spendBlood(displacement.distance(new Point(0, 0)) * 10); | ||||
|   } | ||||
|  | ||||
|   #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() { | ||||
|     for (let y = 0; y < this.map.size.h; y++) { | ||||
|       for (let x = 0; x < this.map.size.w; x++) { | ||||
| @@ -293,26 +295,14 @@ export class HuntMode { | ||||
|     this.drawpile.draw(); | ||||
|   } | ||||
|  | ||||
|   #drawMapCell(offsetInPixels: Point, mapPosition: Point) { | ||||
|     const OFFSET_UNDER_FLOOR = -512 + mapPosition.y; | ||||
|   #drawMapCell(offsetInPixels: Point, world3d: World3D, mapPosition: Point) { | ||||
|     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); | ||||
|  | ||||
|     let cellData = this.map.get(mapPosition); | ||||
|     world3d.drawCell(this.drawpile, gridArt, mapPosition); | ||||
|  | ||||
|     this.drawpile.add(OFFSET_UNDER_FLOOR, () => { | ||||
|       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; | ||||
|     } | ||||
|     let cellData = this.map.get(mapPosition); | ||||
|  | ||||
|     if (!cellData.revealed) { | ||||
|       return; | ||||
| @@ -343,7 +333,6 @@ export class HuntMode { | ||||
|         } | ||||
|  | ||||
|         gridArt.drawFloor(color); | ||||
|         pickup?.drawFloor(gridArt); | ||||
|       }, | ||||
|       gridArt.floorRect, | ||||
|       true, | ||||
| @@ -369,70 +358,38 @@ export class HuntMode { | ||||
|     ); | ||||
|  | ||||
|     if (pickup != null) { | ||||
|       this.drawpile.add(OFFSET_AIR, () => { | ||||
|         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); | ||||
|       }); | ||||
|       pickup.draw(this.drawpile, gridArt); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   #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()); | ||||
|     this.drawpile.add(1024, () => { | ||||
|       D.drawSprite(sprThrallLore, cellOffset, 1, { | ||||
| @@ -440,6 +397,13 @@ export class HuntMode { | ||||
|         yScale: 2, | ||||
|       }); | ||||
|     }); | ||||
|      */ | ||||
|     this.drawpile.add(1024, () => { | ||||
|       D.drawSprite(sprThrallLore, new Point(192, 192), 1, { | ||||
|         xScale: this.faceLeft ? -2 : 2, | ||||
|         yScale: 2, | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   #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) { | ||||
|     let cell = this.map.get(xy); | ||||
|     if (cell.architecture == Architecture.Wall) { | ||||
|   | ||||
| @@ -167,6 +167,10 @@ export class CellView { | ||||
|     this.#point = point; | ||||
|   } | ||||
|  | ||||
|   get xy(): Point { | ||||
|     return this.#point; | ||||
|   } | ||||
|  | ||||
|   set architecture(value: Architecture) { | ||||
|     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 { D } from "./engine/public.ts"; | ||||
| import { | ||||
|   sprCollectibles, | ||||
|   sprCollectiblesSilhouettes, | ||||
|   sprLadder, | ||||
|   sprLock, | ||||
|   sprResourcePickup, | ||||
|   sprStatPickup, | ||||
| } from "./sprites.ts"; | ||||
| import { GridArt } from "./gridart.ts"; | ||||
| import { getCheckModal } from "./checkmodal.ts"; | ||||
| import { Point, Size } from "./engine/datatypes.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 = | ||||
|   | LockPickup | ||||
| @@ -52,14 +55,19 @@ export class LockPickup { | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     for (let z = 0; z < 5; z += 0.25) { | ||||
|       D.drawSprite(sprLock, gridArt.project(z), 0, { | ||||
|         xScale: 2.0, | ||||
|         yScale: 2.0, | ||||
|       }); | ||||
|     } | ||||
|   draw(drawpile: DrawPile, gridArt: GridArt) { | ||||
|     drawpile.add(0, () => { | ||||
|       for (let z = 0; z < 5; z += 0.25) { | ||||
|         D.drawSprite(sprLock, gridArt.project(z), 0, { | ||||
|           xScale: 2.0, | ||||
|           yScale: 2.0, | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   getBlock(): Block3D | null { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   update() {} | ||||
| @@ -72,12 +80,16 @@ export class LockPickup { | ||||
|   onSqueeze() {} | ||||
| } | ||||
|  | ||||
| export type BreakableBlockPickupCallbacks = | ||||
|   | StatPickupCallbacks | ||||
|   | ExperiencePickupCallbacks; | ||||
|  | ||||
| const RECOVERY_PER_TICK: number = 0.1; | ||||
| export class BreakableBlockPickup { | ||||
|   callbacks: StatPickupCallbacks | ExperiencePickupCallbacks; | ||||
|   callbacks: BreakableBlockPickupCallbacks; | ||||
|   breakProgress: number; | ||||
|  | ||||
|   constructor(callbacks: StatPickupCallbacks | ExperiencePickupCallbacks) { | ||||
|   constructor(callbacks: BreakableBlockPickupCallbacks) { | ||||
|     this.callbacks = callbacks; | ||||
|     this.breakProgress = 0.0; | ||||
|   } | ||||
| @@ -102,28 +114,53 @@ export class BreakableBlockPickup { | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     let progress = Math.pow(this.breakProgress, 2.15); | ||||
|     let extraMult = 1.0; | ||||
|     let angleRange = 0; | ||||
|     if (progress != 0) { | ||||
|       extraMult = 1.2; | ||||
|       angleRange = 10; | ||||
|     } | ||||
|   get #adjustedProgress(): number { | ||||
|     return Math.pow(this.breakProgress, 2.15); | ||||
|   } | ||||
|  | ||||
|     this.callbacks.draw(gridArt.project(5), { | ||||
|       xScale: 2 * (1.0 - progress * 0.7) * extraMult, | ||||
|       yScale: 2 * (1.0 - progress * 0.7) * extraMult, | ||||
|       angle: (2 * progress - 1) * angleRange, | ||||
|   draw(drawpile: DrawPile, gridArt: GridArt) { | ||||
|     drawpile.add(384, () => { | ||||
|       let progress = this.#adjustedProgress; | ||||
|       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) { | ||||
|     if (this.breakProgress >= 1.0) { | ||||
|       getPlayerProgress().spendBlood(this.callbacks.cost); | ||||
|       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); | ||||
| @@ -157,8 +194,16 @@ export class StatPickupCallbacks { | ||||
|     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( | ||||
|     at: Point, | ||||
|     gridArt: GridArt, | ||||
|     options: { xScale?: number; yScale?: number; angle?: number }, | ||||
|   ) { | ||||
|     let statIndex = ALL_STATS.indexOf(this.#stat); | ||||
| @@ -166,7 +211,21 @@ export class StatPickupCallbacks { | ||||
|       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(); | ||||
|   } | ||||
|  | ||||
|   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( | ||||
|     at: Point, | ||||
|     gridArt: GridArt, | ||||
|     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; | ||||
|   } | ||||
|  | ||||
|   drawFloor(gridArt: GridArt) { | ||||
|     D.drawSprite(sprLadder, gridArt.project(0.0), 0, { | ||||
|       xScale: 2.0, | ||||
|       yScale: 2.0, | ||||
|   draw(drawpile: DrawPile, gridArt: GridArt) { | ||||
|     drawpile.add(-128, () => { | ||||
|       D.drawSprite(sprLadder, gridArt.project(0.0), 0, { | ||||
|         xScale: 2.0, | ||||
|         yScale: 2.0, | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|   drawInAir() {} | ||||
|  | ||||
|   getBlock(): Block3D | null { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   update() {} | ||||
|  | ||||
| @@ -257,15 +339,20 @@ export class ThrallPickup { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     let data = getThralls().get(this.thrall); | ||||
|     D.drawSprite(data.sprite, gridArt.project(0.0), 0, { | ||||
|       xScale: 2.0, | ||||
|       yScale: 2.0, | ||||
|   draw(drawpile: DrawPile, gridArt: GridArt) { | ||||
|     drawpile.add(0, () => { | ||||
|       let data = getThralls().get(this.thrall); | ||||
|       D.drawSprite(data.sprite, gridArt.project(0.0), 0, { | ||||
|         xScale: 2.0, | ||||
|         yScale: 2.0, | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   getBlock(): Block3D | null { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   update() {} | ||||
|  | ||||
|   onClick(cell: CellView): boolean { | ||||
| @@ -307,15 +394,20 @@ export class ThrallPosterPickup { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     let data = getThralls().get(this.thrall); | ||||
|     D.drawSprite(data.sprite, gridArt.project(0.0), 2, { | ||||
|       xScale: 2.0, | ||||
|       yScale: 2.0, | ||||
|   draw(drawpile: DrawPile, gridArt: GridArt) { | ||||
|     drawpile.add(0, () => { | ||||
|       let data = getThralls().get(this.thrall); | ||||
|       D.drawSprite(data.sprite, gridArt.project(0.0), 2, { | ||||
|         xScale: 2.0, | ||||
|         yScale: 2.0, | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   getBlock(): Block3D | null { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   update() {} | ||||
|  | ||||
|   onClick(cell: CellView): boolean { | ||||
| @@ -358,27 +450,32 @@ export class ThrallRecruitedPickup { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     let data = getThralls().get(this.thrall); | ||||
|     let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); | ||||
|     let ix = 0; | ||||
|     let rot = 0; | ||||
|   draw(drawpile: DrawPile, gridArt: GridArt) { | ||||
|     drawpile.add(0, () => { | ||||
|       let data = getThralls().get(this.thrall); | ||||
|       let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); | ||||
|       let ix = 0; | ||||
|       let rot = 0; | ||||
|  | ||||
|     if (lifeStage == LifeStage.Vampirized) { | ||||
|       ix = 1; | ||||
|     } | ||||
|     if (lifeStage == LifeStage.Dead) { | ||||
|       ix = 1; | ||||
|       rot = 270; | ||||
|     } | ||||
|     D.drawSprite(data.sprite, gridArt.project(0.0), ix, { | ||||
|       xScale: 2.0, | ||||
|       yScale: 2.0, | ||||
|       angle: rot, | ||||
|       if (lifeStage == LifeStage.Vampirized) { | ||||
|         ix = 1; | ||||
|       } | ||||
|       if (lifeStage == LifeStage.Dead) { | ||||
|         ix = 1; | ||||
|         rot = 270; | ||||
|       } | ||||
|       D.drawSprite(data.sprite, gridArt.project(0.0), ix, { | ||||
|         xScale: 2.0, | ||||
|         yScale: 2.0, | ||||
|         angle: rot, | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   getBlock(): Block3D | null { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   update() {} | ||||
|  | ||||
|   onClick(_cell: CellView): boolean { | ||||
| @@ -477,28 +574,33 @@ export class ThrallCollectionPlatePickup { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     let itemStage = getPlayerProgress().getThrallItemStage(this.thrall); | ||||
|     let data = getThralls().get(this.thrall); | ||||
|   draw(drawpile: DrawPile, gridArt: GridArt) { | ||||
|     drawpile.add(0, () => { | ||||
|       let itemStage = getPlayerProgress().getThrallItemStage(this.thrall); | ||||
|       let data = getThralls().get(this.thrall); | ||||
|  | ||||
|     if (itemStage != ItemStage.Delivered) { | ||||
|       D.drawRect( | ||||
|         gridArt.project(0).offset(new Point(-18, -18)), | ||||
|         new Size(36, 36), | ||||
|         FG_TEXT, | ||||
|       ); | ||||
|     } else { | ||||
|       D.drawSprite(data.sprite, gridArt.project(2), 3, { | ||||
|         xScale: 2.0, | ||||
|         yScale: 2.0, | ||||
|       }); | ||||
|     } | ||||
|       if (itemStage != ItemStage.Delivered) { | ||||
|         D.drawRect( | ||||
|           gridArt.project(0).offset(new Point(-18, -18)), | ||||
|           new Size(36, 36), | ||||
|           FG_TEXT, | ||||
|         ); | ||||
|       } else { | ||||
|         D.drawSprite(data.sprite, gridArt.project(2), 3, { | ||||
|           xScale: 2.0, | ||||
|           yScale: 2.0, | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   getBlock(): Block3D | null { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   update() {} | ||||
|  | ||||
|   onClick(_cell: CellView): boolean { | ||||
|   onClick(cell: CellView): boolean { | ||||
|     let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); | ||||
|     let itemStage = getPlayerProgress().getThrallItemStage(this.thrall); | ||||
|     let data = getThralls().get(this.thrall); | ||||
| @@ -548,7 +650,7 @@ export class ThrallCollectionPlatePickup { | ||||
|           }, | ||||
|           null, | ||||
|         ); | ||||
|         data.rewardCallback(); | ||||
|         data.rewardCallback((what) => this.spawn(cell.xy, what)); | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -556,6 +658,29 @@ export class ThrallCollectionPlatePickup { | ||||
|     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() {} | ||||
| } | ||||
|  | ||||
| @@ -585,16 +710,21 @@ export class ThrallItemPickup { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     let data = getThralls().get(this.thrall); | ||||
|   draw(drawpile: DrawPile, gridArt: GridArt) { | ||||
|     drawpile.add(0, () => { | ||||
|       let data = getThralls().get(this.thrall); | ||||
|  | ||||
|     D.drawSprite(data.sprite, gridArt.project(2), 3, { | ||||
|       xScale: 2.0, | ||||
|       yScale: 2.0, | ||||
|       D.drawSprite(data.sprite, gridArt.project(2), 3, { | ||||
|         xScale: 2.0, | ||||
|         yScale: 2.0, | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   getBlock(): Block3D | null { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   update() {} | ||||
|  | ||||
|   onClick(cell: CellView): boolean { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { Sprite } from "./engine/internal/sprite.ts"; | ||||
|  | ||||
| import imgResourcePickup from "./art/pickups/resources.png"; | ||||
| import imgStatPickup from "./art/pickups/stats.png"; | ||||
| import imgCollectibles from "./art/pickups/collectibles.png"; | ||||
| import imgCollectiblesSilhouettes from "./art/pickups/collectibles_silhouettes.png"; | ||||
| import imgLadder from "./art/pickups/ladder.png"; | ||||
| import imgLock from "./art/pickups/lock.png"; | ||||
| 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 imgThrallStealth from "./art/thralls/thrall_stealth.png"; | ||||
|  | ||||
| export let sprResourcePickup = new Sprite( | ||||
|   imgResourcePickup, | ||||
| export let sprCollectibles = new Sprite( | ||||
|   imgCollectibles, | ||||
|   new Size(32, 32), | ||||
|   new Point(16, 16), | ||||
|   new Size(1, 1), | ||||
|   1, | ||||
|   new Size(5, 1), | ||||
|   5, | ||||
| ); | ||||
|  | ||||
| export let sprStatPickup = new Sprite( | ||||
|   imgStatPickup, | ||||
| export let sprCollectiblesSilhouettes = new Sprite( | ||||
|   imgCollectiblesSilhouettes, | ||||
|   new Size(32, 32), | ||||
|   new Point(16, 16), | ||||
|   new Size(4, 1), | ||||
|   4, | ||||
|   new Size(5, 1), | ||||
|   5, | ||||
| ); | ||||
|  | ||||
| export let sprLadder = new Sprite( | ||||
|   | ||||
| @@ -22,7 +22,7 @@ import { | ||||
|   sprThrallStealth, | ||||
| } from "./sprites.ts"; | ||||
| import { Sprite } from "./engine/internal/sprite.ts"; | ||||
| import { getPlayerProgress } from "./playerprogress.ts"; | ||||
| import {Stat} from "./datatypes.ts"; | ||||
|  | ||||
| export type Thrall = { | ||||
|   id: number; | ||||
| @@ -62,7 +62,7 @@ export type ThrallData = { | ||||
|   itemPickupMessage: string; | ||||
|   deliveryMessage: string; | ||||
|   rewardMessage: string; | ||||
|   rewardCallback: () => void; | ||||
|   rewardCallback: (spawn: (what: Stat | "EXP") => void) => void; | ||||
|  | ||||
|   lifeStageText: Record<LifeStage, LifeStageText>; | ||||
| }; | ||||
| @@ -132,8 +132,8 @@ export let thrallParty = table.add({ | ||||
|   deliveryMessage: | ||||
|     '"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!", | ||||
|   rewardCallback: () => { | ||||
|     getPlayerProgress().add("INT", 10); | ||||
|   rewardCallback: (spawn) => { | ||||
|     for (let i = 0; i < 30; i++) { spawn("INT"); } | ||||
|   }, | ||||
|   lifeStageText: { | ||||
|     fresh: { | ||||
| @@ -204,8 +204,8 @@ export let thrallLore = table.add({ | ||||
|   deliveryMessage: | ||||
|     "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!", | ||||
|   rewardCallback: () => { | ||||
|     getPlayerProgress().add("AGI", 10); | ||||
|   rewardCallback: (spawn) => { | ||||
|     for (let i = 0; i < 30; i++) { spawn("AGI"); } | ||||
|   }, | ||||
|   lifeStageText: { | ||||
|     fresh: { | ||||
| @@ -273,9 +273,9 @@ export let thrallBat = table.add({ | ||||
|   deliveryMessage: | ||||
|     'Monica salivates. "This is... this is... simply exquisite!"\n\nShe is happy. She will never leave.', | ||||
|   rewardMessage: "Monica showers you with CHA and INT!", | ||||
|   rewardCallback: () => { | ||||
|     getPlayerProgress().add("CHA", 5); | ||||
|     getPlayerProgress().add("INT", 5); | ||||
|   rewardCallback: (spawn) => { | ||||
|     for (let i = 0; i < 15; i++) { spawn("CHA"); } | ||||
|     for (let i = 0; i < 15; i++) { spawn("INT"); } | ||||
|   }, | ||||
|   lifeStageText: { | ||||
|     fresh: { | ||||
| @@ -343,8 +343,8 @@ export let thrallCharm = table.add({ | ||||
|   deliveryMessage: | ||||
|     "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!", | ||||
|   rewardCallback: () => { | ||||
|     getPlayerProgress().add("PSI", 10); | ||||
|   rewardCallback: (spawn) => { | ||||
|     for (let i = 0; i < 24; i++) { spawn("PSI"); } | ||||
|   }, | ||||
|   lifeStageText: { | ||||
|     fresh: { | ||||
| @@ -410,9 +410,9 @@ export let thrallStealth = table.add({ | ||||
|   deliveryMessage: | ||||
|     "\"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!", | ||||
|   rewardCallback: () => { | ||||
|     getPlayerProgress().add("CHA", 5); | ||||
|     getPlayerProgress().add("AGI", 5); | ||||
|   rewardCallback: (spawn) => { | ||||
|     for (let i = 0; i < 15; i++) { spawn("CHA"); } | ||||
|     for (let i = 0; i < 15; i++) { spawn("AGI"); } | ||||
|   }, | ||||
|   lifeStageText: { | ||||
|     fresh: { | ||||
| @@ -479,8 +479,8 @@ export let thrallStare = table.add({ | ||||
|   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.", | ||||
|   rewardMessage: "Ridley showers you with EXP!", | ||||
|   rewardCallback: () => { | ||||
|     getPlayerProgress().addExperience(100); | ||||
|   rewardCallback: (spawn) => { | ||||
|     for (let i = 0; i < 6; i++) { spawn("EXP"); } | ||||
|   }, | ||||
|   lifeStageText: { | ||||
|     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