diff --git a/CHANGELOG.md b/CHANGELOG.md index a5a49894..b337e1ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Fixed +- Improved NPC rendering, positions should now match the game exactly [#130](https://github.com/CCDirectLink/crosscode-map-editor/issues/130), [#132](https://github.com/CCDirectLink/crosscode-map-editor/issues/132) + ## [2.4.1] 2026-05-16 ### Fixed - Fixed lightmap rendering using inverted black values diff --git a/webapp/src/app/components/widgets/person-expression-widget/custom-expression-widget/custom-expression-widget.component.ts b/webapp/src/app/components/widgets/person-expression-widget/custom-expression-widget/custom-expression-widget.component.ts index 3308a763..e12ced64 100644 --- a/webapp/src/app/components/widgets/person-expression-widget/custom-expression-widget/custom-expression-widget.component.ts +++ b/webapp/src/app/components/widgets/person-expression-widget/custom-expression-widget/custom-expression-widget.component.ts @@ -110,7 +110,7 @@ export class CustomExpressionWidgetComponent extends OverlayWidget imple private async getFace() { const person = (this.settings.person ?? '').replaceAll('.', '/'); let sheet = (await Helper.getJsonPromise('data/characters/' + person) as CharacterSettings | undefined) ?? {}; - sheet.jsonTEMPLATES = getNPCTemplates(); + sheet.jsonTEMPLATES = await getNPCTemplates(); sheet = prepareSheet(sheet); let face: Face = sheet.face ?? {}; diff --git a/webapp/src/app/services/phaser/entities/registry/direction.ts b/webapp/src/app/services/phaser/entities/registry/direction.ts new file mode 100644 index 00000000..b9b72ee3 --- /dev/null +++ b/webapp/src/app/services/phaser/entities/registry/direction.ts @@ -0,0 +1,64 @@ +import { Anims, flattenSUBs } from '../../sheet-parser'; + +// Unit face vectors. Mirrors CC's FACE8. +export const FACE_VECTORS: Record = { + NORTH: { x: 0, y: -1 }, + NORTH_EAST: { x: 1, y: -1 }, + EAST: { x: 1, y: 0 }, + SOUTH_EAST: { x: 1, y: 1 }, + SOUTH: { x: 0, y: 1 }, + SOUTH_WEST: { x: -1, y: 1 }, + WEST: { x: -1, y: 0 }, + NORTH_WEST: { x: -1, y: -1 }, +}; + +// Port of ig.getDirectionIndex — picks a tileOffsets slot from a face vector + dir count. +export function getDirectionIndex(faceX: number, faceY: number, numDirs: number): number { + switch (numDirs) { + case 1: + return 0; + case 2: + return faceX >= 0 ? 0 : 1; + case 4: + return Math.abs(faceY) > Math.abs(faceX) + ? (faceY < 0 ? 0 : 2) + : (faceX > 0 ? 1 : 3); + case 6: + return faceX >= 0 + ? (faceY <= 0 + ? 0 + (57 * faceX > -100 * faceY ? 1 : 0) + : 1 + (57 * faceX < 100 * faceY ? 1 : 0)) + : (faceY <= 0 + ? 4 + (-57 * faceX < -100 * faceY ? 1 : 0) + : 3 + (-57 * faceX > 100 * faceY ? 1 : 0)); + case 8: + return Math.abs(faceY) > 2.414 * Math.abs(faceX) + ? (faceY < 0 ? 0 : 4) + : Math.abs(faceX) > 2.414 * Math.abs(faceY) + ? (faceX > 0 ? 2 : 6) + : (faceX > 0 ? (faceY < 0 ? 1 : 3) : (faceY > 0 ? 5 : 7)); + default: + return Math.floor(numDirs / 2); + } +} + +export function resolveDirIndex(anims: Anims, face: string | undefined, animName?: string): number | undefined { + const vec = face ? FACE_VECTORS[face] : undefined; + if (!vec) { + return undefined; + } + const leaves = flattenSUBs(anims, {}); + const hasDirs = (leaf: Anims) => Array.isArray(leaf.tileOffsets) && leaf.tileOffsets.length > 0; + const leaf = (animName ? leaves.find(l => l.name === animName) : undefined) ?? leaves.find(hasDirs) ?? leaves[0]; + if (!leaf) { + return undefined; + } + let numDirs = typeof leaf.dirs === 'string' ? parseInt(leaf.dirs, 10) : leaf.dirs; + if (!numDirs && Array.isArray(leaf.tileOffsets)) { + numDirs = leaf.tileOffsets.length; + } + if (!numDirs) { + return undefined; + } + return getDirectionIndex(vec.x, vec.y, numDirs); +} diff --git a/webapp/src/app/services/phaser/entities/registry/enemy.ts b/webapp/src/app/services/phaser/entities/registry/enemy.ts index ca14cf0f..bacbf119 100644 --- a/webapp/src/app/services/phaser/entities/registry/enemy.ts +++ b/webapp/src/app/services/phaser/entities/registry/enemy.ts @@ -5,6 +5,7 @@ import { Fix } from '../cc-entity'; import { Helper } from '../../helper'; import { Anims, AnimSheet, Effect, flattenSUBs } from '../../sheet-parser'; import { DefaultEntity } from './default-entity'; +import { resolveDirIndex } from './direction'; interface MultiEntityAnim extends Anims { anims: Record; @@ -27,48 +28,6 @@ interface EntityPart { size: Point3; } -// Unit face vectors (screen coords: +y points down, so NORTH is -y). Mirrors CC's FACE8. -const FACE_VECTORS: Record = { - NORTH: { x: 0, y: -1 }, - NORTH_EAST: { x: 1, y: -1 }, - EAST: { x: 1, y: 0 }, - SOUTH_EAST: { x: 1, y: 1 }, - SOUTH: { x: 0, y: 1 }, - SOUTH_WEST: { x: -1, y: 1 }, - WEST: { x: -1, y: 0 }, - NORTH_WEST: { x: -1, y: -1 }, -}; - -// Port of ig.getDirectionIndex — picks a tileOffsets slot from a face vector + dir count. -function getDirectionIndex(faceX: number, faceY: number, numDirs: number): number { - switch (numDirs) { - case 1: - return 0; - case 2: - return faceX >= 0 ? 0 : 1; - case 4: - return Math.abs(faceY) > Math.abs(faceX) - ? (faceY < 0 ? 0 : 2) - : (faceX > 0 ? 1 : 3); - case 6: - return faceX >= 0 - ? (faceY <= 0 - ? 0 + (57 * faceX > -100 * faceY ? 1 : 0) - : 1 + (57 * faceX < 100 * faceY ? 1 : 0)) - : (faceY <= 0 - ? 4 + (-57 * faceX < -100 * faceY ? 1 : 0) - : 3 + (-57 * faceX > 100 * faceY ? 1 : 0)); - case 8: - return Math.abs(faceY) > 2.414 * Math.abs(faceX) - ? (faceY < 0 ? 0 : 4) - : Math.abs(faceX) > 2.414 * Math.abs(faceY) - ? (faceX > 0 ? 2 : 6) - : (faceX > 0 ? (faceY < 0 ? 1 : 3) : (faceY > 0 ? 5 : 7)); - default: - return Math.floor(numDirs / 2); - } -} - export interface EnemyAttributes { enemyInfo?: EnemyInfo; spawnCondition?: string; @@ -140,29 +99,10 @@ export class Enemy extends DefaultEntity { animName: 'idle', label: settings.enemyInfo.type, baseSize: enemyData.size, - dirIndex: this.resolveDirIndex(rawSheet, settings.enemyInfo.face), + dirIndex: resolveDirIndex(rawSheet, settings.enemyInfo.face), }); } - private resolveDirIndex(anims: Anims, face: string | undefined): number | undefined { - // Find the numDirs used by this anim tree: first tileOffsets array encountered. - let numDirs = 0; - for (const leaf of flattenSUBs(anims, {})) { - if (Array.isArray(leaf.tileOffsets) && leaf.tileOffsets.length > 0) { - numDirs = leaf.tileOffsets.length; - break; - } - } - if (!numDirs) { - return undefined; - } - const vec = face ? FACE_VECTORS[face] : undefined; - if (!vec) { - return undefined; - } - return getDirectionIndex(vec.x, vec.y, numDirs); - } - private renderMultiEntity(animation: MultiEntityAnim, baseSize: Point3): boolean { const anim = animation.anims['idle'] ?? animation.anims['default'] diff --git a/webapp/src/app/services/phaser/entities/registry/npc-templates.ts b/webapp/src/app/services/phaser/entities/registry/npc-templates.ts index fac10c61..a8392b5d 100644 --- a/webapp/src/app/services/phaser/entities/registry/npc-templates.ts +++ b/webapp/src/app/services/phaser/entities/registry/npc-templates.ts @@ -1,176 +1,7 @@ -import { Anims, SubJsonParam } from '../../sheet-parser'; -import { Configs, Face, WalkAnimSet } from './npc'; +import { Globals } from '../../../globals'; -export interface NPCTemplates { - [key: string]: NPCTemplate; -} - -export interface NPCTemplate { - name: SubJsonParam; - gender: SubJsonParam; - animSheet: Anims; - walkAnimSet: WalkAnimSet; - walkAnims: string; - configs: Configs; - face: Face; - realname?: SubJsonParam; -} +export type NPCTemplates = typeof import('../../../../../assets/json-templates.json'); -export function getNPCTemplates(): NPCTemplates { - return { - NPCBasic: { - 'name': {'jsonPARAM': 'name', 'default': null}, - 'gender': {'jsonPARAM': 'gender', 'default': null}, - 'animSheet': { - 'DOCTYPE': 'MULTI_DIR_ANIMATION', - 'namedSheets': { - // @ts-ignore - 'move': {'src': {'jsonPARAM': 'img'}, 'width': 32, 'height': 40, 'xCount': 3, 'offX': {'jsonPARAM': 'x'}, 'offY': {'jsonPARAM': 'y'}}, - // @ts-ignore - 'sit': {'jsonIF': 'sitX', 'src': {'jsonPARAM': 'img'}, 'width': 32, 'height': 40, 'xCount': 1, 'offX': {'jsonPARAM': 'sitX'}, 'offY': {'jsonPARAM': 'sitY'}} - }, - 'shapeType': 'Y_FLAT', - 'offset': {'x': 0, 'y': -2, 'z': 0}, - 'SUB': [ - { - 'sheet': 'move', - 'dirs': 4, - 'flipX': [0, 0, 0, 1], - 'tileOffsets': [0, 3, 6, 3], - 'SUB': [ - {'name': 'idle', 'time': 1, 'repeat': false, 'frames': [1]}, - {'name': 'walk', 'time': 0.133, 'repeat': true, 'frames': [0, 1, 2, 1]} - ] - }, - { - 'jsonIF': 'sitX', - 'sheet': 'sit', - 'dirs': 4, - 'flipX': [0, 0, 0, 1], - 'tileOffsets': [0, 1, 2, 1], - 'SUB': [ - {'name': 'sit', 'time': 1, 'repeat': false, 'frames': [0]} - ] - } - ] - }, - 'walkAnimSet': { - 'normal': { - 'idle': 'idle', - 'move': 'walk' - }, - 'sit': { - 'jsonIF': 'sitX', - 'idle': 'sit' - } - }, - 'walkAnims': 'normal', - 'configs': { - 'normal': { - 'relativeVel': 0.5 - }, - 'sit': { - 'jsonIF': 'sitX', - 'walkAnims': 'sit', - 'shadow': 0 - } - }, - 'face': {'ABSTRACT': {'jsonPARAM': 'face'}} - }, - NPCAvatarSimple: { - 'name': {'jsonPARAM': 'name', 'default': null}, - 'realname': {'jsonPARAM': 'realname', 'default': null}, - 'gender': {'jsonPARAM': 'gender', 'default': null}, - 'animSheet': { - 'DOCTYPE': 'MULTI_DIR_ANIMATION', - 'namedSheets': { - // @ts-ignore - 'move': {'src': {'jsonPARAM': 'img'}, 'width': 32, 'height': 40, 'xCount': 3, 'offX': {'jsonPARAM': 'x'}, 'offY': {'jsonPARAM': 'y'}}, - // @ts-ignore - 'offline': {'src': {'jsonPARAM': 'img'}, 'width': 32, 'height': 40, 'xCount': 3, 'offX': {'jsonPARAM': 'offlineX'}, 'offY': {'jsonPARAM': 'offlineY'}}, - // @ts-ignore - 'run': { - 'jsonIF': 'runSrc', - // @ts-ignore - 'src': {'jsonPARAM': 'runSrc'}, - 'width': 32, - 'height': 40, - 'xCount': 5, - // @ts-ignore - 'offX': {'jsonPARAM': 'runX'}, - // @ts-ignore - 'offY': {'jsonPARAM': 'runY'} - } - }, - 'shapeType': 'Y_FLAT', - 'offset': {'x': 0, 'y': -2, 'z': 0}, - 'SUB': [ - { - 'sheet': 'move', - 'dirs': 4, - 'flipX': [0, 0, 0, 1], - 'tileOffsets': [0, 3, 6, 3], - 'SUB': [ - {'name': 'idle', 'time': 1, 'repeat': false, 'frames': [1]}, - {'name': 'walk', 'time': 0.133, 'repeat': true, 'frames': [0, 1, 2, 1]}, - {'sheet': 'offline', 'name': 'offline', 'time': 0.166, 'repeat': true, 'frames': [0, 1, 2]} - ] - }, - { - 'jsonIF': 'runSrc', - 'sheet': 'run', - 'dirs': 6, - 'flipX': [0, 0, 0, 1, 1, 1], - 'tileOffsets': [0, 5, 10, 10, 5, 0], - 'SUB': [ - {'name': 'run', 'time': 0.1, 'repeat': true, 'frames': [0, 1, 2, 3]}, - {'name': 'jump', 'time': 0.1, 'repeat': true, 'frames': [3]}, - {'name': 'fall', 'time': 0.1, 'repeat': true, 'frames': [4]} - ] - }, - { - 'sheet': 'move', - 'dirs': 2, - 'flipX': [0, 1], - 'tileOffsets': [9, 9], - 'SUB': [ - {'name': 'ground', 'time': 1, 'repeat': false, 'frames': [0], 'offset': {'x': 0, 'y': 2, 'z': 0}} - ] - } - ] - }, - 'walkAnimSet': { - 'normal': { - 'idle': 'idle', - 'move': 'walk', - 'run': {'jsonIF': 'runSrc', 'jsonTHEN': 'run'}, - 'jump': {'jsonIF': 'runSrc', 'jsonTHEN': 'jump'}, - 'fall': {'jsonIF': 'runSrc', 'jsonTHEN': 'fall'} - }, - 'ground': { - 'idle': 'ground' - }, - 'offline': { - 'idle': 'offline' - } - }, - 'walkAnims': 'normal', - 'configs': { - 'normal': { - 'relativeVel': 0.5 - }, - 'run': { - 'jsonIF': 'runSrc', - 'relativeVel': 1 - }, - 'ground': { - 'walkAnims': 'ground' - }, - 'offline': { - 'walkAnims': 'offline' - } - }, - 'face': {'ABSTRACT': {'jsonPARAM': 'face'}} - } - }; +export function getNPCTemplates(): Promise { + return Globals.jsonLoader.loadJsonMerged('json-templates.json'); } diff --git a/webapp/src/app/services/phaser/entities/registry/npc.ts b/webapp/src/app/services/phaser/entities/registry/npc.ts index 4574c4a3..178c77f8 100644 --- a/webapp/src/app/services/phaser/entities/registry/npc.ts +++ b/webapp/src/app/services/phaser/entities/registry/npc.ts @@ -3,9 +3,9 @@ import { Point, Point3 } from '../../../../models/cross-code-map'; import { Helper } from '../../helper'; import { DefaultEntity } from './default-entity'; import { Label } from '../../../../models/events'; -import { Anims, flattenSUBs, IfThen, prepareSheet, SubJsonParam } from '../../sheet-parser'; +import { Anims, IfThen, prepareSheet, SubJsonParam } from '../../sheet-parser'; import { getNPCTemplates } from './npc-templates'; -import { Globals } from '../../../globals'; +import { resolveDirIndex } from './direction'; export interface CharacterSettings { jsonINSTANCE?: string; @@ -135,11 +135,10 @@ export interface Analyzable { text?: Label; active?: boolean; } -// TODO: use DefaultEntity functions for displaying sprites export class NPC extends DefaultEntity { - + protected override async setupType(settings: NpcAttributes) { - + let charSettings = await Helper.getJsonPromise(this.getPath('data/characters/', settings.characterName)) as CharacterSettings | undefined; if (!charSettings) { console.warn(`no char settings found for character name: [${settings.characterName}]`); @@ -153,96 +152,40 @@ export class NPC extends DefaultEntity { throw new Error('no anim sheet found for: ' + charSettings.animSheet + ' in path: ' + path); } } - - charSettings.jsonTEMPLATES = getNPCTemplates(); + + charSettings.jsonTEMPLATES = await getNPCTemplates(); charSettings = prepareSheet(charSettings); delete charSettings.jsonTEMPLATES; - - if (typeof charSettings.animSheet === 'string') { - throw new Error('should never be string'); - } - - const state = settings.npcStates?.[0] ?? {}; - const config = state.config ?? 'normal'; - const face = state.face || 'SOUTH'; - const walkAnims = charSettings.configs?.[config]?.walkAnims ?? 'normal'; - const animSet = charSettings.walkAnimSet?.[walkAnims] || Object.values(charSettings.walkAnimSet ?? {})[0]; - - const usedSet = animSet?.['idle'] || animSet?.['move'] || Object.values(animSet ?? {})[0]; - - const subName = usedSet as string; - - - if (!Array.isArray(charSettings.animSheet?.SUB)) { - console.warn(`animSheet is not an array, abort: [${settings.characterName}]`); - this.generateNoImageType(); - return; - } - const subs = flattenSUBs(charSettings.animSheet!, {}); - - let sub = subs.find(v => v.name === subName); - if (!sub) { - sub = subs[0]; - } - const sheet = sub.namedSheets?.[sub.sheet as string] ?? sub.sheet; - if (!sheet || typeof sheet === 'string') { - this.generateErrorImage(); - return; - } - const exists = await Helper.loadTexture(sheet?.src, this.scene); - - if (!exists) { + + const animSheet = charSettings.animSheet; + if (!animSheet || typeof animSheet === 'string' || !Array.isArray(animSheet.SUB)) { + console.warn(`animSheet broken for character: [${settings.characterName}]`); this.generateErrorImage(); return; } - - let x = sheet.offX ?? 0; - let y = sheet.offY ?? 0; - let flipX = false; - - let dirIndex = 0; - const subDirs = typeof sub.dirs === 'string' ? parseInt(sub.dirs, 10) : sub.dirs; - if (subDirs === 8) { - dirIndex = FACE8[face]; - } else if (subDirs === 4) { - dirIndex = FACE4[face as keyof typeof FACE4]; - } - - const img = Globals.scene.textures.get(sheet.src!).getSourceImage(); - const xCount = sheet.xCount ?? img.width / sheet.width; - - // flip x with some serious type checking - if (sub.flipX) { - if (typeof sub.flipX === 'boolean') { - flipX = sub.flipX; - } else { - const flipXNum = sub.flipX?.[dirIndex]; - if (typeof flipXNum === 'number') { - flipX = !!flipXNum; - } - } + + const state = settings.npcStates?.[0] ?? {}; + const config = state.config || 'normal'; + const face = state.face || 'NORTH'; + + const walkAnims = charSettings.configs?.[config]?.walkAnims ?? charSettings.walkAnims ?? 'normal'; + const animSet = charSettings.walkAnimSet?.[walkAnims] ?? Object.values(charSettings.walkAnimSet ?? {})[0]; + const animName = (animSet?.['idle'] ?? animSet?.['move'] ?? Object.values(animSet ?? {})[0]) as string | undefined; + + const baseSize: Point3 = charSettings.size ?? {x: 12, y: 12, z: 28}; + + const shadowSize = charSettings.configs?.[config]?.shadow ?? charSettings.shadow ?? 16; + if (shadowSize > 0) { + animSheet.shadow = {size: shadowSize, scaleY: charSettings.shadowScaleY}; } - - const tileOffsets = sub.tileOffsets; - const idleFrame = sub.frames?.[0] ?? 0; - const offset = (tileOffsets?.[dirIndex] ?? 0) + idleFrame; - - x += (offset % xCount) * sheet.width; - y += Math.floor(offset / xCount) * sheet.height; - - this.entitySettings.sheets = { - fix: [{ - gfx: sheet?.src, - x: x, - y: y, - w: sheet?.width ?? 16, - h: sheet?.height ?? 16, - flipX: flipX, - flipY: false - }] - }; - this.entitySettings.baseSize = {x: 12, y: 12, z: 28}; - this.updateSettings(); + + await this.applyAnims({ + anims: animSheet, + animName: animName, + label: settings.characterName, + baseSize: baseSize, + dirIndex: resolveDirIndex(animSheet, face, animName), + }); } private getPath(prefix: string, path?: string): string { diff --git a/webapp/src/assets/json-templates.json b/webapp/src/assets/json-templates.json new file mode 100644 index 00000000..4c191497 --- /dev/null +++ b/webapp/src/assets/json-templates.json @@ -0,0 +1,409 @@ +{ + "NPCAvatarSimple": { + "name": { + "jsonPARAM": "name", + "default": null + }, + "realname": { + "jsonPARAM": "realname", + "default": null + }, + "gender": { + "jsonPARAM": "gender", + "default": null + }, + "animSheet": { + "DOCTYPE": "MULTI_DIR_ANIMATION", + "namedSheets": { + "move": { + "src": { + "jsonPARAM": "img" + }, + "width": 32, + "height": 40, + "xCount": 3, + "offX": { + "jsonPARAM": "x" + }, + "offY": { + "jsonPARAM": "y" + } + }, + "offline": { + "src": { + "jsonPARAM": "img" + }, + "width": 32, + "height": 40, + "xCount": 3, + "offX": { + "jsonPARAM": "offlineX" + }, + "offY": { + "jsonPARAM": "offlineY" + } + }, + "run": { + "jsonIF": "runSrc", + "src": { + "jsonPARAM": "runSrc" + }, + "width": 32, + "height": 40, + "xCount": 5, + "offX": { + "jsonPARAM": "runX" + }, + "offY": { + "jsonPARAM": "runY" + } + }, + "sit": { + "jsonIF": "sitX", + "src": { + "jsonPARAM": "img" + }, + "width": 32, + "height": 40, + "xCount": 1, + "offX": { + "jsonPARAM": "sitX" + }, + "offY": { + "jsonPARAM": "sitY" + } + } + }, + "shapeType": "Y_FLAT", + "offset": { + "x": 0, + "y": -2, + "z": 0 + }, + "SUB": [ + { + "sheet": "move", + "dirs": 4, + "flipX": [0, 0, 0, 1], + "tileOffsets": [0, 3, 6, 3], + "SUB": [ + { + "name": "idle", + "time": 1, + "repeat": false, + "frames": [1] + }, + { + "name": "walk", + "time": 0.133, + "repeat": true, + "frames": [0, 1, 2, 1] + }, + { + "sheet": "offline", + "name": "offline", + "time": 0.166, + "repeat": true, + "frames": [0, 1, 2] + } + ] + }, + { + "jsonIF": "runSrc", + "sheet": "run", + "dirs": 6, + "flipX": [0, 0, 0, 1, 1, 1], + "tileOffsets": [0, 5, 10, 10, 5, 0], + "SUB": [ + { + "name": "run", + "time": 0.1, + "repeat": true, + "frames": [0, 1, 2, 3] + }, + { + "name": "jump", + "time": 0.1, + "repeat": true, + "frames": [3] + }, + { + "name": "fall", + "time": 0.1, + "repeat": true, + "frames": [4] + } + ] + }, + { + "jsonIF": "sitX", + "sheet": "sit", + "dirs": 4, + "flipX": [0, 0, 0, 1], + "tileOffsets": [0, 1, 2, 1], + "SUB": [ + { + "name": "sit", + "time": 0.1, + "repeat": false, + "frames": [0] + } + ] + }, + { + "sheet": "move", + "dirs": 2, + "flipX": [0, 1], + "tileOffsets": [9, 9], + "SUB": [ + { + "name": "ground", + "time": 1, + "repeat": false, + "frames": [0], + "offset": { + "x": 0, + "y": 2, + "z": 0 + } + } + ] + } + ] + }, + "walkAnimSet": { + "normal": { + "idle": "idle", + "move": "walk", + "run": { + "jsonIF": "runSrc", + "jsonTHEN": "run" + }, + "jump": { + "jsonIF": "runSrc", + "jsonTHEN": "jump" + }, + "fall": { + "jsonIF": "runSrc", + "jsonTHEN": "fall" + } + }, + "ground": { + "idle": "ground" + }, + "sit": { + "jsonIF": "sitX", + "idle": "sit" + }, + "offline": { + "idle": "offline" + } + }, + "walkAnims": "normal", + "configs": { + "normal": { + "relativeVel": 0.5 + }, + "run": { + "jsonIF": "runSrc", + "relativeVel": 1 + }, + "ground": { + "walkAnims": "ground" + }, + "groundFloat": { + "walkAnims": "ground", + "zGravityFactor": 0, + "shadow": 0 + }, + "sit": { + "jsonIF": "sitX", + "walkAnims": "sit" + }, + "sitFloat": { + "jsonIF": "sitX", + "walkAnims": "sit", + "zGravityFactor": 0, + "shadow": 0 + }, + "offline": { + "walkAnims": "offline" + } + }, + "face": { + "ABSTRACT": { + "jsonPARAM": "face" + } + } + }, + "NPCBasic": { + "name": { + "jsonPARAM": "name", + "default": null + }, + "gender": { + "jsonPARAM": "gender", + "default": null + }, + "animSheet": { + "DOCTYPE": "MULTI_DIR_ANIMATION", + "namedSheets": { + "move": { + "src": { + "jsonPARAM": "img" + }, + "width": { + "jsonPARAM": "width", + "default": 32 + }, + "height": { + "jsonPARAM": "height", + "default": 40 + }, + "xCount": 3, + "offX": { + "jsonPARAM": "x" + }, + "offY": { + "jsonPARAM": "y" + } + }, + "sit": { + "jsonIF": "sitX", + "src": { + "jsonPARAM": "img" + }, + "width": 32, + "height": 40, + "xCount": 1, + "offX": { + "jsonPARAM": "sitX" + }, + "offY": { + "jsonPARAM": "sitY" + } + }, + "sit2": { + "jsonIF": "sit2X", + "src": { + "jsonPARAM": "img" + }, + "width": 32, + "height": 40, + "xCount": 1, + "offX": { + "jsonPARAM": "sit2X" + }, + "offY": { + "jsonPARAM": "sit2Y" + } + } + }, + "shapeType": "Y_FLAT", + "offset": { + "x": 0, + "y": -2, + "z": 0 + }, + "SUB": [ + { + "sheet": "move", + "dirs": 4, + "flipX": [0, 0, 0, 1], + "tileOffsets": [0, 3, 6, 3], + "SUB": [ + { + "name": "idle", + "time": 1, + "repeat": false, + "frames": [1] + }, + { + "name": "walk", + "time": 0.133, + "repeat": true, + "frames": [0, 1, 2, 1] + } + ] + }, + { + "jsonIF": "sitX", + "sheet": "sit", + "dirs": 4, + "flipX": [0, 0, 0, 1], + "tileOffsets": [0, 1, 2, 1], + "SUB": [ + { + "name": "sit", + "time": 1, + "repeat": false, + "frames": [0] + } + ] + }, + { + "jsonIF": "sit2X", + "sheet": "sit2", + "dirs": 4, + "flipX": [0, 0, 0, 1], + "tileOffsets": [0, 1, 2, 1], + "SUB": [ + { + "name": "sit2", + "time": 1, + "repeat": false, + "frames": [0] + } + ] + } + ] + }, + "walkAnimSet": { + "normal": { + "idle": "idle", + "move": "walk" + }, + "sit": { + "jsonIF": "sitX", + "idle": "sit" + }, + "sit2": { + "jsonIF": "sit2X", + "idle": "sit2" + } + }, + "walkAnims": "normal", + "configs": { + "normal": { + "relativeVel": 0.5 + }, + "sit": { + "jsonIF": "sitX", + "walkAnims": "sit", + "shadow": 0 + }, + "sitFloat": { + "jsonIF": "sitX", + "walkAnims": "sit", + "zGravityFactor": 0, + "shadow": 0 + }, + "sit2": { + "jsonIF": "sit2X", + "walkAnims": "sit2", + "shadow": 0 + }, + "sit2Float": { + "jsonIF": "sit2X", + "walkAnims": "sit2", + "shadow": 0, + "zGravityFactor": 0 + } + }, + "face": { + "ABSTRACT": { + "jsonPARAM": "face" + } + } + } +}