From 7e98ec63ac41a5d41c3c9da1ca5b74fc576acb7b Mon Sep 17 00:00:00 2001 From: B Wu Date: Wed, 14 Aug 2024 11:20:46 -0700 Subject: [PATCH] fix: collision and movement distribution fixes --- static/index.js | 202 +++++++++++++++++++++++------------------------- 1 file changed, 95 insertions(+), 107 deletions(-) diff --git a/static/index.js b/static/index.js index 7f7bbf2..e633c7f 100644 --- a/static/index.js +++ b/static/index.js @@ -6,7 +6,6 @@ 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 @@ -49,17 +48,6 @@ const CreatureState = Object.freeze({ 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) @@ -327,7 +315,7 @@ const getWalkStateFromDirection = (direction) => { return CreatureState.WALK_WEST; } - if (isBetween(directionAngle, -Math.PI * 5/8, -Math.PI * 7/8)) { + if (isBetween(directionAngle, -Math.PI * 7/8, -Math.PI * 5/8)) { return CreatureState.WALK_NORTHWEST; } @@ -342,7 +330,7 @@ const getWalkStateFromDirection = (direction) => { * @param {Array} allCreatures all creatures rendered in the scene * @return {(creature: Creature) => Creature} an updateCreature function */ -const updateCreatureWithAllCreaturesContext = (updateCreature, allCreatures) => { +const updateCreatureWithContext = (updateCreature, allCreatures) => { const creaturesMap = Object.fromEntries(allCreatures.map( creature => [ creature.name, creature ] )); @@ -453,8 +441,8 @@ const beginCreatureAnimation = ( */ const isCreaturesColliding = (creature1, creature2) => { return isIntersectingCircles( - creature1.positionX, creature1.positionY, CREATURE_WIDTH * 3/4, - creature2.positionX, creature2.positionY, CREATURE_WIDTH * 3/4, + creature1.positionX, creature1.positionY, CREATURE_WIDTH * 1/2, + creature2.positionX, creature2.positionY, CREATURE_WIDTH * 1/2, ) } @@ -541,63 +529,16 @@ const WALL_DIRECTION_TO_SCRATCH_STATE = Object.freeze({ * @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) { + // always play for at least 8 frames + if (creature.stateDuration < 8 || !rollForNatN(10)) { 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, - ), + positionX: creature.positionX + (creature.walkDirection?.at(0) ?? 0) * WALK_SIZE, + positionY: creature.positionY + (creature.walkDirection?.at(1) ?? 0) * WALK_SIZE, + stateDuration: creature.stateDuration + 1, } } - 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 }; } @@ -620,6 +561,7 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({ )) .filter((otherCreature) => otherCreature.name !== creature.name) + // move away from groups of 2 or more creatures if (spottedCreatures.length > 1) { // run away from the centroid of close creatures const centroidX = spottedCreatures.reduce( @@ -629,28 +571,17 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({ (sum, spottedCreature) => sum + spottedCreature.positionY, 0 ) / spottedCreatures.length; return startCreatureWalkTowardDirection(creature, [ - creature.positionX - centroidX, - creature.positionY - centroidY, - ]) + 2 * creature.positionX - centroidX, + 2 * 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; + // if there's a creature in the view radius, move toward it 25% of the time + if (spottedCreatures.length > 0 && rollForNatN(4)) { + const spottedCreature = /** @type {Creature} */ getRandomChoice(undefined, spottedCreatures); + return startCreatureWalkTowardDirection(creature, [ + spottedCreature.positionX, spottedCreature.positionY, + ]); } const newState = getRandomChoice( @@ -660,7 +591,7 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({ CreatureState.SCRATCH_SELF, // 0.1 CreatureState.TIRED, // 0.1 CreatureState.WALK_NORTH, // 0.1 - CreatureState.IDLE, // 0.1 + CreatureState.IDLE, // 0.6 ], ) @@ -676,22 +607,11 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({ } } - const updatedWalkingCreature = startCreatureWalkTowardDirection(creature, [ + // transition to walking to random point + return 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) { @@ -715,7 +635,7 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({ } if (newState === CreatureState.WALK_NORTH) { - // walk toward center with some random client + // walk toward random direction return startCreatureWalkTowardDirection(creature, [ getRandomInRange(creature.container.clientLeft, creature.container.clientLeft + creature.container.clientWidth), getRandomInRange(creature.container.clientTop, creature.container.clientTop + creature.container.clientHeight), @@ -784,6 +704,72 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({ [CreatureState.WALK_NORTHWEST]: updateWalkStateCreature, }) +/** + * Resolve collisions between the updated creature, the container bounds, and the other creatures. + * If the updated creature collides against the wall, move to a scratch direction state. + * If the updated creature collides with another creature, move to alert state. + * @param {Creature} updatedCreature the creature post update + * @param {Array} allCreatures all creatures in kennel + * @param {Creature} creature previous creature state + * @return {Creature} + */ +const resolveCollision = (updatedCreature, allCreatures, creature) => { + // resolve creature collision + const collidedCreatures = allCreatures + .filter((otherCreature) => isCreaturesColliding(otherCreature, updatedCreature)) + .filter((collidingCreature) => collidingCreature.name !== updatedCreature.name) + + const collidedCreature = collidedCreatures.length > 0 + ? getRandomChoice(undefined, collidedCreatures) + : undefined; + + // one third of the time, try to run away from the collided creature + if (collidedCreature !== undefined && rollForNatN(3)) { + const collidedCreature = getRandomChoice(undefined, collidedCreatures); + return resolveCollision( + startCreatureWalkTowardDirection(creature, [ + creature.positionX - collidedCreature.positionX, + creature.positionY - collidedCreature.positionY, + ]), + allCreatures, + creature, + ) + } + + if (collidedCreature !== undefined) { + return { + ...creature, + state: getRandomChoice(undefined, [CreatureState.ALERT, CreatureState.IDLE, CreatureState.TIRED]), + stateDuration: 0, + walkDirection: undefined, + }; + } + + // resolve wall collision + const wallCollisionDirection = isCreatureOutOfBounds(updatedCreature); + + if (wallCollisionDirection !== undefined) { + return { + ...updatedCreature, + state: WALL_DIRECTION_TO_SCRATCH_STATE[wallCollisionDirection] ?? CreatureState.IDLE, + walkDirection: undefined, + stateDuration: 0, + positionX: constrain( + updatedCreature.positionX, + creature.container.clientLeft + CREATURE_WIDTH / 2, + creature.container.clientLeft + creature.container.clientWidth - CREATURE_WIDTH / 2, + ), + positionY: constrain( + updatedCreature.positionY, + creature.container.clientTop + CREATURE_HEIGHT / 2, + creature.container.clientTop + creature.container.clientHeight - CREATURE_HEIGHT / 2, + ), + } + } + + return { ...updatedCreature }; +} + window.onload = () => { if ( window.matchMedia(`(prefers-reduced-motion: reduce)`).matches @@ -793,7 +779,6 @@ window.onload = () => { return; } - // EXAMPLE SCRIPT const kennelWindowEle = document.querySelector('#kennel-window'); const creatures = Array(32).fill('test-creature') .map((name, index) => createCreature( @@ -809,9 +794,12 @@ window.onload = () => { 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 + const updateCreature = updateCreatureWithContext((creature, allCreatures) => { + return resolveCollision( + CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION[creature.state](creature, allCreatures), + allCreatures, + creature, + ); }, creatures); creatures.forEach((creature) => beginCreatureAnimation(creature, updateCreature))