Skip to content

Commit

Permalink
Add bomb tool
Browse files Browse the repository at this point in the history
  • Loading branch information
jcreedcmu committed Nov 19, 2023
1 parent 169451e commit 4748927
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 25 deletions.
Binary file added public/assets/bomb-cursor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/toolbar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ export function Game(props: GameProps): JSX.Element {
if (tool == 'dynamite') {
return 'url(assets/dynamite-cursor.png) 16 16, pointer';
}
if (tool == 'bomb') {
return 'url(assets/bomb-cursor.png) 16 16, pointer';
}
if (tool == 'hand') {
return 'grab';
}
Expand Down
23 changes: 16 additions & 7 deletions src/core/reduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,13 @@ export type Intent =
| { t: 'dragTile', id: string }
| { t: 'vacuous' }
| { t: 'panWorld' }
| { t: 'kill' }
| { t: 'kill', radius: number, cost: number }
| { t: 'startSelection' }
;

const dynamiteIntent: Intent = { t: 'kill', radius: 0, cost: 1 };
const bombIntent: Intent = { t: 'kill', radius: 1, cost: 3 };

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' };
Expand All @@ -172,7 +175,14 @@ function getIntentOfMouseDown(tool: Tool, wp: WidgetPoint, button: number, mods:
case 'hand': return { t: 'panWorld' };
case 'dynamite':
if (hoverTile || hoverBlock) {
return { t: 'kill' };
return dynamiteIntent;
}
else {
return { t: 'vacuous' };
}
case 'bomb':
if (hoverTile || hoverBlock) {
return bombIntent;
}
else {
return { t: 'vacuous' };
Expand Down Expand Up @@ -211,7 +221,7 @@ function reduceIntent(state: GameState, intent: Intent, wp: WidgetPoint): GameSt
p_in_canvas: wp.p_in_canvas,
}
});
case 'kill': return tryKillTileOfState(vacuous_down(state, wp), wp);
case 'kill': return tryKillTileOfState(vacuous_down(state, wp), wp, intent.radius, intent.cost);
case 'startSelection':
if (wp.t != 'world') return vacuous_down(state, wp);
return produce(deselect(state), s => {
Expand Down Expand Up @@ -251,9 +261,8 @@ function reduceMouseDownInHand(state: GameState, wp: WidgetPoint & { t: 'hand' }
const p_in_hand_int = vm(wp.p_in_local, Math.floor);
const tiles = get_hand_tiles(state);
const tool = currentTool(state);
if (tool == 'dynamite') {
return tryKillTileOfState(vacuous_down(state, wp), wp);
}
if (tool == 'dynamite') return reduceIntent(state, dynamiteIntent, wp);
else if (tool == 'bomb') return reduceIntent(state, bombIntent, wp);
else {
const hoverTile = p_in_hand_int.x == 0 && p_in_hand_int.y >= 0 && p_in_hand_int.y < tiles.length;
if (hoverTile) {
Expand Down Expand Up @@ -308,7 +317,7 @@ function reduceGameAction(state: GameState, action: GameAction): effectful.Resul
return gs(drawOfState(state));
}
if (action.code == 'k') {
return gs(tryKillTileOfState(state, getWidgetPoint(state, state.mouseState.p_in_canvas)));
return gs(tryKillTileOfState(state, getWidgetPoint(state, state.mouseState.p_in_canvas), 0, 1));
}
if (action.code == 'd') {
return gs(checkValid(produce(addWorldTiles(removeAllTiles(state), debugTiles()), s => {
Expand Down
58 changes: 42 additions & 16 deletions src/core/state-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { PanicData, PauseData } from "./clock";
import { getLetterSample } from "./distribution";
import { checkConnected, checkGridWords, mkGridOfMainTiles } from "./grid";
import { Layer, Overlay, getOverlayLayer, overlayAny, overlayPoints, setOverlay } from "./layer";
import { Animation, GameState, Location, SelectionState, Tile, TileEntity } from "./state";
import { Animation, GameState, Location, MainTile, 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 @@ -50,14 +50,26 @@ export function drawOfState(state: GameState): GameState {
}));
}

export function tryKillTileOfState(state: GameState, wp: WidgetPoint): GameState {
if (state.coreState.score > 0 && (wp.t == 'world' || wp.t == 'hand'))
return killTileOfState(state, wp);
export function tryKillTileOfState(state: GameState, wp: WidgetPoint, radius: number, cost: number): GameState {
if (state.coreState.score >= cost && (wp.t == 'world' || wp.t == 'hand'))
return killTileOfState(state, wp, radius, cost);
else
return state;
}

function killTileOfState(state: GameState, wp: DragWidgetPoint): GameState {
function splashDamage(center: Point, radius: number): Point[] {
if (radius == 0)
return [center];
const pts: Point[] = [];
for (let x = -radius; x <= radius; x++) {
for (let y = -radius; y <= radius; y++) {
pts.push({ x: center.x + x, y: center.y + y });
}
}
return pts;
}

function killTileOfState(state: GameState, wp: DragWidgetPoint, radius: number, cost: number): GameState {

// Definitely want to clear the selection, because invariants get
// violated if a tileId gets deleted but remains in the selection
Expand All @@ -69,22 +81,36 @@ function killTileOfState(state: GameState, wp: DragWidgetPoint): GameState {
const anim: Animation = {
t: 'explosion',
center_in_world: vadd(p_in_world_int, { x: 0.5, y: 0.5 }),
duration_ms: 500,
duration_ms: (radius + 1) * 250,
start_ms: Date.now(),
radius,
}
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.coreState.score--;
s.coreState.animations.push(anim);
}));

function tileAt(p: Point): MainTile | undefined {
return get_main_tiles(state).find(tile => vequal(tile.loc.p_in_world_int, p));
}
function blockAt(p: Point) {
return getOverlayLayer(state.coreState.bonusOverlay, bonusLayer, p) == 'block';
}
else if (getOverlayLayer(state.coreState.bonusOverlay, bonusLayer, p_in_world_int) == 'block') {

if (tileAt(p_in_world_int) || blockAt(p_in_world_int)) {
const tilesToDestroy: Point[] = splashDamage(p_in_world_int, radius);
// remove all tiles in radius
tilesToDestroy.forEach(p => {
const tileAtP = tileAt(p);
if (tileAtP !== undefined)
state = removeTile(state, tileAtP.id);
});
// remove all bonuses in radius
state = produce(state, s => {
tilesToDestroy.forEach(p => {
if (blockAt(p))
setOverlay(s.coreState.bonusOverlay, p, 'empty');
});
});

return checkValid(produce(state, s => {
setOverlay(s.coreState.bonusOverlay, p_in_world_int, 'empty');
s.coreState.score--;
s.coreState.score -= cost;
s.coreState.animations.push(anim);
}));
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export type SelectionState = {
};

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

export type CoreState = {
animations: Animation[],
Expand Down
1 change: 1 addition & 0 deletions src/core/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const tools = [
'pointer',
'hand',
'dynamite',
'bomb',
] as const;

export type Tool = (typeof tools)[number];
Expand Down
2 changes: 1 addition & 1 deletion src/ui/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ 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 radius_in_world = (2 * anim.radius + 1) * 0.5 * (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)
Expand Down

0 comments on commit 4748927

Please sign in to comment.