mirror of
https://github.com/misode/misode.github.io.git
synced 2026-05-02 13:42:55 +00:00
Move schemas to npm package
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"copy-webpack-plugin": "^6.0.1",
|
"copy-webpack-plugin": "^6.0.1",
|
||||||
|
"minecraft-schemas": "^0.1.2",
|
||||||
"ts-loader": "^7.0.4",
|
"ts-loader": "^7.0.4",
|
||||||
"typescript": "^3.9.3",
|
"typescript": "^3.9.3",
|
||||||
"webpack": "^4.43.0",
|
"webpack": "^4.43.0",
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
import { INode } from "./nodes/AbstractNode"
|
|
||||||
import { Path, PathElement } from "./model/Path"
|
|
||||||
|
|
||||||
export interface Registry<T> {
|
|
||||||
register(id: string, value: T): void
|
|
||||||
get(id: string): T
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registry for schemas
|
|
||||||
*/
|
|
||||||
class SchemaRegistry implements Registry<INode<any>> {
|
|
||||||
private registry: { [id: string]: INode<any> } = {}
|
|
||||||
|
|
||||||
register(id: string, node: INode<any>) {
|
|
||||||
this.registry[id] = node
|
|
||||||
}
|
|
||||||
|
|
||||||
get(id: string) {
|
|
||||||
const node = this.registry[id]
|
|
||||||
if (node === undefined) {
|
|
||||||
console.error(`Tried to access schema "${id}, but that doesn't exit.`)
|
|
||||||
}
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registry for collections
|
|
||||||
*/
|
|
||||||
class CollectionRegistry implements Registry<string[]> {
|
|
||||||
private registry: { [id: string]: string[] } = {}
|
|
||||||
|
|
||||||
register(id: string, list: string[]) {
|
|
||||||
this.registry[id] = list
|
|
||||||
}
|
|
||||||
|
|
||||||
get(id: string) {
|
|
||||||
const list = this.registry[id]
|
|
||||||
if (list === undefined) {
|
|
||||||
console.warn(`Tried to access collection "${id}", but that doesn't exist`)
|
|
||||||
}
|
|
||||||
return list ?? []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registry for locales
|
|
||||||
*/
|
|
||||||
export interface Locale {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
|
|
||||||
class LocaleRegistry implements Registry<Locale> {
|
|
||||||
private registry: { [id: string]: Locale } = {}
|
|
||||||
language: string = ''
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param id locale identifier
|
|
||||||
* @param locale object mapping keys to translations
|
|
||||||
*/
|
|
||||||
register(id: string, locale: Locale): void {
|
|
||||||
this.registry[id] = locale
|
|
||||||
}
|
|
||||||
|
|
||||||
get(id: string) {
|
|
||||||
const locale = this.registry[id]
|
|
||||||
if (locale === undefined) {
|
|
||||||
console.warn(`Tried to access locale "${id}", but that doesn't exist`)
|
|
||||||
}
|
|
||||||
return locale ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
getLocale(key: string) {
|
|
||||||
return this.get(this.language)[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SCHEMAS = new SchemaRegistry()
|
|
||||||
export const COLLECTIONS = new CollectionRegistry()
|
|
||||||
export const LOCALES = new LocaleRegistry()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the locale of a key from the locale registry.
|
|
||||||
*
|
|
||||||
* @param key string or path that refers to a locale ID.
|
|
||||||
* If a string is given, an exact match is required.
|
|
||||||
* If a path is given, it finds the longest match at the end.
|
|
||||||
* @returns undefined if the key isn't found for the selected language
|
|
||||||
*/
|
|
||||||
export const locale = (key: string | Path) => {
|
|
||||||
if (typeof key === 'string') {
|
|
||||||
return LOCALES.getLocale(key) ?? key
|
|
||||||
}
|
|
||||||
let path = key.getArray().filter(e => (typeof e === 'string'))
|
|
||||||
while (path.length > 0) {
|
|
||||||
const locale = LOCALES.getLocale(path.join('.'))
|
|
||||||
if (locale !== undefined) return locale
|
|
||||||
path.shift()
|
|
||||||
}
|
|
||||||
return key.last()
|
|
||||||
}
|
|
||||||
+11
-8
@@ -1,11 +1,14 @@
|
|||||||
import { ObjectNode } from '../nodes/ObjectNode';
|
import {
|
||||||
import { EnumNode } from '../nodes/EnumNode';
|
ObjectNode,
|
||||||
import { NumberNode } from '../nodes/NumberNode';
|
EnumNode,
|
||||||
import { BooleanNode } from '../nodes/BooleanNode';
|
StringNode,
|
||||||
import { RangeNode } from '../minecraft/nodes/RangeNode';
|
NumberNode,
|
||||||
import { MapNode } from '../nodes/MapNode';
|
BooleanNode,
|
||||||
import { StringNode } from '../nodes/StringNode';
|
RangeNode,
|
||||||
import { ListNode } from '../nodes/ListNode';
|
MapNode,
|
||||||
|
ListNode,
|
||||||
|
SCHEMAS
|
||||||
|
} from 'minecraft-schemas'
|
||||||
|
|
||||||
const EntityCollection = ['sheep', 'pig']
|
const EntityCollection = ['sheep', 'pig']
|
||||||
|
|
||||||
|
|||||||
+9
-6
@@ -1,10 +1,13 @@
|
|||||||
import { DataModel } from '../model/DataModel'
|
import {
|
||||||
import { TreeView } from '../view/TreeView'
|
DataModel,
|
||||||
import { SourceView } from '../view/SourceView'
|
TreeView,
|
||||||
import { ConditionSchema } from '../minecraft/schemas/Condition'
|
SourceView,
|
||||||
import { LootTableSchema } from '../minecraft/schemas/LootTable'
|
ConditionSchema,
|
||||||
|
LootTableSchema,
|
||||||
|
LOCALES
|
||||||
|
} from 'minecraft-schemas'
|
||||||
|
|
||||||
import { SandboxSchema } from './Sandbox'
|
import { SandboxSchema } from './Sandbox'
|
||||||
import { LOCALES } from '../Registries'
|
|
||||||
|
|
||||||
const predicateModel = new DataModel(ConditionSchema)
|
const predicateModel = new DataModel(ConditionSchema)
|
||||||
const lootTableModel = new DataModel(LootTableSchema)
|
const lootTableModel = new DataModel(LootTableSchema)
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
import { AbstractNode, NodeMods, RenderOptions, StateNode } from '../../nodes/AbstractNode'
|
|
||||||
import { Path } from '../../model/Path'
|
|
||||||
import { DataModel } from '../../model/DataModel'
|
|
||||||
import { TreeView } from '../../view/TreeView'
|
|
||||||
import { locale } from '../../Registries'
|
|
||||||
|
|
||||||
export type IRange = number
|
|
||||||
| { min?: number, max?: number, type?: 'uniform' }
|
|
||||||
| { n?: number, p?: number, type: 'binomial' }
|
|
||||||
|
|
||||||
export interface RangeNodeMods extends NodeMods<IRange> {
|
|
||||||
integer?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RangeNode extends AbstractNode<IRange> implements StateNode<IRange> {
|
|
||||||
integer: boolean
|
|
||||||
|
|
||||||
constructor(mods?: RangeNodeMods) {
|
|
||||||
super(mods)
|
|
||||||
this.integer = mods?.integer ? mods.integer : false
|
|
||||||
}
|
|
||||||
|
|
||||||
parseNumber(str: string): number {
|
|
||||||
return this.integer ? parseInt(str) : parseFloat(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
getState(el: Element): IRange {
|
|
||||||
const type = el.querySelector('select')!.value
|
|
||||||
if (type === 'exact') {
|
|
||||||
return this.parseNumber(el.querySelector('input')!.value)
|
|
||||||
}
|
|
||||||
if (type === 'range') {
|
|
||||||
const min = this.parseNumber(el.querySelectorAll('input')[0].value)
|
|
||||||
const max = this.parseNumber(el.querySelectorAll('input')[1].value)
|
|
||||||
return {
|
|
||||||
min: isNaN(min) ? undefined : min,
|
|
||||||
max: isNaN(max) ? undefined : max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const n = parseInt(el.querySelectorAll('input')[0].value)
|
|
||||||
const p = parseFloat(el.querySelectorAll('input')[1].value)
|
|
||||||
return {
|
|
||||||
type: 'binomial',
|
|
||||||
n: isNaN(n) ? undefined : n,
|
|
||||||
p: isNaN(p) ? undefined : p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateModel(el: Element, path: Path, model: DataModel) {
|
|
||||||
model.set(path, this.getState(el))
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRaw(path: Path, value: IRange, view: TreeView, options?: RenderOptions) {
|
|
||||||
let curType = ''
|
|
||||||
let input = ''
|
|
||||||
if (value === undefined || typeof value === 'number') {
|
|
||||||
curType = 'exact'
|
|
||||||
input = `<input value=${value === undefined ? '' : value}>`
|
|
||||||
} else if (value.type === 'binomial') {
|
|
||||||
curType = 'binomial'
|
|
||||||
input = `<label>${locale('range.n')}</label>
|
|
||||||
<input value=${value.n === undefined ? '' : value.n}>
|
|
||||||
<label>${locale('range.p')}</label>
|
|
||||||
<input value=${value.p === undefined ? '' : value.p}>`
|
|
||||||
} else {
|
|
||||||
curType = 'range'
|
|
||||||
input = `<label>${locale('range.min')}</label>
|
|
||||||
<input value=${value.min === undefined ? '' : value.min}>
|
|
||||||
<label>${locale('range.max')}</label>
|
|
||||||
<input value=${value.max === undefined ? '' : value.max}>`
|
|
||||||
}
|
|
||||||
const id = view.register(el => {
|
|
||||||
(el as HTMLInputElement).value = curType
|
|
||||||
el.addEventListener('change', evt => {
|
|
||||||
const target = (el as HTMLInputElement).value
|
|
||||||
const newValue = this.default(target === 'exact' ? undefined :
|
|
||||||
target === 'binomial' ? {type: 'binomial'} : {})
|
|
||||||
view.model.set(path, newValue)
|
|
||||||
evt.stopPropagation()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
|
|
||||||
<select data-id="${id}">
|
|
||||||
<option value="exact">${locale('range.exact')}</option>
|
|
||||||
<option value="range">${locale('range.range')}</option>
|
|
||||||
<option value="binomial">${locale('range.binomial')}</option>
|
|
||||||
</select>
|
|
||||||
${input}`
|
|
||||||
}
|
|
||||||
|
|
||||||
getClassName() {
|
|
||||||
return 'range-node'
|
|
||||||
}
|
|
||||||
|
|
||||||
static isExact(v?: IRange) {
|
|
||||||
return v === undefined || typeof v === 'number'
|
|
||||||
}
|
|
||||||
|
|
||||||
static isRange(v?: IRange) {
|
|
||||||
return v !== undefined && typeof v !== 'number' && v.type !== 'binomial'
|
|
||||||
}
|
|
||||||
|
|
||||||
static isBinomial(v?: IRange) {
|
|
||||||
return v !== undefined && typeof v !== 'number' && v.type === 'binomial'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { NodeMods, RenderOptions } from '../../nodes/AbstractNode'
|
|
||||||
import { EnumNode } from '../../nodes/EnumNode'
|
|
||||||
import { Path } from '../../model/Path'
|
|
||||||
import { TreeView, getId } from '../../view/TreeView'
|
|
||||||
import { locale } from '../../Registries'
|
|
||||||
|
|
||||||
export interface ResourceNodeMods extends NodeMods<string> {
|
|
||||||
additional?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ResourceNode extends EnumNode {
|
|
||||||
additional: boolean
|
|
||||||
|
|
||||||
constructor(options: string[], mods?: ResourceNodeMods) {
|
|
||||||
super(options, {
|
|
||||||
transform: (v) => {
|
|
||||||
if (v === undefined || v.length === 0) return undefined
|
|
||||||
return v.startsWith('minecraft:') ? v : 'minecraft:' + v
|
|
||||||
}, ...mods})
|
|
||||||
this.additional = mods?.additional ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
getState(el: Element) {
|
|
||||||
if (this.additional) {
|
|
||||||
return el.querySelector('input')!.value
|
|
||||||
} else {
|
|
||||||
return super.getState(el)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRaw(path: Path, value: string, view: TreeView, options?: RenderOptions) {
|
|
||||||
if (this.additional) {
|
|
||||||
const id = `datalist-${getId()}`
|
|
||||||
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
|
|
||||||
<input list=${id} value="${value ?? ''}">
|
|
||||||
<datalist id=${id}>${this.options.map(o =>
|
|
||||||
`<option value="${o}">${locale(path.push(o))}</option>`
|
|
||||||
).join('')}</datalist>`
|
|
||||||
} else {
|
|
||||||
return super.renderRaw(path, value, view, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getClassName() {
|
|
||||||
return 'enum-node'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,321 +0,0 @@
|
|||||||
import { COLLECTIONS } from '../../Registries'
|
|
||||||
|
|
||||||
COLLECTIONS.register('conditions', [
|
|
||||||
'alternative',
|
|
||||||
'requirements',
|
|
||||||
'inverted',
|
|
||||||
'reference',
|
|
||||||
'entity_properties',
|
|
||||||
'block_state_property',
|
|
||||||
'match_tool',
|
|
||||||
'damage_source_properties',
|
|
||||||
'location_check',
|
|
||||||
'weather_check',
|
|
||||||
'time_check',
|
|
||||||
'entity_scores',
|
|
||||||
'random_chance',
|
|
||||||
'random_chance_with_looting',
|
|
||||||
'table_bonus',
|
|
||||||
'killed_by_player',
|
|
||||||
'survives_explosion'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('loot-entries', [
|
|
||||||
'empty',
|
|
||||||
'item',
|
|
||||||
'tag',
|
|
||||||
'loot_table',
|
|
||||||
'alternatives',
|
|
||||||
'sequence',
|
|
||||||
'group',
|
|
||||||
'dynamic'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('loot-functions', [
|
|
||||||
'set_count',
|
|
||||||
'set_damage',
|
|
||||||
'set_name',
|
|
||||||
'set_lore',
|
|
||||||
'set_nbt',
|
|
||||||
'set_attributes',
|
|
||||||
'set_contents',
|
|
||||||
'enchant_randomly',
|
|
||||||
'enchant_with_levels',
|
|
||||||
'looting_enchant',
|
|
||||||
'limit_count',
|
|
||||||
'furnace_smelt',
|
|
||||||
'explosion_decay',
|
|
||||||
'fill_player_head',
|
|
||||||
'copy_name',
|
|
||||||
'copy_nbt',
|
|
||||||
'copy_state',
|
|
||||||
'apply_bonus',
|
|
||||||
'exploration_map',
|
|
||||||
'set_stew_effect'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('enchantments', [
|
|
||||||
'aqua_affinity',
|
|
||||||
'bane_of_arthropods',
|
|
||||||
'blast_protection',
|
|
||||||
'channeling',
|
|
||||||
'binding_curse',
|
|
||||||
'vanishing_curse',
|
|
||||||
'depth_strider',
|
|
||||||
'efficiency',
|
|
||||||
'feather_falling',
|
|
||||||
'fire_aspect',
|
|
||||||
'fire_protection',
|
|
||||||
'flame',
|
|
||||||
'fortune',
|
|
||||||
'frost_walker',
|
|
||||||
'impaling',
|
|
||||||
'infinity',
|
|
||||||
'knockback',
|
|
||||||
'looting',
|
|
||||||
'loyalty',
|
|
||||||
'luck_of_the_sea',
|
|
||||||
'lure',
|
|
||||||
'mending',
|
|
||||||
'multishot',
|
|
||||||
'piercing',
|
|
||||||
'power',
|
|
||||||
'projectile_protection',
|
|
||||||
'protection',
|
|
||||||
'punch',
|
|
||||||
'quick_charge',
|
|
||||||
'respiration',
|
|
||||||
'riptide',
|
|
||||||
'sharpness',
|
|
||||||
'silk_touch',
|
|
||||||
'smite',
|
|
||||||
'sweeping',
|
|
||||||
'thorns',
|
|
||||||
'unbreaking'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('biomes', [
|
|
||||||
'badlands',
|
|
||||||
'badlands_plateau',
|
|
||||||
'bamboo_jungle',
|
|
||||||
'bamboo_jungle_hills',
|
|
||||||
'beach',
|
|
||||||
'birch_forest',
|
|
||||||
'birch_forest_hills',
|
|
||||||
'cold_ocean',
|
|
||||||
'dark_forest',
|
|
||||||
'dark_forest_hills',
|
|
||||||
'deep_cold_ocean',
|
|
||||||
'deep_frozen_ocean',
|
|
||||||
'deep_lukewarm_ocean',
|
|
||||||
'deep_ocean',
|
|
||||||
'deep_warm_ocean',
|
|
||||||
'desert',
|
|
||||||
'desert_hills',
|
|
||||||
'desert_lakes',
|
|
||||||
'end_barrens',
|
|
||||||
'end_highlands',
|
|
||||||
'end_midlands',
|
|
||||||
'eroded_badlands',
|
|
||||||
'flower_forest',
|
|
||||||
'forest',
|
|
||||||
'frozen_ocean',
|
|
||||||
'frozen_river',
|
|
||||||
'giant_spruce_taiga',
|
|
||||||
'giant_spruce_taiga_hills',
|
|
||||||
'giant_tree_taiga',
|
|
||||||
'giant_tree_taiga_hills',
|
|
||||||
'gravelly_mountains',
|
|
||||||
'ice_spikes',
|
|
||||||
'jungle',
|
|
||||||
'jungle_edge',
|
|
||||||
'jungle_hills',
|
|
||||||
'lukewarm_ocean',
|
|
||||||
'modified_badlands_plateau',
|
|
||||||
'modified_gravelly_mountains',
|
|
||||||
'modified_jungle',
|
|
||||||
'modified_jungle_edge',
|
|
||||||
'modified_wooded_badlands_plateau',
|
|
||||||
'mountain_edge',
|
|
||||||
'mountains',
|
|
||||||
'mushroom_field_shore',
|
|
||||||
'mushroom_fields',
|
|
||||||
'nether',
|
|
||||||
'ocean',
|
|
||||||
'plains',
|
|
||||||
'river',
|
|
||||||
'savanna',
|
|
||||||
'savanna_plateau',
|
|
||||||
'shattered_savanna',
|
|
||||||
'shattered_savanna_plateau',
|
|
||||||
'small_end_islands',
|
|
||||||
'snowy_beach',
|
|
||||||
'snowy_mountains',
|
|
||||||
'snowy_taiga',
|
|
||||||
'snowy_taiga_hills',
|
|
||||||
'snowy_taiga_mountains',
|
|
||||||
'snowy_tundra',
|
|
||||||
'stone_shore',
|
|
||||||
'sunflower_plains',
|
|
||||||
'swamp',
|
|
||||||
'swamp_hills',
|
|
||||||
'taiga',
|
|
||||||
'taiga_hills',
|
|
||||||
'taiga_mountains',
|
|
||||||
'tall_birch_forest',
|
|
||||||
'tall_birch_hills',
|
|
||||||
'the_end',
|
|
||||||
'the_void',
|
|
||||||
'warm_ocean',
|
|
||||||
'wooded_badlands_plateau',
|
|
||||||
'wooded_hills',
|
|
||||||
'wooded_mountains'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('structures', [
|
|
||||||
'pillager_outpost',
|
|
||||||
'mineshaft',
|
|
||||||
'mansion',
|
|
||||||
'jungle_pyramid',
|
|
||||||
'desert_pyramid',
|
|
||||||
'igloo',
|
|
||||||
'shipwreck',
|
|
||||||
'swamp_hut',
|
|
||||||
'stronghold',
|
|
||||||
'monument',
|
|
||||||
'ocean_ruin',
|
|
||||||
'fortress',
|
|
||||||
'endcity',
|
|
||||||
'buried_treasure',
|
|
||||||
'village'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('dimensions', [
|
|
||||||
'overworld',
|
|
||||||
'the_nether',
|
|
||||||
'the_end'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('slots', [
|
|
||||||
'mainhand',
|
|
||||||
'offhand',
|
|
||||||
'head',
|
|
||||||
'chest',
|
|
||||||
'legs',
|
|
||||||
'feet'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('status-effects', [
|
|
||||||
'speed',
|
|
||||||
'slowness',
|
|
||||||
'haste',
|
|
||||||
'mining_fatigue',
|
|
||||||
'strength',
|
|
||||||
'instant_health',
|
|
||||||
'instant_damage',
|
|
||||||
'jump_boost',
|
|
||||||
'nausea',
|
|
||||||
'regeneration',
|
|
||||||
'resistance',
|
|
||||||
'fire_resistance',
|
|
||||||
'water_breathing',
|
|
||||||
'invisibility',
|
|
||||||
'blindness',
|
|
||||||
'night_vision',
|
|
||||||
'hunger',
|
|
||||||
'weakness',
|
|
||||||
'poison',
|
|
||||||
'wither',
|
|
||||||
'health_boost',
|
|
||||||
'absorption',
|
|
||||||
'saturation',
|
|
||||||
'glowing',
|
|
||||||
'levitation',
|
|
||||||
'luck',
|
|
||||||
'unluck',
|
|
||||||
'slow_falling',
|
|
||||||
'conduit_power',
|
|
||||||
'dolphins_grace',
|
|
||||||
'bad_omen',
|
|
||||||
'hero_of_the_village'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('gamemodes', [
|
|
||||||
'survival',
|
|
||||||
'creative',
|
|
||||||
'adventure',
|
|
||||||
'spectator'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('statistic-types', [
|
|
||||||
'minecraft:broken',
|
|
||||||
'minecraft:crafted',
|
|
||||||
'minecraft:custom',
|
|
||||||
'minecraft:dropped',
|
|
||||||
'minecraft:killed',
|
|
||||||
'minecraft:killed_by',
|
|
||||||
'minecraft:mined',
|
|
||||||
'minecraft:picked_up',
|
|
||||||
'minecraft:used',
|
|
||||||
'killedByTeam',
|
|
||||||
'teamkill'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('entity-sources', [
|
|
||||||
'this',
|
|
||||||
'killer',
|
|
||||||
'killer_player'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('copy-sources', [
|
|
||||||
'block_entity',
|
|
||||||
'this',
|
|
||||||
'killer',
|
|
||||||
'killer_player'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('attributes', [
|
|
||||||
'generic.max_health',
|
|
||||||
'generic.follow_range',
|
|
||||||
'generic.knockback_resistance',
|
|
||||||
'generic.movement_speed',
|
|
||||||
'generic.attack_damage',
|
|
||||||
'generic.armor',
|
|
||||||
'generic.armor_toughness',
|
|
||||||
'generic.attack_speed',
|
|
||||||
'generic.luck',
|
|
||||||
'horse.jump_strength',
|
|
||||||
'generic.attack_knockback',
|
|
||||||
'generic.flying_speed',
|
|
||||||
'zombie.spawn_reinforcements'
|
|
||||||
])
|
|
||||||
|
|
||||||
COLLECTIONS.register('map-decorations', [
|
|
||||||
'mansion',
|
|
||||||
'monument',
|
|
||||||
'player',
|
|
||||||
'frame',
|
|
||||||
'red_marker',
|
|
||||||
'blue_marker',
|
|
||||||
'target_x',
|
|
||||||
'target_point',
|
|
||||||
'player_off_map',
|
|
||||||
'player_off_limits',
|
|
||||||
'red_x',
|
|
||||||
'banner_white',
|
|
||||||
'banner_orange',
|
|
||||||
'banner_magenta',
|
|
||||||
'banner_light_blue',
|
|
||||||
'banner_yellow',
|
|
||||||
'banner_lime',
|
|
||||||
'banner_pink',
|
|
||||||
'banner_gray',
|
|
||||||
'banner_light_gray',
|
|
||||||
'banner_cyan',
|
|
||||||
'banner_purple',
|
|
||||||
'banner_blue',
|
|
||||||
'banner_brown',
|
|
||||||
'banner_green',
|
|
||||||
'banner_red',
|
|
||||||
'banner_black'
|
|
||||||
])
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { EnumNode } from '../../nodes/EnumNode';
|
|
||||||
import { ResourceNode } from '../nodes/ResourceNode';
|
|
||||||
import { NumberNode } from '../../nodes/NumberNode';
|
|
||||||
import { BooleanNode } from '../../nodes/BooleanNode';
|
|
||||||
import { ObjectNode, Switch, Case } from '../../nodes/ObjectNode';
|
|
||||||
import { ListNode } from '../../nodes/ListNode';
|
|
||||||
import { RangeNode } from '../nodes/RangeNode';
|
|
||||||
import { MapNode } from '../../nodes/MapNode';
|
|
||||||
import { StringNode } from '../../nodes/StringNode';
|
|
||||||
import { ReferenceNode } from '../../nodes/ReferenceNode';
|
|
||||||
import { SCHEMAS, COLLECTIONS } from '../../Registries';
|
|
||||||
|
|
||||||
import './Predicates'
|
|
||||||
|
|
||||||
SCHEMAS.register('condition', new ObjectNode({
|
|
||||||
condition: new ResourceNode(COLLECTIONS.get('conditions'), {default: () => 'random_chance'}),
|
|
||||||
[Switch]: 'condition',
|
|
||||||
[Case]: {
|
|
||||||
'alternative': {
|
|
||||||
terms: new ListNode(
|
|
||||||
new ReferenceNode('condition')
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'block_state_property': {
|
|
||||||
block: new ResourceNode(COLLECTIONS.get('blocks')),
|
|
||||||
properties: new MapNode(
|
|
||||||
new StringNode(),
|
|
||||||
new StringNode()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'damage_source_properties': {
|
|
||||||
predicate: new ReferenceNode('damage-source-predicate')
|
|
||||||
},
|
|
||||||
'entity_properties': {
|
|
||||||
entity: new EnumNode(COLLECTIONS.get('entity-sources'), 'this'),
|
|
||||||
predicate: new ReferenceNode('entity-predicate')
|
|
||||||
},
|
|
||||||
'entity_scores': {
|
|
||||||
entity: new EnumNode(COLLECTIONS.get('entity-sources'), 'this'),
|
|
||||||
scores: new MapNode(
|
|
||||||
new StringNode(),
|
|
||||||
new RangeNode()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'inverted': {
|
|
||||||
term: new ReferenceNode('condition')
|
|
||||||
},
|
|
||||||
'killed_by_player': {
|
|
||||||
inverse: new BooleanNode()
|
|
||||||
},
|
|
||||||
'location_check': {
|
|
||||||
offsetX: new NumberNode({integer: true}),
|
|
||||||
offsetY: new NumberNode({integer: true}),
|
|
||||||
offsetZ: new NumberNode({integer: true}),
|
|
||||||
predicate: new ReferenceNode('location-predicate')
|
|
||||||
},
|
|
||||||
'match_tool': {
|
|
||||||
predicate: new ReferenceNode('item-predicate')
|
|
||||||
},
|
|
||||||
'random_chance': {
|
|
||||||
chance: new NumberNode({min: 0, max: 1})
|
|
||||||
},
|
|
||||||
'random_chance_with_looting': {
|
|
||||||
chance: new NumberNode({min: 0, max: 1}),
|
|
||||||
looting_multiplier: new NumberNode()
|
|
||||||
},
|
|
||||||
'requirements': {
|
|
||||||
terms: new ListNode(
|
|
||||||
new ReferenceNode('condition')
|
|
||||||
),
|
|
||||||
},
|
|
||||||
'reference': {
|
|
||||||
name: new StringNode()
|
|
||||||
},
|
|
||||||
'table_bonus': {
|
|
||||||
enchantment: new ResourceNode(COLLECTIONS.get('enchantments')),
|
|
||||||
chances: new ListNode(
|
|
||||||
new NumberNode({min: 0, max: 1})
|
|
||||||
)
|
|
||||||
},
|
|
||||||
'time_check': {
|
|
||||||
value: new RangeNode(),
|
|
||||||
period: new NumberNode()
|
|
||||||
},
|
|
||||||
'weather_check': {
|
|
||||||
raining: new BooleanNode(),
|
|
||||||
thrundering: new BooleanNode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
default: () => ({
|
|
||||||
condition: 'random_chance',
|
|
||||||
chance: 0.5
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const ConditionSchema = SCHEMAS.get('condition')
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
import { EnumNode } from '../../nodes/EnumNode';
|
|
||||||
import { ResourceNode } from '../nodes/ResourceNode';
|
|
||||||
import { NumberNode } from '../../nodes/NumberNode';
|
|
||||||
import { BooleanNode } from '../../nodes/BooleanNode';
|
|
||||||
import { ObjectNode, Switch, Case } from '../../nodes/ObjectNode';
|
|
||||||
import { ListNode } from '../../nodes/ListNode';
|
|
||||||
import { RangeNode } from '../nodes/RangeNode';
|
|
||||||
import { MapNode } from '../../nodes/MapNode';
|
|
||||||
import { StringNode } from '../../nodes/StringNode';
|
|
||||||
import { ReferenceNode } from '../../nodes/ReferenceNode';
|
|
||||||
import { SCHEMAS, COLLECTIONS } from '../../Registries';
|
|
||||||
|
|
||||||
import './Predicates'
|
|
||||||
|
|
||||||
const conditions = {
|
|
||||||
conditions: new ListNode(
|
|
||||||
new ReferenceNode('condition')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const functionsAndConditions = {
|
|
||||||
functions: new ListNode(
|
|
||||||
new ReferenceNode('loot-function')
|
|
||||||
),
|
|
||||||
...conditions
|
|
||||||
}
|
|
||||||
|
|
||||||
SCHEMAS.register('loot-table', new ObjectNode({
|
|
||||||
pools: new ListNode(
|
|
||||||
new ObjectNode({
|
|
||||||
rolls: new RangeNode(),
|
|
||||||
entries: new ListNode(
|
|
||||||
new ReferenceNode('loot-entry')
|
|
||||||
)
|
|
||||||
})
|
|
||||||
),
|
|
||||||
...functionsAndConditions
|
|
||||||
}, {
|
|
||||||
default: () => ({
|
|
||||||
pools: [
|
|
||||||
{
|
|
||||||
rolls: 1,
|
|
||||||
entries: [
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
name: 'minecraft:stone'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('loot-entry', new ObjectNode({
|
|
||||||
type: new EnumNode(COLLECTIONS.get('loot-entries'), {default: () => 'item'}),
|
|
||||||
weight: new NumberNode({
|
|
||||||
integer: true,
|
|
||||||
min: 1,
|
|
||||||
enable: path => path.pop().get()?.length > 1
|
|
||||||
&& !['alternatives', 'group', 'sequence'].includes(path.push('type').get())
|
|
||||||
}),
|
|
||||||
[Switch]: 'type',
|
|
||||||
[Case]: {
|
|
||||||
'alternatives': {
|
|
||||||
children: new ListNode(
|
|
||||||
new ReferenceNode('loot-entry')
|
|
||||||
),
|
|
||||||
...functionsAndConditions
|
|
||||||
},
|
|
||||||
'dynamic': {
|
|
||||||
name: new StringNode(),
|
|
||||||
...functionsAndConditions
|
|
||||||
},
|
|
||||||
'group': {
|
|
||||||
children: new ListNode(
|
|
||||||
new ReferenceNode('loot-entry')
|
|
||||||
),
|
|
||||||
...functionsAndConditions
|
|
||||||
},
|
|
||||||
'item': {
|
|
||||||
name: new StringNode(),
|
|
||||||
...functionsAndConditions
|
|
||||||
},
|
|
||||||
'loot_table': {
|
|
||||||
name: new StringNode(),
|
|
||||||
...functionsAndConditions
|
|
||||||
},
|
|
||||||
'sequence': {
|
|
||||||
children: new ListNode(
|
|
||||||
new ReferenceNode('loot-entry')
|
|
||||||
),
|
|
||||||
...functionsAndConditions
|
|
||||||
},
|
|
||||||
'tag': {
|
|
||||||
name: new StringNode(),
|
|
||||||
expand: new BooleanNode(),
|
|
||||||
...functionsAndConditions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('loot-function', new ObjectNode({
|
|
||||||
function: new EnumNode(COLLECTIONS.get('loot-functions'), {default: () => 'set_count'}),
|
|
||||||
[Switch]: 'function',
|
|
||||||
[Case]: {
|
|
||||||
'apply_bonus': {
|
|
||||||
enchantment: new EnumNode(COLLECTIONS.get('enchantments')),
|
|
||||||
formula: new EnumNode([
|
|
||||||
'uniform_bonus_count',
|
|
||||||
'binomial_with_bonus_count',
|
|
||||||
'ore_drops'
|
|
||||||
]),
|
|
||||||
parameters: new ObjectNode({
|
|
||||||
bonusMultiplier: new NumberNode({
|
|
||||||
enable: path => path.pop().push('formula').get() === 'uniform_bonus_count'
|
|
||||||
}),
|
|
||||||
extra: new NumberNode({
|
|
||||||
enable: path => path.pop().push('formula').get() === 'binomial_with_bonus_count'
|
|
||||||
}),
|
|
||||||
probability: new NumberNode({
|
|
||||||
enable: path => path.pop().push('formula').get() === 'binomial_with_bonus_count'
|
|
||||||
})
|
|
||||||
}, {
|
|
||||||
enable: path => path.push('formula').get() !== 'ore_drops'
|
|
||||||
}),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'copy_name': {
|
|
||||||
source: new EnumNode(COLLECTIONS.get('copy-sources')),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'copy_nbt': {
|
|
||||||
source: new EnumNode(COLLECTIONS.get('copy-sources')),
|
|
||||||
ops: new ListNode(
|
|
||||||
new ObjectNode({
|
|
||||||
source: new StringNode(),
|
|
||||||
target: new StringNode(),
|
|
||||||
op: new EnumNode(['replace', 'append', 'merge'])
|
|
||||||
})
|
|
||||||
),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'copy_state': {
|
|
||||||
block: new ResourceNode(COLLECTIONS.get('blocks')),
|
|
||||||
properties: new ListNode(
|
|
||||||
new StringNode()
|
|
||||||
),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'enchant_randomly': {
|
|
||||||
enchantments: new ListNode(
|
|
||||||
new EnumNode(COLLECTIONS.get('enchantments'))
|
|
||||||
),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'enchant_with_levels': {
|
|
||||||
levels: new RangeNode(),
|
|
||||||
treasure: new BooleanNode(),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'exploration_map': {
|
|
||||||
destination: new EnumNode(COLLECTIONS.get('structures')),
|
|
||||||
decoration: new EnumNode(COLLECTIONS.get('map-decorations')),
|
|
||||||
zoom: new NumberNode({integer: true}),
|
|
||||||
search_radius: new NumberNode({integer: true}),
|
|
||||||
skip_existing_chunks: new BooleanNode(),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'fill_player_head': {
|
|
||||||
entity: new EnumNode(COLLECTIONS.get('entity-sources')),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'limit_count': {
|
|
||||||
limit: new RangeNode(),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'looting_enchant': {
|
|
||||||
count: new RangeNode(),
|
|
||||||
limit: new NumberNode({integer: true}),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'set_attributes': {
|
|
||||||
modifiers: new ListNode(
|
|
||||||
new ReferenceNode('attribute-modifier')
|
|
||||||
),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'set_contents': {
|
|
||||||
entries: new ListNode(
|
|
||||||
new ReferenceNode('loot-entry')
|
|
||||||
),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'set_count': {
|
|
||||||
count: new RangeNode(),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'set_damage': {
|
|
||||||
damage: new RangeNode(),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'set_lore': {
|
|
||||||
entity: new EnumNode(COLLECTIONS.get('entity-sources')),
|
|
||||||
lore: new ListNode(
|
|
||||||
new StringNode()
|
|
||||||
),
|
|
||||||
replace: new BooleanNode(),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'set_name': {
|
|
||||||
entity: new EnumNode(COLLECTIONS.get('entity-sources')),
|
|
||||||
name: new StringNode(),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'set_nbt': {
|
|
||||||
tag: new StringNode(),
|
|
||||||
...conditions
|
|
||||||
},
|
|
||||||
'set_stew_effect': {
|
|
||||||
effects: new ListNode(
|
|
||||||
new ReferenceNode('potion-effect')
|
|
||||||
),
|
|
||||||
...conditions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
default: () => ({
|
|
||||||
function: 'set_count',
|
|
||||||
count: 1
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('attribute-modifier', new ObjectNode({
|
|
||||||
attribute: new EnumNode(COLLECTIONS.get('attributes')),
|
|
||||||
name: new StringNode(),
|
|
||||||
amount: new RangeNode(),
|
|
||||||
operation: new EnumNode([
|
|
||||||
'addition',
|
|
||||||
'multiply_base',
|
|
||||||
'multiply_total'
|
|
||||||
]),
|
|
||||||
slot: new ListNode(
|
|
||||||
new EnumNode(COLLECTIONS.get('slots'))
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const LootTableSchema = SCHEMAS.get('loot-table')
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { ObjectNode } from '../../nodes/ObjectNode';
|
|
||||||
import { ResourceNode } from '../nodes/ResourceNode';
|
|
||||||
import { EnumNode } from '../../nodes/EnumNode';
|
|
||||||
import { ListNode } from '../../nodes/ListNode';
|
|
||||||
import { RangeNode } from '../nodes/RangeNode';
|
|
||||||
import { StringNode } from '../../nodes/StringNode';
|
|
||||||
import { ReferenceNode } from '../../nodes/ReferenceNode';
|
|
||||||
import { BooleanNode } from '../../nodes/BooleanNode';
|
|
||||||
import { MapNode } from '../../nodes/MapNode';
|
|
||||||
import { SCHEMAS, COLLECTIONS } from '../../Registries';
|
|
||||||
|
|
||||||
import './Collections'
|
|
||||||
|
|
||||||
SCHEMAS.register('item-predicate', new ObjectNode({
|
|
||||||
item: new ResourceNode(COLLECTIONS.get('items')),
|
|
||||||
tag: new StringNode(),
|
|
||||||
count: new RangeNode(),
|
|
||||||
durability: new RangeNode(),
|
|
||||||
potion: new StringNode(),
|
|
||||||
nbt: new StringNode(),
|
|
||||||
enchantments: new ListNode(
|
|
||||||
new ReferenceNode('enchantment-predicate')
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('enchantment-predicate', new ObjectNode({
|
|
||||||
enchantment: new ResourceNode(COLLECTIONS.get('enchantments')),
|
|
||||||
levels: new RangeNode()
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('block-predicate', new ObjectNode({
|
|
||||||
block: new ResourceNode(COLLECTIONS.get('blocks')),
|
|
||||||
tag: new StringNode(),
|
|
||||||
nbt: new StringNode(),
|
|
||||||
state: new MapNode(
|
|
||||||
new StringNode(),
|
|
||||||
new StringNode()
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('fluid-predicate', new ObjectNode({
|
|
||||||
fluid: new ResourceNode(COLLECTIONS.get('fluids')),
|
|
||||||
tag: new StringNode(),
|
|
||||||
nbt: new StringNode(),
|
|
||||||
state: new MapNode(
|
|
||||||
new StringNode(),
|
|
||||||
new StringNode()
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('location-predicate', new ObjectNode({
|
|
||||||
position: new ObjectNode({
|
|
||||||
x: new RangeNode(),
|
|
||||||
y: new RangeNode(),
|
|
||||||
z: new RangeNode()
|
|
||||||
}, {collapse: true}),
|
|
||||||
biome: new ResourceNode(COLLECTIONS.get('biomes')),
|
|
||||||
feature: new EnumNode(COLLECTIONS.get('structures')),
|
|
||||||
dimension: new ResourceNode(COLLECTIONS.get('dimensions'), {additional: true}),
|
|
||||||
light: new ObjectNode({
|
|
||||||
light: new RangeNode()
|
|
||||||
}),
|
|
||||||
smokey: new BooleanNode(),
|
|
||||||
block: new ReferenceNode('block-predicate', {collapse: true}),
|
|
||||||
fluid: new ReferenceNode('fluid-predicate', {collapse: true})
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('statistic-predicate', new ObjectNode({
|
|
||||||
type: new EnumNode(COLLECTIONS.get('statistic-types')),
|
|
||||||
stat: new StringNode(),
|
|
||||||
value: new RangeNode()
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('player-predicate', new ObjectNode({
|
|
||||||
gamemode: new EnumNode(COLLECTIONS.get('gamemodes')),
|
|
||||||
level: new RangeNode(),
|
|
||||||
advancements: new MapNode(
|
|
||||||
new StringNode(),
|
|
||||||
new BooleanNode()
|
|
||||||
),
|
|
||||||
recipes: new MapNode(
|
|
||||||
new StringNode(),
|
|
||||||
new BooleanNode()
|
|
||||||
),
|
|
||||||
stats: new ListNode(
|
|
||||||
new ReferenceNode('statistic-predicate')
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('status-effect-predicate', new ObjectNode({
|
|
||||||
amplifier: new RangeNode(),
|
|
||||||
duration: new RangeNode(),
|
|
||||||
ambient: new BooleanNode(),
|
|
||||||
visible: new BooleanNode()
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('distance-predicate', new ObjectNode({
|
|
||||||
x: new RangeNode(),
|
|
||||||
y: new RangeNode(),
|
|
||||||
z: new RangeNode(),
|
|
||||||
absolute: new RangeNode(),
|
|
||||||
horizontal: new RangeNode()
|
|
||||||
}))
|
|
||||||
|
|
||||||
SCHEMAS.register('entity-predicate', new ObjectNode({
|
|
||||||
type: new StringNode(),
|
|
||||||
nbt: new StringNode(),
|
|
||||||
team: new StringNode(),
|
|
||||||
location: new ReferenceNode('location-predicate', {collapse: true}),
|
|
||||||
distance: new ReferenceNode('distance-predicate', {collapse: true}),
|
|
||||||
flags: new ObjectNode({
|
|
||||||
is_on_fire: new BooleanNode(),
|
|
||||||
is_sneaking: new BooleanNode(),
|
|
||||||
is_sprinting: new BooleanNode(),
|
|
||||||
is_swimming: new BooleanNode(),
|
|
||||||
is_baby: new BooleanNode()
|
|
||||||
}, {collapse: true}),
|
|
||||||
equipment: new MapNode(
|
|
||||||
new EnumNode(COLLECTIONS.get('slots')),
|
|
||||||
new ReferenceNode('item-predicate')
|
|
||||||
),
|
|
||||||
vehicle: new ReferenceNode('entity-predicate', {collapse: true}),
|
|
||||||
targeted_entity: new ReferenceNode('entity-predicate', {collapse: true}),
|
|
||||||
player: new ReferenceNode('player-predicate', {collapse: true}),
|
|
||||||
fishing_hook: new ObjectNode({
|
|
||||||
in_open_water: new BooleanNode()
|
|
||||||
}),
|
|
||||||
effects: new MapNode(
|
|
||||||
new ResourceNode(COLLECTIONS.get('status-effects')),
|
|
||||||
new ReferenceNode('status-effect-predicate')
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { Path } from "./Path"
|
|
||||||
import { INode } from "../nodes/AbstractNode"
|
|
||||||
|
|
||||||
export interface ModelListener {
|
|
||||||
invalidated(model: DataModel): void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Holding the data linked to a given schema
|
|
||||||
*/
|
|
||||||
export class DataModel {
|
|
||||||
data: any
|
|
||||||
schema: INode<any>
|
|
||||||
/** A list of listeners that want to be notified when the model is invalidated */
|
|
||||||
listeners: ModelListener[]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param schema node to use as schema for this model
|
|
||||||
*/
|
|
||||||
constructor(schema: INode<any>) {
|
|
||||||
this.schema = schema
|
|
||||||
this.data = schema.default()
|
|
||||||
this.listeners = []
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param listener the listener to notify when the model is invalidated
|
|
||||||
*/
|
|
||||||
addListener(listener: ModelListener) {
|
|
||||||
this.listeners.push(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force notify all listeners that the model is invalidated
|
|
||||||
*/
|
|
||||||
invalidate() {
|
|
||||||
this.listeners.forEach(listener => listener.invalidated(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the full data and notifies listeners
|
|
||||||
* @param value new model data
|
|
||||||
*/
|
|
||||||
reset(value: any) {
|
|
||||||
this.data = value
|
|
||||||
this.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the data at a specified path
|
|
||||||
* @param path path at which to find the data
|
|
||||||
* @returns undefined, if the the path does not exist in the data
|
|
||||||
*/
|
|
||||||
get(path: Path) {
|
|
||||||
let node = this.data;
|
|
||||||
for (let index of path) {
|
|
||||||
if (node === undefined) return node
|
|
||||||
node = node[index]
|
|
||||||
}
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the date on a path. Node will be removed when value is undefined
|
|
||||||
* @param path path to update
|
|
||||||
* @param value new data at the specified path
|
|
||||||
*/
|
|
||||||
set(path: Path, value: any) {
|
|
||||||
let node = this.data;
|
|
||||||
for (let index of path.pop()) {
|
|
||||||
if (node[index] === undefined) {
|
|
||||||
node[index] = {}
|
|
||||||
}
|
|
||||||
node = node[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Set', path.toString(), JSON.stringify(value))
|
|
||||||
|
|
||||||
if (value === undefined || (typeof value === 'number' && isNaN(value))) {
|
|
||||||
if (typeof path.last() === 'number') {
|
|
||||||
node.splice(path.last(), 1)
|
|
||||||
} else {
|
|
||||||
delete node[path.last()]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
node[path.last()] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
this.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { DataModel } from "./DataModel"
|
|
||||||
|
|
||||||
export type PathElement = (string | number)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Immutable helper class to represent a path in data
|
|
||||||
* @implements {Iterable<PathElement>}
|
|
||||||
*/
|
|
||||||
export class Path implements Iterable<PathElement> {
|
|
||||||
private arr: PathElement[]
|
|
||||||
model?: DataModel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param arr Initial array of path elements. Empty if not given
|
|
||||||
* @param model Model attached to this path
|
|
||||||
*/
|
|
||||||
constructor(arr?: PathElement[], model?: DataModel) {
|
|
||||||
this.arr = arr ?? []
|
|
||||||
this.model = model
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The last element of this path
|
|
||||||
*/
|
|
||||||
last(): PathElement {
|
|
||||||
return this.arr[this.arr.length - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A new path with the last element removed
|
|
||||||
*/
|
|
||||||
pop(): Path {
|
|
||||||
return new Path(this.arr.slice(0, -1), this.model)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A new path with an element added at the end
|
|
||||||
* @param element element to push at the end of the array
|
|
||||||
*/
|
|
||||||
push(element: PathElement): Path {
|
|
||||||
return new Path([...this.arr, element], this.model)
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(): Path {
|
|
||||||
return new Path([...this.arr], this.model)
|
|
||||||
}
|
|
||||||
|
|
||||||
getArray(): PathElement[] {
|
|
||||||
return this.arr
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attaches a model to this path and all paths created from this
|
|
||||||
* @param model
|
|
||||||
*/
|
|
||||||
withModel(model: DataModel): Path {
|
|
||||||
return new Path([...this.arr], model)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the data from the model if it was attached
|
|
||||||
* @returns undefined, if no model was attached
|
|
||||||
*/
|
|
||||||
get(): any {
|
|
||||||
return this.model?.get(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
return `[${this.arr.map(e => e.toString()).join(', ')}]`
|
|
||||||
}
|
|
||||||
|
|
||||||
*[Symbol.iterator]() {
|
|
||||||
for (const e of this.arr) {
|
|
||||||
yield e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
import { DataModel } from "../model/DataModel"
|
|
||||||
import { Path } from "../model/Path"
|
|
||||||
import { TreeView } from "../view/TreeView"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Schema node that supports some standard transformations
|
|
||||||
*/
|
|
||||||
export interface INode<T> {
|
|
||||||
default: (value?: T) => T | undefined
|
|
||||||
transform: (path: Path, value: T) => any
|
|
||||||
enabled: (path: Path, model: DataModel) => boolean
|
|
||||||
render: (path: Path, value: T, view: TreeView, options?: RenderOptions) => string
|
|
||||||
renderRaw: (path: Path, value: T, view: TreeView, options?: RenderOptions) => string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StateNode<T> extends INode<T> {
|
|
||||||
getState: (el: Element) => T
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RenderOptions = {
|
|
||||||
hideLabel?: boolean
|
|
||||||
syncModel?: boolean
|
|
||||||
collapse?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NodeChildren = {
|
|
||||||
[name: string]: INode<any>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IDefault<T> = (value?: T) => T | undefined
|
|
||||||
export type ITransform<T> = (value: T) => any
|
|
||||||
export type IEnable = (path: Path) => boolean
|
|
||||||
export type IForce = () => boolean
|
|
||||||
|
|
||||||
export interface NodeMods<T> {
|
|
||||||
default?: IDefault<T>
|
|
||||||
transform?: ITransform<T>
|
|
||||||
enable?: IEnable
|
|
||||||
force?: IForce
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Basic implementation of the nodes
|
|
||||||
*
|
|
||||||
* h
|
|
||||||
*/
|
|
||||||
export abstract class AbstractNode<T> implements INode<T> {
|
|
||||||
defaultMod: IDefault<T>
|
|
||||||
transformMod: ITransform<T>
|
|
||||||
enableMod: IEnable
|
|
||||||
forceMod: IForce
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mods modifiers of the default transformations
|
|
||||||
*/
|
|
||||||
constructor(mods?: NodeMods<T>) {
|
|
||||||
this.defaultMod = mods?.default ?? ((v) => v)
|
|
||||||
this.transformMod = mods?.transform ?? ((v) => v)
|
|
||||||
this.enableMod = mods?.enable ?? (() => true)
|
|
||||||
this.forceMod = mods?.force ?? (() => false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs when the element is mounted to the DOM
|
|
||||||
* @param el mounted element
|
|
||||||
* @param path data path that this node represents
|
|
||||||
* @param view corresponding tree view where this was mounted
|
|
||||||
*/
|
|
||||||
mounted(el: Element, path: Path, view: TreeView) {
|
|
||||||
el.addEventListener('change', evt => {
|
|
||||||
this.updateModel(el, path, view.model)
|
|
||||||
evt.stopPropagation()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs when the DOM element 'change' event is called
|
|
||||||
* @param el mounted element
|
|
||||||
* @param path data path that this node represents
|
|
||||||
* @param model corresponding model
|
|
||||||
*/
|
|
||||||
updateModel(el: Element, path: Path, model: DataModel) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default value of this node
|
|
||||||
* @param value optional original value
|
|
||||||
*/
|
|
||||||
default(value?: T) {
|
|
||||||
return this.defaultMod(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms the data model to the final output format
|
|
||||||
* @param
|
|
||||||
*/
|
|
||||||
transform(path: Path, value: T) {
|
|
||||||
if (!this.enabled(path)) return undefined
|
|
||||||
if (value === undefined && this.force()) value = this.default(value)!
|
|
||||||
return this.transformMod(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether the node should be enabled for this path
|
|
||||||
* @param path
|
|
||||||
* @param model
|
|
||||||
*/
|
|
||||||
enabled(path: Path, model?: DataModel) {
|
|
||||||
if (model) path = path.withModel(model)
|
|
||||||
return this.enableMod(path.pop())
|
|
||||||
}
|
|
||||||
|
|
||||||
force(): boolean {
|
|
||||||
return this.forceMod()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps and renders the node
|
|
||||||
* @param path location
|
|
||||||
* @param value data used at
|
|
||||||
* @param view tree view context, containing the model
|
|
||||||
* @param options optional render options
|
|
||||||
* @returns string HTML wrapped representation of this node using the given data
|
|
||||||
*/
|
|
||||||
render(path: Path, value: T, view: TreeView, options?: RenderOptions): string {
|
|
||||||
if (!this.enabled(path, view.model)) return ''
|
|
||||||
|
|
||||||
const id = view.register(el => {
|
|
||||||
this.mounted(el, path, view)
|
|
||||||
})
|
|
||||||
return `<div data-id="${id}" class="node ${this.getClassName()}">
|
|
||||||
${this.renderRaw(path, value, view, options)}
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the node and handles events to update the model
|
|
||||||
* @param path
|
|
||||||
* @param value
|
|
||||||
* @param view tree view context, containing the model
|
|
||||||
* @param options optional render options
|
|
||||||
* @returns string HTML representation of this node using the given data
|
|
||||||
*/
|
|
||||||
abstract renderRaw(path: Path, value: T, view: TreeView, options?: RenderOptions): string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The CSS classname used in the wrapped <div>
|
|
||||||
*/
|
|
||||||
abstract getClassName(): string
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { AbstractNode, NodeMods, RenderOptions } from "./AbstractNode";
|
|
||||||
import { Path } from "../model/Path";
|
|
||||||
import { TreeView } from "../view/TreeView";
|
|
||||||
import { locale } from "../Registries";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boolean node with two buttons for true/false
|
|
||||||
*/
|
|
||||||
export class BooleanNode extends AbstractNode<boolean> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mods optional node modifiers
|
|
||||||
*/
|
|
||||||
constructor(mods?: NodeMods<boolean>) {
|
|
||||||
super({
|
|
||||||
default: () => false,
|
|
||||||
...mods})
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRaw(path: Path, value: boolean, view: TreeView, options?: RenderOptions) {
|
|
||||||
const falseButton = view.registerClick(el => {
|
|
||||||
view.model.set(path, !this.force() && value === false ? undefined : false)
|
|
||||||
})
|
|
||||||
const trueButton = view.registerClick(el => {
|
|
||||||
view.model.set(path, !this.force() && value === true ? undefined : true)
|
|
||||||
})
|
|
||||||
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
|
|
||||||
<button${value === false ? ' style="font-weight: bold"' : ' '}
|
|
||||||
data-id="${falseButton}">${locale('false')}</button>
|
|
||||||
<button${value === true ? ' style="font-weight: bold"' : ' '}
|
|
||||||
data-id="${trueButton}">${locale('true')}</button>`
|
|
||||||
}
|
|
||||||
|
|
||||||
getClassName() {
|
|
||||||
return 'boolean-node'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { AbstractNode, NodeMods, RenderOptions, StateNode } from './AbstractNode'
|
|
||||||
import { DataModel } from '../model/DataModel'
|
|
||||||
import { TreeView } from '../view/TreeView'
|
|
||||||
import { Path } from '../model/Path'
|
|
||||||
import { locale } from '../Registries'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enum node that shows a list of options to choose from
|
|
||||||
*/
|
|
||||||
export class EnumNode extends AbstractNode<string> implements StateNode<string> {
|
|
||||||
protected options: string[]
|
|
||||||
static className = 'enum-node'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param options options to choose from in the select
|
|
||||||
* @param mods optional node modifiers or a string to be the default value
|
|
||||||
*/
|
|
||||||
constructor(options: string[], mods?: NodeMods<string> | string) {
|
|
||||||
super(typeof mods === 'string' ? {
|
|
||||||
default: () => mods,
|
|
||||||
force: () => true
|
|
||||||
} : mods)
|
|
||||||
this.options = options
|
|
||||||
}
|
|
||||||
|
|
||||||
getState(el: Element) {
|
|
||||||
return el.querySelector('select')!.value
|
|
||||||
}
|
|
||||||
|
|
||||||
updateModel(el: Element, path: Path, model: DataModel) {
|
|
||||||
model.set(path, this.getState(el))
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRaw(path: Path, value: string, view: TreeView, options?: RenderOptions) {
|
|
||||||
const id = view.register(el => (el as HTMLInputElement).value = value)
|
|
||||||
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
|
|
||||||
<select data-id=${id}>
|
|
||||||
${this.options.map(o =>
|
|
||||||
`<option value="${o}">${locale(path.push(o))}</option>`
|
|
||||||
).join('')}
|
|
||||||
</select>`
|
|
||||||
}
|
|
||||||
|
|
||||||
getClassName() {
|
|
||||||
return 'enum-node'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { AbstractNode, NodeMods, INode } from './AbstractNode'
|
|
||||||
import { DataModel } from '../model/DataModel'
|
|
||||||
import { TreeView } from '../view/TreeView'
|
|
||||||
import { Path } from '../model/Path'
|
|
||||||
import { IObject } from './ObjectNode'
|
|
||||||
import { locale } from '../Registries'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List node where children can be added and removed from
|
|
||||||
*/
|
|
||||||
export class ListNode extends AbstractNode<IObject[]> {
|
|
||||||
protected children: INode<any>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param values node used for its children
|
|
||||||
* @param mods optional node modifiers
|
|
||||||
*/
|
|
||||||
constructor(values: INode<any>, mods?: NodeMods<IObject[]>) {
|
|
||||||
super({
|
|
||||||
default: () => [],
|
|
||||||
...mods})
|
|
||||||
this.children = values
|
|
||||||
}
|
|
||||||
|
|
||||||
transform(path: Path, value: IObject[]) {
|
|
||||||
if (!(value instanceof Array)) return undefined
|
|
||||||
const res = value.map((obj, index) =>
|
|
||||||
this.children.transform(path.push(index), obj)
|
|
||||||
)
|
|
||||||
return this.transformMod(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateModel(el: Element, path: Path, model: DataModel) {
|
|
||||||
model.set(path, el.querySelector('select')?.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRaw(path: Path, value: IObject[], view: TreeView) {
|
|
||||||
value = value || []
|
|
||||||
const button = view.registerClick(el => {
|
|
||||||
view.model.set(path, [...value, this.children.default()])
|
|
||||||
})
|
|
||||||
return `<label>${locale(path)}:</label>
|
|
||||||
<button data-id="${button}">${locale('add')}</button>
|
|
||||||
<div class="list-fields">
|
|
||||||
${value.map((obj, index) => {
|
|
||||||
return this.renderEntry(path.push(index), obj, view)
|
|
||||||
}).join('')}
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderEntry(path: Path, value: IObject, view: TreeView) {
|
|
||||||
const button = view.registerClick(el => {
|
|
||||||
view.model.set(path, undefined)
|
|
||||||
})
|
|
||||||
return `<div class="list-entry"><button data-id="${button}">${locale('remove')}</button>
|
|
||||||
${this.children.render(path, value, view, {hideLabel: true})}
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
getClassName() {
|
|
||||||
return 'list-node'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { AbstractNode, NodeMods, INode, StateNode } from './AbstractNode'
|
|
||||||
import { TreeView } from '../view/TreeView'
|
|
||||||
import { Path } from '../model/Path'
|
|
||||||
import { IObject } from './ObjectNode'
|
|
||||||
import { locale } from '../Registries'
|
|
||||||
|
|
||||||
export type IMap = {
|
|
||||||
[name: string]: IObject
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map nodes similar to list nodes, but a string key is required to add children
|
|
||||||
*/
|
|
||||||
export class MapNode extends AbstractNode<IMap> {
|
|
||||||
protected keys: StateNode<string>
|
|
||||||
protected values: INode<any>
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param keys node used for the string key
|
|
||||||
* @param values node used for the map values
|
|
||||||
* @param mods optional node modifiers
|
|
||||||
*/
|
|
||||||
constructor(keys: StateNode<string>, values: INode<any>, mods?: NodeMods<IMap>) {
|
|
||||||
super({
|
|
||||||
default: () => ({}),
|
|
||||||
...mods})
|
|
||||||
this.keys = keys
|
|
||||||
this.values = values
|
|
||||||
}
|
|
||||||
|
|
||||||
transform(path: Path, value: IMap) {
|
|
||||||
if (value === undefined) return undefined
|
|
||||||
let res: any = {}
|
|
||||||
Object.keys(value).forEach(f =>
|
|
||||||
res[f] = this.values.transform(path.push(f), value[f])
|
|
||||||
)
|
|
||||||
return this.transformMod(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRaw(path: Path, value: IMap, view: TreeView) {
|
|
||||||
value = value ?? []
|
|
||||||
const button = view.registerClick(el => {
|
|
||||||
const key = this.keys.getState(el.parentElement!)
|
|
||||||
view.model.set(path.push(key), this.values.default())
|
|
||||||
})
|
|
||||||
return `<label>${locale(path)}:</label>
|
|
||||||
${this.keys.renderRaw(path, '', view, {hideLabel: true, syncModel: false})}
|
|
||||||
<button data-id="${button}">${locale('add')}</button>
|
|
||||||
<div class="map-fields">
|
|
||||||
${Object.keys(value).map(key => {
|
|
||||||
return this.renderEntry(path.push(key), value[key], view)
|
|
||||||
}).join('')}
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderEntry(path: Path, value: IObject, view: TreeView) {
|
|
||||||
const button = view.registerClick(el => {
|
|
||||||
view.model.set(path, undefined)
|
|
||||||
})
|
|
||||||
return `<div class="map-entry"><button data-id="${button}">${locale('remove')}</button>
|
|
||||||
${this.values.render(path, value, view)}
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
|
|
||||||
getClassName() {
|
|
||||||
return 'map-node'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { AbstractNode, NodeMods, RenderOptions, StateNode } from './AbstractNode'
|
|
||||||
import { Path } from '../model/Path'
|
|
||||||
import { DataModel } from '../model/DataModel'
|
|
||||||
import { TreeView } from '../view/TreeView'
|
|
||||||
import { locale } from '../Registries'
|
|
||||||
|
|
||||||
export interface NumberNodeMods extends NodeMods<number> {
|
|
||||||
/** Whether numbers should be converted to integers on input */
|
|
||||||
integer?: boolean
|
|
||||||
/** If specified, number will be capped at this minimum */
|
|
||||||
min?: number
|
|
||||||
/** If specified, number will be capped at this maximum */
|
|
||||||
max?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configurable number node with one text field
|
|
||||||
*/
|
|
||||||
export class NumberNode extends AbstractNode<number> implements StateNode<number> {
|
|
||||||
integer: boolean
|
|
||||||
min: number
|
|
||||||
max: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mods optional node modifiers
|
|
||||||
*/
|
|
||||||
constructor(mods?: NumberNodeMods) {
|
|
||||||
super({
|
|
||||||
default: () => 0,
|
|
||||||
...mods})
|
|
||||||
this.integer = mods?.integer ?? false
|
|
||||||
this.min = mods?.min ?? -Infinity
|
|
||||||
this.max = mods?.max ?? Infinity
|
|
||||||
}
|
|
||||||
|
|
||||||
getState(el: Element) {
|
|
||||||
const value = el.querySelector('input')!.value
|
|
||||||
const parsed = this.integer ? parseInt(value) : parseFloat(value)
|
|
||||||
if (parsed < this.min) return this.min
|
|
||||||
if (parsed > this.max) return this.max
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
updateModel(el: Element, path: Path, model: DataModel) {
|
|
||||||
model.set(path, this.getState(el))
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRaw(path: Path, value: number, view: TreeView, options?: RenderOptions) {
|
|
||||||
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
|
|
||||||
<input value="${value ?? ''}">`
|
|
||||||
}
|
|
||||||
|
|
||||||
getClassName() {
|
|
||||||
return 'number-node'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { NodeMods, INode, NodeChildren, AbstractNode, RenderOptions } from './AbstractNode'
|
|
||||||
import { Path } from '../model/Path'
|
|
||||||
import { TreeView } from '../view/TreeView'
|
|
||||||
import { locale } from '../Registries'
|
|
||||||
|
|
||||||
export const Switch = Symbol('switch')
|
|
||||||
export const Case = Symbol('case')
|
|
||||||
|
|
||||||
export type NestedNodeChildren = {
|
|
||||||
[name: string]: NodeChildren
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IObject = {
|
|
||||||
[name: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FilteredChildren = {
|
|
||||||
[name: string]: INode<any>
|
|
||||||
/** The field to filter on */
|
|
||||||
[Switch]?: string
|
|
||||||
/** Map of filter values to node fields */
|
|
||||||
[Case]?: NestedNodeChildren
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ObjectNodeMods extends NodeMods<object> {
|
|
||||||
/** Whether the object can be collapsed. Necessary when recursively nesting. */
|
|
||||||
collapse?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Object node containing fields with different types.
|
|
||||||
* Has the ability to filter fields based on a switch field.
|
|
||||||
*/
|
|
||||||
export class ObjectNode extends AbstractNode<IObject> {
|
|
||||||
fields: NodeChildren
|
|
||||||
cases: NestedNodeChildren
|
|
||||||
filter?: string
|
|
||||||
collapse?: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param fields children containing the optional switch and case
|
|
||||||
* @param mods optional node modifiers
|
|
||||||
*/
|
|
||||||
constructor(fields: FilteredChildren, mods?: ObjectNodeMods) {
|
|
||||||
super({
|
|
||||||
default: () => ({}),
|
|
||||||
...mods})
|
|
||||||
this.collapse = mods?.collapse ?? false
|
|
||||||
const {[Switch]: _switch, [Case]: _case, ..._fields} = fields
|
|
||||||
this.fields = _fields
|
|
||||||
this.cases = _case ?? {}
|
|
||||||
this.filter = _switch
|
|
||||||
}
|
|
||||||
|
|
||||||
transform(path: Path, value: IObject) {
|
|
||||||
if (value === undefined) return undefined
|
|
||||||
const activeCase = this.filter ? this.cases[value[this.filter]] : {};
|
|
||||||
const activeFields = {...this.fields, ...activeCase}
|
|
||||||
let res: any = {}
|
|
||||||
Object.keys(activeFields).forEach(f => {
|
|
||||||
return res[f] = activeFields[f].transform(path.push(f), value[f])
|
|
||||||
})
|
|
||||||
return this.transformMod(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRaw(path: Path, value: IObject, view: TreeView, options?: RenderOptions) {
|
|
||||||
if (options?.hideLabel) {
|
|
||||||
return this.renderFields(path, value, view)
|
|
||||||
} else if (this.collapse || options?.collapse) {
|
|
||||||
if (value === undefined) {
|
|
||||||
const id = view.registerClick(() => view.model.set(path, this.default()))
|
|
||||||
return `<label class="collapse closed" data-id="${id}">${locale(path)}</label>`
|
|
||||||
} else {
|
|
||||||
const id = view.registerClick(() => view.model.set(path, undefined))
|
|
||||||
return `<label class="collapse open" data-id="${id}">${locale(path)}</label>
|
|
||||||
<div class="object-fields">
|
|
||||||
${this.renderFields(path, value, view)}
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return `<label>${locale(path)}</label>
|
|
||||||
<div class="object-fields">
|
|
||||||
${this.renderFields(path, value, view)}
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFields(path: Path, value: IObject, view: TreeView) {
|
|
||||||
value = value ?? {}
|
|
||||||
const activeCase = this.filter ? this.cases[value[this.filter]] : {};
|
|
||||||
const activeFields = {...this.fields, ...activeCase}
|
|
||||||
return Object.keys(activeFields).map(f => {
|
|
||||||
return activeFields[f].render(path.push(f), value[f], view)
|
|
||||||
}).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
getClassName() {
|
|
||||||
return 'object-node'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { AbstractNode, NodeMods, RenderOptions, INode } from './AbstractNode'
|
|
||||||
import { TreeView } from '../view/TreeView'
|
|
||||||
import { Path } from '../model/Path'
|
|
||||||
import { SCHEMAS } from '../Registries'
|
|
||||||
|
|
||||||
export interface AnyNodeMods extends NodeMods<any> {
|
|
||||||
[name: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reference node. Must be used when recursively adding nodes.
|
|
||||||
*/
|
|
||||||
export class ReferenceNode extends AbstractNode<any> {
|
|
||||||
protected reference: () => INode<any>
|
|
||||||
options: RenderOptions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param id schema id that was registered
|
|
||||||
* @param mods optional node modifiers
|
|
||||||
*/
|
|
||||||
constructor(id: string, mods?: AnyNodeMods) {
|
|
||||||
super(mods)
|
|
||||||
this.options = {
|
|
||||||
collapse: mods?.collapse
|
|
||||||
}
|
|
||||||
this.reference = () => SCHEMAS.get(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
default(value?: any) {
|
|
||||||
return this.reference().default(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
transform(path: Path, value: any) {
|
|
||||||
return this.reference()?.transform(path, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
render(path: Path, value: any, view: TreeView, options?: RenderOptions) {
|
|
||||||
return this.reference()?.render(path, value, view, {...this.options, ...options})
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRaw(path: Path, value: any, view: TreeView, options?: RenderOptions) {
|
|
||||||
return this.reference()?.renderRaw(path, value, view, {...this.options, ...options})
|
|
||||||
}
|
|
||||||
|
|
||||||
getClassName() {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { AbstractNode, NodeMods, RenderOptions, StateNode } from './AbstractNode'
|
|
||||||
import { Path } from '../model/Path'
|
|
||||||
import { DataModel } from '../model/DataModel'
|
|
||||||
import { TreeView } from '../view/TreeView'
|
|
||||||
import { locale } from '../Registries'
|
|
||||||
|
|
||||||
export interface StringNodeMods extends NodeMods<string> {
|
|
||||||
/** Whether the string can also be empty */
|
|
||||||
allowEmpty?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple string node with one text field
|
|
||||||
*/
|
|
||||||
export class StringNode extends AbstractNode<string> implements StateNode<string> {
|
|
||||||
allowEmpty: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mods optional node modifiers
|
|
||||||
*/
|
|
||||||
constructor(mods?: StringNodeMods) {
|
|
||||||
super(mods)
|
|
||||||
this.allowEmpty = mods?.allowEmpty ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
getState(el: Element) {
|
|
||||||
return el.querySelector('input')!.value
|
|
||||||
}
|
|
||||||
|
|
||||||
updateModel(el: Element, path: Path, model: DataModel) {
|
|
||||||
const value = this.getState(el)
|
|
||||||
model.set(path, this.allowEmpty || value !== '' ? value : undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRaw(path: Path, value: string, view: TreeView, options?: RenderOptions) {
|
|
||||||
return `${options?.hideLabel ? `` : `<label>${locale(path)}</label>`}
|
|
||||||
<input value="${value ?? ''}">`
|
|
||||||
}
|
|
||||||
|
|
||||||
getClassName() {
|
|
||||||
return 'string-node'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { DataModel, ModelListener } from "../model/DataModel"
|
|
||||||
import { Path } from "../model/Path"
|
|
||||||
|
|
||||||
type SourceViewOptions = {
|
|
||||||
indentation?: number | string,
|
|
||||||
rows?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON representation view of the model.
|
|
||||||
* Renders the result in a <textarea>.
|
|
||||||
*/
|
|
||||||
export class SourceView implements ModelListener {
|
|
||||||
model: DataModel
|
|
||||||
target: HTMLElement
|
|
||||||
options?: SourceViewOptions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param model data model this view represents and listens to
|
|
||||||
* @param target DOM element to render the view
|
|
||||||
* @param options optional options for the view
|
|
||||||
*/
|
|
||||||
constructor(model: DataModel, target: HTMLElement, options?: SourceViewOptions) {
|
|
||||||
this.model = model
|
|
||||||
this.target = target
|
|
||||||
this.options = options
|
|
||||||
model.addListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const transformed = this.model.schema.transform(new Path([], this.model), this.model.data)
|
|
||||||
const textarea = document.createElement('textarea')
|
|
||||||
textarea.style.width = 'calc(100% - 6px)'
|
|
||||||
textarea.rows = this.options?.rows ?? 20
|
|
||||||
textarea.textContent = JSON.stringify(transformed, null, this.options?.indentation)
|
|
||||||
textarea.addEventListener('change', evt => {
|
|
||||||
const parsed = JSON.parse(textarea.value)
|
|
||||||
this.model.reset(parsed)
|
|
||||||
})
|
|
||||||
this.target.innerHTML = ''
|
|
||||||
this.target.appendChild(textarea)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-renders the view
|
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
invalidated() {
|
|
||||||
this.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { DataModel, ModelListener } from "../model/DataModel"
|
|
||||||
import { Path } from "../model/Path"
|
|
||||||
|
|
||||||
type Registry = {
|
|
||||||
[id: string]: (el: Element) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const registryIdLength = 12
|
|
||||||
const dec2hex = (dec: number) => ('0' + dec.toString(16)).substr(-2)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to generate a random ID
|
|
||||||
*/
|
|
||||||
export function getId() {
|
|
||||||
var arr = new Uint8Array((registryIdLength || 40) / 2)
|
|
||||||
window.crypto.getRandomValues(arr)
|
|
||||||
return Array.from(arr, dec2hex).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DOM representation view of the model.
|
|
||||||
*/
|
|
||||||
export class TreeView implements ModelListener {
|
|
||||||
model: DataModel
|
|
||||||
target: HTMLElement
|
|
||||||
registry: Registry = {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param model data model this view represents and listens to
|
|
||||||
* @param target DOM element to render the view
|
|
||||||
*/
|
|
||||||
constructor(model: DataModel, target: HTMLElement) {
|
|
||||||
this.model = model
|
|
||||||
this.target = target
|
|
||||||
model.addListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a callback and gives an ID
|
|
||||||
* @param callback function that is called when the element is mounted
|
|
||||||
* @returns the ID that should be applied to the data-id attribute
|
|
||||||
*/
|
|
||||||
register(callback: (el: Element) => void): string {
|
|
||||||
const id = getId()
|
|
||||||
this.registry[id] = callback
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers an event and gives an ID
|
|
||||||
* @param type event type
|
|
||||||
* @param callback function that is called when the event is fired
|
|
||||||
* @returns the ID that should be applied to the data-id attribute
|
|
||||||
*/
|
|
||||||
registerEvent(type: string, callback: (el: Element) => void): string {
|
|
||||||
return this.register(el => {
|
|
||||||
el.addEventListener(type, evt => {
|
|
||||||
callback(el)
|
|
||||||
evt.stopPropagation()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a change event and gives an ID
|
|
||||||
* @param callback function that is called when the event is fired
|
|
||||||
* @returns the ID that should be applied to the data-id attribute
|
|
||||||
*/
|
|
||||||
registerChange(callback: (el: Element) => void): string {
|
|
||||||
return this.registerEvent('change', callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a click event and gives an ID
|
|
||||||
* @param callback function that is called when the event is fired
|
|
||||||
* @returns the ID that should be applied to the data-id attribute
|
|
||||||
*/
|
|
||||||
registerClick(callback: (el: Element) => void): string {
|
|
||||||
return this.registerEvent('click', callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.target.innerHTML = this.model.schema.render(
|
|
||||||
new Path(), this.model.data, this, {hideLabel: true})
|
|
||||||
for (const id in this.registry) {
|
|
||||||
const element = this.target.querySelector(`[data-id="${id}"]`)
|
|
||||||
if (element !== null) this.registry[id](element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-renders the view
|
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
invalidated(model: DataModel) {
|
|
||||||
this.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"target": "es5",
|
"target": "es5",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"forceConsistentCasingInFileNames": true
|
"forceConsistentCasingInFileNames": true
|
||||||
|
|||||||
Reference in New Issue
Block a user