Skip to content

Commit

Permalink
Merge branch '13-bonuses'
Browse files Browse the repository at this point in the history
  • Loading branch information
gtanczyk committed Aug 22, 2024
2 parents 2b2026b + dacb754 commit 98658c0
Show file tree
Hide file tree
Showing 23 changed files with 248 additions and 186 deletions.
99 changes: 62 additions & 37 deletions js13k2024/game/src/game-states/gameplay/game-logic.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -7,14 +16,20 @@ 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,
getDirectionFromPositions,
getNewPosition,
isPositionEqual,
isPositionOccupied,
isValidMove,
isValidObstaclePush,
manhattanDistance,
} from './move-utils';

Expand Down Expand Up @@ -48,15 +63,15 @@ 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(
(obstacle) => !obstacle.isDestroying || Date.now() - obstacle.creationTime < OBSTACLE_DESTRUCTION_DURATION,
);

if (
gameState.crusherActive &&
isActiveBonus(newGameState, BonusType.Crusher) &&
gameState.obstacles.find((obstacle) => isPositionEqual(newPosition, obstacle.position))
) {
newGameState.obstacles = gameState.obstacles.map((obstacle) =>
Expand Down Expand Up @@ -116,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
Expand All @@ -133,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(
Expand Down Expand Up @@ -185,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(
Expand Down Expand Up @@ -230,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
Expand All @@ -245,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;
}
};
Expand Down Expand Up @@ -324,11 +328,12 @@ 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);
}
gameState.monsters = [];
gameState.tsunamiLevel = 0;
}
};

Expand All @@ -354,14 +359,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) =>
Expand Down Expand Up @@ -390,19 +400,34 @@ 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);
}
if (gameState.player.hasBlaster && gameState.player.blasterSteps!-- <= 0) {
gameState.player.hasBlaster = false;
}

// 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;
});
};

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))
);
}
};
46 changes: 25 additions & 21 deletions js13k2024/game/src/game-states/gameplay/game-render.ts
Original file line number Diff line number Diff line change
@@ -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 { 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';
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, drawTsunamiWave, generateTsunamiWaves } from './render/bonus-effect-render';

export const PLATFORM_HEIGHT = 20;

Expand Down Expand Up @@ -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);
Expand All @@ -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),
Expand All @@ -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;
Expand All @@ -95,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(
Expand All @@ -104,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':
Expand All @@ -123,7 +126,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) {
Expand Down
11 changes: 4 additions & 7 deletions js13k2024/game/src/game-states/gameplay/gameplay-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -68,8 +63,6 @@ export interface GameState {
explosions: Explosion[];
timeBombs: TimeBomb[];
landMines: Position[];
crusherActive: boolean;
builderActive: boolean;
score: number;
gameEndingState: GameEndingState;
tsunamiLevel: number;
Expand Down Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion js13k2024/game/src/game-states/gameplay/gameplay.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
7 changes: 0 additions & 7 deletions js13k2024/game/src/game-states/gameplay/level-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => ({
Expand All @@ -62,8 +57,6 @@ const generateBaseState = (): GameState => ({
explosions: [],
timeBombs: [],
landMines: [],
crusherActive: false,
builderActive: false,
score: 0,
gameEndingState: 'none',
tsunamiLevel: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { BonusType, GameState, LevelConfig } from '../gameplay-types';
import { GameState, LevelConfig } from '../gameplay-types';
import {
createPosition,
createMonster,
createObstacle,
createPlayer,
generateBaseState,
generateBaseConfig,
createBonus,
} from '../level-generator';

export const generateLevel = (): [GameState, LevelConfig, string] => {
Expand Down
5 changes: 3 additions & 2 deletions js13k2024/game/src/game-states/gameplay/monster-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const moveMonsters = (
obstacles: Obstacle[],
isPlayerInvisible: boolean,
isConfused: boolean,
isPlayerMonster: boolean,
): Monster[] => {
const newPositions: Position[] = [];
return monsters.map((monster) => {
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 98658c0

Please sign in to comment.