fix: collision and movement distribution fixes
This commit is contained in:
parent
90a3446bdd
commit
7e98ec63ac
202
static/index.js
202
static/index.js
|
@ -6,7 +6,6 @@ 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 WALK_SIZE = 8; // magnitude of position change, in px
|
||||||
const NUMERICAL_TOLERANCE = 0.01;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum of directions
|
* Enum of directions
|
||||||
|
@ -49,17 +48,6 @@ 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 client of the sprite with respect to
|
* @typedef {[number, number]} SpriteFrameOffset the client of the sprite with respect to
|
||||||
* the left/top background position client (off by factor of sprite size)
|
* the left/top background position client (off by factor of sprite size)
|
||||||
|
@ -327,7 +315,7 @@ const getWalkStateFromDirection = (direction) => {
|
||||||
return CreatureState.WALK_WEST;
|
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;
|
return CreatureState.WALK_NORTHWEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,7 +330,7 @@ const getWalkStateFromDirection = (direction) => {
|
||||||
* @param {Array<Creature>} allCreatures all creatures rendered in the scene
|
* @param {Array<Creature>} allCreatures all creatures rendered in the scene
|
||||||
* @return {(creature: Creature) => Creature} an updateCreature function
|
* @return {(creature: Creature) => Creature} an updateCreature function
|
||||||
*/
|
*/
|
||||||
const updateCreatureWithAllCreaturesContext = (updateCreature, allCreatures) => {
|
const updateCreatureWithContext = (updateCreature, allCreatures) => {
|
||||||
const creaturesMap = Object.fromEntries(allCreatures.map(
|
const creaturesMap = Object.fromEntries(allCreatures.map(
|
||||||
creature => [ creature.name, creature ]
|
creature => [ creature.name, creature ]
|
||||||
));
|
));
|
||||||
|
@ -453,8 +441,8 @@ const beginCreatureAnimation = (
|
||||||
*/
|
*/
|
||||||
const isCreaturesColliding = (creature1, creature2) => {
|
const isCreaturesColliding = (creature1, creature2) => {
|
||||||
return isIntersectingCircles(
|
return isIntersectingCircles(
|
||||||
creature1.positionX, creature1.positionY, CREATURE_WIDTH * 3/4,
|
creature1.positionX, creature1.positionY, CREATURE_WIDTH * 1/2,
|
||||||
creature2.positionX, creature2.positionY, CREATURE_WIDTH * 3/4,
|
creature2.positionX, creature2.positionY, CREATURE_WIDTH * 1/2,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -541,63 +529,16 @@ const WALL_DIRECTION_TO_SCRATCH_STATE = Object.freeze({
|
||||||
* @return {Creature} updated creature
|
* @return {Creature} updated creature
|
||||||
*/
|
*/
|
||||||
const updateWalkStateCreature = (creature, allCreatures) => {
|
const updateWalkStateCreature = (creature, allCreatures) => {
|
||||||
const newPositionX = creature.positionX + (creature.walkDirection?.at(0) ?? 0) * WALK_SIZE;
|
// always play for at least 8 frames
|
||||||
const newPositionY = creature.positionY + (creature.walkDirection?.at(1) ?? 0) * WALK_SIZE;
|
if (creature.stateDuration < 8 || !rollForNatN(10)) {
|
||||||
const newPositionCreature = {
|
|
||||||
...creature,
|
|
||||||
positionX: newPositionX,
|
|
||||||
positionY: newPositionY,
|
|
||||||
stateDuration: creature.stateDuration + 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
const wallCollisionDirection = isCreatureOutOfBounds({
|
|
||||||
...creature, positionX: newPositionX, positionY: newPositionY
|
|
||||||
})
|
|
||||||
|
|
||||||
if (wallCollisionDirection !== undefined) {
|
|
||||||
return {
|
return {
|
||||||
...creature,
|
...creature,
|
||||||
state: WALL_DIRECTION_TO_SCRATCH_STATE[wallCollisionDirection] ?? CreatureState.IDLE,
|
positionX: creature.positionX + (creature.walkDirection?.at(0) ?? 0) * WALK_SIZE,
|
||||||
walkDirection: undefined,
|
positionY: creature.positionY + (creature.walkDirection?.at(1) ?? 0) * WALK_SIZE,
|
||||||
stateDuration: 0,
|
stateDuration: creature.stateDuration + 1,
|
||||||
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 };
|
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)
|
.filter((otherCreature) => otherCreature.name !== creature.name)
|
||||||
|
|
||||||
|
// move away from groups of 2 or more creatures
|
||||||
if (spottedCreatures.length > 1) {
|
if (spottedCreatures.length > 1) {
|
||||||
// run away from the centroid of close creatures
|
// run away from the centroid of close creatures
|
||||||
const centroidX = spottedCreatures.reduce(
|
const centroidX = spottedCreatures.reduce(
|
||||||
|
@ -629,28 +571,17 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
|
||||||
(sum, spottedCreature) => sum + spottedCreature.positionY, 0
|
(sum, spottedCreature) => sum + spottedCreature.positionY, 0
|
||||||
) / spottedCreatures.length;
|
) / spottedCreatures.length;
|
||||||
return startCreatureWalkTowardDirection(creature, [
|
return startCreatureWalkTowardDirection(creature, [
|
||||||
creature.positionX - centroidX,
|
2 * creature.positionX - centroidX,
|
||||||
creature.positionY - centroidY,
|
2 * creature.positionY - centroidY,
|
||||||
])
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// move toward spotted creature, so long as doing so won't cause collision
|
// if there's a creature in the view radius, move toward it 25% of the time
|
||||||
const updatedTowardSpottedCreature = spottedCreatures
|
if (spottedCreatures.length > 0 && rollForNatN(4)) {
|
||||||
.map((otherCreature) => {
|
const spottedCreature = /** @type {Creature} */ getRandomChoice(undefined, spottedCreatures);
|
||||||
const updatedCreature = startCreatureWalkTowardDirection(creature, [
|
return startCreatureWalkTowardDirection(creature, [
|
||||||
otherCreature.positionX, otherCreature.positionY,
|
spottedCreature.positionX, spottedCreature.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(
|
const newState = getRandomChoice(
|
||||||
|
@ -660,7 +591,7 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
|
||||||
CreatureState.SCRATCH_SELF, // 0.1
|
CreatureState.SCRATCH_SELF, // 0.1
|
||||||
CreatureState.TIRED, // 0.1
|
CreatureState.TIRED, // 0.1
|
||||||
CreatureState.WALK_NORTH, // 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.clientLeft, creature.container.clientLeft + creature.container.clientWidth),
|
||||||
getRandomInRange(creature.container.clientTop, creature.container.clientTop + creature.container.clientHeight),
|
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) => {
|
[CreatureState.ALERT]: (creature, allCreatures) => {
|
||||||
if (creature.stateDuration < 3) {
|
if (creature.stateDuration < 3) {
|
||||||
|
@ -715,7 +635,7 @@ const CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION = Object.freeze({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newState === CreatureState.WALK_NORTH) {
|
if (newState === CreatureState.WALK_NORTH) {
|
||||||
// walk toward center with some random client
|
// walk toward random direction
|
||||||
return startCreatureWalkTowardDirection(creature, [
|
return startCreatureWalkTowardDirection(creature, [
|
||||||
getRandomInRange(creature.container.clientLeft, creature.container.clientLeft + creature.container.clientWidth),
|
getRandomInRange(creature.container.clientLeft, creature.container.clientLeft + creature.container.clientWidth),
|
||||||
getRandomInRange(creature.container.clientTop, creature.container.clientTop + creature.container.clientHeight),
|
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,
|
[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<Creature>} 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 = () => {
|
window.onload = () => {
|
||||||
if (
|
if (
|
||||||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
|
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches
|
||||||
|
@ -793,7 +779,6 @@ window.onload = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXAMPLE SCRIPT
|
|
||||||
const kennelWindowEle = document.querySelector('#kennel-window');
|
const kennelWindowEle = document.querySelector('#kennel-window');
|
||||||
const creatures = Array(32).fill('test-creature')
|
const creatures = Array(32).fill('test-creature')
|
||||||
.map((name, index) => createCreature(
|
.map((name, index) => createCreature(
|
||||||
|
@ -809,9 +794,12 @@ window.onload = () => {
|
||||||
getRandomInRange(kennelWindowEle.clientTop, kennelWindowEle.clientTop + kennelWindowEle.clientHeight),
|
getRandomInRange(kennelWindowEle.clientTop, kennelWindowEle.clientTop + kennelWindowEle.clientHeight),
|
||||||
))
|
))
|
||||||
|
|
||||||
const updateCreature = updateCreatureWithAllCreaturesContext((creature, allCreatures) => {
|
const updateCreature = updateCreatureWithContext((creature, allCreatures) => {
|
||||||
const updatedCreature = CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION[creature.state](creature, allCreatures);
|
return resolveCollision(
|
||||||
return updatedCreature
|
CREATURE_STATE_TO_UPDATE_CREATURE_FUNCTION[creature.state](creature, allCreatures),
|
||||||
|
allCreatures,
|
||||||
|
creature,
|
||||||
|
);
|
||||||
}, creatures);
|
}, creatures);
|
||||||
|
|
||||||
creatures.forEach((creature) => beginCreatureAnimation(creature, updateCreature))
|
creatures.forEach((creature) => beginCreatureAnimation(creature, updateCreature))
|
||||||
|
|
Loading…
Reference in New Issue