Flashier loot animations
This commit is contained in:
		
							
								
								
									
										132
									
								
								src/floater.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/floater.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| 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 (dist < 0.3) { | ||||
|       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), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -16,9 +16,12 @@ 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; | ||||
| @@ -29,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; | ||||
| @@ -109,6 +113,7 @@ export class HuntMode { | ||||
|  | ||||
|     this.#updatePlayer(); | ||||
|     this.#updateFov(); | ||||
|     this.#updateFloaters(); | ||||
|     this.#updatePickups(); | ||||
|  | ||||
|     let world3d = new World3D(this.map.size); | ||||
| @@ -127,6 +132,7 @@ export class HuntMode { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     this.#drawFloaters(globalOffset); | ||||
|     this.#drawPlayer(globalOffset); | ||||
|  | ||||
|     this.#drawBadges(globalOffset); | ||||
| @@ -134,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; | ||||
| @@ -186,7 +196,6 @@ export class HuntMode { | ||||
|       this.faceLeft = false; | ||||
|     } | ||||
|  | ||||
|     let nSteps = 40; | ||||
|     let szX = 0.5; | ||||
|     let szY = 0.5; | ||||
|  | ||||
| @@ -215,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() { | ||||
| @@ -280,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++) { | ||||
| @@ -386,6 +382,12 @@ export class HuntMode { | ||||
|     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()); | ||||
| @@ -452,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) { | ||||
|   | ||||
| @@ -116,6 +116,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, | ||||
|   }; | ||||
| } | ||||
| @@ -15,15 +15,10 @@ import { GridArt } from "./gridart.ts"; | ||||
| import { getCheckModal } from "./checkmodal.ts"; | ||||
| import { Point, Size } from "./engine/datatypes.ts"; | ||||
| import { choose } from "./utils.ts"; | ||||
| import { | ||||
|   BG_CEILING, | ||||
|   FG_BOLD, | ||||
|   FG_TEXT, | ||||
|   SWATCH_EXP, | ||||
|   SWATCH_STAT, | ||||
| } 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 | ||||
| @@ -85,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; | ||||
|   } | ||||
| @@ -145,7 +144,23 @@ export class BreakableBlockPickup { | ||||
|     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); | ||||
| @@ -199,6 +214,19 @@ export class StatPickupCallbacks { | ||||
|     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 }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export class ExperiencePickupCallbacks { | ||||
| @@ -226,7 +254,16 @@ export class ExperiencePickupCallbacks { | ||||
|     options: { xScale?: number; yScale?: number; angle?: number }, | ||||
|   ) { | ||||
|     let at = gridArt.project(100); | ||||
|     D.drawSprite(sprCollectiblesSilhouettes, at, 0, options); | ||||
|     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 }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user