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 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<Creature>} 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<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 = () => {
|
||||
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))
|
||||
|
|
Loading…
Reference in New Issue