buildMonsterModel(.model 골격 복제·외형/EnemyId 베이크·AI-free) + buildMonsterInstance(맵 엔티티) + modelEntryId. fs 접근 없는 순수 함수. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xhLoQbJvQYL65kBtDNDTy
79 lines
5.2 KiB
JavaScript
79 lines
5.2 KiB
JavaScript
// 몬스터 종별 모델(.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: enemyId, sub_entity_id: null, root_entity_id: guid, replaced_model_id: null },
|
|
modelId: modelEntryId(enemyId),
|
|
'@components': components,
|
|
'@version': 1,
|
|
},
|
|
};
|
|
}
|