import type { PropertyValue } from '../../model/Properties'; import type { PropertyDef } from '../../properties/PropertySchema'; import type { NodeDef, PortDef } from '../NodeRegistry'; import { asBoolean, asNumber } from '../values'; export const exec = (key = 'exec', label = ''): PortDef => ({ key, label, type: 'exec' }); export const num = (key: string, label = key): PortDef => ({ key, label, type: 'number' }); export const bool = (key: string, label = key): PortDef => ({ key, label, type: 'boolean' }); export const str = (key: string, label = key): PortDef => ({ key, label, type: 'string' }); export const obj = (key: string, label = key): PortDef => ({ key, label, type: 'object' }); export const list = (key: string, label = key): PortDef => ({ key, label, type: 'list' }); export const struct = (key: string, label = key): PortDef => ({ key, label, type: 'struct' }); export const any = (key: string, label = key): PortDef => ({ key, label, type: 'any' }); export const withDefault = (port: PortDef, value: PropertyValue): PortDef => ({ ...port, default: value }); /** A boolean input that shows a check box while it is not connected. */ export const checkPort = (key: string, label: string, fieldLabel: string, value = false): PortDef => ({ key, label, type: 'boolean', default: value, field: { key, label: fieldLabel, kind: 'boolean', default: value }, }); /** A whole-number input (an index, a count): its field shows no decimals, the node floors it anyway. */ export const int = (key: string, value = 0, label = key): PortDef => ({ key, label, type: 'number', default: value, field: { key, label, kind: 'number', default: value, step: 1, decimals: 0 }, }); /** Outputs of the three collision events: the other body, its collider and the own collider. */ export const collisionOutputs = (): PortDef[] => [ exec(), obj('other'), obj('otherCollider', 'other collider'), obj('collider'), ]; /** * How a contact touches: which way the other side pushes (a unit vector pointing from it towards the * own collider), how hard, and the middle of the contact points in the world. The physics engine * works them out (#146, #149). */ export const contactOutputs = (): PortDef[] => [ num('normalX', 'normal x'), num('normalY', 'normal y'), num('impulse'), num('pointX', 'point x'), num('pointY', 'point y'), ]; /** A tile asset; unconnected as an input, it shows the tile picker. */ export const tilePort = (key = 'tile', label = key): PortDef => ({ key, label, type: 'string', field: { key, label: 'Tile', kind: 'asset', default: '', assetTypes: ['tile'] }, }); export const PER_CELL_INPUT = 'perCell'; /** * Which pairs a collision event hears of a map's trigger tiles (#160): off, one per area of a tile – * a pond is entered once –, on, one per cell. */ export const perCellPort = (): PortDef => checkPort(PER_CELL_INPUT, 'per cell', 'Per cell (trigger tiles)'); /** * The trigger tile a collider lies on (#160) and its cell in the map, which is then `other`. No * tile and cell -1, -1 for anything else: another collider, or a map's solid cells. */ export const tileOutputs = (): PortDef[] => [tilePort(), num('cellX', 'cell x'), num('cellY', 'cell y')]; export const keyParam: PropertyDef = { key: 'key', label: 'Key', kind: 'key', default: 'ArrowRight' }; /** No `assetTypes`: any placeable asset is a valid answer here. */ const assetParam: PropertyDef = { key: 'asset', label: 'Asset', kind: 'asset', default: '' }; /** * The `asset` input of `Spawn` and `Is asset`; unconnected, it shows the asset picker. The key is the * one the setting had before it became a port, so a saved pick is the inline value now. */ export const assetPort = (): PortDef => ({ key: 'asset', label: 'asset', type: 'string', field: assetParam }); /** * The `sound` input of the two sound nodes; unconnected, it shows the sound picker – the way * `machinePort` shows the machine picker. A `Sound` literal is what feeds it through an edge. */ export const soundPort = (): PortDef => ({ key: 'sound', label: 'sound', type: 'string', field: { key: 'sound', label: 'Sound', kind: 'sound', default: '' }, }); /** * The `fx` input of a Play node (#311); unconnected, it shows the picker of sound FX assets. Empty is * the one its asset names, `NO_FX` none (#352). */ export const soundFxPort = (): PortDef => ({ key: 'fx', label: 'fx', type: 'string', field: { key: 'fx', label: 'FX', kind: 'asset', default: '', assetTypes: ['soundfx'], none: 'From the asset', noFx: true, }, }); /** * The `bus` input of a node that plays or reaches a bus (#325). `none` is what an empty pick means * there; `master` offers the master as a pick of its own where empty means something else. */ export const busPort = (none: string, master: boolean): PortDef => ({ key: 'bus', label: 'bus', type: 'string', field: { key: 'bus', label: 'Bus', kind: 'asset', default: '', assetTypes: ['bus'], none, master }, }); /** The `bus` input of a Play node: empty is the bus its asset names. */ export const playedBusPort = (): PortDef => busPort('From the asset', true); /** * The way out of the ownership rule, off by default. Needed for one shape that is common enough to * deserve it: an object that plays its own death sound and destroys itself in the same flow – the * voice would be cut off in the instant it started, because the behavior holding it is gone. */ export const detachedPort = (): PortDef => checkPort('detached', 'detached', 'Detached (outlives this object)'); /** Values for the parameters of a sound FX or an instrument, bundled by their node (#311, #331). */ export const fxParamsPort = (key = 'fxParams', label = 'fx params'): PortDef => ({ key, label, type: 'fxparams', }); /** Whether a Stop node lets what it stops fall away in its release instead of cutting it. */ export const letRingPort = (): PortDef => checkPort('letRing', 'let ring', 'Let ring'); export function binaryMath(type: string, description: string, fn: (a: number, b: number) => number): NodeDef { return { type, label: type, category: 'data', description, inputs: [num('a'), num('b')], outputs: [num('result')], params: [], run: (_ctx, inputs) => ({ outputs: { result: fn(asNumber(inputs.a), asNumber(inputs.b)) } }), }; } export function unaryMath(type: string, description: string, fn: (value: number) => number): NodeDef { return { type, label: type, category: 'data', description, inputs: [num('value')], outputs: [num('result')], params: [], run: (_ctx, inputs) => ({ outputs: { result: fn(asNumber(inputs.value)) } }), }; } export function binaryLogic( type: string, description: string, fn: (a: boolean, b: boolean) => boolean, ): NodeDef { return { type, label: type, category: 'data', description, inputs: [bool('a'), bool('b')], outputs: [bool('result')], params: [], run: (_ctx, inputs) => ({ outputs: { result: fn(asBoolean(inputs.a), asBoolean(inputs.b)) } }), }; } /** Scales (x, y) to length 1; the zero vector stays zero. */ export function normalize(x: number, y: number): { x: number; y: number; length: number } { const length = Math.hypot(x, y); return length === 0 ? { x: 0, y: 0, length: 0 } : { x: x / length, y: y / length, length }; }