Created
October 13, 2025 18:54
-
-
Save goldenratio/6143fc6a07f5755b29cde9f20b15c7d2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| // @ts-expect-error don't want to setup node/npm | |
| import fs from "node:fs"; | |
| // @ts-expect-error don't want to setup node/npm | |
| import path from "node:path"; | |
| // @ts-expect-error don't want to setup node/npm | |
| import { fileURLToPath } from "node:url"; | |
| const _filename = fileURLToPath(import.meta.url); | |
| const _dirname = path.dirname(_filename); | |
| // @ts-expect-error don't want to setup node/npm | |
| const config_input = process?.argv[2]; | |
| if (typeof config_input === "string") { | |
| const pkgPath = path.resolve(config_input); | |
| const config_json = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); | |
| const upgraded_config = JSON.parse(JSON.stringify(upgradeConfig(config_json))); | |
| console.log(JSON.stringify(upgraded_config, undefined, 2)); | |
| } | |
| /** | |
| * Emitter Config is based on pixi-particle | |
| * https://github.com/pixijs-userland/particle-emitter | |
| */ | |
| export interface EmitterConfig { | |
| /** | |
| * Default position to spawn particles from inside the parent container. | |
| */ | |
| readonly pos: { x: number; y: number }; | |
| /** | |
| * Random number configuration for picking the lifetime for each particle.. | |
| */ | |
| readonly lifetime: EmitterRandNumber; | |
| /** | |
| * How often to spawn particles. This is a value in seconds, so a value of 0.5 would be twice a second. | |
| */ | |
| readonly frequency: number; | |
| /** | |
| * How many particles to spawn at once, each time that it is determined that particles should be spawned. | |
| * If omitted, only one particle will spawn at a time. | |
| */ | |
| readonly particles_per_wave?: number; | |
| /** | |
| * How long to run the Emitter before it stops spawning particles. If omitted, runs forever (or until told to stop | |
| * manually). | |
| * @default -1 | |
| */ | |
| readonly emitter_lifetime?: number; | |
| /** | |
| * Maximum number of particles that can be alive at any given time for this emitter. | |
| * @default 20 | |
| */ | |
| readonly max_particles?: number; | |
| /** | |
| * If the emitter should start out emitting particles. If omitted, it will be treated as `true` and will emit particles | |
| * immediately. | |
| * @default true | |
| */ | |
| readonly emit?: boolean; | |
| /** | |
| * The list of behaviors to apply to this emitter. | |
| */ | |
| behaviors: BehaviorEntryType[]; | |
| } | |
| export interface EmitterConfigValue<TValue> { | |
| readonly time: number; | |
| readonly value: TValue; | |
| } | |
| export interface Point { | |
| readonly x: number; | |
| readonly y: number; | |
| } | |
| export type BehaviorEntryType = | |
| | { readonly type: "textureSingle"; readonly config: { readonly texture: string } } | |
| | { readonly type: "color"; readonly config: { readonly color: { readonly list: readonly EmitterConfigValue<string>[] } } } | |
| | { readonly type: "colorStatic"; readonly config: { readonly color: string } } | |
| | { readonly type: "scale"; readonly config: { readonly scale: { readonly list: readonly EmitterConfigValue<number>[] }; readonly minMult: number } } | |
| | { readonly type: "scaleStatic"; readonly config: EmitterRandNumber } | |
| | { readonly type: "moveSpeed"; readonly config: { readonly speed: { readonly list: readonly EmitterConfigValue<number>[] }; readonly minMult: number } } | |
| | { readonly type: "moveSpeedStatic"; readonly config: EmitterRandNumber } | |
| | { readonly type: "moveAcceleration"; readonly config: { readonly minStart: number; readonly maxStart: number; readonly rotate: boolean; readonly accel: Point; readonly maxSpeed: number } } | |
| | { readonly type: "alpha"; readonly config: { readonly alpha: { readonly list: readonly EmitterConfigValue<number>[] } } } | |
| | { readonly type: "alphaStatic"; readonly config: { readonly alpha: number } } | |
| | { readonly type: "noRotation"; readonly config: {} } | |
| // A Rotation behavior that handles starting rotation, rotation speed, and rotational acceleration. | |
| | { readonly type: "rotation"; readonly config: { readonly minStart: number; readonly maxStart: number; readonly minSpeed: number; readonly maxSpeed: number; readonly accel: number } } | |
| // A Rotation behavior that blocks all rotation caused by spawn settings, | |
| // by resetting it to the specified rotation (or 0). | |
| | { readonly type: "rotationStatic"; readonly config: EmitterRandNumber } | |
| // emitter configs | |
| | { readonly type: "spawnPoint"; readonly config: {} } | |
| | { readonly type: "spawnBurst"; readonly config: { readonly start: number; readonly spacing: number; } } | |
| | { | |
| readonly type: "spawnShape"; readonly config: | |
| | { readonly type: "rect"; readonly data: { readonly x: number; readonly y: number; readonly w: number; readonly h: number } } | |
| | { readonly type: "torus"; readonly data: { readonly x: number; readonly y: number; readonly radius: number; readonly innerRadius: number; readonly affectRotation: boolean } }; | |
| }; | |
| /** | |
| * Configuration for how to pick a random number (inclusive). | |
| */ | |
| export interface EmitterRandNumber { | |
| /** | |
| * Maximum pickable value. | |
| */ | |
| readonly max: number; | |
| /** | |
| * Minimum pickable value. | |
| */ | |
| readonly min: number; | |
| } | |
| function upgradeConfig(config: any, art: any = "particle"): EmitterConfig { | |
| // just ensure we aren't given any V3 config data | |
| if ('behaviors' in config) { | |
| return config; | |
| } | |
| const out: EmitterConfig = { | |
| lifetime: config.lifetime, | |
| // ease: config.ease, | |
| particles_per_wave: config.particlesPerWave, | |
| frequency: config.frequency, | |
| // spawn_chance: config.spawnChance, | |
| emitter_lifetime: config.emitterLifetime, | |
| max_particles: config.maxParticles, | |
| // add_at_back: config.addAtBack, | |
| pos: config.pos, | |
| emit: config.emit, | |
| // autoUpdate: config.autoUpdate, | |
| behaviors: [], | |
| }; | |
| // set up the alpha | |
| if (config.alpha) { | |
| if ('start' in config.alpha) { | |
| if (config.alpha.start === config.alpha.end) { | |
| if (config.alpha.start !== 1) { | |
| out.behaviors.push({ | |
| type: 'alphaStatic', | |
| config: { alpha: config.alpha.start }, | |
| }); | |
| } | |
| } | |
| else { | |
| const list = { | |
| list: [ | |
| { time: 0, value: config.alpha.start }, | |
| { time: 1, value: config.alpha.end }, | |
| ], | |
| }; | |
| out.behaviors.push({ | |
| type: 'alpha', | |
| config: { alpha: list }, | |
| }); | |
| } | |
| } | |
| else if (config.alpha.list.length === 1) { | |
| if (config.alpha.list[0].value !== 1) { | |
| out.behaviors.push({ | |
| type: 'alphaStatic', | |
| config: { alpha: config.alpha.list[0].value }, | |
| }); | |
| } | |
| } | |
| else { | |
| out.behaviors.push({ | |
| type: 'alpha', | |
| config: { alpha: config.alpha }, | |
| }); | |
| } | |
| } | |
| // acceleration movement | |
| if (config.acceleration && (config.acceleration.x || config.acceleration.y)) { | |
| let minStart: number; | |
| let maxStart: number; | |
| if ('start' in config.speed) { | |
| minStart = config.speed.start * (config.speed.minimumSpeedMultiplier ?? 1); | |
| maxStart = config.speed.start; | |
| } | |
| else { | |
| minStart = config.speed.list[0].value * ((config).minimumSpeedMultiplier ?? 1); | |
| maxStart = config.speed.list[0].value; | |
| } | |
| out.behaviors.push({ | |
| type: 'moveAcceleration', | |
| config: { | |
| accel: config.acceleration, | |
| minStart, | |
| maxStart, | |
| rotate: !config.noRotation, | |
| maxSpeed: config.maxSpeed, | |
| }, | |
| }); | |
| } | |
| // path movement | |
| else if (config.extraData?.path) { | |
| let list; | |
| let mult: number; | |
| if ('start' in config.speed) { | |
| mult = config.speed.minimumSpeedMultiplier ?? 1; | |
| if (config.speed.start === config.speed.end) { | |
| list = { | |
| list: [{ time: 0, value: config.speed.start }], | |
| }; | |
| } | |
| else { | |
| list = { | |
| list: [ | |
| { time: 0, value: config.speed.start }, | |
| { time: 1, value: config.speed.end }, | |
| ], | |
| }; | |
| } | |
| } | |
| else { | |
| list = config.speed; | |
| mult = ((config).minimumSpeedMultiplier ?? 1); | |
| } | |
| out.behaviors.push({ | |
| type: 'movePath', | |
| config: { | |
| path: config.extraData.path, | |
| speed: list, | |
| minMult: mult, | |
| }, | |
| }); | |
| } | |
| // normal speed movement | |
| else { | |
| if (config.speed) { | |
| if ('start' in config.speed) { | |
| if (config.speed.start === config.speed.end) { | |
| out.behaviors.push({ | |
| type: 'moveSpeedStatic', | |
| config: { | |
| min: config.speed.start * (config.speed.minimumSpeedMultiplier ?? 1), | |
| max: config.speed.start, | |
| }, | |
| }); | |
| } | |
| else { | |
| const list = { | |
| list: [ | |
| { time: 0, value: config.speed.start }, | |
| { time: 1, value: config.speed.end }, | |
| ], | |
| }; | |
| out.behaviors.push({ | |
| type: 'moveSpeed', | |
| config: { speed: list, minMult: config.speed.minimumSpeedMultiplier }, | |
| }); | |
| } | |
| } | |
| else if (config.speed.list.length === 1) { | |
| out.behaviors.push({ | |
| type: 'moveSpeedStatic', | |
| config: { | |
| min: config.speed.list[0].value * ((config).minimumSpeedMultiplier ?? 1), | |
| max: config.speed.list[0].value, | |
| }, | |
| }); | |
| } | |
| else { | |
| out.behaviors.push({ | |
| type: 'moveSpeed', | |
| config: { speed: config.speed, minMult: ((config).minimumSpeedMultiplier ?? 1) }, | |
| }); | |
| } | |
| } | |
| } | |
| // scale | |
| if (config.scale) { | |
| if ('start' in config.scale) { | |
| const mult = config.scale.minimumScaleMultiplier ?? 1; | |
| if (config.scale.start === config.scale.end) { | |
| out.behaviors.push({ | |
| type: 'scaleStatic', | |
| config: { | |
| min: config.scale.start * mult, | |
| max: config.scale.start, | |
| }, | |
| }); | |
| } | |
| else { | |
| const list = { | |
| list: [ | |
| { time: 0, value: config.scale.start }, | |
| { time: 1, value: config.scale.end }, | |
| ], | |
| }; | |
| out.behaviors.push({ | |
| type: 'scale', | |
| config: { scale: list, minMult: mult }, | |
| }); | |
| } | |
| } | |
| else if (config.scale.list.length === 1) { | |
| const mult = (config).minimumScaleMultiplier ?? 1; | |
| const scale = config.scale.list[0].value; | |
| out.behaviors.push({ | |
| type: 'scaleStatic', | |
| config: { min: scale * mult, max: scale }, | |
| }); | |
| } | |
| else { | |
| out.behaviors.push({ | |
| type: 'scale', | |
| config: { scale: config.scale, minMult: (config).minimumScaleMultiplier ?? 1 }, | |
| }); | |
| } | |
| } | |
| // color | |
| if (config.color) { | |
| if ('start' in config.color) { | |
| if (config.color.start === config.color.end) { | |
| if (config.color.start !== 'ffffff') { | |
| out.behaviors.push({ | |
| type: 'colorStatic', | |
| config: { color: config.color.start }, | |
| }); | |
| } | |
| } | |
| else { | |
| const list = { | |
| list: [ | |
| { time: 0, value: config.color.start }, | |
| { time: 1, value: config.color.end }, | |
| ], | |
| }; | |
| out.behaviors.push({ | |
| type: 'color', | |
| config: { color: list }, | |
| }); | |
| } | |
| } | |
| else if (config.color.list.length === 1) { | |
| if (config.color.list[0].value !== 'ffffff') { | |
| out.behaviors.push({ | |
| type: 'colorStatic', | |
| config: { color: config.color.list[0].value }, | |
| }); | |
| } | |
| } | |
| else { | |
| out.behaviors.push({ | |
| type: 'color', | |
| config: { color: config.color }, | |
| }); | |
| } | |
| } | |
| // rotation | |
| if (config.rotationAcceleration || config.rotationSpeed?.min || config.rotationSpeed?.max) { | |
| out.behaviors.push({ | |
| type: 'rotation', | |
| config: { | |
| accel: config.rotationAcceleration || 0, | |
| minSpeed: config.rotationSpeed?.min || 0, | |
| maxSpeed: config.rotationSpeed?.max || 0, | |
| minStart: config.startRotation?.min || 0, | |
| maxStart: config.startRotation?.max || 0, | |
| }, | |
| }); | |
| } | |
| else if (config.startRotation?.min || config.startRotation?.max) { | |
| out.behaviors.push({ | |
| type: 'rotationStatic', | |
| config: { | |
| min: config.startRotation?.min || 0, | |
| max: config.startRotation?.max || 0, | |
| }, | |
| }); | |
| } | |
| if (config.noRotation) { | |
| out.behaviors.push({ | |
| type: 'noRotation', | |
| config: {}, | |
| }); | |
| } | |
| // blend mode | |
| if (config.blendMode && config.blendMode !== 'normal') { | |
| out.behaviors.push({ | |
| type: 'blendMode', | |
| config: { | |
| blendMode: config.blendMode, | |
| }, | |
| }); | |
| } | |
| // animated | |
| if (Array.isArray(art) && typeof art[0] !== 'string' && 'framerate' in art[0]) { | |
| for (let i = 0; i < art.length; ++i) { | |
| if (art[i].framerate === 'matchLife') { | |
| art[i].framerate = -1; | |
| } | |
| } | |
| out.behaviors.push({ | |
| type: 'animatedRandom', | |
| config: { | |
| anims: art, | |
| }, | |
| }); | |
| } | |
| else if (typeof art !== 'string' && 'framerate' in art) { | |
| if (art.framerate === 'matchLife') { | |
| art.framerate = -1; | |
| } | |
| out.behaviors.push({ | |
| type: 'animatedSingle', | |
| config: { | |
| anim: art, | |
| }, | |
| }); | |
| } | |
| // ordered art | |
| else if (config.orderedArt && Array.isArray(art)) { | |
| out.behaviors.push({ | |
| type: 'textureOrdered', | |
| config: { | |
| textures: art, | |
| }, | |
| }); | |
| } | |
| // random texture | |
| else if (Array.isArray(art)) { | |
| out.behaviors.push({ | |
| type: 'textureRandom', | |
| config: { | |
| textures: art, | |
| }, | |
| }); | |
| } | |
| // single texture | |
| else { | |
| out.behaviors.push({ | |
| type: 'textureSingle', | |
| config: { | |
| texture: art, | |
| }, | |
| }); | |
| } | |
| // spawn burst | |
| if (config.spawnType === 'burst') { | |
| out.behaviors.push({ | |
| type: 'spawnBurst', | |
| config: { | |
| start: config.angleStart || 0, | |
| spacing: config.particleSpacing, | |
| // older formats bursted from a single point | |
| // distance: 0, | |
| }, | |
| }); | |
| } | |
| // spawn point | |
| else if (config.spawnType === 'point') { | |
| out.behaviors.push({ | |
| type: 'spawnPoint', | |
| config: {}, | |
| }); | |
| } | |
| // spawn shape | |
| else { | |
| let shape: any; | |
| if (config.spawnType === 'ring') { | |
| shape = { | |
| type: 'torus', | |
| data: { | |
| x: config.spawnCircle.x, | |
| y: config.spawnCircle.y, | |
| radius: config.spawnCircle.r, | |
| innerRadius: config.spawnCircle.minR, | |
| affectRotation: true, | |
| }, | |
| }; | |
| } | |
| else if (config.spawnType === 'circle') { | |
| shape = { | |
| type: 'torus', | |
| data: { | |
| x: config.spawnCircle.x, | |
| y: config.spawnCircle.y, | |
| radius: config.spawnCircle.r, | |
| innerRadius: 0, | |
| affectRotation: false, | |
| }, | |
| }; | |
| } | |
| else if (config.spawnType === 'rect') { | |
| shape = { | |
| type: 'rect', | |
| data: config.spawnRect, | |
| }; | |
| } | |
| else if (config.spawnType === 'polygonalChain') { | |
| shape = { | |
| type: 'polygonalChain', | |
| data: config.spawnPolygon, | |
| }; | |
| } | |
| if (shape) { | |
| out.behaviors.push({ | |
| type: 'spawnShape', | |
| config: shape, | |
| }); | |
| } | |
| } | |
| return out; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment