Release Healer Man 0.1.21
This commit is contained in:
1 parent
699fc858c9
commit
f6f2375145
210 files changed
+976039
-6247
No files matched your search
@@ -0,0 +1,137 @@
|
||||
import type { MobVector3 } from "./mobPopulation";
|
||||
|
||||
export type DungeonWorldObjectKind =
|
||||
| "door"
|
||||
| "gate"
|
||||
| "bridge"
|
||||
| "switch"
|
||||
| "key"
|
||||
| "chest"
|
||||
| "npc"
|
||||
| "quest-object"
|
||||
| "trigger";
|
||||
|
||||
export type DungeonWorldObjectFidelity =
|
||||
| "source-exact"
|
||||
| "source-backed-adapted"
|
||||
| "healer-man-authored";
|
||||
|
||||
export interface DungeonWorldObjectSource {
|
||||
readonly fidelity: DungeonWorldObjectFidelity;
|
||||
readonly files: readonly string[];
|
||||
readonly templateId?: number;
|
||||
readonly spawnId?: number;
|
||||
readonly note?: string;
|
||||
}
|
||||
|
||||
export interface DungeonWorldObjectStateDefinition {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly visible: boolean;
|
||||
readonly blocksMovement: boolean;
|
||||
readonly blocksSight?: boolean;
|
||||
readonly interactionEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface DungeonWorldObjectInteraction {
|
||||
readonly prompt: string;
|
||||
readonly range: number;
|
||||
readonly signalId: string;
|
||||
readonly allowedStates?: readonly string[];
|
||||
readonly oneShot?: boolean;
|
||||
readonly instructions?: string;
|
||||
}
|
||||
|
||||
export interface DungeonWorldObjectVisual {
|
||||
/** Optional independently packaged GLB. Procedural geometry remains the safe fallback. */
|
||||
readonly assetUrl?: string;
|
||||
readonly color?: string;
|
||||
readonly emissive?: string;
|
||||
readonly scale?: MobVector3;
|
||||
}
|
||||
|
||||
export interface DungeonWorldObjectDefinition {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly kind: DungeonWorldObjectKind;
|
||||
readonly position: MobVector3;
|
||||
readonly yaw?: number;
|
||||
/** Half-extents of the interaction/collision box in metres. */
|
||||
readonly halfExtents: MobVector3;
|
||||
readonly initialState: string;
|
||||
readonly states: readonly DungeonWorldObjectStateDefinition[];
|
||||
readonly interaction?: DungeonWorldObjectInteraction;
|
||||
readonly visual?: DungeonWorldObjectVisual;
|
||||
readonly source?: DungeonWorldObjectSource;
|
||||
}
|
||||
|
||||
export interface DungeonWorldInteractionCandidate {
|
||||
readonly object: DungeonWorldObjectDefinition;
|
||||
readonly state: DungeonWorldObjectStateDefinition;
|
||||
readonly distance: number;
|
||||
}
|
||||
|
||||
function finiteDistance(left: MobVector3, right: MobVector3): number {
|
||||
if (!left.every(Number.isFinite) || !right.every(Number.isFinite)) return Number.POSITIVE_INFINITY;
|
||||
return Math.hypot(left[0] - right[0], left[1] - right[1], left[2] - right[2]);
|
||||
}
|
||||
|
||||
export function dungeonWorldObjectState(
|
||||
object: DungeonWorldObjectDefinition,
|
||||
stateId: string | undefined,
|
||||
): DungeonWorldObjectStateDefinition {
|
||||
const requested = stateId ?? object.initialState;
|
||||
return object.states.find((state) => state.id === requested)
|
||||
?? object.states.find((state) => state.id === object.initialState)
|
||||
?? {
|
||||
id: object.initialState,
|
||||
label: object.initialState,
|
||||
visible: true,
|
||||
blocksMovement: false,
|
||||
interactionEnabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function validateDungeonWorldObject(object: DungeonWorldObjectDefinition): void {
|
||||
if (!object.id.trim()) throw new Error("Dungeon world objects require a stable id.");
|
||||
if (!object.name.trim()) throw new Error(`Dungeon world object ${object.id} requires a name.`);
|
||||
if (!object.position.every(Number.isFinite) || !object.halfExtents.every((value) => Number.isFinite(value) && value > 0)) {
|
||||
throw new Error(`Dungeon world object ${object.id} has invalid geometry.`);
|
||||
}
|
||||
const stateIds = new Set<string>();
|
||||
for (const state of object.states) {
|
||||
if (!state.id.trim() || stateIds.has(state.id)) {
|
||||
throw new Error(`Dungeon world object ${object.id} has an invalid or duplicate state id.`);
|
||||
}
|
||||
stateIds.add(state.id);
|
||||
}
|
||||
if (!stateIds.has(object.initialState)) {
|
||||
throw new Error(`Dungeon world object ${object.id} does not define initial state ${object.initialState}.`);
|
||||
}
|
||||
if (object.interaction && (!object.interaction.signalId.trim() || object.interaction.range <= 0)) {
|
||||
throw new Error(`Dungeon world object ${object.id} has an invalid interaction.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function nearestDungeonWorldInteraction(
|
||||
objects: readonly DungeonWorldObjectDefinition[],
|
||||
states: Readonly<Record<string, string>>,
|
||||
origin: MobVector3,
|
||||
hasLineOfSight: (from: MobVector3, to: MobVector3) => boolean = () => true,
|
||||
): DungeonWorldInteractionCandidate | null {
|
||||
let nearest: DungeonWorldInteractionCandidate | null = null;
|
||||
for (const object of objects) {
|
||||
const interaction = object.interaction;
|
||||
if (!interaction) continue;
|
||||
const state = dungeonWorldObjectState(object, states[object.id]);
|
||||
if (!state.visible) continue;
|
||||
if (state.interactionEnabled === false) continue;
|
||||
if (interaction.allowedStates?.length && !interaction.allowedStates.includes(state.id)) continue;
|
||||
const distance = finiteDistance(origin, object.position);
|
||||
if (distance > interaction.range || !hasLineOfSight(origin, object.position)) continue;
|
||||
if (!nearest || distance < nearest.distance || (distance === nearest.distance && object.id < nearest.object.id)) {
|
||||
nearest = { object, state, distance };
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
Reference in new issue
Block a user