증상: 전투맵 진입 시 몬스터마다 [LEA-3023] TypeMismatch(AnimationClip) + [LEA-2007] AttemptToIndex(clip nil) 서버 로그 스팸(몬스터 수만큼 반복). 원인: MonsterAttack.OnBeginPlay(chasemonster 모델 상속·메이커 저작·생성기 없음)가 정적 Sprite인 SpriteRUID를 _ResourceService:LoadAnimationClipAndWait에 넘김 → AnimationClip이 아니라 nil 반환(LEA-3023) → clip.Frames[1] 인덱싱(LEA-2007). 이 멜리 공격 로직은 카드 기반 턴제 전투에서 호출하는 코드가 전혀 없는 죽은 코드라 크래시 외 게임 영향은 없으나 로그를 더럽힘. 수정: LoadAnimationClipAndWait 호출 전 GetTypeAndWait가 ResourceType.AnimationClip이 아니면 early-return + clip nil 가드. 정적 스프라이트 몬스터는 공격범위 설정을 건너뜀 (원래 미사용), 애니메이션 클립 몬스터는 기존대로 동작. 주의: MonsterAttack은 생성기 없는 메이커 저작 codeblock이라 디스크 직접 패치. 적용하려면 메이커에서 로컬 워크스페이스 reload 필요. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
143 lines
5.4 KiB
Plaintext
143 lines
5.4 KiB
Plaintext
{
|
|
"Id": "",
|
|
"GameId": "",
|
|
"EntryKey": "codeblock://monsterattack",
|
|
"ContentType": "x-mod/codeblock",
|
|
"Content": "",
|
|
"Usage": 0,
|
|
"UsePublish": 1,
|
|
"UseService": 0,
|
|
"CoreVersion": "26.5.0.0",
|
|
"StudioVersion": "0.1.0.0",
|
|
"DynamicLoading": 0,
|
|
"ContentProto": {
|
|
"Use": "Json",
|
|
"Json": {
|
|
"CoreVersion": {
|
|
"Major": 0,
|
|
"Minor": 2
|
|
},
|
|
"ScriptVersion": {
|
|
"Major": 1,
|
|
"Minor": 5
|
|
},
|
|
"Description": "",
|
|
"Id": "MonsterAttack",
|
|
"Language": 1,
|
|
"Name": "MonsterAttack",
|
|
"Type": 4,
|
|
"Source": 1,
|
|
"Target": "MOD.Core.AttackComponent",
|
|
"Properties": [
|
|
{
|
|
"Type": "number",
|
|
"DefaultValue": "0.03",
|
|
"SyncDirection": 0,
|
|
"Attributes": [
|
|
{
|
|
"$type": "MOD.Core.Script.MinValueScriptAttribute, MOD.Core",
|
|
"Value": 0.0
|
|
}
|
|
],
|
|
"Name": "AttackInterval"
|
|
},
|
|
{
|
|
"Type": "any",
|
|
"DefaultValue": "nil",
|
|
"SyncDirection": 0,
|
|
"Attributes": [
|
|
{
|
|
"$type": "MOD.Core.Script.HideFromInspectorScriptAttribute, MOD.Core"
|
|
}
|
|
],
|
|
"Name": "Shape"
|
|
},
|
|
{
|
|
"Type": "Vector2",
|
|
"DefaultValue": "Vector2(0,0)",
|
|
"SyncDirection": 0,
|
|
"Attributes": [
|
|
{
|
|
"$type": "MOD.Core.Script.HideFromInspectorScriptAttribute, MOD.Core"
|
|
}
|
|
],
|
|
"Name": "SpriteSize"
|
|
},
|
|
{
|
|
"Type": "Vector2",
|
|
"DefaultValue": "Vector2(0,0)",
|
|
"SyncDirection": 0,
|
|
"Attributes": [
|
|
{
|
|
"$type": "MOD.Core.Script.HideFromInspectorScriptAttribute, MOD.Core"
|
|
}
|
|
],
|
|
"Name": "PositionOffset"
|
|
}
|
|
],
|
|
"Methods": [
|
|
{
|
|
"Return": {
|
|
"Type": "void",
|
|
"DefaultValue": null,
|
|
"SyncDirection": 0,
|
|
"Attributes": [],
|
|
"Name": null
|
|
},
|
|
"Arguments": [],
|
|
"Code": "local monster = self.Entity.Monster\nif not monster then\n\treturn\nend\n\nself.Shape = BoxShape(Vector2.zero, Vector2.one, 0)\n\n-- sprite 사이즈를 가져와 공격 영역으로 사용한다\n_ResourceService:PreloadAsync({self.Entity.SpriteRendererComponent.SpriteRUID}, function()\n\tif _ResourceService:GetTypeAndWait(self.Entity.SpriteRendererComponent.SpriteRUID) ~= ResourceType.AnimationClip then\n\t\treturn\n\tend\n\tlocal clip = _ResourceService:LoadAnimationClipAndWait(self.Entity.SpriteRendererComponent.SpriteRUID)\n\tif clip == nil then\n\t\treturn\n\tend\n\tlocal firstFrameSprite = clip.Frames[1].FrameSprite\n\tlocal firstSpriteSizeInPixel = Vector2(firstFrameSprite.Width, firstFrameSprite.Height)\n\tlocal ppu = firstFrameSprite.PixelPerUnit\n\n\tself.SpriteSize = firstSpriteSizeInPixel / ppu\n\tself.PositionOffset = (firstSpriteSizeInPixel / 2 - firstFrameSprite.PivotPixel:ToVector2()) / ppu\n\t\n\t_TimerService:SetTimerRepeat(function() \n\t\tif monster.IsDead == false then\n\t\t\tself:AttackNear()\n\t\tend\n\tend, self.AttackInterval)\nend)",
|
|
"Scope": 2,
|
|
"ExecSpace": 1,
|
|
"Attributes": [],
|
|
"Name": "OnBeginPlay"
|
|
},
|
|
{
|
|
"Return": {
|
|
"Type": "void",
|
|
"DefaultValue": null,
|
|
"SyncDirection": 0,
|
|
"Attributes": [],
|
|
"Name": null
|
|
},
|
|
"Arguments": [],
|
|
"Code": "local transformComponent = self.Entity.TransformComponent\n\nif isvalid(transformComponent) then\n\tlocal worldPosition = transformComponent.WorldPosition\n\tlocal scaleX = transformComponent.Scale.x\n\tlocal scaleY = transformComponent.Scale.y\n\tlocal radian = math.rad(transformComponent.ZRotation)\n\tlocal offsetX = math.cos(radian) * self.PositionOffset.x * scaleX - math.sin(radian) * self.PositionOffset.y * scaleY\n\tlocal offsetY = math.sin(radian) * self.PositionOffset.x * scaleX + math.cos(radian) * self.PositionOffset.y * scaleY\n\tself.Shape.Size = Vector2(self.SpriteSize.x * math.abs(scaleX), self.SpriteSize.y * math.abs(scaleY))\n\tself.Shape.Position = Vector2(worldPosition.x + offsetX, worldPosition.y + offsetY)\n\tself.Shape.Angle = transformComponent.ZRotation\nend\n\nself:AttackFast(self.Shape, nil, CollisionGroups.Player)",
|
|
"Scope": 2,
|
|
"ExecSpace": 0,
|
|
"Attributes": [],
|
|
"Name": "AttackNear"
|
|
},
|
|
{
|
|
"Return": {
|
|
"Type": "boolean",
|
|
"DefaultValue": null,
|
|
"SyncDirection": 0,
|
|
"Attributes": [],
|
|
"Name": null
|
|
},
|
|
"Arguments": [
|
|
{
|
|
"Type": "Entity",
|
|
"DefaultValue": null,
|
|
"SyncDirection": 0,
|
|
"Attributes": [],
|
|
"Name": "defender"
|
|
},
|
|
{
|
|
"Type": "string",
|
|
"DefaultValue": null,
|
|
"SyncDirection": 0,
|
|
"Attributes": [],
|
|
"Name": "attackInfo"
|
|
}
|
|
],
|
|
"Code": "if isvalid(defender.PlayerComponent) == false then\n\treturn false\nend\n\nreturn __base:IsAttackTarget(defender, attackInfo)",
|
|
"Scope": 2,
|
|
"ExecSpace": 0,
|
|
"Attributes": [],
|
|
"Name": "IsAttackTarget"
|
|
}
|
|
],
|
|
"EntityEventHandlers": []
|
|
}
|
|
}
|
|
} |