Refactor entities

This commit is contained in:
2026-08-06 08:13:15 +02:00
parent aafbdc658e
commit f641ad5f43
28 changed files with 2577 additions and 2387 deletions
+207
View File
@@ -0,0 +1,207 @@
import { compact } from 'lodash';
import {
Entity, PaletteManager, ServerFlags, PaletteRenderable, EntityDescriptor, EntityOptions,
CreateEntityMethod, CreateEntity, EntityFlags, MixinEntity, EntityWorldState, defaultWorldState, InteractAction
} from '../interfaces';
import { ENTITY_TYPE_LIMIT } from '../constants';
import {
setPaletteManager, ponyColliders, ponyCollidersBounds, mixTrigger, mixDraw, mixInteract
} from '../mixins';
import { times } from '../utils';
import { rect } from '../rect';
export const entities: EntityDescriptor[] = [];
export function createBaseEntity(type: number, id: number, x: number, y: number): Entity {
return { id, type, x, y, z: 0, vx: 0, vy: 0, depth: 0, order: 0, state: 0, playerState: 0, flags: 0, timestamp: 0 };
}
function createEntity(
type: number, id: number, x: number, y: number, options: EntityOptions, worldState: EntityWorldState
): Entity {
const descriptor = entities[type];
if (!descriptor) {
throw new Error(`Invalid entity type ${type}`);
}
return descriptor.create(createBaseEntity(type, id, x, y), options, worldState);
}
export function register(typeName: string, create: CreateEntity): CreateEntityMethod {
if (DEVELOPMENT && entities.length >= ENTITY_TYPE_LIMIT) {
throw new Error(`Exceeded entity limit of ${ENTITY_TYPE_LIMIT} with (${typeName})`);
}
if (DEVELOPMENT && entities.some(e => e.typeName === typeName)) {
throw new Error(`Entity name already registered (${typeName})`);
}
const type = entities.length;
entities.push({ type, typeName, create });
const method: any = (x: number, y: number, options: EntityOptions = {}, worldState: EntityWorldState = defaultWorldState) =>
createEntity(type, 0, x, y, options, worldState);
method.type = type;
method.typeName = typeName;
return method;
}
export function registerMix(typeName: string, ...mixins: (MixinEntity | undefined)[]) {
const mixinsCompacted = compact(mixins);
return register(typeName, (base, options, worldState) => {
for (const mixin of mixinsCompacted) {
mixin(base, options, worldState);
}
return base;
});
}
export function getEntityTypeName(type: number): string {
return entities[type].typeName;
}
export function getEntityType(typeName: string): number {
for (let i = 1; i < entities.length; i++) {
if (entities[i].typeName === typeName) {
return i;
}
}
return 0;
}
export function getEntityTypesAndNames() {
return entities.map(({ type, typeName }) => ({ type, name: typeName }));
}
function checkEntity(entity: Entity) {
if (entity.draw && !entity.bounds) {
console.error('missing bounds for', getEntityTypeName(entity.type), entity);
}
if (entity.drawLight && !entity.lightBounds) {
console.error('missing lightBounds for', getEntityTypeName(entity.type), entity);
}
if (entity.drawLightSprite && !entity.lightSpriteBounds) {
console.error('missing lightSpriteBounds for', getEntityTypeName(entity.type), entity);
}
}
export function createAnEntity(
type: number, id: number, x: number, y: number, options: any, paletteManager: PaletteManager,
worldState: EntityWorldState
): Entity {
setPaletteManager(paletteManager);
const entity = createEntity(type, id, x, y, options, worldState);
if (DEVELOPMENT) {
checkEntity(entity);
}
return entity;
}
// helpers
// strips names in release build
export function n(value: string) {
return (DEVELOPMENT || SERVER) ? value : '';
}
export function mixCover(x: number, y: number, w: number, h: number): MixinEntity {
const bounds = rect(x, y, w, h);
return base => base.coverBounds = bounds;
}
export function mixFlags(flags: EntityFlags): MixinEntity {
return base => base.flags |= flags;
}
export function mixInteractAction(action: InteractAction): MixinEntity {
return base => base.interactAction = action;
}
export function mixBounds(x: number, y: number, w: number, h: number): MixinEntity {
const bounds = rect(x, y, w, h);
return base => base.bounds = bounds;
}
export function mixServerFlags(flags: ServerFlags): MixinEntity {
if (SERVER) {
return base => base.serverFlags! |= flags;
} else {
return () => { };
}
}
export function mixOrder(order: number): MixinEntity {
return base => base.order = order;
}
export const collectableInteractive = mixInteract(-8, -12, 16, 16, 1.5);
// entity with centered sprite and larger clickable area
export function collectable(name: string, sprite: PaletteRenderable, paletteIndex = 0, ...other: MixinEntity[]) {
return doodad(name, sprite, Math.floor(sprite.color!.w / 2), sprite.color!.h - 1, paletteIndex,
collectableInteractive,
...other);
}
// for ground details. puts origin point on the top of sprite, so you always go on top of it. collectables are able to spawn overlapping decals
export function decal(name: string, sprite: PaletteRenderable, palette = 0, ...other: MixinEntity[]) {
return registerMix(name,
mixDraw(sprite, Math.floor((sprite.color!.w + sprite.color!.ox) / 2), sprite.color!.oy, palette),
mixFlags(EntityFlags.Decal),
...other);
}
export function decalOffset(name: string, sprite: PaletteRenderable, dx: number, dy: number, palette = 0, ...other: MixinEntity[]) {
return registerMix(name,
mixDraw(sprite, dx, dy, palette),
mixFlags(EntityFlags.Decal),
...other);
}
// for decorative objects
export function doodad(
name: string, sprite: PaletteRenderable, ox: number, oy: number, palatte = 0, ...other: (MixinEntity | undefined)[]
) {
return registerMix(name, mixDraw(sprite, ox, oy, palatte), ...compact(other));
}
export function doodadSet(name: string, sprite: PaletteRenderable, ox: number, oy: number, ...other: MixinEntity[]) {
return times(sprite.palettes!.length, i => registerMix(`${name}-${i}`, mixDraw(sprite, ox, oy, i), ...other));
}
// placeholder entity
registerMix(n('null'), () => { throw new Error('Invalid type (0)'); });
// entities
export const pony = registerMix(n('pony'),
base => {
base.flags = EntityFlags.Movable | EntityFlags.CanCollide;
base.colliders = ponyColliders;
base.collidersBounds = ponyCollidersBounds;
},
mixServerFlags(ServerFlags.DoNotSave));
// triggers
export const triggerDoor = registerMix(n('trigger-door'),
mixTrigger(-32, -6, 64, 12, true));
export const triggerHouseDoor = registerMix(n('trigger-house-door'),
mixTrigger(-32, -6, 64, 12, false));
export const triggerBoat = registerMix(n('trigger-boat'),
mixTrigger(-50, -12, 100, 24, false));
export const trigger3x1 = registerMix(n('trigger-3x1'),
mixTrigger(-48, 0, 96, 24, true));