commit cd8cc594ecc909142866c44b15a7752f54312e74 Author: reidlab Date: Sat Mar 15 15:28:46 2025 -0700 init (accidently [doxxed myself](https://github.com/JannisX11/blockbench/issues/1322)) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..263e3ee --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.nix] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3646cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# figura extension files +.vscode +.luarc.json +avatar.code-workspace diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2fd064 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# figura-skin + +my figura avatar!! this currently targets version **0.1.5** + +`textures/main.png` contains the skins base, which can be used as your default minecraft skin if you so choose + +## development + +to get proper typings, find the correct branch for your figura version [on this repository](github.com/GrandpaScout/FiguraRewriteVSDocs/). then, copy everything from the `src/` folder into your current working folder here + +unfortunately, 0.1.5 typings aren't done yet, so just make sure it works :) diff --git a/avatar.json b/avatar.json new file mode 100644 index 0000000..2cb9aad --- /dev/null +++ b/avatar.json @@ -0,0 +1,14 @@ +{ + "name": "reidlab's avatar", + "description": "meee", + "authors": [ + "reidlab", + "mrsirsquishy: Squishy's API", + "Agapurnis: Gradient and nameplate script", + "skyevg: Oklab script", + "adristel: Wet Clothes/Fur Script" + ], + "color": "#d87b5a", + "version": "0.1.5", + "autoScripts": [ "scripts/main.lua" ] +} diff --git a/avatar.png b/avatar.png new file mode 100644 index 0000000..088cec6 Binary files /dev/null and b/avatar.png differ diff --git a/models/main.bbmodel b/models/main.bbmodel new file mode 100644 index 0000000..f9ed2b0 --- /dev/null +++ b/models/main.bbmodel @@ -0,0 +1 @@ +{"meta":{"format_version":"4.10","model_format":"figura","box_uv":true},"visible_box":[1,1,0],"variable_placeholders":"","variable_placeholder_buttons":[],"timeline_setups":[],"unhandled_root_fields":{},"resolution":{"width":64,"height":64},"elements":[{"name":"Head","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4,24,-4],"to":[4,32,4],"autouv":0,"color":0,"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":[16,8,8,0],"texture":0},"down":{"uv":[24,0,16,8],"texture":0}},"type":"cube","uuid":"29c62794-e1cd-826e-514c-fff0a4ed4624"},{"name":"Hat Layer","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4,24,-4],"to":[4,32,4],"autouv":0,"color":0,"inflate":0.5,"origin":[0,0,0],"uv_offset":[32,0],"faces":{"north":{"uv":[40,8,48,16],"texture":0},"east":{"uv":[32,8,40,16],"texture":0},"south":{"uv":[56,8,64,16],"texture":0},"west":{"uv":[48,8,56,16],"texture":0},"up":{"uv":[48,8,40,0],"texture":0},"down":{"uv":[56,0,48,8],"texture":0}},"type":"cube","uuid":"1bae97f2-ad48-a058-2ecd-afd375b05556"},{"name":"Body","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4,12,-2],"to":[4,24,2],"autouv":0,"color":0,"origin":[0,0,0],"uv_offset":[16,16],"faces":{"north":{"uv":[20,20,28,32],"texture":0},"east":{"uv":[16,20,20,32],"texture":0},"south":{"uv":[32,20,40,32],"texture":0},"west":{"uv":[28,20,32,32],"texture":0},"up":{"uv":[28,20,20,16],"texture":0},"down":{"uv":[36,16,28,20],"texture":0}},"type":"cube","uuid":"a5e50aef-7389-c5a8-d202-09d4c91a26d1"},{"name":"Body Layer","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4,12,-2],"to":[4,24,2],"autouv":0,"color":0,"inflate":0.25,"origin":[0,0,0],"uv_offset":[16,32],"faces":{"north":{"uv":[20,36,28,48],"texture":0},"east":{"uv":[16,36,20,48],"texture":0},"south":{"uv":[32,36,40,48],"texture":0},"west":{"uv":[28,36,32,48],"texture":0},"up":{"uv":[28,36,20,32],"texture":0},"down":{"uv":[36,32,28,36],"texture":0}},"type":"cube","uuid":"c2a6ca2e-e446-9557-b737-f86da58b58eb"},{"name":"Right Arm","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4,12,-2],"to":[7,24,2],"autouv":0,"color":0,"origin":[0,0,0],"uv_offset":[40,16],"faces":{"north":{"uv":[44,20,47,32],"texture":0},"east":{"uv":[40,20,44,32],"texture":0},"south":{"uv":[51,20,54,32],"texture":0},"west":{"uv":[47,20,51,32],"texture":0},"up":{"uv":[47,20,44,16],"texture":0},"down":{"uv":[50,16,47,20],"texture":0}},"type":"cube","uuid":"d45e4e2e-fb66-6baf-88f9-2ecfe050051f"},{"name":"Right Arm Layer","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4,12,-2],"to":[7,24,2],"autouv":0,"color":0,"inflate":0.25,"origin":[0,0,0],"uv_offset":[40,32],"faces":{"north":{"uv":[44,36,47,48],"texture":0},"east":{"uv":[40,36,44,48],"texture":0},"south":{"uv":[51,36,54,48],"texture":0},"west":{"uv":[47,36,51,48],"texture":0},"up":{"uv":[47,36,44,32],"texture":0},"down":{"uv":[50,32,47,36],"texture":0}},"type":"cube","uuid":"9098eb9f-fca5-6396-f040-5a03609f2b6c"},{"name":"Left Arm","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-7,12,-2],"to":[-4,24,2],"autouv":0,"color":0,"origin":[0,0,0],"uv_offset":[32,48],"faces":{"north":{"uv":[36,52,39,64],"texture":0},"east":{"uv":[32,52,36,64],"texture":0},"south":{"uv":[43,52,46,64],"texture":0},"west":{"uv":[39,52,43,64],"texture":0},"up":{"uv":[39,52,36,48],"texture":0},"down":{"uv":[42,48,39,52],"texture":0}},"type":"cube","uuid":"13126cce-ad1f-c013-ee4f-ff135e5f8412"},{"name":"Left Arm Layer","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-7,12,-2],"to":[-4,24,2],"autouv":0,"color":0,"inflate":0.25,"origin":[0,0,0],"uv_offset":[48,48],"faces":{"north":{"uv":[52,52,55,64],"texture":0},"east":{"uv":[48,52,52,64],"texture":0},"south":{"uv":[59,52,62,64],"texture":0},"west":{"uv":[55,52,59,64],"texture":0},"up":{"uv":[55,52,52,48],"texture":0},"down":{"uv":[58,48,55,52],"texture":0}},"type":"cube","uuid":"692d3dd8-7d95-56eb-c120-128e47ad6589"},{"name":"Right Leg","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-0.1,0,-2],"to":[3.9,12,2],"autouv":0,"color":0,"origin":[0,0,0],"uv_offset":[0,16],"faces":{"north":{"uv":[4,20,8,32],"texture":0},"east":{"uv":[0,20,4,32],"texture":0},"south":{"uv":[12,20,16,32],"texture":0},"west":{"uv":[8,20,12,32],"texture":0},"up":{"uv":[8,20,4,16],"texture":0},"down":{"uv":[12,16,8,20],"texture":0}},"type":"cube","uuid":"7b91d518-075a-5435-7fd8-43eb3b67c9e7"},{"name":"Right Leg Layer","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-0.1,0,-2],"to":[3.9,12,2],"autouv":0,"color":0,"inflate":0.25,"origin":[0,0,0],"uv_offset":[0,32],"faces":{"north":{"uv":[4,36,8,48],"texture":0},"east":{"uv":[0,36,4,48],"texture":0},"south":{"uv":[12,36,16,48],"texture":0},"west":{"uv":[8,36,12,48],"texture":0},"up":{"uv":[8,36,4,32],"texture":0},"down":{"uv":[12,32,8,36],"texture":0}},"type":"cube","uuid":"824d4fed-c8cd-e10e-d3ab-e2268c72bade"},{"name":"Left Leg","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.9,0,-2],"to":[0.1,12,2],"autouv":0,"color":0,"origin":[0,0,0],"uv_offset":[16,48],"faces":{"north":{"uv":[20,52,24,64],"texture":0},"east":{"uv":[16,52,20,64],"texture":0},"south":{"uv":[28,52,32,64],"texture":0},"west":{"uv":[24,52,28,64],"texture":0},"up":{"uv":[24,52,20,48],"texture":0},"down":{"uv":[28,48,24,52],"texture":0}},"type":"cube","uuid":"26778122-e4df-4d15-3b26-7b6ac360a6be"},{"name":"Left Leg Layer","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-3.9,0,-2],"to":[0.1,12,2],"autouv":0,"color":0,"inflate":0.25,"origin":[0,0,0],"uv_offset":[0,48],"faces":{"north":{"uv":[4,52,8,64],"texture":0},"east":{"uv":[0,52,4,64],"texture":0},"south":{"uv":[12,52,16,64],"texture":0},"west":{"uv":[8,52,12,64],"texture":0},"up":{"uv":[8,52,4,48],"texture":0},"down":{"uv":[12,48,8,52],"texture":0}},"type":"cube","uuid":"c771fc82-9130-3692-c9d3-330de2be4325"},{"name":"Nose","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-0.25,25,-4.75],"to":[0.75,26,-3.75],"autouv":0,"color":3,"rotation":[0,45,0],"origin":[0.75,25,-3.75],"faces":{"north":{"uv":[1,1,2,2],"texture":1},"east":{"uv":[0,1,1,2],"texture":1},"south":{"uv":[3,1,4,2],"texture":1},"west":{"uv":[2,1,3,2],"texture":1},"up":{"uv":[2,1,1,0],"texture":1},"down":{"uv":[3,0,2,1],"texture":1}},"type":"cube","uuid":"52d78b2d-74e6-0c58-6287-a6cf85551b7c"},{"name":"Tail","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-2,12,0.5],"to":[2,16,6.5],"autouv":0,"color":8,"origin":[0,14,1.5],"uv_offset":[0,2],"faces":{"north":{"uv":[6,8,10,12],"texture":1},"east":{"uv":[0,8,6,12],"texture":1},"south":{"uv":[16,8,20,12],"texture":1},"west":{"uv":[10,8,16,12],"texture":1},"up":{"uv":[10,8,6,2],"texture":1},"down":{"uv":[14,2,10,8],"texture":1}},"type":"cube","uuid":"6456e056-b5be-0eb8-790f-a58325394220"},{"name":"Tail","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-1.5,12,5.25],"to":[1.5,15,9.25],"autouv":0,"color":8,"origin":[0,13.5,6.25],"uv_offset":[20,5],"faces":{"north":{"uv":[24,9,27,12],"texture":1},"east":{"uv":[20,9,24,12],"texture":1},"south":{"uv":[31,9,34,12],"texture":1},"west":{"uv":[27,9,31,12],"texture":1},"up":{"uv":[27,9,24,5],"texture":1},"down":{"uv":[30,5,27,9],"texture":1}},"type":"cube","uuid":"ff534eb2-3019-4904-eb0d-555b2061a2ef"},{"name":"Tail","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-1,12,8.5],"to":[1,14,10.5],"autouv":0,"color":2,"rotation":[-7.5,0,0],"origin":[0,13.5,9],"uv_offset":[34,8],"faces":{"north":{"uv":[36,10,38,12],"texture":1},"east":{"uv":[34,10,36,12],"texture":1},"south":{"uv":[40,10,42,12],"texture":1},"west":{"uv":[38,10,40,12],"texture":1},"up":{"uv":[38,10,36,8],"texture":1},"down":{"uv":[40,8,38,10],"texture":1}},"type":"cube","uuid":"167bc448-32e7-d70d-ffdf-ccb8d82185f9"},{"name":"Right Antler","color":7,"origin":[1.25,33.25,-2.5],"rotation":[90,2.5,-5],"export":true,"visibility":true,"locked":false,"render_order":"default","allow_mirror_modeling":true,"vertices":{"acBj":[1,0,2],"YBJa":[1,0,-2],"VcRI":[-1,0,2],"Fdwp":[-1,0,-2]},"faces":{"ziS673zX":{"uv":{"VcRI":[0,16],"Fdwp":[0,12],"YBJa":[2,12],"acBj":[2,16]},"vertices":["acBj","YBJa","Fdwp","VcRI"],"texture":1}},"type":"mesh","uuid":"f8fc2e09-b189-7612-7505-744c819f7b0c"},{"name":"Left Antler","color":7,"origin":[-1.25,33.25,-2.5],"rotation":[90,-2.5,5],"export":true,"visibility":true,"locked":false,"render_order":"default","allow_mirror_modeling":true,"vertices":{"NciP":[1,0,2],"GGU3":[1,0,-2],"lWMS":[-1,0,2],"IGar":[-1,0,-2]},"faces":{"2LuoOPSO":{"uv":{"lWMS":[2,16],"IGar":[2,12],"GGU3":[4,12],"NciP":[4,16]},"vertices":["NciP","GGU3","IGar","lWMS"],"texture":1}},"type":"mesh","uuid":"7dce6773-054b-e7fc-1a58-f7cfe9195e86"},{"name":"Left Ear","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.25,30.5,-2.5],"to":[-1.25,35.5,-0.5],"autouv":0,"color":0,"rotation":[22.5,0,0],"origin":[-2.25,30.5,-1.5],"uv_offset":[0,16],"faces":{"north":{"uv":[2,18,5,23],"texture":1},"east":{"uv":[0,18,2,23],"texture":1},"south":{"uv":[7,18,10,23],"texture":1},"west":{"uv":[5,18,7,23],"texture":1},"up":{"uv":[5,18,2,16],"texture":1},"down":{"uv":[8,16,5,18],"texture":1}},"type":"cube","uuid":"e805b72c-22c5-b1b8-e668-5592df136505"},{"name":"Left Ear","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4.5,34,-3.25],"to":[-1,36,-0.25],"autouv":0,"color":4,"rotation":[25,5,13],"origin":[-2.5,34,-2.25],"uv_offset":[10,18],"faces":{"north":{"uv":[13,21,16,23],"texture":1},"east":{"uv":[10,21,13,23],"texture":1},"south":{"uv":[19,21,22,23],"texture":1},"west":{"uv":[16,21,19,23],"texture":1},"up":{"uv":[16,21,13,18],"texture":1},"down":{"uv":[19,18,16,21],"texture":1}},"type":"cube","uuid":"39d39fcf-db44-01b9-15c8-7f593739f6da"},{"name":"Left Ear","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-6.75,33,-4.5],"to":[-2.75,34,-0.5],"autouv":0,"color":4,"rotation":[-15,25,10],"origin":[-5,33,-5.5],"uv_offset":[22,18],"faces":{"north":{"uv":[26,22,30,23],"texture":1},"east":{"uv":[22,22,26,23],"texture":1},"south":{"uv":[34,22,38,23],"texture":1},"west":{"uv":[30,22,34,23],"texture":1},"up":{"uv":[30,22,26,18],"texture":1},"down":{"uv":[34,18,30,22],"texture":1}},"type":"cube","uuid":"14fd0361-c2ab-ce1e-379b-7340ae188373"},{"name":"Right Ear","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1.25,30.5,-2.5],"to":[4.25,35.5,-0.5],"autouv":0,"color":0,"mirror_uv":true,"rotation":[22.5,0,0],"origin":[2.25,30.5,-1.5],"uv_offset":[0,23],"faces":{"north":{"uv":[5,25,2,30],"texture":1},"east":{"uv":[7,25,5,30],"texture":1},"south":{"uv":[10,25,7,30],"texture":1},"west":{"uv":[2,25,0,30],"texture":1},"up":{"uv":[2,25,5,23],"texture":1},"down":{"uv":[5,23,8,25],"texture":1}},"type":"cube","uuid":"36360ecb-e129-c2d6-e8e9-e2d34176dff3"},{"name":"Right Ear","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[1,34,-3.25],"to":[4.5,36,-0.25],"autouv":0,"color":4,"mirror_uv":true,"rotation":[25,-5,-13],"origin":[2.5,34,-2.25],"uv_offset":[10,25],"faces":{"north":{"uv":[16,28,13,30],"texture":1},"east":{"uv":[19,28,16,30],"texture":1},"south":{"uv":[22,28,19,30],"texture":1},"west":{"uv":[13,28,10,30],"texture":1},"up":{"uv":[13,28,16,25],"texture":1},"down":{"uv":[16,25,19,28],"texture":1}},"type":"cube","uuid":"58a251a3-eb7b-647c-539e-9df8f8c3c588"},{"name":"Right Ear","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[2.75,33,-4.5],"to":[6.75,34,-0.5],"autouv":0,"color":4,"mirror_uv":true,"rotation":[-15,-25,-10],"origin":[5,33,-5.5],"uv_offset":[22,25],"faces":{"north":{"uv":[30,29,26,30],"texture":1},"east":{"uv":[34,29,30,30],"texture":1},"south":{"uv":[38,29,34,30],"texture":1},"west":{"uv":[26,29,22,30],"texture":1},"up":{"uv":[26,29,30,25],"texture":1},"down":{"uv":[30,25,34,29],"texture":1}},"type":"cube","uuid":"8c07596d-9c30-dd17-3897-9676d41f18d0"}],"outliner":[{"name":"Head","origin":[0,24,0],"color":0,"uuid":"f2d9fcec-bc44-34d1-cef2-17a72b18eb67","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["29c62794-e1cd-826e-514c-fff0a4ed4624","1bae97f2-ad48-a058-2ecd-afd375b05556","52d78b2d-74e6-0c58-6287-a6cf85551b7c",{"name":"Antlers","origin":[0,32,-1.75],"color":0,"uuid":"577f4681-5ed8-51f1-4c50-01ae2ba6af00","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["f8fc2e09-b189-7612-7505-744c819f7b0c","7dce6773-054b-e7fc-1a58-f7cfe9195e86"]},{"name":"Ears","origin":[0.25,32,0],"color":0,"uuid":"745e723c-e83b-6043-3767-d7229b6ea048","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":[{"name":"LeftEar","origin":[-1.75,31.5,-1.5],"rotation":[-35,0,22.5],"color":0,"uuid":"066beedc-6526-dec1-0d61-ce1836bdb6cd","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["e805b72c-22c5-b1b8-e668-5592df136505",{"name":"LeftEar2","origin":[-1.5,35,-0.5],"rotation":[0,0,-12.5],"color":0,"uuid":"d2314210-6a0e-5248-900a-bcf90e04515d","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["39d39fcf-db44-01b9-15c8-7f593739f6da",{"name":"LeftEar3","origin":[-2.75,35,-2],"color":0,"uuid":"bfdb1a0a-5e58-559c-3faa-63bec2cfdc23","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["14fd0361-c2ab-ce1e-379b-7340ae188373"]}]}]},{"name":"RightEar","origin":[1.75,31.5,-1.5],"rotation":[-35,0,-22.5],"color":0,"uuid":"9009809a-7334-dae1-3416-0a49e48cd03e","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["36360ecb-e129-c2d6-e8e9-e2d34176dff3",{"name":"RightEar2","origin":[1.5,35,-0.5],"rotation":[0,0,12.5],"color":0,"uuid":"b02335f6-c200-d6ee-2351-1aa3fdf4b890","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["58a251a3-eb7b-647c-539e-9df8f8c3c588",{"name":"RightEar3","origin":[2.75,35,-2],"color":0,"uuid":"6ba3f451-fec6-d535-f11f-497dc772386c","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["8c07596d-9c30-dd17-3897-9676d41f18d0"]}]}]}]}]},{"name":"Body","origin":[0,24,0],"color":0,"uuid":"50d1f6d3-22a5-9089-5fdb-5dab26a22c5a","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["a5e50aef-7389-c5a8-d202-09d4c91a26d1","c2a6ca2e-e446-9557-b737-f86da58b58eb",{"name":"Tail","origin":[0,14,2],"rotation":[30,0,0],"color":0,"uuid":"2d852aa5-1c07-398c-32e3-beadccb47537","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["6456e056-b5be-0eb8-790f-a58325394220",{"name":"Tail2","origin":[0,13.5,6],"rotation":[-7.5,0,0],"color":0,"uuid":"b57e74da-9b49-ff1f-7c6d-45d865b0f01d","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["ff534eb2-3019-4904-eb0d-555b2061a2ef",{"name":"Tail3","origin":[0,13.5,9],"color":0,"uuid":"70b0d870-e1e6-6c2f-82db-39040e3ac437","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["167bc448-32e7-d70d-ffdf-ccb8d82185f9"]}]}]}]},{"name":"RightArm","origin":[5,22,0],"color":0,"uuid":"09babfb1-9746-5a2d-fe6d-303867c39237","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["d45e4e2e-fb66-6baf-88f9-2ecfe050051f","9098eb9f-fca5-6396-f040-5a03609f2b6c"]},{"name":"LeftArm","origin":[-5,22,0],"color":0,"uuid":"5599990c-a4bc-563b-fc68-0b5020ef5238","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["13126cce-ad1f-c013-ee4f-ff135e5f8412","692d3dd8-7d95-56eb-c120-128e47ad6589"]},{"name":"RightLeg","origin":[1.9,12,0],"color":0,"uuid":"6f1b53f0-41e9-2321-7482-359e820deca3","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["7b91d518-075a-5435-7fd8-43eb3b67c9e7","824d4fed-c8cd-e10e-d3ab-e2268c72bade"]},{"name":"LeftLeg","origin":[-1.9,12,0],"color":0,"uuid":"00b62f2d-b985-5891-5fd7-587ba62f5418","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"selected":false,"children":["26778122-e4df-4d15-3b26-7b6ac360a6be","c771fc82-9130-3692-c9d3-330de2be4325"]}],"textures":[{"name":"main.png","folder":"","namespace":"","id":"0","group":"","width":64,"height":64,"uv_width":64,"uv_height":64,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"9a4a9576-3d02-b89c-05e4-84fd41994c0f","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":false,"saved":true,"uuid":"94519627-5572-046b-7b92-8440e08cc01f","relative_path":"../textures/main.png"},{"name":"extras.png","folder":"block","namespace":"","id":"1","group":"","width":64,"height":64,"uv_width":64,"uv_height":64,"particle":false,"use_as_default":false,"layers_enabled":false,"sync_to_project":"b68a033d-26b6-d9ac-af2c-d9303d49a361","render_mode":"default","render_sides":"auto","pbr_channel":"color","frame_time":1,"frame_order_type":"loop","frame_order":"","frame_interpolate":false,"visible":true,"internal":false,"saved":true,"uuid":"9ee13a66-7dca-d937-c697-ee9f6e63b4a6","relative_path":"../textures/extras.png"}]} diff --git a/scripts/libs/SquAPI.lua b/scripts/libs/SquAPI.lua new file mode 100644 index 0000000..0ee29a9 --- /dev/null +++ b/scripts/libs/SquAPI.lua @@ -0,0 +1,1178 @@ + +--[[-------------------------------------------------------------------------------------- +███████╗ ██████╗ ██╗ ██╗██╗███████╗██╗ ██╗██╗ ██╗ █████╗ ██████╗ ██╗ +██╔════╝██╔═══██╗██║ ██║██║██╔════╝██║ ██║╚██╗ ██╔╝ ██╔══██╗██╔══██╗██║ +███████╗██║ ██║██║ ██║██║███████╗███████║ ╚████╔╝ ███████║██████╔╝██║ +╚════██║██║▄▄ ██║██║ ██║██║╚════██║██╔══██║ ╚██╔╝ ██╔══██║██╔═══╝ ██║ +███████║╚██████╔╝╚██████╔╝██║███████║██║ ██║ ██║ ██║ ██║██║ ██║ +╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ +--]]--------------------------------------------------------------------------------------ANSI Shadow + +-- Author: Squishy +-- Discord tag: @mrsirsquishy + +-- Version: 1.0.0 +-- Legal: ARR + +-- Special Thanks to +-- @jimmyhelp for errors and just generally helping me get things working. + +-- IMPORTANT FOR NEW USERS!!! READ THIS!!! + +-- Thank you for using SquAPI! Unless you're experienced and wish to actually modify the functionality +-- of this script, I wouldn't reccomend snooping around. +-- Don't know exactly what you're doing? This site contains a guide on how to use!(also linked on github): +-- https://mrsirsquishy.notion.site/Squishy-API-Guide-3e72692e93a248b5bd88353c96d8e6c5 + +-- This SquAPI file does have some mini-documentation on paramaters if you need like a quick reference, but +-- do not modify, and do not copy-paste code from this file unless you are an avid scripter who knows what they are doing. + + +-- Don't be afraid to ask me for help, just make sure to provide as much info as possible so I or someone can help you faster. + + + + + + +--setup stuff + +-- Locates SquAssets, if it exists +-- Written by FOX +local squassets +local scripts = listFiles("/", true) +for _, path in pairs(scripts) do + local search = string.find(path, "SquAssets") + if search then + squassets = require(path) + end +end +-- If SquAssets doesn't exist then error +if not squassets then + error("§4Missing SquAssets file! Make sure to download that from the GitHub too!§c") +end + +local squapi = {} + + +-- SQUAPI CONTROL VARIABLES AND CONFIG ---------------------------------------------------------- +------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------- +-- these variables can be changed to control certain features of squapi. + + +--when true it will automatically tick and update all the functions, when false it won't do that. +--if false, you can run each objects respective tick/update functions on your own - better control. +squapi.autoFunctionUpdates = true + + +-- FUNCTIONS -------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------- + + + +-- TAIL PHYSICS +-- guide:(note if it has a * that means you can leave it blank to use reccomended settings) +-- tailSegmentList: the list of each individual tail segment of your tail +-- *idleXMovement: how much the tail should sway side to side +-- *idleYMovement: how much the tail should sway up and down +-- *idleXSpeed: how fast the tail should sway side to side +-- *idleYSpeed: how fast the tail should sway up and down +-- *bendStrength: how strongly the tail moves when you move +-- *velocityPush: this will cause the tail to bend when you move forward/backward, good if your tail is bent downward or upward. +-- *initialMovementOffset: this will offset the tails initial sway, this is good for when you have multiple tails and you want to desync them +-- *offsetBetweenSegments: how much each tail segment should be offset from the previous one +-- *stiffness: how stiff the tail should be +-- *bounce: how bouncy the tail should be +-- *flyingOffset: when flying, riptiding, or swimming, it may look strange to have the tail stick out, so instead it will rotate to this value(so use this to flatten your tail during these movements) +-- *downLimit: the lowest each tail segment can rotate +-- *upLimit: the highest each tail segment can rotate + +squapi.tails = {} +squapi.tail = {} +squapi.tail.__index = squapi.tail +function squapi.tail:new(tailSegmentList, idleXMovement, idleYMovement, idleXSpeed, idleYSpeed, bendStrength, velocityPush, initialMovementOffset, offsetBetweenSegments, stiffness, bounce, flyingOffset, downLimit, upLimit) + local self = setmetatable({}, squapi.tail) + + -- INIT ------------------------------------------------------------------------- + --error checker + if type(tailSegmentList) == "ModelPart" then + tailSegmentList = {tailSegmentList} + end + assert(type(tailSegmentList) == "table", + "your tailSegmentList table seems to to be incorrect") + + self.berps = {} + self.targets = {} + self.stiffness = stiffness or .005 + self.bounce = bounce or .9 + self.downLimit = downLimit or -90 + self.upLimit = upLimit or 45 + for i = 1, #tailSegmentList do + assert(tailSegmentList[i]:getType() == "GROUP", + "§4The tail segment at position "..i.." of the table is not a group. The tail segments need to be groups that are nested inside the previous segment.§c") + self.berps[i] = {squassets.BERP:new(self.stiffness, self.bounce), squassets.BERP:new(self.stiffness, self.bounce, self.downLimit, self.upLimit)} + self.targets[i] = {0, 0} + end + + self.tailSegmentList = tailSegmentList + self.idleXMovement = idleXMovement or 15 + self.idleYMovement = idleYMovement or 5 + self.idleXSpeed = idleXSpeed or 1.2 + self.idleYSpeed = idleYSpeed or 2 + self.bendStrength = bendStrength or 2 + self.velocityPush = velocityPush or 0 + self.initialMovementOffset = initialMovementOffset or 0 + self.flyingOffset = flyingOffset or 90 + self.offsetBetweenSegments = offsetBetweenSegments or 1 + + + -- CONTROL ------------------------------------------------------------------------- + + -- UPDATES ------------------------------------------------------------------------- + + self.currentBodyRot = 0 + self.oldBodyRot = 0 + self.bodyRotSpeed = 0 + + function self:tick() + self.oldBodyRot = self.currentBodyRot + self.currentBodyRot = player:getBodyYaw() + self.bodyRotSpeed = math.max(math.min(self.currentBodyRot-self.oldBodyRot, 20), -20) + + local time = world.getTime() + local vel = squassets.forwardVel() + local yvel = squassets.verticalVel() + local svel = squassets.sideVel() + local bendStrength = self.bendStrength/(math.abs((yvel*30))+vel*30 + 1) + local pose = player:getPose() + + for i = 1, #self.tailSegmentList do + self.targets[i][1] = math.sin((time * self.idleXSpeed)/10 - (i)) * self.idleXMovement + self.targets[i][2] = math.sin((time * self.idleYSpeed)/10 - (i * self.offsetBetweenSegments) + self.initialMovementOffset) * self.idleYMovement + + self.targets[i][1] = self.targets[i][1] + self.bodyRotSpeed*self.bendStrength + svel*self.bendStrength*40 + self.targets[i][2] = self.targets[i][2] + yvel * 15 * self.bendStrength - vel*self.bendStrength*15*self.velocityPush + + if i == 1 then + if pose == "FALL_FLYING" or pose == "SWIMMING" or player:riptideSpinning() then + self.targets[i][2] = self.flyingOffset + end + end + + end + + end + + function self:render(dt, context) + local pose = player:getPose() + if pose ~= "SLEEPING" then + for i, tail in ipairs(self.tailSegmentList) do + tail:setOffsetRot( + self.berps[i][2]:berp(self.targets[i][2], dt), + self.berps[i][1]:berp(self.targets[i][1], dt), + 0 + ) + end + else + + end + end + + + table.insert(squapi.ears, self) + return self +end + + +-- EAR PHYSICS +-- guide:(note if it has a * that means you can leave it blank to use reccomended settings) +-- leftEar: the left ear's model path +-- *rightEar: the right ear's model path, if you don't have a right ear, just leave this blank or set to nil +-- *rangeMultiplier: how far the ears should rotate with your head, reccomended 1 +-- *horizontalEars: if you have elf-like ears(ears that stick out horizontally), set this to true +-- *bendStrength: how much the ears should move when you move, reccomended 2 +-- *doEarFlick: whether or not the ears should randomly flick, reccomended true +-- *earFlickChance: how often the ears should flick, reccomended 400 +-- *earStiffness: how stiff the ears should be, reccomended 0.1 +-- *earBounce: how bouncy the ears should be, reccomended 0.8 + +squapi.ears = {} +squapi.ear = {} +squapi.ear.__index = squapi.ear +function squapi.ear:new(leftEar, rightEar, rangeMultiplier, horizontalEars, bendStrength, doEarFlick, earFlickChance, earStiffness, earBounce) + local self = setmetatable({}, squapi.ear) + + -- INIT ------------------------------------------------------------------------- + + assert(leftEar, + "§4The first ear's model path is incorrect.§c") + self.leftEar = leftEar + self.rightEar = rightEar + self.horizontalEars = horizontalEars + self.rangeMultiplier = rangeMultiplier or 1 + if self.horizontalEars then self.rangeMultiplier = self.rangeMultiplier/2 end + self.bendStrength = bendStrength or 2 + local earStiffness = earStiffness or 0.1 + local earBounce = earBounce or 0.8 + + if doEarFlick == nil then doEarFlick = true end + self.doEarFlick = doEarFlick + self.earFlickChance = earFlickChance or 400 + + -- CONTROL ------------------------------------------------------------------------- + + self.enabled = true + function self:toggle() + self.enabled = not self.enabled + end + function self:disable() + self.enabled = false + end + function self:enable() + self.enabled = true + end + + -- UPDATES ------------------------------------------------------------------------- + + self.eary = squassets.BERP:new(earStiffness, earBounce) + self.earx = squassets.BERP:new(earStiffness, earBounce) + self.earz = squassets.BERP:new(earStiffness, earBounce) + self.targets = {0,0,0} + self.oldpose = "STANDING" + function self:tick() + if self.enabled then + local vel = math.min(math.max(-0.75, squassets.forwardVel()), 0.75) + local yvel = math.min(math.max(-1.5, squassets.verticalVel()), 1.5)*5 + local svel = math.min(math.max(-0.5, squassets.sideVel()),0.5) + local headrot = squassets.getHeadRot() + local bend = self.bendStrength + if headrot[1] < -22.5 then bend = -bend end + + --gives the ears a short push when crouching/uncrouching + local pose = player:getPose() + if pose == "CROUCHING" and self.oldpose == "STANDING" then + self.eary.vel = self.eary.vel + 5 * self.bendStrength + elseif pose == "STANDING" and self.oldpose == "CROUCHING" then + self.eary.vel = self.eary.vel - 5 * self.bendStrength + end + self.oldpose = pose + + --main physics + if self.horizontalEars then + local rot = 10*bend*(yvel + vel*10) + headrot[1] * self.rangeMultiplier + local addrot = headrot[2] * self.rangeMultiplier + self.targets[2] = rot + addrot + self.targets[3] = -rot + addrot + else + self.targets[1] = headrot[1] * self.rangeMultiplier + 2*bend*(yvel + vel * 15) + self.targets[2] = headrot[2] * self.rangeMultiplier - svel*100*self.bendStrength + self.targets[3] = self.targets[2] + end + + --ear flicking + if self.doEarFlick then + if math.random(0, self.earFlickChance) == 1 then + if math.random(0, 1) == 1 then + self.earx.vel = self.earx.vel + 50 + else + self.earz.vel = self.earz.vel - 50 + end + end + end + + else + leftEar:setOffsetRot(0,0,0) + rightEar:setOffsetRot(0,0,0) + end + end + + function self:render(dt, context) + if self.enabled then + self.eary:berp(self.targets[1], dt) + self.earx:berp(self.targets[2], dt) + self.earz:berp(self.targets[3], dt) + + local rot3 = self.earx.pos/4 + local rot3b = self.earz.pos/4 + + if self.horizontalEars then + local y = self.eary.pos/4 + self.leftEar:setOffsetRot(y, self.earx.pos/3, rot3) + if self.rightEar then + self.rightEar:setOffsetRot(y, self.earz.pos/3, rot3b) + end + else + self.leftEar:setOffsetRot(self.eary.pos, rot3, rot3) + if self.rightEar then + self.rightEar:setOffsetRot(self.eary.pos, rot3b, rot3b) + end + end + end + end + + table.insert(squapi.ears, self) + return self +end + + +--CROUCH ANIMATION +-- guide:(note if it has a * that means you can leave it blank to use reccomended settings) +-- crouch: the animation to play when you crouch. Make sure this animation is on "hold on last frame" and override. +-- *uncrouch: the animation to play when you uncrouch. make sure to set to "play once" and set to override. If it's just a pose with no actual animation, than you should leave this blank or set to nil +-- *crawl: same as crouch but for crawling +-- *uncrawl: same as uncrouch but for crawling + +function squapi.crouch(crouch, uncrouch, crawl, uncrawl) + + local oldstate = "STANDING" + function events.render(dt, context) + local pose = player:getPose() + if pose == "SWIMMING" and not player:isInWater() then pose = "CRAWLING" end + + if pose == "CROUCHING" then + if uncrouch ~= nil then + uncrouch:stop() + end + crouch:play() + elseif oldstate == "CROUCHING" then + crouch:stop() + if uncrouch ~= nil then + uncrouch:play() + end + elseif crawl ~= nil then + if pose == "CRAWLING" then + if uncrawl ~= nil then + uncrawl:stop() + end + crawl:play() + elseif oldstate == "CRAWLING" then + crawl:stop() + if uncrawl ~= nil then + uncrawl:play() + end + end + end + + oldstate = pose + end +end + + + +--BEWB PHYSICS +-- guide:(note if it has a * that means you can leave it blank to use reccomended settings) +-- element: the bewb element that you want to affect(models.[modelname].path) +-- bendability(2): how much the bewb should move when you move +-- stiff(0.05): how stiff the bewb should be +-- bounce(0.9): how bouncy the bewb should be +-- doIdle(true): whether or not the bewb should have an idle sway(like breathing) +-- idleStrength(4): how much the bewb should sway when idle +-- idleSpeed(1): how fast the bewb should sway when idle +-- downLimit(-10): the lowest the bewb can rotate +-- upLimit(25): the highest the bewb can rotate + +squapi.bewbs = {} +squapi.bewb = {} +squapi.bewb.__index = squapi.bewb +function squapi.bewb:new(element, bendability, stiff, bounce, doIdle, idleStrength, idleSpeed, downLimit, upLimit) + local self = setmetatable({}, squapi.bewb) + + -- INIT ------------------------------------------------------------------------- + assert(element,"§4Your model path for bewb is incorrect.§c") + self.element = element + if doIdle == nil then doIdle = true end + self.doIdle = doIdle + self.bendability = bendability or 2 + self.bewby = squassets.BERP:new(stiff or 0.05, bounce or 0.9, downLimit or -10, upLimit or 25 ) + self.idleStrength = idleStrength or 4 + self.idleSpeed = idleSpeed or 1 + self.target = 0 + + -- CONTROL ------------------------------------------------------------------------- + + self.enabled = true + function self:toggle() + self.enabled = not self.enabled + end + function self:disable() + self.enabled = false + end + function self:enable() + self.enabled = true + end + + + -- UPDATE ------------------------------------------------------------------------- + + self.oldpose = "STANDING" + function self:tick() + if self.enabled then + local vel = squassets.forwardVel() + local yvel = squassets.verticalVel() + local worldtime = world.getTime() + + if self.doIdle then + self.target = math.sin(worldtime/8*self.idleSpeed)*self.idleStrength + end + + --physics when crouching/uncrouching + local pose = player:getPose() + if pose == "CROUCHING" and self.oldpose == "STANDING" then + self.bewby.vel = self.bewby.vel + self.bendability + elseif pose == "STANDING" and self.oldpose == "CROUCHING" then + self.bewby.vel = self.bewby.vel - self.bendability + end + self.oldpose = pose + + --physics when moving + self.bewby.vel = self.bewby.vel - yvel * self.bendability + self.bewby.vel = self.bewby.vel - vel * self.bendability + else + self.target = 0 + end + end + + function self:render(dt, context) + self.element:setOffsetRot(self.bewby:berp(self.target, dt),0,0) + end + + table.insert(squapi.bewbs, self) + return self +end + + +--RANDOM ANIMATION OBJECT +--this object will take in an animation and plays it randomly every tick by a specified amount. +--animation: the animation to play +--*chanceRange: an optional paramater that sets the range. 0 means every tick, larger values mean lower chances of playing every tick. +--*isBlink: if this is for blinking set this to true so that it doesn't blink while sleeping. + +squapi.randimation = {} +squapi.randimation.__index = squapi.randimation +function squapi.randimation:new(animation, chanceRange, isBlink) + local self = setmetatable({}, squapi.randimation) + + -- INIT ------------------------------------------------------------------------- + self.isBlink = isBlink + self.animation = animation + self.chanceRange = chanceRange or 200 + + + -- CONTROL ------------------------------------------------------------------------- + + self.enabled = true + function self:toggle() + self.enabled = not self.enabled + end + function self:disable() + self.enabled = false + end + function self:enable() + self.enabled = true + end + + -- UPDATES ------------------------------------------------------------------------- + + function events.tick() + if self.enabled and (not self.isBlink or player:getPose() ~= "SLEEPING") and math.random(0, self.chanceRange) == 0 and self.animation:isStopped() then + self.animation:play() + end + end + + return self +end + + +-- MOVING EYES +--guide:(note if it has a * that means you can leave it blank to use reccomended settings) +-- element: the eye element that is going to be moved, each eye is seperate. +-- *leftdistance: the distance from the eye to it's leftmost posistion +-- *rightdistance: the distance from the eye to it's rightmost posistion +-- *updistance: the distance from the eye to it's upmost posistion +-- *downdistance: the distance from the eye to it's downmost posistion +squapi.eyes = {} +squapi.eye = {} +squapi.eye.__index = squapi.eye +function squapi.eye:new(element, leftDistance, rightDistance, upDistance, downDistance, switchValues) + local self = setmetatable({}, squapi.eye) + + -- INIT ------------------------------------------------------------------------- + assert(element, + "§4Your eye model path is incorrect.§c") + self.switchValues = switchValues or false + self.left = leftDistance or .25 + self.right = rightDistance or 1.25 + self.up = upDistance or 0.5 + self.down = downDistance or 0.5 + + self.x = 0 + self.y = 0 + self.eyeScale = 1 + + -- CONTROL ------------------------------------------------------------------------- + + --For funzies if you want to change the scale of the eyes you can use this.(lerps to scale) + function self:setEyeScale(scale) + self.eyeScale = scale + end + + self.enabled = true + function self:toggle() + self.enabled = not self.enabled + end + function self:disable() + self.enabled = false + end + function self:enable() + self.enabled = true + end + + --resets position + function self:zero() + self.x, self.y = 0, 0 + end + + -- UPDATES ------------------------------------------------------------------------- + + function self:tick() + if self.enabled then + local headrot = squassets.getHeadRot() + headrot[2] = math.max(math.min(50, headrot[2]), -50) + + --parabolic curve so that you can control the middle position of the eyes. + self.x = -squassets.parabolagraph(-50, -self.left, 0,0, 50, self.right, headrot[2]) + self.y = squassets.parabolagraph(-90, -self.down, 0,0, 90, self.up, headrot[1]) + + --prevents any eye shenanigans + self.x = math.max(math.min(self.left, self.x), -self.right) + self.y = math.max(math.min(self.up, self.y), -self.down) + end + + end + + function self:render(dt, context) + local c = element:getPos() + if self.switchValues then + element:setPos(0,math.lerp(c[2], self.y, dt),math.lerp(c[3], -self.x, dt)) + else + element:setPos(math.lerp(c[1], self.x, dt),math.lerp(c[2], self.y, dt),0) + end + local scale = math.lerp(element:getOffsetScale()[1], self.eyeScale, dt) + element:setOffsetScale(scale, scale, scale) + end + + table.insert(squapi.eyes, self) + return self +end + + +-- HOVER POINT ITEM +-- guide:(note if it has a * that means you can leave it blank to use reccomended settings) +-- element: the element you are moving. Make sure that your element has +-- *springStrength(0.2):how strongly the object is pulled to it's original spot +-- *mass(5): how heavy the object is(heavier accelerate/deccelerate slower) +-- *resistance(1): how much the elements speed decays(like air resistance) +-- *rotationSpeed(0.05):how fast the element should rotate to it's normal rotation +-- *doCollisions(false):whether or not the element should collide with blocks(warning: the system is janky) + +squapi.hoverPoints = {} +squapi.hoverPoint = {} +squapi.hoverPoint.__index = squapi.hoverPoint +function squapi.hoverPoint:new(element, springStrength, mass, resistance, rotationSpeed, doCollisions) + local self = setmetatable({}, squapi.hoverPoint) + + -- INIT ------------------------------------------------------------------------- + self.element = element + assert(self.element, + "§4The Hover point's model path is incorrect.§c") + self.element:setParentType("WORLD") + + + self.springStrength = springStrength or 0.2 + self.mass = mass or 5 + self.resistance = resistance or 1 + self.rotationSpeed = rotationSpeed or 0.05 + self.doCollisions = doCollisions + + -- CONTROL ------------------------------------------------------------------------- + + self.enabled = true + function self:toggle() + self.enabled = not self.enabled + end + function self:disable() + self.enabled = false + end + function self:enable() + self.enabled = true + end + + -- returns to normal position + function self:reset() + local yaw = math.rad(player:getBodyYaw()) + local sin, cos = math.sin(yaw), math.cos(yaw) + local offset = vec( + cos*self.elementOffset.x - sin*self.elementOffset.z, + self.elementOffset.y, + sin*self.elementOffset.x + cos*self.elementOffset.z + ) + self.element:setPos((player:getPos() - self.elementOffset + offset)*16) + end + + self.pos = vec(0,0,0) + self.vel = vec(0,0,0) + + -- UPDATES ------------------------------------------------------------------------- + + self.elementOffset = vec(0,0,0) + self.init = true + self.delay = 0 + + function self:tick() + if self.enabled then + if self.init then + self.init = false + self.pos = player:getPos() + self.elementOffset = self.element:partToWorldMatrix():apply() + self.element:setPos(self.pos*16) + self.element:setOffsetRot(0,-player:getBodyYaw(),0) + end + + local yaw = math.rad(player:getBodyYaw()) + local sin, cos = math.sin(yaw), math.cos(yaw) + + --adjusts the target based on the players rotation + local offset = vec( + cos*self.elementOffset.x - sin*self.elementOffset.z, + self.elementOffset.y, + sin*self.elementOffset.x + cos*self.elementOffset.z + ) + + local target = (player:getPos() - self.elementOffset) + offset + local pos = self.element:partToWorldMatrix():apply() + local dif = self.pos - target + + local force = vec(0,0,0) + + if self.delay == 0 then + --behold my very janky collision system + if self.doCollisions and world.getBlockState(pos):getCollisionShape()[1] then + local block, hitPos, side = raycast:block(pos-self.vel*2, pos) + self.pos = self.pos + (hitPos - pos) + if side == "east" or side == "west" then + self.vel.x = -self.vel.x*0.5 + elseif side == "north" or side == "south" then + self.vel.z = -self.vel.z*0.5 + else + self.vel.y = -self.vel.y*0.5 + end + self.delay = 2 + else + force = force - dif*self.springStrength --spring force + end + else + self.delay = self.delay - 1 + end + force = force -self.vel*self.resistance --resistive force(based on air resistance) + + self.vel = self.vel + force/self.mass + self.pos = self.pos + self.vel + + + end + end + + function self:render(dt, context) + self.element:setPos( + math.lerp(self.element:getPos(), self.pos*16, dt/2) + ) + self.element:setOffsetRot(0, math.lerp(self.element:getOffsetRot()[2], -player:getBodyYaw(), dt*self.rotationSpeed), 0) + end + + table.insert(squapi.hoverPoints, self) + return self +end + + + + +-- LEG MOVEMENT - will make an element mimic the rotation of a vanilla leg, but allows you to control the strength. Good for different length legs or legs under dresses. +-- guide:(note if it has a * that means you can leave it blank to use reccomended settings) +-- element: the element you want to apply the movement to +-- *strength(1): how much it rotates(1 is default, 0.5 is half, 2 is double, etc.) +-- *isRight(false): if this is the right leg or not +-- *keepPosition(true): if you want the element to keep it's position as well. + +squapi.legs = {} +squapi.leg = {} +squapi.leg.__index = squapi.leg +function squapi.leg:new(element, strength, isRight, keepPosition) + local self = squassets.vanillaElement:new(element, strength, keepPosition) + + -- INIT ------------------------------------------------------------------------- + if isRight == nil then isRight = false end + self.isRight = isRight + + -- CONTROL ------------------------------------------------------------------------- + + -- UPDATES ------------------------------------------------------------------------- + + function self:getVanilla() + if self.isRight then + self.rot = vanilla_model.RIGHT_LEG:getOriginRot() + self.pos = vanilla_model.RIGHT_LEG:getOriginPos() + else + self.rot = vanilla_model.LEFT_LEG:getOriginRot() + self.pos = vanilla_model.LEFT_LEG:getOriginPos() + end + return self.rot, self.pos + end + + table.insert(squapi.legs, self) + return self +end + +-- ARM MOVEMENT - will make an element mimic the rotation of a vanilla arm, but allows you to control the strength. Good for different length arms. +-- guide:(note if it has a * that means you can leave it blank to use reccomended settings) +-- element: the element you want to apply the movement to +-- *strength(1): how much it rotates(1 is default, 0.5 is half, 2 is double, etc.) +-- *isRight(false): if this is the right arm or not +-- *keepPosition(true): if you want the element to keep it's position as well. + +squapi.arms = {} +squapi.arm = {} +squapi.arm.__index = squapi.arm +function squapi.arm:new(element, strength, isRight, keepPosition) + local self = squassets.vanillaElement:new(element, strength, keepPosition) + + -- INIT ------------------------------------------------------------------------- + if isRight == nil then isRight = false end + self.isRight = isRight + + -- CONTROL ------------------------------------------------------------------------- + + --inherits functions from squassets.vanillaElement + + -- UPDATES ------------------------------------------------------------------------- + + function self:getVanilla() + if self.isRight then + self.rot = vanilla_model.RIGHT_ARM:getOriginRot() + else + self.rot = vanilla_model.LEFT_ARM:getOriginRot() + end + self.pos = -vanilla_model.LEFT_ARM:getOriginPos() + return self.rot, self.pos + end + + table.insert(squapi.arms, self) + return self +end + + + +-- SMOOTH HEAD - Mimics a vanilla player head, but smoother and with some extra life. can also do smooth Torsos and Smooth Necks! +-- guide:(note if it has a * that means you can leave it blank to use reccomended settings) +-- element: The head element that you wish to effect +-- *strength: The target rotation is multiplied by this factor. For example setting to 1 will follow vanilla rotation, 0.5 is half of that, and 2 is double vanilla rotation. +-- *tilt: For context the smooth head applies a slight tilt to the head as it's rotated toward the side, this controls the strength of that tilt. +-- *speed: How fast the head will rotate toward the target rotation. For example 1 is base speed, 0.5 is half of that, and 2 is double speed. +-- *keepOriginalHeadPos: When true(automatically true) the heads position will follow the vanilla head position. For example when crouching the head will shift down to follow. set to false to disable. + +--Smooth Neck? Smooth Torso? +--This can do that too if you change what you input for these: +-- element: Instead of a single element, input a table of head elements(imagine it like {element1, element2, etc.}). This will apply the head rotations to each of these. +-- *strength: Instead of an single number, you can put in a table(imagine it like {strength1, strength2, etc.}). This will apply each strength to each respective element.(make sure it is the same length as your element table) +-- As a tip, you can imagine the strength as a percentage of the heads vanilla rotation. +-- So if you have a head and a torso, you might do 0.5 for the head, and 0.5 for the torso to add up to 1(100% of the vanilla heads rotation), or maybe even 0.25 for torso, and 0.75 for head, it's up to you! + +squapi.smoothHeads = {} +squapi.smoothHead = {} +squapi.smoothHead.__index = squapi.smoothHead +function squapi.smoothHead:new(element, strength, tilt, speed, keepOriginalHeadPos) + local self = setmetatable({}, squapi.smoothHead) + + -- INIT ------------------------------------------------------------------------- + if type(element) == "ModelPart" then + assert(element, "§4Your model path for smoothHead is incorrect.§c") + element = {element} + end + assert(type(element) == "table", "§4your element table seems to to be incorrect.§c") + + for i = 1, #element do + assert(element[i]:getType() == "GROUP", + "§4The head element at position "..i.." of the table is not a group. The head elements need to be groups that are nested inside one another to function properly.§c") + assert(element[i], "§4The head segment at position "..i.." is incorrect.§c") + element[i]:setParentType("NONE") + end + self.element = element + + self.strength = strength or 1 + if type(self.strength) == "number" then + local strengthDiv = self.strength/#element + self.strength = {} + for i = 1, #element do + self.strength[i] = strengthDiv + end + end + + self.tilt = tilt or 0.1 + if keepOriginalHeadPos == nil then keepOriginalHeadPos = true end + self.keepOriginalHeadPos = keepOriginalHeadPos + self.headRot = vec(0, 0, 0) + self.offset = vec(0, 0, 0) + self.speed = (speed or 1)/2 + + -- CONTROL ------------------------------------------------------------------------- + + + -- Applies an offset to the heads rotation to more easily modify it. Applies as a vector.(for multisegments it will modify the target rotation) + function self:setOffset(xRot, yRot, zRot) + self.offset = vec(xRot, yRot, zRot) + end + + self.enabled = true + function self:toggle() + self.enabled = not self.enabled + end + function self:disable() + self.enabled = false + end + function self:enable() + self.enabled = true + end + + function self:zero() + for i, v in pairs(self.element) do + v:setPos(0, 0, 0) + v:setOffsetRot(0, 0, 0) + self.headRot = vec(0,0,0) + end + end + + + + -- UPDATE ------------------------------------------------------------------------- + function self:tick() + if self.enabled then + local vanillaHeadRot = squassets.getHeadRot() + + self.headRot[1] = self.headRot[1] + (vanillaHeadRot[1] - self.headRot[1])*self.speed + self.headRot[2] = self.headRot[2] + (vanillaHeadRot[2] - self.headRot[2])*self.speed + self.headRot[3] = self.headRot[2]*self.tilt + end + end + + function self:render(dt, context) + if self.enabled then + dt = dt/5 + for i, v in pairs(self.element) do + local c = self.element[i]:getOffsetRot() + local target = (self.headRot*self.strength[i])-self.offset/#self.element + self.element[i]:setOffsetRot(math.lerp(c[1], target[1], dt), math.lerp(c[2], target[2], dt), math.lerp(c[3], target[3], dt)) + + -- Better Combat SquAPI Compatibility created by @jimmyhelp and @foxy2526 on Discord + if renderer:isFirstPerson() and context == "RENDER" then + self.element[i]:setVisible(false) + else + self.element[i]:setVisible(true) + end + end + + if self.keepOriginalHeadPos then + self.element[#self.element]:setPos(-vanilla_model.HEAD:getOriginPos()) + end + end + end + + table.insert(squapi.smoothHeads, self) + return self +end + + + + + + +--BOUNCE WALK +-- guide:(note if it has a * that means you can leave it blank/nil to use reccomended settings) +-- model: the path to your model element. Most cases, if you're model is named "model", than it'd be models.model (replace model with the name of your model) +-- *bounceMultipler: normally 1, this multiples how much the bounce occurs. values greater than 1 will increase bounce, and values less than 1 will decrease bounce. + +squapi.bounceWalks = {} +squapi.bounceWalk = {} +squapi.bounceWalk.__index = squapi.bounceWalk +function squapi.bounceWalk:new(model, bounceMultiplier) + local self = setmetatable({}, squapi.bounceWalk) + -- INIT ------------------------------------------------------------------------- + assert(model, "Your model path is incorrect for bounceWalk") + self.bounceMultiplier = bounceMultiplier or 1 + self.target = 0 + + -- CONTROL ------------------------------------------------------------------------- + + self.enabled = true + function self:toggle() + self.enabled = not self.enabled + end + function self:disable() + self.enabled = false + end + function self:enable() + self.enabled = true + end + + -- UPDATES ------------------------------------------------------------------------- + + function self:render(dt, context) + local pose = player:getPose() + if self.enabled and (pose == "STANDING" or pose == "CROUCHING") then + + local leftlegrot = vanilla_model.LEFT_LEG:getOriginRot()[1] + local bounce = self.bounceMultiplier + if pose == "CROUCHING" then + bounce = bounce/2 + end + self.target = math.abs(leftlegrot)/40*bounce + + else + self.target = 0 + end + model:setPos(0, math.lerp(model:getPos()[2], self.target, dt), 0) + end + + table.insert(squapi.bounceWalks, self) + return self +end + + + + +--TAUR PHYSICS +-- guide: (note if it has a * that means you can leave it blank) +-- taurBody: the group of the body that contains all parts of the actual centaur part of the body, pivot should be placed near the connection between body and taurs body +-- *frontLegs: the group that contains both front legs +-- *backLegs: the group that contains both back legs + +squapi.taurs = {} +squapi.taur = {} +squapi.taur.__index = squapi.taur +function squapi.taur:new(taurBody, frontLegs, backLegs) + local self = setmetatable({}, squapi.taur) + -- INIT ------------------------------------------------------------------------- + assert(taurBody, "§4Your model path for the body in taurPhysics is incorrect.§c") + --assert(frontLegs, "§4Your model path for the front legs in taurPhysics is incorrect.§c") + --assert(backLegs, "§4Your model path for the back legs in taurPhysics is incorrect.§c") + self.taurBody = taurBody + self.frontLegs = frontLegs + self.backLegs = backLegs + self.taur = squassets.BERP:new(0.01, 0.5) + self.target = 0 + + -- CONTROL ------------------------------------------------------------------------- + self.enabled = true + function self:toggle() + self.enabled = not self.enabled + end + function self:disable() + self.enabled = false + end + function self:enable() + self.enabled = true + end + + + -- UPDATES ------------------------------------------------------------------------- + + function self:tick() + if self.enabled then + self.target = math.min(math.max(-30, squassets.verticalVel() * 40), 45) + end + end + + function self:render(dt, context) + if self.enabled then + self.taur:berp(self.target, dt/2) + local pose = player:getPose() + + if pose == "FALL_FLYING" or pose == "SWIMMING" or (player:isClimbing() and not player:isOnGround()) or player:riptideSpinning() then + self.taurBody:setRot(80, 0, 0) + if self.backLegs then + self.backLegs:setRot(-50, 0, 0) + end + if self.frontLegs then + self.frontLegs:setRot(-50, 0, 0) + end + else + self.taurBody:setRot(self.taur.pos, 0, 0) + if self.backLegs then + self.backLegs:setRot(self.taur.pos*3, 0, 0) + end + if self.frontLegs then + self.frontLegs:setRot(-self.taur.pos*3, 0, 0) + end + end + end + end + + table.insert(squapi.taurs, self) + return self +end + + + +-- CUSTOM FIRST PERSON HAND +--!!Make sure the setting for modifying first person hands is enabled in the Figura settings for this to work properly!! +-- guide:(note if it has a * that means you can leave it blank/nil to use reccomended settings) +-- element: the actual hand element to change +-- *x: the x change +-- *y: the y change +-- *z: the z change +-- *scale: this will multiply the size of the element by this size +-- *onlyVisibleInFP:if true, this will make the element invisible when not in first person + +squapi.FPHands = {} +squapi.FPHand = {} +squapi.FPHand.__index = squapi.FPHand +function squapi.FPHand:new(element, x, y, z, scale, onlyVisibleInFP) + local self = setmetatable(self, squapi.FPHand) + + -- INIT ------------------------------------------------------------------------- + assert(element, "Your First Person Hand path is incorrect") + element:setParentType("RightArm") + self.element = element + self.x = x or 0 + self.y = y or 0 + self.z = z or 0 + self.scale = scale or 1 + self.onlyVisibleInFP = onlyVisibleInFP + + -- CONTROL ------------------------------------------------------------------------- + + function self:updatePos(x, y, z) + self.x = x + self.y = y + self.z = z + end + + -- UPDATES ------------------------------------------------------------------------- + function self:render(dt, context) + if context == "FIRST_PERSON" then + if self.onlyVisibleInFP then + self.element:setVisible(true) + end + self.element:setPos(self.x, self.y, self.z) + self.element:setScale(self.scale,self.scale,self.scale) + else + if self.onlyVisibleInFP then + self.element:setVisible(false) + end + self.element:setPos(0,0,0) + end + + end + + table.insert(squapi.FPHands, self) + return self +end + +-- Easy-use Animated Texture. +-- guide:(note if it has a * that means you can leave it blank to use reccomended settings) +-- element: the part of your model who's texture will be aniamted +-- numberOfFrames: the number of frames +-- framePercent: what percent width/height the uv takes up of the whole texture. for example: if there is a 100x100 texture, and the uv is 20x20, this will be .20 +-- *slowFactor: increase this to slow down the animation. +-- *vertical: set to true if you'd like the animation frames to go down instead of right. +function squapi.animateTexture(element, numberOfFrames, framePercent, slowFactor, vertical) + assert(element, + "§4Your model path for animateTexture is incorrect.§c") + vertical = vertical or false + slowFactor = slowFactor or 1 + function events.tick() + local time = world.getTime() + local frameshift = math.floor(time/slowFactor)%numberOfFrames*framePercent + if vertical then element:setUV(0, frameshift) else element:setUV(frameshift, 0) end + end +end + + + +-- UPDATES ALL SQUAPI FEATURES -------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------- + +if squapi.autoFunctionUpdates then + + function events.tick() + for i, v in pairs(squapi.smoothHeads) do + v:tick() + end + for i, v in pairs(squapi.eyes) do + v:tick() + end + for i, v in pairs(squapi.bewbs) do + v:tick() + end + for i, v in pairs(squapi.hoverPoints) do + v:tick() + end + for i, v in pairs(squapi.ears) do + v:tick() + end + for i, v in pairs(squapi.tails) do + v:tick() + end + for i, v in pairs(squapi.taurs) do + v:tick() + end + end + + function events.render(dt, context) + for i, v in pairs(squapi.smoothHeads) do + v:render(dt, context) + end + for i, v in pairs(squapi.FPHands) do + v:render(dt, context) + end + for i, v in pairs(squapi.bounceWalks) do + v:render(dt, context) + end + for i, v in pairs(squapi.legs) do + v:render(dt, context) + end + for i, v in pairs(squapi.arms) do + v:render(dt, context) + end + for i, v in pairs(squapi.eyes) do + v:render(dt, context) + end + for i, v in pairs(squapi.bewbs) do + v:render(dt, context) + end + for i, v in pairs(squapi.hoverPoints) do + v:render(dt, context) + end + for i, v in pairs(squapi.ears) do + v:render(dt, context) + end + for i, v in pairs(squapi.tails) do + v:render(dt, context) + end + for i, v in pairs(squapi.taurs) do + v:render(dt, context) + end + end + +end + + + + + + + +return squapi diff --git a/scripts/libs/SquAssets.lua b/scripts/libs/SquAssets.lua new file mode 100644 index 0000000..744726e --- /dev/null +++ b/scripts/libs/SquAssets.lua @@ -0,0 +1,428 @@ +--[[-------------------------------------------------------------------------------------- + ____ _ _ _ _ + / ___| __ _ _ _(_)___| |__ _ _ / \ ___ ___ ___| |_ ___ + \___ \ / _` | | | | / __| '_ \| | | | / _ \ / __/ __|/ _ \ __/ __| + ___) | (_| | |_| | \__ \ | | | |_| | / ___ \\__ \__ \ __/ |_\__ \ + |____/ \__, |\__,_|_|___/_| |_|\__, | /_/ \_\___/___/\___|\__|___/ + |_| |___/ +--]]--------------------------------------------------------------------------------------Standard + +--[[ +-- Author: Squishy +-- Discord tag: @mrsirsquishy + +-- Version: 1.0.0 +-- Legal: ARR + +Framework Functions and classes for SquAPI. +This contains some math functions, some simplified calls to figura features, some debugging scripts for convenience, and classes used in SquAPI or for debugging. + +You can also make use of these functions, however it's for more advanced scripters. remember to call: local squassets = require("SquAssets") + + +]] + + + +local squassets = {} + +--Useful Calls +-------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------- + +--detects the fluid the player is in(air is nil), and if they are fully submerged in that fluid. +--vanilla player has an eye height of 1.5 which is used by default for checking if it's submerged, but you can optionally modify this for different height avatars +function squassets.getFluid(eyeHeight) + local fluid + local B = world.getBlockState(player:getPos() + vec(0, eyeHeight or 1.5, 0)) + local submerged = B.id == "minecraft:water" or B.id == "minecraft:lava" + + if player:isInWater() then + fluid = "WATER" + elseif player:isInLava() then + fluid = "LAVA" + end + return fluid, submerged +end + +--better isOnGround, taken from the figura wiki +function squassets.isOnGround() + return world.getBlockState(thisEntity:getPos():add(0, -0.1, 0)):isSolidBlock() +end + +-- returns how fast the player moves forward, negative means backward +function squassets.forwardVel() + return player:getVelocity():dot((player:getLookDir().x_z):normalize()) +end + +-- returns y velocity(negative is down) +function squassets.verticalVel() + return player:getVelocity()[2] +end + +-- returns how fast player moves sideways, negative means left +-- Courtesy of @auriafoxgirl on discord +function squassets.sideVel() + return (player:getVelocity() * matrices.rotation3(0, player:getRot().y, 0)).x +end + +--returns a cleaner vanilla head rotation value to use +function squassets.getHeadRot() + return (vanilla_model.HEAD:getOriginRot()+180)%360-180 +end + + + + +--Math Functions +-------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------- + +--polar to cartesian coordiantes +function squassets.PTOC(r, theta) + return r*math.cos(theta), r*math.sin(theta) +end + +--cartesian to polar coordinates +function squassets.CTOP(x, y) + return squassets.pyth(x, y), math.atan(y/x) +end + +--3D polar to cartesian coordiantes(returns vec3) +function squassets.PTOC3(R, theta, phi) + local r, y = squassets.PTOC(R, phi) + local x, z = squassets.PTOC(r, theta) + return vec(x, y, z) +end + +--3D cartesian to polar coordinates +function squassets.CTOP3(x, y, z) + local v + if type(x) == "Vector3" then + v = x + else + v = vec(x, y, z) + end + local R = v:length() + + return R, math.atan2(v.z, v.x), math.asin(v.y/R) +end + +--pythagorean theoremn +function squassets.pyth(a, b) + return math.sqrt(a^2 + b^2) +end + + +--checks if a point is within a box +function squassets.pointInBox(point, corner1, corner2) + if not (point and corner1 and corner2) then return false end + return + point.x >= corner1.x and point.x <= corner2.x and + point.y >= corner1.y and point.y <= corner2.y and + point.z >= corner1.z and point.z <= corner2.z +end + +--returns true if the number is within range, false otherwise +function squassets.inRange(lower, num, upper) + return lower <= num and num <= upper +end + +-- Linear graph +-- locally generates a graph between two points, returns the y value at t on that graph +function squassets.lineargraph(x1, y1, x2, y2, t) + local slope = (y2-y1)/(x2-x1) + local inter = y2 - slope*x2 + return slope*t + inter +end + +--Parabolic graph +--locally generates a parabolic graph between three points, returns the y value at t on that graph +function squassets.parabolagraph(x1, y1, x2, y2, x3, y3, t) + local denom = (x1 - x2) * (x1 - x3) * (x2 - x3) + + local a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / denom + local b = (x3^2 * (y1 - y2) + x2^2 * (y3 - y1) + x1^2 * (y2 - y3)) / denom + local c = (x2 * x3 * (x2 - x3) * y1 + x3 * x1 * (x3 - x1) * y2 + x1 * x2 * (x1 - x2) * y3) / denom + + return a * t^2 + b * t + c +end + +--returns 1 if num is >= 0, returns -1 if less than 0 +function squassets.sign(num) + if num < 0 then + return -1 + end + return 1 +end + +--returns a vector with the signs of each vector(shows the direction of each vector) +function squassets.Vec3Dir(v) + return vec(squassets.sign(v.x), squassets.sign(v.y), squassets.sign(v.z)) +end + +--raises all values in a vector to a power +function squassets.Vec3Pow(v, power) + local power = power or 2 + return vec(math.pow(v.x, power), math.pow(v.y, power), math.pow(v.z, power)) +end + + + + + + + + +--Debug/Display Functions +-------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------- + +--displays the corners of a bounding box, good for debugging +---@param corner1 vector coordinate of first corner +---@param corner2 vector coordinate of second corner +---@param color vector of the color, or a string of one of the preset colors +function squassets.bbox(corner1, corner2, color) + dx = corner2[1] - corner1[1] + dy = corner2[2] - corner1[2] + dz = corner2[3] - corner1[3] + squassets.pointMarker(corner1, color) + squassets.pointMarker(corner2, color) + squassets.pointMarker(corner1 + vec(dx,0,0), color) + squassets.pointMarker(corner1 + vec(dx,dy,0), color) + squassets.pointMarker(corner1 + vec(dx,0,dz), color) + squassets.pointMarker(corner1 + vec(0,dy,0), color) + squassets.pointMarker(corner1 + vec(0,dy,dz), color) + squassets.pointMarker(corner1 + vec(0,0,dz), color) +end + + +--draws a sphere +function squassets.sphereMarker(pos, radius, color, colorCenter, quality) + local pos = pos or vec(0, 0, 0) + local r = radius or 1 + local quality = (quality or 1)*10 + local colorCenter = colorCenter or color + + + -- Draw the center point + squassets.pointMarker(pos, colorCenter) + + -- Draw surface points + for i = 1, quality do + for j = 1, quality do + local theta = (i / quality) * 2 * math.pi + local phi = (j / quality) * math.pi + + local x = pos.x + r * math.sin(phi) * math.cos(theta) + local y = pos.y + r * math.sin(phi) * math.sin(theta) + local z = pos.z + r * math.cos(phi) + + squassets.pointMarker(vec(x, y, z), color) + end + end +end + +--draws a line between two points with particles, higher density is more particles +function squassets.line(corner1, corner2, color, density) + local l = (corner2 - corner1):length() -- Length of the line + local direction = (corner2 - corner1):normalize() -- Direction vector + local density = density or 10 + + for i = 0, l, 1/density do + local pos = corner1 + direction * i -- Interpolate position + squassets.pointMarker(pos, color) -- Create a particle at the interpolated position + end +end + +--displays a particle at a point, good for debugging +---@param pos vector coordinate where it will render +---@param color vector of the color, or a string of one of the preset colors +function squassets.pointMarker(pos, color) + if type(color) == "string" then + if color == "R" then color = vec(1, 0, 0) + elseif color == "G" then color = vec(0, 1, 0) + elseif color == "B" then color = vec(0, 0, 1) + elseif color == "yellow" then color = vec(1, 1, 0) + elseif color == "purple" then color = vec(1, 0, 1) + elseif color == "cyan" then color = vec(0, 1, 1) + elseif color == "black" then color = vec(0, 0, 0) + else + color = vec(1,1,1) + end + else + color = color or vec(1,1,1) + end + particles:newParticle("minecraft:wax_on", pos):setSize(0.5):setLifetime(0):setColor(color) +end + + + + + + + + + +--Classes +-------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------- + +squassets.vanillaElement = {} +squassets.vanillaElement.__index = squassets.vanillaElement +function squassets.vanillaElement:new(element, strength, keepPosition) + local self = setmetatable({}, squassets.vanillaElement) + + -- INIT ------------------------------------------------------------------------- + self.keepPosition = keepPosition + if keepPosition == nil then self.keepPosition = true end + self.element = element + self.element:setParentType("NONE") + self.strength = strength or 1 + self.rot = vec(0,0,0) + self.pos = vec(0,0,0) + + -- CONTROL ------------------------------------------------------------------------- + + self.enabled = true + function self:disable() + self.enabled = false + end + function self:enable() + self.enabled = true + end + function self:toggle() + self.enabled = not self.enabled + end + --returns it to normal attributes + function self:zero() + self.element:setOffsetRot(0, 0, 0) + self.element:setPos(0, 0, 0) + end + --get the current rot/pos + function self:getPos() + return self.pos + end + function self:getRot() + return self.rot + end + + -- UPDATES ------------------------------------------------------------------------- + + function self:render(dt, context) + if self.enabled then + local rot, pos = self:getVanilla() + self.element:setOffsetRot(rot*self.strength) + if self.keepPosition then + self.element:setPos(pos) + end + end + end + + return self +end + +squassets.BERP3D = {} +squassets.BERP3D.__index = squassets.BERP3D +function squassets.BERP3D:new(stiff, bounce, lowerLimit, upperLimit, initialPos, initialVel) + local self = setmetatable({}, squassets.BERP3D) + + self.stiff = stiff or 0.1 + self.bounce = bounce or 0.1 + self.pos = initialPos or vec(0, 0, 0) + self.vel = initialVel or vec(0, 0, 0) + self.acc = vec(0, 0, 0) + self.lower = lowerLimit or {nil, nil, nil} + self.upper = upperLimit or {nil, nil, nil} + + --target is the target position + --dt, or delta time, the time between now and the last update(delta from the events.update() function) + --if you want it to have a different stiff or bounce when run input a different stiff bounce + function self:berp(target, dt, stiff, bounce) + local target = target or vec(0,0,0) + local dt = dt or 1 + + for i = 1, 3 do + --certified bouncy math + local dif = (target[i]) - self.pos[i] + self.acc[i] = ((dif * math.min(stiff or self.stiff, 1)) * dt) --based off of spring force F = -kx + self.vel[i] = self.vel[i] + self.acc[i] + + --changes the position, but adds a bouncy bit that both overshoots and decays the movement + self.pos[i] = self.pos[i] + (dif * (1-math.min(bounce or self.bounce, 1)) + self.vel[i]) * dt + + --limits range + + if self.upper[i] and self.pos[i] > self.upper[i] then + self.pos[i] = self.upper[i] + self.vel[i] = 0 + elseif self.lower[i] and self.pos[i] < self.lower[i] then + self.pos[i] = self.lower + self.vel[i] = 0 + end + end + + --returns position so that you can immediately apply the position as it is changed. + return self.pos + end + + return self +end + + + +--stiffness factor, > 0 +--bounce factor, reccomended when in range of 0-1. bigger is bouncier. +--if you want to limit the positioning, use lowerlimit and upperlimit, or leave nil +squassets.BERP = {} +squassets.BERP.__index = squassets.BERP +function squassets.BERP:new(stiff, bounce, lowerLimit, upperLimit, initialPos, initialVel) + local self = setmetatable({}, squassets.BERP) + + self.stiff = stiff or 0.1 + self.bounce = bounce or 0.1 + self.pos = initialPos or 0 + self.vel = initialVel or 0 + self.acc = 0 + self.lower = lowerLimit or nil + self.upper = upperLimit or nil + + --target is the target position + --dt, or delta time, the time between now and the last update(delta from the events.update() function) + --if you want it to have a different stiff or bounce when run input a different stiff bounce + function self:berp(target, dt, stiff, bounce) + local dt = dt or 1 + + --certified bouncy math + local dif = (target or 10) - self.pos + self.acc = ((dif * math.min(stiff or self.stiff, 1)) * dt) --based off of spring force F = -kx + self.vel = self.vel + self.acc + + --changes the position, but adds a bouncy bit that both overshoots and decays the movement + self.pos = self.pos + (dif * (1-math.min(bounce or self.bounce, 1)) + self.vel) * dt + + --limits range + + if self.upper and self.pos > self.upper then + self.pos = self.upper + self.vel = 0 + elseif self.lower and self.pos < self.lower then + self.pos = self.lower + self.vel = 0 + end + + --returns position so that you can immediately apply the position as it is changed. + return self.pos + end + + + + return self +end + + +return squassets diff --git a/scripts/libs/gradient.lua b/scripts/libs/gradient.lua new file mode 100644 index 0000000..6af0dc3 --- /dev/null +++ b/scripts/libs/gradient.lua @@ -0,0 +1,370 @@ +-- work in pewgross +-- do not share !! + +---@class UnitRGBA: Vector4 [r, g, b, a]; all values are 0-1. +local UnitRGBA = {}; + +---@param vector Vector4 a Vector4 matching the format of UnitRGBA ([r, g, b, a]; all values are 0-1) +---@return UnitRGBA? output nil the vector with the new metatable if valid, otherwise nil +---@return string? error a string describing the error if invalid, otherwise nil +---@nodiscard +---@see UnitRGBA.fromUnitStrict to throw an error instead of returning nil +function UnitRGBA.fromUnit(vector) + for i, v in ipairs({vector:unpack()}) do + if type(v) ~= "number" then + return nil, "Vector component " .. i .. " is not a number"; + end + if v < 0 or v > 1 then + return nil, "Vector component " .. i .. " is not in the range 0-1; got " .. v; + end + end + return setmetatable({ vector = vector }, UnitRGBA), nil; +end +---@param vector Vector4 a Vector4 matching the format of UnitRGBA ([r, g, b, a]; all values are 0-1) +---@return UnitRGBA vector the vector with the new metatable if valid. otherwise, an error is thrown +---@nodiscard +---@see UnitRGBA.fromUnit to return nil instead of throwing an error +function UnitRGBA.fromUnitStrict(vector) + local result, why = UnitRGBA.fromUnit(vector); + if result == nil then + error("Invalid vector: " .. why); + end + return result; +end +---@param vector Vector4 a Vector4 matching the format of UnitRGBA ([r, g, b, a]; all values are 0-255) +---@return UnitRGBA? output nil the vector with the new metatable if valid, otherwise nil +---@return string? error a string describing the error if invalid, otherwise nil +---@nodiscard +---@see UnitRGBA.fromU8Strict to throw an error instead of returning nil +function UnitRGBA.fromU8(vector) + for i, v in ipairs(vector) do + if type(v) ~= "number" then + return nil, "Vector component " .. i .. " is not a number"; + end + if v < 0 or v > 255 then + return nil, "Vector component " .. i .. " is not in the range 0-255"; + end + end + return setmetatable({ vector = vector / 255 }, UnitRGBA), nil; +end +---@param vector Vector4 a Vector4 matching the format of UnitRGBA ([r, g, b, a]; all values are 0-255) +---@return UnitRGBA vector the vector with the new metatable if valid. otherwise, an error is thrown +---@nodiscard +---@see UnitRGBA.fromU8 to return nil instead of throwing an error +function UnitRGBA.fromU8Strict(vector) + local result, why = UnitRGBA.fromU8(vector); + if result == nil then + error("Invalid vector: " .. why); + end + return result; +end + +---@param vector Vector4 a Vector4 matching the format of UnitRGBA ([r, g, b, a]; all values are 0-1) +---@return UnitRGBA vector the vector with the new metatable +---@nodiscard +function UnitRGBA.fromUnitClamping(vector) + vector[1] = math.clamp(vector[1], 0, 1); + vector[2] = math.clamp(vector[2], 0, 1); + vector[3] = math.clamp(vector[3], 0, 1); + vector[4] = math.clamp(vector[4], 0, 1); + return setmetatable({ vector = vector }, UnitRGBA); +end + +---@param hex string a color in hexadecimal format (e.g. "#FF00FF") +---@return UnitRGBA? output the color with the new metatable if valid, otherwise nil +---@return string? error a string describing the error if invalid, otherwise nil +---@nodiscard +---@see UnitRGBA.fromHexStrict to throw an error instead of returning nil +function UnitRGBA.fromHex(hex) + if type(hex) ~= "string" then + return nil, "not a string"; + end + if hex:sub(1, 1) == "#" then + hex = hex:sub(2); + end + if hex:len() ~= 6 and hex:len() ~= 8 then + return nil, "not 6 or 8 characters long"; + end + local vector = vec(0, 0, 0, 1); + for i = 1, 3 do + local value = tonumber(hex:sub(i * 2 - 1, i * 2), 16); + if value == nil then + return nil, "contains non-hex characters"; + end + vector[i] = value / 255; + end + if hex:len() == 8 then + local value = tonumber(hex:sub(7, 8), 16); + if value == nil then + return nil, "contains non-hex characters"; + end + vector[4] = value / 255; + else + vector[4] = 1; + end + return setmetatable({ vector = vector }, UnitRGBA), nil; +end +---@param hex string a color in hexadecimal format (e.g. "#FF00FF") +---@return UnitRGBA color the color with the new metatable if valid. otherwise, an error is thrown +---@nodiscard +---@see UnitRGBA.fromHex to return nil instead of throwing an error +function UnitRGBA.fromHexStrict(hex) + local result, why = UnitRGBA.fromHex(hex); + if result == nil then + error("Invalid hex color: " .. why); + end + return result; +end + +---@return boolean opaque whether the color is *fully opaque* (as in, a == 1, not just a > 0). +---@see UnitRGBA:isTranslucent to see if the color is partially or fully transparent. +---@see UnitRGBA:isTransparent to see if the color is fully transparent. +---@nodiscard +function UnitRGBA:isOpaque() + return self.vector[4] == 1; +end +---@return boolean transparent whether the color is *fully transparent* (as in, a == 0, not just a < 1). +---@see UnitRGBA:isTranslucent to see if the color is partially transparent. +---@see UnitRGBA:isOpaque to see if the color is fully opaque. +---@nodiscard +function UnitRGBA:isTransparent() + return self.vector[4] == 0; +end +---@return boolean translucent whether the color is *translucent* (as in, a < 1). +---@see UnitRGBA:isTransparent to see if the color is fully transparent. +---@see UnitRGBA:isOpaque to see if the color is fully opaque. +---@nodiscard +function UnitRGBA:isTranslucent() + return self.vector[4] < 1; +end + +function UnitRGBA:toVec3() + return vec(self.vector[1], self.vector[2], self.vector[3]); +end + + +---@return Vector4 vector Vector4 with format ([r, g, b, a]; all values are 0-255) +---@nodiscard +function UnitRGBA:toU8vec4() + return vec(self.vector[1] * 255, self.vector[2] * 255, self.vector[3] * 255, self.vector[4] * 255); +end +---@return Vector3 vector Vector3 with format ([r, g, b]; all values are 0-255) +---@nodiscard +function UnitRGBA:toU8vec3() + local a = vec(self.vector[1] * 255, self.vector[2] * 255, self.vector[3] * 255) + return a +end + + +---@param hash? boolean? whether to include the hash symbol (#) in the output. defaults to true +---@param alpha? boolean? whether to include the alpha channel in the output. defaults to true +---@return string hex the color in RGBA hexadecimal format (e.g. "#FF00FF") +---@nodiscard +function UnitRGBA:toHex(hash, alpha) + local hex = (hash == nil or hash) and "#" or ""; + local iter = (alpha == nil or alpha) and 4 or 3; + for i = 1, iter do + local value = math.round(self.vector[i] * 255); + hex = hex .. string.format("%02X", value); + end + return hex; +end + +function UnitRGBA:__index(key) + return rawget(UnitRGBA, key) or self.vector[key]; +end +function UnitRGBA:__tostring() + return "UnitRGBA(" .. table.concat(self.vector, ", ") .. ")"; +end + +function UnitRGBA:__sub(other) + return UnitRGBA.fromUnitClamping(vec( + self.vector[1] - other.vector[1], + self.vector[2] - other.vector[2], + self.vector[3] - other.vector[3], + self.vector[4] - other.vector[4] + )); +end + +--- Transparent white. +UnitRGBA.TRANSPARENT_WHITE = UnitRGBA.fromUnitStrict(vec(1, 1, 1, 0)); +--- Transparent black. +UnitRGBA.TRANSPARENT_BLACK = UnitRGBA.fromUnitStrict(vec(0, 0, 0, 0)); +--- Opaque white. +UnitRGBA.WHITE = UnitRGBA.fromUnitStrict(vec(1, 1, 1, 1)); +--- Opaque black. +UnitRGBA.BLACK = UnitRGBA.fromUnitStrict(vec(0, 0, 0, 1)); + +---@class Keypoint - A keypoint (interpolation point definition) in a gradient. +---@field public color UnitRGBA color of the keypoint +---@field public time number a number between 0 and 1 (inclusive on both ends) +local Keypoint = {}; + +---Returns a new Keypoint object. +---@param color UnitRGBA color of the keypoint +---@param time number a number between 0 and 1 (inclusive on both ends) +---@return Keypoint +---@nodiscard +function Keypoint.new(color, time) + return setmetatable({ + color = color, + time = time + }, Keypoint); +end + +---@class Gradient A gradient definition. +---@field public keypoints Keypoint[] a list of keypoint objects +---@field public loop boolean whether the gradient should loop when given out-of-bounds time values. if false, an error is instead thrown. +local Gradient = {}; + +---Returns a new Gradient object. +---@param keypoints Keypoint[] a list of keypoint objects +---@param loop boolean? whether the gradient should loop when given out-of-bounds time values. if false, an error is instead thrown. defaults to true. +---@return Gradient +---@nodiscard +function Gradient.new(keypoints, loop) + return setmetatable({ + keypoints = keypoints, + loop = loop == nil or loop + }, Gradient); +end + +local function easeInOutQuad(t) + return t < 0.5 and 2 * t * t or 1 - (-2 * t + 2)^2 / 2 +end + +local oklab = require("scripts.libs.oklab") +---Returns the color of the gradient at a given time. +---@param time number a number between 0 and 1 (inclusive on both ends) (can extend beyond this range if Gradient.loop is true) +---@return UnitRGBA? color the color of the gradient at the given time, or nil if the time is out of bounds and Gradient.loop is false +function Gradient:at(time) + if time < 0 or time > 1 then + if not self.loop then + return nil; + end + time = time % 1; + end + + if time == 0 then return self.keypoints[1].color + elseif time == 1 then return self.keypoints[#self.keypoints].color + end + + for i = 1, #self.keypoints - 1 do + local this = self.keypoints[i] + local next = self.keypoints[i + 1] + if time >= this.time and time < next.time then + local t = easeInOutQuad((time - this.time) / (next.time - this.time)); + + local alpha = math.lerp(this.color.a, next.color.a, t) + local La, Aa, Ba = oklab.srgbToLinear(this.color.xyz):unpack() + local Lb, Ab, Bb = oklab.srgbToLinear(next.color.xyz):unpack() + local mixed = vec( + math.lerp(La, Lb, easeInOutQuad(t)), + math.lerp(Aa, Ab, t), + math.lerp(Ba, Bb, t) + ) + + mixed = oklab.linearToSrgb(mixed); + mixed = vec(mixed.x, mixed.y, mixed.z, alpha) + return UnitRGBA.fromUnitClamping(mixed); + end + end + + if #self.keypoints == 1 then + return self.keypoints[1].color; + end + + error("Gradient.at: time " .. time .. " is out of bounds"); +end + +---@param colors string[] a list of colors in hexadecimal format (e.g. "#FF00FF") +---@param loop boolean? whether the gradient should loop when given out-of-bounds time values. if false, an error is instead thrown. defaults to true. +---@return Gradient gradient the gradient with the new metatable if valid. otherwise, an error is thrown +function Gradient.distributedHex(colors, loop) + local keypoints = {}; + for i, hex in ipairs(colors) do + local time = (i - 1) / (#colors - 1); + local color = UnitRGBA.fromHexStrict(hex); + table.insert(keypoints, Keypoint.new(color, time)); + end + return Gradient.new(keypoints, loop); +end + +function Gradient:__index(key) + return rawget(Gradient, key) or self[key]; +end + + +---@class SimpleGradientBuilder +---@field public colors UnitRGBA[] a list of colors +local SimpleGradientBuilder = {}; +---@return SimpleGradientBuilder builder a simple gradient builder +function SimpleGradientBuilder.new() + return setmetatable({ colors = {} }, { __index = SimpleGradientBuilder }); +end +---Reflect the colors of the gradient; this makes it nicer when looping. +---@param reflect_last boolean? whether to also reflect the final color, such that it would appear twice in a row at the middle. if false, it'll only be present once +---@return SimpleGradientBuilder self builder for chaining +function SimpleGradientBuilder:reflect(reflect_last) + ---@type UnitRGBA[] + local reflected = {}; + for i = 1, #self.colors - 1 do + table.insert(reflected, self.colors[i]); + end + table.insert(reflected, self.colors[#self.colors]) + if reflect_last then + table.insert(reflected, self.colors[#self.colors]) + end + for i = #self.colors - 1, 1, -1 do + table.insert(reflected, self.colors[i]) + end + self.colors = reflected + return self; +end +---"Thicken" the gradient by duplicating every keypoint. This means that the actual colors themselves will have as much presence as the transitions. +---@param amount? integer how many times to duplicate each keypoint. defaults to 1. +---@return SimpleGradientBuilder self builder for chaining +function SimpleGradientBuilder:thicken(amount) + amount = amount or 1; + local thickened = {}; + for i = 1, #self.colors do + for _ = 0, amount do + table.insert(thickened, self.colors[i]) + end + end + self.colors = thickened; + return self; +end +---@param loop boolean? whether the gradient should loop when given out-of-bounds time values. if false, an error is instead thrown. defaults to true. +---@return Gradient gradient the constructed gradient +function SimpleGradientBuilder:build(loop) + loop = loop == nil or loop; + + ---@type Keypoint[] + local keypoints = {}; + for i, color in ipairs(self.colors) do + table.insert(keypoints, Keypoint.new(color, (i - 1) / (#self.colors - 1))); + end + + return setmetatable({ + keypoints = keypoints, + loop = loop + }, Gradient); +end +---@param colors string[] | string hexadecimal color(s) to add +function SimpleGradientBuilder:add(colors) + if type(colors) == "string" then + colors = {colors} + end + for _, color in ipairs(colors) do + table.insert(self.colors, UnitRGBA.fromHexStrict(color)) + end + return self; +end + +return { + UnitRGBA = UnitRGBA, + Keypoint = Keypoint, + Gradient = Gradient, + SimpleGradientBuilder = SimpleGradientBuilder, +} diff --git a/scripts/libs/oklab.lua b/scripts/libs/oklab.lua new file mode 100644 index 0000000..f12a95a --- /dev/null +++ b/scripts/libs/oklab.lua @@ -0,0 +1,113 @@ +local lib = {} + +---@param x number +---@return number +local function fromLinear(x) + if x >= 0.0031308 then + return 1.055 * math.pow(x, 1.0/2.4) - 0.055 + else + return 12.92 * x + end +end + +---@param x number +---@return number +local function toLinear(x) + if x >= 0.04045 then + return math.pow((x + 0.055)/(1 + 0.055), 2.4) + else + return x / 12.92 + end +end + +---Converts from sRGB to Linear sRGB +---@param color Vector3 +---@return Vector3 +function lib.srgbToLinear(color) + return vec(toLinear(color.x), toLinear(color.y), toLinear(color.z)) +end + +---Converts from Linear sRGB to sRGB +---@param color Vector3 +---@return Vector3 +function lib.linearToSrgb(color) + return vec(fromLinear(color.x), fromLinear(color.y), fromLinear(color.z)) +end + +---Converts from Linear sRGB to OKLAB +---@param color Vector3 +---@return Vector3 +function lib.linearToOklab(color) + local l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b + local m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b + local s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b + + local l_ = math.pow(l, 1/3) + local m_ = math.pow(m, 1/3) + local s_ = math.pow(s, 1/3) + + return vec( + 0.2104542553*l_ + 0.7936177850*m_ - 0.0040720468*s_, + 1.9779984951*l_ - 2.4285922050*m_ + 0.4505937099*s_, + 0.0259040371*l_ + 0.7827717662*m_ - 0.8086757660*s_ + ) +end + +---Converts from OKLAB to Linear sRGB +---@param color Vector3 +---@return Vector3 +function lib.oklabToLinear(color) + local l_ = color.x + 0.3963377774 * color.y + 0.2158037573 * color.z; + local m_ = color.x - 0.1055613458 * color.y - 0.0638541728 * color.z; + local s_ = color.x - 0.0894841775 * color.y - 1.2914855480 * color.z; + + local l = l_*l_*l_; + local m = m_*m_*m_; + local s = s_*s_*s_; + + return vec( + 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, + -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, + -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s + ) +end + +---Converts from LAB to LCh +---@param color Vector3 +---@return Vector3 +function lib.labToLch(color) + return vec( + color.x, + math.sqrt(color.y * color.y + color.z * color.z), + math.deg(math.atan2(color.z, color.y)) + ) +end + +---Converts from LCh to LAB +---@param color Vector3 +---@return Vector3 +function lib.lchToLab(color) + return vec( + color.x, + color.y * math.cos(math.rad(color.z)), + color.y * math.sin(math.rad(color.z)) + ) +end + + + +---Converts from sRGB to OKLCh +---@param color Vector3 +---@return Vector3 +function lib.srgbToOklch(color) + return lib.labToLch(lib.linearToOklab(lib.srgbToLinear(color))) +end + +---Converts from OKLCh to sRGB +---@param color Vector3 +---@return Vector3 +function lib.oklchToSrgb(color) + return lib.linearToSrgb(lib.oklabToLinear(lib.lchToLab(color))) +end + +return lib \ No newline at end of file diff --git a/scripts/libs/utils.lua b/scripts/libs/utils.lua new file mode 100644 index 0000000..5bd6bd7 --- /dev/null +++ b/scripts/libs/utils.lua @@ -0,0 +1,17 @@ +local module = {} + +--- @param model ModelPart +--- @param func fun(model: ModelPart) +function module.forEachNonGroup(model, func) + if model:getType() == "GROUP" then + for _, child in pairs(model:getChildren()) do + module.forEachNonGroup(child, func) + end + end + + if model:getType() == "CUBE" or model:getType() == "MESH" then + func(model) + end +end + +return module diff --git a/scripts/main.lua b/scripts/main.lua new file mode 100644 index 0000000..16bb087 --- /dev/null +++ b/scripts/main.lua @@ -0,0 +1,7 @@ +events.ENTITY_INIT:register(function () + require("scripts.vanilla_model") + require("scripts.nameplate") + require("scripts.soggy") + require("scripts.physics") + require("scripts.wheels.main") +end) diff --git a/scripts/nameplate.lua b/scripts/nameplate.lua new file mode 100644 index 0000000..6f9e9b6 --- /dev/null +++ b/scripts/nameplate.lua @@ -0,0 +1,98 @@ +local g = require("scripts.libs.gradient") + +---@class ColoredText +---@field text string +---@field color string + +---@param name string +---@param gradient Gradient +---@param count number number of gradients to be present in the span +---@param phase_shift number shift of the gradient +---@returns string +local function render_gradient_name(name, gradient, count, phase_shift) + local string_width = client.getTextWidth(name) / count; + local string_width_chars = {}; + for i = 1, #name do + string_width_chars[i] = client.getTextWidth(name:sub(i, i)) + end + + ---@type ColoredText[] + local result = {} + + local acc = 0; + for i = 1, #name do + local offset = acc + string_width_chars[i]; + local color = gradient:at((offset / string_width) + phase_shift); + acc = offset + + result[i] = { + text = name:sub(i, i), + color = color:toHex(true, false) + } + end + + return toJson(result) +end + +---@param name string +---@param gradient Gradient +---@param nameplates Nameplate | Nameplate[] where to use the animated name +---@param count number number of gradients to be present in the span +---@param phase_shift_rate number how fast to shift the gradient +---@returns fun():void # function to unregister +local function animate_gradient_name(name, gradient, nameplates, count, phase_shift_rate) + if type(nameplates) ~= "table" then + nameplates = {nameplates} + end + + local phase_shift = 0; + local function render() + if client.isPaused() then + return + end + + phase_shift = phase_shift + phase_shift_rate + + local result = render_gradient_name(name, gradient, count, phase_shift) + for _, nameplate in pairs(nameplates) do + nameplate:setText(result) + end + end + + events.render:register(render) + return function () + events.render:remove(render) + end +end + +---@param name string +---@param gradient Gradient +---@param nameplates Nameplate | Nameplate[] where to use the static name +---@param count number number of gradients to be present in the span +---@param phase_shift number shift of the gradient +local function static_gradient_name(name, gradient, nameplates, count, phase_shift) + if type(nameplates) ~= "table" then + nameplates = {nameplates} + end + + local result = render_gradient_name(name, gradient, count, phase_shift); + for _, nameplate in pairs(nameplates) do + nameplate:setText(result) + end +end + +local name = "reidlab!" +local gradient_count = 1 +local gradient = g.SimpleGradientBuilder.new() + :add({ "#d87b5a", "#e0ab91" }) + :reflect(false) + :build() + +animate_gradient_name(name, gradient, { + nameplate.ENTITY, + nameplate.LIST +}, gradient_count, 0.005) + +static_gradient_name(name, gradient, { + nameplate.CHAT +}, gradient_count, 0) diff --git a/scripts/physics.lua b/scripts/physics.lua new file mode 100644 index 0000000..bb2ecf1 --- /dev/null +++ b/scripts/physics.lua @@ -0,0 +1,39 @@ +local squapi = require("scripts.libs.SquAPI") + +-- ear physics + +squapi.ear:new( + models.models.main.Head.Ears.LeftEar, + models.models.main.Head.Ears.RightEar, + 0.2, + nil, + nil, + false +) + +squapi.ear:new( + models.models.main.Head.Ears.LeftEar.LeftEar2, + models.models.main.Head.Ears.RightEar.RightEar2, + -0.30, + nil, + nil, + false +) + +-- tail physics + +local tail_segments = { + models.models.main.Body.Tail, + models.models.main.Body.Tail2, + models.models.main.Body.Tail3 +} + +squapi.tail:new( + tail_segments, + nil, + nil, + nil, + nil, + 1, + 5 +) diff --git a/scripts/soggy.lua b/scripts/soggy.lua new file mode 100644 index 0000000..2d8c2eb --- /dev/null +++ b/scripts/soggy.lua @@ -0,0 +1,34 @@ +local utils = require("scripts.libs.utils") + +local tw = 1 --top wetness --wet effect +local bw = 1 --bottom wetness +local driptime = 4 --recommended to change based on how low you have TW/BW + +local offset = vec(0,0,0) +local offset2 = vec(0,0,0) +events.TICK:register(function () + if player:isInRain() then --makes clothes wet if in rain + tw = tw - 0.005 + bw = bw - 0.005 + if tw <= 0.6 then tw = 0.6 end + if bw <= 0.6 then bw = 0.6 end + end + offset = vec((math.random()-0.5), math.random(), (math.random()-0.5)) --random offset of particles + offset2 = vec(math.random(-1,1),math.random(-1,1),math.random(-1,1)) -- velocity + if player:isInWater() then bw = 0.6 end --if player is standing in water, make bottom clothes wet + if player:isUnderwater() then tw = 0.6 end --if player is submerged in water, make top clothes wet + if not player:isUnderwater() and tw ~= 1 and not player:isInRain() then tw = tw + 0.005 end --if not submerged in water, dry top clothes + if not player:isInWater() and bw ~= 1 and not player:isInRain() then bw = bw + 0.005 end --if not standing in water, dry bottom clothes + if bw >= 1 then bw = 1 end + if tw >= 1 then tw = 1 end + if world.getTime() % driptime == 0 and tw ~= 1 and not (player:isUnderwater()) then for _ = 0, driptime*0.5 do particles:newParticle("falling_dripstone_water",player:getPos():add(offset+vec(0,0.7,0)),offset2) end end + if world.getTime() % driptime == 0 and bw ~= 1 and not (player:isInWater()) then for _ = 0, driptime*0.5 do particles:newParticle("falling_dripstone_water",player:getPos():add(offset),offset2:mul(0.5)) end end + + utils.forEachNonGroup(models.models.main.LeftArm, function (part) part:setColor(tw,tw,tw) end) + utils.forEachNonGroup(models.models.main.RightArm, function (part) part:setColor(tw,tw,tw) end) + utils.forEachNonGroup(models.models.main.Head, function (part) part:setColor(tw,tw,tw) end) + utils.forEachNonGroup(models.models.main.Body, function (part) part:setColor(tw,tw,tw) end) + + utils.forEachNonGroup(models.models.main.LeftLeg, function (part) part:setColor(bw,bw,bw) end) + utils.forEachNonGroup(models.models.main.RightLeg, function (part) part:setColor(bw,bw,bw) end) +end) diff --git a/scripts/vanilla_model.lua b/scripts/vanilla_model.lua new file mode 100644 index 0000000..f6c7877 --- /dev/null +++ b/scripts/vanilla_model.lua @@ -0,0 +1,6 @@ +for _, vanillaModel in pairs({ + vanilla_model.PLAYER, + vanilla_model.ARMOR +}) do + vanillaModel:setVisible(false) +end diff --git a/scripts/wheels/main.lua b/scripts/wheels/main.lua new file mode 100644 index 0000000..678dcf2 --- /dev/null +++ b/scripts/wheels/main.lua @@ -0,0 +1,18 @@ +local toggles = require("scripts.wheels.toggles") + +local wheels = { + toggles +} + +for i, v in pairs(wheels) do + if i == 1 then action_wheel:setPage(v) end + if #wheels ~= 1 then + v:newAction() + :title("to next page") + :item("minecraft:arrow") + :onLeftClick(function () + local index = (i + 1) > #wheels and 1 or (i + 1) + action_wheel:setPage(wheels[index]) + end) + end +end diff --git a/scripts/wheels/toggles.lua b/scripts/wheels/toggles.lua new file mode 100644 index 0000000..da9309f --- /dev/null +++ b/scripts/wheels/toggles.lua @@ -0,0 +1,14 @@ +local toggles = action_wheel:newPage() + +function pings.toggleArmor(state) + vanilla_model.ARMOR:setVisible(state) +end + +local toggle_armor = toggles:newAction() + :setToggled(false) + :setOnToggle(pings.toggleArmor) + :title("toggle armor") + :item("red_wool") + :toggleItem("green_wool") + +return toggles diff --git a/textures/extras.png b/textures/extras.png new file mode 100644 index 0000000..74cf67a Binary files /dev/null and b/textures/extras.png differ diff --git a/textures/main.png b/textures/main.png new file mode 100644 index 0000000..a59710a Binary files /dev/null and b/textures/main.png differ