// === === const CREATURE_HEIGHT = 32; // in px const CREATURE_WIDTH = 32; // in px const FRAME_RATE = 6; // in FPS const FRAME_DELAY = 1 / FRAME_RATE * 1000; // in ms const DEFAULT_SPRITE_SHEET_COUNT = 1; // number of default sprite sheets const WALK_SIZE = 8; // magnitude of position change, in px const NUMERICAL_TOLERANCE = 0.01; /** * Enum of directions * @readonly * @enum {number} */ const Direction = Object.freeze({ NORTH: 0, EAST: 1, SOUTH: 2, WEST: 3, NORTHEAST: 4, SOUTHEAST: 5, SOUTHWEST: 6, NORTHWEST: 7, }) /** * Enum of creature states * @readonly * @enum {number} */ const CreatureState = Object.freeze({ IDLE: 0, ALERT: 1, SCRATCH_SELF: 2, SCRATCH_NORTH: 3, SCRATCH_SOUTH: 4, SCRATCH_EAST: 5, SCRATCH_WEST: 6, TIRED: 7, SLEEPING: 8, WALK_NORTH: 9, WALK_NORTHEAST: 10, WALK_EAST: 11, WALK_SOUTHEAST: 12, WALK_SOUTH: 13, WALK_SOUTHWEST: 14, WALK_WEST: 15, WALK_NORTHWEST: 16, }) const WALKING_CREATURE_STATES = Object.freeze([ CreatureState.WALK_NORTH, CreatureState.WALK_NORTHEAST, CreatureState.WALK_EAST, CreatureState.WALK_SOUTHEAST, CreatureState.WALK_SOUTH, CreatureState.WALK_SOUTHWEST, CreatureState.WALK_WEST, CreatureState.WALK_NORTHWEST, ]) /** * @typedef {[number, number]} SpriteFrameOffset the client of the sprite with respect to * the left/top background position client (off by factor of sprite size) * @type {Object.>} */ const CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES = Object.freeze({ [CreatureState.IDLE]: [ [-3, -3] ], [CreatureState.ALERT]: [ [-7, -3] ], [CreatureState.SCRATCH_SELF]: [ [-5, 0], [-6, 0], [-7, 0], ], [CreatureState.SCRATCH_NORTH]: [ [0, 0], [0, -1], ], [CreatureState.SCRATCH_SOUTH]: [ [-7, -1], [-6, -2], ], [CreatureState.SCRATCH_EAST]: [ [-2, -2], [-2, -3], ], [CreatureState.SCRATCH_WEST]: [ [-4, 0], [-4, -1], ], [CreatureState.TIRED]: [ [-3, -2] ], [CreatureState.SLEEPING]: [ [-2, 0], [-2, -1], ], [CreatureState.WALK_NORTH]: [ [-1, -2], [-1, -3], ], [CreatureState.WALK_NORTHEAST]: [ [0, -2], [0, -3], ], [CreatureState.WALK_EAST]: [ [-3, 0], [-3, -1], ], [CreatureState.WALK_SOUTHEAST]: [ [-5, -1], [-5, -2], ], [CreatureState.WALK_SOUTH]: [ [-6, -3], [-7, -2], ], [CreatureState.WALK_SOUTHWEST]: [ [-5, -3], [-6, -1], ], [CreatureState.WALK_WEST]: [ [-4, -2], [-4, -3], ], [CreatureState.WALK_NORTHWEST]: [ [-1, 0], [-1, -1], ], }); // === === // === === /** @typedef {[number, number]} vec2 a vector 2 components */ /** * Properties for creatures running around the screen * @typedef {Object} Creature * @property {string} name the name of the creature, used as the HTML element's id * @property {string} spriteSheet the file name of the sprite sheet. should exist in {@link /static/sprites} * @property {number} state the current state of the creature (should be member of {@link CreatureState} enum) * @property {number} stateDuration the number of frames the creature has been in its current state * @property {number} positionX x component of the center of the creature position * @property {number} positionY y component of the center of the creature position * @property {HTMLElement} element the HTML element rendering the creature in the DOM * @property {HTMLElement} container the HTML element containing the creature (the kennel, if you will) * @property {?vec2} walkDirection the (normed) direction a creature is walking in, if it's walking. otherwise null. */ // === === // === === /** * Returns a random number between min (inclusive) and max (exclusive). * If max is less than or equal to min, return min * @param {number} min inclusive lower bound * @param {number} max exclusive upper bound * @return {number} number in [min, max) */ const getRandomInRange = (min, max) => { if (max <= min) return min; return Math.random() * (max - min) + min; } /** * Returns random number between min (inclusive) and max (exclusive) * If max is less than or equal to min, return Math.floor(min) * @param {number} min inclusive lower bound * @param {number} max exclusive upper bound * @return {number} integer in [min, max) */ const getRandomInt = (min, max) => { return Math.floor(getRandomInRange(min, max)); } /** * Returns value if value is in [min, max]. Otherwise, bound it to those limits * @param {number} value * @param {number} [min] lower bound. if not provided, do not bound the value from beneath * @param {number} [max] upper bound. if not provided, do not bound the value from above * @return {number} */ const constrain = (value, min = -Number.MAX_VALUE, max = Number.MAX_VALUE) => { return Math.max(Math.min(value, max), min); } /** * Normalize a vector (ie an array) to be of length 1 * @param {...number} components components of the vector * @return {number[]} normalized vector */ const normalize = (...components) => { const magnitude = Math.sqrt( components.reduce((squaredSum, component) => squaredSum + (component * component), 0) ) return components.map(component => component / magnitude) } /** * Recursive helper for {@link getRandomChoice} * @template {!*} T * @param {Array} weights * @param {Array} choices * @param {number} random * @param {number} cumulativeSum * @return {undefined|T} * @ignore */ const _getRandomChoice = (weights, choices, random, cumulativeSum) => { const choice = choices.shift(); if (choice === undefined) { return undefined; } const newCumulativeSum = (weights.shift() ?? 0) + cumulativeSum; if (random < newCumulativeSum) { return choice; } return _getRandomChoice(weights, choices, random, newCumulativeSum); } /** * Given an array of weights and an array of the same length of choices, * randomly pick an element from the choices whose probability of picking it corresponds * to the corresponding weight (given position in the array). * Returns undefined when there are fewer choices than weights * @template {!*} T type of the choice elements * @param {?Array} weights if undefined, default to equal probability distribution of weights * @param {Array} choices * @return {undefined|T} the chosen element, or undefined if there are more weights than choices */ const getRandomChoice = (weights, choices) => { weights = weights ?? Array(choices.length).fill(1 / choices.length); const weightSum = weights.reduce((sum, probability) => sum + probability, 0); const random = Math.random() * weightSum; return _getRandomChoice([...weights], [...choices], random, 0); } /** * Roll an n-sided, 1-indexed die. * Return true if it's a nat n (ie the die lands on side n). * False otherwise. * @param {number} n number of faces on the die. * @return {boolean} */ const rollForNatN = (n) => { return Math.random() < 1 / n; } /** * Returns true if two circles intersect, false otherwise. * We consider circles to intersect iff there exists a point p that lies in both circles * @param {number} x0 x component of the center of circle 1 * @param {number} y0 y component of the center of circle 1 * @param {number} r0 radius of circle 1 * @param {number} x1 x component of the center of circle 1 * @param {number} y1 y component of the center of circle 1 * @param {number} r1 radius of circle 1 * @return {boolean} */ const isIntersectingCircles = (x0, y0, r0, x1, y1, r1) => { return (x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1) < (r0 + r1) * (r0 + r1); } /** * Return true if val in [min, max) * @param {number} val * @param {number} min * @param {number} max * @return {boolean} */ const isBetween = (val, min, max) => { return val >= min && val < max; } // === === // === === /** * Given a normalized direction a creature is walking in, return * the creature state that corresponds closest to the direction. * Defaults to WALK_NORTH if something goes wrong. * @param {vec2} direction * @return {number} corresponding {@link CreatureState} */ const getWalkStateFromDirection = (direction) => { const directionAngle = Math.atan2(direction.at(0), direction.at(1)); if (Number.isNaN(directionAngle)) { return CreatureState.WALK_NORTH; // default } if (isBetween(directionAngle, Math.PI * 5/8, Math.PI * 7/8)) { return CreatureState.WALK_NORTHEAST; } if (isBetween(directionAngle, Math.PI * 3/8, Math.PI * 5/8)) { return CreatureState.WALK_EAST; } if (isBetween(directionAngle, Math.PI / 8, Math.PI * 3/8)) { return CreatureState.WALK_SOUTHEAST; } if (isBetween(directionAngle, -Math.PI / 8, Math.PI / 8)) { return CreatureState.WALK_SOUTH; } if (isBetween(directionAngle, -Math.PI * 3/8, -Math.PI / 8)) { return CreatureState.WALK_SOUTHWEST; } if (isBetween(directionAngle, -Math.PI * 5/8, -Math.PI * 3/8)) { return CreatureState.WALK_WEST; } if (isBetween(directionAngle, -Math.PI * 5/8, -Math.PI * 7/8)) { return CreatureState.WALK_NORTHWEST; } return CreatureState.WALK_NORTH; } /** * Return a new updateCreature function that implicitly accepts the context * of all other creatures being rendered. * @param {(creature: Creature, allCreatures?: Array) => Creature} updateCreature * updateCreature function that uses the context of all creatures * @param {Array} allCreatures all creatures rendered in the scene * @return {(creature: Creature) => Creature} an updateCreature function */ const updateCreatureWithAllCreaturesContext = (updateCreature, allCreatures) => { const creaturesMap = Object.fromEntries(allCreatures.map( creature => [ creature.name, creature ] )); return (creature) => { const updatedCreature = updateCreature(creature, Array.from(Object.values(creaturesMap))); creaturesMap[updatedCreature.name] = { ...updatedCreature }; return updatedCreature; } } /** * Render a frame of the creature and loads the next frame for render * @param {Creature} creature * @return {Creature} the creature being rendered */ const renderCreature = (creature) => { creature.element.style.setProperty('display', 'block'); // set position const positionX = creature.positionX - (0.5 * CREATURE_WIDTH) + creature.container.clientLeft const positionY = creature.positionY - (0.5 * CREATURE_HEIGHT) + creature.container.clientTop creature.element.style.setProperty('left', `${positionX}px`); creature.element.style.setProperty('top', `${positionY}px`); // set sprite const spriteFrames = CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES[creature.state] const currentSpriteFrameOffset = spriteFrames?.[creature.stateDuration % spriteFrames.length] creature.element.style.setProperty( 'background-position', `${currentSpriteFrameOffset[0] * CREATURE_WIDTH}px ${currentSpriteFrameOffset[1] * CREATURE_HEIGHT}px` ) return creature; } /** * Create the creature and start its rendering * @param {HTMLElement} container container element for creatures. the kennel if you will * @param {string} name name of the creature * @param {string} [spriteSheet] name of the sprite sheet. must be in /static/sprites * uses default sprite sheet if undefined * @param {number} [initialState] starting state of the creature * @param {number} [initialPositionX] initial x position in pixels (from the left side) * @param {number} [initialPositionY] initial y position in pixels (from the top) * @return Creature */ const createCreature = ( container, name, spriteSheet, initialState = CreatureState.IDLE, initialPositionX = 0, initialPositionY = 0 ) => { const creatureEle = document.createElement('div'); const spriteSheetUrl = spriteSheet ? `url('/static/sprites/${spriteSheet}')` : `url('/static/sprites/defaults/${getRandomInt(1, DEFAULT_SPRITE_SHEET_COUNT)}.gif')`; creatureEle.setAttribute('id', name); creatureEle.style.setProperty('width', `${CREATURE_WIDTH}px`); creatureEle.style.setProperty('height', `${CREATURE_HEIGHT}px`); creatureEle.style.setProperty('position', 'fixed'); creatureEle.style.setProperty('image-rendering', 'pixelated'); creatureEle.style.setProperty('background-image', spriteSheetUrl); creatureEle.style.setProperty('display', 'hidden'); container.appendChild(creatureEle); return { name, spriteSheet, state: initialState, stateDuration: 0, positionX: constrain(initialPositionX, 0, container.clientWidth), positionY: constrain(initialPositionY, 0, container.clientHeight), element: creatureEle, container } } /** * Start creature animation * @param {Creature} creature * @param {(creature: Creature) => Creature} [updateCreature] if undefined, uses identity * (via {@link renderAndUpdateCreature}) */ const beginCreatureAnimation = ( creature, updateCreature = (creature) => ({ ... creature }) ) => { const timeoutCallback = (callbackCreature) => { const newCreatureFrame = updateCreature(renderCreature(callbackCreature)); setTimeout(timeoutCallback, FRAME_DELAY, newCreatureFrame); } // render/update initial frame and start animation setTimeout(timeoutCallback, FRAME_DELAY, updateCreature(renderCreature(creature))); } // === === /** * Returns true if any point in creature 1's collision box is in creature 2's collision box * @param {Creature} creature1 * @param {Creature} creature2 * @return {boolean} */ const isCreaturesColliding = (creature1, creature2) => { return isIntersectingCircles( creature1.positionX, creature1.positionY, CREATURE_WIDTH * 3/4, creature2.positionX, creature2.positionY, CREATURE_WIDTH * 3/4, ) } /** * Returns the direction of the wall of collision if it collides. Otherwise, return undefined. * @param {Creature} creature * @return {number | undefined} */ const isCreatureOutOfBounds = (creature) => { if (creature.positionX - CREATURE_WIDTH / 2 < creature.container.clientLeft) { return Direction.WEST; } if (creature.positionX + CREATURE_WIDTH / 2 > creature.container.clientLeft + creature.container.clientWidth) { return Direction.EAST; } if (creature.positionY - CREATURE_HEIGHT / 2 < creature.container.clientTop) { return Direction.NORTH; } if (creature.positionY + CREATURE_HEIGHT / 2 > creature.container.clientTop + creature.container.clientHeight) { return Direction.SOUTH; } return undefined; } /** * Get the creature in the walk state moving toward a destination * @param {Creature} creature * @param {vec2} destination * @return {Creature} the creature in a walking state */ const startCreatureWalkTowardDirection = (creature, destination) => { const walkDirection = /** @type {vec2} */ normalize( destination.at(0) - creature.positionX, destination.at(1) - creature.positionY, ); const state = getWalkStateFromDirection(walkDirection); return { ...creature, walkDirection, state, stateDuration: state === creature.state ? creature.stateDuration + 1 : 0, positionX: creature.positionX + walkDirection.at(0) * WALK_SIZE, positionY: creature.positionY + walkDirection.at(1) * WALK_SIZE, }; } /** * Update function for creatures in any scratch state. * @param {Creature} creature current creature * @param {Creature} creature * @return {Creature} updated creature */ const updateScratchStateCreature = (creature) => { if (creature.stateDuration % 2 < 1) { // animation hasn't finished playing. let it finish. return { ...creature, stateDuration: creature.stateDuration + 1 }; } if (rollForNatN(4)) { return { ...creature, state: CreatureState.IDLE, stateDuration: 0 }; } return { ...creature, stateDuration: creature.stateDuration + 1 }; } /** * Used for {@link updateWalkStateCreature} to map the output of wall collision to new state * @type {Readonly>} * @ignore */ const WALL_DIRECTION_TO_SCRATCH_STATE = Object.freeze({ [Direction.NORTH]: CreatureState.SCRATCH_NORTH, [Direction.EAST]: CreatureState.SCRATCH_EAST, [Direction.SOUTH]: CreatureState.SCRATCH_SOUTH, [Direction.WEST]: CreatureState.SCRATCH_WEST, }) /** * Update function for creatures in any walk state. * @param {Creature} creature current creature * @param {Array} allCreatures all creatures in kennel * @return {Creature} updated creature */ const updateWalkStateCreature = (creature, allCreatures) => { const newPositionX = creature.positionX + (creature.walkDirection?.at(0) ?? 0) * WALK_SIZE; const newPositionY = creature.positionY + (creature.walkDirection?.at(1) ?? 0) * WALK_SIZE; const newPositionCreature = { ...creature, positionX: newPositionX, positionY: newPositionY, stateDuration: creature.stateDuration + 1, } const wallCollisionDirection = isCreatureOutOfBounds({ ...creature, positionX: newPositionX, positionY: newPositionY }) if (wallCollisionDirection !== undefined) { return { ...creature, state: WALL_DIRECTION_TO_SCRATCH_STATE[wallCollisionDirection] ?? CreatureState.IDLE, walkDirection: undefined, stateDuration: 0, positionX: constrain( newPositionX, creature.container.clientLeft + CREATURE_WIDTH / 2, creature.container.clientLeft + creature.container.clientWidth - CREATURE_WIDTH / 2, ), positionY: constrain( newPositionY, creature.container.clientTop + CREATURE_HEIGHT / 2, creature.container.clientTop + creature.container.clientHeight - CREATURE_HEIGHT / 2, ), } } const collidedCreature = allCreatures.reduce((prevCollidedCreature, currentCreature) => { if (prevCollidedCreature !== undefined) { return prevCollidedCreature; } if (currentCreature.name === creature.name) { return undefined; } if (isCreaturesColliding(newPositionCreature, currentCreature)) { return currentCreature; } return undefined; }, undefined); if (collidedCreature !== undefined) { return { ...creature, walkDirection: undefined, stateDuration: 0, state: CreatureState.ALERT }; } if (creature.stateDuration < 8 || !rollForNatN(10)) { // always play for at least 8 frames return newPositionCreature; } return { ...creature, walkDirection: undefined, stateDuration: 0, state: CreatureState.IDLE }; } /** * Creature state transitions * @type {Object.): Creature>} */ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({ [CreatureState.IDLE]: (creature, allCreatures) => { if (creature.stateDuration < 8) { // play for at least 8 frames return { ...creature, stateDuration: creature.stateDuration + 1 }; } const spottedCreatures = allCreatures .filter((otherCreature) => isIntersectingCircles( // circle at creature position with a view distance radius creature.positionX, creature.positionY, CREATURE_WIDTH * 4, otherCreature.positionX, otherCreature.positionY, 0, )) .filter((otherCreature) => otherCreature.name !== creature.name) if (spottedCreatures.length > 1) { // run away from the centroid of close creatures const centroidX = spottedCreatures.reduce( (sum, spottedCreature) => sum + spottedCreature.positionX, 0, ) / spottedCreatures.length; const centroidY = spottedCreatures.reduce( (sum, spottedCreature) => sum + spottedCreature.positionY, 0 ) / spottedCreatures.length; return startCreatureWalkTowardDirection(creature, [ creature.positionX - centroidX, creature.positionY - centroidY, ]) } // move toward spotted creature, so long as doing so won't cause collision const updatedTowardSpottedCreature = spottedCreatures .map((otherCreature) => { const updatedCreature = startCreatureWalkTowardDirection(creature, [ otherCreature.positionX, otherCreature.positionY, ]); if (isCreaturesColliding(updatedCreature, otherCreature)) { return undefined; } return updatedCreature; }) .filter((updatedCreature) => updatedCreature !== undefined) .shift(); if (updatedTowardSpottedCreature !== undefined && rollForNatN(4)) { // if there's a creature in the view radius, move toward it 25% of the time return updatedTowardSpottedCreature; } const newState = getRandomChoice( [0.1, 0.1, 0.1, 0.1, 0.6], [ CreatureState.ALERT, // 0.1 CreatureState.SCRATCH_SELF, // 0.1 CreatureState.TIRED, // 0.1 CreatureState.WALK_NORTH, // 0.1 CreatureState.IDLE, // 0.1 ], ) if (newState === CreatureState.IDLE || newState === undefined) { return { ...creature, stateDuration: creature.stateDuration + 1 }; } if (newState !== CreatureState.WALK_NORTH) { return { ...creature, stateDuration: 0, state: newState, } } const updatedWalkingCreature = startCreatureWalkTowardDirection(creature, [ getRandomInRange(creature.container.clientLeft, creature.container.clientLeft + creature.container.clientWidth), getRandomInRange(creature.container.clientTop, creature.container.clientTop + creature.container.clientHeight), ]); // check for collision const collidingCreature = allCreatures .filter((otherCreature) => otherCreature.name !== updatedWalkingCreature.name) .filter((otherCreature) => isCreaturesColliding(updatedWalkingCreature, otherCreature)) .shift(); if (collidingCreature !== undefined) { return { ...creature, stateDuration: creature.stateDuration + 1 }; } return updatedWalkingCreature; }, [CreatureState.ALERT]: (creature, allCreatures) => { if (creature.stateDuration < 3) { // always play for at three frames return { ...creature, stateDuration: creature.stateDuration + 1 }; } const newState = getRandomChoice( [0.1, 0.1, 0.3, 0.5], [ CreatureState.SCRATCH_SELF, // 0.1 CreatureState.TIRED, // 0.1 CreatureState.IDLE, // 0.3 CreatureState.WALK_NORTH, // 0.5 ] ) if (newState === undefined) { // default if something goes wrong return { ...creature, state: CreatureState.IDLE, stateDuration: 0 }; } if (newState === CreatureState.WALK_NORTH) { // walk toward center with some random client return startCreatureWalkTowardDirection(creature, [ getRandomInRange(creature.container.clientLeft, creature.container.clientLeft + creature.container.clientWidth), getRandomInRange(creature.container.clientTop, creature.container.clientTop + creature.container.clientHeight), ]); } return { ...creature, state: newState, stateDuration: 0 } }, [CreatureState.SCRATCH_SELF]: (creature, allCreatures) => { if (creature.stateDuration % 3 < 2) { // animation hasn't finished playing play until complete return { ...creature, stateDuration: creature.stateDuration + 1 }; } if (rollForNatN(4)) { return { ...creature, state: CreatureState.IDLE, stateDuration: 0 }; } return { ...creature, stateDuration: creature.stateDuration + 1 }; }, [CreatureState.SCRATCH_NORTH]: updateScratchStateCreature, [CreatureState.SCRATCH_SOUTH]: updateScratchStateCreature, [CreatureState.SCRATCH_EAST]: updateScratchStateCreature, [CreatureState.SCRATCH_WEST]: updateScratchStateCreature, [CreatureState.TIRED]: (creature, allCreatures) => { if (creature.stateDuration < 8) { // play for eight frames return { ...creature, stateDuration: creature.stateDuration + 1 }; } if (rollForNatN(20)) { return { ...creature, state: CreatureState.ALERT, stateDuration: 0 }; } return { ...creature, state: CreatureState.SLEEPING, stateDuration: 0 } }, [CreatureState.SLEEPING]: (creature, allCreatures) => { if (creature.stateDuration < 8) { // play for at least eight frames return { ...creature, stateDuration: creature.stateDuration + 1 }; } const newState = getRandomChoice( [0.01, 0.02, 0.02, 0.95], [ CreatureState.TIRED, // 0.01 CreatureState.ALERT, // 0.02 CreatureState.IDLE, // 0.02 CreatureState.SLEEPING, // 0.95 ], ); if (newState === CreatureState.SLEEPING || newState === undefined) { return { ...creature, stateDuration: creature.stateDuration + 1 }; } return { ...creature, state: newState, stateDuration: 0 }; }, [CreatureState.WALK_NORTH]: updateWalkStateCreature, [CreatureState.WALK_NORTHEAST]: updateWalkStateCreature, [CreatureState.WALK_EAST]: updateWalkStateCreature, [CreatureState.WALK_SOUTHEAST]: updateWalkStateCreature, [CreatureState.WALK_SOUTH]: updateWalkStateCreature, [CreatureState.WALK_SOUTHWEST]: updateWalkStateCreature, [CreatureState.WALK_WEST]: updateWalkStateCreature, [CreatureState.WALK_NORTHWEST]: updateWalkStateCreature, }) window.onload = () => { if ( window.matchMedia(`(prefers-reduced-motion: reduce)`).matches || window.matchMedia(`(prefers-reduced-motion: reduce)`).matches ) { // user preference for low motion. do not render creatures return; } // EXAMPLE SCRIPT const kennelWindowEle = document.querySelector('#kennel-window'); const creatures = Array(32).fill('test-creature') .map((name, index) => createCreature( kennelWindowEle, name + index.toString(), undefined, // default sprite sheet getRandomChoice(undefined, [ CreatureState.IDLE, CreatureState.TIRED, CreatureState.SCRATCH_SELF, ]), getRandomInRange(kennelWindowEle.clientLeft, kennelWindowEle.clientLeft + kennelWindowEle.clientWidth), getRandomInRange(kennelWindowEle.clientTop, kennelWindowEle.clientTop + kennelWindowEle.clientHeight), )) const updateCreature = updateCreatureWithAllCreaturesContext((creature, allCreatures) => { const updatedCreature = CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION[creature.state](creature, allCreatures); return updatedCreature }, creatures); creatures.forEach((creature) => beginCreatureAnimation(creature, updateCreature)) } window.onresize = () => { // TODO: dynamically change creature size based on window size }