This commit is contained in:
Lilith Ashley Nyx Arson 🔥 2025-01-09 18:08:49 +01:00
parent 971593f800
commit 566d25c689
Signed by: lilith
SSH key fingerprint: SHA256:LAjgsAMyT3LO2JVtr6fQ4N3RTYoRRlIm5wAKsbDife4
884 changed files with 1168000 additions and 1 deletions

View file

@ -0,0 +1,266 @@
{
"name": "Hoshino",
"description": "Hoshino Takanashi from Blue Archive",
"author": "Gakuto1112",
"version": "0.1.4",
"color": "a3e0fb",
"ignoredTextures": [
"textures.for_modeling.leather_layer_1",
"textures.for_modeling.leather_layer_1_overlay",
"textures.for_modeling.leather_layer_2",
"textures.for_modeling.leather_layer_2_overlay",
"textures.for_modeling.trim",
"textures.for_modeling.trim_leggings",
"textures.for_modeling.water_still",
"textures.for_modeling.water_flow",
"textures.for_modeling.ravager",
"textures.for_modeling.pillager",
"textures.for_modeling.vindicator",
"textures.for_modeling.firework_rocket",
"textures.for_modeling.zombie",
"textures.for_modeling.creeper"
],
"autoScripts": [
"scripts/avatar.lua"
],
"customizations": {
"models.models.main": {
"primaryRenderType": "CUTOUT"
},
"models.models.main.Avatar.Head.HeadRing": {
"parentType": "None"
},
"models.models.main.Avatar.UpperBody": {
"parentType": "None"
},
"models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom": {
"parentType": "None"
},
"models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom": {
"parentType": "None"
},
"models.models.main.Avatar.LowerBody": {
"parentType": "None"
},
"models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom": {
"parentType": "None"
},
"models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom": {
"parentType": "None"
},
"models.models.main.CameraAnchor": {
"parentType": "None"
},
"models.models.armor.ArmorRA": {
"moveTo": "models.models.main.Avatar.UpperBody.Arms.RightArm",
"visible": false
},
"models.models.armor.ArmorRA.RightChestplate.RightChestplateTrim": {
"visible": false
},
"models.models.armor.ArmorRAB": {
"moveTo": "models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom",
"visible": false
},
"models.models.armor.ArmorRAB.RightChestplateBottom.RightChestplateBottomTrim": {
"visible": false
},
"models.models.armor.ArmorLA": {
"moveTo": "models.models.main.Avatar.UpperBody.Arms.LeftArm",
"visible": false
},
"models.models.armor.ArmorLA.LeftChestplate.LeftChestplateTrim": {
"visible": false
},
"models.models.armor.ArmorLAB": {
"moveTo": "models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom",
"visible": false
},
"models.models.armor.ArmorLAB.LeftChestplateBottom.LeftChestplateBottomTrim": {
"visible": false
},
"models.models.armor.ArmorRL": {
"moveTo": "models.models.main.Avatar.LowerBody.Legs.RightLeg"
},
"models.models.armor.ArmorRL.RightLeggings": {
"visible": false,
"parentType": "None"
},
"models.models.armor.ArmorRL.RightLeggings.RightLeggingsTrim": {
"visible": false
},
"models.models.armor.ArmorRL.RightBoots": {
"visible": false
},
"models.models.armor.ArmorRL.RightBoots.RightBootsTrim": {
"visible": false
},
"models.models.armor.ArmorRLB": {
"moveTo": "models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom"
},
"models.models.armor.ArmorRLB.RightLeggingsBottom": {
"visible": false,
"parentType": "None"
},
"models.models.armor.ArmorRLB.RightLeggingsBottom.RightLeggingsBottomTrim": {
"visible": false
},
"models.models.armor.ArmorRLB.RightBootsBottom": {
"visible": false
},
"models.models.armor.ArmorRLB.RightBootsBottom.RightBootsBottomTrim": {
"visible": false
},
"models.models.armor.ArmorLL": {
"moveTo": "models.models.main.Avatar.LowerBody.Legs.LeftLeg"
},
"models.models.armor.ArmorLL.LeftLeggings": {
"visible": false,
"parentType": "None"
},
"models.models.armor.ArmorLL.LeftLeggings.LeftLeggingsTrim": {
"visible": false
},
"models.models.armor.ArmorLL.LeftBoots": {
"visible": false
},
"models.models.armor.ArmorLL.LeftBoots.LeftBootsTrim": {
"visible": false
},
"models.models.armor.ArmorLLB": {
"moveTo": "models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom"
},
"models.models.armor.ArmorLLB.LeftLeggingsBottom": {
"visible": false,
"parentType": "None"
},
"models.models.armor.ArmorLLB.LeftLeggingsBottom.LeftLeggingsBottomTrim": {
"visible": false
},
"models.models.armor.ArmorLLB.LeftBootsBottom": {
"visible": false
},
"models.models.armor.ArmorLLB.LeftBootsBottom.LeftBootsBottomTrim": {
"visible": false
},
"models.models.gun.Gun": {
"moveTo": "models.models.main.Avatar.UpperBody.Body"
},
"models.models.gun.SubGun": {
"moveTo": "models.models.main.Avatar.UpperBody.Body",
"visible": false
},
"models.models.ex_skill_1.ShineEffect": {
"moveTo": "models.models.gun.Gun.Barrel",
"primaryRenderType": "EMISSIVE_SOLID",
"visible": false
},
"models.models.ex_skill_2.WhaleFloat": {
"moveTo": "models.models.main.Avatar.LowerBody",
"visible": false
},
"models.models.ex_skill_2.Waves": {
"visible": false
},
"models.models.ex_skill_3.Illagers": {
"visible": false
},
"models.models.ex_skill_3.Firework": {
"visible": false
},
"models.models.ex_skill_3.Explosion": {
"visible": false,
"primaryRenderType": "EMISSIVE_SOLID"
},
"models.models.ex_skill_4.Zombie": {
"visible": false
},
"models.models.ex_skill_4.Creeper": {
"visible": false
},
"models.models.ex_skill_4.EyeShine": {
"visible": false,
"moveTo": "models.models.main.Avatar.Head",
"primaryRenderType": "EMISSIVE_SOLID"
},
"models.models.ex_skill_4.MuzzleFlash": {
"visible": false,
"moveTo": "models.models.gun.Gun",
"primaryRenderType": "EMISSIVE_SOLID"
},
"models.models.ex_skill_3.Gui": {
"visible": false
},
"models.models.costume_masked.CMaskedH": {
"moveTo": "models.models.main.Avatar.Head",
"visible": false
},
"models.models.costume_swimsuit.CSwimsuitH": {
"moveTo": "models.models.main.Avatar.Head",
"visible": false
},
"models.models.costume_swimsuit.CSwimsuitB": {
"moveTo": "models.models.main.Avatar.UpperBody.Body",
"visible": false
},
"models.models.costume_swimsuit.CSwimsuitB.GunBag": {
"primaryRenderType": "TRANSLUCENT"
},
"models.models.costume_swimsuit.CSwimsuitRA": {
"moveTo": "models.models.main.Avatar.UpperBody.Arms.RightArm",
"visible": false
},
"models.models.costume_swimsuit.CSwimsuitRAB": {
"moveTo": "models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom",
"visible": false
},
"models.models.costume_swimsuit.CSwimsuitLA": {
"moveTo": "models.models.main.Avatar.UpperBody.Arms.LeftArm",
"visible": false
},
"models.models.costume_swimsuit.CSwimsuitLAB": {
"moveTo": "models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom",
"visible": false
},
"models.models.costume_swimsuit.CSwimsuitRL": {
"moveTo": "models.models.main.Avatar.LowerBody.Legs.RightLeg",
"visible": false
},
"models.models.costume_swimsuit.CSwimsuitLL": {
"moveTo": "models.models.main.Avatar.LowerBody.Legs.LeftLeg",
"visible": false
},
"models.models.costume_battle.CBattleH": {
"moveTo": "models.models.main.Avatar.Head",
"visible": false
},
"models.models.costume_battle.CBattleB": {
"moveTo": "models.models.main.Avatar.UpperBody.Body",
"visible": false
},
"models.models.ex_skill_frame.Particles": {
"visible": false
},
"models.models.bubble.Camera.AvatarBubble": {
"visible": false
},
"models.models.bubble.Camera.AvatarBubble.Bullets": {
"visible": false
},
"models.models.bubble.Camera.AvatarBubble.Dots": {
"visible": false
},
"models.models.action_wheel_gui.Gui": {
"visible": false
},
"models.models.barrier": {
"moveTo": "models.models.main.Avatar",
"primaryRenderType": "CUTOUT_EMISSIVE_SOLID",
"visible": false
},
"models.models.death_animation": {
"visible": false,
"parentType": "World"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,902 @@
{
"meta": {
"format_version": "4.9",
"model_format": "free",
"box_uv": false
},
"name": "bubble",
"model_identifier": "",
"visible_box": [
1,
1,
0
],
"variable_placeholders": "",
"variable_placeholder_buttons": [],
"timeline_setups": [],
"unhandled_root_fields": {},
"resolution": {
"width": 16,
"height": 16
},
"elements": [
{
"name": "Bubble",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
-3,
-4,
0
],
"to": [
5,
4,
0
],
"autouv": 0,
"color": 0,
"origin": [
1,
0,
0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
16
],
"texture": 0
},
"east": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"up": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"texture": null
}
},
"type": "cube",
"uuid": "ee16cd89-5e16-d1c0-bdb4-bec63d4d29dc"
},
{
"name": "Emoji",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
-1.5,
-2.5,
-0.05
],
"to": [
3.5,
2.5,
-0.05
],
"autouv": 0,
"color": 0,
"origin": [
1,
0,
0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
16
],
"texture": 1
},
"east": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"up": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"texture": null
}
},
"type": "cube",
"uuid": "e1f1401a-3276-4ed6-1633-16078de30336"
},
{
"name": "Bullet1",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
1.9375,
-1.875,
-0.1
],
"to": [
3.1875,
1.875,
-0.1
],
"autouv": 0,
"color": 0,
"origin": [
1,
0,
0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
16
],
"texture": 2
},
"east": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"up": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"texture": null
}
},
"type": "cube",
"uuid": "015d120f-7c4c-05e7-86b8-e442d2ca89ce"
},
{
"name": "Bullet2",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
0.375,
-1.875,
-0.1
],
"to": [
1.625,
1.875,
-0.1
],
"autouv": 0,
"color": 0,
"origin": [
1,
0,
0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
16
],
"texture": 2
},
"east": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"up": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"texture": null
}
},
"type": "cube",
"uuid": "71c04be5-6d32-4cee-9e7d-b8d98db60f7d"
},
{
"name": "Bullet3",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
-1.1875,
-1.875,
-0.1
],
"to": [
0.0625,
1.875,
-0.1
],
"autouv": 0,
"color": 0,
"origin": [
1,
0,
0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
16
],
"texture": 2
},
"east": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"up": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"texture": null
}
},
"type": "cube",
"uuid": "c13ac616-5ee7-3b04-83ed-f9490507b4f8"
},
{
"name": "Dot1",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
2.25,
-0.625,
-0.1
],
"to": [
3.5,
0.625,
-0.1
],
"autouv": 0,
"color": 0,
"origin": [
1,
0,
0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
16
],
"texture": 4
},
"east": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"up": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"texture": null
}
},
"type": "cube",
"uuid": "f336f75d-4273-d986-7b79-5028befe8efc"
},
{
"name": "Dot2",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
0.375,
-0.625,
-0.1
],
"to": [
1.625,
0.625,
-0.1
],
"autouv": 0,
"color": 0,
"origin": [
1,
0,
0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
16
],
"texture": 4
},
"east": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"up": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"texture": null
}
},
"type": "cube",
"uuid": "9d1c3e18-15c3-3c07-f04a-d705ceec1fed"
},
{
"name": "Dot3",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
-1.5,
-0.625,
-0.1
],
"to": [
-0.25,
0.625,
-0.1
],
"autouv": 0,
"color": 0,
"origin": [
1,
0,
0
],
"faces": {
"north": {
"uv": [
0,
0,
16,
16
],
"texture": 4
},
"east": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"up": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"texture": null
}
},
"type": "cube",
"uuid": "23a7b4da-5022-66ac-bfac-23544ab88f69"
}
],
"outliner": [
{
"name": "Camera",
"origin": [
0,
0,
0
],
"color": 0,
"uuid": "1ad8e11f-891c-06f0-491f-55ff468a6e74",
"export": true,
"mirror_uv": false,
"isOpen": true,
"locked": false,
"visibility": true,
"autouv": 0,
"children": [
{
"name": "AvatarBubble",
"origin": [
-3,
-4,
0
],
"color": 0,
"uuid": "ce33d433-402d-decd-b93f-250225de88c7",
"export": true,
"mirror_uv": false,
"isOpen": true,
"locked": false,
"visibility": true,
"autouv": 0,
"children": [
"ee16cd89-5e16-d1c0-bdb4-bec63d4d29dc",
"e1f1401a-3276-4ed6-1633-16078de30336",
{
"name": "Bullets",
"origin": [
1,
0,
0
],
"color": 0,
"uuid": "2a47b3c1-ef22-e939-70d9-e17ea10d79d9",
"export": true,
"mirror_uv": false,
"isOpen": true,
"locked": false,
"visibility": true,
"autouv": 0,
"children": [
"015d120f-7c4c-05e7-86b8-e442d2ca89ce",
"71c04be5-6d32-4cee-9e7d-b8d98db60f7d",
"c13ac616-5ee7-3b04-83ed-f9490507b4f8"
]
},
{
"name": "Dots",
"origin": [
-3,
-4,
0
],
"color": 0,
"uuid": "55f984b9-0c0d-7ae1-712d-78a7d7fbfa03",
"export": true,
"mirror_uv": false,
"isOpen": true,
"locked": false,
"visibility": true,
"autouv": 0,
"children": [
"f336f75d-4273-d986-7b79-5028befe8efc",
"9d1c3e18-15c3-3c07-f04a-d705ceec1fed",
"23a7b4da-5022-66ac-bfac-23544ab88f69"
]
}
]
}
]
}
],
"textures": [
{
"path": "",
"name": "bubble.png",
"folder": "",
"namespace": "",
"id": "0",
"width": 16,
"height": 16,
"uv_width": 16,
"uv_height": 16,
"particle": false,
"layers_enabled": false,
"sync_to_project": "",
"render_mode": "default",
"render_sides": "auto",
"frame_time": 1,
"frame_order_type": "loop",
"frame_order": "",
"frame_interpolate": false,
"visible": true,
"internal": true,
"saved": true,
"uuid": "849f6abb-5d52-7cbc-bc5e-c54506c00158",
"relative_path": "../../textures/bubble.png",
"source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAGhJREFUOE/tk0sOABAMRNul+1/WroKO+KcSdqykzGvDDNN8yaLOfb0vJKHIXM9cro+brFs1bvsqKEFAMouBAuQK4Lh7PUWc4AMuvIGaz2aiJz4A1Pyd6kJHRP44TMgLIEM8dZxdnJuzAGqUPxJDQDMGAAAAAElFTkSuQmCC"
},
{
"path": "",
"name": "reload.png",
"folder": "",
"namespace": "",
"id": "4",
"width": 16,
"height": 16,
"uv_width": 16,
"uv_height": 16,
"particle": false,
"layers_enabled": false,
"sync_to_project": "",
"render_mode": "default",
"render_sides": "auto",
"frame_time": 1,
"frame_order_type": "loop",
"frame_order": "",
"frame_interpolate": false,
"visible": true,
"internal": true,
"saved": true,
"uuid": "e5650158-1574-55d1-6b46-d58173bbb96b",
"relative_path": "../../textures/emojis/reload.png",
"source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAEpJREFUOE9jZKAQMFKonwFuwOf37/+DDOMVFCRJjCTFIAvQLQIbABKE2QxjEys2aoAg42gYIIUBcp4AJSpYioOJ4xKjXmYiN1cCAKtG/BHrN3pVAAAAAElFTkSuQmCC"
},
{
"path": "",
"name": "bullet.png",
"folder": "",
"namespace": "",
"id": "5",
"width": 4,
"height": 12,
"uv_width": 16,
"uv_height": 16,
"particle": false,
"layers_enabled": false,
"sync_to_project": "",
"render_mode": "default",
"render_sides": "auto",
"frame_time": 1,
"frame_order_type": "loop",
"frame_order": "",
"frame_interpolate": false,
"visible": true,
"internal": true,
"saved": true,
"uuid": "1a2d73a2-6180-0319-1efb-16f49aa91e7a",
"relative_path": "../../textures/bullet.png",
"source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAMCAYAAABFohwTAAAAAXNSR0IArs4c6QAAAGdJREFUGFdjTEhI4Pj1/f93LpbvDH9YuTgZMQSiwuP/V4azMOh4+TKkJi9jYAQJJDt+ZHAK9GFILdoFESj0/cpgEuTBkJoKFSCsAqwlrJAhNXEikhl4BVKdXjE4xAZDbEmJDvvPgAQAMBRGJaIzndEAAAAASUVORK5CYII="
},
{
"path": "",
"name": "dots.png",
"folder": "",
"namespace": "",
"id": "3",
"width": 16,
"height": 16,
"uv_width": 16,
"uv_height": 16,
"particle": false,
"layers_enabled": false,
"sync_to_project": "",
"render_mode": "default",
"render_sides": "auto",
"frame_time": 1,
"frame_order_type": "loop",
"frame_order": "",
"frame_interpolate": false,
"visible": true,
"internal": true,
"saved": true,
"uuid": "7dda2e3d-9f23-52bb-e799-b8a2879d3871",
"relative_path": "../../textures/emojis/dots.png",
"source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAADhJREFUOE9jZKAQMFKon2HUAAZIGHx+//4/iOYVFEQJE2LEGWGKYLEBM4RYccoNoNgLowlpEOQFAJrjL7ESx0KNAAAAAElFTkSuQmCC"
},
{
"path": "",
"name": "black_dot.png",
"folder": "",
"namespace": "",
"id": "6",
"width": 4,
"height": 4,
"uv_width": 16,
"uv_height": 16,
"particle": false,
"layers_enabled": false,
"sync_to_project": "",
"render_mode": "default",
"render_sides": "auto",
"frame_time": 1,
"frame_order_type": "loop",
"frame_order": "",
"frame_interpolate": false,
"visible": true,
"internal": true,
"saved": true,
"uuid": "d1f6e856-c31e-ace6-6507-d03259b83bf5",
"relative_path": "../../textures/emojis/black_dot.png",
"source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAABhJREFUGFdjZICA/1CakRGJAxbDKoCiBQCSkQQDt6KjJwAAAABJRU5ErkJggg=="
}
]
}

View file

@ -0,0 +1,237 @@
{
"meta": {
"format_version": "4.5",
"model_format": "free",
"box_uv": false
},
"name": "bullet",
"model_identifier": "",
"visible_box": [
1,
1,
0
],
"variable_placeholders": "",
"variable_placeholder_buttons": [],
"timeline_setups": [],
"unhandled_root_fields": {},
"resolution": {
"width": 4,
"height": 12
},
"elements": [
{
"name": "Bullet1",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
-0.5,
0,
-1
],
"to": [
0.5,
1,
1
],
"autouv": 0,
"color": 0,
"origin": [
0,
0,
0
],
"faces": {
"north": {
"uv": [
1,
3,
2,
4
],
"texture": 0
},
"east": {
"uv": [
1,
3,
2,
11
],
"rotation": 90,
"texture": 0
},
"south": {
"uv": [
1,
10,
2,
11
],
"texture": 0
},
"west": {
"uv": [
1,
3,
2,
11
],
"rotation": 270,
"texture": 0
},
"up": {
"uv": [
1,
3,
2,
11
],
"texture": 0
},
"down": {
"uv": [
1,
3,
2,
11
],
"rotation": 180,
"texture": 0
}
},
"type": "cube",
"uuid": "eb1cee40-d947-b0fd-992e-a0445207f5c3"
},
{
"name": "Bullet2",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
-0.375,
0.125,
-1.25
],
"to": [
0.375,
0.875,
-1
],
"autouv": 0,
"color": 0,
"origin": [
0,
0,
0
],
"faces": {
"north": {
"uv": [
1,
2,
2,
3
],
"texture": 0
},
"east": {
"uv": [
1,
2,
2,
3
],
"texture": 0
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
1,
2,
2,
3
],
"texture": 0
},
"up": {
"uv": [
1,
2,
2,
3
],
"texture": 0
},
"down": {
"uv": [
1,
2,
2,
3
],
"texture": 0
}
},
"type": "cube",
"uuid": "c69c3bcf-da9b-3ad4-7d0a-e697c8066967"
}
],
"outliner": [
{
"name": "Arrow",
"origin": [
0,
0,
0
],
"color": 0,
"uuid": "0c9fab8f-3742-3b33-0bef-de2eca96287f",
"export": true,
"mirror_uv": false,
"isOpen": true,
"locked": false,
"visibility": true,
"autouv": 0,
"children": [
"eb1cee40-d947-b0fd-992e-a0445207f5c3",
"c69c3bcf-da9b-3ad4-7d0a-e697c8066967"
]
}
],
"textures": [
{
"path": "",
"name": "bullet.png",
"folder": "",
"namespace": "",
"id": "0",
"particle": false,
"render_mode": "default",
"render_sides": "auto",
"frame_time": 1,
"frame_order_type": "loop",
"frame_order": "",
"frame_interpolate": false,
"visible": true,
"mode": "bitmap",
"saved": true,
"uuid": "d4264722-75e2-f338-9736-9de1cd8631d0",
"relative_path": "../../textures/bullet.png",
"source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAMCAYAAABFohwTAAAAAXNSR0IArs4c6QAAAGdJREFUGFdjTEhI4Pj1/f93LpbvDH9YuTgZMQSiwuP/V4azMOh4+TKkJi9jYAQJJDt+ZHAK9GFILdoFESj0/cpgEuTBkJoKFSCsAqwlrJAhNXEikhl4BVKdXjE4xAZDbEmJDvvPgAQAMBRGJaIzndEAAAAASUVORK5CYII="
}
]
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,241 @@
{
"meta": {
"format_version": "4.9",
"model_format": "free",
"box_uv": false
},
"name": "costume_masked",
"model_identifier": "",
"visible_box": [
1,
1,
0
],
"variable_placeholders": "",
"variable_placeholder_buttons": [],
"timeline_setups": [],
"unhandled_root_fields": {},
"resolution": {
"width": 32,
"height": 16
},
"elements": [
{
"name": "Mask",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
-4,
24,
-4
],
"to": [
4,
32,
4
],
"autouv": 0,
"color": 0,
"inflate": 0.51,
"origin": [
0,
0,
0
],
"faces": {
"north": {
"uv": [
8,
8,
16,
16
],
"texture": 0
},
"east": {
"uv": [
0,
8,
8,
16
],
"texture": 0
},
"south": {
"uv": [
24,
8,
32,
16
],
"texture": 0
},
"west": {
"uv": [
16,
8,
24,
16
],
"texture": 0
},
"up": {
"uv": [
8,
0,
16,
8
],
"texture": 0
},
"down": {
"uv": [
16,
0,
24,
8
],
"texture": 0
}
},
"type": "cube",
"uuid": "b747bd75-507f-7929-aeae-3c66cb26fac7"
},
{
"name": "Number",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
-1.07,
29,
-4.55
],
"to": [
1.07,
32,
-4.55
],
"autouv": 0,
"color": 0,
"origin": [
0,
0,
0
],
"faces": {
"north": {
"uv": [
0,
0,
5,
7
],
"texture": 0
},
"east": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"up": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"texture": null
}
},
"type": "cube",
"uuid": "9da77f2b-9ae5-1508-7197-7cd1652e0981"
}
],
"outliner": [
{
"name": "CMaskedH",
"origin": [
0,
0,
0
],
"color": 0,
"uuid": "8aa59adb-e386-5f5c-152e-db9887b01653",
"export": true,
"mirror_uv": false,
"isOpen": true,
"locked": false,
"visibility": true,
"autouv": 0,
"children": [
"b747bd75-507f-7929-aeae-3c66cb26fac7",
"9da77f2b-9ae5-1508-7197-7cd1652e0981"
]
}
],
"textures": [
{
"path": "",
"name": "costume_masked.png",
"folder": "",
"namespace": "",
"id": "1",
"width": 32,
"height": 16,
"uv_width": 32,
"uv_height": 16,
"particle": false,
"layers_enabled": false,
"sync_to_project": "",
"render_mode": "default",
"render_sides": "auto",
"frame_time": 1,
"frame_order_type": "loop",
"frame_order": "",
"frame_interpolate": false,
"visible": true,
"internal": true,
"saved": true,
"uuid": "375bf4c3-b00b-7f4b-fdf5-83236cf51fe5",
"relative_path": "../../textures/costume_masked.png",
"source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAQCAYAAAB3AH1ZAAAAAXNSR0IArs4c6QAAANlJREFUSEtjZICCL9++/+fh4mSE8WH0vXnz/oPYohLCDK9fvMWgCckpJSVhmIlsB1gSZLmxkSfD2XPbGdAd8XnbRrAD0C2HiYFoXJaA9PJ6+eN3AMxykEHYHAAKAZDvkR2BzsZlCUgvVUIA2few4INFCcUhQCgKYCEAcgS65bCQoVsIYIsGqoQALBRw5QL0NACLEliIUBwC6FkPmY8cBehZEVkdrmxKMBHiswCXodQUZxx1wJAIAVhCAhWtoByAzqckTRCVBgaNA9CzKyxEaB4CMAuoGfSwwgwAAvE293iLdaoAAAAASUVORK5CYII="
}
]
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,694 @@
{
"meta": {
"format_version": "4.9",
"model_format": "free",
"box_uv": false
},
"name": "ex_skill_1",
"model_identifier": "",
"visible_box": [
1,
1,
0
],
"variable_placeholders": "",
"variable_placeholder_buttons": [],
"timeline_setups": [],
"unhandled_root_fields": {},
"resolution": {
"width": 14,
"height": 13
},
"elements": [
{
"name": "ShineEffect1",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
0,
1.55,
-8.85
],
"to": [
0,
6.05,
-7.35
],
"autouv": 0,
"color": 0,
"origin": [
0,
0.8,
-11.1
],
"faces": {
"north": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"east": {
"uv": [
6,
2,
9,
11
],
"texture": 0
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
6,
2,
9,
11
],
"texture": 0
},
"up": {
"uv": [
0,
0,
0,
0
],
"rotation": 90,
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"rotation": 270,
"texture": null
}
},
"type": "cube",
"uuid": "fb3d3f3b-bb29-91c5-1423-9f4dbce83643"
},
{
"name": "ShineEffect2",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
0,
3.05,
-10.35
],
"to": [
0,
4.55,
-5.85
],
"autouv": 0,
"color": 0,
"origin": [
0,
0.8,
-11.1
],
"faces": {
"north": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"east": {
"uv": [
3,
5,
12,
8
],
"texture": 0
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
3,
5,
12,
8
],
"texture": 0
},
"up": {
"uv": [
0,
0,
0,
0
],
"rotation": 90,
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"rotation": 270,
"texture": null
}
},
"type": "cube",
"uuid": "4dec548f-8312-44ad-95c8-0c3fe22373b1"
},
{
"name": "ShineEffect3",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
0,
0.55,
-8.35
],
"to": [
0,
7.05,
-7.85
],
"autouv": 0,
"color": 0,
"origin": [
0,
0.8,
-11.1
],
"faces": {
"north": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"east": {
"uv": [
7,
0,
8,
13
],
"texture": 0
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
7,
0,
8,
13
],
"texture": 0
},
"up": {
"uv": [
0,
0,
0,
0
],
"rotation": 90,
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"rotation": 270,
"texture": null
}
},
"type": "cube",
"uuid": "1de56f71-072a-4eef-f9a4-1da443e775db"
},
{
"name": "ShineEffect4",
"box_uv": false,
"rescale": false,
"locked": false,
"render_order": "default",
"allow_mirror_modeling": true,
"from": [
0,
3.55,
-11.35
],
"to": [
0,
4.05,
-4.85
],
"autouv": 0,
"color": 0,
"origin": [
0,
0.8,
-11.1
],
"faces": {
"north": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"east": {
"uv": [
1,
6,
14,
7
],
"texture": 0
},
"south": {
"uv": [
0,
0,
0,
0
],
"texture": null
},
"west": {
"uv": [
1,
6,
14,
7
],
"texture": 0
},
"up": {
"uv": [
0,
0,
0,
0
],
"rotation": 90,
"texture": null
},
"down": {
"uv": [
0,
0,
0,
0
],
"rotation": 270,
"texture": null
}
},
"type": "cube",
"uuid": "0a591207-abc9-1b7f-940c-5a03334e5ff8"
}
],
"outliner": [
{
"name": "ShineEffect",
"origin": [
0,
3.8,
-8.1
],
"rotation": [
0,
90,
0
],
"color": 0,
"uuid": "f767d5d3-75c7-501a-a586-e004357f2c28",
"export": true,
"mirror_uv": false,
"isOpen": true,
"locked": false,
"visibility": true,
"autouv": 0,
"children": [
"fb3d3f3b-bb29-91c5-1423-9f4dbce83643",
"4dec548f-8312-44ad-95c8-0c3fe22373b1",
"1de56f71-072a-4eef-f9a4-1da443e775db",
"0a591207-abc9-1b7f-940c-5a03334e5ff8"
]
}
],
"textures": [
{
"path": "",
"name": "ex_skill_1.png",
"folder": "",
"namespace": "",
"id": "0",
"width": 14,
"height": 13,
"uv_width": 14,
"uv_height": 13,
"particle": false,
"layers_enabled": false,
"sync_to_project": "",
"render_mode": "default",
"render_sides": "auto",
"frame_time": 1,
"frame_order_type": "loop",
"frame_order": "",
"frame_interpolate": false,
"visible": true,
"internal": true,
"saved": true,
"uuid": "8fef8f15-329a-9210-3f20-250416f9d3f0",
"relative_path": "../../textures/ex_skill_1.png",
"source": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAANCAYAAACZ3F9/AAAAAXNSR0IArs4c6QAAANlJREFUKFOVksFOAjEURc8DZ2ZJWOgGTFASMP7/tyiYoCSIC2UBrOhQ5pqZMhpjNWkXbdP23Jv3bk2SzMyIDM0fZNP76F30sNXQy0J2M04D9boSZ8QG17/gPx318a7W2S6vEsD9VsigqrB+/39Q65XIc8gLyDLoXsDJg3Pgj1AescGwEWkmPT2qeVzkkNVrEeBapHRQlnA4fO+da8sP1QTHM1DDdUr+BN5/QT8coxnutqLTgUpYr5fQnM1GmAKY1NW3dYhDFUk5NszyWTa6Tfs5odsz2eQuCn4CIl5duuF7d3AAAAAASUVORK5CYII="
}
],
"animations": [
{
"uuid": "7b05d1aa-9efa-10af-3456-222a207d2ee5",
"name": "ex_skill_1",
"loop": "hold",
"override": true,
"length": 4.6,
"snapping": 20,
"selected": true,
"anim_time_update": "",
"blend_weight": "",
"start_delay": "",
"loop_delay": "",
"animators": {
"f767d5d3-75c7-501a-a586-e004357f2c28": {
"name": "ShineEffect",
"type": "bone",
"keyframes": [
{
"channel": "rotation",
"data_points": [
{
"x": "90",
"y": "135",
"z": "90"
}
],
"uuid": "f3d25414-1f60-6f1d-a29a-387b4d5e3122",
"time": 3.95,
"color": -1,
"interpolation": "bezier",
"bezier_linked": true,
"bezier_left_time": [
-0.1,
-0.024,
-0.1
],
"bezier_left_value": [
0,
0,
0
],
"bezier_right_time": [
0.1,
0.024,
0.1
],
"bezier_right_value": [
0,
0,
0
]
},
{
"channel": "rotation",
"data_points": [
{
"x": "90",
"y": "180",
"z": "90"
}
],
"uuid": "5730c31b-d036-b4f3-4eb8-13ef3955c50f",
"time": 4.15,
"color": -1,
"interpolation": "bezier",
"bezier_linked": true,
"bezier_left_time": [
-0.1,
-0.222,
-0.1
],
"bezier_left_value": [
0,
0,
0
],
"bezier_right_time": [
0.1,
0.222,
0.1
],
"bezier_right_value": [
0,
0,
0
]
},
{
"channel": "scale",
"data_points": [
{
"x": "0",
"y": "0",
"z": "0"
}
],
"uuid": "3535fa90-518c-b623-4297-c02d791f15a8",
"time": 0,
"color": -1,
"uniform": true,
"interpolation": "linear",
"bezier_linked": true,
"bezier_left_time": [
-0.1,
-0.1,
-0.1
],
"bezier_left_value": [
0,
0,
0
],
"bezier_right_time": [
0.1,
0.1,
0.1
],
"bezier_right_value": [
0,
0,
0
]
},
{
"channel": "scale",
"data_points": [
{
"x": 0,
"y": 0,
"z": 0
}
],
"uuid": "6104fb46-48a4-03ed-3818-dd2e47b69e54",
"time": 3.95,
"color": -1,
"uniform": true,
"interpolation": "linear",
"bezier_linked": true,
"bezier_left_time": [
-0.1,
-0.1,
-0.1
],
"bezier_left_value": [
0,
0,
0
],
"bezier_right_time": [
0.1,
0.1,
0.1
],
"bezier_right_value": [
0,
0,
0
]
},
{
"channel": "scale",
"data_points": [
{
"x": "1",
"y": "1",
"z": "1"
}
],
"uuid": "759a303f-e459-6cfd-c8c8-c0cce4530232",
"time": 4,
"color": -1,
"uniform": true,
"interpolation": "linear",
"bezier_linked": true,
"bezier_left_time": [
-0.1,
-0.1,
-0.1
],
"bezier_left_value": [
0,
0,
0
],
"bezier_right_time": [
0.1,
0.1,
0.1
],
"bezier_right_value": [
0,
0,
0
]
},
{
"channel": "scale",
"data_points": [
{
"x": 1,
"y": 1,
"z": 1
}
],
"uuid": "c246e8a8-6da3-8727-42cf-66497c2970af",
"time": 4.15,
"color": -1,
"uniform": true,
"interpolation": "bezier",
"bezier_linked": true,
"bezier_left_time": [
-0.02212,
-0.02212,
-0.02212
],
"bezier_left_value": [
0,
0,
0
],
"bezier_right_time": [
0.02212,
0.02212,
0.02212
],
"bezier_right_value": [
0,
0,
0
]
},
{
"channel": "scale",
"data_points": [
{
"x": "0",
"y": "0",
"z": "0"
}
],
"uuid": "915ee3ec-2806-1987-0aeb-047e3c8d2a07",
"time": 4.35,
"color": -1,
"uniform": true,
"interpolation": "bezier",
"bezier_linked": true,
"bezier_left_time": [
-0.22239,
-0.22239,
-0.22239
],
"bezier_left_value": [
0,
0,
0
],
"bezier_right_time": [
0.22239,
0.22239,
0.22239
],
"bezier_right_value": [
0,
0,
0
]
}
]
}
}
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,229 @@
---@class Avatar アバターのメインクラス
---@field public avatarEvents AvatarEvents
---@field public modelUtils ModelUtils
---@field public playerUtils PlayerUtils
---@field public compatibilityUtils CompatibilityUtils
---@field public characterData BlueArchiveCharacter
---@field public headRing HeadRing
---@field public headBlock HeadBlock
---@field public locale Locale
---@field public config Config
---@field public cameraManager CameraManager
---@field public keyManager KeyManager
---@field public vanillaModel VanillaModel
---@field public arms Arms
---@field public skirt Skirt
---@field public armor Armor
---@field public faceParts FaceParts
---@field public portrait Portrait
---@field public physics Physics
---@field public gun GunHoshino
---@field public nameplate Nameplate
---@field public exSkill ExSkill
---@field public frameParticleManager ExSkillFrameParticleManager
---@field public placementObjectManager PlacementObjectManager
---@field public costume Costume
---@field public actionWheel ActionWheel
---@field public actionWheelGui ActionWheelGui
---@field public bubble Bubble
---@field public barrier Barrier
---@field public deathAnimation DeathAnimation
---@field public hypixelZombies HypixelZombies
---@field public updateChecker UpdateChecker
---@field public shield Shield
---@field public whaleFloat WhaleFloat
---@field public subGun SubGun
---@field public waveParticleManager ExSkill2WaveParticleManager
---@field public instantiate fun(class: table, super: table, ...: any) クラスをインスタンス化する
Avatar = {
---コンストラクタ
---@return Avatar
new = function ()
---@type Avatar
local instance = Avatar.instantiate(Avatar)
--ENTITY_INIT前に読み込み
require("scripts.avatar_modules.avatar_module")
--ユーティリティクラスの読み込み
require("scripts.avatar_modules.events.abstract_event")
require("scripts.avatar_modules.events.script_init_event")
require("scripts.avatar_modules.events.avatar_events")
instance.avatarEvents = AvatarEvents.new(instance)
instance.avatarEvents:init()
require("scripts.avatar_modules.utils.model_utils")
instance.modelUtils = ModelUtils.new(instance)
instance.modelUtils:init()
--アバターモジュールの読み込み
require("scripts.blue_archive_character")
instance.characterData = BlueArchiveCharacter.new(instance)
instance.characterData:init()
require("scripts.avatar_modules.head_ring")
instance.headRing = HeadRing.new(instance)
instance.headRing:init()
require("scripts.avatar_modules.head_model_generator")
require("scripts.avatar_modules.head_block")
instance.headBlock = HeadBlock.new(instance)
instance.headBlock:init()
--生徒固有クラスの読み込み
events.ENTITY_INIT:register(function ()
--ユーティリティクラスの読み込み
require("scripts.avatar_modules.utils.player_utils")
instance.playerUtils = PlayerUtils.new(instance)
instance.playerUtils:init()
require("scripts.avatar_modules.utils.compatibility_utils")
instance.compatibilityUtils = CompatibilityUtils.new(instance)
instance.compatibilityUtils:init()
require("scripts.avatar_modules.utils.spawn_object_manager")
require("scripts.avatar_modules.utils.spawn_object")
--アバターモジュールの読み込み
require("scripts.avatar_modules.locale")
instance.locale = Locale.new(instance)
instance.locale:init()
require("scripts.avatar_modules.config")
instance.config = Config.new(instance)
instance.config:init()
require("scripts.avatar_modules.camera_manager")
instance.cameraManager = CameraManager.new(instance)
instance.cameraManager:init()
require("scripts.avatar_modules.key_manager")
instance.keyManager = KeyManager.new(instance)
instance.keyManager:init()
require("scripts.avatar_modules.vanilla_model")
instance.vanillaModel = VanillaModel.new(instance)
instance.vanillaModel:init()
require("scripts.avatar_modules.arms")
instance.arms = Arms.new(instance)
instance.arms:init()
require("scripts.avatar_modules.skirt")
instance.skirt = Skirt.new(instance)
instance.skirt:init()
require("scripts.avatar_modules.armor")
instance.armor = Armor.new(instance)
instance.armor:init()
require("scripts.avatar_modules.face_parts")
instance.faceParts = FaceParts.new(instance)
instance.faceParts:init()
require("scripts.avatar_modules.portrait")
instance.portrait = Portrait.new(instance)
instance.portrait:init()
require("scripts.avatar_modules.physics")
instance.physics = Physics.new(instance)
instance.physics:init()
require("scripts.avatar_modules.gun")
require("scripts.character_scripts.gun_hoshino")
instance.gun = GunHoshino.new(instance)
instance.gun:init()
require("scripts.avatar_modules.nameplate")
instance.nameplate = Nameplate.new(instance)
instance.nameplate:init()
require("scripts.avatar_modules.ex_skill.ex_skill")
instance.exSkill = ExSkill.new(instance)
instance.exSkill:init()
require("scripts.avatar_modules.ex_skill.ex_skill_frame_particle_manager")
require("scripts.avatar_modules.ex_skill.ex_skill_frame_particle")
instance.frameParticleManager = ExSkillFrameParticleManager.new(instance)
instance.frameParticleManager:init()
require("scripts.avatar_modules.placement_object.placement_object_manager")
require("scripts.avatar_modules.placement_object.placement_object")
instance.placementObjectManager = PlacementObjectManager.new(instance)
instance.placementObjectManager:init()
require("scripts.avatar_modules.costume")
instance.costume = Costume.new(instance)
instance.costume:init()
require("scripts.avatar_modules.action_wheel.action_wheel")
instance.actionWheel = ActionWheel.new(instance)
instance.actionWheel:init()
require("scripts.avatar_modules.action_wheel.action_wheel_gui")
instance.actionWheelGui = ActionWheelGui.new(instance)
instance.actionWheelGui:init()
require("scripts.avatar_modules.bubble")
instance.bubble = Bubble.new(instance)
instance.bubble:init()
require("scripts.avatar_modules.barrier")
instance.barrier = Barrier.new(instance)
instance.barrier:init()
require("scripts.avatar_modules.death_animation")
instance.deathAnimation = DeathAnimation.new(instance)
instance.deathAnimation:init()
require("scripts.avatar_modules.hypixel_zombies")
instance.hypixelZombies = HypixelZombies.new(instance)
instance.hypixelZombies:init()
require("scripts.avatar_modules.action_wheel.update_checker")
instance.updateChecker = UpdateChecker.new(instance)
instance.updateChecker:init()
--生徒固有クラスの読み込み
require("scripts.character_scripts.shield")
instance.shield = Shield.new(instance)
instance.shield:init()
require("scripts.character_scripts.whale_float")
instance.whaleFloat = WhaleFloat.new(instance)
instance.whaleFloat:init()
require("scripts.character_scripts.sub_gun")
instance.subGun = SubGun.new(instance)
instance.subGun:init()
require("scripts.character_scripts.ex_skill_2_wave_particle_manager")
require("scripts.character_scripts.ex_skill_2_wave_particle")
instance.waveParticleManager = ExSkill2WaveParticleManager.new(instance)
instance.waveParticleManager:init()
--SCRIPT_INITイベントを実行
instance.avatarEvents.SCRIPT_INIT:fire()
end)
return instance
end;
---クラスをインスタンス化する。
---@generic S
---@generic C
---@param class `C` インスタンス化するクラス
---@param super? `S` インスタンス化するクラスのスーパークラス
---@param ... any クラスのインスタンス時に渡される引数
---@return C instance インスタンス化されたクラスのオブジェクト
instantiate = function (class, super, ...)
local instance = super and super.new(...) or {}
setmetatable(instance, {__index = class})
setmetatable(class, {__index = super})
return instance
end;
}
AvatarInstance = Avatar.new()

View file

@ -0,0 +1,319 @@
---@class (exact) ActionWheel : AvatarModule アクションホイールを管理するクラス
---@field package mainPage Page アクションホイールのメインページのインスタンスへの参照
---@field package selectingCostume integer 現在選択中の衣装
---@field package selectingName integer 現在選択中の表示名
---@field package selectingShouldShowClubName boolean 現在選択中の「部活名を表示するかどうか」
---@field package selectingExSkillParticleAmount integer 現在選択中のExスキルフレームのパーティクル量
---@field public shouldReplaceVehicleModels boolean 乗り物のモデルを置き換えるかどうか
---@field package isActionWheelOpenedPrev boolean 前ティックにアクションホイールを開けていたかどうか
---@field package refreshCostumeChangeActionTitle fun(self: ActionWheel) 衣装変更アクションのタイトルを更新する
---@field package refreshNameChangeActionTitle fun(self: ActionWheel) 名前変更アクションのタイトルを更新する
---@field package refreshExSkillParticleActionTitle fun(self: ActionWheel) Exスキルアニメーションのパーティクル量調整アクションのタイトルを更新する
---@field package refreshUpdateActionStatus fun(self: ActionWheel) アップデート確認アクションの状態を更新する
ActionWheel = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return ActionWheel
new = function (parent)
---@type ActionWheel
local instance = Avatar.instantiate(ActionWheel, AvatarModule, parent)
instance.mainPage = action_wheel:newPage("main")
instance.selectingCostume = instance.parent.costume.currentCostume
instance.selectingName = instance.parent.nameplate.currentName
instance.selectingShouldShowClubName = instance.parent.nameplate.shouldShowClubName
instance.selectingExSkillParticleAmount = instance.parent.exSkill.frameParticleAmount
instance.shouldReplaceVehicleModels = instance.parent.config:loadConfig("PRIVATE", "replaceVehicleModels", true)
instance.isActionWheelOpenedPrev = false
return instance
end;
---初期化関数
---@param self ActionWheel
init = function (self)
AvatarModule.init(self)
if host:isHost() then
events.TICK:register(function()
local isActionWheelOpened = action_wheel:isEnabled()
if isActionWheelOpened then
local mainAction3 = self.mainPage:getAction(3)
mainAction3:setTitle(self.parent.locale:getLocale("action_wheel.main.action_3.title").."§c"..self.parent.locale:getLocale("action_wheel.toggle_off"))
mainAction3:setToggleTitle(self.parent.locale:getLocale("action_wheel.main.action_3.title").."§a"..self.parent.locale:getLocale("action_wheel.toggle_on"))
local mainAction4 = self.mainPage:getAction(4)
mainAction4:setTitle(self.parent.locale:getLocale("action_wheel.main.action_4.title").."§c"..self.parent.locale:getLocale("action_wheel.toggle_off"))
mainAction4:setToggleTitle(self.parent.locale:getLocale("action_wheel.main.action_4.title").."§a"..self.parent.locale:getLocale("action_wheel.toggle_on"))
local mainAction6 = self.mainPage:getAction(6)
if self.parent.characterData.actionWheel.isVehicleOptionEnabled then
mainAction6:setTitle(self.parent.locale:getLocale("action_wheel.main.action_6.title").."§c"..self.parent.locale:getLocale("action_wheel.toggle_off"))
else
mainAction6:setTitle("§7"..self.parent.locale:getLocale("action_wheel.main.action_6.title")..self.parent.locale:getLocale("action_wheel.toggle_off"))
end
mainAction6:setToggleTitle(self.parent.locale:getLocale("action_wheel.main.action_6.title").."§a"..self.parent.locale:getLocale("action_wheel.toggle_on"))
self:refreshCostumeChangeActionTitle()
self:refreshNameChangeActionTitle()
self:refreshExSkillParticleActionTitle()
self:refreshUpdateActionStatus()
elseif not isActionWheelOpened and self.isActionWheelOpenedPrev then
if self.selectingCostume ~= self.parent.costume.currentCostume then
pings.actionWheelChangeCostume(self.selectingCostume)
self.parent.config:saveConfig("PRIVATE", "costume", self.selectingCostume)
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:item.armor.equip_leather"), player:getPos())
print(self.parent.locale:getLocale("action_wheel.main.action_1.done_first")..self.parent.costume:getCostumeLocalName(self.selectingCostume)..self.parent.locale:getLocale("action_wheel.main.action_1.done_last"))
end
if self.selectingName ~= self.parent.nameplate.currentName or self.selectingShouldShowClubName ~= self.parent.nameplate.shouldShowClubName then
pings.actionWheelChangeName(self.selectingName, self.selectingShouldShowClubName)
self.parent.config:saveConfig("PRIVATE", "name", self.selectingName)
self.parent.config:saveConfig("PRIVATE", "showClubName", self.selectingShouldShowClubName)
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:ui.cartography_table.take_result"), player:getPos())
print(self.parent.locale:getLocale("action_wheel.main.action_2.done_first")..self.parent.nameplate:getName(self.selectingName)..self.parent.locale:getLocale("action_wheel.main.action_2.done_last"))
end
if self.selectingExSkillParticleAmount ~= self.parent.exSkill.frameParticleAmount then
self.parent.exSkill.frameParticleAmount = self.selectingExSkillParticleAmount
self.parent.config:saveConfig("PRIVATE", "exSkillFrameParticleAmount", self.selectingExSkillParticleAmount)
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:entity.item.pickup"), player:getPos(), 1, 0.5)
print(self.parent.locale:getLocale("action_wheel.main.action_5.done_first")..self.parent.locale:getLocale("action_wheel.main.action_5.option_"..self.selectingExSkillParticleAmount)..self.parent.locale:getLocale("action_wheel.main.action_5.done_last"))
end
end
self.isActionWheelOpenedPrev = isActionWheelOpened
end)
--アクションの設定
--アクション1. 衣装を変更
self.mainPage:newAction(1):setItem(self.parent.compatibilityUtils:checkItem("minecraft:leather_chestplate")):setOnScroll(function(direction)
if #self.parent.costume.costumeList >= 2 then
if direction < 0 then
self.selectingCostume = self.selectingCostume == #self.parent.costume.costumeList and 1 or self.selectingCostume + 1
else
self.selectingCostume = self.selectingCostume == 1 and #self.parent.costume.costumeList or self.selectingCostume - 1
end
self:refreshCostumeChangeActionTitle()
else
print(self.parent.locale:getLocale("action_wheel.main.action_1.unavailable"))
end
end):setOnLeftClick(function()
if #self.parent.costume.costumeList >= 2 then
self.selectingCostume = self.parent.costume.currentCostume
self:refreshCostumeChangeActionTitle()
end
end):setOnRightClick(function()
if #self.parent.costume.costumeList >= 2 then
self.selectingCostume = 1
self:refreshCostumeChangeActionTitle()
end
end)
if #self.parent.costume.costumeList >= 2 then
local action = self.mainPage:getAction(1)
action:setColor(0.78, 0.78, 0.78)
action:setHoverColor(1, 1, 1)
else
local action = self.mainPage:getAction(1)
action:setColor(0.16, 0.16, 0.16)
action:setHoverColor(1, 0.33, 0.33)
end
--アクション2. 表示名の変更
self.mainPage:newAction(2):setItem(self.parent.compatibilityUtils:checkItem("minecraft:name_tag")):setColor(0.78, 0.78, 0.78):setHoverColor(1, 1, 1):setOnScroll(function(direction)
if direction < 0 then
self.selectingName = self.selectingName == 6 and 1 or self.selectingName + 1
else
self.selectingName = self.selectingName == 1 and 6 or self.selectingName - 1
end
self:refreshNameChangeActionTitle()
end):setOnLeftClick(function()
self.selectingShouldShowClubName = not self.selectingShouldShowClubName
self:refreshNameChangeActionTitle()
end):setOnRightClick(function()
self.selectingName = 1
self.selectingShouldShowClubName = false
self:refreshNameChangeActionTitle()
end)
--アクション3. 防具の表示
self.mainPage:newAction(3):setItem(self.parent.compatibilityUtils:checkItem("minecraft:iron_chestplate")):setColor(0.67, 0, 0):setHoverColor(1, 0.33, 0.33):setToggleColor(0, 0.67, 0):setOnToggle(function (_, action)
pings.actionWheelSetArmorVisible(true)
action:setHoverColor(0.33, 1, 0.33)
self.parent.config:saveConfig("PRIVATE", "showArmor", true)
end):setOnUntoggle(function(_, action)
pings.actionWheelSetArmorVisible(false)
action:setHoverColor(1, 0.33, 0.33)
self.parent.config:saveConfig("PRIVATE", "showArmor", false)
end)
if self.parent.config:loadConfig("PRIVATE", "showArmor", false) then
local action = self.mainPage:getAction(3)
action:setToggled(true)
action:setHoverColor(0.33, 1, 0.33)
end
--アクション4. 一人称視点での武器モデルの表示
self.mainPage:newAction(4):setItem(self.parent.compatibilityUtils:checkItem("minecraft:bow")):setColor(0.67, 0, 0):setHoverColor(1, 0.33, 0.33):setToggleColor(0, 0.67, 0):setOnToggle(function (_, action)
self.parent.gun.shouldShowWeaponInFirstPerson = true
action:setHoverColor(0.33, 1, 0.33)
self.parent.config:saveConfig("PRIVATE", "firstPersonWeapon", true)
end):setOnUntoggle(function (_, action)
self.parent.gun.shouldShowWeaponInFirstPerson = false
action:setHoverColor(1, 0.33, 0.33)
self.parent.config:saveConfig("PRIVATE", "firstPersonWeapon", false)
end)
if self.parent.config:loadConfig("PRIVATE", "firstPersonWeapon", true) then
local action = self.mainPage:getAction(4)
action:setToggled(true)
action:setHoverColor(0.33, 1, 0.33)
end
--アクション5. Exスキルフレームのパーティクルの量
self.mainPage:newAction(5):setItem(self.parent.compatibilityUtils:checkItem("minecraft:glowstone_dust")):setColor(0.78, 0.78, 0.78):setHoverColor(1, 1, 1):setOnScroll(function (direction)
if direction < 0 then
self.selectingExSkillParticleAmount = self.selectingExSkillParticleAmount == 4 and 1 or self.selectingExSkillParticleAmount + 1
else
self.selectingExSkillParticleAmount = self.selectingExSkillParticleAmount == 1 and 4 or self.selectingExSkillParticleAmount - 1
end
self:refreshExSkillParticleActionTitle()
end):setOnLeftClick(function ()
self.selectingExSkillParticleAmount = self.parent.exSkill.frameParticleAmount
self:refreshExSkillParticleActionTitle()
end):setOnRightClick(function ()
self.selectingExSkillParticleAmount = 1
self:refreshExSkillParticleActionTitle()
end)
--アクション6. 乗り物モデルの置き換え
self.mainPage:newAction(6):setItem(self.parent.compatibilityUtils:checkItem("minecraft:oak_boat")):setColor(0.67, 0, 0):setHoverColor(1, 0.33, 0.33):setToggleColor(0, 0.67, 0):setOnToggle(function (_, action)
if self.parent.characterData.actionWheel.isVehicleOptionEnabled then
pings.actionWheelSetShouldReplaceVehicleModels(true)
action:setHoverColor(0.33, 1, 0.33)
self.parent.config:saveConfig("PRIVATE", "replaceVehicleModels", true)
else
print(self.parent.locale:getLocale("action_wheel.main.action_6.unavailable"))
action:setToggled(false)
end
end):setOnUntoggle(function (_, action)
pings.actionWheelSetShouldReplaceVehicleModels(false)
action:setHoverColor(1, 0.33, 0.33)
self.parent.config:saveConfig("PRIVATE", "replaceVehicleModels", false)
end)
if not self.parent.characterData.actionWheel.isVehicleOptionEnabled then
local action = self.mainPage:getAction(6)
action:setColor(0.16, 0.16, 0.16)
action:setHoverColor(1, 0.33, 0.33)
self.shouldReplaceVehicleModels = false
elseif self.shouldReplaceVehicleModels then
local action = self.mainPage:getAction(6)
action:setToggled(true)
action:setHoverColor(0.33, 1, 0.33)
end
--アクション7. アップデートの確認
self.mainPage:newAction(7):setItem("minecraft:compass"):setOnLeftClick(function ()
if not self.parent.updateChecker.checkerStatus ~= "CHECKING" then
self.parent.updateChecker:checkUpdate()
else
print("action_wheel.main.action_7.ongoing")
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.note_block.bass"), player:getPos(), 1, 0.5)
end
if not net:isNetworkingAllowed() or not net:isLinkAllowed("https://api.github.com") then
print(self.parent.locale:getLocale("action_wheel.main.action_7.networking_api"))
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.note_block.bass"), player:getPos(), 1, 0.5)
end
end):onRightClick(function ()
if self.parent.updateChecker.latestVersion ~= nil then
host:setClipboard("https://github.com/Gakuto1112/FiguraBlueArchiveCharacters/releases/tag/"..self.parent.updateChecker.latestVersion)
print(self.parent.locale:getLocale("action_wheel.main.action_7.copied"))
else
print(self.parent.locale:getLocale("action_wheel.main.action_7.cannot_check_latest"))
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.note_block.bass"), player:getPos(), 1, 0.5)
end
end)
--アクション8. (空欄)
self.mainPage:newAction(8):setColor(0.16, 0.16, 0.16):setHoverColor(0.16, 0.16, 0.16)
action_wheel:setPage(self.mainPage)
end
end;
---衣装変更アクションのタイトルを更新する。
---@param self ActionWheel
refreshCostumeChangeActionTitle = function (self)
if #self.parent.costume.costumeList >= 2 then
self.mainPage:getAction(1):setTitle(self.parent.locale:getLocale("action_wheel.main.action_1.title").."§b"..self.parent.costume:getCostumeLocalName(self.selectingCostume))
else
self.mainPage:getAction(1):setTitle("§7"..self.parent.locale:getLocale("action_wheel.main.action_1.title")..self.parent.costume:getCostumeLocalName(self.selectingCostume))
end
end;
---名前変更アクションのタイトルを更新する。
---@param self ActionWheel
refreshNameChangeActionTitle = function (self)
if self.selectingName >= 2 then
if self.selectingShouldShowClubName then
self.mainPage:getAction(2):setTitle(self.parent.locale:getLocale("action_wheel.main.action_2.title").."§b"..self.parent.nameplate:getName(self.selectingName).."\n§r"..self.parent.locale:getLocale("action_wheel.main.action_2.title_2").."§a"..self.parent.locale:getLocale("action_wheel.toggle_on"))
else
self.mainPage:getAction(2):setTitle(self.parent.locale:getLocale("action_wheel.main.action_2.title").."§b"..self.parent.nameplate:getName(self.selectingName).."\n§r"..self.parent.locale:getLocale("action_wheel.main.action_2.title_2").."§c"..self.parent.locale:getLocale("action_wheel.toggle_off"))
end
else
self.mainPage:getAction(2):setTitle(self.parent.locale:getLocale("action_wheel.main.action_2.title").."§b"..self.parent.nameplate:getName(self.selectingName).."\n§7"..self.parent.locale:getLocale("action_wheel.main.action_2.title_2")..self.parent.locale:getLocale("action_wheel.toggle_"..(self.selectingShouldShowClubName and "on" or "off")))
end
end;
---Exスキルアニメーションのパーティクル量調整アクションのタイトルを更新する。
---@param self ActionWheel
refreshExSkillParticleActionTitle = function (self)
self.mainPage:getAction(5):title(self.parent.locale:getLocale("action_wheel.main.action_5.title").."§b"..self.parent.locale:getLocale("action_wheel.main.action_5.option_"..self.selectingExSkillParticleAmount))
end;
---アップデート確認アクションの状態を更新する。
---@param self ActionWheel
refreshUpdateActionStatus = function (self)
local action = self.mainPage:getAction(7)
local actionTitle = ""
if self.parent.updateChecker.checkerStatus == "CHECKING" then
actionTitle = actionTitle.."§7"..self.parent.locale:getLocale("action_wheel.main.action_7.title_1")..self.parent.locale:getLocale("action_wheel.main.action_7.title_2").."\n"
action:setColor(0.16, 0.16, 0.16)
action:setHoverColor(1, 0.33, 0.33)
else
actionTitle = actionTitle..self.parent.locale:getLocale("action_wheel.main.action_7.title_1").."§b"..self.parent.locale:getLocale("action_wheel.main.action_7.title_2").."\n"
action:setColor(0.78, 0.78, 0.78)
action:setHoverColor(1, 1, 1)
end
if self.parent.updateChecker.latestVersion == nil then
actionTitle = actionTitle.."§7"..self.parent.locale:getLocale("action_wheel.main.action_7.title_3")..self.parent.locale:getLocale("action_wheel.main.action_7.title_4")
else
actionTitle = actionTitle.."§r"..self.parent.locale:getLocale("action_wheel.main.action_7.title_3").."§b"..self.parent.locale:getLocale("action_wheel.main.action_7.title_4")
end
action:setTitle(actionTitle)
end;
}
---アクションホイールから衣装を変更するトリガー関数
---@param costumeId integer 新しい衣装のインデックス番号
function pings.actionWheelChangeCostume(costumeId)
if costumeId >= 2 then
AvatarInstance.costume:setCostume(costumeId)
else
AvatarInstance.costume:resetCostume()
end
end
---アクションホイールから名前を変更するトリガー関数
---@param typeId integer 新しい名前の表示形式のインデックス番号
---@param shouldShowClubName boolean 部活名を表示するかどうか
function pings.actionWheelChangeName(typeId, shouldShowClubName)
AvatarInstance.nameplate:setName(typeId, shouldShowClubName)
end
---アクションホイールから防具の可視性を変更するトリガー関数
---@param visible boolean 防具を表示するかどうか
function pings.actionWheelSetArmorVisible(visible)
AvatarInstance.armor.shouldShowArmor = visible
end
---アクションホイールから乗り物モデルの置き換えを変更するトリガー関数
---@param enabled boolean 乗り物モデルの置き換えを有効化するかどうか
function pings.actionWheelSetShouldReplaceVehicleModels(enabled)
AvatarInstance.actionWheel.shouldReplaceVehicleModels = enabled
end

View file

@ -0,0 +1,113 @@
---@class ActionWheelGui : AvatarModule アクションホイールに表示する追加のGUIを管理するクラス
---@field package isActionWheelOpenedPrev boolean 前ティックにアクションホイールを開けていたかどうか
ActionWheelGui = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return ActionWheelGui
new = function (parent)
---@type ActionWheelGui
local instance = Avatar.instantiate(ActionWheelGui, AvatarModule, parent)
instance.isActionWheelOpenedPrev = false
return instance
end;
---初期化関数
---@param self ActionWheelGui
init = function (self)
AvatarModule.init(self)
if host:isHost() then
events.TICK:register(function ()
local isActionWheelOpened = action_wheel:isEnabled()
if isActionWheelOpened and not self.isActionWheelOpenedPrev then
models.models.action_wheel_gui.Gui:setVisible(true)
local windowSize = client:getScaledWindowSize()
models.models.action_wheel_gui.Gui.BubbleGuide:setPos(windowSize.x * -0.5 + 44, windowSize.y * -0.5 + 5, 0)
models.models.action_wheel_gui.Gui.ExSkillGuide:setPos(windowSize.x * -0.5 + 57, -21, 0)
models.models.action_wheel_gui.Gui.VersionDisplay:setPos(-0.75, -0.5, 0)
local bubbleGuideTextTasks = {models.models.action_wheel_gui.Gui.BubbleGuide:getTask("action_wheel.gui.bubble_guide.title"), models.models.action_wheel_gui.Gui.BubbleGuide.Emojis.GoodEmoji:getTask("bubble_guide.bubble_1.key_name"), models.models.action_wheel_gui.Gui.BubbleGuide.Emojis.HeartEmoji:getTask("bubble_guide.bubble_2.key_name"), models.models.action_wheel_gui.Gui.BubbleGuide.Emojis.NoteEmoji:getTask("bubble_guide.bubble_3.key_name"), models.models.action_wheel_gui.Gui.BubbleGuide.Emojis.QuestionEmoji:getTask("bubble_guide.bubble_4.key_name"), models.models.action_wheel_gui.Gui.BubbleGuide.Emojis.SweatEmoji:getTask("bubble_guide.bubble_5.key_name")}
bubbleGuideTextTasks[1]:setText(self.parent.locale:getLocale("action_wheel.gui.bubble_guide.title"))
for i = 2, #bubbleGuideTextTasks do
bubbleGuideTextTasks[i]:setText("§0"..self.parent.keyManager.keyMappings["bubble_"..(i - 1)]:getKeyName())
end
local bubbleGuideTitleWidth = client.getTextWidth(bubbleGuideTextTasks[1]:getText()) / 2 + 4
local bubbleGuideBodyWidth = 0
for i = 2, #bubbleGuideTextTasks do
bubbleGuideBodyWidth = math.max(bubbleGuideBodyWidth, client.getTextWidth(bubbleGuideTextTasks[i]:getText()) * 0.5)
end
local bubbleGuideWidth = math.max(bubbleGuideTitleWidth + 6, bubbleGuideBodyWidth + 22)
bubbleGuideWidth = math.max(bubbleGuideWidth, 39)
models.models.action_wheel_gui.Gui.BubbleGuide.BubbleGuideBackground.TitleBar:setPos((bubbleGuideWidth - bubbleGuideTitleWidth) / 2, 0, 0)
models.models.action_wheel_gui.Gui.BubbleGuide.BubbleGuideBackground.TitleBar:setScale(bubbleGuideTitleWidth, 1, 1)
models.models.action_wheel_gui.Gui.BubbleGuide.BubbleGuideBackground.TitleCenter:setScale(bubbleGuideWidth - 25, 1, 1)
models.models.action_wheel_gui.Gui.BubbleGuide.BubbleGuideBackground.TitleLeft:setPos(bubbleGuideWidth - 26, 0, 0)
models.models.action_wheel_gui.Gui.BubbleGuide.BubbleGuideBackground.BodyTop:setScale(bubbleGuideWidth, 1, 1)
models.models.action_wheel_gui.Gui.BubbleGuide.BubbleGuideBackground.BodyBottomCenter:setScale(bubbleGuideWidth - 39, 1, 1)
models.models.action_wheel_gui.Gui.BubbleGuide.BubbleGuideBackground.BodyBottomLeft:setPos(bubbleGuideWidth - 40, 0, 0)
models.models.action_wheel_gui.Gui.BubbleGuide.Emojis:setPos((bubbleGuideWidth - (bubbleGuideBodyWidth + 22)) / 2 + bubbleGuideBodyWidth + 9, 0, 0)
bubbleGuideTextTasks[1]:setPos(bubbleGuideWidth / 2, 0, 0)
for i = 2, #bubbleGuideTextTasks do
bubbleGuideTextTasks[i]:setPos(bubbleGuideBodyWidth / -2 - 4, 1.5, 0)
end
local exSkillGuideTextTasks = {models.models.action_wheel_gui.Gui.ExSkillGuide:getTask("action_wheel.gui.ex_skill_guide.title"), models.models.action_wheel_gui.Gui.ExSkillGuide:getTask("action_wheel.gui.ex_skill_guide.body_1"), models.models.action_wheel_gui.Gui.ExSkillGuide:getTask("action_wheel.gui.ex_skill_guide.body_2")}
exSkillGuideTextTasks[1]:setText(self.parent.locale:getLocale("action_wheel.gui.ex_skill_guide.title"))
exSkillGuideTextTasks[2]:setText(self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].subExSkill == nil and "§0§l\""..self.parent.locale:getLocale("action_wheel.gui.ex_skill_guide.ex_skill_"..self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].exSkill..".name").."\"" or "§0§l\""..self.parent.locale:getLocale("action_wheel.gui.ex_skill_guide.ex_skill_"..self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].exSkill..".name").."\"§r§0 - \""..self.parent.keyManager.keyMappings.ex_skill:getKeyName().."\"")
exSkillGuideTextTasks[3]:setText(self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].subExSkill == nil and "§0"..self.parent.locale:getLocale("action_wheel.gui.ex_skill_guide.key_pre")..self.parent.keyManager.keyMappings.ex_skill:getKeyName()..self.parent.locale:getLocale("action_wheel.gui.ex_skill_guide.key_post") or "§0§l\""..self.parent.locale:getLocale("action_wheel.gui.ex_skill_guide.ex_skill_"..self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].subExSkill..".name").."\"§r§0 - \""..self.parent.keyManager.keyMappings.ex_skill_sub:getKeyName().."\"")
local exSkillGuideTitleWidth = client.getTextWidth(exSkillGuideTextTasks[1]:getText()) / 2 + 4
local exSkillGuideWidth = exSkillGuideTitleWidth
for i = 2, #exSkillGuideTextTasks do
exSkillGuideWidth = math.max(exSkillGuideWidth, client.getTextWidth(exSkillGuideTextTasks[i]:getText()) / 2 + 10)
end
exSkillGuideWidth = math.max(exSkillGuideWidth, 39)
models.models.action_wheel_gui.Gui.ExSkillGuide.ExSkillGuideBackground.TitleBar:setPos((exSkillGuideWidth - exSkillGuideTitleWidth) / 2, 0, 0)
models.models.action_wheel_gui.Gui.ExSkillGuide.ExSkillGuideBackground.TitleBar:setScale(exSkillGuideTitleWidth, 1, 1)
models.models.action_wheel_gui.Gui.ExSkillGuide.ExSkillGuideBackground.TitleCenter:setScale(exSkillGuideWidth - 25, 1, 1)
models.models.action_wheel_gui.Gui.ExSkillGuide.ExSkillGuideBackground.TitleLeft:setPos(exSkillGuideWidth - 26, 0, 0)
models.models.action_wheel_gui.Gui.ExSkillGuide.ExSkillGuideBackground.BodyBottomCenter:setScale(exSkillGuideWidth - 39, 1, 1)
models.models.action_wheel_gui.Gui.ExSkillGuide.ExSkillGuideBackground.BodyBottomLeft:setPos(exSkillGuideWidth - 40, 0, 0)
exSkillGuideTextTasks[1]:setPos(exSkillGuideWidth / 2, 0, 0)
for i = 2, #exSkillGuideTextTasks do
exSkillGuideTextTasks[i]:setPos(exSkillGuideWidth / 2, (i - 2) * -5 - 8)
end
local versionDisplayTextTasks = {}
for i = 1, 3 do
table.insert(versionDisplayTextTasks, models.models.action_wheel_gui.Gui.VersionDisplay:getTask("action_wheel.gui.version_display.l"..i))
end
for i = 2, 3 do
versionDisplayTextTasks[i]:setPos(0, (i - 1) * -2.25, 0)
end
elseif not isActionWheelOpened and self.isActionWheelOpenedPrev then
models.models.action_wheel_gui.Gui:setVisible(false)
end
self.isActionWheelOpenedPrev = isActionWheelOpened
end)
models.models.action_wheel_gui.Gui:setScale(2, 2, 2)
for _, textTask in ipairs({models.models.action_wheel_gui.Gui.BubbleGuide:newText("action_wheel.gui.bubble_guide.title"), models.models.action_wheel_gui.Gui.BubbleGuide.Emojis.GoodEmoji:newText("bubble_guide.bubble_1.key_name"), models.models.action_wheel_gui.Gui.BubbleGuide.Emojis.HeartEmoji:newText("bubble_guide.bubble_2.key_name"), models.models.action_wheel_gui.Gui.BubbleGuide.Emojis.NoteEmoji:newText("bubble_guide.bubble_3.key_name"), models.models.action_wheel_gui.Gui.BubbleGuide.Emojis.QuestionEmoji:newText("bubble_guide.bubble_4.key_name"), models.models.action_wheel_gui.Gui.BubbleGuide.Emojis.SweatEmoji:newText("bubble_guide.bubble_5.key_name"), models.models.action_wheel_gui.Gui.ExSkillGuide:newText("action_wheel.gui.ex_skill_guide.title"), models.models.action_wheel_gui.Gui.ExSkillGuide:newText("action_wheel.gui.ex_skill_guide.body_1"), models.models.action_wheel_gui.Gui.ExSkillGuide:newText("action_wheel.gui.ex_skill_guide.body_2")}) do
textTask:setScale(0.5, 0.5, 0.5)
textTask:setAlignment("CENTER")
end
for i = 1, 3 do
local textTask = models.models.action_wheel_gui.Gui.VersionDisplay:newText("action_wheel.gui.version_display.l"..i)
textTask:setScale(0.25, 0.25, 0.25)
textTask:setShadow(true)
if i == 1 then
textTask:setText("Figura Blue Archive Characters (FBAC)")
end
end
end
end;
}

View file

@ -0,0 +1,181 @@
---@alias UpdateChecker.CheckerStatus
---| "INIT" # 初期状態
---| "CHECKING" # アップデート確認中
---| "LATEST" # アップデート確認済み:最新版
---| "UPDATE_AVAILABLE" # アップデート確認済み:アップデートあり
---| "ERROR_INVALID_JSON" # エラー:予期しないJSONデータ
---| "ERROR_INVALID_JSON_SYNTAX" # エラー:不正なJSON構文
---| "ERROR_REQUEST_FAILED" # リクエストに失敗
---| "ERROR_NETWORK_ERR" # ネットワークエラー
---| "ERROR_NOT_ALLOWED" # ネットワーキングAPIが不許可
---@class (exact) UpdateChecker : AvatarModule FBACのアップデートの確認を管理するクラス
---@field package FBAC_VERSION string 現在のFBACバージョン
---@field package BRANCH_NAME string このブランチ名(キャラクター名)
---@field public latestVersion? string リモート上にある最新のFBACバージョン
---@field public checkerStatus UpdateChecker.CheckerStatus アップデートチェッカーの状態
---@field package requestStatus integer 送信したリクエストのステータスコード
---@field package responseHandler Future.HttpResponse|nil httpレスポンスのハンドラ
---@field package textAnimationCount integer 新しいバージョン表示のテキストのアニメーションのカウンター
---@field package isActionWheelOpenedPrev boolean 前ティックにアクションホイールを開けていたかどうか
---@field package compareVersions fun(version1: string, version2: string): string|nil 2つのバージョン文字列を比較し、新しい方を返す
---@field package showNewUpdateMessage fun(self: UpdateChecker) 新FBACバージョンのお知らせを表示する
---@field public checkUpdate fun(self: UpdateChecker) FBACアップデートの確認を行う
UpdateChecker = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return UpdateChecker
new = function (parent)
---@type UpdateChecker
local instance = Avatar.instantiate(UpdateChecker, AvatarModule, parent)
instance.FBAC_VERSION = "v2.1.2"
instance.BRANCH_NAME = "Hoshino"
instance.latestVersion = instance.parent.config:loadConfig("PUBLIC", "latestVersion", nil)
instance.checkerStatus = "INIT"
instance.requestStatus = 0
instance.textAnimationCount = 0
return instance
end;
---初期化関数
---@param self UpdateChecker
init = function (self)
AvatarModule.init(self)
if host:isHost() then
models.models.action_wheel_gui.Gui.VersionDisplay:getTask("action_wheel.gui.version_display.l2"):setText(self.FBAC_VERSION.." - "..self.BRANCH_NAME)
events.TICK:register(function ()
local isActionWheelOpened = action_wheel:isEnabled()
if isActionWheelOpened then
local textTask = models.models.action_wheel_gui.Gui.VersionDisplay:getTask("action_wheel.gui.version_display.l3")
if self.checkerStatus == "UPDATE_AVAILABLE" then
if math.floor(self.textAnimationCount / 20) % 2 == 0 then
textTask:setText("§6§n"..self.parent.locale:getLocale("action_wheel.gui.update_check.update_available")..self.latestVersion)
else
textTask:setText("§n"..self.parent.locale:getLocale("action_wheel.gui.update_check.update_available")..self.latestVersion)
end
self.textAnimationCount = self.textAnimationCount + 1
elseif self.checkerStatus == "ERROR_REQUEST_FAILED" then
textTask:setText(self.parent.locale:getLocale("action_wheel.gui.update_check.error_request_failed").."("..self.requestStatus..")")
else
textTask:setText(self.parent.locale:getLocale("action_wheel.gui.update_check."..self.checkerStatus:lower()))
end
elseif not isActionWheelOpened and self.isActionWheelOpenedPrev then
self.textAnimationCount = 0
end
self.isActionWheelOpenedPrev = isActionWheelOpened
end)
local lastUpdateCheckTime = self.parent.config:loadConfig("PUBLIC", "lastUpdateCheckTime", 0)
if client:getSystemTime() >= lastUpdateCheckTime + 86400000 then
self:checkUpdate()
else
local newerVersion = self.compareVersions(self.latestVersion, self.FBAC_VERSION)
if newerVersion ~= nil and newerVersion ~= self.FBAC_VERSION then
self:showNewUpdateMessage()
self.checkerStatus = "UPDATE_AVAILABLE"
else
self.checkerStatus = "LATEST"
end
end
end
end;
---2つのバージョン文字列を比較し、新しい方を返す。
---@param version1 string 比較するバージョン文字列1
---@param version2 string 比較するバージョン文字列2
---@return string|nil newerVersion 新しい方のバージョン文字列。比較不可能だった場合はnilを返す。
compareVersions = function (version1, version2)
local major1, minor1, patch1 = version1:match("^v(%d+)%.(%d+)%.(%d+)")
local major2, minor2, patch2 = version2:match("^v(%d+)%.(%d+)%.(%d+)")
major1 = tonumber(major1)
minor1 = tonumber(minor1)
patch1 = tonumber(patch1)
major2 = tonumber(major2)
minor2 = tonumber(minor2)
patch2 = tonumber(patch2)
if major1 ~= nil and minor1 ~= nil and patch1 ~= nil and major2 ~= nil and minor2 ~= nil and patch2 ~= nil then
return (major1 > major2 or (major1 == major2 and minor1 > minor2) or (major1 == major2 and minor1 == minor2 and patch1 > patch2)) and version1 or version2
end
end;
---新FBACバージョンのお知らせを表示する。
---@param self UpdateChecker
showNewUpdateMessage = function (self)
print(self.parent.locale:getLocale("action_wheel.gui.update_check.update_available")..self.latestVersion)
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:entity.experience_orb.pickup"), player:getPos(), 1, 1)
end;
---FBACアップデートの確認を行う。
---@param self UpdateChecker
checkUpdate = function (self)
if host:isHost() and self.checkerStatus ~= "CHECKING" then
self.checkerStatus = "CHECKING"
if net:isNetworkingAllowed() and net:isLinkAllowed("https://api.github.com") then
local request = net.http:request("https://api.github.com/repos/Gakuto1112/FiguraBlueArchiveCharacters/tags")
self.responseHandler = request:send()
events.TICK:register(function ()
if self.responseHandler:isDone() then
local response = self.responseHandler:getValue()
if response ~= nil then
local stats = response:getResponseCode()
if math.floor(stats / 100) == 2 then
local stream = response:getData()
local buffer = data:createBuffer()
buffer:readFromStream(stream)
buffer:setPosition(0)
local jsonData = buffer:readByteArray()
if json.isSerializable(jsonData) then
local parseData = parseJson(jsonData)
if parseData[1] ~= nil and parseData[1].name ~= nil then
local newerVersion = self.compareVersions(parseData[1].name, self.FBAC_VERSION)
if newerVersion ~= nil then
if newerVersion ~= self.FBAC_VERSION then
--新しいバージョンがある
self.latestVersion = parseData[1].name
self.checkerStatus = "UPDATE_AVAILABLE"
self:showNewUpdateMessage()
else
--現在は最新
self.latestVersion = parseData[1].name
self.checkerStatus = "LATEST"
end
self.parent.config:saveConfig("PUBLIC", "lastUpdateCheckTime", client:getSystemTime())
self.parent.config:saveConfig("PUBLIC", "latestVersion", parseData[1].name)
else
--予期しないJSONデータ
self.checkerStatus = "ERROR_INVALID_JSON"
end
else
--予期しないJSONデータ
self.checkerStatus = "ERROR_INVALID_JSON"
end
else
--JSON解析エラー
self.checkerStatus = "ERROR_INVALID_JSON_SYNTAX"
end
stream:close()
buffer:close()
else
--ステータスコードが200番台以外
self.checkerStatus = "ERROR_REQUEST_FAILED"
self.requestStatus = stats
end
else
--ネットワークエラー
self.checkerStatus = "ERROR_NETWORK_ERR"
end
events.TICK:remove("update_checker_http_tick")
end
end, "update_checker_http_tick")
else
---ネットワーキングAPIが不許可
self.checkerStatus = "ERROR_NOT_ALLOWED"
end
end
end;
}

View file

@ -0,0 +1,364 @@
---防具の部位
---@alias Armor.ArmorPart
---| "HELMET" # ヘルメット
---| "CHEST_PLATE" # チェストプレート
---| "LEGGINGS" # レギンス
---| "BOOTS" # ブーツ
---@class (exact) Armor : AvatarModule 防具の表示を制御するクラス
---@field public shouldShowArmor boolean 防具を表示するかどうか
---@field public armorSlotItems ItemStack[] 現ティックの防具スロットのアイテム
---@field package armorSlotItemsPrev ItemStack[] 前ティックの防具スロットのアイテム
---@field public isArmorVisible Armor.VisiblePartsSet 各防具の部位(ヘルメット、チェストプレート、レギンス、ブーツ)が可視状態かどうか
---@field package textureQueue Armor.TextureQueueData[] テクスチャ処理のキュー
---@field package getArmorColor fun(armorItem: ItemStack): number 防具の色を取得する
---@field package compareTrims fun(trim1?: Armor.TrimData, trim2?: Armor.TrimData): boolean 防具装飾が同じものか比較する
---@field package addTextureQueue fun(self: Armor, texture: Texture, paletteName: string) テクスチャの処理のキューにデータを挿入する
---@field package getTrimTexture fun(self: Armor, trimData?: Armor.TrimData, armorId: string): Texture|nil バニラパーツの防具装飾のテクスチャを取得する。テクスチャの処理は次のチック以降行われる。
---@field public setHelmet fun(self: Armor, helmetItem: ItemStack) ヘルメットを更新する
---@field public setChestplate fun(self: Armor, chestplateItem: ItemStack) チェストプレートを更新する
---@field public setLeggings fun(self: Armor, leggingsItem: ItemStack) レギンスを更新する
---@field public setBoots fun(self: Armor, bootsItem: ItemStack) ブーツを更新する
---@class (exact) Armor.VisiblePartsSet 各防具の部位の可視状態のセット
---@field public helmet boolean ヘルメット
---@field public chestplate boolean チェストプレート
---@field public leggings boolean レギンス
---@field public boots boolean ブーツ
---@class (exact) Armor.TextureQueueData テクスチャキューに入るデータの構造体
---@field public texture Texture 処理対象のテクスチャ
---@field public palette Texture 処理に使用するパレットのテクスチャ
---@field public iterationCount integer 現在の繰り返しカウンター
---@class (exact) Armor.TrimData 防具装飾のデータセット
---@field public pattern string 防具装飾の模様
---@field public material string 防具装飾の素材
Armor = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Armor
new = function (parent)
---@type Armor
local instance = Avatar.instantiate(Armor, AvatarModule, parent)
instance.shouldShowArmor = instance.parent.config:loadConfig("PRIVATE", "showArmor", false)
instance.armorSlotItems = {world.newItem(instance.parent.compatibilityUtils:checkItem("minecraft:air")), world.newItem(instance.parent.compatibilityUtils:checkItem("minecraft:air")), world.newItem(instance.parent.compatibilityUtils:checkItem("minecraft:air")), world.newItem(instance.parent.compatibilityUtils:checkItem("minecraft:air"))}
instance.armorSlotItemsPrev = {world.newItem(instance.parent.compatibilityUtils:checkItem("minecraft:air")), world.newItem(instance.parent.compatibilityUtils:checkItem("minecraft:air")), world.newItem(instance.parent.compatibilityUtils:checkItem("minecraft:air")), world.newItem(instance.parent.compatibilityUtils:checkItem("minecraft:air"))}
instance.isArmorVisible = {
helmet = false;
chestplate = false;
leggings = false;
boots = false;
}
instance.textureQueue = {}
return instance
end;
---初期化関数
---@param self Armor
init = function (self)
AvatarModule.init(self)
events.TICK:register(function ()
self.armorSlotItems = self.shouldShowArmor and {player:getItem(6), player:getItem(5), player:getItem(4), player:getItem(3)} or {world.newItem(self.parent.compatibilityUtils:checkItem("minecraft:air")), world.newItem(self.parent.compatibilityUtils:checkItem("minecraft:air")), world.newItem(self.parent.compatibilityUtils:checkItem("minecraft:air")), world.newItem(self.parent.compatibilityUtils:checkItem("minecraft:air"))}
if self.armorSlotItems[1].id ~= self.armorSlotItemsPrev[1].id then
self:setHelmet(self.armorSlotItems[1])
end
if self.armorSlotItems[2].id ~= self.armorSlotItemsPrev[2].id then
self:setChestplate(self.armorSlotItems[2])
end
if self.armorSlotItems[3].id ~= self.armorSlotItemsPrev[3].id then
self:setLeggings(self.armorSlotItems[3])
end
if self.armorSlotItems[4].id ~= self.armorSlotItemsPrev[4].id then
self:setBoots(self.armorSlotItems[4])
end
for index, armorSlotItem in ipairs(self.armorSlotItems) do
local glint = armorSlotItem:hasGlint()
if glint ~= self.armorSlotItemsPrev[index]:hasGlint() then
--エンチャント変更
local renderType = glint and "GLINT" or "NONE"
if index == 2 then
for _, armorPart in ipairs({models.models.main.Avatar.UpperBody.Arms.RightArm.ArmorRA.RightChestplate, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.ArmorRAB.RightChestplateBottom, models.models.main.Avatar.UpperBody.Arms.LeftArm.ArmorLA.LeftChestplate, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.ArmorLAB.LeftChestplateBottom}) do
armorPart:setSecondaryRenderType(renderType)
end
elseif index == 3 then
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightLeggings, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightLeggingsBottom, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftLeggings, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftLeggingsBottom}) do
armorPart:setSecondaryRenderType(renderType)
end
else
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightBoots, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightBootsBottom, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftBoots, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftBootsBottom}) do
armorPart:setSecondaryRenderType(renderType)
end
end
end
local armorColor = self.getArmorColor(armorSlotItem)
if armorColor ~= self.getArmorColor(self.armorSlotItemsPrev[index]) then
--色変更
local colorVector = vectors.intToRGB(armorColor)
if index == 2 then
for _, armorPart in ipairs({models.models.main.Avatar.UpperBody.Arms.RightArm.ArmorRA.RightChestplate.RightChestplate, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.ArmorRAB.RightChestplateBottom.RightChestplateBottom, models.models.main.Avatar.UpperBody.Arms.LeftArm.ArmorLA.LeftChestplate.LeftChestplate, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.ArmorLAB.LeftChestplateBottom.LeftChestplateBottom}) do
armorPart:setColor(colorVector)
end
elseif index == 3 then
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightLeggings.RightLeggings, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightLeggingsBottom.RightLeggingsBottom, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftLeggings.LeftLeggings, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftLeggingsBottom.LeftLeggingsBottom}) do
armorPart:setColor(colorVector)
end
else
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightBoots.RightBoots, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightBootsBottom.RightBootsBottom, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftBoots.LeftBoots, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftBootsBottom.LeftBootsBottom}) do
armorPart:setColor(colorVector)
end
end
end
local trim = self.armorSlotItems[index].tag.Trim
if not self.compareTrims(trim, self.armorSlotItemsPrev[index].tag.Trim) then
--トリム変更
if index == 2 then
local trimTexture = self:getTrimTexture(trim, self.armorSlotItems[2].id)
if trimTexture then
for _, armorPart in ipairs({models.models.main.Avatar.UpperBody.Arms.RightArm.ArmorRA.RightChestplate.RightChestplateTrim, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.ArmorRAB.RightChestplateBottom.RightChestplateBottomTrim, models.models.main.Avatar.UpperBody.Arms.LeftArm.ArmorLA.LeftChestplate.LeftChestplateTrim, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.ArmorLAB.LeftChestplateBottom.LeftChestplateBottomTrim}) do
armorPart:setVisible(true)
armorPart:setPrimaryTexture("CUSTOM", trimTexture)
end
else
for _, armorPart in ipairs({models.models.main.Avatar.UpperBody.Arms.RightArm.ArmorRA.RightChestplate.RightChestplateTrim, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.ArmorRAB.RightChestplateBottom.RightChestplateBottomTrim, models.models.main.Avatar.UpperBody.Arms.LeftArm.ArmorLA.LeftChestplate.LeftChestplateTrim, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.ArmorLAB.LeftChestplateBottom.LeftChestplateBottomTrim}) do
armorPart:setVisible(false)
end
end
elseif index == 3 then
local trimTexture = self:getTrimTexture(trim, self.armorSlotItems[3].id)
if trimTexture then
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightLeggings.RightLeggingsTrim, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightLeggingsBottom.RightLeggingsBottomTrim, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftLeggings.LeftLeggingsTrim, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftLeggingsBottom.LeftLeggingsBottomTrim}) do
armorPart:setVisible(true)
armorPart:setPrimaryTexture("CUSTOM", trimTexture)
end
else
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightLeggings.RightLeggingsTrim, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightLeggingsBottom.RightLeggingsBottomTrim, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftLeggings.LeftLeggingsTrim, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftLeggingsBottom.LeftLeggingsBottomTrim}) do
armorPart:setVisible(false)
end
end
else
local trimTexture = self:getTrimTexture(trim, self.armorSlotItems[4].id)
if trimTexture then
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightBoots.RightBootsTrim, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightBootsBottom.RightBootsBottomTrim, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftBoots.LeftBootsTrim, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftBootsBottom.LeftBootsBottomTrim}) do
armorPart:setVisible(true)
armorPart:setPrimaryTexture("CUSTOM", trimTexture)
end
else
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightBoots.RightBootsTrim, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightBootsBottom.RightBootsBottomTrim, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftBoots.LeftBootsTrim, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftBootsBottom.LeftBootsBottomTrim}) do
armorPart:setVisible(false)
end
end
end
end
end
--テクスチャの作成処理
if #self.textureQueue > 0 then
local instructionsAvailable = avatar:getMaxTickCount() - 3000 --このTICKで使用出来る残りの命令数
while #self.textureQueue > 0 and instructionsAvailable > 0 do
local dimension = self.textureQueue[1].texture:getDimensions()
for y = math.floor(self.textureQueue[1].iterationCount / dimension.x), dimension.y - 1 do
for x = self.textureQueue[1].iterationCount % dimension.x, dimension.x - 1 do
local pixel = self.textureQueue[1].texture:getPixel(x, y)
if pixel.w == 1 then
self.textureQueue[1].texture:setPixel(x, y, self.textureQueue[1].palette:getPixel(7 - math.floor(pixel.x * 8), 0))
end
self.textureQueue[1].iterationCount = self.textureQueue[1].iterationCount + 1
instructionsAvailable = instructionsAvailable - 45
if instructionsAvailable <= 0 then
break
end
end
if instructionsAvailable <= 0 then
break
end
end
self.textureQueue[1].texture:update()
if self.textureQueue[1].iterationCount == dimension.x * dimension.y then
table.remove(self.textureQueue, 1)
end
end
end
self.armorSlotItemsPrev = self.armorSlotItems
end)
for _, vanillaModel in ipairs({vanilla_model.HELMET, vanilla_model.CHESTPLATE, vanilla_model.LEGGINGS}) do
vanillaModel:setVisible(false)
end
for _, overlayPart in ipairs({models.models.main.Avatar.UpperBody.Arms.RightArm.ArmorRA.RightChestplate.RightChestplateOverlay, models.models.main.Avatar.UpperBody.Arms.LeftArm.ArmorLA.LeftChestplate.LeftChestplateOverlay, models.models.main.Avatar.UpperBody.Arms.RightArm.ArmorRA.RightChestplate.RightChestplateOverlay, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.ArmorRAB.RightChestplateBottom.RightChestplateBottomOverlay, models.models.main.Avatar.UpperBody.Arms.LeftArm.ArmorLA.LeftChestplate.LeftChestplateOverlay, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.ArmorLAB.LeftChestplateBottom.LeftChestplateBottomOverlay, models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightBoots.RightBootsOverlay, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightBootsBottom.RightBootsBottomOverlay, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftBoots.LeftBootsOverlay, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftBootsBottom.LeftBootsBottomOverlay}) do
overlayPart:setPrimaryTexture("RESOURCE", "minecraft:textures/models/armor/leather_layer_1_overlay.png")
end
for _, overlayPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightLeggings.RightLeggingsOverlay, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightLeggingsBottom.RightLeggingsBottomOverlay, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftLeggings.LeftLeggingsOverlay, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftLeggingsBottom.LeftLeggingsBottomOverlay}) do
overlayPart:setPrimaryTexture("RESOURCE", "minecraft:textures/models/armor/leather_layer_2_overlay.png")
end
end;
---防具の色を取得する。
---@param armorItem ItemStack 調べるアイテムのオブジェクト
---@return number color 防具モデルに設定すべき色
getArmorColor = function (armorItem)
if armorItem.id:find("^minecraft:leather_") then
if armorItem.tag then
if armorItem.tag.display then
return armorItem.tag.display.color and armorItem.tag.display.color or 10511680
else
return 10511680
end
else
return 10511680
end
else
return 16777215
end
end;
---防具装飾が同じものか比較する。
---@param trim1? Armor.TrimData 比較する防具装飾のテーブル1
---@param trim2? Armor.TrimData 比較する防具装飾のテーブル2
---@return boolean isTrimSame 2つの防具装飾が同じものかどうか
compareTrims = function (trim1, trim2)
if type(trim1) == type(trim2) then
if trim1 then
if trim1.pattern ~= trim2.pattern then
return false
elseif trim1.material ~= trim2.material then
return false
else
return true
end
else
return true
end
else
return false
end
end;
---テクスチャの処理のキューにデータを挿入する。
---@param self Armor
---@param texture Texture 処理を行うテクスチャ
---@param paletteName string 使用するパレットの名前
addTextureQueue = function (self, texture, paletteName)
if textures["trim_palette_"..paletteName] == nil then
textures:fromVanilla("trim_palette_"..paletteName, "minecraft:textures/trims/color_palettes/"..paletteName..".png")
end
table.insert(self.textureQueue, 1, {
texture = texture,
palette = textures["trim_palette_"..paletteName],
iterationCount = 0
})
end;
---バニラパーツの防具装飾のテクスチャを取得する。テクスチャの処理は次のチック以降行われる。
---@param self Armor
---@param trimData? Armor.TrimData 防具装飾のデータ
---@param armorId string 防具アイテムのID。
---@return Texture|nil trimTexture 色を付けた防具装飾のテクスチャ。防具や防具装飾が非バニラの場合はnilを返す。
getTrimTexture = function (self, trimData, armorId)
if trimData and trimData.pattern:find("^minecraft:.+$") and trimData.material:find("^minecraft:.+$") and armorId:find("^minecraft:.+_.+$") then
local normalizedPatternName = trimData.pattern:match("^minecraft:(%a+)$")
local normalizedArmorMaterialName = armorId:match("^minecraft:(%a+)_.+$")
normalizedArmorMaterialName = normalizedArmorMaterialName == "golden" and "gold" or normalizedArmorMaterialName
local normalizedMaterialName = trimData.material:match("^minecraft:(%a+)$")
normalizedMaterialName = normalizedMaterialName..(normalizedArmorMaterialName == normalizedMaterialName and "_darker" or "")
local isLeggings = armorId:find("^minecraft:.+_leggings$")
local textureName = "trim_"..normalizedPatternName.."_"..normalizedMaterialName..(isLeggings and "_leggings" or "")
if textures[textureName] then
return textures[textureName]
else
local texture = textures:fromVanilla(textureName, "minecraft:textures/trims/models/armor/"..normalizedPatternName..(armorId:find("^minecraft:.+_leggings$") ~= nil and "_leggings" or "")..".png")
self:addTextureQueue(texture, normalizedMaterialName)
return texture
end
end
end;
---ヘルメットを更新する。
---@param self Armor
---@param helmetItem ItemStack ヘルメットのスロットに入っているアイテム
setHelmet = function (self, helmetItem)
local helmetFound = helmetItem.id ~= "minecraft:air"
vanilla_model.HELMET:setVisible(helmetFound)
self.isArmorVisible.helmet = helmetFound
if self.parent.characterData.costume.callbacks ~= nil and self.parent.characterData.costume.callbacks.onArmorChange ~= nil then
self.parent.characterData.costume.callbacks.onArmorChange(self.parent.characterData, "HELMET", self.isArmorVisible.helmet)
end
end;
---チェストプレートを更新する。
---@param self Armor
---@param chestplateItem ItemStack チェストプレートスロットに入っているアイテム
setChestplate = function (self, chestplateItem)
local chestplateFound = chestplateItem.id:find("^minecraft:.+_chestplate$") ~= nil
vanilla_model.CHESTPLATE:setVisible(chestplateFound)
for _, armorPart in ipairs({models.models.main.Avatar.UpperBody.Arms.RightArm.ArmorRA, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.ArmorRAB, models.models.main.Avatar.UpperBody.Arms.LeftArm.ArmorLA, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.ArmorLAB}) do
armorPart:setVisible(chestplateFound)
end
self.isArmorVisible.chestplate = chestplateFound
if self.isArmorVisible.chestplate then
local material = chestplateItem.id:match("^minecraft:(%a+)_chestplate$")
for _, armorPart in ipairs({models.models.main.Avatar.UpperBody.Arms.RightArm.ArmorRA.RightChestplate.RightChestplate, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.ArmorRAB.RightChestplateBottom.RightChestplateBottom, models.models.main.Avatar.UpperBody.Arms.LeftArm.ArmorLA.LeftChestplate.LeftChestplate, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.ArmorLAB.LeftChestplateBottom.LeftChestplateBottom}) do
armorPart:setPrimaryTexture("RESOURCE", "minecraft:textures/models/armor/"..(material == "golden" and "gold" or material).."_layer_1.png")
end
end
local overlayVisible = chestplateItem.id == "minecraft:leather_chestplate"
for _, armorPart in ipairs({models.models.main.Avatar.UpperBody.Arms.RightArm.ArmorRA.RightChestplate.RightChestplateOverlay, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.ArmorRAB.RightChestplateBottom.RightChestplateBottomOverlay, models.models.main.Avatar.UpperBody.Arms.LeftArm.ArmorLA.LeftChestplate.LeftChestplateOverlay, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.ArmorLAB.LeftChestplateBottom.LeftChestplateBottomOverlay}) do
armorPart:setVisible(overlayVisible)
end
if self.parent.characterData.costume.callbacks ~= nil and self.parent.characterData.costume.callbacks.onArmorChange ~= nil then
self.parent.characterData.costume.callbacks.onArmorChange(self.parent.characterData, "CHEST_PLATE", self.isArmorVisible.chestplate)
end
end;
---レギンスを更新する。
---@param self Armor
---@param leggingsItem ItemStack レギンススロットに入っているアイテム
setLeggings = function (self, leggingsItem)
local leggingsFound = leggingsItem.id:find("^minecraft:.+_leggings$") ~= nil
vanilla_model.LEGGINGS:setVisible(leggingsFound)
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightLeggings, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightLeggingsBottom, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftLeggings, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftLeggingsBottom}) do
armorPart:setVisible(leggingsFound)
end
self.isArmorVisible.leggings = leggingsFound
if self.isArmorVisible.leggings then
local material = leggingsItem.id:match("^minecraft:(%a+)_leggings$")
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightLeggings.RightLeggings, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightLeggingsBottom.RightLeggingsBottom, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftLeggings.LeftLeggings, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftLeggingsBottom.LeftLeggingsBottom}) do
armorPart:setPrimaryTexture("RESOURCE", "minecraft:textures/models/armor/"..(material == "golden" and "gold" or material).."_layer_2.png")
end
end
local overlayVisible = leggingsItem.id == "minecraft:leather_leggings"
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightLeggings.RightLeggingsOverlay, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightLeggingsBottom.RightLeggingsBottomOverlay, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftLeggings.LeftLeggingsOverlay, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftLeggingsBottom.LeftLeggingsBottomOverlay}) do
armorPart:setVisible(overlayVisible)
end
if self.parent.characterData.costume.callbacks ~= nil and self.parent.characterData.costume.callbacks.onArmorChange ~= nil then
self.parent.characterData.costume.callbacks.onArmorChange(self.parent.characterData, "LEGGINGS", self.isArmorVisible.leggings)
end
end;
---ブーツを更新する。
---@param self Armor
---@param bootsItem ItemStack ブーツスロットに入っているアイテム
setBoots = function (self, bootsItem)
local bootsFound = bootsItem.id:find("^minecraft:.+_boots$") ~= nil
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightBoots, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightBootsBottom, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftBoots, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftBootsBottom}) do
armorPart:setVisible(bootsFound)
end
self.isArmorVisible.boots = bootsFound
if self.isArmorVisible.boots then
local material = bootsItem.id:match("^minecraft:(%a+)_boots$")
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightBoots.RightBoots, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightBootsBottom.RightBootsBottom, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftBoots.LeftBoots, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftBootsBottom.LeftBootsBottom}) do
armorPart:setPrimaryTexture("RESOURCE", "minecraft:textures/models/armor/"..(material == "golden" and "gold" or material).."_layer_1.png")
end
end
local overlayVisible = bootsItem.id == "minecraft:leather_boots"
for _, armorPart in ipairs({models.models.main.Avatar.LowerBody.Legs.RightLeg.ArmorRL.RightBoots.RightBootsOverlay, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB.RightBootsBottom.RightBootsBottomOverlay, models.models.main.Avatar.LowerBody.Legs.LeftLeg.ArmorLL.LeftBoots.LeftBootsOverlay, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB.LeftBootsBottom.LeftBootsBottomOverlay}) do
armorPart:setVisible(overlayVisible)
end
if self.parent.characterData.costume.callbacks ~= nil and self.parent.characterData.costume.callbacks.onArmorChange ~= nil then
self.parent.characterData.costume.callbacks.onArmorChange(self.parent.characterData, "BOOTS", self.isArmorVisible.boots)
end
end;
}

View file

@ -0,0 +1,192 @@
---@class (exact) Arms : AvatarModule アバターの腕を制御するクラス
---@field public armState Arms.ArmStateSet 腕の状態:0. バニラ状態, 1. 銃を構えている際の、銃を構えている方の腕, 2. 銃を構えている際の、銃を構えていない方の腕, 3. クロスボウ装填中
---@field package armStatePrev Arms.ArmStateSet 前ティックの腕の状態
---@field package swingCount integer 腕をプラプラさせるカウンター
---@field package isSwingCountProcessed boolean 腕プラプラカウンターを処理したかどうか
---@field public processArmSwingCount fun(self: Arms) 腕プラプラカウンターを処理する
---@field public setArmState fun(self: Arms, right?: integer, left?: integer) 腕の状態を設定する
---@class (exact) Arms.ArmStateSet 腕の状態を示すデータセット
---@field public right integer 右腕の状態
---@field public left integer 左腕の状態
Arms = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Arms
new = function (parent)
---@type Arms
local instance = Avatar.instantiate(Arms, AvatarModule, parent)
instance.armState = {
right = 0;
left = 0;
}
instance.armStatePrev = {
right = 0;
left = 0;
}
instance.swingCount = 0
instance.isSwingCountProcessed = false
return instance
end;
---初期化関数
---@param self Arms
init = function (self)
AvatarModule.init(self)
events.TICK:register(function ()
self.isSwingCountProcessed = false
end)
end;
---腕プラプラカウンターを処理する。
---@param self Arms
processArmSwingCount = function (self)
if not client:isPaused() and not self.isSwingCountProcessed then
self.swingCount = self.swingCount + 1
self.swingCount = self.swingCount == 100 and 0 or self.swingCount
self.isSwingCountProcessed = true
end
end;
---腕の状態を設定する。
---@param self Arms
---@param right? integer 右腕の状態
---@param left? integer 左腕の状態
setArmState = function (self, right, left)
if right ~= nil then
self.armState.right = right
end
if left ~= nil then
self.armState.left = left
end
if (self.armState.right == 1 or self.armState.left == 1) and player:getActiveItem().id == "minecraft:crossbow" then
self:setArmState(3, 3)
return
end
if self.parent.characterData.arms.callbacks ~= nil and self.parent.characterData.arms.callbacks.onArmStateChanged ~= nil then
local result = self.parent.characterData.arms.callbacks.onArmStateChanged(self.parent.characterData, self.armState.right, self.armState.left)
if result ~= nil then
if result.right ~= nil then
self.armState.right = result.right
end
if result.left ~= nil then
self.armState.left = result.left
end
end
end
--右腕の操作
if self.armState.right ~= self.armStatePrev.right then
--腕の状態をリセット
models.models.main.Avatar.UpperBody.Arms.RightArm:setRot()
models.models.main.Avatar.UpperBody.Arms.RightArm:setParentType("RightArm")
events.TICK:remove("right_arm_tick")
events.RENDER:remove("right_arm_render")
if self.armState.right == 1 then
--銃を構えている際の、銃を構えている方の腕
events.TICK:register(function ()
if self.armState.right == 1 then
self:processArmSwingCount()
if player:isSwingingArm() and not player:isLeftHanded() then
models.models.main.Avatar.UpperBody.Arms.RightArm:setParentType("RightArm")
else
models.models.main.Avatar.UpperBody.Arms.RightArm:setParentType("Body")
end
if player:getActiveItem().id == "minecraft:crossbow" then
self:setArmState(3, 3)
end
end
end, "right_arm_tick")
events.RENDER:register(function (delta)
local headRot = vanilla_model.HEAD:getOriginRot()
models.models.main.Avatar.UpperBody.Arms.RightArm:setRot(player:isSwingingArm() and not player:isLeftHanded() and vectors.vec3() or vectors.vec3(headRot.x + math.sin((self.swingCount + delta) / 100 * math.pi * 2) * 2.5 + 90 + (player:isCrouching() and 30 or 0), headRot.y, 0))
end, "right_arm_render")
elseif self.armState.right == 2 then
--銃を構えている際の、銃を構えていない方の腕
events.TICK:register(function ()
self:processArmSwingCount()
end, "right_arm_tick")
events.RENDER:register(function (delta, context)
local headRot = vanilla_model.HEAD:getOriginRot()
local isSwingingArm = player:isSwingingArm() and not player:isLeftHanded()
models.models.main.Avatar.UpperBody.Arms.RightArm:setParentType((isSwingingArm or context == "FIRST_PERSON") and "RightArm" or "Body")
models.models.main.Avatar.UpperBody.Arms.RightArm:setRot(isSwingingArm and vectors.vec3() or vectors.vec3(headRot.x + math.sin((self.swingCount + delta) / 100 * math.pi * 2) * 2.5 + 90 + (player:isCrouching() and 30 or 0), math.map((headRot.y + 180) % 360 - 180, -50, 50, -21, 78), 0))
end, "right_arm_render")
elseif self.armState.right == 3 then
--クロスボウ装填中
events.TICK:register(function ()
if player:getActiveItem().id ~= "minecraft:crossbow" and self.armState.right == 3 then
if self.parent.gun.currentGunPosition == "RIGHT" then
self:setArmState(1, 2)
elseif self.parent.gun.currentGunPosition == "LEFT" then
self:setArmState(2, 1)
end
end
end, "right_arm_tick")
end
if self.parent.characterData.arms.callbacks ~= nil and self.parent.characterData.arms.callbacks.onAdditionalRightArmProcess ~= nil then
self.parent.characterData.arms.callbacks.onAdditionalRightArmProcess(self.parent.characterData, self.armState.right)
end
self.armStatePrev.right = self.armState.right
end
--左腕の操作
if self.armState.left ~= self.armStatePrev.left then
--腕の状態をリセット
models.models.main.Avatar.UpperBody.Arms.LeftArm:setRot()
models.models.main.Avatar.UpperBody.Arms.LeftArm:setParentType("LeftArm")
events.TICK:remove("left_arm_tick")
events.RENDER:remove("left_arm_render")
if self.armState.left == 1 then
--銃を構えている際の、銃を構えている方の腕
events.TICK:register(function ()
if self.armState.left == 1 then
self:processArmSwingCount()
if player:isSwingingArm() and player:isLeftHanded() then
models.models.main.Avatar.UpperBody.Arms.LeftArm:setParentType("LeftArm")
else
models.models.main.Avatar.UpperBody.Arms.LeftArm:setParentType("Body")
end
if player:getActiveItem().id == "minecraft:crossbow" then
self:setArmState(3, 3)
end
end
end, "left_arm_tick")
events.RENDER:register(function (delta)
local headRot = vanilla_model.HEAD:getOriginRot()
models.models.main.Avatar.UpperBody.Arms.LeftArm:setRot(player:isSwingingArm() and player:isLeftHanded() and vectors.vec3() or vectors.vec3(headRot.x + math.sin((self.swingCount + delta) / 100 * math.pi * 2) * -2.5 + 90 + (player:isCrouching() and 30 or 0), headRot.y, 0))
end, "left_arm_render")
elseif self.armState.left == 2 then
--銃を構えている際の、銃を構えていない方の腕
models.models.main.Avatar.UpperBody.Arms.LeftArm:setParentType("Body")
events.TICK:register(function ()
self:processArmSwingCount()
end, "left_arm_tick")
events.RENDER:register(function (delta, context)
local headRot = vanilla_model.HEAD:getOriginRot()
local isSwingingArm = player:isSwingingArm() and player:isLeftHanded()
models.models.main.Avatar.UpperBody.Arms.LeftArm:setParentType((isSwingingArm or context == "FIRST_PERSON") and "LeftArm" or "Body")
models.models.main.Avatar.UpperBody.Arms.LeftArm:setRot(isSwingingArm and vectors.vec3() or vectors.vec3(headRot.x + math.sin((self.swingCount + delta) / 100 * math.pi * 2) * -2.5 + 90 + (player:isCrouching() and 30 or 0), math.map((headRot.y + 180) % 360 - 180, -50, 50, -78, 21), 0))
end, "left_arm_render")
elseif self.armState.left == 3 then
--クロスボウ装填中
events.TICK:register(function ()
if player:getActiveItem().id ~= "minecraft:crossbow" and self.armState.left == 3 then
if self.parent.gun.currentGunPosition == "RIGHT" then
self:setArmState(1, 2)
elseif self.parent.gun.currentGunPosition == "LEFT" then
self:setArmState(2, 1)
end
end
end, "left_arm_tick")
end
if self.parent.characterData.arms.callbacks ~= nil and self.parent.characterData.arms.callbacks.onAdditionalLeftArmProcess ~= nil then
self.parent.characterData.arms.callbacks.onAdditionalLeftArmProcess(self.parent.characterData, self.armState.left)
end
self.armStatePrev.left = self.armState.left
end
end;
}

View file

@ -0,0 +1,23 @@
---@class AvatarModule アバターの動作を構成するモジュールの抽象クラス
---@field public parent Avatar アバターのメインクラスへの参照
---@field public new fun(parent: Avatar): AvatarModule コンストラクタ
---@field public init fun(self: AvatarModule) 初期化関数
AvatarModule = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return AvatarModule
new = function (parent)
---@type AvatarModule
local instance = Avatar.instantiate(AvatarModule)
instance.parent = parent
return instance
end;
---初期化関数
---@param self AvatarModule
init = function (self)
end;
}

View file

@ -0,0 +1,88 @@
---@class (exact) Barrier : AvatarModule バリアの視覚効果を管理するクラス
---@field package animationCounts number[] バリアのアニメーションのカウンター
---@field public isBarrierVisible boolean バリアが可視化状態かどうか
---@field package hadAbsorptionPrev boolean 前ティックの衝撃吸収のハートを持っていたかどうか
---@field package colorFactor number バリアの色の係数
---@field public enable fun(self: Barrier) バリア機能を有効にする
---@field public disable fun(self: Barrier) バリア機能を無効にする
Barrier = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Barrier
new = function (parent)
---@type Barrier
local instance = Avatar.instantiate(Barrier, AvatarModule, parent)
instance.animationCounts = {}
instance.isBarrierVisible = false
instance.hadAbsorptionPrev = false
instance.colorFactor = client:hasShaderPack() and 0.5 or 1
for i = 1, 32 do
instance.animationCounts[i] = math.random(0, 39)
end
return instance
end;
---初期化関数
---@param self Barrier
init = function (self)
AvatarModule.init(self)
models.models.main.Avatar.barrier:setLight(15)
events.TICK:register(function ()
local hasAbsorption = player:getAbsorptionAmount() > 0 and player:getHealth() > 0
if hasAbsorption and not self.hadAbsorptionPrev then
self:enable()
elseif not hasAbsorption and self.hadAbsorptionPrev then
self:disable()
end
self.hadAbsorptionPrev = hasAbsorption
end)
end;
---バリア機能を有効にする。
---@param self Barrier
enable = function (self)
for i = 1, 32 do
self.animationCounts[i] = math.random(0, 39)
end
events.TICK:register(function ()
for i = 1, 32 do
self.animationCounts[i] = self.animationCounts[i] + 1
if self.animationCounts[i] == 40 then
self.animationCounts[i] = 0
end
end
self.colorFactor = client:hasShaderPack() and 0.5 or 1
end, "barrier_tick")
events.RENDER:register(function (delta, context)
if context == "FIRST_PERSON" or context == "RENDER" then
models.models.main.Avatar.barrier:setVisible(true)
for i = 1, 32 do
local opacity = math.abs(-0.025 * (self.animationCounts[i] + delta) + 0.5) + 0.5
models.models.main.Avatar.barrier.Barrier["Barrier"..i]:setOpacity(opacity)
models.models.main.Avatar.barrier.Barrier["Barrier"..i]:setColor(opacity * self.colorFactor, opacity * self.colorFactor, 1)
end
else
models.models.main.Avatar.barrier:setVisible(false)
end
end, "barrier_render")
self.isBarrierVisible = true
end;
---バリア機能を無効にする。
---@param self Barrier
disable = function (self)
models.models.main.Avatar.barrier:setVisible(false)
events.TICK:remove("barrier_tick")
events.RENDER:remove("barrier_render")
self.isBarrierVisible = false
end;
}

View file

@ -0,0 +1,224 @@
---@alias Bubble.BubbleType
---| "GOOD" # 👍
---| "HEART" # 💗
---| "NOTE" # 🎵
---| "QUESTION" # ❓
---| "SWEAT" # 💦
---| "RELOAD" # 弾薬をリロードする絵文字
---| "DOTS" # …
---| "V" # ✌
---@class (exact) Bubble : AvatarModule 吹き出しエモートを管理するクラス
---@field public bubbleCount integer 吹き出しの表示時間を測るカウンター
---@field public emoji Bubble.BubbleType 吹き出しの絵文字
---@field package duration integer 吹き出しを表示する時間。-1は時間無制限を示す。
---@field package isAutoBubble boolean 吹き出しエモートが自動で出たものかどうか
---@field package shouldShowInHud boolean 一人称用にHUDに吹き出しを表示するかどうか
---@field package emojiAnimationCount number 絵文字のアニメーションのタイミングを測るカウンター
---@field package isForcedStop boolean 吹き出しエモートが強制停止させられたかどうか
---@field public isChatOpened boolean チャットを開けているかどうか
---@field package isChatOpenedPrev boolean 前ティックにチャットを開けていたかどうか
---@field public play fun(self: Bubble, type: Bubble.BubbleType, duration: integer, offsetPos: Vector2, offsetRot: number, shouldShowInHud: boolean) 吹き出しエモートを再生する
---@field public stop fun(self: Bubble) 吹き出しエモートを停止する
Bubble = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Bubble
new = function (parent)
---@type Bubble
local instance = Avatar.instantiate(Bubble, AvatarModule, parent)
instance.bubbleCount = 0
instance.emoji = "GOOD"
instance.duration = 0
instance.isAutoBubble = false
instance.shouldShowInHud = false
instance.emojiAnimationCount = 0
instance.isForcedStop = false
instance.isChatOpened = false
instance.isChatOpenedPrev = false
return instance
end;
---初期化関数
---@param self Bubble
init = function (self)
AvatarModule.init(self)
models.models.bubble:addChild(models:newPart("Gui", "Gui"))
models.models.bubble.Gui:addChild(models.models.bubble.Camera.AvatarBubble:copy("FirstPersonBubble"))
models.models.bubble.Gui.FirstPersonBubble:setVisible(false)
for _, modelPart in ipairs({models.models.bubble.Camera.AvatarBubble, models.models.bubble.Gui.FirstPersonBubble}) do
modelPart:setScale(0, 0, 0)
end
--エモートガイド
if host:isHost() then
self.parent.keyManager:register("bubble_1", self.parent.config:loadConfig("PRIVATE", "keybind.bubble_1", "key.keyboard.j")):onPress(function ()
if self.parent.exSkill.animationCount == -1 and (self.bubbleCount == 0 or self.isAutoBubble) then
pings.showBubbleEmote("GOOD")
end
end)
self.parent.keyManager:register("bubble_2", self.parent.config:loadConfig("PRIVATE", "keybind.bubble_2", "key.keyboard.k")):onPress(function ()
if self.parent.exSkill.animationCount == -1 and (self.bubbleCount == 0 or self.isAutoBubble) then
pings.showBubbleEmote("HEART")
end
end)
self.parent.keyManager:register("bubble_3", self.parent.config:loadConfig("PRIVATE", "keybind.bubble_3", "key.keyboard.n")):onPress(function ()
if self.parent.exSkill.animationCount == -1 and (self.bubbleCount == 0 or self.isAutoBubble) then
pings.showBubbleEmote("NOTE")
end
end)
self.parent.keyManager:register("bubble_4", self.parent.config:loadConfig("PRIVATE", "keybind.bubble_4", "key.keyboard.m")):onPress(function ()
if self.parent.exSkill.animationCount == -1 and (self.bubbleCount == 0 or self.isAutoBubble) then
pings.showBubbleEmote("QUESTION")
end
end)
self.parent.keyManager:register("bubble_5", self.parent.config:loadConfig("PRIVATE", "keybind.bubble_5", "key.keyboard.comma")):onPress(function ()
if self.parent.exSkill.animationCount == -1 and (self.bubbleCount == 0 or self.isAutoBubble) then
pings.showBubbleEmote("SWEAT")
end
end)
end
events.TICK:register(function ()
if host:isHost() then
local isChatOpened = host:isChatOpen()
if isChatOpened ~= self.isChatOpened then
pings.setChatOpen(isChatOpened)
end
self.isChatOpenedPrev = isChatOpened
end
if player:getActiveItem().id == "minecraft:crossbow" then
if self.bubbleCount == 0 or (self.isAutoBubble and self.emoji ~= "RELOAD") then
self:play("RELOAD", -1, vectors.vec2(), 0, false)
self.isAutoBubble = true
end
elseif self.isChatOpened and self.parent.exSkill.transitionCount == 0 then
if self.bubbleCount == 0 or (self.isAutoBubble and self.emoji ~= "DOTS") then
self:play("DOTS", -1, vectors.vec2(), 0, false)
self.isAutoBubble = true
end
elseif self.isAutoBubble then
self:stop()
self.isAutoBubble = false
end
end)
end;
---吹き出しエモートを再生する。
---@param self Bubble
---@param type Bubble.BubbleType 再生する絵文字の種類
---@param duration integer 吹き出しを表示している時間。-1にすると停止するまでずっと表示する。
---@param offsetPos Vector2 吹き出しの位置のオフセット値
---@param offsetRot number アバター周回上の、吹き出しが表示される位置のオフセット値
---@param shouldShowInHud boolean 一人称用にHUDに吹き出しを表示するかどうか
play = function (self, type, duration, offsetPos, offsetRot, shouldShowInHud)
self.emoji = type
self.duration = duration
self.shouldShowInHud = shouldShowInHud
self.bubbleCount = 1
self.emojiAnimationCount = 0
models.models.bubble.Camera.AvatarBubble.Emoji:setPrimaryTexture("CUSTOM", textures["textures.emojis."..self.emoji:lower()])
models.models.bubble.Camera.AvatarBubble.Bullets:setVisible(self.emoji == "RELOAD")
models.models.bubble.Camera.AvatarBubble.Dots:setVisible(self.emoji == "DOTS")
if self.shouldShowInHud then
models.models.bubble.Gui.FirstPersonBubble.Emoji:setPrimaryTexture("CUSTOM", textures["textures.emojis."..self.emoji:lower()])
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:entity.item.pickup"), self.parent.modelUtils.getModelWorldPos(models.models.main.Avatar))
end
if events.TICK:getRegisteredCount("bubble_tick") == 0 then
events.TICK:register(function ()
if not client:isPaused() then
models.models.bubble.Gui.FirstPersonBubble:setVisible(self.shouldShowInHud and renderer:isFirstPerson())
self.bubbleCount = self.bubbleCount + 1
if self.bubbleCount == 0 then
for _, modelPart in ipairs({models.models.bubble.Camera.AvatarBubble, models.models.bubble.Gui.FirstPersonBubble, models.models.bubble.Camera.AvatarBubble.Bullets, models.models.bubble.Gui.FirstPersonBubble.Bullets}) do
modelPart:setVisible(false)
end
events.TICK:remove("bubble_tick")
events.RENDER:remove("bubble_render")
if self.parent.characterData.bubble.callbacks ~= nil and self.parent.characterData.bubble.callbacks.onStop ~= nil then
self.parent.characterData.bubble.callbacks.onStop(self.parent.characterData, type, self.isForcedStop)
end
elseif self.duration >= 0 and self.bubbleCount == self.duration + 2 then
self:stop()
end
if self.emoji == "RELOAD" or self.emoji == "DOTS" then
self.emojiAnimationCount = self.emojiAnimationCount + 1
self.emojiAnimationCount = self.emojiAnimationCount == 25 and 0 or self.emojiAnimationCount
if self.emoji == "DOTS" then
for i = 1, 3 do
models.models.bubble.Camera.AvatarBubble.Dots["Dot"..i]:setVisible(self.emojiAnimationCount >= 6 * i)
end
end
end
end
end, "bubble_tick")
end
if events.RENDER:getRegisteredCount("bubble_render") == 0 then
events.RENDER:register(function (delta, context)
models.models.bubble.Camera.AvatarBubble:setVisible(context ~= "OTHER")
if not client:isPaused() then
local bubbleScale = math.min(math.abs(0.5 * (self.bubbleCount + delta)), 1)
models.models.bubble.Camera.AvatarBubble:setScale(vectors.vec3(1, 1, 1):scale(bubbleScale))
local playerPos = self.parent.modelUtils.getModelWorldPos(models.models.main.Avatar)
local avatarBubblePos = context == "PAPERDOLL" and vectors.vec3(0, 32, 0) or vectors.rotateAroundAxis(player:getBodyYaw(delta) + 180, playerPos:copy():sub(player:getPos(delta)):scale(17.067):add(0, 32 + offsetPos.y, 0), 0, 1, 0)
if not renderer:isFirstPerson() then
local cameraPos = client:getCameraPos()
avatarBubblePos:add(vectors.rotateAroundAxis(math.deg(math.atan2(cameraPos.z - playerPos.z, cameraPos.x - playerPos.x) - math.pi / 2) % 360 - (player:getBodyYaw(delta) + offsetRot) % 360, 12 + offsetPos.x, 0, 0, 0, -1, 0))
else
avatarBubblePos:add(12 + offsetPos.x, 0, 0)
end
models.models.bubble.Camera:setOffsetPivot(avatarBubblePos)
models.models.bubble.Camera.AvatarBubble:setPos(avatarBubblePos)
if host:isHost() and self.shouldShowInHud then
local windowSize = client:getScaledWindowSize()
models.models.bubble.Gui.FirstPersonBubble:setPos(-windowSize.x + 10, -windowSize.y + (action_wheel:isEnabled() and 125 or 10), 0)
models.models.bubble.Gui.FirstPersonBubble:setScale(vectors.vec3(1, 1, 1):scale(bubbleScale * 4))
end
if self.emoji == "RELOAD" then
local bullet1Counter = math.clamp((self.emojiAnimationCount + delta) * 0.2 - 1, 0, 1)
models.models.bubble.Camera.AvatarBubble.Bullets.Bullet1:setPos(0, 1 - bullet1Counter, 0)
models.models.bubble.Camera.AvatarBubble.Bullets.Bullet1:setOpacity(bullet1Counter)
local bullet2Counter = math.clamp((self.emojiAnimationCount + delta) * 0.2 - 2, 0, 1)
models.models.bubble.Camera.AvatarBubble.Bullets.Bullet2:setPos(0, 1 - bullet2Counter, 0)
models.models.bubble.Camera.AvatarBubble.Bullets.Bullet2:setOpacity(bullet2Counter)
local bullet3Counter = math.clamp((self.emojiAnimationCount + delta) * 0.2 - 3, 0, 1)
models.models.bubble.Camera.AvatarBubble.Bullets.Bullet3:setPos(0, 1 - bullet3Counter, 0)
models.models.bubble.Camera.AvatarBubble.Bullets.Bullet3:setOpacity(bullet3Counter)
end
end
end, "bubble_render")
end
if self.parent.characterData.bubble.callbacks ~= nil and self.parent.characterData.bubble.callbacks.onPlay ~= nil then
self.parent.characterData.bubble.callbacks.onPlay(self.parent.characterData, type, duration, shouldShowInHud)
end
end;
---吹き出しエモートを停止する。
---@param self Bubble
stop = function (self)
if self.bubbleCount > 0 then
self.isForcedStop = self.duration == -1 or self.bubbleCount < self.duration + 2
self.bubbleCount = -2
end
end;
}
---吹き出しエモートを表示する。
---@param type Bubble.BubbleType 表示する絵文字の種類
function pings.showBubbleEmote(type)
AvatarInstance.bubble:play(type, 50, vectors.vec2(), 0, true)
AvatarInstance.bubble.isAutoBubble = false
end
---Bubbleのチャットを開けているフラグを更新する。
---@param value boolean 新しい値
function pings.setChatOpen(value)
AvatarInstance.bubble.isChatOpened = value
end

View file

@ -0,0 +1,99 @@
---@class (exact) CameraManager : AvatarModule カメラ制御全般を管理するクラス
---@field package COLLISION_DENIAL_DISABLED boolean カメラの当たり判定打ち消し機能を無効にする。撮影用。
---@field package thirdPersonCameraDistance number 三人称視点でのカメラと回転軸の距離
---@field package isCameraCollisionDenialEnabled boolean 三人称視点でのカメラの当たり判定打ち消し機能が有効かどうか
---@field public setCameraManagerRender fun(self: CameraManager, enabled: boolean) CameraManagerのレンダー関数を設定する
---@field public setCameraPivot fun(newPivot?: Vector3) カメラの回転軸のオフセット位置を変更する
---@field public setCameraRot fun(newRot?: Vector3) カメラ方向を変更する
---@field public setThirdPersonCameraDistance fun(self: CameraManager, distance: number) 三人称視点でのカメラと回転軸の距離を設定する
---@field public setCameraCollisionDenial fun(self: CameraManager, enabled: boolean) カメラの当たり判定打ち消し機能を設定する
CameraManager = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return CameraManager
new = function (parent)
---@type CameraManager
local instance = Avatar.instantiate(CameraManager, AvatarModule, parent)
instance.COLLISION_DENIAL_DISABLED = false
instance.thirdPersonCameraDistance = 4
instance.isCameraCollisionDenialEnabled = false
return instance
end;
---CameraManagerのレンダー関数を設定する。
---@param self CameraManager
---@param enabled boolean CameraManagerのレンダー関数を有効化するかどうか
setCameraManagerRender = function (self, enabled)
if enabled and events.RENDER:getRegisteredCount("camera_manager_render") == 0 then
events.RENDER:register(function ()
if renderer:isFirstPerson() then
renderer:setCameraPos()
else
local rawOffsetCameraPivot = renderer:getCameraOffsetPivot()
rawOffsetCameraPivot = rawOffsetCameraPivot == nil and vectors.vec3() or rawOffsetCameraPivot
local cameraPivot = player:getPos():add(0, 1.62, 0):add(rawOffsetCameraPivot)
local cameraDir = client:getCameraDir()
local baseVector = vectors.rotateAroundAxis(math.deg(math.asin(cameraDir.y)), 0, 0.21, 0, vectors.rotateAroundAxis(math.deg(math.atan2(cameraDir.z, cameraDir.x)) * -1 - 90, 1, 0, 0, 0, 1, 0))
local minDistance = math.max(self.thirdPersonCameraDistance, 4)
if not self.COLLISION_DENIAL_DISABLED then
for i = 0, 3 do
local startPos = vectors.rotateAroundAxis(i * 90 + 45, baseVector:copy(), cameraDir):add(cameraPivot)
local _, collisionPos, _ = raycast:block(startPos, startPos:copy():add(cameraDir:copy():scale(-4)), "VISUAL", "NONE")
minDistance = math.min(collisionPos:copy():sub(startPos):length(), minDistance)
end
end
renderer:setCameraPos(0, 0, (minDistance > self.thirdPersonCameraDistance or self.isCameraCollisionDenialEnabled) and self.thirdPersonCameraDistance - minDistance or 0)
end
end, "camera_manager_render")
elseif not enabled then
events.RENDER:remove("camera_manager_render")
renderer:setCameraPos()
end
end;
---カメラの回転軸のオフセット位置を変更する。
---@param newPivot? Vector3 設定する新しいカメラ回転軸のオフセット位置。nilの場合は設定値がリセットされる。
setCameraPivot = function (newPivot)
if host:isHost() then
renderer:setOffsetCameraPivot(newPivot)
end
end;
---カメラ方向を変更する。
---@param newRot? Vector3 設定する新しいカメラのオフセット方向。nilの場合は設定値がリセットされる。
setCameraRot = function (newRot)
if host:isHost() then
renderer:setCameraRot(newRot)
end
end;
---三人称視点でのカメラと回転軸の距離を設定する。
---@param self CameraManager
---@param distance number 設定する新しい距離(ブロック単位)。デフォルトは4ブロック。
setThirdPersonCameraDistance = function (self, distance)
if host:isHost() then
if distance ~= 4 then
self:setCameraManagerRender(true)
elseif not self.isCameraCollisionDenialEnabled then
self:setCameraManagerRender(false)
end
self.thirdPersonCameraDistance = distance
end
end;
---カメラの当たり判定打ち消し機能を設定する。
---@param enabled boolean カメラの当たり判定打ち消し機能を有効にするかどうか。有効にするとカメラがブロックの中にめり込むようになる。
setCameraCollisionDenial = function (self, enabled)
if host:isHost() then
if enabled then
self:setCameraManagerRender(true)
elseif self.thirdPersonCameraDistance == 4 then
self:setCameraManagerRender(false)
end
self.isCameraCollisionDenialEnabled = enabled
end
end;
}

View file

@ -0,0 +1,116 @@
---@alias Config.Storage
---| "PUBLIC" # FBACキャラクター共通ストレージ
---| "PRIVATE" # キャラクター固有ストレージ
---@class (exact) Config : AvatarModule アバター設定を管理するクラス
---@field package privateStorageName string キャラクター固有ストレージの名前
---@field package defaultValues {[string]: number|boolean|string} 読み込んだ値のデフォルト値を保持するテーブル
---@field package nextSyncCount integer 次の同期pingまでのカウンター
---@field package isSynced boolean 設定値がホストと同期されているかどうか
---@field public loadConfig fun(self: Config, storage: Config.Storage, keyName: string, defaultValue: any): any 設定を読み出す
---@field public saveConfig fun(self: Config, storage: Config.Storage, keyName: string, valueToSave: any) 設定を保存する
Config = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Config
new = function (parent)
---@type Config
local instance = Avatar.instantiate(Config, AvatarModule, parent)
instance.privateStorageName = "BlueArchive_"..instance.parent.characterData.basic.firstName.en_us..instance.parent.characterData.basic.lastName.en_us
instance.defaultValues = {}
instance.nextSyncCount = 0
instance.isSynced = host:isHost()
return instance
end;
---初期化関数
---@param self Config
init = function (self)
AvatarModule.init(self)
if host:isHost() then
events.TICK:register(function ()
if self.nextSyncCount == 0 then
pings.syncAvatarConfig(self.parent.nameplate.currentName, self.parent.nameplate.shouldShowClubName, self.parent.costume.currentCostume, self.parent.armor.shouldShowArmor, self.parent.actionWheel.shouldReplaceVehicleModels, self.parent.bubble.isChatOpened, self.parent.characterData.dataSync.syncData)
self.nextSyncCount = 300
else
self.nextSyncCount = self.nextSyncCount - 1
end
end)
end
end;
---設定を読み出す。
---@generic T
---@param self Config
---@param storage Config.Storage 読み出し先のストレージ
---@param keyName string 読み出す設定の名前
---@param defaultValue `T` 該当の設定が無い場合や、ホスト外での実行の場合はこの値が返される。
---@return `T` loadedValue 読み出した値
loadConfig = function (self, storage, keyName, defaultValue)
if host:isHost() then
if storage == "PUBLIC" then
config:setName("BlueArchive_public")
else
config:setName(self.privateStorageName)
end
local loadedData = config:load(keyName)
self.defaultValues[keyName] = defaultValue
if loadedData ~= nil then
return loadedData
else
return defaultValue
end
else
return defaultValue
end
end;
---設定を保存する。
---@param self Config
---@param storage Config.Storage 書き込み先のストレージ
---@param keyName string 保存する設定の名前
---@param valueToSave any 保存する値
saveConfig = function (self, storage, keyName, valueToSave)
if host:isHost() then
if storage == "PUBLIC" then
config:setName("BlueArchive_public")
else
config:setName(self.privateStorageName)
end
if self.defaultValues[keyName] == valueToSave then
config:save(keyName, nil)
else
config:save(keyName, valueToSave)
end
end
end;
}
---アバター設定を他Figuraクライアントと同期する。
---@param nameTypeId integer 表示名の種類ID
---@param shouldShowClubName boolean 部活名を表示するかどうか
---@param costumeId integer 現在の衣装ID
---@param shouldShowArmor boolean 防具が見えているかどうか
---@param shouldReplaceVehicleModels boolean 乗り物モデルを置き換えるかどうか
---@param isChatOpened boolean チャット欄を開いているかどうか
---@param additionalData {[string]: any} キャラクター固有用に追加で同期するデータ
function pings.syncAvatarConfig(nameTypeId, shouldShowClubName, costumeId, shouldShowArmor, shouldReplaceVehicleModels, isChatOpened, additionalData)
if not AvatarInstance.config.isSynced then
AvatarInstance.nameplate:setName(nameTypeId, shouldShowClubName)
AvatarInstance.armor.shouldShowArmor = shouldShowArmor
AvatarInstance.actionWheel.shouldReplaceVehicleModels = shouldReplaceVehicleModels
AvatarInstance.bubble.isChatOpened = isChatOpened
if costumeId >= 2 then
AvatarInstance.costume:setCostume(costumeId)
end
AvatarInstance.characterData.dataSync.syncData = additionalData
if AvatarInstance.characterData.dataSync.callbacks ~= nil and AvatarInstance.characterData.dataSync.callbacks.onDataSynced ~= nil then
AvatarInstance.characterData.dataSync.callbacks.onDataSynced(AvatarInstance.characterData)
end
AvatarInstance.config.isSynced = true
end
end

View file

@ -0,0 +1,113 @@
---@class (exact) Costume : AvatarModule キャラクターのコスチュームを管理し、円滑に切り替えられるようにするクラス
---@field public costumeList string[] 利用可能なコスチューム一覧。BlueArchiveCharacterクラスから動的に生成される。
---@field public currentCostume integer 現在のコスチューム
---@field public isChangingCostume boolean コスチュームを変更中かどうか
---@field public getCostumeLocalName fun(self: Costume, costumeId: integer) 設定言語を考慮した、衣装の名前を返す
---@field public setCostumeTextureOffset fun(offset: integer) メインモデルのテクスチャのオフセット値を設定する
---@field public setCostume fun(self: Costume, costume: integer) コスチュームを設定する
---@field public resetCostume fun(self: Costume) コスチュームをリセットしデフォルトのコスチュームにする
Costume = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Costume
new = function (parent)
---@type Costume
local instance = Avatar.instantiate(Costume, AvatarModule, parent)
instance.costumeList = {}
instance.currentCostume = instance.parent.config:loadConfig("PRIVATE", "costume", 1)
instance.isChangingCostume = false
return instance
end;
---初期化関数
---@param self Costume
init = function (self)
AvatarModule.init(self)
for _, costume in ipairs(self.parent.characterData.costume.costumes) do
table.insert(self.costumeList, costume.name)
end
if self.currentCostume >= 2 then
if self.costumeList[self.currentCostume] ~= nil then
self:setCostume(self.currentCostume)
else
self.currentCostume = 1
if host:isHost() then
self.parent.config:saveConfig("PRIVATE", "costume", 1)
end
end
end
self.parent.headBlock:generateHeadModel()
self.parent.portrait:generateHeadModel()
events.TICK:register(function ()
self.isChangingCostume = false
end)
end;
---設定言語を考慮した、衣装の名前を返す。
---@param self Costume
---@param costumeId integer ローカル名を取得する衣装のID
---@return string localCostumeName 衣装のローカル名
getCostumeLocalName = function(self, costumeId)
return self.parent.locale:getLocale("costume."..self.costumeList[costumeId])
end;
---メインモデルのテクスチャのオフセット値を設定する。
---@param offset integer オフセット値
setCostumeTextureOffset = function (offset)
for _, modelPart in ipairs({models.models.main.Avatar.UpperBody.Body.Body, models.models.main.Avatar.UpperBody.Body.BodyLayer, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArm, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmLayer, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.RightArmBottom, models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.RightArmBottomLayer, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArm, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmLayer, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.LeftArmBottom, models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.LeftArmBottomLayer, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLeg, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegLayer, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.RightLegBottom, models.models.main.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.RightLegBottomLayer, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLeg, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegLayer, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.LeftLegBottom, models.models.main.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.LeftLegBottomLayer}) do
modelPart:setUVPixels(0, offset * 48)
end
end;
---コスチュームを設定する。
---@param self Costume
---@param costume integer 設定するコスチューム
setCostume = function(self, costume)
self:resetCostume()
if self.parent.characterData.costume.callbacks ~= nil and self.parent.characterData.costume.callbacks.onChange ~= nil then
self.parent.characterData.costume.callbacks.onChange(self.parent.characterData, self.parent.characterData.costume.costumes[costume].name:upper())
end
self.parent.headBlock:generateHeadModel()
self.parent.portrait:generateHeadModel()
if self.parent.armor.isArmorVisible.chestplate then
self.parent.armor:setChestplate(self.parent.armor.armorSlotItems[2])
end
if self.parent.armor.isArmorVisible.leggings then
self.parent.armor:setLeggings(self.parent.armor.armorSlotItems[3])
end
if self.parent.armor.isArmorVisible.boots then
self.parent.armor:setBoots(self.parent.armor.armorSlotItems[4])
end
self.currentCostume = costume
end;
---コスチュームをリセットし、デフォルトのコスチュームにする。
---@param self Costume
resetCostume = function (self)
self.isChangingCostume = true
if self.parent.exSkill.transitionCount > 0 then
self.parent.exSkill:forceStop()
end
self.setCostumeTextureOffset(0)
if self.parent.characterData.costume.callbacks ~= nil and self.parent.characterData.costume.callbacks.onReset ~= nil then
self.parent.characterData.costume.callbacks.onReset(self.parent.characterData)
end
self.parent.headBlock:generateHeadModel()
self.parent.portrait:generateHeadModel()
if self.parent.armor.isArmorVisible.chestplate then
self.parent.armor:setChestplate(self.parent.armor.armorSlotItems[2])
end
if self.parent.armor.isArmorVisible.leggings then
self.parent.armor:setLeggings(self.parent.armor.armorSlotItems[3])
end
if self.parent.armor.isArmorVisible.boots then
self.parent.armor:setBoots(self.parent.armor.armorSlotItems[4])
end
self.currentCostume = 1
end;
}

View file

@ -0,0 +1,294 @@
---@class (exact) DeathAnimation : AvatarModule プレイヤーが死亡した際のキャラクターがヘリコプターで回収されるアニメーションを管理するクラス
---@field package DEBUG_MODE boolean デバッグモードを有効にするかどうか。デバッグモードモードではxキーでフェーズ1のモデルを、cキーでフェーズ2のモデルを表示できる。
---@field public dummyAvatarRoot? ModelPart 死亡アニメーションに使用されるダミーのアバターのルート。アバターが未生成の場合はnilが入っている。
---@field package animationCount integer 死亡アニメーションの再生カウンター
---@field package animationPos Vector3 アニメーションを再生している場所の座標
---@field package animationRot number アニメーションを再生している向き(度数法で示す)
---@field package costumeIndex integer 死亡アニメーションのコスチュームのインデックス
---@field package isPlayerInvisible boolean プレイヤーモデルが不可視状態かどうか
---@field package isScriptLoaded false スクリプトを全て読み込んだかどうか
---@field package removeUnsafeModel fun(target?: ModelPart) 存在しないかもしれないモデルパーツを安全に削除する
---@field package spawnHelicopterParticles fun(self: DeathAnimation) ヘリコプターの出現/消滅パーティクルを生成する
---@field package generateDummyAvatar fun(self: DeathAnimation, parent: ModelPart) 死亡アニメーション用のダミーアバターを生成する
---@field package resetDummyAvatar fun(avatarRoot: ModelPart) ダミーアバター状態をリセットする
---@field package setPhase1Pose fun(avatarRoot: ModelPart) ダミーアバターをフェーズ1のポーズにする
---@field package setPhase2Pose fun(avatarRoot: ModelPart) ダミーアバターをフェーズ2のポーズにする
---@field package play fun(self: DeathAnimation) 死亡アニメーションを再生する
---@field package stop fun(self: DeathAnimation) 死亡アニメーションを停止する
DeathAnimation = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return DeathAnimation
new = function (parent)
---@type DeathAnimation
local instance = Avatar.instantiate(DeathAnimation, AvatarModule, parent)
instance.DEBUG_MODE = false
instance.dummyAvatarRoot = models.models.death_animation.Avatar
instance.animationCount = 0
instance.animationPos = vectors.vec3()
instance.animationRot = 0
instance.costumeIndex = 1
instance.isPlayerInvisible = false
instance.isScriptLoaded = false
return instance
end;
---初期化関数
---@param self DeathAnimation
init = function (self)
AvatarModule.init(self)
events.TICK:register(function ()
if self.parent.playerUtils.damageStatus == "DIED" then
self:play()
models.models.main:setVisible(false)
for _, vanillaModel in ipairs({vanilla_model.RIGHT_ITEM, vanilla_model.LEFT_ITEM, vanilla_model.ELYTRA}) do
vanillaModel:setVisible(false)
end
self.isPlayerInvisible = true
end
if self.isPlayerInvisible and player:getHealth() > 0 then
models.models.main:setVisible(true)
for _, vanillaModel in ipairs({vanilla_model.RIGHT_ITEM, vanilla_model.LEFT_ITEM, vanilla_model.ELYTRA}) do
vanillaModel:setVisible(true)
end
self.isPlayerInvisible = false
end
end)
self.parent.avatarEvents.SCRIPT_INIT:register(function ()
self.isScriptLoaded = true
end)
if self.DEBUG_MODE then
models:addChild(models:newPart("script_death_animation_debug", "World"))
keybinds:newKeybind("[DEBUG] Spawn death animation phase1 model", "key.keyboard.x"):onPress(function ()
self.removeUnsafeModel(models.script_death_animation_debug.Avatar)
self:generateDummyAvatar(models.script_death_animation_debug)
self.resetDummyAvatar(models.script_death_animation_debug.Avatar)
self.setPhase1Pose(models.script_death_animation_debug.Avatar)
models.script_death_animation_debug.Avatar:setPos(player:getPos():add(0, -0.75, 0):scale(16))
if self.parent.characterData.deathAnimation.callbacks ~= nil and self.parent.characterData.deathAnimation.callbacks.onPhase1 ~= nil then
self.parent.characterData.deathAnimation.callbacks.onPhase1(self.parent.characterData, models.script_death_animation_debug.Avatar, self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].name:upper())
end
end)
keybinds:newKeybind("[DEBUG] Spawn death animation phase2 model", "key.keyboard.c"):onPress(function ()
self.removeUnsafeModel(models.script_death_animation_debug.Avatar)
self:generateDummyAvatar(models.script_death_animation_debug)
self.resetDummyAvatar(models.script_death_animation_debug.Avatar)
self.setPhase2Pose(models.script_death_animation_debug.Avatar)
models.script_death_animation_debug.Avatar:setPos(player:getPos():scale(16))
if self.parent.characterData.deathAnimation.callbacks ~= nil and self.parent.characterData.deathAnimation.callbacks.onPhase1 ~= nil then
self.parent.characterData.deathAnimation.callbacks.onPhase1(self.parent.characterData, models.script_death_animation_debug.Avatar, self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].name:upper())
end
if self.parent.characterData.deathAnimation.callbacks ~= nil and self.parent.characterData.deathAnimation.callbacks.onPhase2 ~= nil then
self.parent.characterData.deathAnimation.callbacks.onPhase2(self.parent.characterData, models.script_death_animation_debug.Avatar, self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].name:upper())
end
end)
end
end;
---存在しないかもしれないモデルパーツを安全に削除する。
---@param target? ModelPart 削除対象のモデルパーツ
removeUnsafeModel = function (target)
if target ~= nil then
target:getParent():removeChild(target)
target:remove()
end
end;
---ヘリコプターの出現/消滅パーティクルを生成する。
---@param self DeathAnimation
spawnHelicopterParticles = function (self)
local helicopterPos = self.parent.modelUtils.getModelWorldPos(models.models.death_animation.Helicopter)
for _ = 1, 100 do
particles:newParticle(self.parent.compatibilityUtils:checkParticle("minecraft:poof"), helicopterPos:copy():add(vectors.rotateAroundAxis(self.animationRot, math.random() * 9.375 - 4.6875, math.random() * 11.125 - 5.5625, math.random() * 23.875 - 11.9375, 0, math.abs(helicopterPos.y), 0)))
end
end;
---死亡アニメーション用のダミーアバターを生成する。
---@param self DeathAnimation
---@param parent ModelPart ダミーアバターをアタッチする親のモデルパーツ
generateDummyAvatar = function (self, parent)
local isArmorVisible = {
helmet = self.parent.armor.isArmorVisible.helmet;
chestplate = self.parent.armor.isArmorVisible.chestplate;
leggings = self.parent.armor.isArmorVisible.leggings;
boots = self.parent.armor.isArmorVisible.boots;
}
if isArmorVisible.helmet then
self.parent.armor:setHelmet(world.newItem(self.parent.compatibilityUtils:checkItem("minecraft:air")))
end
if isArmorVisible.chestplate then
self.parent.armor:setChestplate(world.newItem(self.parent.compatibilityUtils:checkItem("minecraft:air")))
end
if isArmorVisible.leggings then
self.parent.armor:setLeggings(world.newItem(self.parent.compatibilityUtils:checkItem("minecraft:air")))
end
if isArmorVisible.boots then
self.parent.armor:setBoots(world.newItem(self.parent.compatibilityUtils:checkItem("minecraft:air")))
end
self.parent.physics:disable()
if self.parent.characterData.deathAnimation.callbacks ~= nil and self.parent.characterData.deathAnimation.callbacks.onBeforeModelCopy ~= nil then
self.parent.characterData.deathAnimation.callbacks.onBeforeModelCopy(self.parent.characterData, self.isScriptLoaded)
end
parent:addChild(self.parent.modelUtils:copyModel(models.models.main.Avatar))
parent.Avatar.Head.FaceParts.Eyes.EyeRight:setUVPixels(self.parent.characterData.faceParts.leftEye.TIRED:copy():scale(6))
parent.Avatar.Head.FaceParts.Eyes.EyeLeft:setUVPixels(self.parent.characterData.faceParts.rightEye.TIRED:copy():scale(6))
parent.Avatar.Head.HeadRing:setRot()
for _, modelPart in ipairs({parent.Avatar.UpperBody.Arms.RightArm.RightArmBottom.RightItemPivot, parent.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.LeftItemPivot}) do
modelPart:remove()
end
local unsafeModels = {parent.Avatar.Head.FaceParts.Mouth, parent.Avatar.Head.ArmorH, parent.Avatar.UpperBody.Body.ArmorB, parent.Avatar.UpperBody.Arms.RightArm.ArmorRA, parent.Avatar.UpperBody.Arms.RightArm.RightArmBottom.ArmorRAB, parent.Avatar.UpperBody.Arms.LeftArm.ArmorLA, parent.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.ArmorLAB, parent.Avatar.LowerBody.Legs.RightLeg.ArmorRL, parent.Avatar.LowerBody.Legs.RightLeg.RightLegBottom.ArmorRLB, parent.Avatar.LowerBody.Legs.LeftLeg.ArmorLL, parent.Avatar.LowerBody.Legs.LeftLeg.LeftLegBottom.ArmorLLB}
for i = 1, 11 do
self.removeUnsafeModel(unsafeModels[i])
end
if parent.Avatar.UpperBody.Body.Gun ~= nil then
if self.parent.characterData.gun.gunPosition.put.type == "BODY" then
local leftHanded = player:isLeftHanded()
parent.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, 12, 0):add(self.parent.characterData.gun.gunPosition.put.pos[leftHanded and "left" or "right"]))
parent.Avatar.UpperBody.Body.Gun:setRot(self.parent.characterData.gun.gunPosition.put.rot[leftHanded and "left" or "right"])
else
parent.Avatar.UpperBody.Body.Gun:remove()
end
end
if isArmorVisible.helmet then
self.parent.armor:setHelmet(self.parent.armor.armorSlotItems[1])
end
if isArmorVisible.chestplate then
self.parent.armor:setChestplate(self.parent.armor.armorSlotItems[2])
end
if isArmorVisible.leggings then
self.parent.armor:setLeggings(self.parent.armor.armorSlotItems[3])
end
if isArmorVisible.boots then
self.parent.armor:setBoots(self.parent.armor.armorSlotItems[4])
end
self.parent.physics:enable()
if self.parent.characterData.deathAnimation.callbacks ~= nil and self.parent.characterData.deathAnimation.callbacks.onAfterModelCopy ~= nil then
self.parent.characterData.deathAnimation.callbacks.onAfterModelCopy(self.parent.characterData, self.isScriptLoaded)
end
end;
---ダミーアバター状態をリセットする。
---@param avatarRoot ModelPart ダミーアバターのルート
resetDummyAvatar = function (avatarRoot)
for _, modelPart in ipairs({avatarRoot, avatarRoot.Head, avatarRoot.UpperBody, avatarRoot.UpperBody.Body, avatarRoot.UpperBody.Arms, avatarRoot.UpperBody.Arms.RightArm, avatarRoot.UpperBody.Arms.RightArm.RightArmBottom, avatarRoot.UpperBody.Arms.LeftArm, avatarRoot.UpperBody.Arms.LeftArm.LeftArmBottom, avatarRoot.LowerBody, avatarRoot.LowerBody.Legs, avatarRoot.LowerBody.Legs.RightLeg, avatarRoot.LowerBody.Legs.RightLeg.RightLegBottom, avatarRoot.LowerBody.Legs.LeftLeg, avatarRoot.LowerBody.Legs.LeftLeg.LeftLegBottom}) do
modelPart:setPos()
modelPart:setRot()
modelPart:setScale()
end
end;
---ダミーアバターをフェーズ1のポーズにする。
---@param avatarRoot ModelPart ダミーアバターのルート
setPhase1Pose = function (avatarRoot)
avatarRoot:setPos(0, -12, 0)
avatarRoot.Head:setRot(-30, 0, 0)
avatarRoot.UpperBody.Arms.RightArm:setRot(35, 0, -20)
avatarRoot.UpperBody.Arms.LeftArm:setRot(35, 0, 20)
avatarRoot.LowerBody.Legs.RightLeg:setRot(90, -10, 0)
avatarRoot.LowerBody.Legs.LeftLeg:setRot(90, 10, 0)
end;
---ダミーアバターをフェーズ2のポーズにする。
---@param avatarRoot ModelPart ダミーアバターのルート
setPhase2Pose = function (avatarRoot)
avatarRoot:setPos(3, -210, 2)
avatarRoot:setRot(105, 75, 90)
avatarRoot.Head:setRot(0, -40, 0)
avatarRoot.UpperBody.Arms.RightArm:setRot(47.5, 0, 20)
avatarRoot.UpperBody.Arms.LeftArm:setRot(-30, 0, -15)
avatarRoot.LowerBody.Legs.RightLeg:setRot(80, 0, 0)
avatarRoot.LowerBody.Legs.RightLeg.RightLegBottom:setRot(-75, 0, 0)
avatarRoot.LowerBody.Legs.LeftLeg:setRot(10, 0, 0)
avatarRoot:setLight()
end;
---死亡アニメーションを再生する。
---@param self DeathAnimation
play = function (self)
self:stop()
self.costumeIndex = self.parent.costume.currentCostume
--ダミーアバターを生成する。
local unsafeModels = {models.models.death_animation.Avatar, models.models.death_animation.Helicopter.RopeLadder.RopeLadder2.RopeLadder3.RopeLadder4.RopeLadder5.RopeLadder6.RopeLadder7.RopeLadder8.RopeLadder9.RopeLadder10.RopeLadder11.RopeLadder12.RopeLadder13.RopeLadder14.Avatar}
for i = 1, 2 do
self.removeUnsafeModel(unsafeModels[i])
end
self:generateDummyAvatar(models.models.death_animation)
--死亡アニメーションを生成する。
self.resetDummyAvatar(models.models.death_animation.Avatar)
self.setPhase1Pose(models.models.death_animation.Avatar)
self.animationPos = player:getPos()
models.models.death_animation:setPos(self.animationPos:copy():scale(16))
self.animationRot = (player:getBodyYaw() * -1 + 180) % 360
models.models.death_animation:setRot(0, self.animationRot)
models.models.death_animation:setVisible(true)
animations["models.death_animation"]["death_animation"]:play()
if self.parent.characterData.deathAnimation.callbacks ~= nil and self.parent.characterData.deathAnimation.callbacks.onPhase1 ~= nil then
self.parent.characterData.deathAnimation.callbacks.onPhase1(self.parent.characterData, models.models.death_animation.Avatar, self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].name:upper())
end
if events.TICK:getRegisteredCount("death_animation_tick") == 0 then
events.TICK:register(function ()
local particleAnchorPos = self.parent.modelUtils.getModelWorldPos(models.models.death_animation.DeathAnimationParticleAnchor)
for _ = 1, 3 do
local particleRot = math.random() * math.pi * 2
local particleOffset = math.random() * 3
particles:newParticle(self.parent.compatibilityUtils:checkParticle("minecraft:poof"), particleAnchorPos:copy():add(math.cos(particleRot) * particleOffset, 0, math.sin(particleRot) * particleOffset)):setVelocity(math.cos(particleRot), 0, math.sin(particleRot))
end
if self.animationCount % 2 == 1 then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.bamboo_wood_door.close"), self.parent.modelUtils.getModelWorldPos(models.models.death_animation.Helicopter.DeathAnimationSoundAnchor1), 1, 0.5):setAttenuation(2)
end
if self.animationCount < 120 then
models.models.death_animation.Avatar:setLight(world.getLightLevel(self.animationPos))
end
if self.animationCount == 1 then
self:spawnHelicopterParticles()
elseif self.animationCount == 10 then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.iron_door.open"), self.parent.modelUtils.getModelWorldPos(models.models.death_animation.Helicopter.DeathAnimationSoundAnchor1), 1, 0.5)
elseif self.animationCount >= 57 and self.animationCount < 76 then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:entity.player.attack.sweep"), self.parent.modelUtils.getModelWorldPos(models.models.death_animation.Helicopter.RopeLadder.RopeLadder2.RopeLadder3.RopeLadder4.RopeLadder5.RopeLadder6.RopeLadder7.RopeLadder8.RopeLadder9.RopeLadder10.RopeLadder11.RopeLadder12.RopeLadder13.RopeLadder14), 0.25, -0.056 * (self.animationCount - 57) + 2)
elseif self.animationCount == 120 then
self.dummyAvatarRoot = models.models.death_animation.Avatar:moveTo(models.models.death_animation.Helicopter.RopeLadder.RopeLadder2.RopeLadder3.RopeLadder4.RopeLadder5.RopeLadder6.RopeLadder7.RopeLadder8.RopeLadder9.RopeLadder10.RopeLadder11.RopeLadder12.RopeLadder13.RopeLadder14)
self.setPhase2Pose(models.models.death_animation.Helicopter.RopeLadder.RopeLadder2.RopeLadder3.RopeLadder4.RopeLadder5.RopeLadder6.RopeLadder7.RopeLadder8.RopeLadder9.RopeLadder10.RopeLadder11.RopeLadder12.RopeLadder13.RopeLadder14.Avatar)
if self.parent.characterData.deathAnimation.callbacks ~= nil and self.parent.characterData.deathAnimation.callbacks.onPhase2 ~= nil then
self.parent.characterData.deathAnimation.callbacks.onPhase2(self.parent.characterData, models.models.death_animation.Helicopter.RopeLadder.RopeLadder2.RopeLadder3.RopeLadder4.RopeLadder5.RopeLadder6.RopeLadder7.RopeLadder8.RopeLadder9.RopeLadder10.RopeLadder11.RopeLadder12.RopeLadder13.RopeLadder14.Avatar, self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].name:upper())
end
elseif self.animationCount == 180 then
models.models.death_animation.Helicopter.RopeLadder.RopeLadder2.RopeLadder3.RopeLadder4.RopeLadder5.RopeLadder6.RopeLadder7.RopeLadder8.RopeLadder9.RopeLadder10.RopeLadder11.RopeLadder12.RopeLadder13.RopeLadder14.Avatar:setVisible(false)
elseif self.animationCount == 230 then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.iron_door.close"), self.parent.modelUtils.getModelWorldPos(models.models.death_animation.Helicopter.DeathAnimationSoundAnchor1), 1, 0.5)
elseif self.animationCount == 255 then
self:spawnHelicopterParticles()
self:stop()
end
end, "death_animation_tick")
end
if events.WORLD_TICK:getRegisteredCount("death_animation_world_tick") == 0 then
events.WORLD_TICK:register(function ()
self.animationCount = self.animationCount + 1
end, "death_animation_world_tick")
end
end;
---死亡アニメーションを停止する。
---@param self DeathAnimation
stop = function (self)
models.models.death_animation:setVisible(false)
animations["models.death_animation"]["death_animation"]:stop()
events.TICK:remove("death_animation_tick")
events.WORLD_TICK:remove("death_animation_world_tick")
self.dummyAvatarRoot = nil
self.animationCount = 0
end;
}

View file

@ -0,0 +1,71 @@
---@class AbstractEvent : AvatarModule
---@field public registerTable {[string]: fun()[]} 登録されたコールバック関数を管理するテーブル
---@field public register fun(self: AbstractEvent, callback: fun(...: any), name?: string) イベントにコールバック関数登録する
---@field public remove fun(self: AbstractEvent, name: string) 指定した名前のコールバック関数を1つ削除する
---@field public getRegisteredCount fun(self: AbstractEvent, name: string): integer 指定した名前で登録されているコールバック関数の数を返す
---@field public fire fun(self: AbstractEvent, ...: any) 登録された全てのコールバック関数を呼ぶ
AbstractEvent = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return AbstractEvent
new = function (parent)
---@type AbstractEvent
local instance = Avatar.instantiate(AbstractEvent, AvatarModule, parent)
instance.registerTable = {}
return instance
end;
---イベントにコールバック関数登録する。
---@param self AbstractEvent
---@param callback fun(...: any) 登録するコールバック関数
---@param name? string コールバック関数の名前
register = function (self, callback, name)
local callbackName = name ~= nil and name or client.intUUIDToString(client.generateUUID())
if self.registerTable[callbackName] ~= nil then
table.insert(self.registerTable[callbackName], callback)
else
local callbackTable = {}
table.insert(callbackTable, callback)
self.registerTable[callbackName] = callbackTable
end
end;
---指定した名前のコールバック関数を1つ削除する。
---@param self AbstractEvent
---@param name string コールバック関数の名前
remove = function (self, name)
if self.registerTable[name] ~= nil then
table.remove(self.registerTable[name])
if #self.registerTable[name] == 0 then
self.registerTable[name] = nil
end
end
end;
---指定した名前で登録されているコールバック関数の数を返す。
---@param self AbstractEvent
---@param name string コールバック関数の名前
---@return integer registeredCount 登録されていたコールバック関数の数
getRegisteredCount = function (self, name)
if self.registerTable[name] ~= nil then
return #self.registerTable[name]
else
return 0
end
end;
---登録された全てのコールバック関数を呼ぶ。
---@param self AbstractEvent
---@param ... any callbackArgs コールバック引数
fire = function (self, ...)
for _, eventTable in pairs(self.registerTable) do
for _, callback in ipairs(eventTable) do
---@diagnostic disable-next-line: redundant-parameter
callback(...)
end
end
end;
}

View file

@ -0,0 +1,16 @@
---@class AvatarEvents : AvatarModule アバター独自のイベントを定義し、管理するクラス
---@field public SCRIPT_INIT ScriptInitEvent 全てのスクリプトの初期化が完了した後に1度だけ呼ばれるイベント
AvatarEvents = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return AvatarEvents
new = function (parent)
---@type AvatarEvents
local instance = Avatar.instantiate(AvatarEvents, AvatarModule, parent)
instance.SCRIPT_INIT = ScriptInitEvent.new(instance.parent)
return instance
end;
}

View file

@ -0,0 +1,25 @@
---@class ScriptInitEvent : AbstractEvent 全てのスクリプトの初期化が完了した後に1度だけ呼ばれるイベント
---@field public register fun(self: AbstractEvent, callback: fun(), name?: string) イベントにコールバック関数登録する
---@field public fire fun(self: AbstractEvent) 登録された全てのコールバック関数を呼ぶ
ScriptInitEvent = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return ScriptInitEvent
new = function (parent)
---@type ScriptInitEvent
local instance = Avatar.instantiate(ScriptInitEvent, AbstractEvent, parent)
return instance
end;
---登録された全てのコールバック関数を呼ぶ。
---@param self ScriptInitEvent
fire = function (self, ...)
for _, eventTable in pairs(self.registerTable) do
for _, callback in ipairs(eventTable) do
callback()
end
end
end;
}

View file

@ -0,0 +1,434 @@
---@alias ExSkill.AutoPlayMode
---| "NONE" # 自動再生なし
---| "MAIN" # メインExスキル
---| "SUB" # サブExスキル
---@alias ExSkill.TransitionPhase
---| "PRE" # Exスキルアニメーション開始前
---| "POST" # Exスキルアニメーション終了後
---@class (exact) ExSkill : AvatarModule Exスキルのアニメーションを管理するクラス
---@field package AUTO_PLAY ExSkill.AutoPlayMode アバター読み込み時に自動的にExスキルが再生される。デバッグ用。
---@field public frameParticleAmount integer Exスキルフレームのパーティクルの量:1. 標準, 2. 少なめ, 3. なし, 4. Exスキルフレーム非表示、パーティクル量は標準
---@field package exSkillIndex integer 現在再生中のExスキルのインデックス番号
---@field public animationCount integer Exスキルのアニメーション再生中に増加するカウンター。-1はアニメーション停止中を示す。
---@field package animationLength number Exスキルのアニメーションの長さ。スクリプトで自動で代入する。
---@field public transitionCount number Exスキルのアニメーション前後のカメラのトランジションの進捗を示すカウンター
---@field package keyPressCount integer Exスキルキーを押下し続ける時間を計るカウンター
---@field package bodyYaw number[] プレイヤーの体の回転
---@field package isDebugInit boolean デバッグモードが初期化されたかどうか
---@field public canPlayAnimation fun(self: ExSkill): boolean アニメーションが再生可能かどうかを返す
---@field package transition fun(self: ExSkill, direction: ExSkill.TransitionPhase, callback: fun()) Exスキルのアニメーションの前後のカメラのトランジションを行う関数
---@field public play fun(self: ExSkill, isSubExSkill: boolean) アニメーションを再生する
---@field public stop fun(self: ExSkill) アニメーションを停止する
---@field public forceStop fun(self: ExSkill) アニメーションを停止する。終了時のトランジションも無効。
ExSkill = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return ExSkill
new = function (parent)
---@type ExSkill
local instance = Avatar.instantiate(ExSkill, AvatarModule, parent)
instance.AUTO_PLAY = "NONE"
instance.frameParticleAmount = instance.parent.config:loadConfig("PRIVATE", "exSkillFrameParticleAmount", 1)
instance.exSkillIndex = 1
instance.animationCount = -1
instance.animationLength = 0
instance.transitionCount = 0
instance.keyPressCount = 0
instance.bodyYaw = {}
instance.isDebugInit = false
return instance
end;
---初期化関数
---@param self ExSkill
init = function (self)
AvatarModule.init(self)
if host:isHost() then
for _, exSkill in ipairs(self.parent.characterData.exSkill) do
local offset = (exSkill.camera.fixMode ~= nil and exSkill.camera.fixMode) and 1 or 0.9375
exSkill.camera.start.pos:mul(-1, 1, 1):scale(1 / 16 * offset)
exSkill.camera.fin.pos:mul(-1, 1, 1):scale(1 / 16 * offset)
end
local exSkillKey = self.parent.keyManager:register("ex_skill", self.parent.config:loadConfig("PRIVATE", "keybind.ex_skill", "key.keyboard.g"))
exSkillKey:setOnPress(function ()
while events.TICK:getRegisteredCount("ex_skill_keypress_tick") > 0 do
events.TICK:remove("ex_skill_keypress_tick")
end
events.TICK:register(function ()
if self.keyPressCount == 30 then
events.TICK:remove("ex_skill_keypress_tick")
self.parent.placementObjectManager:removeAll()
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:entity.zombie.break_wooden_door"), player:getPos(), 0.25, 2)
self.keyPressCount = 0
return
end
self.keyPressCount = self.keyPressCount + 1
end, "ex_skill_keypress_tick")
end)
exSkillKey:setOnRelease(function ()
events.TICK:remove("ex_skill_keypress_tick")
if self.keyPressCount > 0 then
if self:canPlayAnimation() and self.animationCount == -1 and self.transitionCount == 0 then
pings.exSkill()
else
print(self.parent.locale:getLocale("key_bind.ex_skill.unavailable"..(renderer:isFirstPerson() and "_firstperson" or "")))
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.note_block.bass"), player:getPos(), 1, 0.5)
end
self.keyPressCount = 0
end
end)
self.parent.keyManager:register("ex_skill_sub", self.parent.config:loadConfig("PRIVATE", "keybind.ex_skill_sub", "key.keyboard.h")):setOnPress(function ()
if self:canPlayAnimation() and self.animationCount == -1 and self.transitionCount == 0 and self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].subExSkill ~= nil then
pings.subExSkill()
else
print(self.parent.locale:getLocale(self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].subExSkill == nil and "action_wheel.main.action_6.unavailable" or "key_bind.ex_skill.unavailable"..(renderer:isFirstPerson() and "_firstperson" or "")))
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.note_block.bass"), player:getPos(), 1, 0.5)
end
end)
end
table.insert(self.bodyYaw, player:getBodyYaw() % 360)
events.TICK:register(function ()
if not renderer:isFirstPerson() then
table.insert(self.bodyYaw, player:getBodyYaw() % 360)
if #self.bodyYaw == 3 then
table.remove(self.bodyYaw, 1)
end
end
end)
if self.AUTO_PLAY ~= "NONE" then
events.TICK:register(function ()
if not self.isDebugInit then
events.TICK:remove("ex_skill_debug_tick")
self:play(self.AUTO_PLAY == "SUB")
self.isDebugInit = true
end
end, "ex_skill_debug_tick")
end
end;
---アニメーションが再生可能かどうかを返す。
---@param self ExSkill
---@return boolean canPlayAnimation Exスキルアニメーションが再生可能かどうか
canPlayAnimation = function (self)
return player:getPose() == "STANDING" and player:getVelocity():length() < 0.01 and self.bodyYaw[1] == self.bodyYaw[2] and player:isOnGround() and not player:isInWater() and not player:isInLava() and player:getFrozenTicks() == 0 and not renderer:isFirstPerson() and self.parent.playerUtils.damageStatus == "NONE" and player:getSwingArm() == nil and player:getActiveItem().id == "minecraft:air" and not self.parent.costume.isChangingCostume
end;
---Exスキルのアニメーションの前後のカメラのトランジションを行う関数
---@param self ExSkill
---@param direction ExSkill.TransitionPhase カメラのトランジションの向き
---@param callback fun() トランジション終了時に呼び出されるコールバック関数
transition = function (self, direction, callback)
events.TICK:register(function ()
if not client:isPaused() then
if events.TICK:getRegisteredCount("ex_skill_transition_tick") == 1 then
self.transitionCount = direction == "PRE" and math.min(self.transitionCount + 1, 10) or math.max(self.transitionCount - 1, 0)
end
if (direction == "PRE" and self.transitionCount == 10) or (direction == "POST" and self.transitionCount == 0) then
if host:isHost() then
local windowSize = client:getScaledWindowSize()
models.models.ex_skill_frame.Gui.FrameBar:setPos(0, 0, 0)
if direction == "PRE" and self.frameParticleAmount < 4 then
for _, modelPart in ipairs({models.models.ex_skill_frame.Gui.Frame.FrameTopLeft, models.models.ex_skill_frame.Gui.Frame.FrameTopRight, models.models.ex_skill_frame.Gui.Frame.FrameBottomLeft, models.models.ex_skill_frame.Gui.Frame.FrameBottomRight}) do
modelPart:setVisible(true)
end
models.models.ex_skill_frame.Gui.Frame.FrameTop:setPos(-windowSize.x + 16, -16)
models.models.ex_skill_frame.Gui.Frame.FrameTop:setScale(windowSize.x / 16 - 2, 1, 1)
models.models.ex_skill_frame.Gui.Frame.FrameLeft:setPos(-16, -windowSize.y + 16)
models.models.ex_skill_frame.Gui.Frame.FrameLeft:setScale(1, windowSize.y / 16 - 2, 1)
models.models.ex_skill_frame.Gui.Frame.FrameBottom:setPos(-windowSize.x + 16, -windowSize.y)
models.models.ex_skill_frame.Gui.Frame.FrameBottom:setScale(windowSize.x / 16 - 2, 1, 1)
models.models.ex_skill_frame.Gui.Frame.FrameRight:setPos(-windowSize.x, -windowSize.y + 16)
models.models.ex_skill_frame.Gui.Frame.FrameRight:setScale(1, windowSize.y / 16 - 2, 1)
elseif direction == "POST" then
for _, modelPart in ipairs({models.models.ex_skill_frame.Gui.Frame.FrameTopLeft, models.models.ex_skill_frame.Gui.Frame.FrameTopRight, models.models.ex_skill_frame.Gui.Frame.FrameBottomLeft, models.models.ex_skill_frame.Gui.Frame.FrameBottomRight}) do
modelPart:setVisible(false)
end
for _, modelPart in ipairs({models.models.ex_skill_frame.Gui.Frame.FrameTop, models.models.ex_skill_frame.Gui.Frame.FrameLeft, models.models.ex_skill_frame.Gui.Frame.FrameBottom, models.models.ex_skill_frame.Gui.Frame.FrameRight}) do
modelPart:setScale(0, 0, 0)
end
end
end
callback()
events.TICK:remove("ex_skill_transition_tick")
events.RENDER:remove("ex_skill_transition_render")
end
if host:isHost() then
local barPos = models.models.ex_skill_frame.Gui.FrameBar:getPos().x * -1
local windowSizeY = client:getScaledWindowSize().y
if self.frameParticleAmount ~= 3 and self.transitionCount >= 1 and self.transitionCount <= 9 then
for _ = 1, windowSizeY / (self.frameParticleAmount == 2 and 100 or 20) do
local particleOffset = math.random() * windowSizeY
self.parent.frameParticleManager:spawn(vectors.vec2(barPos - particleOffset - math.random() * 50, particleOffset):scale(-1), vectors.vec2(500, 0))
end
end
end
end
end, "ex_skill_transition_tick")
events.RENDER:register(function (delta)
--カメラのトランジション
if not client:isPaused() and host:isHost() then
local lookDir = player:getLookDir()
local cameraRot = renderer:isCameraBackwards() and vectors.vec3(math.deg(math.asin(lookDir.y)), math.deg(math.atan2(lookDir.z, lookDir.x) + math.pi / 2)) or vectors.vec3(math.deg(math.asin(-lookDir.y)), math.deg(math.atan2(lookDir.z, lookDir.x) - math.pi / 2))
local targetCameraPos = vectors.vec3()
local targetCameraRot = vectors.vec3()
if direction == "PRE" then
targetCameraPos = vectors.rotateAroundAxis(self.bodyYaw[2] * -1 + 180, self.parent.characterData.exSkill[self.exSkillIndex].camera.start.pos, 0, 1):add(0, -1.62)
targetCameraRot = self.parent.characterData.exSkill[self.exSkillIndex].camera.start.rot:copy():add(0, self.bodyYaw[2], 0)
else
targetCameraPos = vectors.rotateAroundAxis(self.bodyYaw[2] * -1 + 180, self.parent.characterData.exSkill[self.exSkillIndex].camera.fin.pos, 0, 1):add(0, -1.62)
targetCameraRot = self.parent.characterData.exSkill[self.exSkillIndex].camera.fin.rot:copy():add(0, self.bodyYaw[2], 0)
end
if math.abs(cameraRot.y - targetCameraRot.y) >= 180 then
if cameraRot.y < targetCameraRot.y then
cameraRot.y = cameraRot.y + 360
else
targetCameraRot.y = targetCameraRot.y + 360
end
end
local trueDelta = direction == "PRE" and delta or delta * -1
self.parent.cameraManager.setCameraPivot(targetCameraPos:scale((self.transitionCount + trueDelta) / 10))
self.parent.cameraManager.setCameraRot(targetCameraRot:copy():sub(cameraRot):scale((self.transitionCount + trueDelta) / 10):add(cameraRot))
self.parent.cameraManager:setThirdPersonCameraDistance(4 - (self.transitionCount + trueDelta) / 10 * 4)
--フレーム演出
local windowSize = client:getScaledWindowSize()
local barPos = (windowSize.x + windowSize.y + math.sqrt(2) * 16) * (direction == "PRE" and (self.transitionCount + trueDelta) / 10 or (1 - (self.transitionCount + trueDelta) / 10))
models.models.ex_skill_frame.Gui.FrameBar:setPos(-barPos, 0, 0)
if self.frameParticleAmount < 4 then
local frameTopLength = math.clamp(barPos, 32, windowSize.x)
local frameLeftLength = math.clamp(barPos, 32, windowSize.y)
local frameBottomLength = math.clamp(barPos - windowSize.y + 16, 32, windowSize.x)
local frameRightLength = math.clamp(barPos - windowSize.x + 16, 32, windowSize.y)
models.models.ex_skill_frame.Gui.Frame.FrameTopLeft:setPos(-16, -16)
models.models.ex_skill_frame.Gui.Frame.FrameTopRight:setPos(-windowSize.x, -16)
models.models.ex_skill_frame.Gui.Frame.FrameBottomLeft:setPos(-16, -windowSize.y)
models.models.ex_skill_frame.Gui.Frame.FrameBottomRight:setPos(-windowSize.x, -windowSize.y)
if direction == "PRE" then
models.models.ex_skill_frame.Gui.Frame.FrameTopLeft:setVisible(barPos >= 16)
models.models.ex_skill_frame.Gui.Frame.FrameTopRight:setVisible(barPos >= windowSize.x + 16)
models.models.ex_skill_frame.Gui.Frame.FrameBottomLeft:setVisible(barPos >= windowSize.y + 16)
models.models.ex_skill_frame.Gui.Frame.FrameBottomRight:setVisible(barPos >= windowSize.x + windowSize.y)
models.models.ex_skill_frame.Gui.Frame.FrameTop:setPos(-frameTopLength + 16, -16)
models.models.ex_skill_frame.Gui.Frame.FrameTop:setScale(frameTopLength / 16 - 2, 1, 1)
models.models.ex_skill_frame.Gui.Frame.FrameLeft:setPos(-16, -frameLeftLength + 16)
models.models.ex_skill_frame.Gui.Frame.FrameLeft:setScale(1, frameLeftLength / 16 - 2, 1)
models.models.ex_skill_frame.Gui.Frame.FrameBottom:setPos(-frameBottomLength + 16, -windowSize.y)
models.models.ex_skill_frame.Gui.Frame.FrameBottom:setScale(frameBottomLength / 16 - 2, 1, 1)
models.models.ex_skill_frame.Gui.Frame.FrameRight:setPos(-windowSize.x, -frameRightLength + 16)
models.models.ex_skill_frame.Gui.Frame.FrameRight:setScale(1, frameRightLength / 16 - 2, 1)
else
models.models.ex_skill_frame.Gui.Frame.FrameTopLeft:setVisible(barPos < 16)
models.models.ex_skill_frame.Gui.Frame.FrameTopRight:setVisible(barPos < windowSize.x + 16)
models.models.ex_skill_frame.Gui.Frame.FrameBottomLeft:setVisible(barPos < windowSize.y + 16)
models.models.ex_skill_frame.Gui.Frame.FrameBottomRight:setVisible(barPos < windowSize.x + windowSize.y)
models.models.ex_skill_frame.Gui.Frame.FrameTop:setPos(-windowSize.x + 16, -16)
models.models.ex_skill_frame.Gui.Frame.FrameTop:setScale((windowSize.x - frameTopLength) / 16, 1, 1)
models.models.ex_skill_frame.Gui.Frame.FrameLeft:setPos(-16, -windowSize.y + 16)
models.models.ex_skill_frame.Gui.Frame.FrameLeft:setScale(1, (windowSize.y - frameLeftLength) / 16, 1)
models.models.ex_skill_frame.Gui.Frame.FrameBottom:setPos(-windowSize.x + 16, -windowSize.y)
models.models.ex_skill_frame.Gui.Frame.FrameBottom:setScale((windowSize.x - frameBottomLength) / 16, 1, 1)
models.models.ex_skill_frame.Gui.Frame.FrameRight:setPos(-windowSize.x, -windowSize.y + 16)
models.models.ex_skill_frame.Gui.Frame.FrameRight:setScale(1, (windowSize.y - frameRightLength) / 16, 1)
end
end
end
end, "ex_skill_transition_render")
end;
---アニメーションを再生する。
---@param self ExSkill
---@param isSubExSkill boolean サブExスキルを再生するかどうか
play = function (self, isSubExSkill)
if isSubExSkill then
if self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].subExSkill ~= nil then
self.exSkillIndex = self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].subExSkill
else
return
end
else
self.exSkillIndex = self.parent.characterData.costume.costumes[self.parent.costume.currentCostume].exSkill
end
self.parent.bubble:stop()
renderer:setFOV(70 / client:getFOV())
renderer:setRenderHUD(false)
self.parent.cameraManager:setCameraCollisionDenial(true)
models.models.ex_skill_frame.Gui:setColor(self.parent.characterData.exSkill[self.exSkillIndex].formationType == "STRIKER" and vectors.vec3(1, 0.75, 0.75) or vectors.vec3(0.75, 1, 1))
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:entity.player.levelup"), player:getPos(), 5, 2)
if self.parent.characterData.exSkill[self.exSkillIndex].callbacks ~= nil and self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPreTransition ~= nil then
self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPreTransition(self.parent.characterData)
end
models.models.ex_skill_frame.Gui.FrameBar:setScale(1, client:getScaledWindowSize().y * math.sqrt(2) / 16 + 1, 1)
events.TICK:register(function ()
if not self:canPlayAnimation() then
self:forceStop()
end
end, "ex_skill_tick")
self:transition("PRE", function ()
self.parent.physics:disable()
for _, itemModel in ipairs({vanilla_model.RIGHT_ITEM, vanilla_model.LEFT_ITEM}) do
itemModel:setVisible(false)
end
for _, modelPart in ipairs(self.parent.characterData.exSkill[self.exSkillIndex].models) do
modelPart:setVisible(true)
end
for _, modelPart in ipairs(self.parent.characterData.exSkill[self.exSkillIndex].animations) do
animations["models."..modelPart]["ex_skill_"..self.exSkillIndex]:play()
end
if self.parent.characterData.exSkill[self.exSkillIndex].callbacks ~= nil and self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPreAnimation ~= nil then
self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPreAnimation(self.parent.characterData)
end
self.parent.cameraManager:setThirdPersonCameraDistance(0)
events.TICK:register(function ()
if not client:isPaused() then
if self.animationCount == self.animationLength - 1 then
self:stop()
elseif self:canPlayAnimation() and animations["models.main"]["ex_skill_"..self.exSkillIndex]:isPlaying() then
if self.parent.characterData.exSkill[self.exSkillIndex].callbacks ~= nil and self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onAnimationTick ~= nil then
self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onAnimationTick(self.parent.characterData, self.animationCount)
end
self.animationCount = self.animationCount > -1 and self.animationCount + 1 or self.animationCount
end
if host:isHost() then
local windowSize = client:getScaledWindowSize()
local windowCenter = windowSize:copy():scale(0.5)
if self.frameParticleAmount < 3 then
for _ = 1, (windowSize.x * 2 + windowSize.y * 2) / (self.frameParticleAmount == 1 and 100 or 500) do
local particlePos = vectors.vec2(math.random() * (windowSize.x * 2 + windowSize.y * 2), math.random() * 16)
particlePos = particlePos.x <= windowSize.x and particlePos or (particlePos.x <= windowSize.x + windowSize.y and vectors.vec2(windowSize.x - particlePos.y, particlePos.x - windowSize.x) or (particlePos.x <= windowSize.x * 2 + windowSize.y and vectors.vec2(particlePos.x - (windowSize.x + windowSize.y), windowSize.y - particlePos.y) or vectors.vec2(particlePos.y, particlePos.x - (windowSize.x * 2 + windowSize.y))))
self.parent.frameParticleManager:spawn(particlePos:copy():scale(-1), windowCenter:copy():sub(particlePos):scale(0.25))
end
end
end
end
end, "ex_skill_animation_tick")
if host:isHost() then
events.RENDER:register(function ()
if not client:isPaused() then
self.parent.cameraManager.setCameraPivot(vectors.rotateAroundAxis(self.bodyYaw[2] * -1 + 180, models.models.main.CameraAnchor:getAnimPos():scale(1 / 16 * ((self.parent.characterData.exSkill[self.exSkillIndex].camera.fixMode ~= nil and self.parent.characterData.exSkill[self.exSkillIndex].camera.fixMode) and 1 or 0.9375)), 0, 1, 0):add(0, -1.62, 0))
self.parent.cameraManager.setCameraRot(models.models.main.CameraAnchor:getAnimRot():scale(-1):add(0, self.bodyYaw[2], 0))
end
end, "ex_skill_animation_render")
end
self.animationCount = 0
self.parent.gun:processGunTick()
self.animationLength = math.round(animations["models.main"]["ex_skill_"..self.exSkillIndex]:getLength() * 20)
end)
end;
---アニメーションを停止する。
---@param self ExSkill
stop = function (self)
events.TICK:remove("ex_skill_animation_tick")
if host:isHost() then
events.RENDER:remove("ex_skill_animation_render")
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:entity.player.levelup"), player:getPos(), 5, 2):setAttenuation(100)
end
for _, itemModel in ipairs({vanilla_model.RIGHT_ITEM, vanilla_model.LEFT_ITEM}) do
itemModel:setVisible(true)
end
for _, modelPart in ipairs(self.parent.characterData.exSkill[self.exSkillIndex].models) do
modelPart:setVisible(false)
end
for _, modelPart in ipairs(self.parent.characterData.exSkill[self.exSkillIndex].animations) do
animations["models."..modelPart]["ex_skill_"..self.exSkillIndex]:stop()
end
self.parent.physics:enable()
renderer:setFOV()
self.animationCount = -1
if self.parent.characterData.exSkill[self.exSkillIndex].callbacks ~= nil and self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPostAnimation ~= nil then
self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPostAnimation(self.parent.characterData, false)
end
self:transition("POST", function ()
events.TICK:remove("ex_skill_tick")
self.parent.cameraManager.setCameraPivot()
self.parent.cameraManager.setCameraRot()
self.parent.cameraManager:setThirdPersonCameraDistance(4)
self.parent.cameraManager:setCameraCollisionDenial(false)
renderer:setRenderHUD(true)
if self.parent.characterData.exSkill[self.exSkillIndex].callbacks ~= nil and self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPostTransition ~= nil then
self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPostTransition(self.parent.characterData, false)
end
end)
end;
---アニメーションを停止する。終了時のトランジションも無効。
---@param self ExSkill
forceStop = function (self)
events.RENDER:remove("ex_skill_transition_render")
if host:isHost() then
events.TICK:remove("ex_skill_transition_tick")
events.RENDER:remove("ex_skill_animation_render")
end
for _, itemModel in ipairs({vanilla_model.RIGHT_ITEM, vanilla_model.LEFT_ITEM}) do
itemModel:setVisible(true)
end
for _, modelPart in ipairs(self.parent.characterData.exSkill[self.exSkillIndex].models) do
modelPart:setVisible(false)
end
for _, modelPart in ipairs(self.parent.characterData.exSkill[self.exSkillIndex].animations) do
animations["models."..modelPart]["ex_skill_"..self.exSkillIndex]:stop()
end
for _, eventName in ipairs({"ex_skill_tick", "ex_skill_animation_tick"}) do
events.TICK:remove(eventName)
end
self.parent.physics:enable()
models.models.ex_skill_frame.Gui.FrameBar:setPos()
for _, modelPart in ipairs({models.models.ex_skill_frame.Gui.Frame.FrameTopLeft, models.models.ex_skill_frame.Gui.Frame.FrameTopRight, models.models.ex_skill_frame.Gui.Frame.FrameBottomLeft, models.models.ex_skill_frame.Gui.Frame.FrameBottomRight}) do
modelPart:setVisible(false)
end
for _, modelPart in ipairs({models.models.ex_skill_frame.Gui.Frame.FrameTop, models.models.ex_skill_frame.Gui.Frame.FrameLeft, models.models.ex_skill_frame.Gui.Frame.FrameBottom, models.models.ex_skill_frame.Gui.Frame.FrameRight}) do
modelPart:setScale(0, 0, 0)
end
self.parent.faceParts:resetEmotion()
self.parent.cameraManager.setCameraPivot()
self.parent.cameraManager.setCameraRot()
self.parent.cameraManager:setThirdPersonCameraDistance(4)
self.parent.cameraManager:setCameraCollisionDenial(false)
renderer:setRenderHUD(true)
renderer:setFOV()
if self.animationCount >= 0 and self.parent.characterData.exSkill[self.exSkillIndex].callbacks ~= nil and self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPostAnimation ~= nil then
self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPostAnimation(self.parent.characterData, true)
end
if self.parent.characterData.exSkill[self.exSkillIndex].callbacks ~= nil and self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPostTransition ~= nil then
self.parent.characterData.exSkill[self.exSkillIndex].callbacks.onPostTransition(self.parent.characterData, true)
end
self.animationCount = -1
self.transitionCount = 0
end;
}
---Exスキルを再生する。
function pings.exSkill()
AvatarInstance.exSkill:play(false)
end
---サブExスキルを再生する。
function pings.subExSkill()
AvatarInstance.exSkill:play(true)
end

View file

@ -0,0 +1,68 @@
---@class (exact) ExSkillFrameParticle : SpawnObject Exスキルのフレームで使用するパーティクルの単一を管理するクラス
---@field package object ModelPart インスタンスで制御するオブジェクト
---@field package currentPos Vector2 パーティクルの現在位置
---@field package nextPos Vector2 次ティックのパーティクルの位置
---@field package velocity Vector2 パーティクルの速度
---@field package particleCount integer パーティクルのアニメーションを制御するためのカウンター
---@field public new fun(parent: Avatar, pos: Vector2, velocity: Vector2, type: ExSkillFrameParticleManager.ParticleType): ExSkillFrameParticle コンストラクター
ExSkillFrameParticle = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@param pos Vector2 パーティクルをスポーンさせるスクリーン上の座標。GUIスケールも考慮される。
---@param velocity Vector2 パーティクルの秒間移動距離(ピクセル)
---@param type ExSkillFrameParticleManager.ParticleType このインスタンスのパーティクルの種類
---@return ExSkillFrameParticle
new = function (parent, pos, velocity, type)
---@type ExSkillFrameParticle
local instance = Avatar.instantiate(ExSkillFrameParticle, SpawnObject, parent)
instance.object = models.models.ex_skill_frame.Particles["Particle"..(type == "NORMAL" and 1 or 2)]:copy(instance.uuid)
instance.currentPos = pos
instance.nextPos = instance.currentPos
instance.velocity = velocity
instance.particleCount = 0
instance.callbacks = {
---@param self ExSkillFrameParticle
onInit = function (self)
models.models.ex_skill_frame.Gui.script_ex_skill_frame_particles:addChild(self.object)
self.object:setRot(90, math.random() * 360, 180)
end;
---@param self ExSkillFrameParticle
onDeinit = function (self)
self.object:remove()
end;
---@param self ExSkillFrameParticle
onTick = function (self)
--パーティクル位置を強制更新
self.currentPos = self.nextPos:copy()
self.object:setPos(self.currentPos:copy():augmented(0))
self.object:setScale(vectors.vec3(1, 1, 1):scale(1 - self.particleCount / 5))
--カウンターを更新
self.particleCount = self.particleCount + 1
if self.particleCount == 5 then
self.object:remove()
elseif self.particleCount == 6 then
self.shouldDeinit = true
end
--次ティックの位置を計算
if self.velocity:length() > 0 then
self.nextPos = self.currentPos:copy():add(self.velocity:copy():scale(-0.05))
end
end;
---@param self ExSkillFrameParticle
onRender = function (self, delta)
self.object:setPos(self.nextPos:copy():sub(self.currentPos):scale(delta):add(self.currentPos):augmented(0))
self.object:setScale(vectors.vec3(1, 1, 1):scale(1 - (self.particleCount + delta) / 5))
end;
}
return instance
end;
}

View file

@ -0,0 +1,48 @@
---@alias ExSkillFrameParticleManager.ParticleType
---| "NORMAL" # 通常のパーティクル
---| "FIGURA" # Figuraマークのパーティクル(穴空き三角形)
---@class (exact) ExSkillFrameParticleManager : SpawnObjectManager Exスキルのフレームで使用するパーティクルを管理するクラス
---@field public objects ExSkillFrameParticle[] インスタンスで制御するオブジェクト
---@field public getObject fun(self: ExSkillFrameParticleManager, pos: Vector2, velocity: Vector2): ExSkillFrameParticle Exスキルフレームのパーティクルのインスタンスを生成して返す
---@field public spawn fun(self: ExSkillFrameParticleManager, pos: Vector2, velocity: Vector2) Exスキルフレームのパーティクルをスポーンさせる
ExSkillFrameParticleManager = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return ExSkillFrameParticleManager
new = function (parent)
---@type ExSkillFrameParticleManager
local instance = Avatar.instantiate(ExSkillFrameParticleManager, SpawnObjectManager, parent)
instance.managerName = "ex_skill_frame_particle"
return instance
end;
---初期化関数
---@param self ExSkillFrameParticleManager
init = function (self)
SpawnObjectManager.init(self)
---@diagnostic disable-next-line: discard-returns
models.models.ex_skill_frame.Gui:newPart("script_ex_skill_frame_particles")
end;
---Exスキルフレームのパーティクルのインスタンスを生成して返す。
---@param self ExSkillFrameParticleManager
---@param pos Vector2 パーティクルをスポーンさせる画面上の座標
---@param velocity Vector2 パーティクルの速度
---@return ExSkillFrameParticle instance 生成したインスタンス
getObject = function (self, pos, velocity)
return ExSkillFrameParticle.new(self.parent, pos, velocity, math.random() > 0.9999 and "FIGURA" or "NORMAL")
end;
---Exスキルフレームのパーティクルをスポーンさせる。
---@param self ExSkillFrameParticleManager
---@param pos Vector2 パーティクルをスポーンさせる画面上の座標
---@param velocity Vector2 パーティクルの速度
spawn = function (self, pos, velocity)
SpawnObjectManager.spawn(self, pos, velocity)
end;
}

View file

@ -0,0 +1,88 @@
---@class (exact) FaceParts : AvatarModule 目と口のテクスチャを管理するクラス
---@field package emotionCount integer エモートの時間を計るカウンター
---@field public blinkCount integer 瞬きのタイミングを計るカウンター
---@field public setEmotion fun(self: FaceParts, rightEye: BlueArchiveCharacter.RightEyeTextures, leftEye: BlueArchiveCharacter.LeftEyeTextures, mouth: BlueArchiveCharacter.MouthTextures, duration: integer, forced?: boolean) 表情を設定する
---@field public resetEmotion fun(self: FaceParts) 表情をリセットする
FaceParts = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return FaceParts
new = function (parent)
---@type FaceParts
local instance = Avatar.instantiate(FaceParts, AvatarModule, parent)
instance.emotionCount = 0
instance.blinkCount = 200
return instance
end;
---初期化関数
---@param self FaceParts
init = function (self)
AvatarModule.init(self)
events.TICK:register(function ()
if not client:isPaused() then
if self.parent.playerUtils.damageStatus == "DAMAGE" then
if self.parent.characterData.faceParts.emotionSet ~= nil and self.parent.characterData.faceParts.emotionSet.onDamage ~= nil then
self:setEmotion(self.parent.characterData.faceParts.emotionSet.onDamage.rightEye, self.parent.characterData.faceParts.emotionSet.onDamage.leftEye, self.parent.characterData.faceParts.emotionSet.onDamage.mouth, 8, true)
else
self:setEmotion("SURPRISED", "SURPRISED", "NORMAL", 8, true)
end
elseif self.parent.playerUtils.damageStatus == "DIED" then
if self.parent.characterData.faceParts.emotionSet ~= nil and self.parent.characterData.faceParts.emotionSet.onDied ~= nil then
self:setEmotion(self.parent.characterData.faceParts.emotionSet.onDied.rightEye, self.parent.characterData.faceParts.emotionSet.onDied.leftEye, self.parent.characterData.faceParts.emotionSet.onDied.mouth, 20, true)
else
self:setEmotion("SURPRISED", "SURPRISED", "NORMAL", 20, true)
end
elseif self.emotionCount == 0 then
self:setEmotion("NORMAL", "NORMAL", "NORMAL", 0)
end
if self.blinkCount == 0 then
self:setEmotion("CLOSED", "CLOSED", "NORMAL", 2)
self.blinkCount = 200
else
self.blinkCount = self.blinkCount - 1
end
self.emotionCount = self.emotionCount > 0 and self.emotionCount - 1 or self.emotionCount
end
end)
end;
---表情を設定する。
---@param self FaceParts
---@param rightEye BlueArchiveCharacter.RightEyeTextures 設定する右目の名前
---@param leftEye BlueArchiveCharacter.LeftEyeTextures 設定する左目の名前
---@param mouth BlueArchiveCharacter.MouthTextures 設定する口の名前
---@param duration integer この表情を有効にする時間
---@param forced? boolean trueにすると以前のエモーションが再生中でも強制的に現在のエモーションを適用させる。
setEmotion = function (self, rightEye, leftEye, mouth, duration, forced)
if self.emotionCount == 0 or forced then
--右目
models.models.main.Avatar.Head.FaceParts.Eyes.EyeLeft:setUVPixels(self.parent.characterData.faceParts.rightEye[rightEye]:copy():scale(6))
--左目
models.models.main.Avatar.Head.FaceParts.Eyes.EyeRight:setUVPixels(self.parent.characterData.faceParts.leftEye[leftEye]:copy():scale(6))
--口
if mouth ~= "NORMAL" then
models.models.main.Avatar.Head.FaceParts.Mouth:setVisible(true)
models.models.main.Avatar.Head.FaceParts.Mouth:setUVPixels(self.parent.characterData.faceParts.mouth[mouth]:copy():mul(16, 8))
else
models.models.main.Avatar.Head.FaceParts.Mouth:setVisible(false)
end
self.emotionCount = duration
end
end;
---表情をリセットする。
---@param self FaceParts
resetEmotion = function (self)
self.emotionCount = 0
end;
}

View file

@ -0,0 +1,376 @@
---@alias Gun.GunPosition
---| "NONE" # 構えていない
---| "RIGHT" # 右手に構える
---| "LEFT" # 左手に構える
---@alias Gun.HandDirection
---| "RIGHT" # 右手
---| "LEFT" # 左手
---@class (exact) Gun : AvatarModule 生徒の銃を制御するクラス
---@field public gunItems Minecraft.itemID[] 銃のモデルを適用するアイテム
---@field public shouldShowWeaponInFirstPerson boolean 一人称で武器(銃を含む)のモデルを表示するかどうか
---@field public currentGunPosition Gun.GunPosition 現在の銃の位置
---@field package heldItemsPrev Gun.HeldItemSet 前ティックの手持ちアイテム
---@field public isLeftHandedPrev boolean 前ティックに左利きだったかどうか
---@field public isGunTickProcessed boolean このティック内で銃ティックを処理したかどうか
---@field package setBodyGunPos fun(self: Gun) 背中の銃の位置・向きを設定する
---@field public setGunPosition fun(self: Gun, gonPosition: Gun.GunPosition) 銃の位置を変更する
---@field public processGunTick fun(self: Gun) 銃ティックを処理する
---@class (exact) Gun.HeldItemSet 手持ちアイテムを示すデータセット
---@field public mainHand ItemStack メインハンド
---@field public offHand ItemStack オフハンド
Gun = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Gun
new = function (parent)
---@type Gun
local instance = Avatar.instantiate(Gun, AvatarModule, parent)
instance.gunItems = {"minecraft:bow", "minecraft:crossbow"}
instance.shouldShowWeaponInFirstPerson = instance.parent.config:loadConfig("PRIVATE", "firstPersonWeapon", true)
instance.currentGunPosition = "NONE"
instance.heldItemsPrev = {
mainHand = world.newItem(instance.parent.compatibilityUtils:checkItem("minecraft:air"));
offHand = world.newItem(instance.parent.compatibilityUtils:checkItem("minecraft:air"));
}
instance.isLeftHandedPrev = player:isLeftHanded()
instance.isGunTickProcessed = false
return instance
end;
---初期化関数
---@param self Gun
init = function (self)
AvatarModule.init(self)
events.TICK:register(function ()
self:processGunTick()
self.isGunTickProcessed = false
end)
events.ON_PLAY_SOUND:register(function (id, pos, _, _, _, _, path)
if path ~= nil then
local velocityDistance = player:getVelocity():length()
local distanceFromSound = math.abs(pos:copy():sub(player:getPos()):length() - velocityDistance)
if (id == "minecraft:entity.arrow.shoot" or id == "minecraft:item.crossbow.loading_end" or id == "minecraft:item.crossbow.shoot") and math.abs(velocityDistance - distanceFromSound) < 1 then
if id == "minecraft:item.crossbow.loading_end" then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.dispenser.fail"), pos, 1, 2)
elseif player:isUnderwater() then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:entity.generic.extinguish_fire"), pos, 0.5, 1.5)
else
local particleAnchor = self.parent.modelUtils.getModelWorldPos(renderer:isFirstPerson() and (Gun.CurrentGunPosition == "RIGHT" and models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.RightItemPivot or models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.LeftItemPivot) or models.models.main.Avatar.UpperBody.Body.Gun.MuzzleAnchor)
for _ = 1, 5 do
particles:newParticle(self.parent.compatibilityUtils:checkParticle("minecraft:smoke"), particleAnchor)
end
sounds:playSound(self.parent.compatibilityUtils:checkSound(self.parent.characterData.gun.sound.name), pos, 1, self.parent.characterData.gun.sound.pitch)
end
return true
elseif (id == "minecraft:item.crossbow.loading_start" or id == "minecraft:item.crossbow.loading_middle" or id:match("^minecraft:item%.crossbow%.quick_charge_[1-3]$") ~= nil) and distanceFromSound < 1 and player:getActiveItem().id == "minecraft:crossbow" then
local activeItemTime = player:getActiveItemTime()
local quickChargeLevel = 0
local activeItem = player:getActiveItem()
if activeItem.tag.Enchantments ~= nil then
for _, enchant in ipairs(activeItem.tag.Enchantments) do
if enchant.id == "minecraft:quick_charge" then
quickChargeLevel = enchant.lvl
break
end
end
end
if (quickChargeLevel <= 4 and activeItemTime + quickChargeLevel >= 4 and activeItemTime + quickChargeLevel <= 6) or (quickChargeLevel == 5 and activeItemTime <= 2) then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:item.flintandsteel.use"), pos, 1, 2)
return true
elseif id == "minecraft:item.crossbow.loading_middle" then
return true
end
end
end
end)
local this = self --Figuraにスクリプトを再構築させると参照がおかしくなることに対処しているコード
events.ITEM_RENDER:register(function (item, mode, _, _, _, leftHanded)
self = this
if mode ~= "HEAD" and self.currentGunPosition == (leftHanded and "LEFT" or "RIGHT") and (self.shouldShowWeaponInFirstPerson or mode =="THIRD_PERSON_LEFT_HAND" or mode == "THIRD_PERSON_RIGHT_HAND") then
for _, gunItem in ipairs(self.gunItems) do
if item.id == gunItem then
if leftHanded then
if mode == "FIRST_PERSON_LEFT_HAND" then
local offsetPos = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.firstPersonPos ~= nil and self.parent.characterData.gun.gunPosition.hold.firstPersonPos.left ~= nil then
offsetPos = self.parent.characterData.gun.gunPosition.hold.firstPersonPos.left
end
local offsetRot = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.firstPersonRot ~= nil and self.parent.characterData.gun.gunPosition.hold.firstPersonRot.left ~= nil then
offsetRot = self.parent.characterData.gun.gunPosition.hold.firstPersonRot.left
end
local activeItemId = player:getActiveItem().id
if activeItemId == "minecraft:bow" then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -2.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(20, -7.5, -5):add(offsetRot))
elseif activeItemId == "minecraft:crossbow" then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, 0.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
elseif item.id == "minecraft:crossbow" and item.tag.Charged == 1 then
if player:isLeftHanded() then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(-10, -1.25, 6):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 10, 0):add(offsetRot))
else
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -1.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
end
else
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -1.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
end
elseif mode == "THIRD_PERSON_LEFT_HAND" then
local offsetPos = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.thirdPersonPos ~= nil and self.parent.characterData.gun.gunPosition.hold.thirdPersonPos.left ~= nil then
offsetPos = self.parent.characterData.gun.gunPosition.hold.thirdPersonPos.left
end
local offsetRot = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.thirdPersonRot ~= nil and self.parent.characterData.gun.gunPosition.hold.thirdPersonRot.left ~= nil then
offsetRot = self.parent.characterData.gun.gunPosition.hold.thirdPersonRot.left
end
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -4.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
end
else
if mode == "FIRST_PERSON_RIGHT_HAND" then
local offsetPos = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.firstPersonPos ~= nil and self.parent.characterData.gun.gunPosition.hold.firstPersonPos.right ~= nil then
offsetPos = self.parent.characterData.gun.gunPosition.hold.firstPersonPos.right
end
local offsetRot = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.firstPersonRot ~= nil and self.parent.characterData.gun.gunPosition.hold.firstPersonRot.right ~= nil then
offsetRot = self.parent.characterData.gun.gunPosition.hold.firstPersonRot.right
end
local activeItemId = player:getActiveItem().id
if activeItemId == "minecraft:bow" then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -2.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(20, 7.5, 5):add(offsetRot))
elseif activeItemId == "minecraft:crossbow" then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, 0.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
elseif item.id == "minecraft:crossbow" and item.tag.Charged == 1 then
if player:isLeftHanded() then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -1.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
else
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(10, -1.25, 6):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, -10, 0):add(offsetRot))
end
else
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -1.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
end
elseif mode == "THIRD_PERSON_RIGHT_HAND" then
local offsetPos = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.thirdPersonPos ~= nil and self.parent.characterData.gun.gunPosition.hold.thirdPersonPos.right ~= nil then
offsetPos = self.parent.characterData.gun.gunPosition.hold.thirdPersonPos.right
end
local offsetRot = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.thirdPersonRot ~= nil and self.parent.characterData.gun.gunPosition.hold.thirdPersonRot.right ~= nil then
offsetRot = self.parent.characterData.gun.gunPosition.hold.thirdPersonRot.right
end
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -4.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
end
end
return models.models.main.Avatar.UpperBody.Body.Gun
end
end
end
end)
models.models.main.Avatar.UpperBody.Body.Gun:setScale(vectors.vec3(1, 1, 1):scale(self.parent.characterData.gun.scale))
self:setGunPosition("NONE")
if self.parent.characterData.gun.callbacks ~= nil and self.parent.characterData.gun.callbacks.onMainHandChange ~= nil then
self.parent.characterData.gun.callbacks.onMainHandChange(self.parent.characterData, self.isLeftHandedPrev and "LEFT" or "RIGHT")
end
end;
---背中の銃の位置・向きを設定する。
---@param self Gun
setBodyGunPos = function (self)
if models.models.main.Avatar.UpperBody.Body.Gun ~= nil then
local offsetPos = vectors.vec3()
local offsetRot = vectors.vec3()
if player:isLeftHanded() then
if self.parent.characterData.gun.gunPosition.put.pos ~= nil and self.parent.characterData.gun.gunPosition.put.pos.left ~= nil then
offsetPos = self.parent.characterData.gun.gunPosition.put.pos.left
end
if self.parent.characterData.gun.gunPosition.put.rot ~= nil and self.parent.characterData.gun.gunPosition.put.rot.left ~= nil then
offsetRot = self.parent.characterData.gun.gunPosition.put.rot.left
end
else
if self.parent.characterData.gun.gunPosition.put.pos ~= nil and self.parent.characterData.gun.gunPosition.put.pos.right ~= nil then
offsetPos = self.parent.characterData.gun.gunPosition.put.pos.right
end
if self.parent.characterData.gun.gunPosition.put.rot ~= nil and self.parent.characterData.gun.gunPosition.put.rot.right ~= nil then
offsetRot = self.parent.characterData.gun.gunPosition.put.rot.right
end
end
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, 12, 0):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(offsetRot)
end
end;
---銃の位置を変更する。
---@param self Gun
---@param gunPosition Gun.GunPosition 変更先の構え位置
setGunPosition = function (self, gunPosition)
if gunPosition == "NONE" then
for _, tickName in ipairs({"right_gun_tick", "left_gun_tick"}) do
events.TICK:remove(tickName)
end
models.models.main.Avatar.UpperBody.Body.Gun:setParentType("None")
models.models.main.Avatar.UpperBody.Body.Gun:setSecondaryRenderType("NONE")
if self.parent.characterData.gun.gunPosition.hold.type == "NORMAL" then
self.parent.arms:setArmState(0, 0)
elseif self.parent.characterData.gun.gunPosition.hold.type == "CUSTOM" then
for _, animationName in ipairs({"gun_hold_right", "gun_hold_left"}) do
animations["models.main"][animationName]:stop()
end
end
if self.parent.characterData.gun.gunPosition.put.type == "BODY" then
self:setBodyGunPos()
elseif self.parent.characterData.gun.gunPosition.put.type == "HIDDEN" then
models.models.main.Avatar.UpperBody.Body.Gun:setVisible(false)
end
elseif gunPosition == "RIGHT" then
events.TICK:remove("left_gun_tick")
if events.TICK:getRegisteredCount("right_gun_tick") == 0 then
events.TICK:register(function ()
local heldItem = player:getHeldItem(player:isLeftHanded())
local hasGlint = false
for _, gunItem in ipairs(self.gunItems) do
if gunItem == heldItem.id and heldItem:hasGlint() then
models.models.main.Avatar.UpperBody.Body.Gun:setSecondaryRenderType("GLINT")
hasGlint = true
break
end
end
if not hasGlint then
models.models.main.Avatar.UpperBody.Body.Gun:setSecondaryRenderType("NONE")
end
end, "right_gun_tick")
end
if self.parent.characterData.gun.gunPosition.hold.type == "NORMAL" then
self.parent.arms:setArmState(1, 2)
elseif self.parent.characterData.gun.gunPosition.hold.type == "CUSTOM" then
animations["models.main"]["gun_hold_left"]:stop()
animations["models.main"]["gun_hold_right"]:play()
end
models.models.main.Avatar.UpperBody.Body.Gun:setVisible(true)
models.models.main.Avatar.UpperBody.Body.Gun:setParentType("Item")
if not client:isPaused() then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:item.flintandsteel.use"), player:getPos(), 1, 2)
end
elseif gunPosition == "LEFT" then
events.TICK:remove("right_gun_tick")
if events.TICK:getRegisteredCount("left_gun_tick") == 0 then
events.TICK:register(function ()
local heldItem = player:getHeldItem(not player:isLeftHanded())
local hasGlint = false
for _, gunItem in ipairs(self.gunItems) do
if gunItem == heldItem.id and heldItem:hasGlint() then
models.models.main.Avatar.UpperBody.Body.Gun:setSecondaryRenderType("GLINT")
hasGlint = true
break
end
end
if not hasGlint then
models.models.main.Avatar.UpperBody.Body.Gun:setSecondaryRenderType("NONE")
end
end, "left_gun_tick")
end
if self.parent.characterData.gun.gunPosition.hold.type == "NORMAL" then
self.parent.arms:setArmState(2, 1)
elseif self.parent.characterData.gun.gunPosition.hold.type == "CUSTOM" then
animations["models.main"]["gun_hold_right"]:stop()
animations["models.main"]["gun_hold_left"]:play()
end
models.models.main.Avatar.UpperBody.Body.Gun:setVisible(true)
models.models.main.Avatar.UpperBody.Body.Gun:setParentType("Item")
if not client:isPaused() then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:item.flintandsteel.use"), player:getPos(), 1, 2)
end
end
self.currentGunPosition = gunPosition
end;
---銃ティックを処理する。
---@param self Gun
processGunTick = function (self)
if not self.isGunTickProcessed then
local heldItems = {}
if player:getPose() ~= "SLEEPING" and self.parent.exSkill.animationCount == -1 then
heldItems.mainHand = player:getHeldItem(false)
heldItems.offHand = player:getHeldItem(true)
else
heldItems.mainHand = world.newItem(self.parent.compatibilityUtils:checkItem("minecraft:air"))
heldItems.offHand = world.newItem(self.parent.compatibilityUtils:checkItem("minecraft:air"))
end
local targetItemFound = false
local isLeftHanded = player:isLeftHanded()
if heldItems.mainHand.id ~= self.heldItemsPrev.mainHand.id or heldItems.offHand.id ~= self.heldItemsPrev.offHand.id or isLeftHanded ~= self.isLeftHandedPrev then
for _, gunItem in ipairs(self.gunItems) do
if heldItems.mainHand.id == gunItem then
--メインハンドに対象アイテムを持つ
targetItemFound = true
if isLeftHanded then
if self.currentGunPosition ~= "LEFT" then
self:setGunPosition("LEFT")
end
else
if self.currentGunPosition ~= "RIGHT" then
self:setGunPosition("RIGHT")
end
end
break
end
end
if not targetItemFound then
for _, gunItem in ipairs(self.gunItems) do
if heldItems.offHand.id == gunItem then
--オフハンドに対象アイテムを持つ
targetItemFound = true
if isLeftHanded then
if self.currentGunPosition ~= "RIGHT" then
self:setGunPosition("RIGHT")
end
else
if self.currentGunPosition ~= "LEFT" then
self:setGunPosition("LEFT")
end
end
break
end
end
end
if not targetItemFound then
--対象アイテムは持たない
if self.currentGunPosition ~= "NONE" then
self:setGunPosition("NONE")
end
end
if isLeftHanded ~= self.leftHandedPrev then
if self.currentGunPosition == "NONE" then
self:setBodyGunPos()
end
if self.parent.characterData.gun.callbacks ~= nil and self.parent.characterData.gun.callbacks.onMainHandChange ~= nil then
self.parent.characterData.gun.callbacks.onMainHandChange(self.parent.characterData, isLeftHanded and "LEFT" or "RIGHT")
end
self.isLeftHandedPrev = isLeftHanded
end
self.heldItemsPrev = heldItems
end
self.isGunTickProcessed = true
end
end;
}

View file

@ -0,0 +1,33 @@
---@class (exact) HeadBlock : HeadModelGenerator 頭ブロックのモデルを制御するクラス
---@field package forceGenerateCount integer 強制的に頭ブロックを生成するまでのカウンター。これが発火するのはアバタープレイヤーがオフラインの時のみ。
HeadBlock = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return HeadBlock
new = function (parent)
---@type HeadBlock
local instance = Avatar.instantiate(HeadBlock, HeadModelGenerator, parent)
instance.processData = instance.parent.characterData.headBlock
instance.parentName = "head_block"
instance.parentType = "Skull"
instance.forceGenerateCount = 2
return instance
end;
---初期化関数
---@param self HeadBlock
init = function (self)
HeadModelGenerator.init(self)
events.WORLD_TICK:register(function ()
self.forceGenerateCount = self.forceGenerateCount - 1
if self.forceGenerateCount == 0 then
self:generateHeadModel()
events.WORLD_TICK:remove("head_block_world_tick")
end
end, "head_block_world_tick")
end;
}

View file

@ -0,0 +1,85 @@
---@class (exact) HeadModelGenerator : AvatarModule 頭のモデルの生成を含む抽象クラス
---@field public processData BlueArchiveCharacter.HeadBlockStruct 頭モデルを生成する過程で参照するデータ
---@field public parentName string モデルを生成する名前空間
---@field public parentType ModelPart.parentType コピーした頭モデルに適用する親タイプ
---@field public generateHeadModel fun(self: HeadModelGenerator) 頭モデルのコピーを生成する
---@field package isScriptLoaded boolean スクリプトが全てロードされているかどうか
HeadModelGenerator = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return HeadModelGenerator
new = function (parent)
---@type HeadModelGenerator
local instance = Avatar.instantiate(HeadModelGenerator, AvatarModule, parent)
instance.isScriptLoaded = false
return instance
end;
---初期化関数
---@param self HeadModelGenerator
init = function (self)
AvatarModule.init(self)
---@diagnostic disable-next-line: discard-returns
models:newPart("script_"..self.parentName, "None")
self.parent.avatarEvents.SCRIPT_INIT:register(function ()
self.isScriptLoaded = true
end)
end;
---頭モデルのコピーを生成する。
---@param self HeadModelGenerator
generateHeadModel = function (self)
--既存の頭ブロックのモデルを削除する。
if models["script_"..self.parentName].Head ~= nil then
models["script_"..self.parentName].Head:remove()
end
--ヘルメットを着けている場合は外しておく。
local isHelmetVisible = self.isScriptLoaded and self.parent.armor.isArmorVisible.helmet or false
if isHelmetVisible then
self.parent.armor:setHelmet(world.newItem(self.parent.compatibilityUtils:checkItem("minecraft:air")))
end
if self.isScriptLoaded then
self.parent.physics:disable()
end
if self.processData.callbacks ~= nil and self.processData.callbacks.onBeforeModelCopy ~= nil then
self.processData.callbacks.onBeforeModelCopy(self.parent.characterData, self.isScriptLoaded)
end
--現在の衣装を基に新たな頭ブロックのモデルを生成する。
local copiedPart = self.parent.modelUtils:copyModel(models.models.main.Avatar.Head)
if copiedPart ~= nil then
models["script_"..self.parentName]:addChild(copiedPart)
models["script_"..self.parentName].Head:setParentType(self.parentType)
models["script_"..self.parentName].Head:setPos(0, -24, 0)
models["script_"..self.parentName].Head.HeadRing:setRot(self.parent.headRing.initialHaloRot)
models["script_"..self.parentName].Head.HeadRing:setLight(15)
for _, modelPart in ipairs({models["script_"..self.parentName].Head.FaceParts.Eyes.EyeRight, models["script_"..self.parentName].Head.FaceParts.Eyes.EyeLeft}) do
modelPart:setUVPixels()
end
for _, modelPart in ipairs(self.processData.includeModels) do
local copiedIncludePart = self.parent.modelUtils:copyModel(modelPart)
if copiedIncludePart ~= nil then
models["script_"..self.parentName].Head:addChild(copiedIncludePart)
end
end
end
--非表示にしたモデルを元に戻す。
if isHelmetVisible then
self.parent.armor:setHelmet(self.parent.armor.armorSlotItems[1])
end
if self.isScriptLoaded then
self.parent.physics:enable()
end
if self.processData.callbacks ~= nil and self.processData.callbacks.onAfterModelCopy ~= nil then
self.processData.callbacks.onAfterModelCopy(self.parent.characterData, self.isScriptLoaded)
end
end;
}

View file

@ -0,0 +1,79 @@
---@class (exact) HeadRing : AvatarModule ヘイロー(頭の輪っか)を制御するクラス
---@field public initialHaloRot number ヘイローの初期角度
---@field package headRotData number[] 一定期間内の頭の角度を保持するテーブル
---@field package headRotAverage number[] 頭の角度の移動平均値
---@field package floatCount integer ヘイローが上下するアニメーションのカウンター
HeadRing = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return HeadRing
new = function (parent)
---@type HeadRing
local instance = Avatar.instantiate(HeadRing, AvatarModule, parent)
instance.initialHaloRot = models.models.main.Avatar.Head.HeadRing:getRot().x
instance.haloRotPrev = instance.initialHaloRot
instance.headRotData = {}
instance.headRotAverage = {0, 0}
instance.floatCount = 0
return instance
end;
---初期化関数
---@param self HeadRing
init = function (self)
AvatarModule.init(self)
events.TICK:register(function ()
if not client:isPaused() then
--移動平均値の算出
local headRot = self.parent.exSkill.animationCount > -1 and models.models.main.Avatar.Head:getAnimRot().x or math.deg(math.asin(player:getLookDir().y))
local headRotAverage = self.headRotAverage[2]
headRotAverage = (#self.headRotData * headRotAverage + headRot) / (#self.headRotData + 1)
table.insert(self.headRotData, headRot)
--古いデータの切り捨て
if #self.headRotData > 5 then
headRotAverage = (#self.headRotData * headRotAverage - self.headRotData[1]) / (#self.headRotData - 1)
table.remove(self.headRotData, 1)
end
table.insert(self.headRotAverage, headRotAverage)
table.remove(self.headRotAverage, 1)
end
end)
events.WORLD_TICK:register(function ()
if not client:isPaused() then
self.floatCount = self.floatCount + 1
self.floatCount = self.floatCount == 80 and 0 or self.floatCount
end
end)
events.RENDER:register(function (delta)
if not client:isPaused() then
--ヘイローの位置・角度を設定
local playerPose = player:getPose()
local headRot = self.parent.physics.getValueBetweenTicks(self.headRotAverage, delta)
local floatOffset = math.sin((self.floatCount + delta) / 80 * 2 * math.pi) * 0.25
if playerPose == "SWIMMING" or playerPose == "FALL_FLYING" then
models.models.main.Avatar.Head.HeadRing:setPos(self.parent.physics.velocityAverage[3][2] * 3, math.cos(math.rad(headRot)) * self.parent.physics.velocityAverage[1][2] * -1 + math.sin(math.rad(headRot)) * self.parent.physics.velocityAverage[2][2] * -1 + floatOffset, math.cos(math.rad(headRot)) * self.parent.physics.velocityAverage[2][2] * -3 + math.sin(math.rad(headRot)) * self.parent.physics.velocityAverage[1][2])
else
models.models.main.Avatar.Head.HeadRing:setPos(self.parent.physics.velocityAverage[3][2] * -3, math.cos(math.rad(headRot)) * self.parent.physics.velocityAverage[2][2] * -1 + math.sin(math.rad(headRot)) * self.parent.physics.velocityAverage[1][2] + floatOffset, math.cos(math.rad(headRot)) * self.parent.physics.velocityAverage[1][2] * 3 + math.sin(math.rad(headRot)) * self.parent.physics.velocityAverage[2][2])
end
if self.parent.deathAnimation.dummyAvatarRoot ~= nil then
self.parent.deathAnimation.dummyAvatarRoot.Head.HeadRing:setPos(0, floatOffset, 0)
end
models.models.main.Avatar.Head.HeadRing:setRot(headRot - (self.parent.exSkill.animationCount > -1 and models.models.main.Avatar.Head:getAnimRot().x or math.deg(math.asin(player:getLookDir().y))) + self.initialHaloRot, 0, 0)
end
end)
events.WORLD_RENDER:register(function (delta)
if not client:isPaused() and models.script_head_block.Head ~= nil and models.script_head_block.Head.HeadRing ~= nil then
models.script_head_block.Head.HeadRing:setPos(0, math.sin((self.floatCount + delta) / 80 * 2 * math.pi) * 0.25, 0)
end
end)
models.models.main.Avatar.Head.HeadRing:setLight(15)
end;
}

View file

@ -0,0 +1,85 @@
---@class (exact) HypixelZombies : AvatarModule Hypixelの「Zombies」にアバターを対応させるパッチ
---@field package isEnabled boolean Zombiesモードが有効かどうか
---@field package damagerPercentPrev number[] 前ティック以前のツールの耐久度割合
---@field public enable fun(self: HypixelZombies) Zombiesモードを有効にする
---@field public disable fun(self: HypixelZombies) Zombiesモードを無効にする
HypixelZombies = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return HypixelZombies
new = function (parent)
---@type HypixelZombies
local instance = Avatar.instantiate(HypixelZombies, AvatarModule, parent)
instance.isEnabled = false
instance.damagerPercentPrev = {1, 1}
return instance
end;
---Zombiesモードを有効にする。
---@param self HypixelZombies
enable = function (self)
if not self.isEnabled then
self.parent.gun.gunItems = {"minecraft:bow", "minecraft:crossbow", "minecraft:wooden_hoe", "minecraft:stone_hoe", "minecraft:iron_hoe", "minecraft:wooden_shovel", "minecraft:stone_shovel", "minecraft:shears", "minecraft:diamond_hoe", "minecraft:golden_hoe", "minecraft:iron_shovel", "minecraft:diamond_pickaxe", "minecraft:golden_pickaxe", "minecraft:golden_shovel", "minecraft:flint_and_steel"}
events.TICK:register(function ()
local heldItem = player:getHeldItem()
local maxDamage = heldItem:getMaxDamage()
local damagePercent = (maxDamage - heldItem:getDamage()) / maxDamage
if maxDamage > 0 then
if damagePercent - self.damagerPercentPrev[1] > 0 and damagePercent - self.damagerPercentPrev[1] <= 0.2 then
if self.parent.bubble.bubbleCount == 0 then
self.parent.bubble:play("RELOAD", -1, vectors.vec2(), 0, false)
end
elseif self.parent.bubble.emoji == "RELOAD" then
self.parent.bubble:stop()
end
table.insert(self.damagerPercentPrev, damagePercent)
else
if self.parent.bubble.emoji == "RELOAD" then
self.parent.bubble:stop()
end
table.insert(self.damagerPercentPrev, 1)
end
table.remove(self.damagerPercentPrev, 1)
end, "hypixel_zombies_tick")
if host:isHost() then
print(self.parent.locale:getLocale("avatar.zombies_mode.enable"))
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:entity.lightning_bolt.thunder"), player:getPos(), 0.5, 2)
end
self.isEnabled = true
elseif host:isHost() then
print(self.parent.locale:getLocale("avatar.zombies_mode.already_enabled"))
end
end;
---Zombiesモードを無効にする。
---@param self HypixelZombies
disable = function (self)
if self.isEnabled then
events.TICK:remove("hypixel_zombies_tick")
self.parent.gun.gunItems = {"minecraft:bow", "minecraft:crossbow"}
if host:isHost() then
print(self.parent.locale:getLocale("avatar.zombies_mode.disable"))
end
elseif host:isHost() then
print(self.parent.locale:getLocale("avatar.zombies_mode.already_disabled"))
end
self.isEnabled = false
end
}
---Zombiesモードを有効にする。
function pings.enableZombiesMode()
AvatarInstance.hypixelZombies:enable()
end
---Zombiesモードを無効にする。
function pings.disableZombiesMode()
AvatarInstance.hypixelZombies:disable()
end

View file

@ -0,0 +1,45 @@
---@class (exact) KeyManager : AvatarModule アバターのキー割り当てを管理するクラス。ここで管理する割り当ては設定で変更された場合はそれが保存される。
---@field public keyMappings {[string]: Keybind} キーの割り当てのテーブル
---@field public register fun(self: KeyManager, assignName: string, keyName: Minecraft.keyCode): Keybind キー割り当てを登録する
KeyManager = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return KeyManager
new = function (parent)
---@type KeyManager
local instance = Avatar.instantiate(KeyManager, AvatarModule, parent)
instance.keyMappings = {}
return instance
end;
---初期化関数
---@param self KeyManager
init = function (self)
AvatarModule.init(self)
if host:isHost() then
events.TICK:register(function ()
for key, value in pairs(self.keyMappings) do
if not value:isDefault() then
local newKey = value:getKey()
self.parent.config:saveConfig("PRIVATE", "keybind."..key, newKey)
value:setKey(newKey)
end
end
end)
end
end;
---キー割り当てを登録する。
---@param self KeyManager
---@param assignName string 割り当ての名前
---@param keyName Minecraft.keyCode 割当先のキー
---@return Keybind assignedKey キーマネージャーによって登録がされたキーバインド
register = function (self, assignName, keyName)
self.keyMappings[assignName] = keybinds:newKeybind(self.parent.locale:getLocale("key_name."..assignName), self.parent.config:loadConfig("PRIVATE", "keybind."..assignName, keyName))
return self.keyMappings[assignName]
end;
}

View file

@ -0,0 +1,118 @@
---@alias Locale.Language
---| "en_us" # 英語(米国)
---| "ja_jp" # 日本語
---@class (exact) Locale : AvatarModule アバターの表示言語を管理するクラス
---@field public localeData {[Locale.Language]: {[string]: string}} 言語データ
---@field public getLocale fun(self: Locale, key: string): string 翻訳キーに対する訳文を返す。設定言語が存在しない場合は英語の文が返される。また、指定したキーの訳が無い場合は英語->キーそのままが返される。
Locale = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Locale
new = function (parent)
---@type Locale
local instance = Avatar.instantiate(Locale, AvatarModule, parent)
instance.localeData = {
en_us = {};
ja_jp = {};
}
---言語データのリスト
---1. キー名, 2. 英語(米国), 3. 日本語
local localeStrings = {
{"avatar.old_version_warning", "For the best experience, playing with 1.20.1 or higher is recommended!", "生徒さんとより良い時間を過ごすためにバージョン1.20.1以上でのプレイをおすすめします!"};
{"avatar.zombies_mode.enable", "Enabled Zombies mode.", "Zombiesモードを有効にしました。"};
{"avatar.zombies_mode.already_enabled", "Already enabled Zombies mode.", "Zombiesモードは既に有効化されています。"};
{"avatar.zombies_mode.disable", "Disabled Zombies mode.", "Zombiesモードを無効にしました。"};
{"avatar.zombies_mode.already_disabled", "Already disabled Zombies mode.", "Zombiesモードは既に無効化されています。"};
{"action_wheel.toggle_off", "off", "オフ"};
{"action_wheel.toggle_on", "on", "オン"};
{"action_wheel.main.action_1.title", "Change costume: ", "衣装を変更:"};
{"action_wheel.main.action_1.unavailable", "There is no costume available.", "利用可能な衣装はありません。"};
{"action_wheel.main.action_1.done_first", "Changed costume to §b", "衣装を§b"};
{"action_wheel.main.action_1.done_last", "§r.", "§rに変更しました。"};
{"action_wheel.main.action_2.title", "Change display name: ", "表示名を変更:"};
{"action_wheel.main.action_2.title_2", "Show club name: ", "部活名を表示:"};
{"action_wheel.main.action_2.done_first", "Changed display name to §b", "表示名を§b"};
{"action_wheel.main.action_2.done_last", "§r.", "§rに変更しました。"};
{"action_wheel.main.action_3.title", "Show armors: ", "防具を表示:"};
{"action_wheel.main.action_4.title", "Show weapon models in first person: ", "一人称視点で武器モデルを表示:"};
{"action_wheel.main.action_5.title", "Amount of particles in Ex skill frame: ", "Exスキルフレームのパーティクルの量:"};
{"action_wheel.main.action_5.option_1", "Standard", "標準"};
{"action_wheel.main.action_5.option_2", "Minimum", "少なめ"};
{"action_wheel.main.action_5.option_3", "None", "なし"};
{"action_wheel.main.action_5.option_4", "Hide Ex skill frame", "スキルフレーム非表示"};
{"action_wheel.main.action_5.done_first", "Changed amount of particles in Ex skill frame to§b", "Exスキルフレームのパーティクルの量を§b"};
{"action_wheel.main.action_5.done_last", "§r.", "§rに変更しました。"};
{"action_wheel.main.action_6.title", "Replace vehicle models: ", "乗り物のモデルを置き換え:"};
{"action_wheel.main.action_6.unavailable", "This option is unavailable for this character.", "この生徒さんでは利用できません。"};
{"action_wheel.main.action_7.title_1", "Check for new FBAC Updates ", "FBACアップデートの確認"};
{"action_wheel.main.action_7.title_2", "(Left-click)", "(左クリック)"};
{"action_wheel.main.action_7.title_3", "Copy URL for the latest FBAC ", "最新FBACバージョンのURLをコピー"};
{"action_wheel.main.action_7.title_4", "(Right-click)", "(右クリック)"};
{"action_wheel.main.action_7.networking_api", "To enable update checking, you need to allow Figura's Networking and put \"api.github.com\" in the Network Filter from Figura settings!", "アップデート確認機能を有効にするにはFiguraの設定より、FiguraのNetworkingの使用を許可し、\"api.github.com\"を許可リストに入れる必要があります!"};
{"action_wheel.main.action_7.ongoing", "Checking for updates is ongoing. Please DO NOT click repeatedly!", "現在アップデートの確認中です。連打しないでください!"};
{"action_wheel.main.action_7.copied", "Coped the link to the latest FBAC to your clipboard. Please open the link in your browser.", "最新のFBACへのリンクをクリップボードにコピーしました。ブラウザでリンクを開いてください。"};
{"action_wheel.main.action_7.cannot_check_latest", "Cannot get the link because cannot check the latest FBAC.", "FBACの最新バージョンを確認できないため、リンクを取得できません。"};
{"action_wheel.gui.bubble_guide.title", "§0Bubble emote guide", "§0吹き出しエモートガイド"};
{"action_wheel.gui.ex_skill_guide.title", "§0Ex skill guide", "§0Exスキルガイド"};
{"action_wheel.gui.ex_skill_guide.key_pre", "Press \"", "\""};
{"action_wheel.gui.ex_skill_guide.key_post", "\"key to play", "\"キーで再生"};
{"action_wheel.gui.update_check.checking", "Checking for updates...", "アップデートを確認中..."};
{"action_wheel.gui.update_check.latest", "No FBAC update available", "最新のFBACを使用中です"};
{"action_wheel.gui.update_check.update_available", "New FBAC update is available: ", "FBACのアップデートが利用可能です:"};
{"action_wheel.gui.update_check.error_not_allowed", "Failed to check for updates - Networking API not allowed", "アップデート確認失敗 - ネットワーキング機能が不許可"};
{"action_wheel.gui.update_check.error_network_err", "Failed to check for updates - Network error", "アップデート確認失敗 - ネットワークエラー"};
{"action_wheel.gui.update_check.error_request_failed", "Failed to check for updates - Request failure ", "アップデート確認失敗 - リクエスト失敗 "};
{"action_wheel.gui.update_check.error_invalid_json_syntax", "Failed to check for updates - Json parsing failure", "アップデート確認失敗 - リクエスト解析失敗"};
{"action_wheel.gui.update_check.error_invalid_json", "Failed to check for updates - Unexpected Request", "アップデート確認失敗 - 予期しないリクエスト"};
{"key_name.ex_skill", "Ex Skill", "Exスキル"};
{"key_name.ex_skill_sub", "Sub Ex Skill", "サブExスキル"};
{"key_name.bubble_1", "Bubble: Good", "吹き出し:いいね"};
{"key_name.bubble_2", "Bubble: Heart", "吹き出し:ハート"};
{"key_name.bubble_3", "Bubble: Note", "吹き出し:音符"};
{"key_name.bubble_4", "Bubble: Question", "吹き出し:はてな"};
{"key_name.bubble_5", "Bubble: Sweat", "吹き出し:汗"};
{"key_bind.ex_skill.unavailable", "You cannot do this now.", "今は再生できません。"};
{"key_bind.ex_skill.unavailable_firstperson", "You cannot do this in first person perspective.", "一人称視点では再生できません。"};
}
for _, localeSet in ipairs(localeStrings) do
instance.localeData.en_us[localeSet[1]] = localeSet[2]
instance.localeData.ja_jp[localeSet[1]] = localeSet[3]
end
return instance
end;
---初期化関数
---@param self Locale
init = function (self)
AvatarModule.init(self)
self.localeData.en_us["nameplate.club_name"] = self.parent.characterData.basic.clubName.en_us
self.localeData.ja_jp["nameplate.club_name"] = self.parent.characterData.basic.clubName.ja_jp
if host:isHost() then
for index, exSkill in ipairs(self.parent.characterData.exSkill) do
self.localeData.en_us["action_wheel.gui.ex_skill_guide.ex_skill_"..index..".name"] = exSkill.name.en_us
self.localeData.ja_jp["action_wheel.gui.ex_skill_guide.ex_skill_"..index..".name"] = exSkill.name.ja_jp
end
for _, costume in ipairs(self.parent.characterData.costume.costumes) do
self.localeData.en_us["costume."..costume.name] = costume.displayName.en_us
self.localeData.ja_jp["costume."..costume.name] = costume.displayName.ja_jp
end
end
end;
---翻訳キーに対する訳文を返す。設定言語が存在しない場合は英語の文が返される。また、指定したキーの訳が無い場合は英語->キーそのままが返される。
---@param self Locale
---@param key string 翻訳キー
---@return string translatedString 翻訳キーに対する翻訳データ。設定言語での翻訳が存在しない場合は英文が返される。英文すら存在しない場合は翻訳キーがそのまま返される。
getLocale = function (self, key)
local activeLanguage = client:getActiveLang()
return (self.localeData[activeLanguage] ~= nil and self.localeData[activeLanguage][key] ~= nil) and self.localeData[activeLanguage][key] or (self.localeData.en_us[key] and self.localeData.en_us[key] or key)
end;
}

View file

@ -0,0 +1,77 @@
---@class (exact) Nameplate : AvatarModule プレイヤーの表示名を制御するクラス
---@field public currentName integer 現在の表示名:1. プレイヤー名, 2. 名のみ(英語), 3. 名のみ(日本語), 4. 名性(英語), 5. 性名(英語), 6. 性名(日本語)
---@field public shouldShowClubName boolean 部活名を表示するかどうか
---@field package localePrev string 前ティックの設定言語
---@field public getName fun(self: Nameplate, typeId: integer): string 指定されたtypeIdでの表示名を返す
---@field public setName fun(self: Nameplate, typeId: integer, shouldShowClubName: boolean) 入力された設定で表示名を設定する
Nameplate = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Nameplate
new = function (parent)
---@type Nameplate
local instance = Avatar.instantiate(Nameplate, AvatarModule, parent)
instance.currentName = instance.parent.config:loadConfig("PRIVATE", "name", 1)
instance.shouldShowClubName = instance.parent.config:loadConfig("PRIVATE", "showClubName", false)
instance.localePrev = client:getActiveLang()
return instance
end;
---指定されたtypeIdでの表示名を返す。
---@param self Nameplate
---@param typeId integer 表示名の種類:1. プレイヤー名, 2. 名のみ(英語), 3. 名のみ(日本語), 4. 名性(英語), 5. 性名(英語), 6. 性名(日本語)
---@return string displayName 指定されたtypeIdでの表示名
getName = function (self, typeId)
local displayName = typeId == 1 and player:getName() or ((typeId == 2 or typeId == 4) and self.parent.characterData.basic.firstName.en_us or (typeId == 5 and self.parent.characterData.basic.lastName.en_us or (typeId == 3 and self.parent.characterData.basic.firstName.ja_jp or self.parent.characterData.basic.lastName.ja_jp)))
if typeId >= 4 then
displayName = displayName.." "..(typeId == 4 and self.parent.characterData.basic.lastName.en_us or (typeId == 5 and self.parent.characterData.basic.firstName.en_us or self.parent.characterData.basic.firstName.ja_jp))
end
return displayName
end;
---入力された設定で表示名を設定する。
---@param self Nameplate
---@param typeId integer 表示名の種類:1. プレイヤー名, 2. 名のみ(英語), 3. 名のみ(日本語), 4. 名性(英語), 5. 性名(英語), 6. 性名(日本語)
---@param shouldShowClubName boolean 部活名を表示するかどうか
setName = function (self, typeId, shouldShowClubName)
local date = client:getDate()
local displayName = self:getName(typeId)..((typeId >= 2 and date.month == self.parent.characterData.basic.birth.month and date.day == self.parent.characterData.basic.birth.day) and " :cake:" or "")
nameplate.ALL:setText(displayName)
if typeId >= 2 and shouldShowClubName then
nameplate.ENTITY:setText(displayName.."\n§7"..self.parent.locale:getLocale("nameplate.club_name"))
end
self.currentName = typeId
self.shouldShowClubName = shouldShowClubName
end;
---初期化関数
---@param self Nameplate
init = function (self)
AvatarModule.init(self)
events.TICK:register(function ()
local locale = client:getActiveLang()
if locale ~= self.localePrev then
if self.shouldShowClubName then
self:setName(self.currentName, true)
end
self.localePrev = locale
end
end)
events.RENDER:register(function (delta, context)
if context ~= "PAPERDOLL" then
nameplate.ENTITY:setPivot(self.parent.modelUtils.getModelWorldPos(models.models.main.Avatar.UpperBody.Body.NameplateAnchor):sub(player:getPos(delta)):add(0, self.parent.barrier.isBarrierVisible and 1.095 or 0.895, 0))
else
nameplate.ENTITY:setPivot()
end
end)
if self.currentName >= 2 then
self:setName(self.currentName, self.shouldShowClubName)
end
end;
}

View file

@ -0,0 +1,199 @@
---@class (exact) Physics : AvatarModule 物理演算を制御するクラス
---@field public velocityData number[][] 速度データ:1. 頭前後, 2. 上下, 3. 頭左右, 4. 頭角速度, 5. 体前後, 6. 体左右, 7. 体角速度
---@field public velocityAverage number[][] 速度の平均値:1. 頭前後, 2. 上下, 3. 頭左右, 4. 頭角速度, 5. 体前後, 6. 体左右, 7. 体角速度
---@field package directionPrev number[] 前ティックのdirectionテーブル
---@field public getValueBetweenTicks fun(tickData: number[], delta: number): number 2つのティックデータの間からレンダーのデルタ値を加味した値を返す
---@field package decomposeHorizontalVelocity fun(self: Physics, direction: number, index: integer): number, number, number 速度を指定された方向から見て前後方向、左右方向に分解する
---@field package getPhysicRot fun(self: Physics, physicData: BlueArchiveCharacter.PhysicCoreData, delta: number): number 物理演算で計算した角度を返す
---@field public enable fun(self: Physics) 物理演算を初期化し、有効にする
---@field public disable fun(self: Physics) 物理演算を無効にする。物理演算で管理していたモデルの回転をリセットする
Physics = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Physics
new = function (parent)
---@type Physics
local instance = Avatar.instantiate(Physics, AvatarModule, parent)
instance.velocityData = {{}, {}, {}, {}, {}, {}, {}}
instance.velocityAverage = {{0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}}
instance.directionPrev = {}
return instance
end;
---2つのティックデータの間からレンダーのデルタ値を加味した値を返す。
---@param tickData number[] ティックデータ:1. 前ティック, 2. 現ティック
---@param delta number デルタ値
---@return number deltaValue 2つのティックデータの間からデルタ値で補完した値
getValueBetweenTicks = function (tickData, delta)
return tickData[1] + (tickData[2] - tickData[1]) * delta
end;
---速度を指定された方向から見て前後方向、左右方向に分解する。
---@param self Physics
---@param direction number 基準にする方向
---@param index integer データ管理用のインデックス番号(呼び出しの度に異なるインデックス番号になるようにする)
---@return number velocityFront 指定された方向から見た前後方向の速度
---@return number velocityRight 指定された方向から見た左右方向の速度
---@return number velocityRot 指定された方向を基準とした角速度
decomposeHorizontalVelocity = function (self, direction, index)
local velocity = player:getVelocity()
if self.directionPrev[index] == nil then
self.directionPrev[index] = 0
end
local velocityRot = math.deg(math.atan2(velocity.z, velocity.x))
velocityRot = velocityRot < 0 and 360 + velocityRot or velocityRot
local directionAbsFront = math.abs(velocityRot - (direction) % 360)
directionAbsFront = directionAbsFront > 180 and 360 - directionAbsFront or directionAbsFront
local directionAbsRight = math.abs(velocityRot - (direction + 90) % 360)
directionAbsRight = directionAbsRight > 180 and 360 - directionAbsRight or directionAbsRight
local directionDelta = direction - self.directionPrev[index]
directionDelta = directionDelta > 180 and (360 - directionDelta) * 20 or (directionDelta < -180 and (360 + directionDelta) * 20 or directionDelta * 20)
self.directionPrev[index] = direction
return math.sqrt(velocity.x ^ 2 + velocity.z ^ 2) * math.cos(math.rad(directionAbsFront)), math.sqrt(velocity.x ^ 2 + velocity.z ^ 2) * math.cos(math.rad(directionAbsRight)), directionDelta
end;
---物理演算で計算した角度を返す。
---@param self Physics
---@param physicData BlueArchiveCharacter.PhysicCoreData 物理演算データ
---@param delta number デルタ値
---@return number physicDirection 物理演算で計算したモデルの角度
getPhysicRot = function (self, physicData, delta)
local rot = physicData.neutral
local waterMultiplayer = player:isUnderwater() and 2 or 1
if physicData.headX ~= nil then
rot = rot + math.clamp(self.getValueBetweenTicks(self.velocityAverage[1], delta) * physicData.headX.multiplayer * waterMultiplayer, physicData.headX.min - physicData.neutral, physicData.headX.max - physicData.neutral)
end
if physicData.headZ ~= nil then
rot = rot + math.clamp(self.getValueBetweenTicks(self.velocityAverage[3], delta) * physicData.headZ.multiplayer * waterMultiplayer, physicData.headZ.min - physicData.neutral, physicData.headZ.max - physicData.neutral)
end
if physicData.headRot ~= nil then
rot = rot + math.clamp(math.abs(self.getValueBetweenTicks(self.velocityAverage[4], delta)) * -1 * physicData.headRot.multiplayer, physicData.headRot.min - physicData.neutral, physicData.headRot.max - physicData.neutral)
end
if physicData.bodyX ~= nil then
rot = rot + math.clamp(self.getValueBetweenTicks(self.velocityAverage[5], delta) * physicData.bodyX.multiplayer * waterMultiplayer, physicData.bodyX.min - physicData.neutral, physicData.bodyX.max - physicData.neutral)
end
if physicData.bodyY ~= nil then
rot = rot + math.clamp(self.getValueBetweenTicks(self.velocityAverage[2], delta) * physicData.bodyY.multiplayer * waterMultiplayer, physicData.bodyY.min - physicData.neutral, physicData.bodyY.max - physicData.neutral)
end
if physicData.bodyZ ~= nil then
rot = rot + math.clamp(self.getValueBetweenTicks(self.velocityAverage[player:getVehicle() == nil and 6 or 3], delta) * physicData.bodyZ.multiplayer * waterMultiplayer, physicData.bodyZ.min - physicData.neutral, physicData.bodyZ.max - physicData.neutral)
end
if physicData.bodyRot ~= nil then
rot = rot + math.clamp(-math.abs(self.getValueBetweenTicks(self.velocityAverage[7], delta)) * physicData.bodyRot.multiplayer, physicData.bodyRot.min - physicData.neutral, physicData.bodyRot.max - physicData.neutral)
end
rot = math.clamp(rot, physicData.min, physicData.max)
if physicData.headRotMultiplayer ~= nil then
rot = rot + math.deg(math.asin(player:getLookDir().y)) * physicData.headRotMultiplayer
end
if physicData.sneakOffset ~= nil and player:isCrouching() then
rot = rot + physicData.sneakOffset
end
return rot
end;
---物理演算を初期化し、有効にする。
---@param self Physics
enable = function (self)
self.velocityData = {{}, {}, {}, {}, {}, {}, {}}
self.velocityAverage = {{0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}}
self.directionPrev = {}
if events.TICK:getRegisteredCount("physics_tick") == 0 then
events.TICK:register(function ()
if not client:isPaused() then
local lookDir = player:getLookDir()
local velocityHeadFront, velocityHeadRight, velocityHeadRot = self:decomposeHorizontalVelocity(math.deg(math.atan2(lookDir.z, lookDir.x)), 1)
local velocityAverage = {self.velocityAverage[1][2], self.velocityAverage[2][2], self.velocityAverage[3][2], self.velocityAverage[4][2], self.velocityAverage[5][2], self.velocityAverage[6][2], self.velocityAverage[7][2]}
local velocityY = player:getVelocity().y
velocityAverage[1] = (#self.velocityData[1] * velocityAverage[1] + velocityHeadFront) / (#self.velocityData[1] + 1)
table.insert(self.velocityData[1], velocityHeadFront)
velocityAverage[2] = (#self.velocityData[2] * velocityAverage[2] + velocityY) / (#self.velocityData[2] + 1)
table.insert(self.velocityData[2], velocityY)
velocityAverage[3] = (#self.velocityData[3] * velocityAverage[3] + velocityHeadRight) / (#self.velocityData[3] + 1)
table.insert(self.velocityData[3], velocityHeadRight)
velocityAverage[4] = (#self.velocityData[4] * velocityAverage[4] + velocityHeadRot) / (#self.velocityData[4] + 1)
table.insert(self.velocityData[4], velocityHeadRot)
local velocityBodyFront, velocityBodyRight, velocityBodyRot = self:decomposeHorizontalVelocity((player:getBodyYaw() + models.models.main.Avatar.UpperBody:getTrueRot().y - 90) % 360 - 180, 2)
velocityAverage[5] = (#self.velocityData[5] * velocityAverage[5] + velocityBodyFront) / (#self.velocityData[5] + 1)
table.insert(self.velocityData[5], velocityBodyFront)
velocityAverage[6] = (#self.velocityData[6] * velocityAverage[6] + velocityBodyRight) / (#self.velocityData[6] + 1)
table.insert(self.velocityData[6], velocityBodyRight)
velocityAverage[7] = (#self.velocityData[7] * velocityAverage[7] + velocityBodyRot) / (#self.velocityData[7] + 1)
table.insert(self.velocityData[7], velocityBodyRot)
--古いデータの切り捨て
for index, velocityTable in ipairs(self.velocityData) do
if #velocityTable > 5 then
velocityAverage[index] = (#velocityTable * velocityAverage[index] - velocityTable[1]) / (#velocityTable - 1)
table.remove(velocityTable, 1)
end
table.insert(self.velocityAverage[index], velocityAverage[index])
table.remove(self.velocityAverage[index], 1)
end
end
end, "physics_tick")
events.RENDER:register(function (delta)
local playerPose = player:getPose()
local isHorizontal = playerPose == "SWIMMING" or playerPose == "FALL_FLYING"
for _, physicData in ipairs(self.parent.characterData.physics.physicData) do
for _, modelPart in ipairs(physicData.models) do
if modelPart:getVisible() then
local rot = vectors.vec3()
if physicData.x ~= nil then
if isHorizontal and physicData.x.horizontal then
rot.x = self:getPhysicRot(physicData.x.horizontal, delta)
elseif not isHorizontal and physicData.x.vertical then
rot.x = self:getPhysicRot(physicData.x.vertical, delta)
end
end
if physicData.y ~= nil then
if isHorizontal and physicData.y.horizontal then
rot.y = self:getPhysicRot(physicData.y.horizontal, delta)
elseif not isHorizontal and physicData.y.vertical then
rot.y = self:getPhysicRot(physicData.y.vertical, delta)
end
end
if physicData.z ~= nil then
if isHorizontal and physicData.z.horizontal then
rot.z = self:getPhysicRot(physicData.z.horizontal, delta)
elseif not isHorizontal and physicData.z.vertical then
rot.z = self:getPhysicRot(physicData.z.vertical, delta)
end
end
modelPart:setRot(rot)
if self.parent.characterData.physics.callbacks ~= nil and self.parent.characterData.physics.callbacks.onPhysicPerformed ~= nil then
self.parent.characterData.physics.callbacks.onPhysicPerformed(self.parent.characterData, modelPart)
end
end
end
end
end, "physics_render")
end
end;
---物理演算を無効にする。物理演算で管理していたモデルの回転をリセットする。
---@param self Physics
disable = function (self)
events.TICK:remove("physics_tick")
events.RENDER:remove("physics_render")
for _, physicData in ipairs(self.parent.characterData.physics.physicData) do
local initialRot = vectors.vec3()
if physicData.x ~= nil and physicData.x.vertical ~= nil then
initialRot.x = physicData.x.vertical.neutral
end
if physicData.y ~= nil and physicData.y.vertical ~= nil then
initialRot.y = physicData.y.vertical.neutral
end
if physicData.z ~= nil and physicData.z.vertical ~= nil then
initialRot.z = physicData.z.vertical.neutral
end
for _, modelPart in ipairs(physicData.models) do
modelPart:setRot(initialRot)
end
end
end;
}

View file

@ -0,0 +1,231 @@
---@class (exact) PlacementObject : SpawnObject 単一の設置物を管理するクラス
---@field package object ModelPart インスタンスで制御するオブジェクト
---@field public index integer 設置物データのインデックス番号。設置物のデータを参照するときに使用する。
---@field package boundingBox Vector3 設置物の当たり判定
---@field package gravity number この設置物に働く重力の大きさ
---@field package hasFireResistance boolean この設置物に炎耐性を付けるかどうか
---@field public removeReason PlacementObjectManager.RemoveReason この設置物のインスタンスが破棄される理由
---@field package modelOffsetPos Vector3 設置物"モデル"の位置オフセット値
---@field package offsetPos Vector3 設置物の中心座標のオフセット値
---@field public currentPos Vector3 設置物の現在位置
---@field package nextPos Vector3 設置物の次ティックの位置
---@field package fallingSpeed number 設置物の落下速度
---@field package isOnGround boolean 設置物が接地しているかどうか
---@field public new fun(parent: Avatar, model: ModelPart, index: integer, data: BlueArchiveCharacter.PlacementObjectStruct, pos: Vector3, rot: number): PlacementObject コンストラクター
PlacementObject = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@param index integer 設置物データのインデックス番号。設置物のデータを参照するときに使用する。
---@param pos Vector3 設置物を設置するワールド座標
---@param rot number 設置物のワールド方向
---@return PlacementObject
new = function (parent, index, pos, rot)
---@type PlacementObject
local instance = Avatar.instantiate(PlacementObject, SpawnObject, parent)
instance.index = index
instance.object = instance.parent.characterData.placementObjects[instance.index].placementMode == "COPY" and instance.parent.characterData.placementObjects[instance.index].model:copy(instance.uuid) or instance.parent.characterData.placementObjects[instance.index].model
instance.boundingBox = instance.parent.characterData.placementObjects[instance.index].boundingBox.size:copy():scale(0.0625)
instance.gravity = instance.parent.characterData.placementObjects[instance.index].gravity ~= nil and instance.parent.characterData.placementObjects[instance.index].gravity or 1
instance.hasFireResistance = instance.parent.characterData.placementObjects[instance.index].hasFireResistance ~= nil and instance.parent.characterData.placementObjects[instance.index].hasFireResistance or false
instance.removeReason = "REMOVED_BY_SCRIPTS"
instance.modelOffsetPos = vectors.vec3()
instance.offsetPos = vectors.vec3()
instance.currentPos = pos
instance.nextPos = instance.currentPos
instance.fallingSpeed = 0
instance.isOnGround = false
instance.callbacks = {
---@param self PlacementObject
onInit = function (self)
if self.parent.characterData.placementObjects[self.index].placementMode == "COPY" then
models.script_placement_object:addChild(self.object)
end
local objectOffset = vectors.vec3()
if self.parent.characterData.placementObjects[self.index].boundingBox.offsetPos ~= nil then
objectOffset = self.parent.characterData.placementObjects[self.index].boundingBox.offsetPos:copy():scale(-0.0625)
end
if self.gravity >= 0 then
self.modelOffsetPos = objectOffset:copy():add(0, 0.075, 0)
else
self.modelOffsetPos = objectOffset:copy():add(0, -0.075, 0)
end
--self.objectModel:setPos(self.currentPos:copy():add(self.modelOffsetPos):scale(16))
self.object:setRot(0, rot, 0)
self.object:setVisible(true)
if self.parent.characterData.placementObjects[self.index].callbacks ~= nil and self.parent.characterData.placementObjects[self.index].callbacks.onInit ~= nil then
self.parent.characterData.placementObjects[self.index].callbacks.onInit(self.parent.characterData, self)
end
end;
---@param self PlacementObject
onDeinit = function (self)
if self.parent.characterData.placementObjects[self.index].placementMode == "COPY" then
models.script_placement_object:removeChild(self.object)
self.object:remove()
else
self.object:setVisible(false)
end
if self.parent.characterData.placementObjects[self.index].callbacks ~= nil and self.parent.characterData.placementObjects[self.index].callbacks.onDeinit ~= nil then
self.parent.characterData.placementObjects[self.index].callbacks.onDeinit(self.parent.characterData, self)
end
end;
---@param self PlacementObject
onTick = function (self)
--設置物の位置を強制更新
self.currentPos = self.nextPos
self.object:setPos(self.currentPos:copy():add(self.modelOffsetPos):scale(16))
--当たり判定同士が重複しているか確認
local boundingBoxStartPos = vectors.vec3(self.currentPos.x - self.boundingBox.x / 2, self.currentPos.y, self.currentPos.z - self.boundingBox.z / 2)
local boundingBoxEndPos = vectors.vec3(self.currentPos.x + self.boundingBox.x / 2, self.currentPos.y + self.boundingBox.y, self.currentPos.z + self.boundingBox.z / 2)
local boundingBoxCenter = boundingBoxEndPos:copy():sub(boundingBoxStartPos):scale(0.5):add(boundingBoxStartPos)
for z = math.floor(boundingBoxStartPos.z), math.floor(boundingBoxEndPos.z) do
for y = math.floor(boundingBoxStartPos.y), math.floor(boundingBoxEndPos.y) do
for x = math.floor(boundingBoxStartPos.x), math.floor(boundingBoxEndPos.x) do
for _, collisionBox in ipairs( world.getBlockState(x, y, z):getCollisionShape()) do
local collisionStartPos = collisionBox[1]:copy():add(x, y, z)
local collisionEndPos = collisionBox[2]:copy():add(x, y, z)
local collisionBoxCenter = collisionStartPos:copy():add(collisionEndPos:copy():sub(collisionStartPos):scale(0.5))
if math.abs(collisionBoxCenter.x - boundingBoxCenter.x) < ((collisionEndPos.x - collisionStartPos.x) + (boundingBoxEndPos.x - boundingBoxStartPos.x)) / 2 and math.abs(collisionBoxCenter.y - boundingBoxCenter.y) < ((collisionEndPos.y - collisionStartPos.y) + (boundingBoxEndPos.y - boundingBoxStartPos.y)) / 2 and math.abs(collisionBoxCenter.z - boundingBoxCenter.z) < ((collisionEndPos.z - collisionStartPos.z) + (boundingBoxEndPos.z - boundingBoxStartPos.z)) / 2 then
self.removeReason = "OVERLAPPED"
self.shouldDeinit = true
return
end
end
end
end
end
--落下速度を更新
local fluidTags = world.getBlockState(self.currentPos):getFluidTags()
if fluidTags[2] == "c:water" then
if self.gravity >= 0 then
self.fallingSpeed = math.max(self.fallingSpeed - 0.1 * self.gravity, 0.1 * self.gravity)
else
self.fallingSpeed = math.min(self.fallingSpeed - 0.1 * self.gravity, 0.1 * self.gravity)
end
elseif fluidTags[2] == "c:lava" then
if self.gravity >= 0 then
self.fallingSpeed = math.max(self.fallingSpeed - 0.1 * self.gravity, 0.02 * self.gravity)
else
self.fallingSpeed = math.min(self.fallingSpeed - 0.1 * self.gravity, 0.02 * self.gravity)
end
else
if self.gravity >= 0 then
self.fallingSpeed = math.min(self.fallingSpeed + 0.035 * self.gravity, 3.575 * self.gravity)
else
self.fallingSpeed = math.max(self.fallingSpeed + 0.035 * self.gravity, 3.575 * self.gravity)
end
end
self.nextPos = self.currentPos:copy():add(0, self.fallingSpeed * -1, 0)
--現ティックと次ティックから直方体を算出
local nextBoxStartPos = vectors.vec3()
local nextBoxEndPos = vectors.vec3()
if self.gravity >= 0 then
nextBoxStartPos = vectors.vec3(self.currentPos.x - self.boundingBox.x / 2, math.min(self.currentPos.y, self.currentPos.y - self.fallingSpeed), self.currentPos.z - self.boundingBox.z / 2)
nextBoxEndPos = vectors.vec3(self.currentPos.x + self.boundingBox.x / 2, math.max(self.currentPos.y, self.currentPos.y - self.fallingSpeed), self.currentPos.z + self.boundingBox.z / 2)
else
nextBoxStartPos = vectors.vec3(self.currentPos.x - self.boundingBox.x / 2, math.min(self.currentPos.y, self.currentPos.y - self.fallingSpeed) + self.boundingBox.y, self.currentPos.z - self.boundingBox.z / 2)
nextBoxEndPos = vectors.vec3(self.currentPos.x + self.boundingBox.x / 2, math.max(self.currentPos.y, self.currentPos.y - self.fallingSpeed) + self.boundingBox.y, self.currentPos.z + self.boundingBox.z / 2)
end
local nextBoxCenter = nextBoxStartPos:copy():add(nextBoxEndPos:copy():sub(nextBoxStartPos):scale(0.5))
--直方体と重なるブロック座標を全て算出
local collisionDetected = false
if self.gravity >= 0 then
local collisionYPos = math.floor(nextBoxStartPos.y)
for y = math.floor(nextBoxEndPos.y), math.floor(nextBoxStartPos.y) - 1, -1 do
for z = math.floor(nextBoxStartPos.z), math.floor(nextBoxEndPos.z) do
for x = math.floor(nextBoxStartPos.x), math.floor(nextBoxEndPos.x) do
for _, collisionBox in ipairs( world.getBlockState(x, y, z):getCollisionShape()) do
local collisionStartPos = collisionBox[1]:copy():add(x, y, z)
local collisionEndPos = collisionBox[2]:copy():add(x, y, z)
local collisionBoxCenter = collisionStartPos:copy():add(collisionEndPos:copy():sub(collisionStartPos):scale(0.5))
if math.abs(collisionBoxCenter.x - nextBoxCenter.x) < ((collisionEndPos.x - collisionStartPos.x) + (nextBoxEndPos.x - nextBoxStartPos.x)) / 2 and math.abs(collisionBoxCenter.y - nextBoxCenter.y) < ((collisionEndPos.y - collisionStartPos.y) + (nextBoxEndPos.y - nextBoxStartPos.y)) / 2 and math.abs(collisionBoxCenter.z - nextBoxCenter.z) < ((collisionEndPos.z - collisionStartPos.z) + (nextBoxEndPos.z - nextBoxStartPos.z)) / 2 then
if collisionEndPos.y > self.nextPos.y then
self.nextPos.y = collisionEndPos.y
collisionYPos = y
self.fallingSpeed = 0
collisionDetected = true
end
end
end
end
end
if y == collisionYPos - 1 then
break
end
end
else
local collisionYPos = math.floor(nextBoxEndPos.y)
for y = math.floor(nextBoxStartPos.y), math.floor(nextBoxEndPos.y), 1 do
for z = math.floor(nextBoxStartPos.z), math.floor(nextBoxEndPos.z) do
for x = math.floor(nextBoxStartPos.x), math.floor(nextBoxEndPos.x) do
for _, collisionBox in ipairs( world.getBlockState(x, y, z):getCollisionShape()) do
local collisionStartPos = collisionBox[1]:copy():add(x, y, z)
local collisionEndPos = collisionBox[2]:copy():add(x, y, z)
local collisionBoxCenter = collisionStartPos:copy():add(collisionEndPos:copy():sub(collisionStartPos):scale(0.5))
if math.abs(collisionBoxCenter.x - nextBoxCenter.x) < ((collisionEndPos.x - collisionStartPos.x) + (nextBoxEndPos.x - nextBoxStartPos.x)) / 2 and math.abs(collisionBoxCenter.y - nextBoxCenter.y) < ((collisionEndPos.y - collisionStartPos.y) + (nextBoxEndPos.y - nextBoxStartPos.y)) / 2 and math.abs(collisionBoxCenter.z - nextBoxCenter.z) < ((collisionEndPos.z - collisionStartPos.z) + (nextBoxEndPos.z - nextBoxStartPos.z)) / 2 then
if collisionStartPos.y < self.nextPos.y + self.boundingBox.y then
self.nextPos.y = collisionStartPos.y - self.boundingBox.y
collisionYPos = y
self.fallingSpeed = 0
collisionDetected = true
end
end
end
end
end
if y == collisionYPos + 1 then
break
end
end
end
if collisionDetected and not self.isOnGround and self.parent.characterData.placementObjects[self.index].callbacks ~= nil and self.parent.characterData.placementObjects[self.index].callbacks.onGround ~= nil then
self.parent.characterData.placementObjects[self.index].callbacks.onGround(self.parent.characterData, self)
end
self.isOnGround = collisionDetected
local nextBlock = world.getBlockState(self.nextPos)
local isNextBlockFire = false
for _, tag in ipairs(nextBlock:getTags()) do
if tag == "minecraft:fire" then
isNextBlockFire = true
break
end
end
if self.nextPos.y < -128 then
self.removeReason = "TOO_LOW"
self.shouldDeinit = true
elseif self.nextPos.y > 384 then
self.removeReason = "TOO_HIGH"
self.shouldDeinit = true
elseif not self.hasFireResistance and (nextBlock:getFluidTags()[2] == "c:lava" or isNextBlockFire) then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.fire.extinguish"), self.nextPos)
for _ = 0, self.boundingBox.x * self.boundingBox.y * self.boundingBox.z * 8 do
particles:newParticle(self.parent.compatibilityUtils:checkParticle("minecraft:smoke"), vectors.vec3(self.nextPos.x + math.random() * self.boundingBox.x - self.boundingBox.x / 2, self.nextPos.y + math.random() * self.boundingBox.y, self.nextPos.z + math.random() * self.boundingBox.z - self.boundingBox.z / 2))
end
self.removeReason = "BURNT"
self.shouldDeinit = true
end
if self.parent.characterData.placementObjects[self.index].callbacks ~= nil and self.parent.characterData.placementObjects[self.index].callbacks.onTick ~= nil then
self.parent.characterData.placementObjects[self.index].callbacks.onTick(self.parent.characterData, self)
end
end;
---@param self PlacementObject
onRender = function (self, delta, context)
self.object:setPos(self.nextPos:copy():sub(self.currentPos):scale(delta):add(self.currentPos):add(self.modelOffsetPos):scale(16))
if self.parent.characterData.placementObjects[self.index].callbacks ~= nil and self.parent.characterData.placementObjects[self.index].callbacks.onRender ~= nil then
self.parent.characterData.placementObjects[self.index].callbacks.onRender(self.parent.characterData, self)
end
end
}
return instance
end;
}

View file

@ -0,0 +1,87 @@
---設置物の設置モードの列挙型
---@alias PlacementObjectManager.PlacementMode
---| "COPY" # コピーモード。BBアニメーションは使えないが、複数の設置物を設置可能。
---| "MOVE" # 移動モード。同時に1つしか設置物を設置できないが、BBアニメーションが使える。
---設置物が削除された理由の列挙型
---@alias PlacementObjectManager.RemoveReason
---| "REMOVED_BY_SCRIPTS" # スクリプトによって削除
---| "OVERLAPPED" # 設置物がブロックと重なって削除
---| "BURNT" # 炎に焼かれて削除
---| "TOO_LOW" # 設置物のY座標が低過ぎて削除
---| "TOO_HIGH" # 設置物のY座標が高過ぎて削除
---@class (exact) PlacementObjectManager : SpawnObjectManager 設置物を管理するマネージャークラス
---@field public objects PlacementObject[] インスタンスで制御するオブジェクト
---@field public getObject fun(self: PlacementObjectManager, index: integer, pos: Vector3, rot: number): PlacementObject 設置物のインスタンスを生成して返す
---@field public spawn fun(self: PlacementObjectManager, index: integer, pos: Vector3, rot: number) 設置物を設置する
---@field public applyFunc fun(self: PlacementObjectManager, index: integer, func: fun(object: PlacementObject, i: integer)) 設置済み設置物の指定した設置物データのインデックス番号のみに関数を適用する
PlacementObjectManager = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return PlacementObjectManager
new = function (parent)
---@type PlacementObjectManager
local instance = Avatar.instantiate(PlacementObjectManager, SpawnObjectManager, parent)
instance.managerName = "placement_object"
return instance
end;
---初期化関数
---@param self PlacementObjectManager
init = function (self)
SpawnObjectManager.init(self)
---@diagnostic disable-next-line: discard-returns
models:newPart("script_placement_object", "World")
for _, data in ipairs(self.parent.characterData.placementObjects) do
if data.placementMode == "MOVE" then
data.model = data.model:moveTo(models.script_placement_object)
data.model:setVisible(false)
end
end
end;
---設置物のインスタンスを生成して返す。
---@param self PlacementObjectManager
---@param index integer 設置物データのインデックス番号
---@param pos Vector3 設置物を設置するワールド座標
---@param rot number 設置物を設置するワールド方向(Y軸のみ)
---@return PlacementObject instance 生成したインスタンス
getObject = function (self, index, pos, rot)
if self.parent.characterData.placementObjects[index].placementMode == "MOVE" then
for i, placementObject in ipairs(self.objects) do
if placementObject.index == index then
self:remove(i)
break
end
end
end
return PlacementObject.new(self.parent, index, pos, rot)
end;
---設置物を設置する。
---@param self PlacementObjectManager
---@param index integer 設置物データのインデックス番号
---@param pos Vector3 設置物を設置するワールド座標
---@param rot number 設置物を設置するワールド方向(Y軸のみ)
spawn = function (self, index, pos, rot)
SpawnObjectManager.spawn(self, index, pos, rot)
end;
---設置済み設置物の、指定した設置物データのインデックス番号のみに関数を適用する。
---@param self PlacementObjectManager
---@param index integer 関数実行対象の設置物データのインデックス番号
---@param func fun(object: PlacementObject, i: integer) 実行する関数
applyFunc = function (self, index, func)
for i, obj in ipairs(self.objects) do
if obj.index == index then
func(obj, i)
end
end
end;
}

View file

@ -0,0 +1,32 @@
---@class (exact) Portrait : HeadModelGenerator ポートレートのモデルを管理するクラス
Portrait = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Portrait
new = function (parent)
---@type Portrait
local instance = Avatar.instantiate(Portrait, HeadModelGenerator, parent)
instance.processData = instance.parent.characterData.portrait
instance.parentName = "portrait"
instance.parentType = "Portrait"
return instance
end;
---初期化関数
---@param self Portrait
init = function (self)
HeadModelGenerator.init(self)
end;
---頭モデルのコピーを生成する。
---@param self HeadBlock
generateHeadModel = function (self)
HeadModelGenerator.generateHeadModel(self)
if models.script_portrait.Head ~= nil then
models.script_portrait.Head.HeadRing:remove()
end
end;
}

View file

@ -0,0 +1,30 @@
---@class Skirt : AvatarModule スカートを制御するクラス
Skirt = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Skirt
new = function (parent)
---@type Skirt
local instance = Avatar.instantiate(Skirt, AvatarModule, parent)
return instance
end;
---初期化関数
---@param self AvatarModule
init = function (self)
AvatarModule.init(self)
if self.parent.characterData.skirt.skirtModels ~= nil and #self.parent.characterData.skirt.skirtModels > 0 then
events.TICK:register(function ()
local isCrouching = player:isCrouching()
for _, skirtModel in ipairs(self.parent.characterData.skirt.skirtModels) do
if skirtModel:getVisible() then
skirtModel:setRot(isCrouching and 30 or 0, 0, 0)
end
end
end)
end
end;
}

View file

@ -0,0 +1,169 @@
---レジストリの種類を示す列挙型
---@alias CompatibilityUtils.RegistryType
---| "BLOCK" # ブロック名
---| "ITEM" # アイテム名
---| "PARTICLE" # パーティクル名
---| "SOUND" # サウンド名
---@class (exact) CompatibilityUtils : AvatarModule Minecraftのゲームバージョンが異なっていてもある程度互換性を確保するためのユーティリティクラス
---@field package registries {block: Minecraft.blockID[], item: Minecraft.itemID[], particle: Minecraft.particleID[], sound: Minecraft.soundID[]} ゲームから取得した全アイテム名を保持するテーブル
---@field package checkedTable {block: {[Minecraft.blockID]: boolean}, item: {[Minecraft.itemID]: boolean}, particle: {[Minecraft.particleID]: boolean}, sound: {[Minecraft.soundID]: boolean}} レジストリへの確認が済んでいるIDを保持するテーブル
---@field package find fun(self: CompatibilityUtils, registryType: CompatibilityUtils.RegistryType, target: string): boolean 指定されたターゲットがレジストリに登録されているかどうかを返す。
---@field public checkBlock fun(self: CompatibilityUtils, block: Minecraft.blockID): Minecraft.blockID 指定されたブロックIDがレジストリに登録されているか確認する。レジストリに未登録の場合は"minecraft:dirt"を返す。
---@field public checkItem fun(self: CompatibilityUtils, item: Minecraft.itemID): Minecraft.itemID 指定されたアイテムIDがレジストリに登録されているか確認する。レジストリに未登録の場合は"minecraft:barrier"を返す。
---@field public checkParticle fun(self: CompatibilityUtils, particle: Minecraft.particleID): Minecraft.particleID 指定されたパーティクルIDがレジストリに登録されているか確認する。レジストリに未登録の場合は"minecraft:poof"を返す。
---@field public checkSound fun(self: CompatibilityUtils, sound: Minecraft.soundID): Minecraft.soundID 指定されたサウンドIDがレジストリに登録されているか確認する。レジストリに未登録の場合は"minecraft:empty"を返す。
---@field public getBlockParticleId fun(block: Minecraft.blockID): string ブロックの破片のパーティクルを示す文字列を返す。Minecraftのバージョン違いを吸収するための関数。
---@field public getDustParticleId fun(color: Vector3, size: number): string dustパーティクルを示す文字列を返す。Minecraftのバージョン違いを吸収するための関数。
---@field public setPostEffect fun(effect?: Minecraft.shaderName) renderer:setPostEffect()のラッパー関数。1.20.5でレンダーエフェクトが削除されたことによる対応。
CompatibilityUtils = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return CompatibilityUtils
new = function (parent)
---@type CompatibilityUtils
local instance = Avatar.instantiate(CompatibilityUtils, AvatarModule, parent)
instance.registries = {}
instance.checkedTable = {
block = {};
item = {};
particle = {};
sound = {};
}
return instance
end;
---初期化関数
---@param self CompatibilityUtils
init = function (self)
AvatarModule.init(self)
self.registries.block = client.getRegistry("minecraft:block")
self.registries.item = client.getRegistry("minecraft:item")
self.registries.particle = client.getRegistry("minecraft:particle_type")
self.registries.sound = client.getRegistry("minecraft:sound_event")
for name, _ in pairs(self.registries) do
table.sort(self.registries[name])
end
self.checkedTable.block["minecraft:dirt"] = true
self.checkedTable.item["minecraft:barrier"] = true
self.checkedTable.particle["minecraft:poof"] = true
self.checkedTable.sound["minecraft:empty"] = true
if host:isHost() and client:getVersion() < "1.20.1" then
print(self.parent.locale:getLocale("avatar.old_version_warning"))
end
end;
---指定されたターゲットがレジストリに登録されているかどうかを返す。
---@param self CompatibilityUtils
---@param registryType CompatibilityUtils.RegistryType 検索をかける対象のレジストリ
---@param target string 検索対象名。"minecraft:"を抜かないこと。
---@return boolean idFound 指定されたターゲットがレジストリで見つかったかどうか
find = function (self, registryType, target)
---リスト内の中央の要素(偶数の場合は中央から1つ左の要素)と指定されたターゲットのUnicode順を比較する。
---@param from integer リストの検索開始のインデックス番号
---@param to integer リストの検索終了のインスタンス番号(指定したインデックス番号の要素も検索に含む)
---@return integer compareResult 比較結果。0は同じ文字列、1はターゲットの方が大きい、-1はターゲットの方が小さいことを表す。
local function compareToCenterElement(from, to)
local centerIndex = math.floor((to - from) / 2) + from
if self.registries[registryType:lower()][centerIndex] < target then
return 1
elseif self.registries[registryType:lower()][centerIndex] > target then
return -1
else
return 0
end
end
local startIndex = 1
local endIndex = #self.registries[registryType:lower()]
while startIndex < endIndex do
local compareResult = compareToCenterElement(startIndex, endIndex)
if compareResult == 1 then
startIndex = math.floor((endIndex - startIndex) / 2) + startIndex + 1
elseif compareResult == -1 then
endIndex = math.floor((endIndex - startIndex) / 2) + startIndex
else
break
end
end
if startIndex == endIndex then
return compareToCenterElement(startIndex, endIndex) == 0
else
return true
end
end;
---指定されたブロックIDがレジストリに登録されているか確認する。レジストリに未登録の場合は"minecraft:dirt"を返す。
---@param self CompatibilityUtils
---@param block Minecraft.blockID 確認対象のブロックID
---@param blockState string? ブロックステートを示す文字列
---@return Minecraft.blockID blockID レジストリに登録してある場合は確認対象のブロックIDをそのまま返し、未登録の場合は"minecraft:dirt"が返す。
checkBlock = function (self, block, blockState)
if self.checkedTable.block[block] == nil then
self.checkedTable.block[block] = self:find("BLOCK", block)
end
local state = blockState ~= nil and blockState or ""
return self.checkedTable.block[block] and block..state or "minecraft:dirt"
end;
---指定されたアイテムIDがレジストリに登録されているか確認する。レジストリに未登録の場合は"minecraft:barrier"を返す。
---@param self CompatibilityUtils
---@param item Minecraft.itemID 確認対象のアイテムID
---@return Minecraft.itemID blockID レジストリに登録してある場合は確認対象のアイテムIDをそのまま返し、未登録の場合は"minecraft:barrier"が返す。
checkItem = function (self, item)
if self.checkedTable.item[item] == nil then
self.checkedTable.item[item] = self:find("ITEM", item)
end
return self.checkedTable.item[item] and item or "minecraft:barrier"
end;
---指定されたパーティクルIDがレジストリに登録されているか確認する。レジストリに未登録の場合は"minecraft:poof"を返す。
---@param self CompatibilityUtils
---@param particle Minecraft.particleID 確認対象のパーティクルID
---@return Minecraft.particleID particleID レジストリに登録してある場合は確認対象のパーティクルIDをそのまま返し、未登録の場合は"minecraft:poof"が返す。
checkParticle = function (self, particle)
if self.checkedTable.particle[particle] == nil then
self.checkedTable.particle[particle] = self:find("PARTICLE", particle)
end
return self.checkedTable.particle[particle] and particle or "minecraft:poof"
end;
---指定されたサウンドIDがレジストリに登録されているか確認する。レジストリに未登録の場合は"minecraft:empty"を返す。
---@param self CompatibilityUtils
---@param sound Minecraft.soundID 確認対象のサウンドID
---@return Minecraft.soundID particleID レジストリに登録してある場合は確認対象のサウンドIDをそのまま返し、未登録の場合は"minecraft:empty"が返す。
checkSound = function (self, sound)
if self.checkedTable.sound[sound] == nil then
self.checkedTable.sound[sound] = self:find("SOUND", sound)
end
return self.checkedTable.sound[sound] and sound or "minecraft:empty"
end;
---ブロックの破片のパーティクルを示す文字列を返す。Minecraftのバージョン違いを吸収するための関数。
---@param block Minecraft.blockID ブロックの破片パーティクルとして表示するブロックのID。レジストリへの確認は行わない。
---@return string particleData ブロックの破片のパーティクルを示す文字列
getBlockParticleId = function (block)
return client:getVersion() >= "1.20.5" and "minecraft:block{block_state:\""..block.."\"}" or "minecraft:block "..block
end;
---dustパーティクルを示す文字列を返す。Minecraftのバージョン違いを吸収するための関数。
---@param color Vector3 dustの色
---@param size number dustの大きさ
---@return string particleData dustの破片のパーティクルを示す文字列
getDustParticleId = function (color, size)
return client:getVersion() >= "1.20.5" and "minecraft:dust{color:["..color.x..","..color.y..","..color.z.."],scale:"..math.clamp(size, 0.01, 4).."}" or "minecraft:dust "..color.x.." "..color.y.." "..color.z.." "..size
end;
---renderer:setPostEffect()のラッパー関数
---1.20.5でレンダーエフェクトが削除されたことによる対応
---@param effect? Minecraft.shaderName 適用するエフェクト
setPostEffect = function (effect)
if client:getVersion() < "1.20.5" then
renderer:setPostEffect(effect)
end
end;
}

View file

@ -0,0 +1,59 @@
---@class (exact) ModelUtils : AvatarModule モデルに関するユーティリティ関数群
---@field public getModelWorldPos fun(model: ModelPart): Vector3 指定したモデルのワールド位置を返す。
---@field public copyModel fun(self: ModelUtils, modelPart: ModelPart, name?: string, forceCopy?: boolean): ModelPart|nil モデルパーツをディープコピーする。
---@field public copyModel fun(self: ModelUtils, modelPart: ModelPart, name?: string, forceCopy?: true): ModelPart モデルパーツをディープコピーする。
---@field public moveTo fun(target: ModelPart, destination: ModelPart, originalParent: ModelPart) モデルパーツを別の親に移動させる。組み込みmoveTo()で何故かモデルパーツが残ってしまう問題に対処済み。
ModelUtils = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return ModelUtils
new = function (parent)
---@type ModelUtils
local instance = Avatar.instantiate(ModelUtils, AvatarModule, parent)
return instance
end;
---指定したモデルのワールド位置を返す。
---@param model ModelPart ワールド位置を取得するモデルパーツ
---@return Vector3 worldPos モデルのワールド位置
getModelWorldPos = function(model)
local modelMatrix = model:partToWorldMatrix()
return vectors.vec3(modelMatrix[4][1], modelMatrix[4][2], modelMatrix[4][3])
end;
---モデルパーツをディープコピーする。
---非表示のモデルパーツはコピーしない。
---@param modelPart ModelPart コピーするモデルパーツ
---@param name? string コピーしたモデルパーツの名前。省略した際はコピー元と同じ名前になる。
---@param forceCopy? boolean 非表示のモデルも強制的にコピーするかどうか
---@return ModelPart|nil copiedModelPart コピーされたモデルパーツ。入力されたモデルパーツが非表示の場合はnilが返る。
copyModel = function (self, modelPart, name, forceCopy)
if modelPart:getVisible() or forceCopy then
local copy = modelPart:copy(name ~= nil and name or modelPart:getName())
copy:setParentType("None")
for _, child in ipairs(copy:getChildren()) do
copy:removeChild(child)
local childModel = self:copyModel(child)
if childModel ~= nil then
copy:addChild(childModel)
end
end
return copy
end
end;
---モデルパーツを別の親に移動させる。
---組み込みmoveTo()で何故かモデルパーツが残ってしまう問題に対処済み。
---@param target ModelPart 移動させる対象のモデルパーツ
---@param destination ModelPart 移動先の親
---@param originalParent ModelPart 移動元の親
moveTo = function (target, destination, originalParent)
target:moveTo(destination)
local modelName = target:getName()
if originalParent[modelName] ~= nil then
originalParent:removeChild(target)
end
end;
}

View file

@ -0,0 +1,35 @@
---@alias PlayerUtils.DamageStatus
---| "NONE" # ダメージなし
---| "DAMAGE" # ダメージを受けた
---| "DIED" # 死亡した
---@class (exact) PlayerUtils : AvatarModule プレイヤーに関するユーティリティ関数群
---@field public damageStatus PlayerUtils.DamageStatus 現在のティックのダメージステータス
---@field package healthPrev integer 前ティックのHP量
PlayerUtils = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return PlayerUtils
new = function (parent)
---@type PlayerUtils
local instance = Avatar.instantiate(PlayerUtils, AvatarModule, parent)
instance.damageStatus = "NONE"
instance.healthPrev = player:getHealth()
return instance
end;
---初期化関数
---@param self PlayerUtils
init = function (self)
AvatarModule.init(self)
events.TICK:register(function ()
local health = player:getHealth()
self.damageStatus = self.healthPrev > health and (health == 0 and "DIED" or "DAMAGE") or "NONE"
self.healthPrev = health
end)
end;
}

View file

@ -0,0 +1,26 @@
---@class (exact) SpawnObject : AvatarModule オブジェクト(設置物、独自定義パーティクル、bbモデルetc.)クラス
---@field public object any インスタンスで制御するオブジェクト。ModelPartやRenderTaskを想定している。
---@field public uuid string このインスタンスのUUID。オブジェクトの名前付けなどにどうぞ。
---@field public shouldDeinit boolean このオブジェクトを破棄するかどうか。trueにするとオブジェクトが破棄され、その際に、onDeinit()コールバック関数が呼ばれる。
---@field public callbacks? SpawnObject.CallbackSet スポーンオブジェクトのコールバック関数
---@class (exact) SpawnObject.CallbackSet スポーンオブジェクトのコールバック関数のセット
---@field public onInit? fun(self: SpawnObject) オブジェクトの初期化直後に呼ばれる関数
---@field public onDeinit? fun(self: SpawnObject) オブジェクトの破棄直前に呼ばれる関数
---@field public onTick? fun(self: SpawnObject) 各ティック毎に呼ばれる関数
---@field public onRender? fun(self: SpawnObject, delta: number, context: Event.Render.context) 各レンダーティック毎に呼ばれる関数
SpawnObject = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return SpawnObject
new = function (parent)
---@type SpawnObject
local instance = Avatar.instantiate(SpawnObject, AvatarModule, parent)
instance.uuid = client.intUUIDToString(client.generateUUID())
instance.shouldDeinit = false
return instance
end;
}

View file

@ -0,0 +1,102 @@
---@class (exact) SpawnObjectManager : AvatarModule オブジェクト(設置物、独自定義パーティクル、bbモデルetc.)をスポーンさせ、管理するマネージャークラス
---@field public managerName string このマネージャーの名前
---@field public objects SpawnObject[] スポーンさせたオブジェクトを保持するテーブル
---@field public getObject fun(self: SpawnObjectManager): SpawnObject スポーンオブジェクトのインスタンスを生成して返す
---@field public spawn fun(self: SpawnObjectManager, ...: any) オブジェクトをスポーンさせる
---@field public remove fun(self: SpawnObjectManager, index: integer) オブジェクトを1つ削除する
---@field public removeAll fun(self: SpawnObjectManager) オブジェクトをすべて削除する
SpawnObjectManager = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return SpawnObjectManager
new = function (parent)
---@type SpawnObjectManager
local instance = Avatar.instantiate(SpawnObjectManager, AvatarModule, parent)
instance.managerName = "spawn_object"
instance.objects = {}
return instance
end;
---スポーンオブジェクトのインスタンスを生成して返す。
---@param self SpawnObjectManager
---@return SpawnObject instance 生成したスポーンオブジェクト
getObject = function (self)
return SpawnObject.new(self.parent)
end;
---オブジェクトをスポーンさせる。
---@param self SpawnObjectManager
---@param ... any インスタンス生成時の引数
spawn = function (self, ...)
---@diagnostic disable-next-line: redundant-parameter
local instance = self:getObject(...)
table.insert(self.objects, instance)
if instance.callbacks ~= nil and instance.callbacks.onInit ~= nil then
instance.callbacks.onInit(instance)
end
if #self.objects == 1 then
events.TICK:register(function ()
if not client:isPaused() then
for index, ins in ipairs(self.objects) do
if ins.callbacks ~= nil and ins.callbacks.onTick ~= nil then
ins.callbacks.onTick(ins)
end
if ins.shouldDeinit then
if ins.callbacks ~= nil and ins.callbacks.onDeinit ~= nil then
ins.callbacks.onDeinit(ins)
end
table.remove(self.objects, index)
if #self.objects == 0 then
events.TICK:remove(self.managerName.."_tick")
events.RENDER:remove(self.managerName.."_render")
end
end
end
end
end, self.managerName.."_tick")
events.RENDER:register(function (delta, ctx)
if not client:isPaused() then
for _, ins in ipairs(self.objects) do
if ins.callbacks ~= nil and ins.callbacks.onRender ~= nil then
ins.callbacks.onRender(ins, delta, ctx)
end
end
end
end, self.managerName.."_render")
end
end;
---オブジェクトを1つ削除する。
---@param self SpawnObjectManager
---@param index integer 削除するオブジェクトのインデックス番号
remove = function (self, index)
if self.objects[index] ~= nil then
if self.objects[index].callbacks ~= nil and self.objects[index].callbacks.onDeinit ~= nil then
self.objects[index].callbacks.onDeinit(self.objects[index])
end
table.remove(self.objects, index)
if #self.objects == 0 then
events.TICK:remove(self.managerName.."_tick")
events.RENDER:remove(self.managerName.."_render")
end
end
end;
---オブジェクトをすべて削除する。
---@param self SpawnObjectManager
removeAll = function (self)
while #self.objects > 0 do
if self.objects[1].callbacks ~= nil and self.objects[1].callbacks.onDeinit ~= nil then
self.objects[1].callbacks.onDeinit(self.objects[1])
end
table.remove(self.objects, 1)
end
events.TICK:remove(self.managerName.."_tick")
events.RENDER:remove(self.managerName.."_render")
end;
}

View file

@ -0,0 +1,23 @@
---@class (exact) VanillaModel : AvatarModule バニラーのモデルの管理クラス
VanillaModel = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return VanillaModel
new = function (parent)
---@type VanillaModel
local instance = Avatar.instantiate(VanillaModel, AvatarModule, parent)
return instance
end;
---初期化関数
---@param self AvatarModule
init = function (self)
AvatarModule.init(self)
for _, vanillaModel in ipairs({vanilla_model.PLAYER, vanilla_model.CHESTPLATE_RIGHT_ARM, vanilla_model.CHESTPLATE_LEFT_ARM, vanilla_model.LEGGINGS_RIGHT_LEG, vanilla_model.LEGGINGS_LEFT_LEG, vanilla_model.BOOTS}) do
vanillaModel:setVisible(false)
end
end;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,52 @@
---@class ExSkill2WaveParticle : SpawnObject 水着のExスキルアニメーション後の波を表現するパーティクルの1グループを管理するクラス
---@field package object Particle[] インスタンスで制御するオブジェクト
---@field package pos Vector3 パーティクルの基準位置
---@field package rot number パーティクルの向き(度数法)
---@field package animationCount integer パーティクルの速度調整のためのティックカウンター
---@field public new fun(parent: Avatar, pos: Vector3, rot: number): ExSkill2WaveParticle コンストラクタ
ExSkill2WaveParticle = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@param pos Vector3 パーティクルの基準位置
---@param rot number パーティクルの向き(度数法)
---@return ExSkill2WaveParticle
new = function (parent, pos, rot)
---@type ExSkill2WaveParticle
local instance = Avatar.instantiate(ExSkill2WaveParticle, SpawnObject, parent)
instance.object = {}
instance.pos = pos:copy()
instance.rot = rot
instance.animationCount = 0
instance.callbacks = {
---@param self ExSkill2WaveParticle
onInit = function (self)
local particlePos = vectors.rotateAroundAxis(self.rot * -1 + 120, 0, 0, 0.5, 0, 1, 0):add(self.pos)
local particleOffset = vectors.rotateAroundAxis(self.rot * -1 + 120, -1, 0, 0, 0, 1, 0)
for i = -0.5, 0.5, 0.5 do
for _ = 1, 3 do
local colorFactor = math.random()
table.insert(instance.object, particles:newParticle(self.parent.compatibilityUtils.getDustParticleId(vectors.vec3(1000000000, 1000000000, 1000000000), 2), particlePos:copy():add(particleOffset:copy():scale(i))):setVelocity(vectors.rotateAroundAxis(self.rot * -1 + 120, math.random() * 0.1 - 0.05, 0, 0.3, 0, 1, 0)):setColor(colorFactor, 1, 1):setLifetime(colorFactor * 25 + 5))
end
end
end;
---@param self ExSkill2WaveParticle
onTick = function (self)
local velocityAddition = vectors.rotateAroundAxis(self.rot * -1 + 120, 0, math.cos(self.animationCount / 60 * 2 * math.pi) * 0.03, math.cos(self.animationCount / 80 * math.pi) * -0.02, 0, 1, 0)
for _, particle in ipairs(self.object) do
particle:setVelocity(particle:getVelocity():add(velocityAddition))
end
if self.animationCount == 30 then
self.shouldDeinit = true
else
self.animationCount = self.animationCount + 1
end
end;
}
return instance
end;
}

View file

@ -0,0 +1,55 @@
---@class ExSkill2WaveParticleManager : SpawnObjectManager 水着のExスキルアニメーション後の波を表現するパーティクルを管理するクラス
---@field package animationCount integer パーティクルの再生タイミングを計るカウンター
---@field public getObject fun(self: ExSkill2WaveParticleManager, pos: Vector3, rot: number): ExSkill2WaveParticleManager パーティクルのインスタンスを生成して返す
---@field public spawn fun(self: ExSkill2WaveParticleManager, pos: Vector3, rot: number) パーティクルを生成する
---@field public play fun(self: ExSkill2WaveParticleManager) 波のパーティクルを再生する
ExSkill2WaveParticleManager = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return ExSkill2WaveParticleManager
new = function (parent)
---@type ExSkill2WaveParticleManager
local instance = Avatar.instantiate(ExSkill2WaveParticleManager, SpawnObjectManager, parent)
instance.managerName = "ex_skill_2_particles"
instance.animationCount = 0
return instance
end;
---パーティクルのインスタンスを生成して返す。
---@param self ExSkill2WaveParticleManager
---@param pos Vector3 パーティクルの基準位置
---@param rot number パーティクルの向き(度数法)
---@return ExSkill2WaveParticle instance 生成したインスタンス
getObject = function (self, pos, rot)
return ExSkill2WaveParticle.new(self.parent, pos, rot)
end;
---パーティクルを生成する。
---@param self ExSkill2WaveParticleManager
---@param pos Vector3 パーティクルの基準位置
---@param rot number パーティクルの向き(度数法)
spawn = function (self, pos, rot)
SpawnObjectManager.spawn(self, pos, rot)
end;
---波のパーティクルを再生する。
---@param self ExSkill2WaveParticleManager
play = function (self)
events.TICK:remove("ex_skill_2_particles_play_tick")
local playerPos = player:getPos()
local bodyYaw = player:getBodyYaw()
events.TICK:register(function ()
self:spawn(playerPos, bodyYaw)
if self.animationCount == 20 then
events.TICK:remove("ex_skill_2_particles_play_tick")
self.animationCount = 0
else
self.animationCount = self.animationCount + 1
end
end, "ex_skill_2_particles_play_tick")
end;
}

View file

@ -0,0 +1,170 @@
---@class (exact) GunHoshino : Gun ホシノ専用gun.lua
GunHoshino = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return GunHoshino
new = function (parent)
---@type GunHoshino
local instance = Avatar.instantiate(GunHoshino, Gun, parent)
return instance
end;
---初期化関数
---@param self GunHoshino
init = function (self)
AvatarModule.init(self)
events.TICK:register(function ()
self:processGunTick()
self.isGunTickProcessed = false
end)
events.ON_PLAY_SOUND:register(function (id, pos, _, _, _, _, path)
if path ~= nil then
local velocityDistance = player:getVelocity():length()
local distanceFromSound = math.abs(pos:copy():sub(player:getPos()):length() - velocityDistance)
if (id == "minecraft:entity.arrow.shoot" or id == "minecraft:item.crossbow.loading_end" or id == "minecraft:item.crossbow.shoot") and math.abs(velocityDistance - distanceFromSound) < 1 then
if id == "minecraft:item.crossbow.loading_end" then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.dispenser.fail"), pos, 1, 2)
elseif player:isUnderwater() then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:entity.generic.extinguish_fire"), pos, 0.5, 1.5)
else
local gunPosition = self.parent.gun.currentGunPosition
if self.parent.subGun.hasSubGun and math.random() >= 0.5 then
gunPosition = gunPosition == "RIGHT" and "LEFT" or "RIGHT"
end
local particleAnchor = ModelUtils.getModelWorldPos(renderer:isFirstPerson() and (gunPosition == "RIGHT" and models.models.main.Avatar.UpperBody.Arms.RightArm.RightArmBottom.RightItemPivot or models.models.main.Avatar.UpperBody.Arms.LeftArm.LeftArmBottom.LeftItemPivot) or (self.parent.gun.currentGunPosition == gunPosition and models.models.main.Avatar.UpperBody.Body.Gun.MuzzleAnchor or models.models.main.Avatar.UpperBody.Body.SubGun.MuzzleAnchor2))
for _ = 1, 5 do
particles:newParticle(self.parent.compatibilityUtils:checkParticle("minecraft:smoke"), particleAnchor)
end
if self.parent.gun.currentGunPosition == gunPosition then
sounds:playSound(self.parent.compatibilityUtils:checkSound(self.parent.characterData.gun.sound.name), pos, 1, self.parent.characterData.gun.sound.pitch)
else
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:entity.iron_golem.hurt"), pos, 1, 2)
end
end
return true
elseif (id == "minecraft:item.crossbow.loading_start" or id == "minecraft:item.crossbow.loading_middle" or id:match("^minecraft:item%.crossbow%.quick_charge_[1-3]$") ~= nil) and distanceFromSound < 1 and player:getActiveItem().id == "minecraft:crossbow" then
local activeItemTime = player:getActiveItemTime()
local quickChargeLevel = 0
local activeItem = player:getActiveItem()
if activeItem.tag.Enchantments ~= nil then
for _, enchant in ipairs(activeItem.tag.Enchantments) do
if enchant.id == "minecraft:quick_charge" then
quickChargeLevel = enchant.lvl
break
end
end
end
if (quickChargeLevel <= 4 and activeItemTime + quickChargeLevel >= 4 and activeItemTime + quickChargeLevel <= 6) or (quickChargeLevel == 5 and activeItemTime <= 2) then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:item.flintandsteel.use"), pos, 1, 2)
return true
elseif id == "minecraft:item.crossbow.loading_middle" then
return true
end
end
end
end)
events.ITEM_RENDER:register(function (item, mode, _, _, _, leftHanded)
if mode ~= "HEAD" and self.currentGunPosition == (leftHanded and "LEFT" or "RIGHT") and (self.shouldShowWeaponInFirstPerson or mode =="THIRD_PERSON_LEFT_HAND" or mode == "THIRD_PERSON_RIGHT_HAND") then
for _, gunItem in ipairs(self.gunItems) do
if item.id == gunItem then
if leftHanded then
if mode == "FIRST_PERSON_LEFT_HAND" then
local offsetPos = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.firstPersonPos ~= nil and self.parent.characterData.gun.gunPosition.hold.firstPersonPos.left ~= nil then
offsetPos = self.parent.characterData.gun.gunPosition.hold.firstPersonPos.left
end
local offsetRot = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.firstPersonRot ~= nil and self.parent.characterData.gun.gunPosition.hold.firstPersonRot.left ~= nil then
offsetRot = self.parent.characterData.gun.gunPosition.hold.firstPersonRot.left
end
local activeItemId = player:getActiveItem().id
if activeItemId == "minecraft:bow" then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -2.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(20, -7.5, -5):add(offsetRot))
elseif activeItemId == "minecraft:crossbow" then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, 0.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
elseif item.id == "minecraft:crossbow" and item.tag.Charged == 1 then
if player:isLeftHanded() then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(-10, -1.25, 6):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 10, 0):add(offsetRot))
else
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -1.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
end
else
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -1.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
end
elseif mode == "THIRD_PERSON_LEFT_HAND" then
local offsetPos = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.thirdPersonPos ~= nil and self.parent.characterData.gun.gunPosition.hold.thirdPersonPos.left ~= nil then
offsetPos = self.parent.characterData.gun.gunPosition.hold.thirdPersonPos.left
end
local offsetRot = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.thirdPersonRot ~= nil and self.parent.characterData.gun.gunPosition.hold.thirdPersonRot.left ~= nil then
offsetRot = self.parent.characterData.gun.gunPosition.hold.thirdPersonRot.left
end
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -4.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
end
else
if mode == "FIRST_PERSON_RIGHT_HAND" then
local offsetPos = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.firstPersonPos ~= nil and self.parent.characterData.gun.gunPosition.hold.firstPersonPos.right ~= nil then
offsetPos = self.parent.characterData.gun.gunPosition.hold.firstPersonPos.right
end
local offsetRot = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.firstPersonRot ~= nil and self.parent.characterData.gun.gunPosition.hold.firstPersonRot.right ~= nil then
offsetRot = self.parent.characterData.gun.gunPosition.hold.firstPersonRot.right
end
local activeItemId = player:getActiveItem().id
if activeItemId == "minecraft:bow" then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -2.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(20, 7.5, 5):add(offsetRot))
elseif activeItemId == "minecraft:crossbow" then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, 0.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
elseif item.id == "minecraft:crossbow" and item.tag.Charged == 1 then
if player:isLeftHanded() then
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -1.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
else
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(10, -1.25, 6):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, -10, 0):add(offsetRot))
end
else
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -1.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
end
elseif mode == "THIRD_PERSON_RIGHT_HAND" then
local offsetPos = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.thirdPersonPos ~= nil and self.parent.characterData.gun.gunPosition.hold.thirdPersonPos.right ~= nil then
offsetPos = self.parent.characterData.gun.gunPosition.hold.thirdPersonPos.right
end
local offsetRot = vectors.vec3()
if self.parent.characterData.gun.gunPosition.hold.thirdPersonRot ~= nil and self.parent.characterData.gun.gunPosition.hold.thirdPersonRot.right ~= nil then
offsetRot = self.parent.characterData.gun.gunPosition.hold.thirdPersonRot.right
end
models.models.main.Avatar.UpperBody.Body.Gun:setPos(vectors.vec3(0, -4.25, 4.25):add(offsetPos))
models.models.main.Avatar.UpperBody.Body.Gun:setRot(vectors.vec3(0, 0, 0):add(offsetRot))
end
end
return models.models.main.Avatar.UpperBody.Body.Gun
end
end
end
end)
models.models.main.Avatar.UpperBody.Body.Gun:setScale(vectors.vec3(1, 1, 1):scale(self.parent.characterData.gun.scale))
self:setGunPosition("NONE")
if self.parent.characterData.gun.callbacks ~= nil and self.parent.characterData.gun.callbacks.onMainHandChange ~= nil then
self.parent.characterData.gun.callbacks.onMainHandChange(self.parent.characterData, self.isLeftHandedPrev and "LEFT" or "RIGHT")
end
end;
}

View file

@ -0,0 +1,139 @@
---@class (exact) Shield : AvatarModule 盾を制御するクラス
---@field public hasShield boolean 盾を手に持っているかどうか
---@field public setShield fun(self: Shield, value: boolean, shouldPlayShieldSound: boolean) 盾の展開状態を設定する
Shield = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return Shield
new = function (parent)
---@type Shield
local instance = Avatar.instantiate(Shield, AvatarModule, parent)
instance.hasShield = false
return instance
end;
---初期化関数
---@param self Shield
init = function (self)
AvatarModule.init(self)
events.TICK:register(function ()
self:setShield((player:getHeldItem().id == "minecraft:shield" or player:getHeldItem(true).id == "minecraft:shield") and self.parent.exSkill.animationCount == -1, true)
end)
events.ITEM_RENDER:register(function (item, mode)
if item.id == "minecraft:shield" and mode ~= "HEAD" and self.hasShield and (self.parent.gun.shouldShowWeaponInFirstPerson or mode == "THIRD_PERSON_LEFT_HAND" or mode == "THIRD_PERSON_RIGHT_HAND") then
if mode == "FIRST_PERSON_LEFT_HAND" then
local leftHanded = player:isLeftHanded()
if player:getActiveItemTime() > 0 and ((player:getActiveHand() == "OFF_HAND" and not leftHanded) or (player:getActiveHand() == "MAIN_HAND" and leftHanded)) then
models.models.main.Avatar.UpperBody.Body.Shield:setPos(8, -20.25, 2.5)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(0, 0, -5)
else
models.models.main.Avatar.UpperBody.Body.Shield:setPos(6, -22.5, 2.5)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(0, 0, 5)
end
elseif mode == "FIRST_PERSON_RIGHT_HAND" then
local leftHanded = player:isLeftHanded()
if player:getActiveItemTime() > 0 and ((player:getActiveHand() == "MAIN_HAND" and not leftHanded) or (player:getActiveHand() == "OFF_HAND" and leftHanded)) then
models.models.main.Avatar.UpperBody.Body.Shield:setPos(0, -19.25, 2.5)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(0, 0, 5)
else
models.models.main.Avatar.UpperBody.Body.Shield:setPos(2, -22.5, 2.5)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(0, 0, -5)
end
elseif mode == "THIRD_PERSON_LEFT_HAND" then
if self.parent.arms.armState.left == 4 then
if player:isCrouching() then
models.models.main.Avatar.UpperBody.Body.Shield:setPos(3.5, -19.5, 0)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(80, 5, 30)
else
models.models.main.Avatar.UpperBody.Body.Shield:setPos(2, -20.5, -1)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(55, 20, 25)
end
else
local leftHanded = player:isLeftHanded()
if player:getActiveItemTime() > 0 and ((player:getActiveHand() == "OFF_HAND" and not leftHanded) or (player:getActiveHand() == "MAIN_HAND" and leftHanded)) then
models.models.main.Avatar.UpperBody.Body.Shield:setPos(2, -20.5, -2)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(50, 30, 30)
else
models.models.main.Avatar.UpperBody.Body.Shield:setPos(2, -20.5, 2.5)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(5, 90, 0)
end
end
elseif mode == "THIRD_PERSON_RIGHT_HAND" then
if self.parent.arms.armState.right == 4 then
if player:isCrouching() then
models.models.main.Avatar.UpperBody.Body.Shield:setPos(4.5, -19.5, 0)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(80, -5, -30)
else
models.models.main.Avatar.UpperBody.Body.Shield:setPos(6, -20.5, -1)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(55, -20, -25)
end
else
local leftHanded = player:isLeftHanded()
if player:getActiveItemTime() > 0 and ((player:getActiveHand() == "MAIN_HAND" and not leftHanded) or (player:getActiveHand() == "OFF_HAND" and leftHanded)) then
models.models.main.Avatar.UpperBody.Body.Shield:setPos(6, -20.5, -2)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(50, -30, -30)
else
models.models.main.Avatar.UpperBody.Body.Shield:setPos(6, -20.5, 2.5)
models.models.main.Avatar.UpperBody.Body.Shield:setRot(5, -90, 0)
end
end
end
models.models.main.Avatar.UpperBody.Body.Shield:setSecondaryRenderType(item:hasGlint() and "GLINT" or "NONE")
models.models.main.Avatar.UpperBody.Body.Shield:setVisible(true)
return models.models.main.Avatar.UpperBody.Body.Shield
end
end)
events.ON_PLAY_SOUND:register(function (id, pos, _, _, _, _, path)
if path ~= nil then
if id == "minecraft:item.shield.block" and math.abs(pos:copy():sub(player:getPos()):length() - player:getVelocity():length()) < 0.2 and player:getActiveItem().id == "minecraft:shield" then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.anvil.place"), pos, 1, 4)
return true
end
end
end)
end;
---盾の展開状態を設定する。
---@param self Shield
---@param value boolean 新しい値
---@param shouldPlayShieldSound boolean 盾の展開音を再生するかどうか
setShield = function (self, value, shouldPlayShieldSound)
if value and not self.hasShield then
models.models.main.Avatar.UpperBody.Body.Shield:setParentType("Item")
models.models.main.Avatar.UpperBody.Body.Shield.Section2.ShoulderBelt:setVisible(false)
for _, modelPart in ipairs({models.models.main.Avatar.UpperBody.Body.Shield.Section2, models.models.main.Avatar.UpperBody.Body.Shield.Section2.Section1}) do
modelPart:setRot()
end
for _, modelPart in ipairs({models.models.main.Avatar.UpperBody.Body.Shield.Section2.GasCylinder3.GasPiston3, models.models.main.Avatar.UpperBody.Body.Shield.Section2.GasCylinder4.GasPiston4, models.models.main.Avatar.UpperBody.Body.Shield.Section2.Section1.GasCylinder1.GasPiston1, models.models.main.Avatar.UpperBody.Body.Shield.Section2.Section1.GasCylinder2.GasPiston2}) do
modelPart:setPos(0, -1.4, 0)
end
models.models.main.Avatar.UpperBody.Body.Shield.Section3.Handle2:setPos(0, 0.25, 0)
models.models.main.Avatar.UpperBody.Body.Shield.Section2.Section1.Handle:setPos(0, -0.25, 0)
if shouldPlayShieldSound then
sounds:playSound(self.parent.compatibilityUtils:checkSound("minecraft:block.anvil.place"), player:getPos(), 0.1, 2)
end
elseif not value and self.hasShield then
models.models.main.Avatar.UpperBody.Body.Shield:setVisible(self.parent.costume.currentCostume ~= 3)
models.models.main.Avatar.UpperBody.Body.Shield:setParentType("None")
if self.parent.exSkill.animationCount == -1 then
models.models.main.Avatar.UpperBody.Body.Shield.Section2.ShoulderBelt:setVisible(true)
end
models.models.main.Avatar.UpperBody.Body.Shield:setPos()
models.models.main.Avatar.UpperBody.Body.Shield:setRot(5, 90, 0)
for _, modelPart in ipairs({models.models.main.Avatar.UpperBody.Body.Shield.Section2, models.models.main.Avatar.UpperBody.Body.Shield.Section2.Section1}) do
modelPart:setRot(-180, 0, 0)
end
for _, modelPart in ipairs({models.models.main.Avatar.UpperBody.Body.Shield.Section2.GasCylinder3.GasPiston3, models.models.main.Avatar.UpperBody.Body.Shield.Section2.GasCylinder4.GasPiston4, models.models.main.Avatar.UpperBody.Body.Shield.Section2.Section1.GasCylinder1.GasPiston1, models.models.main.Avatar.UpperBody.Body.Shield.Section2.Section1.GasCylinder2.GasPiston2, models.models.main.Avatar.UpperBody.Body.Shield.Section3.Handle2, models.models.main.Avatar.UpperBody.Body.Shield.Section2.Section1.Handle}) do
modelPart:setPos()
end
models.models.main.Avatar.UpperBody.Body.Shield:setSecondaryRenderType("NONE")
end
self.hasShield = value
end;
}

View file

@ -0,0 +1,78 @@
---@class (exact) SubGun : AvatarModule 臨戦衣装の拳銃を制御するクラス
---@field public hasSubGun boolean サブハンドガンを持っているかどうか
---@field public enable fun(self: SubGun) サブハンドガンを有効にする
---@field public disable fun() サブハンドガンを無効にする
SubGun = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return SubGun
new = function (parent)
---@type SubGun
local instance = Avatar.instantiate(SubGun, AvatarModule, parent)
instance.hasSubGun = false
return instance
end;
---サブハンドガンを有効にする。
---@param self SubGun
enable = function (self)
events.TICK:register(function ()
if self.parent.gun.currentGunPosition ~= "NONE" then
local isLeftHanded = player:isLeftHanded()
local heldItem = player:getHeldItem(self.parent.gun.currentGunPosition == "RIGHT" ~= isLeftHanded)
self.hasSubGun = false
for _, gunItem in ipairs(self.parent.gun.gunItems) do
if gunItem == heldItem.id then
self.hasSubGun = true
break
end
end
end
if self.hasSubGun and self.parent.exSkill.animationCount == -1 then
models.models.main.Avatar.UpperBody.Body.SubGun:setScale(1.5, 1.5, 1.5)
models.models.main.Avatar.UpperBody.Body.SubGun:setParentType("Item")
elseif self.parent.exSkill.animationCount == -1 then
models.models.main.Avatar.UpperBody.Body.SubGun:setPos(-1, 17.5, -1.9)
models.models.main.Avatar.UpperBody.Body.SubGun:setRot(-30, 90, 0)
models.models.main.Avatar.UpperBody.Body.SubGun:setScale()
models.models.main.Avatar.UpperBody.Body.SubGun:setParentType("None")
end
end, "sun_gun_tick")
events.ITEM_RENDER:register(function (_, mode)
if self.hasSubGun then
if self.parent.gun.currentGunPosition == "RIGHT" then
if mode == "FIRST_PERSON_LEFT_HAND" then
models.models.main.Avatar.UpperBody.Body.SubGun:setPos(-1, 0.5, -2.5)
models.models.main.Avatar.UpperBody.Body.SubGun:setRot()
return models.models.main.Avatar.UpperBody.Body.SubGun
elseif mode == "THIRD_PERSON_LEFT_HAND" then
models.models.main.Avatar.UpperBody.Body.SubGun:setPos(0, -2, -1)
models.models.main.Avatar.UpperBody.Body.SubGun:setRot()
return models.models.main.Avatar.UpperBody.Body.SubGun
end
elseif self.parent.gun.currentGunPosition == "LEFT" then
if mode == "FIRST_PERSON_RIGHT_HAND" then
models.models.main.Avatar.UpperBody.Body.SubGun:setPos(-1, 0.5, -1)
models.models.main.Avatar.UpperBody.Body.SubGun:setRot()
return models.models.main.Avatar.UpperBody.Body.SubGun
elseif mode == "THIRD_PERSON_RIGHT_HAND" then
models.models.main.Avatar.UpperBody.Body.SubGun:setPos(0, -2, -1)
models.models.main.Avatar.UpperBody.Body.SubGun:setRot()
return models.models.main.Avatar.UpperBody.Body.SubGun
end
end
end
end, "sun_gun_item_render")
end;
---サブハンドガンを無効にする。
---@param self SubGun
disable = function (self)
events.TICK:remove("sun_gun_tick")
events.ITEM_RENDER:remove("sun_gun_item_render")
self.hasSubGun = false
end;
}

View file

@ -0,0 +1,177 @@
---@class (exact) WhaleFloat : AvatarModule クジラフロートを制御するクラス
---@field public whaleFloatEnabled boolean クジラフローに乗っているか
---@field package whaleFloatEnabledPrev boolean 前ティックにクジラフロートに乗っていたかどうか
---@field package lookDirPrev Vector3 前ティックに見ていた方法
---@field package whaleFloatAfkCount integer クジラフロート上でのAFKカウンター
---@field public isAfk boolean AFK中かどうか
---@field public enable fun(self: WhaleFloat) クジラフローを有効にする
---@field public disable fun(self: WhaleFloat) クジラフローを無効にする
WhaleFloat = {
---コンストラクタ
---@param parent Avatar アバターのメインクラスへの参照
---@return WhaleFloat
new = function (parent)
---@type WhaleFloat
local instance = Avatar.instantiate(WhaleFloat, AvatarModule, parent)
instance.whaleFloatEnabled = false
instance.whaleFloatEnabledPrev = false
instance.lookDirPrev = player:getLookDir()
instance.whaleFloatAfkCount = 0
instance.isAfk = false
return instance
end;
---初期化関数
---@param self AvatarModule
init = function (self)
AvatarModule.init(self)
end;
---クジラフローを有効にする。
---@param self WhaleFloat
enable = function (self)
events.TICK:register(function ()
local vehicle = player:getVehicle()
if vehicle ~= nil then
local id = vehicle:getType()
local whaleFloatEnabled = self.parent.actionWheel.shouldReplaceVehicleModels and (id == "minecraft:boat" or id == "minecraft:chest_boat") and #vehicle:getPassengers() == 1
if whaleFloatEnabled then
if not self.whaleFloatEnabledPrev then
models.models.main.Avatar.LowerBody.WhaleFloat:setVisible(true)
renderer:setRenderVehicle(false)
models.models.main.Avatar.Head:setRot(10, 0, 0)
if self.parent.gun.currentGunPosition == "RIGHT" then
self.parent.arms:setArmState(1, 2)
elseif self.parent.gun.currentGunPosition == "LEFT" then
self.parent.arms:setArmState(2, 1)
else
self.parent.arms:setArmState(5, 5)
end
for _, animationModel in ipairs({"models.main", "models.ex_skill_2"}) do
animations[animationModel]["float_ride"]:play()
end
events.TICK:register(function ()
if world.getBlockState(player:getPos()).id == "minecraft:water" then
animations["models.main"]["whale_float"]:setPlaying(true)
if self.parent.physics.velocityAverage[5][2] >= 0.35 then
self.parent.faceParts:setEmotion("CLOSED", "CLOSED", "W", 1)
end
if self.parent.physics.velocityAverage[5][2] >= 0.1 then
local bodyYaw = player:getBodyYaw()
local anchorPos = ModelUtils.getModelWorldPos(models.models.main.Avatar.LowerBody.WhaleFloat.WhaleParticleAnchor1):add(vectors.rotateAroundAxis(bodyYaw * -1, 0.1875, 0, 0, 0, 1, 0))
for _ = 1, 5 do
local particleDirection = math.random() * 60 - 30
particleDirection = particleDirection > 0 and particleDirection + 30 or particleDirection - 30
particles:newParticle(self.parent.compatibilityUtils.getDustParticleId(vectors.vec3(1000000000, 1000000000, 1000000000), 3), anchorPos):setVelocity(vectors.rotateAroundAxis(bodyYaw * -1 + particleDirection + 150, vectors.vec3(1, 1, 1), 0, 1, 0):scale(math.random()):normalize():scale(self.parent.physics.velocityAverage[5][2])):setGravity(0.5):setLifetime(10)
end
end
if player:getVehicle():getNbt().Type == "bamboo" then
models.models.main.Avatar:setPos(0, -6, 0)
self.parent.cameraManager.setCameraPivot(vectors.vec3(0, 0.1875, 0))
renderer:setEyeOffset(0, 0.1875, 0)
else
models.models.main.Avatar:setPos()
self.parent.cameraManager.setCameraPivot(vectors.vec3(0, 0.5625, 0))
renderer:setEyeOffset(0, 0.5625, 0)
end
else
animations["models.main"]["whale_float"]:setPlaying(false)
if player:getVehicle():getNbt().Type == "bamboo" then
models.models.main.Avatar:setPos(0, -9, 0)
self.parent.cameraManager.setCameraPivot(vectors.vec3())
renderer:setEyeOffset()
else
models.models.main.Avatar:setPos(0, -3, 0)
self.parent.cameraManager.setCameraPivot(vectors.vec3(0, 0.375, 0))
renderer:setEyeOffset(0, 0.375, 0)
end
end
local lookDir = player:getLookDir()
if player:getVelocity():length() < 0.01 and self.lookDirPrev:copy():sub(lookDir):length() == 0 and not player:isSwingingArm() and self.parent.playerUtils.damageStatus == "NONE" and player:getActiveItem().id == "minecraft:air" then
self.whaleFloatAfkCount = self.whaleFloatAfkCount + 1
if self.whaleFloatAfkCount == 2400 then
self.isAfk = true
for _, animationModel in ipairs({"models.main", "models.costume_swimsuit", "models.ex_skill_2"}) do
animations[animationModel]["float_afk"]:setSpeed(1)
animations[animationModel]["float_afk"]:play()
end
self.parent.arms:setArmState(0, 0)
self.parent.physics:disable()
elseif self.whaleFloatAfkCount >= 2430 then
self.parent.faceParts:setEmotion("CLOSED2", "CLOSED2", "YAWN", 1, false)
end
else
if self.isAfk then
self.isAfk = false
for _, animationModel in ipairs({"models.main", "models.costume_swimsuit", "models.ex_skill_2"}) do
animations[animationModel]["float_afk"]:setSpeed(-1)
end
events.TICK:remove("whale_float_afk_end_tick")
events.TICK:register(function ()
if animations["models.main"]["float_afk"]:getTime() == 0 then
for _, animationModel in ipairs({"models.main", "models.costume_swimsuit", "models.ex_skill_2"}) do
animations[animationModel]["float_afk"]:stop()
end
if self.parent.gun.currentGunPosition == "RIGHT" then
self.parent.arms:setArmState(1, 2)
elseif self.parent.gun.currentGunPosition == "LEFT" then
self.parent.arms:setArmState(2, 1)
else
self.parent.arms:setArmState(5, 5)
end
self.parent.physics:enable()
events.TICK:remove("whale_float_afk_end_tick")
end
end, "whale_float_afk_end_tick")
end
self.lookDirPrev = lookDir
end
end, "whale_float_tick_2")
end
self.whaleFloatEnabledPrev = true
elseif self.whaleFloatEnabledPrev then
self:disable()
end
elseif self.whaleFloatEnabledPrev then
self.whaleFloatEnabled = false
self:disable()
end
end, "whale_float_tick")
end;
---クジラフロートを無効にする。
---@param self WhaleFloat
disable = function (self)
for _, eventName in ipairs({"whale_float_tick", "whale_float_tick_2", "whale_float_afk_end_tick"}) do
events.TICK:remove(eventName)
end
models.models.main.Avatar.LowerBody.WhaleFloat:setVisible(false)
renderer:setRenderVehicle(true)
models.models.main.Avatar.Head:setRot()
if self.parent.gun.currentGunPosition == "RIGHT" then
self.parent.arms:setArmState(1, 2)
elseif self.parent.gun.currentGunPosition == "LEFT" then
self.parent.arms:setArmState(2, 1)
else
self.parent.arms:setArmState(0, 0)
end
for _, animationModel in ipairs({"models.main", "models.ex_skill_2"}) do
animations[animationModel]["float_ride"]:stop()
animations[animationModel]["float_afk"]:stop()
end
animations["models.costume_swimsuit"]["float_afk"]:stop()
animations["models.main"]["whale_float"]:stop()
models.models.main.Avatar:setPos()
self.parent.cameraManager.setCameraPivot(vectors.vec3())
renderer:setEyeOffset()
self.whaleFloatAfkCount = 0
self.isAfk = false
self.whaleFloatEnabledPrev = false
end;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,007 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Some files were not shown because too many files have changed in this diff Show more