feat: implement state machine for creatures
This commit is contained in:
		
							parent
							
								
									b005f4b83a
								
							
						
					
					
						commit
						90a3446bdd
					
				
							
								
								
									
										582
									
								
								static/index.js
								
								
								
								
							
							
						
						
									
										582
									
								
								static/index.js
								
								
								
								
							| 
						 | 
					@ -5,6 +5,24 @@ const CREATURE_WIDTH = 32; // in px
 | 
				
			||||||
const FRAME_RATE = 6; // in FPS
 | 
					const FRAME_RATE = 6; // in FPS
 | 
				
			||||||
const FRAME_DELAY = 1 / FRAME_RATE * 1000; // in ms
 | 
					const FRAME_DELAY = 1 / FRAME_RATE * 1000; // in ms
 | 
				
			||||||
const DEFAULT_SPRITE_SHEET_COUNT = 1; // number of default sprite sheets
 | 
					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
 | 
					 * Enum of creature states
 | 
				
			||||||
| 
						 | 
					@ -31,9 +49,20 @@ const CreatureState = Object.freeze({
 | 
				
			||||||
    WALK_NORTHWEST: 16,
 | 
					    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 offset of the sprite with respect to
 | 
					 * @typedef {[number, number]} SpriteFrameOffset the client of the sprite with respect to
 | 
				
			||||||
 *                                               the left/top background position offset (off by factor of sprite size)
 | 
					 *                                               the left/top background position client (off by factor of sprite size)
 | 
				
			||||||
 * @type {Object.<number, Array<SpriteFrameOffset>>}
 | 
					 * @type {Object.<number, Array<SpriteFrameOffset>>}
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES = Object.freeze({
 | 
					const CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES = Object.freeze({
 | 
				
			||||||
| 
						 | 
					@ -109,6 +138,8 @@ const CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES = Object.freeze({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// === <TYPES> ===
 | 
					// === <TYPES> ===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** @typedef {[number, number]} vec2 a vector 2 components */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Properties for creatures running around the screen
 | 
					 * Properties for creatures running around the screen
 | 
				
			||||||
 * @typedef {Object} Creature
 | 
					 * @typedef {Object} Creature
 | 
				
			||||||
| 
						 | 
					@ -116,10 +147,11 @@ const CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES = Object.freeze({
 | 
				
			||||||
 * @property {string} spriteSheet the file name of the sprite sheet. should exist in {@link /static/sprites}
 | 
					 * @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} 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} stateDuration the number of frames the creature has been in its current state
 | 
				
			||||||
 * @property {number} positionX the number of pixels away from the left side of the container element
 | 
					 * @property {number} positionX x component of the center of the creature position
 | 
				
			||||||
 * @property {number} positionY the number of pixels away from the top of the container element
 | 
					 * @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} 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 {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.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// === <TYPES> ===
 | 
					// === <TYPES> ===
 | 
				
			||||||
| 
						 | 
					@ -127,15 +159,26 @@ const CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES = Object.freeze({
 | 
				
			||||||
// === <MATH_UTILS> ===
 | 
					// === <MATH_UTILS> ===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Returns random number between min (inclusive) and max (exclusive)
 | 
					 * Returns a random number between min (inclusive) and max (exclusive).
 | 
				
			||||||
 * If max is less than or equal to min, return min
 | 
					 * If max is less than or equal to min, return min
 | 
				
			||||||
 * @param {number} min inclusive lower bound
 | 
					 * @param {number} min inclusive lower bound
 | 
				
			||||||
 * @param {number} max exclusive upper bound
 | 
					 * @param {number} max exclusive upper bound
 | 
				
			||||||
 * @return {number} number in [min, max)
 | 
					 * @return {number} number in [min, max)
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const randomInt = (min, max) => {
 | 
					const getRandomInRange = (min, max) => {
 | 
				
			||||||
    if (max <= min) return min;
 | 
					    if (max <= min) return min;
 | 
				
			||||||
    return Math.floor(Math.random() * (max - min) + 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));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -161,10 +204,136 @@ const normalize = (...components) => {
 | 
				
			||||||
    return components.map(component => component / magnitude)
 | 
					    return components.map(component => component / magnitude)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Recursive helper for {@link getRandomChoice}
 | 
				
			||||||
 | 
					 * @template {!*} T
 | 
				
			||||||
 | 
					 * @param {Array<number>} weights
 | 
				
			||||||
 | 
					 * @param {Array<T>} 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<number>} weights if undefined, default to equal probability distribution of weights
 | 
				
			||||||
 | 
					 * @param {Array<T>} 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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// === </MATH_UTILS> ===
 | 
					// === </MATH_UTILS> ===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// === <CREATURE_UTILS> ===
 | 
					// === <CREATURE_UTILS> ===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 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
 | 
					 * Return a new updateCreature function that implicitly accepts the context
 | 
				
			||||||
 * of all other creatures being rendered.
 | 
					 * of all other creatures being rendered.
 | 
				
			||||||
| 
						 | 
					@ -194,8 +363,10 @@ const renderCreature = (creature) => {
 | 
				
			||||||
    creature.element.style.setProperty('display', 'block');
 | 
					    creature.element.style.setProperty('display', 'block');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // set position
 | 
					    // set position
 | 
				
			||||||
    creature.element.style.setProperty('left', `${creature.positionX}px`);
 | 
					    const positionX = creature.positionX - (0.5 * CREATURE_WIDTH) + creature.container.clientLeft
 | 
				
			||||||
    creature.element.style.setProperty('top', `${creature.positionY}px`);
 | 
					    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
 | 
					    // set sprite
 | 
				
			||||||
    const spriteFrames = CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES[creature.state]
 | 
					    const spriteFrames = CREATURE_STATE_TO_SPRITE_FRAME_OFFSET_INDICES[creature.state]
 | 
				
			||||||
| 
						 | 
					@ -229,7 +400,7 @@ const createCreature = (
 | 
				
			||||||
    const creatureEle = document.createElement('div');
 | 
					    const creatureEle = document.createElement('div');
 | 
				
			||||||
    const spriteSheetUrl = spriteSheet
 | 
					    const spriteSheetUrl = spriteSheet
 | 
				
			||||||
        ? `url('/static/sprites/${spriteSheet}')`
 | 
					        ? `url('/static/sprites/${spriteSheet}')`
 | 
				
			||||||
        : `url('/static/sprites/defaults/${randomInt(1, DEFAULT_SPRITE_SHEET_COUNT)}.gif')`;
 | 
					        : `url('/static/sprites/defaults/${getRandomInt(1, DEFAULT_SPRITE_SHEET_COUNT)}.gif')`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    creatureEle.setAttribute('id', name);
 | 
					    creatureEle.setAttribute('id', name);
 | 
				
			||||||
    creatureEle.style.setProperty('width', `${CREATURE_WIDTH}px`);
 | 
					    creatureEle.style.setProperty('width', `${CREATURE_WIDTH}px`);
 | 
				
			||||||
| 
						 | 
					@ -274,43 +445,378 @@ const beginCreatureAnimation = (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// === </CREATURE_UTILS> ===
 | 
					// === </CREATURE_UTILS> ===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 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<Object.<number, number>>}
 | 
				
			||||||
 | 
					 * @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<Creature>} 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.<number, function(creature: Creature, allCreatures: Array<Creature>): 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 = () => {
 | 
					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
 | 
					    // EXAMPLE SCRIPT
 | 
				
			||||||
    const kennelWindowEle = document.querySelector('#kennel-window');
 | 
					    const kennelWindowEle = document.querySelector('#kennel-window');
 | 
				
			||||||
    const creatures = ['test-creature-1', 'test-creature-2', 'test-creature-3']
 | 
					    const creatures = Array(32).fill('test-creature')
 | 
				
			||||||
      .map((name) => createCreature(
 | 
					      .map((name, index) => createCreature(
 | 
				
			||||||
        kennelWindowEle,
 | 
					        kennelWindowEle,
 | 
				
			||||||
        name,
 | 
					        name + index.toString(),
 | 
				
			||||||
        undefined, // default sprite sheet
 | 
					        undefined, // default sprite sheet
 | 
				
			||||||
        randomInt(0, 17), // random state
 | 
					        getRandomChoice(undefined, [
 | 
				
			||||||
        randomInt(0, 2) * kennelWindowEle.clientWidth, // randomly left or right side
 | 
					            CreatureState.IDLE,
 | 
				
			||||||
        randomInt(0, 2) * kennelWindowEle.clientHeight, // randomly top or bottom
 | 
					            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 updateCreature = updateCreatureWithAllCreaturesContext((creature, allCreatures) => {
 | 
				
			||||||
        // example update creature script to bring creatures to the centroid
 | 
					        const updatedCreature = CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION[creature.state](creature, allCreatures);
 | 
				
			||||||
 | 
					        return updatedCreature
 | 
				
			||||||
        const centroidX = allCreatures.reduce(
 | 
					 | 
				
			||||||
          (sum, creature) => sum + creature.positionX,
 | 
					 | 
				
			||||||
          0,
 | 
					 | 
				
			||||||
        ) / allCreatures.length;
 | 
					 | 
				
			||||||
        const centroidY = allCreatures.reduce(
 | 
					 | 
				
			||||||
          (sum, creature) => sum + creature.positionY,
 | 
					 | 
				
			||||||
          0,
 | 
					 | 
				
			||||||
        ) / allCreatures.length;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const [deltaX, deltaY] = normalize(
 | 
					 | 
				
			||||||
          centroidX - creature.positionX,
 | 
					 | 
				
			||||||
          centroidY - creature.positionY,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            ...creature,
 | 
					 | 
				
			||||||
            positionX: constrain(creature.positionX + deltaX, 0, creature.container.clientWidth),
 | 
					 | 
				
			||||||
            positionY: constrain(creature.positionY + deltaY, 0, creature.container.clientHeight),
 | 
					 | 
				
			||||||
            stateDuration: creature.stateDuration + 1,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }, creatures);
 | 
					    }, creatures);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    creatures.forEach((creature) => beginCreatureAnimation(creature, updateCreature))
 | 
					    creatures.forEach((creature) => beginCreatureAnimation(creature, updateCreature))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					window.onresize = () => {
 | 
				
			||||||
 | 
					    // TODO: dynamically change creature size based on window size
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,12 @@
 | 
				
			||||||
#kennel-window {
 | 
					#kennel-window {
 | 
				
			||||||
    width: 80vw;
 | 
					    width: 100%;
 | 
				
			||||||
    min-width: 8rem;
 | 
					    height: 100vh;
 | 
				
			||||||
    height: 80vw;
 | 
					    background-color: whitesmoke;
 | 
				
			||||||
    min-height: 8rem;
 | 
					    margin: 0;
 | 
				
			||||||
    margin: auto;
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue