const CREATURE_HEIGHT = 32; // in px const CREATURE_WIDTH = 32; // in px const FRAME_RATE = 12; // in FPS const FRAME_DELAY = 1 / FRAME_RATE * 1000; // in ms const DEFAULT_SPRITE_SHEET_COUNT = 1; // number of default sprite sheets /** * 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, }) /** * @typedef {[number, number]} SpriteFrameOffset the offset of the sprite with respect to * the left/top background position offset (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], ], }); /** * 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 the number of pixels away from the left side of the container element * @property {number} positionY the number of pixels away from the top of the container element * @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) */ /** * Returns 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 randomInt = (min, max) => { if (max <= min) return min; return Math.floor(Math.random() * (max - min) + min); } /** * Return a creature's next frame based on the current creature frame * @param {Creature} creature * @return Creature the next frame of the creature */ const getNextCreatureFrame = (creature) => { // TODO return { ...creature, stateDuration: creature.stateDuration + 1 }; } /** * Render a frame of the creature and loads the next frame for render * @param {Creature} creature * @return Creature the next frame of the creature */ const renderCreature = (creature) => { // set position const positionX = Math.min(creature.positionX, creature.container.clientWidth); const positionY = Math.min(creature.positionY, creature.container.clientHeight); 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` ) const nextCreatureFrame = getNextCreatureFrame(creature); setTimeout(renderCreature, FRAME_DELAY, nextCreatureFrame) } /** * 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) */ const createCreature = ( container, name, spriteSheet, initialState = CreatureState.IDLE, initialPositionX = 0, initialPositionY = 0 ) => { const creatureEl = document.createElement('div'); const spriteSheetUrl = spriteSheet ? `url('/static/sprites/${spriteSheet}')` : `url('/static/sprites/defaults/${randomInt(1, DEFAULT_SPRITE_SHEET_COUNT)}.gif')`; creatureEl.setAttribute('id', name); creatureEl.style.setProperty('width', `${CREATURE_WIDTH}px`); creatureEl.style.setProperty('height', `${CREATURE_HEIGHT}px`); creatureEl.style.setProperty('position', 'fixed'); creatureEl.style.setProperty('image-rendering', 'pixelated'); creatureEl.style.setProperty('background-image', spriteSheetUrl); container.appendChild(creatureEl); renderCreature({ name, spriteSheet, state: initialState, stateDuration: 0, positionX: Math.max(0, initialPositionX), positionY: Math.max(0, initialPositionY), element: creatureEl, container }); } window.onload = () => { const kennelWindowEle = document.querySelector('#kennel-window'); createCreature( kennelWindowEle, 'test-creature', undefined, CreatureState.SCRATCH_SELF ) }