Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
12 kB · 427 lines
TypeScript
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428/* eslint-disable eqeqeq */import type { ComponentQuery, EntityQuery, ValueQuery,} from "@thorium/.server/classes/Plugins/TimelineStep";import { pubsub } from "@thorium/.server/init/pubsub";import { executeBlocks } from "@thorium/utils/.server/executeBlocks";import { getPluginTextPatterns, interpolateText } from "@thorium/utils/interpolationEngine";import type z from "zod";
import type { ECS, Entity } from "../ecs";import type { actionItem, conditionSchema } from "../flags/actionSchema";import { getNavigationDistance } from "../starmap/getNavigationDistance";import { getCompletePositionFromOrbit, getObjectSystem } from "../starmap/position";import { lightMinuteToKilometer, lightYearToLightMinute } from "../unitTypes";
export function evaluateEntityQuery(ecs: ECS, query: EntityQuery): Entity[] { const output: Entity[] = []; const entitySet = new Set<Entity>(); for (const componentQuery of query) { const entities = ecs.componentCache.get(componentQuery.component as any); if (!entities) continue; for (const entity of entities) { entitySet.add(entity); } } for (const entity of entitySet) { let match = true; for (const componentQuery of query) { const evaluation = evaluateComponentQuery(ecs, entity, componentQuery); if (evaluation == undefined) continue; match = evaluation; if (!match) break; } if (match) { output.push(entity); } } return output;}
function evaluateComponentQuery(ecs: ECS, entity: Entity, componentQuery: ComponentQuery) { if (!componentQuery || !componentQuery.component || !componentQuery.property) { // Ignore it if it's undefined return; } const component = entity.components[componentQuery.component]; // This is the only case where we allow the component to not exist if (componentQuery.property === "isNotPresent" && !component) { return; } if (!component) { return false; } if (componentQuery.property === "isPresent") { return; } // @ts-expect-error const property = component[componentQuery.property]; let value: any = componentQuery.value; if (typeof value === "object" && "query" in value) { value = selectValueQuery(ecs, value); if (value.length === 0) { return false; } if (value.length === 1) { value = value[0]; } else { // When there are multiple values, we just check and see if any of them evaluate return value.some((v: any) => evaluateComponentQuery(ecs, entity, { ...componentQuery, value: v, }), ); } }
// Use double equals to coerce values if (componentQuery.comparison) { if (componentQuery.comparison === "contains") { if (property.includes(value)) { return true; } } if (componentQuery.comparison === "length") { if (property.length == value) { return true; } } if (componentQuery.comparison === "true") { if (property === true) { return true; } } if (componentQuery.comparison === "false") { if (property === false) { return true; } } if (componentQuery.comparison === "=") { if (property == value) { return true; } } else if (componentQuery.comparison === "!=") { if (property != value) { return true; } } else if (componentQuery.comparison === ">") { if (property > value) { return true; } } else if (componentQuery.comparison === "<") { if (property < value) { return true; } } else if (componentQuery.comparison === ">=") { if (property >= value) { return true; } } else if (componentQuery.comparison === "<=") { if (property <= value) { return true; } } else if (componentQuery.comparison === "is empty") { if (!property || (Array.isArray(property) && property.length === 0)) { return true; } } else if (componentQuery.comparison === "is not empty") { if (!property || (Array.isArray(property) && property.length === 0)) { } else { return true; } } } else { if (property == value) { return true; } } return false;}
export function selectValueQuery(ecs: ECS, entityQuery: ValueQuery): any[] { const entities = evaluateEntityQuery(ecs, entityQuery.query); if (entities.length === 0) return []; if (entityQuery.select) { const values = entities .map((e) => // @ts-expect-error entityQuery.select.component === "id" ? e.id : // @ts-expect-error e.components[entityQuery.select.component]?.[entityQuery.select.property], ) .filter((t: any) => t !== undefined);
if (entityQuery.select.matchType === "all") { return values; } if (entityQuery.select.matchType === "first") { return [values[0]]; } if (entityQuery.select.matchType === "random") { return [values[Math.floor(Math.random() * entities.length)]]; } } return [];}
export function evaluateTriggerCondition( ecs: ECS, conditions: z.infer<typeof conditionSchema>[], event?: { event: string; values: any },) { let match: any = true; for (const condition of conditions) { if (condition.type === "eventListener") { if (event?.event === condition.event) { // Check arg values if (condition.values) { for (const key in condition.values) { let conditionValue = condition.values[key];
if ( conditionValue && typeof conditionValue === "object" && "query" in conditionValue && typeof conditionValue.query === "object" && "select" in conditionValue ) { conditionValue = selectValueQuery(ecs, conditionValue as any); if (conditionValue.length === 0) return false; let conditionMatch = false; for (const value of conditionValue) { if (event.values[key] === value) { conditionMatch = event.values; break; } } if (!conditionMatch) { match = false; break; } } else if (event.values[key] != conditionValue) { match = false; break; } } } if (match) { match = event.values; } } else { match = false; break; } } if (condition.type === "distance") { const entityA = ecs.getEntityById(condition.entityA); const entityB = ecs.getEntityById(condition.entityB); if (!entityA || !entityB) { match = false; break; } const distance = getEntityDistance([entityA], [entityB], condition.condition); if (condition.condition === "less than") { if (distance > condition.distance) { match = false; break; } } else { if (distance < condition.distance) { match = false; break; } } } if (condition.type === "entityMatch") { const entities = evaluateEntityQuery(ecs, condition.query as any); if (condition.matchCount === "any") { if (entities.length < 1) { match = false; break; } match = entities; } else if (condition.matchCount === "no" && entities.length !== 0) { match = false; break; } else if (condition.matchCount === "one") { if (entities.length !== 1) { match = false; break; } match = entities[0]; } } } return match;}
function getEntityDistance( entityA: Entity[], entityB: Entity[], condition: "less than" | "more than",) { // Calculate positions const positionsA = entityA.map(getEntityPosition).filter(Boolean); const positionsB = entityB.map(getEntityPosition).filter(Boolean);
const distances: number[] = []; for (const a of positionsA) { for (const b of positionsB) { if (!a || !b) continue; if ((a.type === "ship" && b.type !== "ship") || (a.type !== "ship" && b.type === "ship")) continue; if (a.type === "ship" && b.type === "ship" && a.parentObject !== b.parentObject) continue; let distance = 0; if (a.type === "ship" && b.type === "ship") { distance = Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z); } else { const distanceOutput = getNavigationDistance(a, b, a.parentObject, b.parentObject); if (!distanceOutput) continue; distance = distanceOutput.unit === "LY" ? lightMinuteToKilometer(lightYearToLightMinute(distanceOutput.distance)) : distanceOutput.distance; } distances.push(distance); } }
if (condition === "less than") { return Math.min(...distances); } return Math.max(...distances);}
function getEntityPosition(e: Entity) { if (e.components.position) { let parentObject = null; const { x, y, z, type } = e.components.position; if (e.components.position.type === "solar") { parentObject = getObjectSystem(e); } if (e.components.position.type === "ship" && e.components.position.parentId) { parentObject = e.ecs?.getEntityById(e.components.position.parentId) || null; } let parentPosition: { id: number; x: number; y: number; z: number } | null | undefined = parentObject?.components.position ? { id: parentObject.id, ...parentObject.components.position } : null; if (parentObject && !parentPosition) { parentPosition = parentObject ? { id: parentObject.id, ...getCompletePositionFromOrbit(parentObject) } : null; } return { x, y, z, type, parentObject: parentPosition }; } if (e.components.satellite) { const { x, y, z } = getCompletePositionFromOrbit(e); const parentObject = getObjectSystem(e); let parentPosition: { id: number; x: number; y: number; z: number } | null | undefined = parentObject?.components.position ? { id: parentObject.id, ...parentObject.components.position } : null; if (parentObject && !parentPosition) { parentPosition = parentObject ? { id: parentObject.id, ...getCompletePositionFromOrbit(parentObject) } : null; }
return { type: "solar", x, y, z, parentObject: parentPosition }; } return null;}
// Action evaluator and executor.export function evaluateAction(ecs: ECS, action: z.infer<typeof actionItem>) { // Based on the results of the entity queries, we might execute this // action multiple times with different values. const actionValues: Map<string, Set<any>> = new Map(); for (const [name, value] of Object.entries(action.values)) { if (!actionValues.get(name)) actionValues.set(name, new Set()); if (typeof value === "object" && "query" in value) { const values = selectValueQuery(ecs, value as any); for (const v of values) { actionValues.get(name)?.add(v); } } else { actionValues.get(name)?.add(value); } } // Generate every permutation of the values const values: any[] = generatePermutations(actionValues);
return values;}
function generatePermutations(inputMap: Map<string, Set<any>>) { const keys = Array.from(inputMap.keys()); const permutations: any[] = [];
function permute(index: number, current: any) { if (index === keys.length) { permutations.push(current); return; }
const key = keys[index]; const valueSet = inputMap.get(key); if (!valueSet) return; for (const value of valueSet) { const updated = { ...current }; updated[key] = value; permute(index + 1, updated); } }
permute(0, {});
return permutations;}
export async function triggerStep(step: Entity) { const timeline = step.ecs.getEntityById(step.components.isTimelineStep?.timelineId || -1); const localVariables = timeline?.components.variables?.variables.reduce((prev: Record<string, any>, next) => { prev[next.name] = next.value; return prev; }, {}) || {};
const blocks = step?.components.isTimelineStep?.blocks; if (!blocks) return; step.updateComponent("isTimelineStep", { state: "executing" }); const { waiting } = await executeBlocks(step.ecs, blocks, { stepId: step.id, localVariables }); step.updateComponent("identity", { description: interpolateText( step.components.identity?.description || "", localVariables, getPluginTextPatterns(step.ecs.server), step.ecs.rng, ), }); // If there are no triggers associated with this timeline step, then we can consider it executed let hasTrigger = false;
for (const entity of step.ecs.componentCache.get("isTrigger") || []) { if ( entity.components.isTrigger?.stepId === step.id && entity.components.isTrigger.triggeredAt === null ) { hasTrigger = true; break; } }
if (!hasTrigger && !waiting) { step.updateComponent("isTimelineStep", { state: "executed" }); }
if (timeline?.components.isTimeline?.type === "report") { pubsub.publish.damageReports.damageReports({ shipId: timeline?.components.isTimeline?.shipId || -1, }); }}