// 몬스터 종별 모델(.model)과 맵 인스턴스 엔티티의 공용 빌더. // 단일 소스: data/enemies.json의 appearance. fs 접근 없음(호출자가 skeleton 주입) — 테스트 용이. const STR_TYPE = 'System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'; const SINGLE_TYPE = 'System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'; const VEC2_TYPE = 'MOD.Core.MODVector2, MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null'; const DICT_TYPE = 'MOD.Core.MODSyncDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], MOD.Core, Version=26.5.0.0, Culture=neutral, PublicKeyToken=null'; const native = (type) => ({ $type: 'MODNativeType', type }); const vec2 = (v) => ({ $type: 'MOD.Core.MODVector2, MOD.Core', x: v.x, y: v.y }); const DAMAGE_SKIN_ID = '02c22d93421b4038b3c413b3e40b57ec'; export function modelEntryId(enemyId) { return `monster-${enemyId}`; } function requireAppearance(enemyId, enemy) { if (!enemy?.appearance?.sheet?.stand) throw new Error(`[monster-model] ${enemyId}: appearance.sheet.stand 없음 — data/enemies.json 확인`); return enemy.appearance; } // .model 파일 전체 객체 생성 — ChaseMonster.model(skeleton)을 골격으로 복제·확장. export function buildMonsterModel(enemyId, enemy, skeletonJson) { const app = requireAppearance(enemyId, enemy); const file = JSON.parse(JSON.stringify(skeletonJson)); // 순수성: 입력 비변형 const json = file.ContentProto.Json; file.EntryKey = `model://${modelEntryId(enemyId)}`; json.Id = modelEntryId(enemyId); json.Name = enemyId; json.Components = json.Components .filter((c) => !c.includes('AIWander') && !c.includes('AIChase')) .concat(['MOD.Core.DamageSkinSettingComponent', 'script.CombatMonster']); const setValue = (TargetType, Name, typeStr, Value) => { json.Values = json.Values.filter((v) => !(v.TargetType === TargetType && v.Name === Name)); json.Values.push({ TargetType, Name, ValueType: native(typeStr), Value }); }; setValue('MOD.Core.SpriteRendererComponent', 'SpriteRUID', STR_TYPE, app.sheet.stand); setValue('MOD.Core.SpriteRendererComponent', 'SortingLayer', STR_TYPE, 'MapLayer0'); setValue('MOD.Core.StateAnimationComponent', 'ActionSheet', DICT_TYPE, { ...app.sheet }); // 메이커 미해석 시 이 줄만 제거(런타임은 인스턴스 값 사용) setValue('MOD.Core.HitComponent', 'BoxSize', VEC2_TYPE, vec2(app.box)); setValue('MOD.Core.HitComponent', 'ColliderOffset', VEC2_TYPE, vec2(app.off)); setValue('MOD.Core.MovementComponent', 'InputSpeed', SINGLE_TYPE, 0); setValue('script.CombatMonster', 'EnemyId', STR_TYPE, enemyId); // 편의 베이크 — 실패해도 무해(인스턴스가 정본) return file; } // 맵 인스턴스 엔티티 — 현행 맵 몬스터 인스턴스 골격(map01 실측)과 동일 형태. export function buildMonsterInstance({ enemyId, enemy, name, guid, mapTag, x, group }) { const app = requireAppearance(enemyId, enemy); const components = [ { '@type': 'MOD.Core.TransformComponent', Position: { x, y: 0.03499998, z: 999.999 }, QuaternionRotation: { x: 0, y: 0, z: 0, w: 1 }, Scale: { x: 1, y: 1, z: 1 }, Enable: true }, { '@type': 'MOD.Core.StateAnimationComponent', ActionSheet: { ...app.sheet }, Enable: true }, { '@type': 'MOD.Core.SpriteRendererComponent', ActionSheet: {}, EndFrameIndex: 0, RenderSetting: 1, SortingLayer: 'MapLayer0', SpriteRUID: app.sheet.stand, StartFrameIndex: 0, Enable: true }, { '@type': 'MOD.Core.RigidbodyComponent', MoveVelocity: { x: 0, y: 0 }, RealMoveVelocity: { x: 0, y: 0 }, Enable: true }, { '@type': 'MOD.Core.MovementComponent', InputSpeed: 0, JumpForce: 6, Enable: false }, { '@type': 'MOD.Core.StateComponent', IsLegacy: false, Enable: true }, { '@type': 'MOD.Core.HitComponent', BoxSize: { x: app.box.x, y: app.box.y }, ColliderOffset: { x: app.off.x, y: app.off.y }, IsLegacy: false, Enable: true }, { '@type': 'MOD.Core.DamageSkinSpawnerComponent', Enable: true }, { '@type': 'script.Monster', Enable: true, IsDead: false }, { '@type': 'script.MonsterAttack', Enable: true, SpriteSize: { x: 0, y: 0 }, PositionOffset: { x: 0, y: 0 } }, { '@type': 'MOD.Core.KinematicbodyComponent', MoveVelocity: { x: 0, y: 0 }, Enable: true }, { '@type': 'MOD.Core.SideviewbodyComponent', MoveVelocity: { x: 0, y: 0 }, Enable: true }, { '@type': 'MOD.Core.DamageSkinSettingComponent', DamageSkinId: { DataId: DAMAGE_SKIN_ID }, Enable: true }, { '@type': 'script.CombatMonster', Enable: true, EnemyId: enemyId, Group: group }, ]; const path = `/maps/map${mapTag}/${name}`; return { id: guid, path, componentNames: components.map((c) => c['@type']).join(','), jsonString: { name, path, nameEditable: true, enable: true, visible: true, localize: false, displayOrder: 4, pathConstraints: '///', revision: 2, origin: { type: 'Model', entry_id: modelEntryId(enemyId), sub_entity_id: null, root_entity_id: guid, replaced_model_id: null }, modelId: modelEntryId(enemyId), '@components': components, '@version': 1, }, }; }