Skip to content

Commit

Permalink
Animations for dynamite (fix #54)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcreedcmu committed Nov 5, 2023
1 parent 242eeae commit b31feac
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 11 deletions.
9 changes: 9 additions & 0 deletions src/core/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ export function overlayPoints<T>(layer: Overlay<T>): Point[] {
return Object.keys(layer.cells).map(k => parseCoord(k));
}

export function overlayAny<T>(layer: Overlay<T>, predicate: (p: Point) => boolean): boolean {
for (const k of Object.keys(layer.cells)) {
if (predicate(parseCoord(k))) {
return true;
}
}
return false;
}

export function mkOverlayFrom(points: Point[]): Overlay<boolean> {
const layer: Overlay<boolean> = mkOverlay();
points.forEach(p => {
Expand Down
17 changes: 12 additions & 5 deletions src/core/reduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Action, Effect, GameAction } from './action';
import { getPanicFraction } from './clock';
import { Overlay, getOverlayLayer, mkOverlay, mkOverlayFrom, overlayPoints, setOverlay } from './layer';
import { GameState, Location, SceneState, SelectionState, TileEntity, mkGameSceneState } from './state';
import { addWorldTiles, checkValid, drawOfState, isCollision, isOccupied, tryKillTileOfState } from './state-helpers';
import { addWorldTiles, checkValid, drawOfState, filterExpiredAnimations, isCollision, isOccupied, isTilePinned, tryKillTileOfState } from './state-helpers';
import { getTileId, get_hand_tiles, get_main_tiles, get_tiles, putTileInHand, putTileInWorld, removeAllTiles, setTileLoc } from "./tile-helpers";
import { Tool, currentTool, toolOfIndex } from './tools';

Expand Down Expand Up @@ -156,14 +156,14 @@ export type Intent =
| { t: 'startSelection' }
;

function getIntentOfMouseDown(tool: Tool, wp: WidgetPoint, button: number, mods: Set<string>, hoverTile: TileEntity | undefined, hoverBlock: boolean): Intent {
function getIntentOfMouseDown(tool: Tool, wp: WidgetPoint, button: number, mods: Set<string>, hoverTile: TileEntity | undefined, hoverBlock: boolean, pinned: boolean): Intent {
if (button == 2)
return { t: 'panWorld' };

switch (tool) {
case 'pointer':
if (hoverTile) {
if (hoverTile.loc.t == 'world' && vequal(hoverTile.loc.p_in_world_int, { x: 0, y: 0 }))
if (pinned)
return { t: 'panWorld' };
return { t: 'dragTile', id: hoverTile.id };
}
Expand Down Expand Up @@ -232,7 +232,9 @@ function reduceMouseDownInWorld(state: GameState, wp: WidgetPoint, button: numbe
}
}
const hoverBlock = getOverlayLayer(state.bonusOverlay, state.bonusLayer, p_in_world_int) == 'block';
const intent = getIntentOfMouseDown(currentTool(state), wp, button, mods, hoverTile, hoverBlock);
let pinned =
(hoverTile && hoverTile.loc.t == 'world') ? isTilePinned(state, hoverTile.id, hoverTile.loc) : false;
const intent = getIntentOfMouseDown(currentTool(state), wp, button, mods, hoverTile, hoverBlock, pinned);
return reduceIntent(state, intent, wp);
}

Expand Down Expand Up @@ -316,11 +318,16 @@ function reduceGameAction(state: GameState, action: GameAction): effectful.Resul
s.mouseState.p_in_canvas = action.p;
}));
case 'repaint':
const now = Date.now();
const newAnimations = filterExpiredAnimations(now, state.animations);
state = produce(state, s => {
s.animations = newAnimations;
});
if (state.panic !== undefined) {
if (getPanicFraction(state.panic) > 1) {
return { state: { t: 'menu' }, effects: [] };
}
return gs(produce(state, s => { s.panic!.currentTime = Date.now(); }));
return gs(produce(state, s => { s.panic!.currentTime = now; }));
}
else {
return gs(state);
Expand Down
28 changes: 25 additions & 3 deletions src/core/state-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { WidgetPoint } from "../ui/widget-helpers";
import { logger } from "../util/debug";
import { produce } from "../util/produce";
import { Point } from "../util/types";
import { vequal, vint } from "../util/vutil";
import { vadd, vequal, vint } from "../util/vutil";
import { getAssets } from "./assets";
import { Bonus } from "./bonus";
import { getLetterSample } from "./distribution";
import { checkConnected, checkGridWords, mkGridOfMainTiles } from "./grid";
import { Layer, Overlay, getOverlayLayer, setOverlay } from "./layer";
import { GameState, Tile, TileEntity } from "./state";
import { Layer, Overlay, getOverlayLayer, overlayAny, overlayPoints, setOverlay } from "./layer";
import { Animation, GameState, Location, SelectionState, Tile, TileEntity } from "./state";
import { addHandTile, addWorldTile, ensureTileId, get_hand_tiles, get_main_tiles, get_tiles, removeTile } from "./tile-helpers";

export function addWorldTiles(state: GameState, tiles: Tile[]): GameState {
Expand Down Expand Up @@ -65,18 +65,26 @@ function killTileOfState(state: GameState, wp: WidgetPoint): GameState {
switch (wp.t) {
case 'world': {
const p_in_world_int = vint(wp.p_in_local);
const anim: Animation = {
t: 'explosion',
center_in_world: vadd(p_in_world_int, { x: 0.5, y: 0.5 }),
duration_ms: 500,
start_ms: Date.now(),
}
const tile = get_main_tiles(state).find(tile => vequal(tile.loc.p_in_world_int, p_in_world_int));
if (tile != undefined) {

return checkValid(produce(removeTile(state, tile.id), s => {
s.score--;
s.animations.push(anim);
}));

}
else if (getOverlayLayer(state.bonusOverlay, state.bonusLayer, p_in_world_int) == 'block') {
return checkValid(produce(state, s => {
setOverlay(s.bonusOverlay, p_in_world_int, 'empty');
s.score--;
s.animations.push(anim);
}));
}
else {
Expand Down Expand Up @@ -145,3 +153,17 @@ export function checkValid(state: GameState): GameState {
s.connectedSet = connectedSet;
});
}


export function isTilePinned(state: GameState, tileId: string, loc: Location & { t: 'world' }): boolean {
if (state.selected && state.selected.selectedIds.includes(tileId)) {
return overlayAny(state.selected.overlay, p => vequal(p, { x: 0, y: 0 }));
}
else {
return vequal(loc.p_in_world_int, { x: 0, y: 0 });
}
}

export function filterExpiredAnimations(now_ms: number, anims: Animation[]): Animation[] {
return anims.filter(anim => now_ms <= anim.start_ms + anim.duration_ms);
}
5 changes: 5 additions & 0 deletions src/core/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ export type SelectionState = {
selectedIds: string[],
};

export type Animation =
| { t: 'explosion', start_ms: number, duration_ms: number, center_in_world: Point };

export type GameState = {
animations: Animation[],
toolIndex: number,
tile_entities: Record<string, TileEntity>,
invalidWords: LocatedWord[],
Expand Down Expand Up @@ -101,6 +105,7 @@ export function mkGameSceneState(seed?: number): SceneState {
export function mkGameState(seed?: number): GameState {
seed = seed ?? 12345678;
return {
animations: [],
toolIndex: 0,
tile_entities: {},
invalidWords: [],
Expand Down
1 change: 1 addition & 0 deletions src/ui/instructions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function render(ci: CanvasInfo, props: CanvasProps) {

function exampleState(): GameState {
const state: GameState = {
animations: [],
toolIndex: 0,
invalidWords: [],
connectedSet: mkGridOf([]),
Expand Down
36 changes: 33 additions & 3 deletions src/ui/render.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { getAssets } from "../core/assets";
import { getPanicFraction } from "../core/clock";
import { LocatedWord, getGrid } from "../core/grid";
import { getOverlay, getOverlayLayer, overlayForEach } from "../core/layer";
import { GameState, Tile, TileEntity } from "../core/state";
import { getTileId, get_hand_tiles, get_main_tiles, get_tiles, isSelectedForDrag } from "../core/tile-helpers";
import { getOverlay, getOverlayLayer } from "../core/layer";
import { Animation, GameState, TileEntity } from "../core/state";
import { getTileId, get_hand_tiles, get_main_tiles, isSelectedForDrag } from "../core/tile-helpers";
import { fillRect, strokeRect } from "../util/dutil";
import { SE2, apply, compose, inverse, translate } from '../util/se2';
import { apply_to_rect } from "../util/se2-extra";
Expand All @@ -24,6 +24,29 @@ export function paintWithScale(ci: CanvasInfo, state: GameState) {

const backgroundGray = '#eeeeee';

function drawAnimation(d: CanvasRenderingContext2D, pan_canvas_from_world: SE2, time_ms: number, anim: Animation): void {
switch (anim.t) {
case 'explosion': {
const radius_in_world = 3 * (time_ms - anim.start_ms) / anim.duration_ms;
const radvec: Point = { x: radius_in_world, y: radius_in_world };
const rect_in_canvas = apply_to_rect(pan_canvas_from_world, {
p: vsub(anim.center_in_world, radvec), sz: vscale(radvec, 2)
});
d.strokeStyle = '#ff0000';
d.lineWidth = 3;
d.beginPath();
d.arc(rect_in_canvas.p.x + rect_in_canvas.sz.x / 2,
rect_in_canvas.p.y + rect_in_canvas.sz.y / 2,
rect_in_canvas.sz.y / 2,
0, 360,
);
d.stroke();

} break;
}

}

export function rawPaint(ci: CanvasInfo, state: GameState) {


Expand Down Expand Up @@ -232,10 +255,17 @@ export function rawPaint(ci: CanvasInfo, state: GameState) {
}
}

function drawAnimations(time_ms: number) {
state.animations.forEach(anim => {
drawAnimation(d, pan_canvas_from_world, time_ms, anim);
});
}

drawToolbar();
drawWorld();
drawHand();
drawOtherUi();
drawAnimations(Date.now());
}

export class RenderPane {
Expand Down

0 comments on commit b31feac

Please sign in to comment.