From d0735c9a23a3f363247af47ba2262a533df9b813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Ta=C5=84czyk?= Date: Thu, 22 Aug 2024 14:11:19 +0300 Subject: [PATCH 1/7] finalize blaster implementation --- .../src/game-states/gameplay/entity-render.ts | 6 ++- .../src/game-states/gameplay/game-logic.ts | 40 ++++++++++++++----- .../src/game-states/gameplay/game-render.ts | 3 +- .../src/game-states/gameplay/player-render.ts | 16 ++++---- .../game-states/gameplay/tooltip-render.ts | 16 ++++---- 5 files changed, 52 insertions(+), 29 deletions(-) diff --git a/js13k2024/game/src/game-states/gameplay/entity-render.ts b/js13k2024/game/src/game-states/gameplay/entity-render.ts index 6fad9e3d..453dece5 100644 --- a/js13k2024/game/src/game-states/gameplay/entity-render.ts +++ b/js13k2024/game/src/game-states/gameplay/entity-render.ts @@ -149,7 +149,7 @@ export const drawEntityTentacles = ( } }; -export const renderEntity = (params: EntityRenderParams) => { +export const renderEntity = (params: EntityRenderParams): { bounceOffset: number } => { const { ctx, isoX, @@ -245,4 +245,6 @@ export const renderEntity = (params: EntityRenderParams) => { drawEntityMouth(ctx, renderX, renderY, entityHeight, headRadius, bounceOffset); ctx.restore(); -}; \ No newline at end of file + + return { bounceOffset }; +}; diff --git a/js13k2024/game/src/game-states/gameplay/game-logic.ts b/js13k2024/game/src/game-states/gameplay/game-logic.ts index f56980d6..f84f03e1 100644 --- a/js13k2024/game/src/game-states/gameplay/game-logic.ts +++ b/js13k2024/game/src/game-states/gameplay/game-logic.ts @@ -11,6 +11,7 @@ import { BLASTER_SHOT_DURATION, MOVE_ANIMATION_DURATION, OBSTACLE_DESTRUCTION_DU import { soundEngine } from '../../sound/sound-engine'; import { getDirectionFromKey, + getDirectionFromPositions, getNewPosition, isPositionEqual, isPositionOccupied, @@ -390,19 +391,38 @@ const handleBlasterShot = (gameState: GameState, direction: Direction, levelConf gameState.blasterShots.push(shot); // Play the blaster sound effect soundEngine.playBlasterSound(); - // Check if the shot hits a monster - const hitMonster = gameState.monsters.find((monster) => isPositionEqual(monster.position, shot.endPosition)); - if (hitMonster) { - gameState.monsters = gameState.monsters.filter((monster) => monster !== hitMonster); - } + + // Check if the shot hits monsters along its path + gameState.monsters = gameState.monsters.filter((monster) => { + const isHit = isMonsterOnBlasterPath(monster.position, start, end, direction); + return !isHit; + }); + if (gameState.player.hasBlaster && gameState.player.blasterSteps!-- <= 0) { gameState.player.hasBlaster = false; } }; -const getDirectionFromPositions = (from: Position, to: Position): Direction => { - if (to.x > from.x) return Direction.Right; - if (to.x < from.x) return Direction.Left; - if (to.y > from.y) return Direction.Down; - return Direction.Up; +const isMonsterOnBlasterPath = ( + monsterPos: Position, + start: Position, + end: Position, + direction: Direction, +): boolean => { + switch (direction) { + case Direction.Up: + case Direction.Down: + return ( + monsterPos.x === start.x && + ((direction === Direction.Up && monsterPos.y <= start.y && monsterPos.y >= end.y) || + (direction === Direction.Down && monsterPos.y >= start.y && monsterPos.y <= end.y)) + ); + case Direction.Left: + case Direction.Right: + return ( + monsterPos.y === start.y && + ((direction === Direction.Left && monsterPos.x <= start.x && monsterPos.x >= end.x) || + (direction === Direction.Right && monsterPos.x >= start.x && monsterPos.x <= end.x)) + ); + } }; diff --git a/js13k2024/game/src/game-states/gameplay/game-render.ts b/js13k2024/game/src/game-states/gameplay/game-render.ts index 7617fe6b..f2293247 100644 --- a/js13k2024/game/src/game-states/gameplay/game-render.ts +++ b/js13k2024/game/src/game-states/gameplay/game-render.ts @@ -123,7 +123,8 @@ export const drawGameState = ( // Draw tooltip if there's an active bonus const tooltipBonus = gameState.activeBonuses.find( (bonus) => - bonus.duration === 13 || [BonusType.Builder, BonusType.CapOfInvisibility, BonusType.Crusher].includes(bonus.type), + bonus.duration === 13 || + [BonusType.Builder, BonusType.CapOfInvisibility, BonusType.Crusher, BonusType.Blaster].includes(bonus.type), ); if (tooltipBonus) { diff --git a/js13k2024/game/src/game-states/gameplay/player-render.ts b/js13k2024/game/src/game-states/gameplay/player-render.ts index 573f2307..539b39aa 100644 --- a/js13k2024/game/src/game-states/gameplay/player-render.ts +++ b/js13k2024/game/src/game-states/gameplay/player-render.ts @@ -18,7 +18,7 @@ export const drawPlayer = ( isInvisible: boolean = false, obstacles: Obstacle[] = [], isMonster: boolean = false, - hasBlaster: boolean = false + hasBlaster: boolean = false, ) => { const { position, previousPosition, moveTimestamp, teleportTimestamp, isVanishing, isVictorious, isClimbing } = player; @@ -70,7 +70,7 @@ export const drawPlayer = ( ctx.globalAlpha = calculateTeleportingOpacity(Date.now() - teleportTimestamp); } - renderEntity(playerRenderParams); + const { bounceOffset } = renderEntity(playerRenderParams); // Reset global alpha ctx.globalAlpha = 1; @@ -92,7 +92,7 @@ export const drawPlayer = ( // Add blaster visual if (hasBlaster) { - drawBlaster(ctx, isoX, isoY, cellSize); + drawBlaster(ctx, isoX, isoY, cellSize, bounceOffset); } // Add teleportation effect if the player has just teleported @@ -193,7 +193,7 @@ const drawMonsterEffects = (ctx: CanvasRenderingContext2D, x: number, y: number, // Draw glowing eyes const eyeSize = cellSize * 0.1; const eyeOffset = cellSize * 0.15; - + ctx.beginPath(); ctx.arc(-eyeOffset, -eyeOffset, eyeSize, 0, Math.PI * 2); ctx.arc(eyeOffset, -eyeOffset, eyeSize, 0, Math.PI * 2); @@ -208,9 +208,9 @@ const drawMonsterEffects = (ctx: CanvasRenderingContext2D, x: number, y: number, ctx.restore(); }; -const drawBlaster = (ctx: CanvasRenderingContext2D, x: number, y: number, cellSize: number) => { +const drawBlaster = (ctx: CanvasRenderingContext2D, x: number, y: number, cellSize: number, bounceOffset: number) => { ctx.save(); - ctx.translate(x, y); + ctx.translate(x - cellSize / 10, y - bounceOffset); const blasterLength = cellSize * 0.4; const blasterWidth = cellSize * 0.1; @@ -233,7 +233,7 @@ const drawBlaster = (ctx: CanvasRenderingContext2D, x: number, y: number, cellSi ctx.fill(); // Add an energy charge effect - const chargeSize = (Math.sin(Date.now() / 200) + 1) * blasterWidth / 4; + const chargeSize = ((Math.sin(Date.now() / 200) + 1) * blasterWidth) / 4; ctx.beginPath(); ctx.arc(0, -blasterLength, chargeSize, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 255, 0, 0.8)'; @@ -313,4 +313,4 @@ const drawTeleportationEffect = ( } ctx.restore(); -}; \ No newline at end of file +}; diff --git a/js13k2024/game/src/game-states/gameplay/tooltip-render.ts b/js13k2024/game/src/game-states/gameplay/tooltip-render.ts index ed60833b..a3c557c5 100644 --- a/js13k2024/game/src/game-states/gameplay/tooltip-render.ts +++ b/js13k2024/game/src/game-states/gameplay/tooltip-render.ts @@ -118,7 +118,7 @@ const drawTsunamiIcon = (ctx: CanvasRenderingContext2D, x: number, y: number, si ctx.beginPath(); ctx.moveTo(x, y + size); ctx.quadraticCurveTo(x + size / 4, y, x + size / 2, y + size / 2); - ctx.quadraticCurveTo(x + size * 3 / 4, y + size, x + size, y); + ctx.quadraticCurveTo(x + (size * 3) / 4, y + size, x + size, y); ctx.stroke(); ctx.fillStyle = 'rgba(0, 100, 255, 0.5)'; @@ -136,7 +136,7 @@ const drawMonsterIcon = (ctx: CanvasRenderingContext2D, x: number, y: number, si ctx.fillStyle = 'red'; ctx.beginPath(); ctx.arc(x + size / 3, y + size / 3, size / 10, 0, Math.PI * 2); - ctx.arc(x + size * 2 / 3, y + size / 3, size / 10, 0, Math.PI * 2); + ctx.arc(x + (size * 2) / 3, y + size / 3, size / 10, 0, Math.PI * 2); ctx.fill(); }; @@ -163,27 +163,27 @@ const drawSokobanIcon = (ctx: CanvasRenderingContext2D, x: number, y: number, si ctx.beginPath(); ctx.moveTo(x, y + size / 2); ctx.lineTo(x + size, y + size / 2); - ctx.lineTo(x + size * 3 / 4, y + size / 4); + ctx.lineTo(x + (size * 3) / 4, y + size / 4); ctx.moveTo(x + size, y + size / 2); - ctx.lineTo(x + size * 3 / 4, y + size * 3 / 4); + ctx.lineTo(x + (size * 3) / 4, y + (size * 3) / 4); ctx.stroke(); }; const drawBlasterIcon = (ctx: CanvasRenderingContext2D, x: number, y: number, size: number) => { // Draw blaster body ctx.fillStyle = '#808080'; - ctx.fillRect(x, y + size / 3, size * 2 / 3, size / 3); + ctx.fillRect(x, y + size / 3, (size * 2) / 3, size / 3); // Draw blaster nozzle ctx.beginPath(); - ctx.arc(x + size * 2 / 3, y + size / 2, size / 6, 0, Math.PI * 2); + ctx.arc(x + (size * 2) / 3, y + size / 2, size / 6, 0, Math.PI * 2); ctx.fill(); // Draw "laser" beam ctx.strokeStyle = 'red'; ctx.setLineDash([size / 10, size / 20]); ctx.beginPath(); - ctx.moveTo(x + size * 2 / 3, y + size / 2); + ctx.moveTo(x + (size * 2) / 3, y + size / 2); ctx.lineTo(x + size, y + size / 2); ctx.stroke(); ctx.setLineDash([]); @@ -194,4 +194,4 @@ const drawDefaultIcon = (ctx: CanvasRenderingContext2D, x: number, y: number, si ctx.arc(x + size / 2, y + size / 2, size / 3, 0, Math.PI * 2); ctx.stroke(); ctx.fillText('?', x + size / 2, y + size / 2 + 5); -}; \ No newline at end of file +}; From 570e8f740d1da2a3c4f15061222c95f57ccc2f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Ta=C5=84czyk?= Date: Thu, 22 Aug 2024 14:12:57 +0300 Subject: [PATCH 2/7] small refactor, move render stuff to render/ --- .../src/game-states/gameplay/game-logic.ts | 6 ++++- .../src/game-states/gameplay/game-render.ts | 24 +++++++++---------- .../src/game-states/gameplay/gameplay.tsx | 2 +- .../src/game-states/gameplay/move-utils.ts | 2 +- .../gameplay/{ => render}/animation-utils.ts | 2 +- .../{ => render}/bonus-effect-render.ts | 2 +- .../gameplay/{ => render}/bonus-render.ts | 2 +- .../{ => render}/discharges-render.ts | 0 .../{ => render}/entity-render-utils.ts | 0 .../gameplay/{ => render}/entity-render.ts | 0 .../gameplay/{ => render}/explosion-render.ts | 2 +- .../{ => render}/grid-objects-render.ts | 2 +- .../gameplay/{ => render}/grid-render.ts | 16 ++++++------- .../gameplay/{ => render}/isometric-utils.ts | 2 +- .../gameplay/{ => render}/monster-render.ts | 11 ++++++--- .../{ => render}/move-arrows-render.ts | 4 ++-- .../gameplay/{ => render}/player-render.ts | 2 +- .../gameplay/{ => render}/tooltip-render.ts | 2 +- .../src/game-states/intro/game-preview.tsx | 2 +- 19 files changed, 46 insertions(+), 37 deletions(-) rename js13k2024/game/src/game-states/gameplay/{ => render}/animation-utils.ts (98%) rename js13k2024/game/src/game-states/gameplay/{ => render}/bonus-effect-render.ts (97%) rename js13k2024/game/src/game-states/gameplay/{ => render}/bonus-render.ts (98%) rename js13k2024/game/src/game-states/gameplay/{ => render}/discharges-render.ts (100%) rename js13k2024/game/src/game-states/gameplay/{ => render}/entity-render-utils.ts (100%) rename js13k2024/game/src/game-states/gameplay/{ => render}/entity-render.ts (100%) rename js13k2024/game/src/game-states/gameplay/{ => render}/explosion-render.ts (97%) rename js13k2024/game/src/game-states/gameplay/{ => render}/grid-objects-render.ts (98%) rename js13k2024/game/src/game-states/gameplay/{ => render}/grid-render.ts (93%) rename js13k2024/game/src/game-states/gameplay/{ => render}/isometric-utils.ts (98%) rename js13k2024/game/src/game-states/gameplay/{ => render}/monster-render.ts (92%) rename js13k2024/game/src/game-states/gameplay/{ => render}/move-arrows-render.ts (92%) rename js13k2024/game/src/game-states/gameplay/{ => render}/player-render.ts (99%) rename js13k2024/game/src/game-states/gameplay/{ => render}/tooltip-render.ts (98%) diff --git a/js13k2024/game/src/game-states/gameplay/game-logic.ts b/js13k2024/game/src/game-states/gameplay/game-logic.ts index f84f03e1..c8037e3d 100644 --- a/js13k2024/game/src/game-states/gameplay/game-logic.ts +++ b/js13k2024/game/src/game-states/gameplay/game-logic.ts @@ -7,7 +7,11 @@ import { isInExplosionRange, } from './monster-logic'; import { generateLevel } from './level-generator'; -import { BLASTER_SHOT_DURATION, MOVE_ANIMATION_DURATION, OBSTACLE_DESTRUCTION_DURATION } from './animation-utils'; +import { + BLASTER_SHOT_DURATION, + MOVE_ANIMATION_DURATION, + OBSTACLE_DESTRUCTION_DURATION, +} from './render/animation-utils'; import { soundEngine } from '../../sound/sound-engine'; import { getDirectionFromKey, diff --git a/js13k2024/game/src/game-states/gameplay/game-render.ts b/js13k2024/game/src/game-states/gameplay/game-render.ts index f2293247..f3d00424 100644 --- a/js13k2024/game/src/game-states/gameplay/game-render.ts +++ b/js13k2024/game/src/game-states/gameplay/game-render.ts @@ -1,16 +1,16 @@ import { BonusType, GameState, LevelConfig } from './gameplay-types'; -import { drawObstacles, drawGoal } from './grid-objects-render'; -import { drawGrid } from './grid-render'; -import { drawPlayer } from './player-render'; -import { drawMonsters } from './monster-render'; -import { drawBonuses, drawLandMines, drawTimeBombs } from './bonus-render'; -import { drawExplosions } from './explosion-render'; -import { calculateDrawingOrder } from './isometric-utils'; -import { calculateShakeOffset, interpolatePosition } from './animation-utils'; -import { drawTooltip } from './tooltip-render'; -import { drawElectricalDischarges } from './discharges-render'; -import { drawPlatform } from './grid-render'; -import { drawBlasterShot, drawSlideTrail, drawTsunamiEffect } from './bonus-effect-render'; +import { drawObstacles, drawGoal } from './render/grid-objects-render'; +import { drawGrid } from './render/grid-render'; +import { drawPlayer } from './render/player-render'; +import { drawMonsters } from './render/monster-render'; +import { drawBonuses, drawLandMines, drawTimeBombs } from './render/bonus-render'; +import { drawExplosions } from './render/explosion-render'; +import { calculateDrawingOrder } from './render/isometric-utils'; +import { calculateShakeOffset, interpolatePosition } from './render/animation-utils'; +import { drawTooltip } from './render/tooltip-render'; +import { drawElectricalDischarges } from './render/discharges-render'; +import { drawPlatform } from './render/grid-render'; +import { drawBlasterShot, drawSlideTrail, drawTsunamiEffect } from './render/bonus-effect-render'; export const PLATFORM_HEIGHT = 20; diff --git a/js13k2024/game/src/game-states/gameplay/gameplay.tsx b/js13k2024/game/src/game-states/gameplay/gameplay.tsx index f5728d4d..fa55d653 100644 --- a/js13k2024/game/src/game-states/gameplay/gameplay.tsx +++ b/js13k2024/game/src/game-states/gameplay/gameplay.tsx @@ -1,6 +1,6 @@ import { FunctionComponent, useState, useRef, useEffect } from 'react'; import { drawGameState } from './game-render'; -import { drawMoveArrows } from './move-arrows-render'; +import { drawMoveArrows } from './render/move-arrows-render'; import { doGameUpdate, handleKeyPress, isGameEnding } from './game-logic'; import { getMoveFromClick, getValidMoves } from './move-utils'; import { HUD } from './hud'; diff --git a/js13k2024/game/src/game-states/gameplay/move-utils.ts b/js13k2024/game/src/game-states/gameplay/move-utils.ts index 0268d638..fe123821 100644 --- a/js13k2024/game/src/game-states/gameplay/move-utils.ts +++ b/js13k2024/game/src/game-states/gameplay/move-utils.ts @@ -1,5 +1,5 @@ import { BonusType, Direction, GameState, LevelConfig, Position } from './gameplay-types'; -import { getArrowShape } from './move-arrows-render'; +import { getArrowShape } from './render/move-arrows-render'; export const isPositionEqual = (pos1: Position, pos2: Position): boolean => pos1.x === pos2.x && pos1.y === pos2.y; diff --git a/js13k2024/game/src/game-states/gameplay/animation-utils.ts b/js13k2024/game/src/game-states/gameplay/render/animation-utils.ts similarity index 98% rename from js13k2024/game/src/game-states/gameplay/animation-utils.ts rename to js13k2024/game/src/game-states/gameplay/render/animation-utils.ts index 1b1400b8..419267b3 100644 --- a/js13k2024/game/src/game-states/gameplay/animation-utils.ts +++ b/js13k2024/game/src/game-states/gameplay/render/animation-utils.ts @@ -1,4 +1,4 @@ -import { ElectricalDischarge } from './gameplay-types'; +import { ElectricalDischarge } from '../gameplay-types'; export const MOVE_ANIMATION_DURATION = 250; // 1/4 second export const TELEPORT_ANIMATION_DURATION = 500; // 1/2 second diff --git a/js13k2024/game/src/game-states/gameplay/bonus-effect-render.ts b/js13k2024/game/src/game-states/gameplay/render/bonus-effect-render.ts similarity index 97% rename from js13k2024/game/src/game-states/gameplay/bonus-effect-render.ts rename to js13k2024/game/src/game-states/gameplay/render/bonus-effect-render.ts index 2b2b9a34..6b475bc2 100644 --- a/js13k2024/game/src/game-states/gameplay/bonus-effect-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/bonus-effect-render.ts @@ -1,5 +1,5 @@ import { interpolatePosition } from './animation-utils'; -import { BlasterShot, Direction, GameState, Position } from './gameplay-types'; +import { BlasterShot, Direction, GameState, Position } from '../gameplay-types'; import { toIsometric } from './isometric-utils'; export const drawTsunamiEffect = ( diff --git a/js13k2024/game/src/game-states/gameplay/bonus-render.ts b/js13k2024/game/src/game-states/gameplay/render/bonus-render.ts similarity index 98% rename from js13k2024/game/src/game-states/gameplay/bonus-render.ts rename to js13k2024/game/src/game-states/gameplay/render/bonus-render.ts index e6b7377f..1d2e9dc9 100644 --- a/js13k2024/game/src/game-states/gameplay/bonus-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/bonus-render.ts @@ -1,4 +1,4 @@ -import { Position, Bonus, BonusType } from './gameplay-types'; +import { Position, Bonus, BonusType } from '../gameplay-types'; import { toIsometric, TILE_WIDTH, TILE_HEIGHT } from './isometric-utils'; import { drawShadow } from './grid-render'; diff --git a/js13k2024/game/src/game-states/gameplay/discharges-render.ts b/js13k2024/game/src/game-states/gameplay/render/discharges-render.ts similarity index 100% rename from js13k2024/game/src/game-states/gameplay/discharges-render.ts rename to js13k2024/game/src/game-states/gameplay/render/discharges-render.ts diff --git a/js13k2024/game/src/game-states/gameplay/entity-render-utils.ts b/js13k2024/game/src/game-states/gameplay/render/entity-render-utils.ts similarity index 100% rename from js13k2024/game/src/game-states/gameplay/entity-render-utils.ts rename to js13k2024/game/src/game-states/gameplay/render/entity-render-utils.ts diff --git a/js13k2024/game/src/game-states/gameplay/entity-render.ts b/js13k2024/game/src/game-states/gameplay/render/entity-render.ts similarity index 100% rename from js13k2024/game/src/game-states/gameplay/entity-render.ts rename to js13k2024/game/src/game-states/gameplay/render/entity-render.ts diff --git a/js13k2024/game/src/game-states/gameplay/explosion-render.ts b/js13k2024/game/src/game-states/gameplay/render/explosion-render.ts similarity index 97% rename from js13k2024/game/src/game-states/gameplay/explosion-render.ts rename to js13k2024/game/src/game-states/gameplay/render/explosion-render.ts index 27229f7b..7b72bc93 100644 --- a/js13k2024/game/src/game-states/gameplay/explosion-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/explosion-render.ts @@ -1,4 +1,4 @@ -import { Explosion } from './gameplay-types'; +import { Explosion } from '../gameplay-types'; import { toIsometric, TILE_WIDTH, TILE_HEIGHT } from './isometric-utils'; export const drawExplosions = (ctx: CanvasRenderingContext2D, explosions: Explosion[]) => { diff --git a/js13k2024/game/src/game-states/gameplay/grid-objects-render.ts b/js13k2024/game/src/game-states/gameplay/render/grid-objects-render.ts similarity index 98% rename from js13k2024/game/src/game-states/gameplay/grid-objects-render.ts rename to js13k2024/game/src/game-states/gameplay/render/grid-objects-render.ts index 2b4e2766..c9b24998 100644 --- a/js13k2024/game/src/game-states/gameplay/grid-objects-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/grid-objects-render.ts @@ -1,4 +1,4 @@ -import { Position, Obstacle } from './gameplay-types'; +import { Position, Obstacle } from '../gameplay-types'; import { toIsometric, TILE_WIDTH, TILE_HEIGHT } from './isometric-utils'; import { calculateObstacleHeight } from './animation-utils'; diff --git a/js13k2024/game/src/game-states/gameplay/grid-render.ts b/js13k2024/game/src/game-states/gameplay/render/grid-render.ts similarity index 93% rename from js13k2024/game/src/game-states/gameplay/grid-render.ts rename to js13k2024/game/src/game-states/gameplay/render/grid-render.ts index 187d221f..22d2dcdc 100644 --- a/js13k2024/game/src/game-states/gameplay/grid-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/grid-render.ts @@ -1,6 +1,6 @@ -import { PLATFORM_HEIGHT } from './game-render'; +import { PLATFORM_HEIGHT } from '../game-render'; import { TILE_HEIGHT, TILE_WIDTH, toIsometric } from './isometric-utils'; -import { GameState } from './gameplay-types'; +import { GameState } from '../gameplay-types'; export const drawPlatform = (ctx: CanvasRenderingContext2D, gridSize: number) => { const topLeft = toIsometric(0, 0); @@ -94,7 +94,10 @@ const drawTsunamiTile = (ctx: CanvasRenderingContext2D, x: number, y: number, ts ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(isoX - TILE_WIDTH / 2, isoY + TILE_HEIGHT / 2 - waterHeight + Math.sin(Date.now() / 200 + x * 0.5) * 3); - ctx.lineTo(isoX + TILE_WIDTH / 2, isoY + TILE_HEIGHT / 2 - waterHeight + Math.sin(Date.now() / 200 + (x + 1) * 0.5) * 3); + ctx.lineTo( + isoX + TILE_WIDTH / 2, + isoY + TILE_HEIGHT / 2 - waterHeight + Math.sin(Date.now() / 200 + (x + 1) * 0.5) * 3, + ); ctx.stroke(); }; @@ -102,10 +105,7 @@ const drawSlideTile = (ctx: CanvasRenderingContext2D, x: number, y: number) => { const { x: isoX, y: isoY } = toIsometric(x, y); // Draw a subtle ice-like effect - const gradient = ctx.createLinearGradient( - isoX - TILE_WIDTH / 2, isoY, - isoX + TILE_WIDTH / 2, isoY + TILE_HEIGHT - ); + const gradient = ctx.createLinearGradient(isoX - TILE_WIDTH / 2, isoY, isoX + TILE_WIDTH / 2, isoY + TILE_HEIGHT); gradient.addColorStop(0, 'rgba(200, 200, 255, 0.2)'); gradient.addColorStop(0.5, 'rgba(220, 220, 255, 0.3)'); gradient.addColorStop(1, 'rgba(200, 200, 255, 0.2)'); @@ -140,4 +140,4 @@ export const drawShadow = (ctx: CanvasRenderingContext2D, x: number, y: number, ctx.beginPath(); ctx.ellipse(x + width / 2, y + height / 2 + SHADOW_OFFSET, width / 2, height / 4, 0, 0, Math.PI * 2); ctx.fill(); -}; \ No newline at end of file +}; diff --git a/js13k2024/game/src/game-states/gameplay/isometric-utils.ts b/js13k2024/game/src/game-states/gameplay/render/isometric-utils.ts similarity index 98% rename from js13k2024/game/src/game-states/gameplay/isometric-utils.ts rename to js13k2024/game/src/game-states/gameplay/render/isometric-utils.ts index c2f34105..e2950e2e 100644 --- a/js13k2024/game/src/game-states/gameplay/isometric-utils.ts +++ b/js13k2024/game/src/game-states/gameplay/render/isometric-utils.ts @@ -1,4 +1,4 @@ -import { Position } from './gameplay-types'; +import { Position } from '../gameplay-types'; // Constants for isometric tile dimensions export const TILE_WIDTH = 60; diff --git a/js13k2024/game/src/game-states/gameplay/monster-render.ts b/js13k2024/game/src/game-states/gameplay/render/monster-render.ts similarity index 92% rename from js13k2024/game/src/game-states/gameplay/monster-render.ts rename to js13k2024/game/src/game-states/gameplay/render/monster-render.ts index c3a1812b..160930f3 100644 --- a/js13k2024/game/src/game-states/gameplay/monster-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/monster-render.ts @@ -1,10 +1,15 @@ -import { Monster } from './gameplay-types'; +import { Monster } from '../gameplay-types'; import { toIsometric } from './isometric-utils'; import { interpolatePosition } from './animation-utils'; import { EntityRenderParams } from './entity-render-utils'; import { renderEntity } from './entity-render'; -export const drawMonsters = (ctx: CanvasRenderingContext2D, monsters: Monster[], cellSize: number, isPlayerMonster: boolean) => { +export const drawMonsters = ( + ctx: CanvasRenderingContext2D, + monsters: Monster[], + cellSize: number, + isPlayerMonster: boolean, +) => { monsters.forEach((monster) => { const interpolatedPosition = interpolatePosition(monster.position, monster.previousPosition, monster.moveTimestamp); @@ -67,4 +72,4 @@ const drawPlayerFeatures = (ctx: CanvasRenderingContext2D, x: number, y: number, } ctx.restore(); -}; \ No newline at end of file +}; diff --git a/js13k2024/game/src/game-states/gameplay/move-arrows-render.ts b/js13k2024/game/src/game-states/gameplay/render/move-arrows-render.ts similarity index 92% rename from js13k2024/game/src/game-states/gameplay/move-arrows-render.ts rename to js13k2024/game/src/game-states/gameplay/render/move-arrows-render.ts index 1f4ecdb7..305fbd78 100644 --- a/js13k2024/game/src/game-states/gameplay/move-arrows-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/move-arrows-render.ts @@ -1,6 +1,6 @@ -import { Position, Direction } from './gameplay-types'; +import { Position, Direction } from '../gameplay-types'; import { toIsometric } from './isometric-utils'; -import { getNewPosition, getOrthogonalDirection } from './move-utils'; +import { getNewPosition, getOrthogonalDirection } from '../move-utils'; // Updated function to draw move arrows diff --git a/js13k2024/game/src/game-states/gameplay/player-render.ts b/js13k2024/game/src/game-states/gameplay/render/player-render.ts similarity index 99% rename from js13k2024/game/src/game-states/gameplay/player-render.ts rename to js13k2024/game/src/game-states/gameplay/render/player-render.ts index 539b39aa..6dc60c1b 100644 --- a/js13k2024/game/src/game-states/gameplay/player-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/player-render.ts @@ -1,4 +1,4 @@ -import { Obstacle, Player } from './gameplay-types'; +import { Obstacle, Player } from '../gameplay-types'; import { toIsometric } from './isometric-utils'; import { interpolatePosition, diff --git a/js13k2024/game/src/game-states/gameplay/tooltip-render.ts b/js13k2024/game/src/game-states/gameplay/render/tooltip-render.ts similarity index 98% rename from js13k2024/game/src/game-states/gameplay/tooltip-render.ts rename to js13k2024/game/src/game-states/gameplay/render/tooltip-render.ts index a3c557c5..1937018e 100644 --- a/js13k2024/game/src/game-states/gameplay/tooltip-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/tooltip-render.ts @@ -1,4 +1,4 @@ -import { Position, BonusType, getBonusDescription } from './gameplay-types'; +import { Position, BonusType, getBonusDescription } from '../gameplay-types'; import { toIsometric } from './isometric-utils'; export const drawTooltip = ( diff --git a/js13k2024/game/src/game-states/intro/game-preview.tsx b/js13k2024/game/src/game-states/intro/game-preview.tsx index 594428e9..5c6fe4a1 100644 --- a/js13k2024/game/src/game-states/intro/game-preview.tsx +++ b/js13k2024/game/src/game-states/intro/game-preview.tsx @@ -1,6 +1,6 @@ import { FunctionComponent, useEffect, useRef } from 'react'; import { drawGameState } from '../gameplay/game-render'; -import { drawGrid } from '../gameplay/grid-render'; +import { drawGrid } from '../gameplay/render/grid-render'; import { GameState, BonusType } from '../gameplay/gameplay-types'; const PREVIEW_WIDTH = 300; From 5c73c00048dbd4d3334e180e99d7ff9456299061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Ta=C5=84czyk?= Date: Thu, 22 Aug 2024 20:29:32 +0300 Subject: [PATCH 3/7] sokoban fix --- .../src/game-states/gameplay/game-logic.ts | 12 ++++++--- .../gameplay/levels/01-the-first-step.ts | 4 +-- .../src/game-states/gameplay/move-utils.ts | 27 ++++++++++++++++++- .../gameplay/render/isometric-utils.ts | 4 +-- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/js13k2024/game/src/game-states/gameplay/game-logic.ts b/js13k2024/game/src/game-states/gameplay/game-logic.ts index c8037e3d..7abc1fd6 100644 --- a/js13k2024/game/src/game-states/gameplay/game-logic.ts +++ b/js13k2024/game/src/game-states/gameplay/game-logic.ts @@ -20,6 +20,7 @@ import { isPositionEqual, isPositionOccupied, isValidMove, + isValidObstaclePush, manhattanDistance, } from './move-utils'; @@ -53,7 +54,7 @@ export const doGameUpdate = (direction: Direction, gameState: GameState, levelCo // Handle Sokoban bonus if (newGameState.activeBonuses.some((bonus) => bonus.type === BonusType.Sokoban)) { - handleSokobanMovement(newGameState, oldPosition, newPosition); + handleSokobanMovement(newGameState, oldPosition, newPosition, levelConfig.gridSize); } newGameState.obstacles = gameState.obstacles.filter( @@ -359,14 +360,19 @@ const handleSlideMovement = (gameState: GameState, direction: Direction, levelCo return newPosition; }; -const handleSokobanMovement = (gameState: GameState, oldPosition: Position, newPosition: Position): void => { +const handleSokobanMovement = ( + gameState: GameState, + oldPosition: Position, + newPosition: Position, + gridSize: number, +): void => { const pushedObstacle = gameState.obstacles.find((obstacle) => isPositionEqual(obstacle.position, newPosition)); if (pushedObstacle) { const obstacleNewPosition = getNewPosition( pushedObstacle.position, getDirectionFromPositions(oldPosition, newPosition), ); - if (isValidMove(obstacleNewPosition, gameState, { gridSize: gameState.obstacles.length })) { + if (isValidObstaclePush(obstacleNewPosition, gameState, { gridSize })) { pushedObstacle.position = obstacleNewPosition; // Check if the pushed obstacle crushes a monster const crushedMonster = gameState.monsters.find((monster) => diff --git a/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts b/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts index 0c6e4bd1..cb6565da 100644 --- a/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts +++ b/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts @@ -16,8 +16,8 @@ export const generateLevel = (): [GameState, LevelConfig, string] => { state.player = createPlayer(0, 3); state.goal = createPosition(6, 3); state.monsters = [createMonster(3, 0)]; - state.obstacles = [createObstacle(5, 2), createObstacle(5, 3)]; - state.bonuses = [createBonus(1, 3, BonusType.Blaster)]; + state.obstacles = [createObstacle(2, 3), createObstacle(5, 2), createObstacle(5, 3)]; + state.bonuses = [createBonus(1, 3, BonusType.Sokoban)]; return [state, config, config.levelStory]; }; diff --git a/js13k2024/game/src/game-states/gameplay/move-utils.ts b/js13k2024/game/src/game-states/gameplay/move-utils.ts index fe123821..8c26bb88 100644 --- a/js13k2024/game/src/game-states/gameplay/move-utils.ts +++ b/js13k2024/game/src/game-states/gameplay/move-utils.ts @@ -55,6 +55,29 @@ export function getOrthogonalDirection(direction: Direction, side: -1 | 1): Dire } } +export const isValidObstaclePush = ( + newPosition: Position, + gameState: GameState, + { gridSize }: Pick, +): boolean => { + const isWithinGrid = newPosition.x >= 0 && newPosition.x < gridSize && newPosition.y >= 0 && newPosition.y < gridSize; + + if (!isWithinGrid) { + return false; + } + + const isObstaclePresent = isPositionOccupied( + newPosition, + gameState.obstacles.filter((obstacle) => !obstacle.isDestroying).map(({ position }) => position), + ); + + if (isObstaclePresent) { + return false; + } + + return true; +}; + export const isValidMove = ( newPosition: Position, gameState: GameState, @@ -77,9 +100,11 @@ export const isValidMove = ( if (gameState.activeBonuses.some((bonus) => bonus.type === BonusType.Sokoban)) { const pushDirection = getDirectionFromPositions(gameState.player.position, newPosition); const pushedPosition = getNewPosition(newPosition, pushDirection); + const pushedWithinGrid = + pushedPosition.x >= 0 && pushedPosition.x < gridSize && pushedPosition.y >= 0 && pushedPosition.y < gridSize; // Check if the pushed position is valid if ( - isWithinGrid && + pushedWithinGrid && !isPositionOccupied( pushedPosition, gameState.obstacles.map((o) => o.position), diff --git a/js13k2024/game/src/game-states/gameplay/render/isometric-utils.ts b/js13k2024/game/src/game-states/gameplay/render/isometric-utils.ts index e2950e2e..4cdf67c3 100644 --- a/js13k2024/game/src/game-states/gameplay/render/isometric-utils.ts +++ b/js13k2024/game/src/game-states/gameplay/render/isometric-utils.ts @@ -108,13 +108,13 @@ export function drawIsometricCube( function getZIndex(type: string): number { switch (type) { case 'obstacle': - return 1; + return 3; case 'bonus': case 'landMine': case 'timeBomb': return 2; case 'goal': - return 3; + return 2; case 'monster': return 4; case 'player': From ef10b35f63301b8435be9fc23641cf695b28854b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Ta=C5=84czyk?= Date: Thu, 22 Aug 2024 23:12:03 +0300 Subject: [PATCH 4/7] tsunami rendering improvement --- .../src/game-states/gameplay/game-logic.ts | 1 + .../src/game-states/gameplay/game-render.ts | 12 ++-- .../gameplay/levels/01-the-first-step.ts | 2 +- .../gameplay/render/bonus-effect-render.ts | 69 +++++++++++-------- .../gameplay/render/grid-render.ts | 30 -------- 5 files changed, 50 insertions(+), 64 deletions(-) diff --git a/js13k2024/game/src/game-states/gameplay/game-logic.ts b/js13k2024/game/src/game-states/gameplay/game-logic.ts index 7abc1fd6..501081e4 100644 --- a/js13k2024/game/src/game-states/gameplay/game-logic.ts +++ b/js13k2024/game/src/game-states/gameplay/game-logic.ts @@ -335,6 +335,7 @@ const handleTsunamiEffect = (gameState: GameState): void => { startGameOverAnimation(gameState); } gameState.monsters = []; + gameState.tsunamiLevel = 0; } }; diff --git a/js13k2024/game/src/game-states/gameplay/game-render.ts b/js13k2024/game/src/game-states/gameplay/game-render.ts index f3d00424..79a46083 100644 --- a/js13k2024/game/src/game-states/gameplay/game-render.ts +++ b/js13k2024/game/src/game-states/gameplay/game-render.ts @@ -10,7 +10,7 @@ import { calculateShakeOffset, interpolatePosition } from './render/animation-ut import { drawTooltip } from './render/tooltip-render'; import { drawElectricalDischarges } from './render/discharges-render'; import { drawPlatform } from './render/grid-render'; -import { drawBlasterShot, drawSlideTrail, drawTsunamiEffect } from './render/bonus-effect-render'; +import { drawBlasterShot, drawSlideTrail, drawTsunamiWave, generateTsunamiWaves } from './render/bonus-effect-render'; export const PLATFORM_HEIGHT = 20; @@ -42,10 +42,8 @@ export const drawGameState = ( // Draw the grid drawGrid(ctx, gridSize, gameState); - // Draw tsunami effect - if (gameState.tsunamiLevel > 0) { - drawTsunamiEffect(ctx, gameState, gridSize, cellSize); - } + // Generate tsunami effect + const tsunamiWaves = gameState.tsunamiLevel > 0 ? generateTsunamiWaves(gameState, gridSize, cellSize) : []; // Draw electrical discharges drawElectricalDischarges(ctx, gridSize, gameState.monsterSpawnSteps, gameState.player.moveTimestamp, cellSize); @@ -57,6 +55,7 @@ export const drawGameState = ( // Prepare all game objects for sorting const allObjects = [ + ...tsunamiWaves.map((obj) => ({ position: obj.grid, type: 'wave', obj }) as const), ...gameState.obstacles.map((obj) => ({ position: obj.position, type: 'obstacle', obj }) as const), ...gameState.bonuses.map((obj) => ({ position: obj.position, type: 'bonus', obj }) as const), ...gameState.landMines.map((obj) => ({ position: obj, type: 'landMine' }) as const), @@ -82,6 +81,9 @@ export const drawGameState = ( for (const sortedObject of sortedObjects) { const { type, position } = sortedObject; switch (type) { + case 'wave': + drawTsunamiWave(ctx, sortedObject.obj); + break; case 'obstacle': drawObstacles(ctx, [sortedObject.obj], cellSize); break; diff --git a/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts b/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts index cb6565da..2894e8fa 100644 --- a/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts +++ b/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts @@ -17,7 +17,7 @@ export const generateLevel = (): [GameState, LevelConfig, string] => { state.goal = createPosition(6, 3); state.monsters = [createMonster(3, 0)]; state.obstacles = [createObstacle(2, 3), createObstacle(5, 2), createObstacle(5, 3)]; - state.bonuses = [createBonus(1, 3, BonusType.Sokoban)]; + state.bonuses = [createBonus(1, 3, BonusType.Tsunami)]; return [state, config, config.levelStory]; }; diff --git a/js13k2024/game/src/game-states/gameplay/render/bonus-effect-render.ts b/js13k2024/game/src/game-states/gameplay/render/bonus-effect-render.ts index 6b475bc2..e62ccf98 100644 --- a/js13k2024/game/src/game-states/gameplay/render/bonus-effect-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/bonus-effect-render.ts @@ -2,45 +2,58 @@ import { interpolatePosition } from './animation-utils'; import { BlasterShot, Direction, GameState, Position } from '../gameplay-types'; import { toIsometric } from './isometric-utils'; -export const drawTsunamiEffect = ( - ctx: CanvasRenderingContext2D, - gameState: GameState, - gridSize: number, - cellSize: number, -) => { +type TsunamiWave = { + tsunamiLevel: number; + grid: Position; + iso: [Position, Position, Position, Position]; +}; + +export const generateTsunamiWaves = (gameState: GameState, gridSize: number, cellSize: number): TsunamiWave[] => { const { tsunamiLevel } = gameState; const maxWaterHeight = cellSize * 0.8; // Maximum water height when tsunamiLevel reaches 13 - ctx.save(); - ctx.globalAlpha = 0.6; // Make the water slightly transparent + const waves: TsunamiWave[] = []; for (let y = 0; y < gridSize; y++) { for (let x = 0; x < gridSize; x++) { - const { x: isoX, y: isoY } = toIsometric(x, y); + const topL = toIsometric(x, y); + const topR = toIsometric(x + 1, y); + const bottomL = toIsometric(x, y + 1); + const bottomR = toIsometric(x + 1, y + 1); const waterHeight = (tsunamiLevel / 13) * maxWaterHeight; - ctx.fillStyle = `rgba(0, 100, 255, ${tsunamiLevel / 13})`; // Blue color with increasing opacity - ctx.beginPath(); - ctx.moveTo(isoX, isoY); - ctx.lineTo(isoX + cellSize / 2, isoY + cellSize / 4); - ctx.lineTo(isoX, isoY + cellSize / 2); - ctx.lineTo(isoX - cellSize / 2, isoY + cellSize / 4); - ctx.closePath(); - ctx.fill(); - - // Add wave effect - ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(isoX - cellSize / 2, isoY + cellSize / 4 - waterHeight + Math.sin(Date.now() / 200 + x * 0.5) * 5); - ctx.lineTo( - isoX + cellSize / 2, - isoY + cellSize / 4 - waterHeight + Math.sin(Date.now() / 200 + (x + 1) * 0.5) * 5, - ); - ctx.stroke(); + const wave1 = waterHeight + Math.sin(Date.now() / 200 + x * 0.5) * 5; + const wave2 = waterHeight + Math.sin(Date.now() / 200 + (x + 1) * 0.5) * 5; + + waves.push({ + tsunamiLevel, + grid: { x, y }, + iso: [ + { x: topL.x, y: topL.y - wave1 }, + { x: bottomL.x, y: bottomL.y - wave2 }, + { x: bottomR.x, y: bottomR.y - wave2 }, + { x: topR.x, y: topR.y - wave1 }, + ], + }); } } + return waves; +}; + +export const drawTsunamiWave = (ctx: CanvasRenderingContext2D, wave: TsunamiWave) => { + ctx.save(); + ctx.globalAlpha = 0.6; // Make the water slightly transparent + + ctx.fillStyle = `rgba(0, 100, 255, ${wave.tsunamiLevel / 13})`; // Blue color with increasing opacity + ctx.beginPath(); + ctx.moveTo(wave.iso[0].x, wave.iso[0].y); + for (let i = 1; i < wave.iso.length; i++) { + ctx.lineTo(wave.iso[i].x, wave.iso[i].y); + } + ctx.closePath(); + ctx.fill(); + ctx.restore(); }; diff --git a/js13k2024/game/src/game-states/gameplay/render/grid-render.ts b/js13k2024/game/src/game-states/gameplay/render/grid-render.ts index 22d2dcdc..0e5be5ae 100644 --- a/js13k2024/game/src/game-states/gameplay/render/grid-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/grid-render.ts @@ -63,11 +63,6 @@ export const drawGrid = (ctx: CanvasRenderingContext2D, gridSize: number, gameSt ctx.stroke(); } - // Draw Tsunami effect - if (gameState.tsunamiLevel > 0) { - drawTsunamiTile(ctx, x, y, gameState.tsunamiLevel); - } - // Draw Slide effect if active if (gameState.isSliding) { drawSlideTile(ctx, x, y); @@ -76,31 +71,6 @@ export const drawGrid = (ctx: CanvasRenderingContext2D, gridSize: number, gameSt } }; -const drawTsunamiTile = (ctx: CanvasRenderingContext2D, x: number, y: number, tsunamiLevel: number) => { - const { x: isoX, y: isoY } = toIsometric(x, y); - const waterHeight = (tsunamiLevel / 13) * TILE_HEIGHT * 0.5; // Max water height is half of tile height - - ctx.fillStyle = `rgba(0, 100, 255, ${tsunamiLevel / 26})`; // Increasing opacity as tsunami level increases - ctx.beginPath(); - ctx.moveTo(isoX, isoY - waterHeight); - ctx.lineTo(isoX + TILE_WIDTH / 2, isoY + TILE_HEIGHT / 2 - waterHeight); - ctx.lineTo(isoX, isoY + TILE_HEIGHT - waterHeight); - ctx.lineTo(isoX - TILE_WIDTH / 2, isoY + TILE_HEIGHT / 2 - waterHeight); - ctx.closePath(); - ctx.fill(); - - // Add wave effect - ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(isoX - TILE_WIDTH / 2, isoY + TILE_HEIGHT / 2 - waterHeight + Math.sin(Date.now() / 200 + x * 0.5) * 3); - ctx.lineTo( - isoX + TILE_WIDTH / 2, - isoY + TILE_HEIGHT / 2 - waterHeight + Math.sin(Date.now() / 200 + (x + 1) * 0.5) * 3, - ); - ctx.stroke(); -}; - const drawSlideTile = (ctx: CanvasRenderingContext2D, x: number, y: number) => { const { x: isoX, y: isoY } = toIsometric(x, y); From 15466bd7f6d7a61d9909ee507e25bb0b9cbef0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Ta=C5=84czyk?= Date: Thu, 22 Aug 2024 23:39:08 +0300 Subject: [PATCH 5/7] wave rendering improvements --- .../gameplay/render/bonus-effect-render.ts | 48 +++++++++++++++---- .../gameplay/render/isometric-utils.ts | 1 + 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/js13k2024/game/src/game-states/gameplay/render/bonus-effect-render.ts b/js13k2024/game/src/game-states/gameplay/render/bonus-effect-render.ts index e62ccf98..e16d1841 100644 --- a/js13k2024/game/src/game-states/gameplay/render/bonus-effect-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/bonus-effect-render.ts @@ -5,7 +5,9 @@ import { toIsometric } from './isometric-utils'; type TsunamiWave = { tsunamiLevel: number; grid: Position; - iso: [Position, Position, Position, Position]; + isoTop: Position[]; + isoFront: Position[] | undefined; + isoSide: Position[] | undefined; }; export const generateTsunamiWaves = (gameState: GameState, gridSize: number, cellSize: number): TsunamiWave[] => { @@ -28,12 +30,30 @@ export const generateTsunamiWaves = (gameState: GameState, gridSize: number, cel waves.push({ tsunamiLevel, grid: { x, y }, - iso: [ + isoTop: [ { x: topL.x, y: topL.y - wave1 }, { x: bottomL.x, y: bottomL.y - wave2 }, { x: bottomR.x, y: bottomR.y - wave2 }, { x: topR.x, y: topR.y - wave1 }, ], + isoFront: + y === gridSize - 1 + ? [ + { x: bottomL.x, y: bottomL.y - wave2 }, + { x: bottomL.x, y: bottomL.y }, + { x: bottomR.x, y: bottomR.y }, + { x: bottomR.x, y: bottomR.y - wave2 }, + ] + : undefined, + isoSide: + x === gridSize - 1 + ? [ + { x: bottomR.x, y: bottomR.y - wave2 }, + { x: bottomR.x, y: bottomR.y }, + { x: topR.x, y: topR.y }, + { x: topR.x, y: topR.y - wave1 }, + ] + : undefined, }); } } @@ -45,18 +65,28 @@ export const drawTsunamiWave = (ctx: CanvasRenderingContext2D, wave: TsunamiWave ctx.save(); ctx.globalAlpha = 0.6; // Make the water slightly transparent - ctx.fillStyle = `rgba(0, 100, 255, ${wave.tsunamiLevel / 13})`; // Blue color with increasing opacity - ctx.beginPath(); - ctx.moveTo(wave.iso[0].x, wave.iso[0].y); - for (let i = 1; i < wave.iso.length; i++) { - ctx.lineTo(wave.iso[i].x, wave.iso[i].y); + drawTsunamieWavePlane(ctx, wave.isoTop, `rgba(0, 100, 255, ${wave.tsunamiLevel / 13})`); + if (wave.isoFront) { + drawTsunamieWavePlane(ctx, wave.isoFront, `rgba(0, 39, 98, ${wave.tsunamiLevel / 13})`); + } + if (wave.isoSide) { + drawTsunamieWavePlane(ctx, wave.isoSide, `rgba(0, 63, 157, ${wave.tsunamiLevel / 13})`); } - ctx.closePath(); - ctx.fill(); ctx.restore(); }; +function drawTsunamieWavePlane(ctx: CanvasRenderingContext2D, plane: Position[], fillStyle: string) { + ctx.fillStyle = fillStyle; + ctx.beginPath(); + ctx.moveTo(plane[0].x, plane[0].y); + for (let i = 1; i < plane.length; i++) { + ctx.lineTo(plane[i].x, plane[i].y); + } + ctx.closePath(); + ctx.fill(); +} + export const drawSlideTrail = (ctx: CanvasRenderingContext2D, start: Position, end: Position) => { const { x: startX, y: startY } = toIsometric(start.x, start.y); const { x: endX, y: endY } = toIsometric(end.x, end.y); diff --git a/js13k2024/game/src/game-states/gameplay/render/isometric-utils.ts b/js13k2024/game/src/game-states/gameplay/render/isometric-utils.ts index 4cdf67c3..623820ba 100644 --- a/js13k2024/game/src/game-states/gameplay/render/isometric-utils.ts +++ b/js13k2024/game/src/game-states/gameplay/render/isometric-utils.ts @@ -107,6 +107,7 @@ export function drawIsometricCube( // Function to get z-index for different object types function getZIndex(type: string): number { switch (type) { + case 'wave': case 'obstacle': return 3; case 'bonus': From 2e0e5517ab8e8a2d18d85a2d03572a093b9abfd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Ta=C5=84czyk?= Date: Thu, 22 Aug 2024 23:55:06 +0300 Subject: [PATCH 6/7] isActiveBonus function instead of bonus state on player/game state props --- .../src/game-states/gameplay/game-logic.ts | 42 ++++++++----------- .../src/game-states/gameplay/game-render.ts | 9 ++-- .../game-states/gameplay/gameplay-types.ts | 11 ++--- .../game-states/gameplay/level-generator.ts | 7 ---- .../gameplay/levels/01-the-first-step.ts | 2 +- .../src/game-states/gameplay/monster-logic.ts | 5 ++- .../src/game-states/gameplay/move-utils.ts | 8 +++- .../gameplay/render/player-render.ts | 4 +- .../src/game-states/intro/game-preview.tsx | 7 ---- 9 files changed, 39 insertions(+), 56 deletions(-) diff --git a/js13k2024/game/src/game-states/gameplay/game-logic.ts b/js13k2024/game/src/game-states/gameplay/game-logic.ts index 501081e4..00dfb4a2 100644 --- a/js13k2024/game/src/game-states/gameplay/game-logic.ts +++ b/js13k2024/game/src/game-states/gameplay/game-logic.ts @@ -1,4 +1,13 @@ -import { GameState, Position, LevelConfig, Direction, BonusType, ActiveBonus, BlasterShot } from './gameplay-types'; +import { + GameState, + Position, + LevelConfig, + Direction, + BonusType, + ActiveBonus, + BlasterShot, + isActiveBonus, +} from './gameplay-types'; import { moveMonsters, checkCollision, @@ -62,7 +71,7 @@ export const doGameUpdate = (direction: Direction, gameState: GameState, levelCo ); if ( - gameState.crusherActive && + isActiveBonus(newGameState, BonusType.Crusher) && gameState.obstacles.find((obstacle) => isPositionEqual(newPosition, obstacle.position)) ) { newGameState.obstacles = gameState.obstacles.map((obstacle) => @@ -122,12 +131,13 @@ export const doGameUpdate = (direction: Direction, gameState: GameState, levelCo newGameState.player.position, levelConfig.gridSize, newGameState.obstacles, - newGameState.activeBonuses.some((bonus) => bonus.type === BonusType.CapOfInvisibility), - newGameState.activeBonuses.some((bonus) => bonus.type === BonusType.ConfusedMonsters), + isActiveBonus(newGameState, BonusType.CapOfInvisibility), + isActiveBonus(newGameState, BonusType.ConfusedMonsters), + isActiveBonus(newGameState, BonusType.Monster), ); // Handle Monster bonus - if (newGameState.player.isMonster) { + if (isActiveBonus(newGameState, BonusType.Monster)) { handleMonsterBonus(newGameState); } else { // Check for collisions with monsters @@ -139,7 +149,7 @@ export const doGameUpdate = (direction: Direction, gameState: GameState, levelCo } // Handle Blaster bonus - if (newGameState.player.hasBlaster) { + if (isActiveBonus(newGameState, BonusType.Blaster)) { handleBlasterShot(newGameState, direction, levelConfig); } newGameState.blasterShots = newGameState.blasterShots.filter( @@ -191,13 +201,8 @@ export const doGameUpdate = (direction: Direction, gameState: GameState, levelCo .map((bonus) => ({ ...bonus, duration: bonus.duration - 1 })) .filter((bonus) => bonus.duration > 0); - newGameState.builderActive = newGameState.activeBonuses.some((bonus) => bonus.type === BonusType.Builder); - newGameState.crusherActive = newGameState.activeBonuses.some((bonus) => bonus.type === BonusType.Crusher); - newGameState.player.isClimbing = newGameState.activeBonuses.some((bonus) => bonus.type === BonusType.Climber); - newGameState.isSliding = newGameState.activeBonuses.some((bonus) => bonus.type === BonusType.Slide); - // Handle builder bonus - if (gameState.builderActive) { + if (isActiveBonus(newGameState, BonusType.Builder)) { const newObstacle = { position: oldPosition, creationTime: Date.now(), isRaising: true, isDestroying: false }; if ( !isPositionOccupied( @@ -236,13 +241,10 @@ export const applyBonus = (gameState: GameState, bonusType: BonusType) => { }); break; case BonusType.Crusher: - gameState.crusherActive = true; break; case BonusType.Builder: - gameState.builderActive = true; break; case BonusType.Climber: - gameState.player.isClimbing = true; break; case BonusType.Teleport: // Teleport is handled immediately when collected @@ -251,17 +253,13 @@ export const applyBonus = (gameState: GameState, bonusType: BonusType) => { gameState.tsunamiLevel = 1; break; case BonusType.Monster: - gameState.player.isMonster = true; break; case BonusType.Slide: gameState.isSliding = true; break; case BonusType.Sokoban: - // Sokoban logic is handled in movement break; case BonusType.Blaster: - gameState.player.hasBlaster = true; - gameState.player.blasterSteps = 13; break; } }; @@ -330,7 +328,7 @@ const performTeleportation = (gameState: GameState, teleportPoint: Position): vo const handleTsunamiEffect = (gameState: GameState): void => { gameState.tsunamiLevel++; if (gameState.tsunamiLevel >= 13) { - if (!gameState.player.isClimbing) { + if (!isActiveBonus(gameState, BonusType.Climber)) { gameState.gameEndingState = 'gameOver'; startGameOverAnimation(gameState); } @@ -408,10 +406,6 @@ const handleBlasterShot = (gameState: GameState, direction: Direction, levelConf const isHit = isMonsterOnBlasterPath(monster.position, start, end, direction); return !isHit; }); - - if (gameState.player.hasBlaster && gameState.player.blasterSteps!-- <= 0) { - gameState.player.hasBlaster = false; - } }; const isMonsterOnBlasterPath = ( diff --git a/js13k2024/game/src/game-states/gameplay/game-render.ts b/js13k2024/game/src/game-states/gameplay/game-render.ts index 79a46083..d3ccfdd3 100644 --- a/js13k2024/game/src/game-states/gameplay/game-render.ts +++ b/js13k2024/game/src/game-states/gameplay/game-render.ts @@ -1,4 +1,4 @@ -import { BonusType, GameState, LevelConfig } from './gameplay-types'; +import { BonusType, GameState, isActiveBonus, LevelConfig } from './gameplay-types'; import { drawObstacles, drawGoal } from './render/grid-objects-render'; import { drawGrid } from './render/grid-render'; import { drawPlayer } from './render/player-render'; @@ -97,7 +97,7 @@ export const drawGameState = ( drawTimeBombs(ctx, [sortedObject.obj], cellSize); break; case 'monster': - drawMonsters(ctx, [sortedObject.obj], cellSize, gameState.player.isMonster); + drawMonsters(ctx, [sortedObject.obj], cellSize, isActiveBonus(gameState, BonusType.Monster)); break; case 'player': drawPlayer( @@ -106,8 +106,9 @@ export const drawGameState = ( cellSize, gameState.activeBonuses.some((bonus) => bonus.type === BonusType.CapOfInvisibility), gameState.obstacles, - gameState.player.isMonster, - gameState.player.hasBlaster, + isActiveBonus(gameState, BonusType.Monster), + isActiveBonus(gameState, BonusType.Blaster), + isActiveBonus(gameState, BonusType.Climber), ); break; case 'goal': diff --git a/js13k2024/game/src/game-states/gameplay/gameplay-types.ts b/js13k2024/game/src/game-states/gameplay/gameplay-types.ts index 6bbea369..87f93bd1 100644 --- a/js13k2024/game/src/game-states/gameplay/gameplay-types.ts +++ b/js13k2024/game/src/game-states/gameplay/gameplay-types.ts @@ -38,13 +38,8 @@ export interface Player { previousPosition: Position; moveTimestamp: number; teleportTimestamp?: number; - isInvisible: boolean; isVictorious: boolean; isVanishing: boolean; - isClimbing: boolean; - isMonster: boolean; - hasBlaster: boolean; - blasterSteps: number | undefined; } export interface Obstacle { @@ -68,8 +63,6 @@ export interface GameState { explosions: Explosion[]; timeBombs: TimeBomb[]; landMines: Position[]; - crusherActive: boolean; - builderActive: boolean; score: number; gameEndingState: GameEndingState; tsunamiLevel: number; @@ -217,3 +210,7 @@ export interface EntityAnimationState { bounceOffset: number; tentacleAnimationFactor: number; } + +export function isActiveBonus(gameState: GameState, bonusType: BonusType) { + return gameState.activeBonuses.some((bonus) => bonus.type === bonusType); +} diff --git a/js13k2024/game/src/game-states/gameplay/level-generator.ts b/js13k2024/game/src/game-states/gameplay/level-generator.ts index 8b6e0117..8d8bfa2a 100644 --- a/js13k2024/game/src/game-states/gameplay/level-generator.ts +++ b/js13k2024/game/src/game-states/gameplay/level-generator.ts @@ -34,13 +34,8 @@ const createPlayer = (x: number, y: number): Player => ({ position: createPosition(x, y), previousPosition: createPosition(x, y), moveTimestamp: Date.now(), - isInvisible: false, isVictorious: false, isVanishing: false, - isClimbing: false, - isMonster: false, - hasBlaster: false, - blasterSteps: undefined, }); const createObstacle = (x: number, y: number): Obstacle => ({ @@ -62,8 +57,6 @@ const generateBaseState = (): GameState => ({ explosions: [], timeBombs: [], landMines: [], - crusherActive: false, - builderActive: false, score: 0, gameEndingState: 'none', tsunamiLevel: 0, diff --git a/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts b/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts index 2894e8fa..b1cecde4 100644 --- a/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts +++ b/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts @@ -17,7 +17,7 @@ export const generateLevel = (): [GameState, LevelConfig, string] => { state.goal = createPosition(6, 3); state.monsters = [createMonster(3, 0)]; state.obstacles = [createObstacle(2, 3), createObstacle(5, 2), createObstacle(5, 3)]; - state.bonuses = [createBonus(1, 3, BonusType.Tsunami)]; + state.bonuses = [createBonus(1, 3, BonusType.Builder)]; return [state, config, config.levelStory]; }; diff --git a/js13k2024/game/src/game-states/gameplay/monster-logic.ts b/js13k2024/game/src/game-states/gameplay/monster-logic.ts index 4ec0d5c2..b831cc9f 100644 --- a/js13k2024/game/src/game-states/gameplay/monster-logic.ts +++ b/js13k2024/game/src/game-states/gameplay/monster-logic.ts @@ -8,6 +8,7 @@ export const moveMonsters = ( obstacles: Obstacle[], isPlayerInvisible: boolean, isConfused: boolean, + isPlayerMonster: boolean, ): Monster[] => { const newPositions: Position[] = []; return monsters.map((monster) => { @@ -21,8 +22,8 @@ export const moveMonsters = ( } let newPosition: Position; - if (isConfused) { - // If monsters are confused, move in the opposite direction + if (isConfused || isPlayerMonster) { + // If monsters are confused, or player is the monster, move in the opposite direction newPosition = getConfusedPosition(monster.position, path[1] || monster.position, gridSize, obstacles, monsters); } else { newPosition = path.length > 1 ? path[1] : monster.position; diff --git a/js13k2024/game/src/game-states/gameplay/move-utils.ts b/js13k2024/game/src/game-states/gameplay/move-utils.ts index 8c26bb88..93ac9932 100644 --- a/js13k2024/game/src/game-states/gameplay/move-utils.ts +++ b/js13k2024/game/src/game-states/gameplay/move-utils.ts @@ -1,4 +1,4 @@ -import { BonusType, Direction, GameState, LevelConfig, Position } from './gameplay-types'; +import { BonusType, Direction, GameState, isActiveBonus, LevelConfig, Position } from './gameplay-types'; import { getArrowShape } from './render/move-arrows-render'; export const isPositionEqual = (pos1: Position, pos2: Position): boolean => pos1.x === pos2.x && pos1.y === pos2.y; @@ -95,7 +95,11 @@ export const isValidMove = ( ); // Allow movement onto obstacles if the player has the Climber bonus active - if (isObstaclePresent && !gameState.player.isClimbing && !gameState.crusherActive) { + if ( + isObstaclePresent && + !isActiveBonus(gameState, BonusType.Climber) && + !isActiveBonus(gameState, BonusType.Crusher) + ) { // Check if Sokoban bonus is active if (gameState.activeBonuses.some((bonus) => bonus.type === BonusType.Sokoban)) { const pushDirection = getDirectionFromPositions(gameState.player.position, newPosition); diff --git a/js13k2024/game/src/game-states/gameplay/render/player-render.ts b/js13k2024/game/src/game-states/gameplay/render/player-render.ts index 6dc60c1b..f02fdcfc 100644 --- a/js13k2024/game/src/game-states/gameplay/render/player-render.ts +++ b/js13k2024/game/src/game-states/gameplay/render/player-render.ts @@ -19,9 +19,9 @@ export const drawPlayer = ( obstacles: Obstacle[] = [], isMonster: boolean = false, hasBlaster: boolean = false, + isClimbing: boolean = false, ) => { - const { position, previousPosition, moveTimestamp, teleportTimestamp, isVanishing, isVictorious, isClimbing } = - player; + const { position, previousPosition, moveTimestamp, teleportTimestamp, isVanishing, isVictorious } = player; const isTeleporting = teleportTimestamp && Date.now() - teleportTimestamp < TELEPORT_ANIMATION_DURATION; const interpolatedPosition = isTeleporting diff --git a/js13k2024/game/src/game-states/intro/game-preview.tsx b/js13k2024/game/src/game-states/intro/game-preview.tsx index 5c6fe4a1..a51f0005 100644 --- a/js13k2024/game/src/game-states/intro/game-preview.tsx +++ b/js13k2024/game/src/game-states/intro/game-preview.tsx @@ -13,13 +13,8 @@ const createPreviewGameState = (): GameState => ({ position: { x: 2, y: 2 }, previousPosition: { x: 2, y: 2 }, moveTimestamp: Date.now(), - isInvisible: false, isVictorious: false, isVanishing: false, - isClimbing: false, - isMonster: false, - hasBlaster: false, - blasterSteps: undefined, }, goal: { x: 4, y: 4 }, obstacles: [ @@ -54,8 +49,6 @@ const createPreviewGameState = (): GameState => ({ explosions: [], timeBombs: [], landMines: [], - crusherActive: false, - builderActive: false, score: 0, gameEndingState: 'none', tsunamiLevel: 0, From dacb754251ade3c0e20e04f996ac22920f66df77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Ta=C5=84czyk?= Date: Thu, 22 Aug 2024 23:56:38 +0300 Subject: [PATCH 7/7] cleanup --- .../game/src/game-states/gameplay/levels/01-the-first-step.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts b/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts index b1cecde4..b328ffc6 100644 --- a/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts +++ b/js13k2024/game/src/game-states/gameplay/levels/01-the-first-step.ts @@ -1,4 +1,4 @@ -import { BonusType, GameState, LevelConfig } from '../gameplay-types'; +import { GameState, LevelConfig } from '../gameplay-types'; import { createPosition, createMonster, @@ -6,7 +6,6 @@ import { createPlayer, generateBaseState, generateBaseConfig, - createBonus, } from '../level-generator'; export const generateLevel = (): [GameState, LevelConfig, string] => { @@ -17,7 +16,6 @@ export const generateLevel = (): [GameState, LevelConfig, string] => { state.goal = createPosition(6, 3); state.monsters = [createMonster(3, 0)]; state.obstacles = [createObstacle(2, 3), createObstacle(5, 2), createObstacle(5, 3)]; - state.bonuses = [createBonus(1, 3, BonusType.Builder)]; return [state, config, config.levelStory]; };