Flashier loot animations
This commit is contained in:
parent
27459787c1
commit
09e619208a
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 { getCheckModal } from "./checkmodal.ts";
|
||||||
import { CARDINAL_DIRECTIONS } from "./mapgen.ts";
|
import { CARDINAL_DIRECTIONS } from "./mapgen.ts";
|
||||||
import { Block3D, Floor3D, World3D } from "./world3d.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;
|
||||||
@ -29,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;
|
||||||
@ -109,6 +113,7 @@ export class HuntMode {
|
|||||||
|
|
||||||
this.#updatePlayer();
|
this.#updatePlayer();
|
||||||
this.#updateFov();
|
this.#updateFov();
|
||||||
|
this.#updateFloaters();
|
||||||
this.#updatePickups();
|
this.#updatePickups();
|
||||||
|
|
||||||
let world3d = new World3D(this.map.size);
|
let world3d = new World3D(this.map.size);
|
||||||
@ -127,6 +132,7 @@ export class HuntMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#drawFloaters(globalOffset);
|
||||||
this.#drawPlayer(globalOffset);
|
this.#drawPlayer(globalOffset);
|
||||||
|
|
||||||
this.#drawBadges(globalOffset);
|
this.#drawBadges(globalOffset);
|
||||||
@ -134,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;
|
||||||
@ -186,7 +196,6 @@ export class HuntMode {
|
|||||||
this.faceLeft = false;
|
this.faceLeft = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nSteps = 40;
|
|
||||||
let szX = 0.5;
|
let szX = 0.5;
|
||||||
let szY = 0.5;
|
let szY = 0.5;
|
||||||
|
|
||||||
@ -215,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() {
|
||||||
@ -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() {
|
#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++) {
|
||||||
@ -386,6 +382,12 @@ export class HuntMode {
|
|||||||
world3d.set(mapPosition, new Floor3D());
|
world3d.set(mapPosition, new Floor3D());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#drawFloaters(globalOffset: Point) {
|
||||||
|
for (let f of this.floaters.values()) {
|
||||||
|
f.draw(this.drawpile, globalOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#drawPlayer(_globalOffset: Point) {
|
#drawPlayer(_globalOffset: Point) {
|
||||||
/*
|
/*
|
||||||
let cellOffset = this.pixelPlayer.offset(globalOffset.negate());
|
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) {
|
#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) {
|
||||||
|
@ -116,6 +116,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,
|
||||||
|
};
|
||||||
|
}
|
@ -15,15 +15,10 @@ 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 {
|
import { FG_BOLD, FG_TEXT, SWATCH_EXP, SWATCH_STAT } from "./colors.ts";
|
||||||
BG_CEILING,
|
|
||||||
FG_BOLD,
|
|
||||||
FG_TEXT,
|
|
||||||
SWATCH_EXP,
|
|
||||||
SWATCH_STAT,
|
|
||||||
} from "./colors.ts";
|
|
||||||
import { Block3D } from "./world3d.ts";
|
import { Block3D } from "./world3d.ts";
|
||||||
import { DrawPile } from "./drawpile.ts";
|
import { DrawPile } from "./drawpile.ts";
|
||||||
|
import { Floater } from "./floater.ts";
|
||||||
|
|
||||||
export type Pickup =
|
export type Pickup =
|
||||||
| LockPickup
|
| LockPickup
|
||||||
@ -85,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;
|
||||||
}
|
}
|
||||||
@ -145,7 +144,23 @@ export class BreakableBlockPickup {
|
|||||||
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);
|
||||||
@ -199,6 +214,19 @@ export class StatPickupCallbacks {
|
|||||||
let at = gridArt.project(100);
|
let at = gridArt.project(100);
|
||||||
D.drawSprite(sprCollectiblesSilhouettes, at, statIndex, options);
|
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 {
|
export class ExperiencePickupCallbacks {
|
||||||
@ -226,7 +254,16 @@ export class ExperiencePickupCallbacks {
|
|||||||
options: { xScale?: number; yScale?: number; angle?: number },
|
options: { xScale?: number; yScale?: number; angle?: number },
|
||||||
) {
|
) {
|
||||||
let at = gridArt.project(100);
|
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 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user