commit e448a542dc9caaf58c754f4ab8ad26deec85dcb9 Author: Patrick <147879351+WinniePatGG@users.noreply.github.com> Date: Fri May 1 19:42:33 2026 +0200 first commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47d576a --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# include specific folders in .idea/ +.idea/* + +# gradle +.gradle/ +build/ + +# eclipse +*.classpath +*.project +*.settings +/bin/ +/subprojects/*/bin/ +.metadata/ +atlassian-ide-plugin.xml + +# NetBeans +.nb-gradle +.nb-gradle-properties + +# Vim +*.sw[nop] + +# Emacs +*~ +\#*\# +.\#* +.kotlin +run + +# Textmate +.textmate + +# Sublime Text +*.sublime-* + +# jEnv +.java-version + +# macOS +.DS_Store + +# HPROF +*.hprof + +# Work dirs +/incoming-distributions +/intTestHomeDir + +# Logs +/*.log + +# delombok +*/src/main/lombok diff --git a/aang/blockbench/air_bending.bbmodel b/aang/blockbench/air_bending.bbmodel new file mode 100644 index 0000000..c9a7676 --- /dev/null +++ b/aang/blockbench/air_bending.bbmodel @@ -0,0 +1 @@ +{"meta":{"format_version":"4.10","model_format":"animated_entity_model","box_uv":true},"name":"air_bending","model_identifier":"","visible_box":[1,1,0],"variable_placeholders":"","variable_placeholder_buttons":[],"timeline_setups":[],"unhandled_root_fields":{},"resolution":{"width":16,"height":16},"elements":[{"name":"bipedHead","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,24,-4],"to":[4,32,4],"autouv":0,"color":1,"origin":[0,0,0],"faces":{"north":{"uv":[8,8,16,16]},"east":{"uv":[0,8,8,16]},"south":{"uv":[24,8,32,16]},"west":{"uv":[16,8,24,16]},"up":{"uv":[16,8,8,0]},"down":{"uv":[24,0,16,8]}},"type":"cube","uuid":"e5841845-2e3f-0f50-b112-0da10d6bac1e"},{"name":"bipedBody","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,12,-2],"to":[4,24,2],"autouv":0,"color":3,"origin":[0,0,0],"uv_offset":[0,16],"faces":{"north":{"uv":[4,20,12,32]},"east":{"uv":[0,20,4,32]},"south":{"uv":[16,20,24,32]},"west":{"uv":[12,20,16,32]},"up":{"uv":[12,20,4,16]},"down":{"uv":[20,16,12,20]}},"type":"cube","uuid":"a6a9f27d-0d1d-91f6-95bb-c69e6c8044c4"},{"name":"bipedRightArm","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[4,12,-2],"to":[8,24,2],"autouv":0,"color":5,"origin":[0,0,0],"uv_offset":[16,32],"faces":{"north":{"uv":[20,36,24,48]},"east":{"uv":[16,36,20,48]},"south":{"uv":[28,36,32,48]},"west":{"uv":[24,36,28,48]},"up":{"uv":[24,36,20,32]},"down":{"uv":[28,32,24,36]}},"type":"cube","uuid":"f916afcf-94f8-26ed-0e3a-52807ea6dd91"},{"name":"bipedLeftArm","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-8,12,-2],"to":[-4,24,2],"autouv":0,"color":7,"origin":[0,0,0],"uv_offset":[32,0],"faces":{"north":{"uv":[36,4,40,16]},"east":{"uv":[32,4,36,16]},"south":{"uv":[44,4,48,16]},"west":{"uv":[40,4,44,16]},"up":{"uv":[40,4,36,0]},"down":{"uv":[44,0,40,4]}},"type":"cube","uuid":"73ee629c-6c19-3c63-7c37-d3c416aa9a99"},{"name":"bipedLeftLeg","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,0,-2],"to":[0,12,2],"autouv":0,"color":9,"origin":[0,0,0],"uv_offset":[0,32],"faces":{"north":{"uv":[4,36,8,48]},"east":{"uv":[0,36,4,48]},"south":{"uv":[12,36,16,48]},"west":{"uv":[8,36,12,48]},"up":{"uv":[8,36,4,32]},"down":{"uv":[12,32,8,36]}},"type":"cube","uuid":"8ee88bce-95e1-f080-3568-e704d5028ab9"},{"name":"bipedRightLeg","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[0,0,-2],"to":[4,12,2],"autouv":0,"color":2,"origin":[0,0,0],"uv_offset":[24,16],"faces":{"north":{"uv":[28,20,32,32]},"east":{"uv":[24,20,28,32]},"south":{"uv":[36,20,40,32]},"west":{"uv":[32,20,36,32]},"up":{"uv":[32,20,28,16]},"down":{"uv":[36,16,32,20]}},"type":"cube","uuid":"80cf2ca9-ce87-05a5-2475-bcf6e2bf2458"}],"outliner":[{"name":"bipedRig","origin":[0,0,0],"color":0,"uuid":"f7d20b62-01ed-bab3-fa05-d56a255afe6b","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"children":[{"name":"bipedHead","origin":[0,24,0],"color":1,"uuid":"f65b6d21-44d9-8222-c6bc-a6d42ede554e","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["e5841845-2e3f-0f50-b112-0da10d6bac1e",{"name":"armorHead","origin":[0,24,0],"color":2,"uuid":"60aca50e-f327-41f4-b4de-efd0d0f43fa8","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedBody","origin":[0,24,0],"color":3,"uuid":"9b034992-30d9-e07e-b869-d1b12f8aa24c","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["a6a9f27d-0d1d-91f6-95bb-c69e6c8044c4",{"name":"armorBody","origin":[0,24,0],"color":4,"uuid":"a0c0e5da-6af5-0e2f-881d-0e4e05e2e29d","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedRightArm","origin":[5,22,0],"color":5,"uuid":"7a1d791a-94df-a32d-4340-13c665e7bb9f","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["f916afcf-94f8-26ed-0e3a-52807ea6dd91",{"name":"armorRightArm","origin":[4,22,0],"color":6,"uuid":"b753db07-83c3-df73-8f84-4775193ed7cd","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedLeftArm","origin":[-5,22,0],"color":7,"uuid":"d3708a24-fc02-e5e3-3652-282e2b7b7ba9","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["73ee629c-6c19-3c63-7c37-d3c416aa9a99",{"name":"armorLeftArm","origin":[-4,22,0],"color":8,"uuid":"b11f5810-cf1f-fb21-0113-bdc9a7709870","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedLeftLeg","origin":[-2,12,0],"color":9,"uuid":"41a8bbae-789c-6c63-7652-76cf641172eb","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["8ee88bce-95e1-f080-3568-e704d5028ab9",{"name":"armorLeftLeg","origin":[-2,12,0],"color":0,"uuid":"959464aa-5959-fc92-a387-be5dcd0ddc45","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]},{"name":"armorLeftBoot","origin":[-2,12,0],"color":1,"uuid":"065cb27b-2980-87d8-28bf-a2cbaf9cd8f1","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedRightLeg","origin":[2,12,0],"color":2,"uuid":"46c209af-600b-324c-bc48-89b6f4326cb2","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["80cf2ca9-ce87-05a5-2475-bcf6e2bf2458",{"name":"armorRightLeg","origin":[2,12,0],"color":3,"uuid":"6f96ee97-660c-8ddd-356d-e1b0d9c2008d","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]},{"name":"armorRightBoot","origin":[2,12,0],"color":4,"uuid":"ec7590f1-8742-6b03-593d-63630e76e720","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]}]}],"textures":[],"animations":[{"uuid":"5769abdb-54e6-f3ba-0772-5776ee03936e","name":"air_bending","loop":"hold","override":false,"length":0.72,"snapping":25,"selected":true,"saved":false,"path":"E:\\Coding\\NoRiskClient\\heroes\\aang\\src\\main\\resources\\assets\\aang\\emotes\\air_bending.animation.json","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"7a1d791a-94df-a32d-4340-13c665e7bb9f":{"name":"bipedRightArm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"55383dbe-f934-5f07-5a83-4882a810581f","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":-69.48873959265939,"y":39.09545106848327,"z":11.285891166173315}],"uuid":"e3fdd817-669a-a4be-c34f-ead89ef12e73","time":0.72,"color":-1,"interpolation":"linear","easing":"easeOutQuint"}]},"d3708a24-fc02-e5e3-3652-282e2b7b7ba9":{"name":"bipedLeftArm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":0,"z":0}],"uuid":"6fd7ad0f-34f9-00f5-0b44-06e1c3aafd06","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":68.53429949436622,"y":-42.00279471254862,"z":-163.26946646431134}],"uuid":"08d46ff9-de92-d565-d25a-d822aced11d2","time":0.72,"color":-1,"interpolation":"linear","easing":"easeOutQuint"}]}}}],"geckoSettings":{"formatVersion":2,"modSDK":"Forge 1.12 - 1.16","objectType":"OBJ_TYPE_ENTITY","entityType":"Entity","javaPackage":"com.example.mod","animFileNamespace":"MODID","animFilePath":"animations/ANIMATIONFILE.json"}} \ No newline at end of file diff --git a/aang/blockbench/air_scooter.bbmodel b/aang/blockbench/air_scooter.bbmodel new file mode 100644 index 0000000..c597d31 --- /dev/null +++ b/aang/blockbench/air_scooter.bbmodel @@ -0,0 +1 @@ +{"meta":{"format_version":"4.10","model_format":"animated_entity_model","box_uv":true},"name":"air_scooter","model_identifier":"","visible_box":[2,3.5,1.25],"variable_placeholders":"","variable_placeholder_buttons":[],"timeline_setups":[],"unhandled_root_fields":{},"resolution":{"width":16,"height":16},"elements":[{"name":"bipedHead","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,24,-4],"to":[4,32,4],"autouv":0,"color":1,"origin":[0,0,0],"faces":{"north":{"uv":[8,8,16,16]},"east":{"uv":[0,8,8,16]},"south":{"uv":[24,8,32,16]},"west":{"uv":[16,8,24,16]},"up":{"uv":[16,8,8,0]},"down":{"uv":[24,0,16,8]}},"type":"cube","uuid":"e5841845-2e3f-0f50-b112-0da10d6bac1e"},{"name":"bipedBody","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,12,-2],"to":[4,24,2],"autouv":0,"color":3,"origin":[0,0,0],"uv_offset":[0,16],"faces":{"north":{"uv":[4,20,12,32]},"east":{"uv":[0,20,4,32]},"south":{"uv":[16,20,24,32]},"west":{"uv":[12,20,16,32]},"up":{"uv":[12,20,4,16]},"down":{"uv":[20,16,12,20]}},"type":"cube","uuid":"a6a9f27d-0d1d-91f6-95bb-c69e6c8044c4"},{"name":"bipedRightArm","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[4,12,-2],"to":[8,24,2],"autouv":0,"color":5,"origin":[0,0,0],"uv_offset":[16,32],"faces":{"north":{"uv":[20,36,24,48]},"east":{"uv":[16,36,20,48]},"south":{"uv":[28,36,32,48]},"west":{"uv":[24,36,28,48]},"up":{"uv":[24,36,20,32]},"down":{"uv":[28,32,24,36]}},"type":"cube","uuid":"f916afcf-94f8-26ed-0e3a-52807ea6dd91"},{"name":"bipedLeftArm","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-8,12,-2],"to":[-4,24,2],"autouv":0,"color":7,"origin":[0,0,0],"uv_offset":[32,0],"faces":{"north":{"uv":[36,4,40,16]},"east":{"uv":[32,4,36,16]},"south":{"uv":[44,4,48,16]},"west":{"uv":[40,4,44,16]},"up":{"uv":[40,4,36,0]},"down":{"uv":[44,0,40,4]}},"type":"cube","uuid":"73ee629c-6c19-3c63-7c37-d3c416aa9a99"},{"name":"bipedLeftLeg","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,0,-2],"to":[0,12,2],"autouv":0,"color":9,"origin":[0,0,0],"uv_offset":[0,32],"faces":{"north":{"uv":[4,36,8,48]},"east":{"uv":[0,36,4,48]},"south":{"uv":[12,36,16,48]},"west":{"uv":[8,36,12,48]},"up":{"uv":[8,36,4,32]},"down":{"uv":[12,32,8,36]}},"type":"cube","uuid":"8ee88bce-95e1-f080-3568-e704d5028ab9"},{"name":"bipedRightLeg","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[0,0,-2],"to":[4,12,2],"autouv":0,"color":2,"origin":[0,0,0],"uv_offset":[24,16],"faces":{"north":{"uv":[28,20,32,32]},"east":{"uv":[24,20,28,32]},"south":{"uv":[36,20,40,32]},"west":{"uv":[32,20,36,32]},"up":{"uv":[32,20,28,16]},"down":{"uv":[36,16,32,20]}},"type":"cube","uuid":"80cf2ca9-ce87-05a5-2475-bcf6e2bf2458"},{"name":"airlocator","position":[-6,12,0],"rotation":[0,0,0],"ignore_inherited_scale":false,"visibility":true,"locked":false,"uuid":"dc665afe-6ae8-a864-7a53-b3929d9076ed","type":"locator"},{"name":"airlocatorrightarm","position":[6,12,0],"rotation":[0,0,0],"ignore_inherited_scale":false,"visibility":true,"locked":false,"uuid":"8f5f5737-d6cc-f3b7-f034-acf35151ef55","type":"locator"}],"outliner":[{"name":"bipedRig","origin":[0,0,0],"color":0,"uuid":"f7d20b62-01ed-bab3-fa05-d56a255afe6b","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"children":[{"name":"bipedHead","origin":[0,24,0],"color":1,"uuid":"f65b6d21-44d9-8222-c6bc-a6d42ede554e","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["e5841845-2e3f-0f50-b112-0da10d6bac1e",{"name":"armorHead","origin":[0,24,0],"color":2,"uuid":"60aca50e-f327-41f4-b4de-efd0d0f43fa8","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedBody","origin":[0,24,0],"color":3,"uuid":"9b034992-30d9-e07e-b869-d1b12f8aa24c","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["a6a9f27d-0d1d-91f6-95bb-c69e6c8044c4",{"name":"armorBody","origin":[0,24,0],"color":4,"uuid":"a0c0e5da-6af5-0e2f-881d-0e4e05e2e29d","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedRightArm","origin":[5,22,0],"color":5,"uuid":"7a1d791a-94df-a32d-4340-13c665e7bb9f","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"children":["f916afcf-94f8-26ed-0e3a-52807ea6dd91",{"name":"armorRightArm","origin":[4,22,0],"color":6,"uuid":"b753db07-83c3-df73-8f84-4775193ed7cd","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"children":[{"name":"airlocatorrightarm","origin":[6,12,0],"color":0,"uuid":"3f43f2e0-525a-5f33-fd8e-8bbdd50726e1","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"children":["8f5f5737-d6cc-f3b7-f034-acf35151ef55"]}]}]},{"name":"bipedLeftArm","origin":[-5,22,0],"color":7,"uuid":"d3708a24-fc02-e5e3-3652-282e2b7b7ba9","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"children":["73ee629c-6c19-3c63-7c37-d3c416aa9a99",{"name":"armorLeftArm","origin":[-4,22,0],"color":8,"uuid":"b11f5810-cf1f-fb21-0113-bdc9a7709870","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"children":[{"name":"airlocator","origin":[-6,12,0],"color":0,"uuid":"9dbee4ce-4355-f9b6-9f0f-cce03b045e2d","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"children":["dc665afe-6ae8-a864-7a53-b3929d9076ed"]}]}]},{"name":"bipedLeftLeg","origin":[-2,12,0],"color":9,"uuid":"41a8bbae-789c-6c63-7652-76cf641172eb","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["8ee88bce-95e1-f080-3568-e704d5028ab9",{"name":"armorLeftLeg","origin":[-2,12,0],"color":0,"uuid":"959464aa-5959-fc92-a387-be5dcd0ddc45","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]},{"name":"armorLeftBoot","origin":[-2,12,0],"color":1,"uuid":"065cb27b-2980-87d8-28bf-a2cbaf9cd8f1","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedRightLeg","origin":[2,12,0],"color":2,"uuid":"46c209af-600b-324c-bc48-89b6f4326cb2","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["80cf2ca9-ce87-05a5-2475-bcf6e2bf2458",{"name":"armorRightLeg","origin":[2,12,0],"color":3,"uuid":"6f96ee97-660c-8ddd-356d-e1b0d9c2008d","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]},{"name":"armorRightBoot","origin":[2,12,0],"color":4,"uuid":"ec7590f1-8742-6b03-593d-63630e76e720","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]}]}],"textures":[],"animations":[{"uuid":"7e5e98ff-38e7-ab6f-5290-a0aae6cd3fdc","name":"air_scooter","loop":"once","override":false,"length":0.84,"snapping":25,"selected":true,"saved":false,"path":"E:\\Coding\\NoRiskClient\\heroes\\aang\\src\\main\\resources\\assets\\aang\\emotes\\air_scooter_2.animation.json","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"f7d20b62-01ed-bab3-fa05-d56a255afe6b":{"name":"bipedRig","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":0,"y":0,"z":0}],"uuid":"80c2504d-810a-3e19-9396-25524ae455ea","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":0,"y":360,"z":0}],"uuid":"ac5f366f-e236-48a7-9ae2-e0aef477cf87","time":0.8,"color":-1,"interpolation":"linear","easing":"easeInExpo"},{"channel":"rotation","data_points":[{"x":0,"y":0,"z":0}],"uuid":"fc10abd9-7425-dbb4-01f0-c95e62de85dd","time":0.84,"color":-1,"interpolation":"linear"}]},"7a1d791a-94df-a32d-4340-13c665e7bb9f":{"name":"bipedRightArm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":-78.99034,"y":24.59477,"z":4.62934}],"uuid":"9089c6ba-4c55-49ce-bab3-8ae5e58ac366","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":73.84641,"y":51.3798,"z":167.24859}],"uuid":"dd92a38f-5233-79d0-07b8-74d36c97194b","time":0.28,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":31.34641,"y":51.3798,"z":167.24859}],"uuid":"03645a83-b1ff-a814-2841-adab415486b6","time":0.56,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":-81.56011,"y":-42.11662,"z":4.58533}],"uuid":"5f593f4a-9004-2bea-e933-329657aaeecb","time":0.8,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":0,"y":0,"z":0}],"uuid":"ad17b95d-07a2-0682-31b5-2c68496f3573","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":0,"y":3,"z":0}],"uuid":"7d881893-850c-2aaa-b3b3-3c915e701da9","time":0.56,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":0,"y":-1,"z":-3}],"uuid":"3d92282e-5290-16b3-dcb1-a925cb1b700c","time":0.8,"color":-1,"interpolation":"linear"}]},"d3708a24-fc02-e5e3-3652-282e2b7b7ba9":{"name":"bipedLeftArm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":86.44785,"y":-33.16347,"z":-168.10969}],"uuid":"c2b1202c-fbe4-e47a-dcef-8ed1910ff120","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":-82.49399,"y":-66.60691,"z":3.05019}],"uuid":"95e21264-ad24-6519-e1fc-be173f2397f7","time":0.28,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":-42.49399,"y":-66.60691,"z":3.05019}],"uuid":"d6beeae0-3902-84a8-e989-6be85535db19","time":0.56,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":-72.50228,"y":13.17432,"z":52.2119}],"uuid":"d9f76f38-dae7-f8cc-2202-46d5b274721d","time":0.8,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":0,"y":0,"z":0}],"uuid":"3b61c8dd-f3f6-bd4a-9863-d57572cc7457","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":0,"y":0,"z":-2}],"uuid":"530b5b3f-1622-2aa8-ad22-5a5bb1333125","time":0.56,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":-1,"y":0,"z":-2}],"uuid":"dddad19e-3f98-36c2-f229-4bfdc8a7ba83","time":0.8,"color":-1,"interpolation":"linear"}]},"effects":{"name":"Effects","type":"effect","keyframes":[{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"b30bd473-9b73-7fed-f5a5-2b84fce8663a","time":0,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1 ","locator":"airlocator","script":"","file":""}],"uuid":"3ea2573f-8fec-6fc5-a3b7-0dbd619917b9","time":0.04,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocatorrightarm","script":"","file":""}],"uuid":"2a6b7cd6-5f4c-a036-caa1-0c3c20900b39","time":0.08,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"53945296-49e5-2c26-0abe-3c2eeeb9cb6e","time":0.12,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocatorrightarm","script":"","file":""}],"uuid":"8054b3d8-b873-b0fa-fd80-4165f2b6c8ab","time":0.16,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"b6093a6f-f590-222e-da18-c3083da97009","time":0.2,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"6ca4542e-36ca-7ea9-a401-049d24e29b3e","time":0.24,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocatorrightarm","script":"","file":""}],"uuid":"ee5b5931-4a99-5bb5-e128-6c6062a06bb7","time":0.28,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"39fda599-4b07-a46e-9ba4-11f191bef061","time":0.32,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"d139b9f3-cfb0-94df-d456-09cab0a056cd","time":0.36,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"89baa0b4-caef-dcce-4d3b-43ff24432913","time":0.4,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocatorrightarm","script":"","file":""}],"uuid":"17d5f191-1d78-57ef-a1fb-dc243e7ef06e","time":0.44,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"f7708b40-7b6d-0be2-68d0-bf6b091a09d6","time":0.48,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"a7ab4e3a-5b1f-20dc-76cc-dcb05faffd9b","time":0.52,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocatorrightarm","script":"","file":""}],"uuid":"3d23d3c5-bd32-f14a-e19b-6cce26f72d24","time":0.56,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"5ed4d480-740b-5263-da7a-11c2876e0d14","time":0.6,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"1ea68cdb-148f-ff4c-5834-f8198d0967c9","time":0.64,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocatorrightarm","script":"","file":""}],"uuid":"2a37bee5-ebde-da7e-739c-0b5c5ee68a27","time":0.68,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"9081872b-4991-e376-7e42-74c7d1358abd","time":0.72,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"067a2d76-ad3d-daec-d6cf-e5088fee514d","time":0.76,"color":-1,"interpolation":"linear"},{"channel":"particle","data_points":[{"effect":"bending_air_1","locator":"airlocator","script":"","file":""}],"uuid":"df215b76-3561-557d-f81e-dbae1c522ea4","time":0.8,"color":-1,"interpolation":"linear"}]}}}],"geckoSettings":{"formatVersion":2,"modSDK":"Forge 1.12 - 1.16","objectType":"OBJ_TYPE_ENTITY","entityType":"Entity","javaPackage":"com.example.mod","animFileNamespace":"MODID","animFilePath":"animations/ANIMATIONFILE.json"}} \ No newline at end of file diff --git a/aang/blockbench/levitation.bbmodel b/aang/blockbench/levitation.bbmodel new file mode 100644 index 0000000..82d75df --- /dev/null +++ b/aang/blockbench/levitation.bbmodel @@ -0,0 +1 @@ +{"meta":{"format_version":"4.10","model_format":"animated_entity_model","box_uv":true},"name":"levitation","model_identifier":"","visible_box":[1,1,0],"variable_placeholders":"","variable_placeholder_buttons":[],"timeline_setups":[],"unhandled_root_fields":{},"resolution":{"width":16,"height":16},"elements":[{"name":"bipedHead","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,24,-4],"to":[4,32,4],"autouv":0,"color":1,"origin":[0,0,0],"faces":{"north":{"uv":[8,8,16,16]},"east":{"uv":[0,8,8,16]},"south":{"uv":[24,8,32,16]},"west":{"uv":[16,8,24,16]},"up":{"uv":[16,8,8,0]},"down":{"uv":[24,0,16,8]}},"type":"cube","uuid":"e5841845-2e3f-0f50-b112-0da10d6bac1e"},{"name":"bipedBody","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,12,-2],"to":[4,24,2],"autouv":0,"color":3,"origin":[0,0,0],"uv_offset":[0,16],"faces":{"north":{"uv":[4,20,12,32]},"east":{"uv":[0,20,4,32]},"south":{"uv":[16,20,24,32]},"west":{"uv":[12,20,16,32]},"up":{"uv":[12,20,4,16]},"down":{"uv":[20,16,12,20]}},"type":"cube","uuid":"a6a9f27d-0d1d-91f6-95bb-c69e6c8044c4"},{"name":"bipedRightArm","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[4,12,-2],"to":[8,24,2],"autouv":0,"color":5,"origin":[0,0,0],"uv_offset":[16,32],"faces":{"north":{"uv":[20,36,24,48]},"east":{"uv":[16,36,20,48]},"south":{"uv":[28,36,32,48]},"west":{"uv":[24,36,28,48]},"up":{"uv":[24,36,20,32]},"down":{"uv":[28,32,24,36]}},"type":"cube","uuid":"f916afcf-94f8-26ed-0e3a-52807ea6dd91"},{"name":"bipedLeftArm","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-8,12,-2],"to":[-4,24,2],"autouv":0,"color":7,"origin":[0,0,0],"uv_offset":[32,0],"faces":{"north":{"uv":[36,4,40,16]},"east":{"uv":[32,4,36,16]},"south":{"uv":[44,4,48,16]},"west":{"uv":[40,4,44,16]},"up":{"uv":[40,4,36,0]},"down":{"uv":[44,0,40,4]}},"type":"cube","uuid":"73ee629c-6c19-3c63-7c37-d3c416aa9a99"},{"name":"bipedLeftLeg","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,0,-2],"to":[0,12,2],"autouv":0,"color":9,"origin":[0,0,0],"uv_offset":[0,32],"faces":{"north":{"uv":[4,36,8,48]},"east":{"uv":[0,36,4,48]},"south":{"uv":[12,36,16,48]},"west":{"uv":[8,36,12,48]},"up":{"uv":[8,36,4,32]},"down":{"uv":[12,32,8,36]}},"type":"cube","uuid":"8ee88bce-95e1-f080-3568-e704d5028ab9"},{"name":"bipedRightLeg","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[0,0,-2],"to":[4,12,2],"autouv":0,"color":2,"origin":[0,0,0],"uv_offset":[24,16],"faces":{"north":{"uv":[28,20,32,32]},"east":{"uv":[24,20,28,32]},"south":{"uv":[36,20,40,32]},"west":{"uv":[32,20,36,32]},"up":{"uv":[32,20,28,16]},"down":{"uv":[36,16,32,20]}},"type":"cube","uuid":"80cf2ca9-ce87-05a5-2475-bcf6e2bf2458"}],"outliner":[{"name":"bipedRig","origin":[0,0,0],"color":0,"uuid":"f7d20b62-01ed-bab3-fa05-d56a255afe6b","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"children":[{"name":"bipedHead","origin":[0,24,0],"color":1,"uuid":"f65b6d21-44d9-8222-c6bc-a6d42ede554e","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["e5841845-2e3f-0f50-b112-0da10d6bac1e",{"name":"armorHead","origin":[0,24,0],"color":2,"uuid":"60aca50e-f327-41f4-b4de-efd0d0f43fa8","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedBody","origin":[0,24,0],"color":3,"uuid":"9b034992-30d9-e07e-b869-d1b12f8aa24c","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["a6a9f27d-0d1d-91f6-95bb-c69e6c8044c4",{"name":"armorBody","origin":[0,24,0],"color":4,"uuid":"a0c0e5da-6af5-0e2f-881d-0e4e05e2e29d","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedRightArm","origin":[5,22,0],"color":5,"uuid":"7a1d791a-94df-a32d-4340-13c665e7bb9f","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["f916afcf-94f8-26ed-0e3a-52807ea6dd91",{"name":"armorRightArm","origin":[4,22,0],"color":6,"uuid":"b753db07-83c3-df73-8f84-4775193ed7cd","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedLeftArm","origin":[-5,22,0],"color":7,"uuid":"d3708a24-fc02-e5e3-3652-282e2b7b7ba9","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["73ee629c-6c19-3c63-7c37-d3c416aa9a99",{"name":"armorLeftArm","origin":[-4,22,0],"color":8,"uuid":"b11f5810-cf1f-fb21-0113-bdc9a7709870","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedLeftLeg","origin":[-2,12,0],"color":9,"uuid":"41a8bbae-789c-6c63-7652-76cf641172eb","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"children":["8ee88bce-95e1-f080-3568-e704d5028ab9",{"name":"armorLeftLeg","origin":[-2,12,0],"color":0,"uuid":"959464aa-5959-fc92-a387-be5dcd0ddc45","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]},{"name":"armorLeftBoot","origin":[-2,12,0],"color":1,"uuid":"065cb27b-2980-87d8-28bf-a2cbaf9cd8f1","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedRightLeg","origin":[2,12,0],"color":2,"uuid":"46c209af-600b-324c-bc48-89b6f4326cb2","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["80cf2ca9-ce87-05a5-2475-bcf6e2bf2458",{"name":"armorRightLeg","origin":[2,12,0],"color":3,"uuid":"6f96ee97-660c-8ddd-356d-e1b0d9c2008d","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]},{"name":"armorRightBoot","origin":[2,12,0],"color":4,"uuid":"ec7590f1-8742-6b03-593d-63630e76e720","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]}]}],"textures":[],"animations":[{"uuid":"958a80c0-cf42-d05b-f837-22e0c003ea43","name":"levitation","loop":"hold","override":false,"length":0.6,"snapping":25,"selected":true,"saved":true,"path":"E:\\Coding\\NoRiskClient\\heroes\\aang\\src\\main\\resources\\assets\\aang\\emotes\\levitation.animation.json","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"f7d20b62-01ed-bab3-fa05-d56a255afe6b":{"name":"bipedRig","type":"bone","keyframes":[{"channel":"position","data_points":[{"x":"0","y":"Math.cos(query.anim_time * 100) * 0.5 -0.5","z":"0"}],"uuid":"60bda820-6255-e40c-dc4d-6f667030a8a0","time":0.6,"color":-1,"interpolation":"linear"}]},"7a1d791a-94df-a32d-4340-13c665e7bb9f":{"name":"bipedRightArm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":"0","z":65}],"uuid":"6c4caa13-2862-b4be-45dd-0fca9b419313","time":0.6,"color":-1,"interpolation":"linear","easing":"easeOutQuint"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"f125baa8-e2fa-1d62-ced2-5c619a31b223","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":-1,"y":"0","z":"0"}],"uuid":"e0617660-a990-b316-6782-3a2fb4930adb","time":0.6,"color":-1,"interpolation":"linear","easing":"easeOutQuint"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"14c08e35-9eaf-f4fa-f773-5e8a5d95661b","time":0,"color":-1,"interpolation":"linear"}]},"d3708a24-fc02-e5e3-3652-282e2b7b7ba9":{"name":"bipedLeftArm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"0","y":0,"z":0}],"uuid":"7929af38-c378-8065-5da8-3c682f76b0d4","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":"0","y":0,"z":-65}],"uuid":"4296ad1e-93a8-be15-2073-a5f66986db14","time":0.6,"color":-1,"interpolation":"linear","easing":"easeOutQuint"},{"channel":"position","data_points":[{"x":0,"y":"0","z":"0"}],"uuid":"c2e1fb2e-0a10-7ecd-fd81-c6831fdfc322","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":1,"y":"0","z":"0"}],"uuid":"9f53ee9c-3064-bbd8-9cf5-68d837197d9a","time":0.6,"color":-1,"interpolation":"linear","easing":"easeOutQuint"}]},"41a8bbae-789c-6c63-7652-76cf641172eb":{"name":"bipedLeftLeg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"Math.cos(query.anim_time * 150 +25) * -15","y":"0","z":42.5}],"uuid":"fe45bae9-2440-83ee-965c-e2c450949ffe","time":0.6,"color":-1,"interpolation":"linear","easing":"easeOutExpo"},{"channel":"rotation","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"87478077-e4e8-455b-509c-78f8138296ef","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":6,"y":0,"z":"0"}],"uuid":"bdecf0bc-b620-5f3f-1a63-0dfdb996d011","time":0.6,"color":-1,"interpolation":"linear","easing":"easeOutExpo"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"c4624365-6117-9d87-3369-d9afcc4c8b35","time":0,"color":-1,"interpolation":"linear"}]},"46c209af-600b-324c-bc48-89b6f4326cb2":{"name":"bipedRightLeg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":"Math.cos(query.anim_time * 150 +25) * 15","y":"0","z":"0"}],"uuid":"e6499834-e4fe-3003-881d-2cf948c64e42","time":0.6,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":"0","y":"0","z":"0"}],"uuid":"83edec61-3e9d-e6a6-a49a-d507438812bb","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":-2,"y":-1,"z":0}],"uuid":"ac943fa9-42b8-05a6-c1ea-46e4afb0060a","time":0.6,"color":-1,"interpolation":"linear","easing":"easeOutQuint"}]}}}],"geckoSettings":{"formatVersion":2,"modSDK":"Forge 1.12 - 1.16","objectType":"OBJ_TYPE_ENTITY","entityType":"Entity","javaPackage":"com.example.mod","animFileNamespace":"MODID","animFilePath":"animations/ANIMATIONFILE.json"}} \ No newline at end of file diff --git a/aang/blockbench/spiritual_projection.bbmodel b/aang/blockbench/spiritual_projection.bbmodel new file mode 100644 index 0000000..6e1f92a --- /dev/null +++ b/aang/blockbench/spiritual_projection.bbmodel @@ -0,0 +1 @@ +{"meta":{"format_version":"4.10","model_format":"animated_entity_model","box_uv":true},"name":"spiritual_projection","model_identifier":"","visible_box":[1,1,0],"variable_placeholders":"","variable_placeholder_buttons":[],"timeline_setups":[],"unhandled_root_fields":{},"resolution":{"width":16,"height":16},"elements":[{"name":"bipedHead","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,24,-4],"to":[4,32,4],"autouv":0,"color":1,"origin":[0,0,0],"faces":{"north":{"uv":[8,8,16,16]},"east":{"uv":[0,8,8,16]},"south":{"uv":[24,8,32,16]},"west":{"uv":[16,8,24,16]},"up":{"uv":[16,8,8,0]},"down":{"uv":[24,0,16,8]}},"type":"cube","uuid":"e5841845-2e3f-0f50-b112-0da10d6bac1e"},{"name":"bipedBody","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,12,-2],"to":[4,24,2],"autouv":0,"color":3,"origin":[0,0,0],"uv_offset":[0,16],"faces":{"north":{"uv":[4,20,12,32]},"east":{"uv":[0,20,4,32]},"south":{"uv":[16,20,24,32]},"west":{"uv":[12,20,16,32]},"up":{"uv":[12,20,4,16]},"down":{"uv":[20,16,12,20]}},"type":"cube","uuid":"a6a9f27d-0d1d-91f6-95bb-c69e6c8044c4"},{"name":"bipedRightArm","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[4,12,-2],"to":[8,24,2],"autouv":0,"color":5,"origin":[0,0,0],"uv_offset":[16,32],"faces":{"north":{"uv":[20,36,24,48]},"east":{"uv":[16,36,20,48]},"south":{"uv":[28,36,32,48]},"west":{"uv":[24,36,28,48]},"up":{"uv":[24,36,20,32]},"down":{"uv":[28,32,24,36]}},"type":"cube","uuid":"f916afcf-94f8-26ed-0e3a-52807ea6dd91"},{"name":"bipedLeftArm","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-8,12,-2],"to":[-4,24,2],"autouv":0,"color":7,"origin":[0,0,0],"uv_offset":[32,0],"faces":{"north":{"uv":[36,4,40,16]},"east":{"uv":[32,4,36,16]},"south":{"uv":[44,4,48,16]},"west":{"uv":[40,4,44,16]},"up":{"uv":[40,4,36,0]},"down":{"uv":[44,0,40,4]}},"type":"cube","uuid":"73ee629c-6c19-3c63-7c37-d3c416aa9a99"},{"name":"bipedLeftLeg","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[-4,0,-2],"to":[0,12,2],"autouv":0,"color":9,"origin":[0,0,0],"uv_offset":[0,32],"faces":{"north":{"uv":[4,36,8,48]},"east":{"uv":[0,36,4,48]},"south":{"uv":[12,36,16,48]},"west":{"uv":[8,36,12,48]},"up":{"uv":[8,36,4,32]},"down":{"uv":[12,32,8,36]}},"type":"cube","uuid":"8ee88bce-95e1-f080-3568-e704d5028ab9"},{"name":"bipedRightLeg","box_uv":true,"rescale":false,"locked":false,"render_order":"default","allow_mirror_modeling":true,"from":[0,0,-2],"to":[4,12,2],"autouv":0,"color":2,"origin":[0,0,0],"uv_offset":[24,16],"faces":{"north":{"uv":[28,20,32,32]},"east":{"uv":[24,20,28,32]},"south":{"uv":[36,20,40,32]},"west":{"uv":[32,20,36,32]},"up":{"uv":[32,20,28,16]},"down":{"uv":[36,16,32,20]}},"type":"cube","uuid":"80cf2ca9-ce87-05a5-2475-bcf6e2bf2458"}],"outliner":[{"name":"bipedRig","origin":[0,0,0],"color":0,"uuid":"f7d20b62-01ed-bab3-fa05-d56a255afe6b","export":true,"mirror_uv":false,"isOpen":true,"locked":false,"visibility":true,"autouv":0,"children":[{"name":"bipedHead","origin":[0,24,0],"color":1,"uuid":"f65b6d21-44d9-8222-c6bc-a6d42ede554e","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["e5841845-2e3f-0f50-b112-0da10d6bac1e",{"name":"armorHead","origin":[0,24,0],"color":2,"uuid":"60aca50e-f327-41f4-b4de-efd0d0f43fa8","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedBody","origin":[0,24,0],"color":3,"uuid":"9b034992-30d9-e07e-b869-d1b12f8aa24c","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["a6a9f27d-0d1d-91f6-95bb-c69e6c8044c4",{"name":"armorBody","origin":[0,24,0],"color":4,"uuid":"a0c0e5da-6af5-0e2f-881d-0e4e05e2e29d","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedRightArm","origin":[5,22,0],"color":5,"uuid":"7a1d791a-94df-a32d-4340-13c665e7bb9f","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["f916afcf-94f8-26ed-0e3a-52807ea6dd91",{"name":"armorRightArm","origin":[4,22,0],"color":6,"uuid":"b753db07-83c3-df73-8f84-4775193ed7cd","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedLeftArm","origin":[-5,22,0],"color":7,"uuid":"d3708a24-fc02-e5e3-3652-282e2b7b7ba9","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["73ee629c-6c19-3c63-7c37-d3c416aa9a99",{"name":"armorLeftArm","origin":[-4,22,0],"color":8,"uuid":"b11f5810-cf1f-fb21-0113-bdc9a7709870","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedLeftLeg","origin":[-2,12,0],"color":9,"uuid":"41a8bbae-789c-6c63-7652-76cf641172eb","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["8ee88bce-95e1-f080-3568-e704d5028ab9",{"name":"armorLeftLeg","origin":[-2,12,0],"color":0,"uuid":"959464aa-5959-fc92-a387-be5dcd0ddc45","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]},{"name":"armorLeftBoot","origin":[-2,12,0],"color":1,"uuid":"065cb27b-2980-87d8-28bf-a2cbaf9cd8f1","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedRightLeg","origin":[2,12,0],"color":2,"uuid":"46c209af-600b-324c-bc48-89b6f4326cb2","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["80cf2ca9-ce87-05a5-2475-bcf6e2bf2458",{"name":"armorRightLeg","origin":[2,12,0],"color":3,"uuid":"6f96ee97-660c-8ddd-356d-e1b0d9c2008d","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]},{"name":"armorRightBoot","origin":[2,12,0],"color":4,"uuid":"ec7590f1-8742-6b03-593d-63630e76e720","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]}]}],"textures":[],"animations":[{"uuid":"08ad0d61-297e-00f7-4b5c-80c2daf3a4bf","name":"emote","loop":"once","override":false,"length":2.12,"snapping":25,"selected":true,"saved":false,"path":"E:\\Coding\\NoRiskClient\\heroes\\aang\\src\\main\\resources\\assets\\aang\\emotes\\spiritual_projection_start.animation.json","anim_time_update":"","blend_weight":"","start_delay":"","loop_delay":"","animators":{"f7d20b62-01ed-bab3-fa05-d56a255afe6b":{"name":"bipedRig","type":"bone","keyframes":[{"channel":"position","data_points":[{"x":0,"y":"Math.cos(query.anim_time * 100) * 1 -0.5","z":0}],"uuid":"ade0a8da-1f9e-32e2-fe39-ab04124b9e16","time":0,"color":-1,"interpolation":"linear"}]},"7a1d791a-94df-a32d-4340-13c665e7bb9f":{"name":"bipedRightArm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":0,"y":0,"z":0}],"uuid":"ba81cfbc-e289-ec6a-f1e7-eaa8d3e4f47d","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":0,"y":0,"z":80}],"uuid":"cea17130-01bb-84db-7ade-a343f117f309","time":0.72,"color":-1,"interpolation":"linear","easing":"easeOutExpo"},{"channel":"position","data_points":[{"x":0,"y":0,"z":0}],"uuid":"6964b40c-fa01-e623-4fff-f4483fce0c92","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":-2,"y":0,"z":0}],"uuid":"6d7ad44c-4ee9-0eb0-6183-917c9fbd2177","time":0.72,"color":-1,"interpolation":"linear","easing":"easeOutExpo"}]},"d3708a24-fc02-e5e3-3652-282e2b7b7ba9":{"name":"bipedLeftArm","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":0,"y":0,"z":0}],"uuid":"13e6cc3f-2cc5-31d6-307c-49586cdfcf22","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":0,"y":0,"z":-80}],"uuid":"370f57b3-10c5-082d-544c-662c3a24b616","time":0.72,"color":-1,"interpolation":"linear","easing":"easeOutExpo"},{"channel":"position","data_points":[{"x":0,"y":0,"z":0}],"uuid":"78eb8538-15ed-4566-52d7-de0886f40ffd","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":2,"y":0,"z":0}],"uuid":"a336eec4-c49b-8860-3784-ae2346587b87","time":0.72,"color":-1,"interpolation":"linear","easing":"easeOutExpo"}]},"41a8bbae-789c-6c63-7652-76cf641172eb":{"name":"bipedLeftLeg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":0,"y":0,"z":0}],"uuid":"98029be8-29cb-eb86-c446-e38c1cc2810b","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":-71.88679,"y":33.64409,"z":10.27206}],"uuid":"dd6258bc-9829-0b30-2d52-8fba3f651a1d","time":0.72,"color":-1,"interpolation":"linear","easing":"easeOutExpo"},{"channel":"position","data_points":[{"x":0,"y":0,"z":0}],"uuid":"7d9bee4d-6561-ccca-9722-ab4ee15815cc","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":5,"y":-2,"z":0}],"uuid":"97029824-9ab1-4d2d-0149-70a79eec99a0","time":0.72,"color":-1,"interpolation":"linear","easing":"easeOutExpo"}]},"46c209af-600b-324c-bc48-89b6f4326cb2":{"name":"bipedRightLeg","type":"bone","keyframes":[{"channel":"rotation","data_points":[{"x":0,"y":0,"z":0}],"uuid":"b377202c-9fde-9b9a-832c-a5d3537ae562","time":0,"color":-1,"interpolation":"linear"},{"channel":"rotation","data_points":[{"x":-89.38679,"y":-33.64409,"z":-10.27206}],"uuid":"062feb2a-8b63-d239-9d5e-6ad806fc64ff","time":0.72,"color":-1,"interpolation":"linear","easing":"easeOutExpo"},{"channel":"position","data_points":[{"x":0,"y":0,"z":0}],"uuid":"cae11aa3-bcb9-1e0b-f0eb-6b3af885b65f","time":0,"color":-1,"interpolation":"linear"},{"channel":"position","data_points":[{"x":-6,"y":-2,"z":1}],"uuid":"1d6b6552-0313-2cd0-bae7-7b65339952bc","time":0.72,"color":-1,"interpolation":"linear","easing":"easeOutExpo"}]}}}],"geckoSettings":{"formatVersion":2,"modSDK":"Forge 1.12 - 1.16","objectType":"OBJ_TYPE_ENTITY","entityType":"Entity","javaPackage":"com.example.mod","animFileNamespace":"MODID","animFilePath":"animations/ANIMATIONFILE.json"}} \ No newline at end of file diff --git a/aang/build.gradle.kts b/aang/build.gradle.kts new file mode 100644 index 0000000..0296f12 --- /dev/null +++ b/aang/build.gradle.kts @@ -0,0 +1,16 @@ +dependencies { + implementation(project(":hero-api", configuration = "namedElements")) + implementation(project(":datatracker", configuration = "namedElements")) + + modApi(libs.bundles.fabric) + modApi(libs.bundles.silk) + modApi(libs.bundles.performance) + modApi(libs.owolib) + modApi(libs.geckolib) + modApi(libs.emoteLib) +} + +loom { + accessWidenerPath.set(file("src/main/resources/aang.accesswidener")) +} + diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/EntityMixin.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/EntityMixin.java new file mode 100644 index 0000000..5eb7cef --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/EntityMixin.java @@ -0,0 +1,19 @@ +package gg.norisk.heroes.aang.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import gg.norisk.heroes.aang.ability.AirScooterAbility; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.Box; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Entity.class) +public abstract class EntityMixin { + @ModifyReturnValue( + method = "calculateBoundingBox", + at = @At("RETURN") + ) + private Box airScooterBoxInjection(Box original) { + return AirScooterAbility.INSTANCE.handleBox((Entity) (Object) this, original); + } +} diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/LivingEntityMixin.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/LivingEntityMixin.java new file mode 100644 index 0000000..6f8d949 --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/LivingEntityMixin.java @@ -0,0 +1,36 @@ +package gg.norisk.heroes.aang.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import gg.norisk.heroes.aang.ability.AirScooterAbility; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityMixin { + @ModifyExpressionValue( + method = "travel", + at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;isOnGround()Z", ordinal = 2) + ) + private boolean isOnGroundInjection(boolean original) { + return AirScooterAbility.INSTANCE.handleDrag((Entity) (Object) this) || original; + } + + @ModifyExpressionValue( + method = "getMovementSpeed(F)F", + at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;isOnGround()Z") + ) + private boolean isOnGroundInjection2(boolean original) { + return AirScooterAbility.INSTANCE.handleDrag((Entity) (Object) this) || original; + } + + @ModifyReturnValue( + method = "canWalkOnFluid", + at = @At(value = "RETURN") + ) + private boolean canWalkOnFluidInjection(boolean original) { + return AirScooterAbility.INSTANCE.handleDrag((Entity) (Object) this) || original; + } +} diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/PlayerEntityMixin.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/PlayerEntityMixin.java new file mode 100644 index 0000000..94a603c --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/PlayerEntityMixin.java @@ -0,0 +1,138 @@ +package gg.norisk.heroes.aang.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import gg.norisk.heroes.aang.ability.AirBallAbility; +import gg.norisk.heroes.aang.ability.AirScooterAbility; +import gg.norisk.heroes.aang.ability.LevitationAbility; +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility; +import gg.norisk.heroes.aang.entity.IAangPlayer; +import gg.norisk.heroes.aang.entity.TornadoEntity; +import gg.norisk.heroes.aang.utils.EntitySpinTracker; +import gg.norisk.heroes.aang.utils.PlayerRotationTracker; +import kotlinx.coroutines.Job; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.data.DataTracker; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArgs; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.invoke.arg.Args; + +import java.util.ArrayList; +import java.util.List; + +@Mixin(PlayerEntity.class) +public abstract class PlayerEntityMixin extends LivingEntity implements IAangPlayer { + @Shadow + public abstract void remove(RemovalReason removalReason); + + @Unique + private PlayerRotationTracker rotationTracker; + @Unique + private final List airScooterTasks = new ArrayList<>(); + @Unique + private final List tornadoTasks = new ArrayList<>(); + @Unique + private final List spiritualProjectionTasks = new ArrayList<>(); + @Unique + private TornadoEntity tornadoEntity; + @Unique + private final EntitySpinTracker entitySpinTracker = new EntitySpinTracker(); + + protected PlayerEntityMixin(EntityType entityType, World world) { + super(entityType, world); + } + + @Inject(method = "travel", at = @At("HEAD"), cancellable = true) + private void airScooterTravelInjection(Vec3d vec3d, CallbackInfo ci) { + var player = (PlayerEntity) (Object) this; + if (AirScooterAbility.INSTANCE.isAirScooting(player)) { + super.travel(AirScooterAbility.INSTANCE.handleTravel(player, vec3d)); + ci.cancel(); + } + } + + @Inject(method = "tick", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/player/PlayerEntity;noClip:Z", shift = At.Shift.AFTER)) + private void handleAangTickAfterNoClip(CallbackInfo ci) { + SpiritualProjectionAbility.INSTANCE.handleTick((PlayerEntity) (Object) this); + } + + @Inject(method = "tick", at = @At("TAIL")) + private void handleAangTick(CallbackInfo ci) { + AirBallAbility.INSTANCE.handleTick((PlayerEntity) (Object) this); + LevitationAbility.INSTANCE.handleTick((PlayerEntity) (Object) this); + } + + @Inject(method = "handleFallDamage", at = @At("HEAD"), cancellable = true) + private void handleFallAangInjection(float f, float g, DamageSource damageSource, CallbackInfoReturnable cir) { + AirScooterAbility.INSTANCE.handleFallDamage((PlayerEntity) (Object) this, f, g, damageSource, cir); + } + + @Nullable + @Override + public PlayerRotationTracker getRotationTracker() { + return rotationTracker; + } + + @Override + public void setRotationTracker(PlayerRotationTracker rotationTracker) { + this.rotationTracker = rotationTracker; + } + + // DUMMY PLAYER STUFF + + @ModifyArgs(method = "getDisplayName", at = @At(value = "INVOKE", target = "Lnet/minecraft/scoreboard/Team;decorateName(Lnet/minecraft/scoreboard/AbstractTeam;Lnet/minecraft/text/Text;)Lnet/minecraft/text/MutableText;")) + private void getFakeDisplayName(Args args) { + SpiritualProjectionAbility.INSTANCE.replaceNameWithOwner((PlayerEntity) (Object) this, args); + } + + @WrapOperation( + method = "isPartVisible", + at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerEntity;getDataTracker()Lnet/minecraft/entity/data/DataTracker;") + ) + private DataTracker aang$redirectIsModelPartVisible(PlayerEntity instance, Operation original) { + return SpiritualProjectionAbility.INSTANCE.replaceDataTrackerWithOwner(instance, original); + } + + @Override + public @NotNull List getAang_airScooterTasks() { + return airScooterTasks; + } + + @Override + public @Nullable TornadoEntity getAang_tornadoEntity() { + return tornadoEntity; + } + + @Override + public void setAang_tornadoEntity(@Nullable TornadoEntity tornadoEntity) { + this.tornadoEntity = tornadoEntity; + } + + @Override + public @NotNull List getAang_tornadoTasks() { + return tornadoTasks; + } + + @Override + public @NotNull List getAang_spiritualProjectionsTasks() { + return spiritualProjectionTasks; + } + + @Override + public @NotNull EntitySpinTracker getAang_airBallSpinTracker() { + return entitySpinTracker; + } +} diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/accessor/CameraAccessor.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/accessor/CameraAccessor.java new file mode 100644 index 0000000..deb96ce --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/accessor/CameraAccessor.java @@ -0,0 +1,15 @@ +package gg.norisk.heroes.aang.mixin.accessor; + +import net.minecraft.client.render.Camera; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(Camera.class) +public interface CameraAccessor { + @Invoker("setRotation") + void invokeSetRotation(float f, float g); + + @Invoker("setPos") + void invokeSetPos(double d, double e, double f); +} + diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/AbstractClientPlayerEntityMixin.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/AbstractClientPlayerEntityMixin.java new file mode 100644 index 0000000..e950139 --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/AbstractClientPlayerEntityMixin.java @@ -0,0 +1,27 @@ +package gg.norisk.heroes.aang.mixin.client; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.mojang.authlib.GameProfile; +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.util.SkinTextures; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(AbstractClientPlayerEntity.class) +public abstract class AbstractClientPlayerEntityMixin extends PlayerEntity { + public AbstractClientPlayerEntityMixin(World world, BlockPos blockPos, float f, GameProfile gameProfile) { + super(world, blockPos, f, gameProfile); + } + + @ModifyReturnValue( + method = "getSkinTextures", + at = @At("RETURN") + ) + private SkinTextures replaceSkinWithOwner(SkinTextures original) { + return SpiritualProjectionAbility.INSTANCE.replaceSkinWithOwner((AbstractClientPlayerEntity) (Object) this, original); + } +} diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/CameraMixin.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/CameraMixin.java new file mode 100644 index 0000000..de08679 --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/CameraMixin.java @@ -0,0 +1,18 @@ +package gg.norisk.heroes.aang.mixin.client; + +import gg.norisk.heroes.aang.ability.TornadoAbility; +import net.minecraft.client.render.Camera; +import net.minecraft.entity.Entity; +import net.minecraft.world.BlockView; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Camera.class) +public abstract class CameraMixin { + @Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setPos(DDD)V", shift = At.Shift.AFTER)) + private void tornadoCameraMode(BlockView blockView, Entity entity, boolean bl, boolean bl2, float f, CallbackInfo ci) { + TornadoAbility.INSTANCE.handleTornadoCamera((Camera) (Object) this, blockView, entity, bl, bl2, f); + } +} diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/ClientPlayNetworkHandlerMixin.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/ClientPlayNetworkHandlerMixin.java new file mode 100644 index 0000000..7cdaffd --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/ClientPlayNetworkHandlerMixin.java @@ -0,0 +1,32 @@ +package gg.norisk.heroes.aang.mixin.client; + +import gg.norisk.heroes.aang.entity.DummyPlayer; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientCommonNetworkHandler; +import net.minecraft.client.network.ClientConnectionState; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.listener.ClientPlayPacketListener; +import net.minecraft.network.listener.TickablePacketListener; +import net.minecraft.network.packet.s2c.play.EntitySpawnS2CPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ClientPlayNetworkHandler.class) +public abstract class ClientPlayNetworkHandlerMixin extends ClientCommonNetworkHandler implements ClientPlayPacketListener, TickablePacketListener { + @Shadow private ClientWorld world; + + protected ClientPlayNetworkHandlerMixin(MinecraftClient minecraftClient, ClientConnection clientConnection, ClientConnectionState clientConnectionState) { + super(minecraftClient, clientConnection, clientConnectionState); + } + + @Inject(method = "createEntity", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V", remap = false), cancellable = true) + private void createFakePlayerInjection(EntitySpawnS2CPacket entitySpawnS2CPacket, CallbackInfoReturnable cir) { + DummyPlayer.Companion.handleDummyPlayerSpawn(entitySpawnS2CPacket, cir, this.world); + } +} diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/GameOptionsMixin.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/GameOptionsMixin.java new file mode 100644 index 0000000..dcd3899 --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/GameOptionsMixin.java @@ -0,0 +1,25 @@ +package gg.norisk.heroes.aang.mixin.client; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import gg.norisk.heroes.aang.ability.TornadoAbility; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.GameOptions; +import net.minecraft.client.option.Perspective; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GameOptions.class) +public abstract class GameOptionsMixin { + + @ModifyReturnValue( + method = "getPerspective", + at = @At("RETURN") + ) + private Perspective aang$tornadoStaticPerspective(Perspective original) { + var player = MinecraftClient.getInstance().player; + if (player != null && TornadoAbility.INSTANCE.isTornadoMode(player)) { + return Perspective.THIRD_PERSON_BACK; + } + return original; + } +} diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/GameRendererMixin.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/GameRendererMixin.java new file mode 100644 index 0000000..1a8ed6f --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/GameRendererMixin.java @@ -0,0 +1,47 @@ +package gg.norisk.heroes.aang.mixin.client; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.blaze3d.systems.RenderSystem; +import gg.norisk.heroes.aang.ability.AirScooterAbility; +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.render.Camera; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.player.PlayerEntity; +import org.joml.Matrix4f; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = GameRenderer.class) +public abstract class GameRendererMixin { + @Inject(method = "renderHand", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/item/HeldItemRenderer;renderItem(FLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider$Immediate;Lnet/minecraft/client/network/ClientPlayerEntity;I)V"), cancellable = true) + private void renderHandBeforeSpiritual(Camera camera, float f, Matrix4f matrix4f, CallbackInfo ci, @Local MatrixStack matrixStack) { + matrixStack.push(); + if (camera.getFocusedEntity() instanceof PlayerEntity player && SpiritualProjectionAbility.INSTANCE.isSpiritualTransparent(player)) { + RenderSystem.setShaderColor(1f, 1f, 1f, SpiritualProjectionAbility.INSTANCE.getAlpha(player)); + } + } + + @Inject(method = "renderHand", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/item/HeldItemRenderer;renderItem(FLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider$Immediate;Lnet/minecraft/client/network/ClientPlayerEntity;I)V", shift = At.Shift.AFTER)) + private void renderHandAfterSpiritual(Camera camera, float f, Matrix4f matrix4f, CallbackInfo ci, @Local MatrixStack matrixStack) { + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + matrixStack.pop(); + } + + @ModifyExpressionValue( + method = {"renderWorld", "renderHand"}, + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/option/SimpleOption;getValue()Ljava/lang/Object;", ordinal = 0) + ) + private Object aang$disableViewBobbing(T original) { + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (player != null && AirScooterAbility.INSTANCE.isAirScooting(player)) { + return false; + } + return original; + } +} diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/HeldItemRendererMixin.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/HeldItemRendererMixin.java new file mode 100644 index 0000000..288b383 --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/HeldItemRendererMixin.java @@ -0,0 +1,32 @@ +package gg.norisk.heroes.aang.mixin.client; + +import com.mojang.blaze3d.systems.RenderSystem; +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.item.HeldItemRenderer; +import net.minecraft.client.render.model.json.ModelTransformationMode; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(HeldItemRenderer.class) +public abstract class HeldItemRendererMixin { + @Inject(method = "renderItem(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/render/model/json/ModelTransformationMode;ZLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/item/ItemRenderer;renderItem(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/render/model/json/ModelTransformationMode;ZLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;Lnet/minecraft/world/World;III)V")) + private void renderHandBeforeSpiritual(LivingEntity livingEntity, ItemStack itemStack, ModelTransformationMode modelTransformationMode, boolean bl, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { + //matrixStack.push(); + if (livingEntity instanceof PlayerEntity player && SpiritualProjectionAbility.INSTANCE.isSpiritualTransparent(player)) { + //RenderSystem.setShaderColor(1f, 1f, 1f, 0.5f); + } + } + + @Inject(method = "renderItem(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/render/model/json/ModelTransformationMode;ZLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/item/ItemRenderer;renderItem(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/render/model/json/ModelTransformationMode;ZLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;Lnet/minecraft/world/World;III)V", shift = At.Shift.AFTER)) + private void renderHandAfterSpiritual(LivingEntity livingEntity, ItemStack itemStack, ModelTransformationMode modelTransformationMode, boolean bl, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { + //RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + //matrixStack.pop(); + } +} diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/LivingEntityRendererMixin.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/LivingEntityRendererMixin.java new file mode 100644 index 0000000..69cf3d8 --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/LivingEntityRendererMixin.java @@ -0,0 +1,81 @@ +package gg.norisk.heroes.aang.mixin.client; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import gg.norisk.heroes.aang.ability.AirScooterAbility; +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility; +import gg.norisk.heroes.aang.client.render.entity.feature.AirScooterFeatureRenderer; +import gg.norisk.heroes.aang.entity.TornadoEntity; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.render.entity.EntityRendererFactory; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.feature.FeatureRenderer; +import net.minecraft.client.render.entity.feature.FeatureRendererContext; +import net.minecraft.client.render.entity.model.EntityModel; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntityRenderer.class) +public abstract class LivingEntityRendererMixin> extends EntityRenderer implements FeatureRendererContext { + protected LivingEntityRendererMixin(EntityRendererFactory.Context context) { + super(context); + } + + @Inject(method = "render(Lnet/minecraft/entity/LivingEntity;FFLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/math/MatrixStack;pop()V")) + private void airballRenderer(T livingEntity, float f, float g, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { + } + + @Shadow + protected abstract boolean addFeature(FeatureRenderer featureRenderer); + + @Shadow + protected M model; + + @WrapOperation( + method = "render(Lnet/minecraft/entity/LivingEntity;FFLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;getScale()F") + ) + private float redirectGetScaleWithLerpedScale(LivingEntity instance, Operation original, T livingEntity, float f, float g, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i) { + if (instance instanceof TornadoEntity tornado) { + return tornado.getLerpedScale(g); + } else { + return original.call(instance); + } + } + + @Inject(method = "", at = @At("TAIL")) + private void initDataTrackerInjecetion(EntityRendererFactory.Context context, EntityModel entityModel, float f, CallbackInfo ci) { + this.addFeature(new AirScooterFeatureRenderer<>(this)); + } + + @Inject(method = "render(Lnet/minecraft/entity/LivingEntity;FFLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/entity/model/EntityModel;riding:Z", shift = At.Shift.AFTER)) + private void ridingInjection(T livingEntity, float f, float g, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { + if (livingEntity instanceof PlayerEntity player && AirScooterAbility.INSTANCE.isAirScooting(player)) { + this.model.riding = true; + } + } + + @ModifyVariable(method = "render(Lnet/minecraft/entity/LivingEntity;FFLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At("STORE"), ordinal = 0) + private boolean modifyIsVisibleForAang(boolean original, T livingEntity, float f, float g, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i) { + if (livingEntity instanceof PlayerEntity player) { + return original && !SpiritualProjectionAbility.INSTANCE.isSpiritualTransparent(player); + } + return original; + } + + @ModifyVariable(method = "render(Lnet/minecraft/entity/LivingEntity;FFLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At("STORE"), ordinal = 1) + private boolean modifyIsInVisibleToForAang(boolean original, T livingEntity, float f, float g, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i) { + if (livingEntity instanceof PlayerEntity player) { + return original && SpiritualProjectionAbility.INSTANCE.isSpiritualTransparent(player); + } + return original; + } +} diff --git a/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/PlayerEntityRendererMixin.java b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/PlayerEntityRendererMixin.java new file mode 100644 index 0000000..3ccfc7c --- /dev/null +++ b/aang/src/main/java/gg/norisk/heroes/aang/mixin/client/PlayerEntityRendererMixin.java @@ -0,0 +1,48 @@ +package gg.norisk.heroes.aang.mixin.client; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility; +import net.minecraft.client.model.ModelPart; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.EntityRendererFactory; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.PlayerEntityRenderer; +import net.minecraft.client.render.entity.model.PlayerEntityModel; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(PlayerEntityRenderer.class) +public abstract class PlayerEntityRendererMixin extends LivingEntityRenderer> { + public PlayerEntityRendererMixin(EntityRendererFactory.Context context, PlayerEntityModel entityModel, float f) { + super(context, entityModel, f); + } + + @WrapOperation( + method = "renderArm", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/RenderLayer;getEntitySolid(Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/RenderLayer;") + ) + private RenderLayer makeSpiritualHandTransparent(Identifier identifier, Operation original, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, AbstractClientPlayerEntity abstractClientPlayerEntity, ModelPart modelPart, ModelPart modelPart2) { + if (SpiritualProjectionAbility.INSTANCE.isSpiritualTransparent(abstractClientPlayerEntity)) { + return RenderLayer.getItemEntityTranslucentCull(identifier); + } else { + return original.call(identifier); + } + } + + @WrapOperation( + method = "renderArm", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/RenderLayer;getEntityTranslucent(Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/RenderLayer;") + ) + private RenderLayer makeSpiritualHandTransparent2(Identifier identifier, Operation original, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, AbstractClientPlayerEntity abstractClientPlayerEntity, ModelPart modelPart, ModelPart modelPart2) { + if (SpiritualProjectionAbility.INSTANCE.isSpiritualTransparent(abstractClientPlayerEntity)) { + return RenderLayer.getItemEntityTranslucentCull(identifier); + } else { + return original.call(identifier); + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/AangManager.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/AangManager.kt new file mode 100644 index 0000000..ec01548 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/AangManager.kt @@ -0,0 +1,64 @@ +package gg.norisk.heroes.aang + +import gg.norisk.heroes.aang.ability.* +import gg.norisk.heroes.aang.registry.* +import gg.norisk.heroes.client.renderer.SkinUtils +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.HeroManager.registerHero +import net.fabricmc.api.ClientModInitializer +import net.fabricmc.api.DedicatedServerModInitializer +import net.fabricmc.api.ModInitializer +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents +import net.minecraft.util.Identifier +import org.apache.logging.log4j.LogManager +import java.awt.Color + +object AangManager : ModInitializer, ClientModInitializer, DedicatedServerModInitializer { + const val MOD_ID = "aang" + val logger = LogManager.getLogger(MOD_ID) + fun String.toId() = Identifier.of(MOD_ID, this) + val aangSkin = "aang.png".toId() + val aangOverlaySkin = "aang_overlay.png".toId() + override fun onInitialize() { + logger.info("Starting $MOD_ID Hero...") + EntityRegistry.init() + ParticleRegistry.init() + EmoteRegistry.init() + AirBallAbility.init() + SpiritualProjectionAbility.init() + SoundRegistry.init() + TornadoAbility.init() + LevitationAbility.init() + } + + override fun onInitializeClient() { + SkinUtils.initClient() + EntityRendererRegistry.init() + ParticleRendererRegistry.init() + AirScooterAbility.initClient() + AirBallAbility.initClient() + TornadoAbility.initClient() + SpiritualProjectionAbility.initClient() + ClientLifecycleEvents.CLIENT_STARTED.register { + registerHeroes() + } + } + + override fun onInitializeServer() { + registerHero(Aang) + } + + private fun registerHeroes() { + registerHero(Aang) + } + + val Aang by Hero("Aang") { + ability(AirScooterAbility.Ability) + ability(AirBallAbility.Ability) + ability(SpiritualProjectionAbility.Ability) + ability(TornadoAbility.Ability) + ability(LevitationAbility.Ability) + color = Color.decode("#33C3FFFF").rgb + overlaySkin = aangOverlaySkin + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/AirBallAbility.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/AirBallAbility.kt new file mode 100644 index 0000000..b670a25 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/AirBallAbility.kt @@ -0,0 +1,282 @@ +package gg.norisk.heroes.aang.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.datatracker.entity.syncedValueChangeEvent +import gg.norisk.emote.ext.playEmote +import gg.norisk.emote.ext.stopEmote +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility.isUsingSpiritualProjection +import gg.norisk.heroes.aang.client.sound.AirBendingCircleSoundInstance +import gg.norisk.heroes.aang.entity.AirScooterEntity +import gg.norisk.heroes.aang.entity.IAangPlayer +import gg.norisk.heroes.aang.entity.aang +import gg.norisk.heroes.aang.registry.EmoteRegistry +import gg.norisk.heroes.aang.registry.EntityRegistry +import gg.norisk.heroes.client.events.ClientEvents +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.common.HeroesManager.client +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.ToggleAbility +import gg.norisk.heroes.common.networking.Networking.mousePacket +import gg.norisk.heroes.common.networking.Networking.mouseScrollPacket +import gg.norisk.heroes.common.networking.dto.MousePacket +import gg.norisk.heroes.common.utils.sound +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.AbstractClientPlayerEntity +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.particle.ParticleTypes +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.sound.SoundEvents +import net.minecraft.util.Identifier +import net.minecraft.util.math.Vec3d +import net.silkmc.silk.commands.command +import net.silkmc.silk.core.entity.directionVector +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.literal +import kotlin.random.Random + +object AirBallAbility { + val AIR_BENDING_KEY = "AangIsAirBending" + val CURRENT_AIR_BENDING_KEY = "AangCurrentBendingId" + + val airBallMaxSize = NumberProperty(3.0, 3, "Max Size", AddValueTotal(1.0, 1.0, 3.0)).apply { + icon = { + Components.item(Items.WIND_CHARGE.defaultStack) + } + } + + fun initClient() { + WorldRenderEvents.END.register(WorldRenderEvents.End { event -> + val world = event.world() ?: return@End + world.players.filter { it.isAirBending }.forEach { player -> + player.spawnAirBendingParticle() + } + }) + } + + fun init() { + syncedValueChangeEvent.listen { event -> + if (event.key != AIR_BENDING_KEY) return@listen + if (!event.entity.world.isClient) return@listen + val player = event.entity as? AbstractClientPlayerEntity ?: return@listen + if (player.isAirBending) { + player.playEmote(EmoteRegistry.AIR_BENDING) + } else { + player.stopEmote(EmoteRegistry.AIR_BENDING) + } + } + if (!FabricLoader.getInstance().isDevelopmentEnvironment) return + command("aang") { + literal("toggleairbending") { + runs { + val player = this.source.playerOrThrow + player.isAirBending = !player.isAirBending + } + } + } + } + + fun PlayerEntity.spawnAirBendingParticle() { + if (!world.isClient) return + val pos = this.getAirBendingPos() + world.addParticle( + ParticleTypes.CLOUD, + pos.x, + pos.y, + pos.z, + 0.0, + 0.0, + 0.0 + ) + } + + fun PlayerEntity.getAirBendingPos(): Vec3d { + return this.eyePos.add(this.directionVector.normalize().multiply(3.0)) + } + + fun PlayerEntity.handleTick() { + } + + var PlayerEntity.isAirBending: Boolean + get() = this.getSyncedData(AIR_BENDING_KEY) ?: false + set(value) = this.setSyncedData(AIR_BENDING_KEY, value) + + val PlayerEntity.currentBendingEntity: AirScooterEntity? + get() { + val id = if (currentBendingEntityId != -1) currentBendingEntityId else return null + return world.getEntityById(id) as? AirScooterEntity? + } + + var PlayerEntity.currentBendingEntityId: Int + get() = this.getSyncedData(CURRENT_AIR_BENDING_KEY) ?: -1 + set(value) = this.setSyncedData(CURRENT_AIR_BENDING_KEY, value) + + private fun ServerPlayerEntity.scaleWindCharges(packet: Boolean) { + val world = this.serverWorld + + val scale = 0.5 + val forceStrength = if (packet) scale else -scale + + val windCharges = + world.iterateEntities().filterIsInstance() + .filter { it.bendingType == AirScooterEntity.Type.PROJECTILE } + .filter { it.ownerId == id } + var soundFlag = true + windCharges.forEach { + val scaleAttribute = + it.attributes.getCustomInstance(EntityAttributes.GENERIC_SCALE) ?: return@forEach + scaleAttribute.baseValue += forceStrength + if (scaleAttribute.baseValue < 0.5) { + scaleAttribute.baseValue = 0.5 + soundFlag = false + } + if (scaleAttribute.baseValue > airBallMaxSize.getValue(this.uuid)) { + scaleAttribute.baseValue = airBallMaxSize.getValue(this.uuid) + soundFlag = false + } + } + if (soundFlag) { + windCharges.randomOrNull()?.sound(SoundEvents.ENTITY_BREEZE_IDLE_AIR, 0.1f, Random.nextDouble(0.8, 1.2)) + } + } + + private fun ServerPlayerEntity.launchAnyAirBall(packet: MousePacket) { + if (packet.isLeft() && packet.isClicked()) { + val windCharges = getLaunchableWindCharges().randomOrNull() + windCharges?.damage(this.damageSources.playerAttack(this), 0f) + } else if (packet.isMiddle() && packet.isClicked()) { + getLaunchableWindCharges().randomOrNull()?.launchBoomerang() + } + } + + private fun ServerPlayerEntity.getLaunchableWindCharges(): List { + return serverWorld.iterateEntities().filterIsInstance() + .filter { it.bendingType == AirScooterEntity.Type.PROJECTILE } + .filter { it.wasBended } + .filter { !it.wasLaunched } + } + + val Ability = object : ToggleAbility("Air Ball") { + init { + client { + this.keyBind = HeroKeyBindings.firstKeyBind + + ClientEvents.preHotbarScrollEvent.listen { event -> + val player = MinecraftClient.getInstance().player ?: return@listen + val entity = player.currentBendingEntity + if (entity != null && !entity.wasLaunched) { + event.isCancelled.set(true) + } + } + } + + this.cooldownProperty = + buildCooldown(10.0, 5, AddValueTotal(-1.0, -1.0, -1.0, -1.0, -1.0)) + this.maxDurationProperty = + buildMaxDuration(5.0, 5, AddValueTotal(0.1, 0.4, 0.2, 0.8, 1.5, 1.0)) + + this.properties = listOf(airBallMaxSize) + + syncedValueChangeEvent.listen { + val player = it.entity as? PlayerEntity ?: return@listen + if (it.key == AIR_BENDING_KEY && player.world.isClient) { + if (player.isAirBending && player == MinecraftClient.getInstance().player) { + MinecraftClient.getInstance().soundManager.play(AirBendingCircleSoundInstance(player)) + } + } + } + + mouseScrollPacket.receiveOnServer { packet, context -> + mcCoroutineTask(sync = true, client = false) { + context.player.scaleWindCharges(packet) + } + } + + mousePacket.receiveOnServer { packet, context -> + mcCoroutineTask(sync = true, client = false) { + context.player.launchAnyAirBall(packet) + } + } + } + + override fun getIconComponent(): Component { + return Components.item(Items.WIND_CHARGE.defaultStack) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/quartz_block_bottom.png") + } + + override fun canUse(player: ServerPlayerEntity): Boolean { + return !player.isUsingSpiritualProjection() + } + + override fun onTick(player: PlayerEntity) { + super.onTick(player) + if (player is ServerPlayerEntity) { + if (player.isAirBending) { + (player as IAangPlayer).aang_airBallSpinTracker.update(player) + + val progress = player.aang.aang_airBallSpinTracker.getSpinProgress().toDouble() + player.currentBendingEntity?.getAttributeInstance(EntityAttributes.GENERIC_SCALE)?.baseValue = + 2 * (progress / 100.0) + + if (player.aang_airBallSpinTracker.hasSpunWildly()) { + player.isAirBending = false + player.currentBendingEntity?.wasBended = true + player.sound(SoundEvents.ENTITY_BREEZE_IDLE_AIR, 0.2, 1f) + (player as IAangPlayer).aang_airBallSpinTracker.clear() + } + } else { + (player as IAangPlayer).aang_airBallSpinTracker.clear() + } + } + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + val airScooter = EntityRegistry.AIR_SCOOTER.create(player.world) ?: return + player.isAirBending = true + airScooter.ownerId = player.id + airScooter.bendingType = AirScooterEntity.Type.PROJECTILE + airScooter.setPosition(player.getAirBendingPos()) + player.serverWorld.spawnEntity(airScooter) + player.currentBendingEntityId = airScooter.id + } + } + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + player.stopAirBall(true) + } + + private fun PlayerEntity.stopAirBall(forceDiscard: Boolean = false) { + if (this is ServerPlayerEntity) { + this.isAirBending = false + val currentEntity = this.currentBendingEntity + if (forceDiscard) { + currentEntity?.discard() + this.currentBendingEntityId = -1 + } else if (currentEntity != null && currentEntity.wasBended.not()) { + this.sound(SoundEvents.ENTITY_BREEZE_IDLE_AIR, 0.1, 1.5f) + this.currentBendingEntity?.discard() + this.currentBendingEntityId = -1 + } + } + } + + override fun onEnd(player: PlayerEntity, abilityEndInformation: AbilityEndInformation) { + super.onEnd(player, abilityEndInformation) + player.stopAirBall() + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/AirScooterAbility.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/AirScooterAbility.kt new file mode 100644 index 0000000..3400703 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/AirScooterAbility.kt @@ -0,0 +1,272 @@ +package gg.norisk.heroes.aang.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.datatracker.entity.syncedValueChangeEvent +import gg.norisk.emote.network.EmoteNetworking.playEmote +import gg.norisk.emote.network.EmoteNetworking.stopEmote +import gg.norisk.heroes.aang.AangManager.toId +import gg.norisk.heroes.aang.ability.AirBallAbility.currentBendingEntity +import gg.norisk.heroes.aang.ability.AirBallAbility.isAirBending +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility.isUsingSpiritualProjection +import gg.norisk.heroes.aang.client.sound.AirScooterSoundInstance +import gg.norisk.heroes.aang.entity.AirScooterEntity +import gg.norisk.heroes.aang.entity.aang +import gg.norisk.heroes.aang.registry.EmoteRegistry +import gg.norisk.heroes.aang.registry.EntityRegistry +import gg.norisk.heroes.aang.registry.ParticleRegistry +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.client.renderer.Speedlines.showSpeedlines +import gg.norisk.heroes.common.HeroesManager.client +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.ToggleAbility +import gg.norisk.heroes.common.hero.ability.task.abilityCoroutineTask +import gg.norisk.heroes.common.utils.sound +import gg.norisk.utils.Easing +import gg.norisk.utils.OldAnimation +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import kotlinx.coroutines.cancel +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents +import net.minecraft.client.MinecraftClient +import net.minecraft.client.world.ClientWorld +import net.minecraft.entity.Entity +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.damage.DamageSource +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundEvents +import net.minecraft.util.Identifier +import net.minecraft.util.math.Box +import net.minecraft.util.math.Vec3d +import net.silkmc.silk.core.Silk +import net.silkmc.silk.core.Silk.server +import net.silkmc.silk.core.entity.modifyVelocity +import net.silkmc.silk.core.task.infiniteMcCoroutineTask +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.broadcastText +import net.silkmc.silk.network.packet.s2cPacket +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +object AirScooterAbility { + val airScooterSoundPacketS2C = s2cPacket("air-scooter-sound".toId()) + val AIR_SCOOTING_KEY = "AangIsAirScooting" + + val airScooterSpeed = NumberProperty(0.2, 3, "Speed", AddValueTotal(0.1, 0.1, 0.1)).apply { + icon = { + Components.item(Items.WIND_CHARGE.defaultStack) + } + } + val airScooterStepHeight = NumberProperty(2.0, 3, "Step Height", AddValueTotal(1.0, 1.0, 1.0)).apply { + icon = { + Components.item(Items.QUARTZ_STAIRS.defaultStack) + } + } + + fun initClient() { + airScooterSoundPacketS2C.receiveOnClient { packet, context -> + mcCoroutineTask(sync = true, client = true) { + val client = context.client + val entity = context.client.world?.getEntityById(packet) ?: return@mcCoroutineTask + client.soundManager.play(AirScooterSoundInstance(entity)) + } + } + + ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { event -> + val world = event.world ?: return@EndTick + for (player in world.players) { + val scooter = world.entities.filterIsInstance() + .filter { it.bendingType == AirScooterEntity.Type.SCOOTER } + .filter { it.ownerId == player.id } + scooter.forEach { + it.setPosition(player.pos.add(0.0, 0.2, 0.0)) + } + } + }) + } + + var PlayerEntity.isAirScooting: Boolean + get() = this.getSyncedData(AIR_SCOOTING_KEY) ?: false + set(value) = this.setSyncedData(AIR_SCOOTING_KEY, value) + + fun Entity.handleDrag(): Boolean { + return (this is PlayerEntity && this.isAirScooting) || this is AirScooterEntity + } + + fun PlayerEntity.spawnAirScooter() { + val world = world as? ServerWorld? ?: return + val airScooter = EntityRegistry.AIR_SCOOTER.create(world) ?: return + airScooter.bendingType = AirScooterEntity.Type.SCOOTER + airScooter.ownerId = id + airScooter.setPosition(this.pos) + world.spawnEntity(airScooter) + } + + fun Entity.handleBox(box: Box): Box { + if (this is PlayerEntity && this.isAirScooting) { + return box.stretch(0.0, -1.0, 0.0) + } + return box + } + + fun PlayerEntity.handleFallDamage( + f: Float, + g: Float, + damageSource: DamageSource, + cir: CallbackInfoReturnable + ) { + if (isAirScooting) { + cir.returnValue = false + } + } + + fun PlayerEntity.handleTravel(vec3d: Vec3d): Vec3d { + spawnAirScooterDust() + val x = sidewaysSpeed * 0.5f + val z = 1f + + this.movementSpeed = 0.5f + return Vec3d(x.toDouble(), vec3d.y, z.toDouble()) + } + + private fun Entity.spawnAirScooterDust() { + if (!world.isClient) return + repeat(5) { + val offset = 0.2 + val randomX = Random.nextDouble(-offset, offset).toFloat() + val randomY = Random.nextDouble(-offset, offset).toFloat() + val randomZ = Random.nextDouble(-offset, offset).toFloat() + world.addParticle( + ParticleRegistry.AIR_SCOOTER_DUST, + this.x + randomX, + this.y + randomY, + this.z + randomZ, + 0.0, + 0.0, + 0.0 + ) + } + } + + fun PlayerEntity.stopRidingAirBall() { + aang.aang_airScooterTasks.forEach { it.cancel() } + if (this is ServerPlayerEntity) { + this.isAirScooting = false + this.showSpeedlines = false + //das hier suckt iwie lieber modifiers usen + this.getAttributeInstance(EntityAttributes.GENERIC_STEP_HEIGHT)?.baseValue = 0.6 + this.getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED)?.baseValue = + 0.10000000149011612 + this.getAttributeInstance(EntityAttributes.GENERIC_GRAVITY)?.baseValue = 0.08 + this.stopEmote(EmoteRegistry.AIR_SCOOTER_SITTING) + this.sound(SoundEvents.ENTITY_BREEZE_IDLE_AIR, 0.2, 2f) + } else if (this == MinecraftClient.getInstance().player) { + this.showSpeedlines = false + } + } + + val Ability = object : ToggleAbility("Air Scooter") { + + init { + client { + this.keyBind = HeroKeyBindings.secondKeyBind + } + + this.cooldownProperty = + buildCooldown(90.0, 4, AddValueTotal(-5.0, -5.0, -5.0, -5.0)) + this.maxDurationProperty = + buildMaxDuration(5.0, 5, AddValueTotal(0.1, 0.4, 0.2, 0.8, 1.5, 1.0)) + + this.properties = listOf( + airScooterSpeed, + airScooterStepHeight + ) + + syncedValueChangeEvent.listen { + val player = it.entity as? PlayerEntity ?: return@listen + if (it.key == AIR_SCOOTING_KEY) { + if (player.isAirScooting) { + player.spawnAirScooter() + } else { + if (player.world.isClient) { + + } + } + } + } + } + + override fun canUse(player: ServerPlayerEntity): Boolean { + return !player.isUsingSpiritualProjection() + } + + override fun getIconComponent(): Component { + return Components.item(Items.WIND_CHARGE.defaultStack) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/quartz_block_bottom.png") + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + player.playEmote(EmoteRegistry.AIR_SCOOTER) + player.sound(SoundEvents.ENTITY_BREEZE_IDLE_AIR, 0.5) + player.aang.aang_airScooterTasks += abilityCoroutineTask( + sync = true, + client = false, + delay = 0.6.seconds, + executingPlayer = player + ) { + player.modifyVelocity(0.0, 0.55, 0.0) + } + player.aang.aang_airScooterTasks += abilityCoroutineTask( + sync = true, + client = false, + delay = 0.83.seconds, + executingPlayer = player + ) { + //player.modifyVelocity(0.0,1.0,0.0) + airScooterSoundPacketS2C.sendToAll(player.id) + player.isAirScooting = true + player.getAttributeInstance(EntityAttributes.GENERIC_STEP_HEIGHT)?.baseValue = + airScooterStepHeight.getValue(player.uuid) + val speedAnimation = + OldAnimation( + 0.1f, + airScooterSpeed.getValue(player.uuid).toFloat(), + 1.seconds.toJavaDuration(), + Easing.CUBIC_IN + ) + player.aang.aang_airScooterTasks += infiniteMcCoroutineTask(sync = true, client = false) { + if (speedAnimation.isDone) cancel() + player.getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED)?.baseValue = + speedAnimation.get().toDouble() + } + player.getAttributeInstance(EntityAttributes.GENERIC_GRAVITY)?.baseValue = 0.02 + player.playEmote(EmoteRegistry.AIR_SCOOTER_SITTING) + } + } else if (player == MinecraftClient.getInstance().player) { + player.showSpeedlines = true + } + } + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + player.stopRidingAirBall() + } + + override fun onEnd(player: PlayerEntity, abilityEndInformation: AbilityEndInformation) { + super.onEnd(player, abilityEndInformation) + player.stopRidingAirBall() + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/LevitationAbility.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/LevitationAbility.kt new file mode 100644 index 0000000..18d0b34 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/LevitationAbility.kt @@ -0,0 +1,124 @@ +package gg.norisk.heroes.aang.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.datatracker.entity.syncedValueChangeEvent +import gg.norisk.emote.ext.playEmote +import gg.norisk.emote.ext.stopEmote +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility.isUsingSpiritualProjection +import gg.norisk.heroes.aang.client.sound.VelocityBasedFlyingSoundInstance +import gg.norisk.heroes.aang.registry.EmoteRegistry +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.common.HeroesManager.client +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.HoldAbility +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.AbstractClientPlayerEntity +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.damage.DamageTypes +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.particle.ParticleTypes +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.util.Identifier +import net.silkmc.silk.core.event.EntityEvents + +object LevitationAbility { + val AIR_LEVITATING_KEY = "AangIsAirLevitating" + + var PlayerEntity.isAirLevitating: Boolean + get() = this.getSyncedData(AIR_LEVITATING_KEY) ?: false + set(value) = this.setSyncedData(AIR_LEVITATING_KEY, value) + + fun init() { + syncedValueChangeEvent.listen { event -> + if (event.key != AIR_LEVITATING_KEY) return@listen + if (!event.entity.world.isClient) return@listen + val player = event.entity as? AbstractClientPlayerEntity ?: return@listen + if (player.isAirLevitating) { + player.playEmote(EmoteRegistry.LEVITATION) + MinecraftClient.getInstance().soundManager.play(VelocityBasedFlyingSoundInstance(player) { + (it as? PlayerEntity?)?.isAirLevitating == true + }) + } else { + player.stopEmote(EmoteRegistry.LEVITATION) + } + } + EntityEvents.checkInvulnerability.listen { event -> + if (event.source.isOf(DamageTypes.FALL)) { + val player = event.entity as? PlayerEntity ?: return@listen + if (player.isAirLevitating) { + event.isInvulnerable.set(true) + } + } + } + } + + fun PlayerEntity.handleTick() { + if (isAirLevitating) { + world.addParticle( + ParticleTypes.CLOUD, + this.getParticleX(0.5), + this.randomBodyY, + this.getParticleZ(0.5), + 0.001, + 0.001, + 0.001, + ) + } + } + + val Ability = object : HoldAbility("Levitation") { + init { + client { + this.keyBind = HeroKeyBindings.fifthKeyBind + } + + this.cooldownProperty = + buildCooldown(10.0, 5, AddValueTotal(-0.1, -0.4, -0.2, -0.8, -1.5, -1.0)) + this.maxDurationProperty = + buildMaxDuration(5.0, 5, AddValueTotal(0.1, 0.4, 0.2, 0.8, 1.5, 1.0)) + } + + override fun getIconComponent(): Component { + return Components.item(Items.FEATHER.defaultStack) + } + + override fun canUse(player: ServerPlayerEntity): Boolean { + return !player.isUsingSpiritualProjection() + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/quartz_block_bottom.png") + } + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + cleanUp(player) + } + + private fun cleanUp(player: PlayerEntity) { + if (player is ServerPlayerEntity) { + player.isAirLevitating = false + player.getAttributeInstance(EntityAttributes.GENERIC_GRAVITY)?.baseValue = + EntityAttributes.GENERIC_GRAVITY.value().defaultValue + } + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + player.isAirLevitating = true + player.getAttributeInstance(EntityAttributes.GENERIC_GRAVITY)?.baseValue = 0.01 + } + } + + override fun onEnd(player: PlayerEntity, abilityEndInformation: AbilityEndInformation) { + super.onEnd(player, abilityEndInformation) + cleanUp(player) + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/SpiritualProjectionAbility.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/SpiritualProjectionAbility.kt new file mode 100644 index 0000000..43fa9f2 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/SpiritualProjectionAbility.kt @@ -0,0 +1,388 @@ +package gg.norisk.heroes.aang.ability + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation +import com.mojang.authlib.GameProfile +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.datatracker.entity.syncedValueChangeEvent +import gg.norisk.emote.network.EmoteNetworking.playEmote +import gg.norisk.emote.network.EmoteNetworking.stopEmote +import gg.norisk.heroes.aang.AangManager.Aang +import gg.norisk.heroes.aang.AangManager.toId +import gg.norisk.heroes.aang.ability.AirBallAbility.isAirBending +import gg.norisk.heroes.aang.ability.AirScooterAbility.isAirScooting +import gg.norisk.heroes.aang.ability.LevitationAbility.isAirLevitating +import gg.norisk.heroes.aang.client.sound.AirBendingLevitationSoundInstance +import gg.norisk.heroes.aang.client.sound.VelocityBasedFlyingSoundInstance +import gg.norisk.heroes.aang.entity.DummyPlayer +import gg.norisk.heroes.aang.entity.aang +import gg.norisk.heroes.aang.registry.EmoteRegistry +import gg.norisk.heroes.aang.registry.EmoteRegistry.toEmote +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.client.renderer.RenderUtils +import gg.norisk.heroes.common.HeroesManager.client +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.PressAbility +import gg.norisk.heroes.common.hero.ability.task.abilityCoroutineTask +import gg.norisk.heroes.common.hero.setHero +import gg.norisk.heroes.common.utils.sound +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback +import net.fabricmc.fabric.api.event.player.AttackEntityCallback +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents +import net.fabricmc.fabric.api.event.player.UseBlockCallback +import net.fabricmc.fabric.api.event.player.UseItemCallback +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.AbstractClientPlayerEntity +import net.minecraft.client.util.SkinTextures +import net.minecraft.entity.Entity +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.data.DataTracker +import net.minecraft.entity.effect.StatusEffectInstance +import net.minecraft.entity.effect.StatusEffects +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundEvents +import net.minecraft.text.Text +import net.minecraft.util.ActionResult +import net.minecraft.util.Identifier +import net.minecraft.util.TypedActionResult +import net.minecraft.world.TeleportTarget +import net.silkmc.silk.commands.command +import net.silkmc.silk.core.entity.modifyVelocity +import net.silkmc.silk.core.kotlin.ticks +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.core.text.literalText +import org.apache.commons.lang3.RandomStringUtils +import org.spongepowered.asm.mixin.injection.invoke.arg.Args +import java.util.* +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds + +object SpiritualProjectionAbility { + val LEVITATION_KEY = "IsSpiritualLevitating" + val OVERLAY = "textures/misc/spiritual_vignette.png".toId() + + fun init() { + syncedValueChangeEvent.listen { event -> + if (!event.entity.world.isClient) return@listen + if (LEVITATION_KEY == event.key) { + val player = event.entity as? PlayerEntity? ?: return@listen + if (player.isSpiritualLevitating) { + MinecraftClient.getInstance().soundManager.play(AirBendingLevitationSoundInstance(player)) + } + } + if (event.key == "IsSpiritualTransparent") { + val player = event.entity as? PlayerEntity? ?: return@listen + if (player.isSpiritualTransparent) { + MinecraftClient.getInstance().soundManager.play(VelocityBasedFlyingSoundInstance(player) { + (it as? PlayerEntity?)?.isSpiritualTransparent == true + }) + } + } + } + + UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult -> + if (player.isSpiritualTransparent && !world.isClient) { + player.cancelSpiritMode() + //TODO + return@UseBlockCallback ActionResult.FAIL + } + return@UseBlockCallback ActionResult.PASS + }) + + UseItemCallback.EVENT.register(UseItemCallback { player, world, hand -> + if (player.isSpiritualTransparent && !world.isClient) { + player.cancelSpiritMode() + //TODO + return@UseItemCallback TypedActionResult.fail(player.getStackInHand(hand)) + } + return@UseItemCallback TypedActionResult.pass(player.getStackInHand(hand)) + }) + + AttackEntityCallback.EVENT.register(AttackEntityCallback { player, world, hand, entity, hitResult -> + if (player.isSpiritualTransparent && !world.isClient) { + player.cancelSpiritMode() + //TODO + return@AttackEntityCallback ActionResult.FAIL + } + return@AttackEntityCallback ActionResult.PASS + }) + + PlayerBlockBreakEvents.BEFORE.register(PlayerBlockBreakEvents.Before { world, player, pos, state, blockEntity -> + if (player.isSpiritualTransparent) { + return@Before !player.cancelSpiritMode() + } + return@Before true + }) + + if (!FabricLoader.getInstance().isDevelopmentEnvironment) return + command("aang") { + literal("togglespiritualtransparency") { + runs { + val player = this.source.playerOrThrow + player.isSpiritualTransparent = !player.isSpiritualTransparent + } + } + literal("togglespirituallevitating") { + runs { + val player = this.source.playerOrThrow + player.isSpiritualLevitating = !player.isSpiritualLevitating + } + } + } + } + + fun LivingEntity.getAlpha(): Float { + val pulseSpeed = 10.0 // Bestimmt, wie schnell das Overlay pulsiert (höherer Wert = langsameres Pulsieren) + return (Math.sin(age / pulseSpeed) * 0.25 + 0.75).toFloat() // Wert zwischen 0.5 und 1.0 + } + + fun initClient() { + HudRenderCallback.EVENT.register(HudRenderCallback { drawContext, tickCounter -> + val player = MinecraftClient.getInstance().player ?: return@HudRenderCallback + if (player.isSpiritualLevitating || player.isSpiritualTransparent) { + // Entity age verwenden, um einen kontinuierlichen Wert zu erhalten + val pulseSpeed = + 10.0 // Bestimmt, wie schnell das Overlay pulsiert (höherer Wert = langsameres Pulsieren) + val alpha = (Math.sin(player.age / pulseSpeed) * 0.25 + 0.75).toFloat() // Wert zwischen 0.5 und 1.0 + RenderUtils.renderOverlay(drawContext, OVERLAY, alpha) + } + }) + } + + fun PlayerEntity.isUsingSpiritualProjection(): Boolean { + return isSpiritualLevitating || isSpiritualTransparent + } + + fun PlayerEntity.replaceNameWithOwner(args: Args) { + val owner = world.getEntityById(spiritualOwner) as? PlayerEntity? ?: return + args.set(1, owner.gameProfile.name.literal) + } + + fun AbstractClientPlayerEntity.replaceSkinWithOwner(original: SkinTextures): SkinTextures { + val owner = world.getEntityById(spiritualOwner) as? AbstractClientPlayerEntity? ?: return original + return owner.skinTextures + } + + fun PlayerEntity.replaceDataTrackerWithOwner(original: Operation): DataTracker { + val owner = world.getEntityById(spiritualOwner) as? PlayerEntity? ?: return original.call(this) + return owner.dataTracker + } + + fun DummyPlayer.cancelProjection(reason: Entity?) { + val owner = world.getEntityById(spiritualOwner) as? ServerPlayerEntity? + owner?.isSpiritualLevitating = false + owner?.isSpiritualTransparent = false + owner?.teleportTo( + TeleportTarget( + world as ServerWorld, + this.pos, velocity, yaw, pitch, TeleportTarget.NO_OP + ) + ) + owner?.abilities?.flying = false + owner?.abilities?.allowFlying = false + owner?.sendAbilitiesUpdate() + owner?.sound(SoundEvents.BLOCK_BEACON_DEACTIVATE, 0.2f, 2f) + discard() + } + + private fun ServerPlayerEntity.spawnFakePlayer() { + val fakePlayer = DummyPlayer( + world, blockPos, pitch, GameProfile(UUID(0, Random.nextLong()), RandomStringUtils.randomAlphabetic(16)) + ) + fakePlayer.updatePositionAndAngles(this.pos.x, this.pos.y, this.pos.z, this.yaw, this.pitch) + fakePlayer.setNoGravity(true) + fakePlayer.spiritualOwner = this.id + fakePlayer.isSpiritualLevitating = true + fakePlayer.setHero(Aang) + world.spawnEntity(fakePlayer) + mcCoroutineTask(sync = true, client = false, delay = 1.ticks) { + fakePlayer.playEmote("spiritual_projection_loop".toEmote()) + } + } + + fun PlayerEntity.handleTick() { + if (isSpiritualTransparent) { + noClip = isSpiritualTransparent + + if (!world.isClient) { + val body = (this as ServerPlayerEntity).serverWorld + .iterateEntities() + .filterIsInstance() + .filter { it.spiritualOwner == this.id } + .randomOrNull() + if (body != null) { + val distance = body.distanceTo(this) + if (distance > projectionMaxDistance.getValue(this.uuid).toFloat()) { + sendMessage(Text.translatable("heroes.katara.ability.spiritual_projection.too_far_away")) + cancelSpiritMode(body) + } + } + } + } + if (isSpiritualTransparent) { + world.addParticle( + StatusEffects.LEVITATION.value().createParticle(StatusEffectInstance(StatusEffects.LEVITATION)), + this.getParticleX(0.5), + this.randomBodyY, + this.getParticleZ(0.5), + 1.0, + 1.0, + 1.0 + ) + } + } + + private fun PlayerEntity.cancelSpiritMode(toClear: DummyPlayer? = null): Boolean { + if (this.world.isClient) return false + val body = toClear ?: (this as ServerPlayerEntity).serverWorld + .iterateEntities() + .filterIsInstance() + .filter { it.spiritualOwner == this.id } + .randomOrNull() + if (body != null) { + Ability.addCooldown(this) + body.cancelProjection(null) + return true + } + return false + } + + var PlayerEntity.isSpiritualTransparent: Boolean + get() = this.getSyncedData("IsSpiritualTransparent") ?: false + set(value) = this.setSyncedData("IsSpiritualTransparent", value) + + var PlayerEntity.spiritualOwner: Int + get() = this.getSyncedData("SpiritualOwnerId") ?: -1 + set(value) = this.setSyncedData("SpiritualOwnerId", value) + + var PlayerEntity.isSpiritualLevitating: Boolean + get() = this.getSyncedData(LEVITATION_KEY) ?: false + set(value) = this.setSyncedData(LEVITATION_KEY, value) + + val projectionMaxDistance = NumberProperty( + 25.0, 5, + "Spiritual Projection Max Distance", + AddValueTotal(10.0, 10.0, 10.0, 10.0, 10.0) + ).apply { + icon = { + Components.item(Items.SPYGLASS.defaultStack) + } + } + + val Ability = object : PressAbility("Spiritual Projection") { + init { + client { + this.keyBind = HeroKeyBindings.thirdKeyBind + } + + this.cooldownProperty = + buildCooldown(10.0, 5, AddValueTotal(-1.0, -1.0, -1.0, -1.0, -1.0)) + + this.properties = listOf(projectionMaxDistance) + } + + override fun canUse(player: ServerPlayerEntity): Boolean { + if (player.isAirScooting) { + return false + } + + if (player.hasVehicle()) { + return false + } + + if (player.isAirBending) { + return false + } + + if (player.isAirLevitating) { + return false + } + + return super.canUse(player) + } + + override fun getIconComponent(): Component { + return Components.item(Items.BLUE_STAINED_GLASS.defaultStack) + } + + override fun getUnlockCondition(): Text { + return literalText { + text(Text.translatable("heroes.ability.$internalKey.unlock_condition")) + } + } + + override fun hasUnlocked(player: PlayerEntity): Boolean { + return player.isCreative || (LevitationAbility.Ability.cooldownProperty.isMaxed(player.uuid) && LevitationAbility.Ability.maxDurationProperty.isMaxed( + player.uuid + )) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/quartz_block_bottom.png") + } + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + player.cancelSpiritMode() + player.stopLevitation() + } + + private fun PlayerEntity.stopLevitation() { + aang.aang_spiritualProjectionsTasks.forEach { it.cancel() } + isSpiritualLevitating = false + isSpiritualTransparent = false + removeStatusEffect(StatusEffects.LEVITATION) + (this as? ServerPlayerEntity)?.stopEmote(EmoteRegistry.SPIRITUAL_PROJECTION_START) + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + abilityScope.cancelCooldown() + if (player.cancelSpiritMode()) { + return + } + if (!player.isSpiritualLevitating) { + player.isSpiritualTransparent = false + player.isSpiritualLevitating = true + player.abilities.flying = false + player.abilities.allowFlying = false + player.addStatusEffect( + StatusEffectInstance( + StatusEffects.LEVITATION, + 2.12.seconds.inWholeMilliseconds.toInt() / 50 + ) + ) + player.playEmote(EmoteRegistry.SPIRITUAL_PROJECTION_START) + player.aang.aang_spiritualProjectionsTasks += abilityCoroutineTask( + sync = true, + client = false, + delay = 2.12.seconds, + executingPlayer = player + ) { + player.isSpiritualTransparent = true + player.isSpiritualLevitating = false + player.abilities.flying = true + player.abilities.allowFlying = true + player.sendAbilitiesUpdate() + player.spawnFakePlayer() + player.sound(SoundEvents.BLOCK_BEACON_ACTIVATE, 0.2, 2f) + player.modifyVelocity(0.0, 1.0, 0.0) + } + } else { + abilityScope.applyCooldown() + player.stopLevitation() + } + } + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/TornadoAbility.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/TornadoAbility.kt new file mode 100644 index 0000000..f655f12 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/ability/TornadoAbility.kt @@ -0,0 +1,242 @@ +package gg.norisk.heroes.aang.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.heroes.aang.AangManager.toId +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility.isUsingSpiritualProjection +import gg.norisk.heroes.aang.client.sound.TornadoSoundInstance +import gg.norisk.heroes.aang.entity.TornadoEntity +import gg.norisk.heroes.aang.entity.aang +import gg.norisk.heroes.aang.mixin.accessor.CameraAccessor +import gg.norisk.heroes.aang.registry.EntityRegistry +import gg.norisk.heroes.aang.utils.PlayerRotationTracker +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.common.HeroesManager.client +import gg.norisk.heroes.common.ability.CooldownProperty +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.PressAbility +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.Camera +import net.minecraft.entity.Entity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.network.packet.s2c.play.TitleFadeS2CPacket +import net.minecraft.network.packet.s2c.play.TitleS2CPacket +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.math.MathHelper +import net.minecraft.util.math.RotationAxis +import net.minecraft.util.math.Vec3d +import net.minecraft.world.BlockView +import net.silkmc.silk.commands.command +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.core.text.literalText +import net.silkmc.silk.network.packet.s2cPacket +import kotlin.math.sqrt +import kotlin.time.Duration.Companion.seconds + +object TornadoAbility { + val tornadoSoundPacketS2C = s2cPacket("tornado-sound-packet".toId()) + var currentYaw: Float = 0f + var currentPitch: Float = 0f + var rotationAngle = 0f // Variable zum Speichern des Rotationswinkels + + var PlayerEntity.isTornadoMode: Boolean + get() = this.getSyncedData("isTornadoMode") ?: false + set(value) = this.setSyncedData("isTornadoMode", value) + + fun Camera.handleTornadoCamera(blockView: BlockView, entity: Entity, bl: Boolean, bl2: Boolean, f: Float) { + val dummy = (this as CameraAccessor) + val player = entity as? PlayerEntity? ?: return + val pos = pos.add(20.0, 19.0, 0.0) + val (pitch, yaw) = lookAt(pos, entity.pos) + if (!player.isTornadoMode) { + currentYaw = yaw + currentPitch = pitch + return + } + currentYaw = MathHelper.lerp(f * 0.05f, currentYaw, yaw) + currentPitch = MathHelper.lerp(f * 0.05f, currentPitch, pitch) + invokeSetPos(pos.x, pos.y, pos.z) + invokeSetRotation(currentYaw, currentPitch) + } + + fun lookAt(currentPos: Vec3d, center: Vec3d): Pair { + val d = center.x - currentPos.x + val e = center.y - currentPos.y + val f = center.z - currentPos.z + val g = sqrt(d * d + f * f) + val pitch = MathHelper.wrapDegrees((-(MathHelper.atan2(e, g) * 180.0f / Math.PI.toFloat())).toFloat()) + val yaw = MathHelper.wrapDegrees((MathHelper.atan2(f, d) * 180.0f / Math.PI.toFloat()).toFloat() - 90.0f) + return Pair(pitch, yaw) + } + + private fun ServerPlayerEntity.summonTornado() { + val tornadoEntity = EntityRegistry.TORNADO.create(this.serverWorld) ?: return + aang.aang_tornadoEntity = tornadoEntity + tornadoEntity.setPosition(this.pos) + tornadoEntity.ownerId = this.id + tornadoEntity.rotationTracker = PlayerRotationTracker() + tornadoEntity.isGrowingMode = true + aang.aang_tornadoTasks += mcCoroutineTask(sync = true, client = false, delay = 5.seconds) { + tornadoEntity.isGrowingMode = false + tornadoEntity.rotationTracker?.movementIncreaseRate = + tornadoIncreaseRateProperty.getValue(this@summonTornado.uuid).toFloat() + tornadoEntity.rotationTracker?.onlyDecay = true + tornadoEntity.rotationTracker?.movementDecayRate = + tornadoDecreaseRateProperty.getValue(this@summonTornado.uuid).toFloat() + aang.aang_tornadoTasks += mcCoroutineTask( + sync = true, + client = false, + delay = tornadoMaxDurationProperty.getValue(this@summonTornado.uuid).seconds + ) { + tornadoEntity.disappear(tornadoEntity.controllingPassenger) + } + } + this.networkHandler.sendPacket(TitleS2CPacket("SPIN YOUR MOUSE".literal)) + this.networkHandler.sendPacket(TitleFadeS2CPacket(5, 20, 5)) + this.isTornadoMode = true + this.serverWorld.spawnEntity(tornadoEntity) + this.startRiding(tornadoEntity, true) + } + + fun initClient() { + tornadoSoundPacketS2C.receiveOnClient { packet, context -> + mcCoroutineTask(sync = true, client = true) { + val client = context.client + val entity = context.client.world?.getEntityById(packet) as? TornadoEntity? ?: return@mcCoroutineTask + client.soundManager.play(TornadoSoundInstance(entity)) + } + } + HudRenderCallback.EVENT.register(HudRenderCallback { drawContext, tickCounter -> + val player = MinecraftClient.getInstance().player ?: return@HudRenderCallback + val vehicle = player.vehicle as? TornadoEntity? ?: return@HudRenderCallback + if (vehicle.ownerId != player.id) return@HudRenderCallback + if (!vehicle.isGrowingMode) return@HudRenderCallback + val rotationTracker = vehicle.rotationTracker ?: return@HudRenderCallback + val scale = rotationTracker.getPercentageBetween(1f, 5f) + val speed = rotationTracker.getPercentageBetween(2f, 7f) + + val width = drawContext.scaledWindowWidth / 2 + val height = drawContext.scaledWindowHeight / 2 + val matrixStack = drawContext.matrices + rotationAngle = (rotationAngle + speed) % 360f + matrixStack.push() + // Bewege den Ursprungspunkt auf die Mitte des Bildschirms + matrixStack.translate(width.toDouble(), height.toDouble(), 0.0) + matrixStack.scale(scale, scale, scale) + + // Drehe das Zeichen basierend auf der aktuellen Rotation + matrixStack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(rotationAngle)) + + // Bewege den Ursprungspunkt zurück, um den Text korrekt zu positionieren + matrixStack.translate(-width.toDouble(), -height.toDouble(), 0.0) + drawContext.drawText(MinecraftClient.getInstance().textRenderer, "↓".literal, width, height, -1, false) + matrixStack.pop() + }) + } + + fun init() { + if (!FabricLoader.getInstance().isDevelopmentEnvironment) return + command("aang") { + literal("toggleplayerrotationtracker") { + runs { + this.source.playerOrThrow.summonTornado() + } + } + } + } + + val tornadoMaxDurationProperty = CooldownProperty( + 10.0, 3, + "Max Duration", + AddValueTotal(5.0, 5.0, 5.0) + ) + val tornadoIncreaseRateProperty = NumberProperty( + 0.005, 3, + "Tornado Increase Rate", + AddValueTotal(0.0025, 0.0025, 0.005) + ).apply { + icon = { + Components.item(Items.GLOWSTONE_DUST.defaultStack) + } + } + val tornadoDecreaseRateProperty = NumberProperty( + 0.2, 3, + "Tornado Decrease Rate", + AddValueTotal(-0.0025, -0.0025, -0.005) + ).apply { + icon = { + Components.item(Items.REDSTONE.defaultStack) + } + } + + val Ability = object : PressAbility("Tornado") { + + init { + client { + this.keyBind = HeroKeyBindings.fourthKeyBinding + } + + this.cooldownProperty = + buildCooldown(130.0, 4, AddValueTotal(-10.0, -10.0, -10.0, -10.0)) + + this.properties = + listOf(tornadoMaxDurationProperty, tornadoIncreaseRateProperty, tornadoDecreaseRateProperty) + } + + override fun canUse(player: ServerPlayerEntity): Boolean { + return !player.isUsingSpiritualProjection() + } + + override fun getIconComponent(): Component { + return Components.item(Items.WIND_CHARGE.defaultStack) + } + + override fun hasUnlocked(player: PlayerEntity): Boolean { + return player.isCreative || (AirBallAbility.Ability.cooldownProperty.isMaxed(player.uuid) && AirBallAbility.airBallMaxSize.isMaxed( + player.uuid + )) + } + + override fun getUnlockCondition(): Text { + return literalText { + text(Text.translatable("heroes.ability.$internalKey.unlock_condition")) + } + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/quartz_block_bottom.png") + } + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + cleanUp(player) + } + + private fun cleanUp(player: PlayerEntity) { + player.aang.aang_tornadoTasks.forEach { it.cancel() } + player.aang.aang_tornadoEntity?.disappear(player) + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + if (!player.isTornadoMode) { + abilityScope.cancelCooldown() + player.summonTornado() + } else { + cleanUp(player) + } + } + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/particle/AirScooterDustParticle.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/particle/AirScooterDustParticle.kt new file mode 100644 index 0000000..abec7eb --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/particle/AirScooterDustParticle.kt @@ -0,0 +1,75 @@ +package gg.norisk.heroes.aang.client.particle + +import gg.norisk.utils.Easing +import gg.norisk.utils.OldAnimation +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.particle.* +import net.minecraft.client.world.ClientWorld +import net.minecraft.particle.ParticleEffect +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +@Environment(EnvType.CLIENT) +class AirScooterDustParticle internal constructor( + clientWorld: ClientWorld, + d: Double, + e: Double, + f: Double, + g: Double, + h: Double, + i: Double, + bl: Boolean +) : SpriteBillboardParticle(clientWorld, d, e, f) { + val scaleAnimation = OldAnimation(0f, 3.5f, 1.seconds.toJavaDuration(), Easing.LINEAR) + + init { + this.setBoundingBoxSpacing(0.25f, 0.25f) + this.maxAge = random.nextInt(50) + this.gravityStrength = 3.0E-6f + this.velocityX = g + this.velocityY = h + this.velocityZ = i + } + + override fun tick() { + this.prevPosX = this.x + this.prevPosY = this.y + this.prevPosZ = this.z + this.scale = scaleAnimation.get() + if (age++ < this.maxAge && !(this.alpha <= 0.0f)) { + this.velocityX += (random.nextFloat() / 5000.0f * (if (random.nextBoolean()) 1 else -1).toFloat()).toDouble() + this.velocityZ += (random.nextFloat() / 5000.0f * (if (random.nextBoolean()) 1 else -1).toFloat()).toDouble() + this.velocityY -= gravityStrength.toDouble() + this.move(this.velocityX, this.velocityY, this.velocityZ) + if (this.age >= this.maxAge - 60 && this.alpha > 0.01f) { + this.alpha -= 0.015f + } + } else { + this.markDead() + } + } + + override fun getType(): ParticleTextureSheet { + return ParticleTextureSheet.PARTICLE_SHEET_TRANSLUCENT + } + + @Environment(EnvType.CLIENT) + class Factory(private val spriteProvider: SpriteProvider) : ParticleFactory { + override fun createParticle( + defaultParticleType: ParticleEffect, + clientWorld: ClientWorld, + d: Double, + e: Double, + f: Double, + g: Double, + h: Double, + i: Double + ): Particle { + val airScooterParticle = AirScooterDustParticle(clientWorld, d, e, f, g, h, i, false) + airScooterParticle.setAlpha(0.5f) + airScooterParticle.setSprite(this.spriteProvider) + return airScooterParticle + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/particle/BendingAirParticle.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/particle/BendingAirParticle.kt new file mode 100644 index 0000000..8b5d6a4 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/particle/BendingAirParticle.kt @@ -0,0 +1,74 @@ +package gg.norisk.heroes.aang.client.particle + +import gg.norisk.utils.Easing +import gg.norisk.utils.OldAnimation +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.particle.* +import net.minecraft.client.world.ClientWorld +import net.minecraft.particle.ParticleEffect +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +class BendingAirParticle internal constructor( + clientWorld: ClientWorld, + d: Double, + e: Double, + f: Double, + g: Double, + h: Double, + i: Double, + bl: Boolean +) : SpriteBillboardParticle(clientWorld, d, e, f) { + val scaleAnimation = OldAnimation(0.2f, 0.5f, 1.seconds.toJavaDuration(), Easing.LINEAR) + + init { + this.setBoundingBoxSpacing(0.25f, 0.25f) + this.maxAge = random.nextInt(50) + this.gravityStrength = 3.0E-6f + this.velocityX = g + this.velocityY = h + this.velocityZ = i + } + + override fun tick() { + this.prevPosX = this.x + this.prevPosY = this.y + this.prevPosZ = this.z + this.scale = scaleAnimation.get() + if (age++ < this.maxAge && !(this.alpha <= 0.0f)) { + this.velocityX += (random.nextFloat() / 5000.0f * (if (random.nextBoolean()) 1 else -1).toFloat()).toDouble() + this.velocityZ += (random.nextFloat() / 5000.0f * (if (random.nextBoolean()) 1 else -1).toFloat()).toDouble() + this.velocityY -= gravityStrength.toDouble() + this.move(this.velocityX, this.velocityY, this.velocityZ) + if (this.age >= this.maxAge - 60 && this.alpha > 0.01f) { + this.alpha -= 0.015f + } + } else { + this.markDead() + } + } + + override fun getType(): ParticleTextureSheet { + return ParticleTextureSheet.PARTICLE_SHEET_TRANSLUCENT + } + + @Environment(EnvType.CLIENT) + class Factory(private val spriteProvider: SpriteProvider) : ParticleFactory { + override fun createParticle( + defaultParticleType: ParticleEffect, + clientWorld: ClientWorld, + d: Double, + e: Double, + f: Double, + g: Double, + h: Double, + i: Double + ): Particle { + val airScooterParticle = BendingAirParticle(clientWorld, d, e, f, g, h, i, false) + airScooterParticle.setAlpha(0.5f) + airScooterParticle.setSprite(this.spriteProvider) + return airScooterParticle + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/AirScooterEntityRenderer.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/AirScooterEntityRenderer.kt new file mode 100644 index 0000000..5eda3c2 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/AirScooterEntityRenderer.kt @@ -0,0 +1,63 @@ +package gg.norisk.heroes.aang.client.render.entity + +import gg.norisk.heroes.aang.AangManager.toId +import gg.norisk.heroes.aang.client.render.entity.model.AirScooterEntityModel +import gg.norisk.heroes.aang.entity.AirScooterEntity +import gg.norisk.heroes.aang.registry.EntityRendererRegistry +import net.minecraft.client.render.OverlayTexture +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.entity.EntityRendererFactory +import net.minecraft.client.render.entity.LivingEntityRenderer +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.util.Identifier +import net.minecraft.util.math.MathHelper + +class AirScooterEntityRenderer(context: EntityRendererFactory.Context) : + LivingEntityRenderer( + context, + AirScooterEntityModel(context.getPart(EntityRendererRegistry.AIR_SCOOTER_LAYER)), + 0f + ) { + override fun render( + abstractWindChargeEntity: AirScooterEntity, + f: Float, + g: Float, + matrixStack: MatrixStack, + vertexConsumerProvider: VertexConsumerProvider, + i: Int + ) { + if (abstractWindChargeEntity.age >= 2 || !(dispatcher.camera.focusedEntity.squaredDistanceTo( + abstractWindChargeEntity + ) < RANDOM_MOJANG_FIELD.toDouble()) + ) { + matrixStack.push() + val h = abstractWindChargeEntity.age.toFloat() + g + val vertexConsumer = vertexConsumerProvider.getBuffer( + RenderLayer.getBreezeWind( + TEXTURE, + getXOffset(h) % 1.0f, 0.0f + ) + ) + model.setAngles(abstractWindChargeEntity, 0.0f, 0.0f, h, 0.0f, 0.0f) + val scale: Float = abstractWindChargeEntity.getLerpedScale(g * 0.05f) + matrixStack.scale(scale, scale, scale) + model.render(matrixStack, vertexConsumer, i, OverlayTexture.DEFAULT_UV) + matrixStack.pop() + } + } + + + override fun getTexture(abstractWindChargeEntity: AirScooterEntity): Identifier { + return TEXTURE + } + + companion object { + fun getXOffset(f: Float): Float { + return f * 0.03f + } + + private val RANDOM_MOJANG_FIELD = MathHelper.square(3.5f) + val TEXTURE: Identifier = "textures/entity/projectiles/air_scooter.png".toId() + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/TornadoEntityRenderer.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/TornadoEntityRenderer.kt new file mode 100644 index 0000000..a36fd38 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/TornadoEntityRenderer.kt @@ -0,0 +1,94 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// +package gg.norisk.heroes.aang.client.render.entity + +import gg.norisk.heroes.aang.client.render.entity.feature.TornadoWindFeatureRenderer +import gg.norisk.heroes.aang.client.render.entity.model.TornadoEntityModel +import gg.norisk.heroes.aang.entity.TornadoEntity +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.model.ModelPart +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.entity.EntityRendererFactory +import net.minecraft.client.render.entity.MobEntityRenderer +import net.minecraft.client.render.entity.model.EntityModelLayers +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.util.Identifier +import net.minecraft.util.math.RotationAxis + +@Environment(EnvType.CLIENT) +class TornadoEntityRenderer(context: EntityRendererFactory.Context) : + MobEntityRenderer( + context, TornadoEntityModel(context.getPart(EntityModelLayers.BREEZE)), 0f + ) { + init { + addFeature(TornadoWindFeatureRenderer(context, this)) + } + + override fun getTexture(entity: TornadoEntity): Identifier { + return TEXTURE + } + + override fun render( + breezeEntity: TornadoEntity, + f: Float, + g: Float, + matrixStack: MatrixStack, + vertexConsumerProvider: VertexConsumerProvider, + i: Int + ) { + matrixStack.push() + val model: TornadoEntityModel = getModel() + updatePartVisibility(model, model.head, model.rods) + model.head.visible = false + model.rods.visible = false + // Hier wird die neue Rotation berechnet + val m = breezeEntity.age.toFloat() + g + val rotationAngle = this.getRotationAngle(m) + + // Rotiert die Entität kontinuierlich um die Y-Achse (vertikale Achse) + matrixStack.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(rotationAngle)) + super.render(breezeEntity, f, g, matrixStack, vertexConsumerProvider, i) + matrixStack.pop() + } + + private fun getRotationAngle(ageInTicks: Float): Float { + val rotationSpeed = 20.0f // Passt die Rotationsgeschwindigkeit an (Winkel pro Tick) + return (ageInTicks * rotationSpeed) % 360.0f // Vollständige Rotation (0 bis 360 Grad) + } + + override fun setupTransforms( + livingEntity: TornadoEntity?, + matrixStack: MatrixStack?, + f: Float, + g: Float, + h: Float, + i: Float + ) { + + } + + companion object { + private val TEXTURE: Identifier = Identifier.ofVanilla("textures/entity/breeze/breeze.png") + + fun updatePartVisibility( + breezeEntityModel: TornadoEntityModel, vararg modelParts: ModelPart + ): TornadoEntityModel { + breezeEntityModel.head.visible = false + breezeEntityModel.eyes.visible = false + breezeEntityModel.rods.visible = false + breezeEntityModel.windBody.visible = false + val var2: Array = modelParts + val var3 = modelParts.size + + for (var4 in 0 until var3) { + val modelPart = var2[var4] + modelPart.visible = true + } + + return breezeEntityModel + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/feature/AirScooterFeatureRenderer.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/feature/AirScooterFeatureRenderer.kt new file mode 100644 index 0000000..8db425e --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/feature/AirScooterFeatureRenderer.kt @@ -0,0 +1,45 @@ +package gg.norisk.heroes.aang.client.render.entity.feature + +import gg.norisk.heroes.aang.client.render.entity.model.AirScooterEntityModel +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.entity.feature.FeatureRenderer +import net.minecraft.client.render.entity.feature.FeatureRendererContext +import net.minecraft.client.render.entity.model.EntityModel +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.Entity + +class AirScooterFeatureRenderer>(featureRendererContext: FeatureRendererContext) : + FeatureRenderer(featureRendererContext) { + val airBall = AirScooterEntityModel(AirScooterEntityModel.getTexturedModelData().createModel()) + + override fun render( + matrixStack: MatrixStack, + vertexConsumerProvider: VertexConsumerProvider, + i: Int, + entity: T, + f: Float, + g: Float, + h: Float, + j: Float, + k: Float, + l: Float + ) { + /*val player = entity as? PlayerEntity? ?: return + if (!player.isAirScooting) return + matrixStack.push() + val age = entity.age.toFloat() + h + val vertexConsumer = vertexConsumerProvider.getBuffer( + RenderLayer.getBreezeWind( + TEXTURE, + getXOffset(age) % 1.0f, 0.0f + ) + ) + val scale: Float = 3.0f + matrixStack.translate(0.0, entity.getEyeHeight(entity.pose).toDouble(), 0.0) + matrixStack.scale(scale, scale, scale) + airBall.setAngles(null, 0.0f, 0.0f, age, 0.0f, 0.0f) + airBall.render(matrixStack, vertexConsumer, i, OverlayTexture.DEFAULT_UV) + matrixStack.pop()*/ + + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/feature/TornadoWindFeatureRenderer.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/feature/TornadoWindFeatureRenderer.kt new file mode 100644 index 0000000..9121235 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/feature/TornadoWindFeatureRenderer.kt @@ -0,0 +1,56 @@ +package gg.norisk.heroes.aang.client.render.entity.feature + +import gg.norisk.heroes.aang.client.render.entity.TornadoEntityRenderer.Companion.updatePartVisibility +import gg.norisk.heroes.aang.client.render.entity.model.TornadoEntityModel +import gg.norisk.heroes.aang.entity.TornadoEntity +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.render.OverlayTexture +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.entity.EntityRendererFactory +import net.minecraft.client.render.entity.feature.FeatureRenderer +import net.minecraft.client.render.entity.feature.FeatureRendererContext +import net.minecraft.client.render.entity.model.EntityModelLayers +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.util.Identifier + +@Environment(EnvType.CLIENT) +class TornadoWindFeatureRenderer( + context: EntityRendererFactory.Context, + featureRendererContext: FeatureRendererContext? +) : FeatureRenderer(featureRendererContext) { + private val model = TornadoEntityModel(context.getPart(EntityModelLayers.BREEZE_WIND)) + + override fun render( + matrixStack: MatrixStack, + vertexConsumerProvider: VertexConsumerProvider, + i: Int, + breezeEntity: TornadoEntity, + f: Float, + g: Float, + h: Float, + j: Float, + k: Float, + l: Float + ) { + val m = breezeEntity.age.toFloat() + h + val vertexConsumer = + vertexConsumerProvider.getBuffer(RenderLayer.getBreezeWind(TEXTURE, this.getXOffset(m) % 1.0f, 0.0f)) + model.setAngles(breezeEntity, f, g, j, k, l) + updatePartVisibility(this.model, model.windBody).render( + matrixStack, + vertexConsumer, + i, + OverlayTexture.DEFAULT_UV + ) + } + + private fun getXOffset(f: Float): Float { + return f * 0.02f + } + + companion object { + private val TEXTURE: Identifier = Identifier.ofVanilla("textures/entity/breeze/breeze_wind.png") + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/model/AirScooterEntityModel.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/model/AirScooterEntityModel.kt new file mode 100644 index 0000000..15d3369 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/model/AirScooterEntityModel.kt @@ -0,0 +1,47 @@ +package gg.norisk.heroes.aang.client.render.entity.model + +import gg.norisk.heroes.aang.entity.AirScooterEntity +import net.minecraft.client.model.* +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.entity.model.SinglePartEntityModel + +class AirScooterEntityModel(modelPart: ModelPart) : SinglePartEntityModel( + RenderLayer::getEntityTranslucent +) { + private val bone: ModelPart = modelPart.getChild("bone") + private val windCharge: ModelPart = bone.getChild("wind_charge") + private val wind: ModelPart = bone.getChild("wind") + + override fun setAngles( + abstractWindChargeEntity: AirScooterEntity?, f: Float, g: Float, h: Float, i: Float, j: Float + ) { + windCharge.yaw = -h * 16.0f * ((Math.PI / 180.0).toFloat()) + wind.yaw = h * 16.0f * ((Math.PI / 180.0).toFloat()) + } + + override fun getPart(): ModelPart { + return this.bone + } + + companion object { + fun getTexturedModelData(): TexturedModelData { + val modelData = ModelData() + val modelPartData = modelData.root + val modelPartData2 = + modelPartData.addChild("bone", ModelPartBuilder.create(), ModelTransform.pivot(0.0f, 0.0f, 0.0f)) + modelPartData2.addChild( + "wind", + ModelPartBuilder.create() + .uv(15, 20).cuboid(-4.0f, -1.0f, -4.0f, 8.0f, 2.0f, 8.0f, Dilation(0.0f)) + .uv(0, 9).cuboid(-3.0f, -2.0f, -3.0f, 6.0f, 4.0f, 6.0f, Dilation(0.0f)), + ModelTransform.of(0.0f, 0.0f, 0.0f, 0.0f, -0.7854f, 0.0f) + ) + modelPartData2.addChild( + "wind_charge", + ModelPartBuilder.create().uv(0, 0).cuboid(-2.0f, -2.0f, -2.0f, 4.0f, 4.0f, 4.0f, Dilation(0.0f)), + ModelTransform.pivot(0.0f, 0.0f, 0.0f) + ) + return TexturedModelData.of(modelData, 64, 32) + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/model/TornadoEntityModel.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/model/TornadoEntityModel.kt new file mode 100644 index 0000000..201b839 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/render/entity/model/TornadoEntityModel.kt @@ -0,0 +1,108 @@ +package gg.norisk.heroes.aang.client.render.entity.model + +import gg.norisk.heroes.aang.entity.TornadoEntity +import net.minecraft.client.model.* +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.entity.model.SinglePartEntityModel +import net.minecraft.util.math.MathHelper +import java.util.function.Function + +class TornadoEntityModel(private val root: ModelPart) : SinglePartEntityModel( + Function(RenderLayer::getEntityTranslucent) +) { + val head: ModelPart + val eyes: ModelPart + val windBody: ModelPart = root.getChild("wind_body") + private val windTop: ModelPart + private val windMid: ModelPart + private val windBottom: ModelPart = windBody.getChild("wind_bottom") + val rods: ModelPart + + init { + this.windMid = windBottom.getChild("wind_mid") + this.windTop = windMid.getChild("wind_top") + this.head = root.getChild("body").getChild("head") + this.eyes = head.getChild("eyes") + this.rods = root.getChild("body").getChild("rods") + } + + override fun setAngles(breezeEntity: TornadoEntity, f: Float, g: Float, h: Float, i: Float, j: Float) { + this.part.traverse().forEach { obj: ModelPart -> obj.resetTransform() } + val k = h * 3.1415927f * -0.1f + windTop.pivotX = MathHelper.cos(k) * 1.0f * 0.6f + windTop.pivotZ = MathHelper.sin(k) * 1.0f * 0.6f + windMid.pivotX = MathHelper.sin(k) * 0.5f * 0.8f + windMid.pivotZ = MathHelper.cos(k) * 0.8f + windBottom.pivotX = MathHelper.cos(k) * -0.25f * 1.0f + windBottom.pivotZ = MathHelper.sin(k) * -0.25f * 1.0f + head.pivotY = 4.0f + MathHelper.cos(k) / 4.0f + rods.yaw = h * 3.1415927f * 0.1f + } + + override fun getPart(): ModelPart { + return this.root + } + + companion object { + private const val field_47431 = 0.6f + private const val field_47432 = 0.8f + private const val field_47433 = 1.0f + fun getTexturedModelData(i: Int, j: Int): TexturedModelData { + val modelData = ModelData() + val modelPartData = modelData.root + val modelPartData2 = + modelPartData.addChild("body", ModelPartBuilder.create(), ModelTransform.pivot(0.0f, 0.0f, 0.0f)) + val modelPartData3 = + modelPartData2.addChild("rods", ModelPartBuilder.create(), ModelTransform.pivot(0.0f, 8.0f, 0.0f)) + modelPartData3.addChild( + "rod_1", + ModelPartBuilder.create().uv(0, 17).cuboid(-1.0f, 0.0f, -3.0f, 2.0f, 8.0f, 2.0f, Dilation(0.0f)), + ModelTransform.of(2.5981f, -3.0f, 1.5f, -2.7489f, -1.0472f, 3.1416f) + ) + modelPartData3.addChild( + "rod_2", + ModelPartBuilder.create().uv(0, 17).cuboid(-1.0f, 0.0f, -3.0f, 2.0f, 8.0f, 2.0f, Dilation(0.0f)), + ModelTransform.of(-2.5981f, -3.0f, 1.5f, -2.7489f, 1.0472f, 3.1416f) + ) + modelPartData3.addChild( + "rod_3", + ModelPartBuilder.create().uv(0, 17).cuboid(-1.0f, 0.0f, -3.0f, 2.0f, 8.0f, 2.0f, Dilation(0.0f)), + ModelTransform.of(0.0f, -3.0f, -3.0f, 0.3927f, 0.0f, 0.0f) + ) + val modelPartData4 = modelPartData2.addChild( + "head", + ModelPartBuilder.create().uv(4, 24).cuboid(-5.0f, -5.0f, -4.2f, 10.0f, 3.0f, 4.0f, Dilation(0.0f)) + .uv(0, 0).cuboid(-4.0f, -8.0f, -4.0f, 8.0f, 8.0f, 8.0f, Dilation(0.0f)), + ModelTransform.pivot(0.0f, 4.0f, 0.0f) + ) + modelPartData4.addChild( + "eyes", + ModelPartBuilder.create().uv(4, 24).cuboid(-5.0f, -5.0f, -4.2f, 10.0f, 3.0f, 4.0f, Dilation(0.0f)) + .uv(0, 0).cuboid(-4.0f, -8.0f, -4.0f, 8.0f, 8.0f, 8.0f, Dilation(0.0f)), + ModelTransform.pivot(0.0f, 0.0f, 0.0f) + ) + val modelPartData5 = + modelPartData.addChild("wind_body", ModelPartBuilder.create(), ModelTransform.pivot(0.0f, 0.0f, 0.0f)) + val modelPartData6 = modelPartData5.addChild( + "wind_bottom", + ModelPartBuilder.create().uv(1, 83).cuboid(-2.5f, -7.0f, -2.5f, 5.0f, 7.0f, 5.0f, Dilation(0.0f)), + ModelTransform.pivot(0.0f, 24.0f, 0.0f) + ) + val modelPartData7 = modelPartData6.addChild( + "wind_mid", + ModelPartBuilder.create().uv(74, 28).cuboid(-6.0f, -6.0f, -6.0f, 12.0f, 6.0f, 12.0f, Dilation(0.0f)) + .uv(78, 32).cuboid(-4.0f, -6.0f, -4.0f, 8.0f, 6.0f, 8.0f, Dilation(0.0f)).uv(49, 71) + .cuboid(-2.5f, -6.0f, -2.5f, 5.0f, 6.0f, 5.0f, Dilation(0.0f)), + ModelTransform.pivot(0.0f, -7.0f, 0.0f) + ) + modelPartData7.addChild( + "wind_top", + ModelPartBuilder.create().uv(0, 0).cuboid(-9.0f, -8.0f, -9.0f, 18.0f, 8.0f, 18.0f, Dilation(0.0f)) + .uv(6, 6).cuboid(-6.0f, -8.0f, -6.0f, 12.0f, 8.0f, 12.0f, Dilation(0.0f)).uv(105, 57) + .cuboid(-2.5f, -8.0f, -2.5f, 5.0f, 8.0f, 5.0f, Dilation(0.0f)), + ModelTransform.pivot(0.0f, -6.0f, 0.0f) + ) + return TexturedModelData.of(modelData, i, j) + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/AirBendingCircleSoundInstance.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/AirBendingCircleSoundInstance.kt new file mode 100644 index 0000000..76f70f4 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/AirBendingCircleSoundInstance.kt @@ -0,0 +1,51 @@ +package gg.norisk.heroes.aang.client.sound + +import gg.norisk.heroes.aang.ability.AirBallAbility.isAirBending +import gg.norisk.heroes.aang.entity.aang +import gg.norisk.heroes.aang.registry.SoundRegistry +import net.minecraft.client.sound.MovingSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.sound.SoundCategory + +class AirBendingCircleSoundInstance(private val entity: PlayerEntity) : + MovingSoundInstance(SoundRegistry.FLYING, SoundCategory.NEUTRAL, SoundInstance.createRandom()) { + var fadeTime = 20 + var isFading = false + + init { + this.repeat = true + this.repeatDelay = 0 + this.volume = 0.01f + } + + override fun tick() { + if (isFading) { + --fadeTime + this.volume *= 0.9f + if (fadeTime < 0) { + this.setDone() + return + } + } + + this.x = entity.x.toFloat().toDouble() + this.y = entity.y.toFloat().toDouble() + this.z = entity.z.toFloat().toDouble() + + val progress = entity.aang.aang_airBallSpinTracker.getSpinProgress() + if (!entity.isAirBending) { + isFading = true + return + } + + if (!entity.isRemoved) { + val percentage = (progress / 100.0) * 0.2 + val f: Float = Math.min(0.5f, Math.max(0.1f, percentage.toFloat())) + this.volume = f + this.pitch = 1f + this.volume + } else { + isFading = true + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/AirBendingLevitationSoundInstance.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/AirBendingLevitationSoundInstance.kt new file mode 100644 index 0000000..1cddabd --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/AirBendingLevitationSoundInstance.kt @@ -0,0 +1,42 @@ +package gg.norisk.heroes.aang.client.sound + +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility.isSpiritualLevitating +import net.minecraft.client.sound.MovingSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvents + +class AirBendingLevitationSoundInstance(private val entity: PlayerEntity) : + MovingSoundInstance(SoundEvents.BLOCK_BEACON_ACTIVATE, SoundCategory.NEUTRAL, SoundInstance.createRandom()) { + var fadeTime = 20 + var isFading = false + + init { + this.repeat = true + this.repeatDelay = 0 + this.volume = 0.01f + } + + override fun tick() { + if (isFading) { + --fadeTime + this.volume *= 0.9f + if (fadeTime < 0) { + this.setDone() + return + } + } + + this.x = entity.x.toFloat().toDouble() + this.y = entity.y.toFloat().toDouble() + this.z = entity.z.toFloat().toDouble() + + if (!entity.isRemoved && entity.isSpiritualLevitating) { + this.volume = 0.75f + this.pitch = 0.5f + } else { + isFading = true + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/AirScooterSoundInstance.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/AirScooterSoundInstance.kt new file mode 100644 index 0000000..2fdd71a --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/AirScooterSoundInstance.kt @@ -0,0 +1,39 @@ +package gg.norisk.heroes.aang.client.sound + +import gg.norisk.heroes.aang.ability.AirScooterAbility.isAirScooting +import gg.norisk.heroes.aang.entity.AirScooterEntity +import gg.norisk.heroes.aang.registry.SoundRegistry +import net.minecraft.client.sound.MovingSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.entity.Entity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.sound.SoundCategory + +class AirScooterSoundInstance(private val entity: Entity) : + MovingSoundInstance(SoundRegistry.FLYING, SoundCategory.NEUTRAL, SoundInstance.createRandom()) { + + init { + this.repeat = true + this.repeatDelay = 0 + this.volume = 0.3f + } + + override fun tick() { + val flag = when { + entity is AirScooterEntity -> true + entity is PlayerEntity && entity.isAirScooting -> true + else -> false + } + if (!entity.isRemoved && flag) { + this.x = entity.x.toFloat().toDouble() + this.y = entity.y.toFloat().toDouble() + this.z = entity.z.toFloat().toDouble() + + val f: Float = Math.min(0.3f, Math.max(0.1f, this.entity.velocity.lengthSquared().toFloat())) + this.volume = f + this.pitch = 1f + this.volume + } else { + this.setDone() + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/TornadoSoundInstance.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/TornadoSoundInstance.kt new file mode 100644 index 0000000..c4d95de --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/TornadoSoundInstance.kt @@ -0,0 +1,41 @@ +package gg.norisk.heroes.aang.client.sound + +import gg.norisk.heroes.aang.entity.TornadoEntity +import gg.norisk.heroes.aang.registry.SoundRegistry +import net.minecraft.client.MinecraftClient +import net.minecraft.client.sound.MovingSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.sound.SoundCategory + +class TornadoSoundInstance(private val entity: TornadoEntity) : + MovingSoundInstance(SoundRegistry.FLYING, SoundCategory.NEUTRAL, SoundInstance.createRandom()) { + + init { + this.repeat = true + this.repeatDelay = 0 + this.volume = 0.3f + } + + override fun tick() { + if (!entity.isRemoved) { + + val clientPlayer = MinecraftClient.getInstance().player ?: return + if (entity.controllingPassenger?.id == clientPlayer.id) { + this.x = clientPlayer.x.toFloat().toDouble() + 20 + this.y = clientPlayer.y.toFloat().toDouble() + 19 + this.z = clientPlayer.z.toFloat().toDouble() + } else { + this.x = entity.x.toFloat().toDouble() + this.y = entity.y.toFloat().toDouble() + this.z = entity.z.toFloat().toDouble() + } + + + val f: Float = this.entity.scale / 5f + this.volume = f + this.pitch = 1f + this.volume + } else { + this.setDone() + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/VelocityBasedFlyingSoundInstance.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/VelocityBasedFlyingSoundInstance.kt new file mode 100644 index 0000000..712aac5 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/client/sound/VelocityBasedFlyingSoundInstance.kt @@ -0,0 +1,30 @@ +package gg.norisk.heroes.aang.client.sound + +import gg.norisk.heroes.aang.registry.SoundRegistry +import net.minecraft.client.sound.MovingSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.entity.Entity +import net.minecraft.sound.SoundCategory + +class VelocityBasedFlyingSoundInstance(private val entity: Entity, val condition: (Entity) -> Boolean) : + MovingSoundInstance(SoundRegistry.FLYING, SoundCategory.NEUTRAL, SoundInstance.createRandom()) { + + init { + this.repeat = true + this.repeatDelay = 0 + this.volume = 0.3f + } + + override fun tick() { + if (!entity.isRemoved && condition.invoke(entity)) { + this.x = entity.x.toFloat().toDouble() + this.y = entity.y.toFloat().toDouble() + this.z = entity.z.toFloat().toDouble() + val f: Float = Math.min(0.3f, Math.max(0.01f, this.entity.velocity.lengthSquared().toFloat())) + this.volume = f + this.pitch = 1f + this.volume + } else { + this.setDone() + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/AirScooterEntity.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/AirScooterEntity.kt new file mode 100644 index 0000000..ecca8ea --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/AirScooterEntity.kt @@ -0,0 +1,309 @@ +package gg.norisk.heroes.aang.entity + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.heroes.aang.ability.AirBallAbility.getAirBendingPos +import gg.norisk.heroes.aang.ability.AirBallAbility.isAirBending +import gg.norisk.heroes.aang.ability.AirScooterAbility +import gg.norisk.heroes.aang.ability.AirScooterAbility.isAirScooting +import gg.norisk.heroes.aang.ability.AirScooterAbility.stopRidingAirBall +import gg.norisk.heroes.common.utils.sound +import gg.norisk.utils.Easing +import gg.norisk.utils.OldAnimation +import net.minecraft.entity.* +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.damage.DamageSource +import net.minecraft.entity.damage.DamageTypes +import net.minecraft.entity.mob.MobEntity +import net.minecraft.entity.mob.PathAwareEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.fluid.FluidState +import net.minecraft.particle.ParticleTypes +import net.minecraft.registry.Registries +import net.minecraft.registry.tag.BlockTags +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.sound.SoundEvents +import net.minecraft.util.math.Box +import net.minecraft.util.math.MathHelper +import net.minecraft.util.math.Vec3d +import net.minecraft.world.World +import net.minecraft.world.explosion.AdvancedExplosionBehavior +import net.silkmc.silk.core.entity.modifyVelocity +import net.silkmc.silk.core.text.broadcastText +import java.util.* +import java.util.function.Function +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +class AirScooterEntity(entityType: EntityType, world: World) : + PathAwareEntity(entityType, world) { + val startScaleAnimation = OldAnimation(0f, 3f, 1.seconds.toJavaDuration(), Easing.EXPO_OUT) + var currentScale: Float = 0f + var wasBended = false + var isComingBack = false + val pickedUpEntities = mutableSetOf() + + var wasLaunched: Boolean + get() = this.getSyncedData("AirScooter:WasLaunched") ?: false + set(value) = this.setSyncedData("AirScooter:WasLaunched", value) + + enum class Type { + SCOOTER, PROJECTILE + } + + init { + this.ignoreCameraFrustum = true + this.getAttributeInstance(EntityAttributes.GENERIC_STEP_HEIGHT)?.baseValue = 2.0 + this.getAttributeInstance(EntityAttributes.GENERIC_SCALE)?.baseValue = 0.0 + this.getAttributeInstance(EntityAttributes.GENERIC_GRAVITY)?.baseValue = 0.02 + } + + // Apply player-controlled movement + override fun travel(pos: Vec3d) { + this.setNoDrag(true) + this.setNoGravity(wasLaunched) + super.travel(pos) + } + + override fun canWalkOnFluid(fluidState: FluidState): Boolean { + return true + } + + override fun shouldRenderName(): Boolean { + return false + } + + override fun onStartedTrackingBy(player: ServerPlayerEntity) { + super.onStartedTrackingBy(player) + if (bendingType == Type.SCOOTER) { + AirScooterAbility.airScooterSoundPacketS2C.send(id, player) + } + } + + fun getLerpedScale(f: Float): Float { + currentScale = MathHelper.lerp(f, currentScale, this.scale) + return currentScale + } + + override fun tick() { + super.tick() + when (bendingType) { + Type.SCOOTER -> handleAirScooterType() + Type.PROJECTILE -> { + handleProjectileType() + } + } + } + + private fun handleProjectileType() { + noClip = false + + if (!wasLaunched && !isBoomerang && !world.isClient) { + val owner = getOwner() + val targetPos = owner?.getAirBendingPos() + if (owner != null && targetPos != null) { + val direction = targetPos.subtract(this.pos).normalize() + val distance = targetPos.distanceTo(this.pos) + + // Je näher das Projektil am Ziel ist, desto kleiner wird der Multiplikationsfaktor + val speedMultiplier = distance + + modifyVelocity(direction.multiply(speedMultiplier)) + } + } + + val player = getOwner() + if (isBoomerang && player != null && !world.isClient) { + val distanceToPlayer = this.distanceTo(player) + pickUpNearbyItems() + + if (isComingBack) { + modifyVelocity(player.getAirBendingPos().subtract(this.pos).normalize().multiply(2.0)) + if (distanceToPlayer < 4) { + isBoomerang = false + } + } else if (distanceToPlayer > 25) { + isComingBack = true + } + } + + val distanceFlag = if (player != null) distanceTo(player) > 100 else false + + if (!world.isClient && wasLaunched) { + if (horizontalCollision || verticalCollision || player == null || distanceFlag) { + this.discard() + this.createExplosion(this.blockPos.toCenterPos()) + } + } + } + + private fun pickUpNearbyItems() { + val player = getOwner() ?: return + world.getOtherEntities(this, this.boundingBox.expand(2.0)) { it is ItemEntity || it is MobEntity }.forEach { + val direction = this.pos.subtract(it.pos) + if (it.distanceTo(player) < 8) { + it.modifyVelocity(direction.normalize().multiply(0.8)) + } else { + it.modifyVelocity(direction) + } + if (!pickedUpEntities.contains(it.uuid)) { + pickedUpEntities.add(it.uuid) + it.sound(SoundEvents.ENTITY_ITEM_PICKUP, 0.2f, Random.nextDouble(1.0, 2.0)) + } + } + } + + private fun handleAirScooterType() { + noClip = true + this.getAttributeInstance(EntityAttributes.GENERIC_SCALE)?.baseValue = startScaleAnimation.get().toDouble() + val owner = getOwner() + if (owner != null && !world.isClient) { + setPosition(owner.pos.add(0.0, 0.2, 0.0)) + } + if (getOwner()?.isAirScooting == false) { + this.discard() + } + } + + private fun getOwner(): PlayerEntity? { + val id = if (ownerId != -1) ownerId else return null + return world.getEntityById(id) as? PlayerEntity? + } + + override fun handleFallDamage(f: Float, g: Float, damageSource: DamageSource?): Boolean { + return false + } + + override fun damage(damageSource: DamageSource, f: Float): Boolean { + if (world.isClient) { + return false + } else if (this.isDead) { + return false + } + if (damageSource.isOf(DamageTypes.GENERIC_KILL)) { + return super.damage(damageSource, f) + } + val attacker = damageSource.attacker as? LivingEntity ?: return false + if (attacker.id == ownerId) { + if (bendingType == Type.SCOOTER) { + return false + } + if ((attacker as? PlayerEntity?)?.isAirBending == true) return false + wasLaunched = true + sound(SoundEvents.ENTITY_BREEZE_IDLE_AIR, 0.2f, pitch = 2f) + setVelocity(attacker, attacker.pitch, attacker.yaw, 0.0f, 2.5f, 1.0f) + return false + } else { + getOwner()?.stopRidingAirBall() + this.discard() + this.createExplosion(this.pos, 1f) + } + return false + } + + fun launchBoomerang() { + isBoomerang = true + sound(SoundEvents.ENTITY_BREEZE_IDLE_AIR, 0.2f, pitch = 2f) + val player = getOwner() ?: return + setVelocity(player, player.pitch, player.yaw, 0.0f, 1.5f, 1.0f) + } + + var isBoomerang: Boolean + get() = this.getSyncedData("AirBallIsBoomerang") ?: false + set(value) { + if (!value) { + isComingBack = false + pickedUpEntities.clear() + } + this.setSyncedData("AirBallIsBoomerang", value) + } + + var ownerId: Int + get() = this.getSyncedData("AirBallOwnerId") ?: -1 + set(value) { + this.setSyncedData("AirBallOwnerId", value) + } + + var bendingType: Type + get() = Type.valueOf(this.getSyncedData("BendingType") ?: Type.SCOOTER.name) + set(value) { + this.setSyncedData("BendingType", value.name) + } + + override fun calculateBoundingBox(): Box { + val f = getDimensions(EntityPose.STANDING).withEyeHeight(0f).width / 2.0f + val g = getDimensions(EntityPose.STANDING).withEyeHeight(0f).height + val h = 0.15f * scale + return Box( + pos.x - f.toDouble(), + pos.y - h, + pos.z - f.toDouble(), + pos.x + f.toDouble(), + pos.y - h + g.toDouble(), + pos.z + f.toDouble() + ) + } + + fun calculateVelocity(d: Double, e: Double, f: Double, g: Float, h: Float): Vec3d { + return Vec3d(d, e, f) + .normalize() + .add( + random.nextTriangular(0.0, 0.0172275 * h.toDouble()), + random.nextTriangular(0.0, 0.0172275 * h.toDouble()), + random.nextTriangular(0.0, 0.0172275 * h.toDouble()) + ) + .multiply(g.toDouble()) + } + + fun setVelocity(entity: Entity, f: Float, g: Float, h: Float, i: Float, j: Float) { + val k = -MathHelper.sin(g * (Math.PI / 180.0).toFloat()) * MathHelper.cos(f * (Math.PI / 180.0).toFloat()) + val l = -MathHelper.sin((f + h) * (Math.PI / 180.0).toFloat()) + val m = MathHelper.cos(g * (Math.PI / 180.0).toFloat()) * MathHelper.cos(f * (Math.PI / 180.0).toFloat()) + this.setVelocity(k.toDouble(), l.toDouble(), m.toDouble(), i, j) + val vec3d = entity.movement + this.velocity = velocity.add(vec3d.x, if (entity.isOnGround) 0.0 else vec3d.y, vec3d.z) + } + + fun setVelocity(d: Double, e: Double, f: Double, g: Float, h: Float) { + val vec3d: Vec3d = this.calculateVelocity(d, e, f, g, h) + this.velocity = vec3d + this.velocityDirty = true + val i = vec3d.horizontalLength() + this.yaw = (MathHelper.atan2(vec3d.x, vec3d.z) * 180.0f / Math.PI.toFloat()).toFloat() + this.pitch = (MathHelper.atan2(vec3d.y, i) * 180.0f / Math.PI.toFloat()).toFloat() + this.prevYaw = this.yaw + this.prevPitch = this.pitch + } + + private fun createExplosion(vec3d: Vec3d, power: Float = 1.2f * scale) { + world + .createExplosion( + this, + null, + AdvancedExplosionBehavior( + true, + false, + Optional.of(power), + Registries.BLOCK.getEntryList(BlockTags.BLOCKS_WIND_CHARGE_EXPLOSIONS).map(Function.identity()) + ), + vec3d.getX(), + vec3d.getY(), + vec3d.getZ(), + power, + false, + World.ExplosionSourceType.TRIGGER, + ParticleTypes.GUST_EMITTER_SMALL, + ParticleTypes.GUST_EMITTER_LARGE, + SoundEvents.ENTITY_WIND_CHARGE_WIND_BURST + ) + } + + override fun isCollidable(): Boolean { + return false + } + + override fun collidesWith(entity: Entity?): Boolean { + return false + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/DummyPlayer.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/DummyPlayer.kt new file mode 100644 index 0000000..59e6ecd --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/DummyPlayer.kt @@ -0,0 +1,86 @@ +package gg.norisk.heroes.aang.entity + +import com.mojang.authlib.GameProfile +import gg.norisk.emote.network.EmoteNetworking.emoteS2CPacket +import gg.norisk.emote.network.EmoteSync +import gg.norisk.heroes.aang.ability.SpiritualProjectionAbility.cancelProjection +import net.minecraft.client.network.OtherClientPlayerEntity +import net.minecraft.client.world.ClientWorld +import net.minecraft.entity.Entity +import net.minecraft.entity.damage.DamageSource +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.network.packet.s2c.play.EntitySpawnS2CPacket +import net.minecraft.util.ActionResult +import net.minecraft.util.Hand +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Vec3d +import net.minecraft.world.World +import org.apache.commons.lang3.RandomStringUtils +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable +import org.spongepowered.asm.mixin.injection.invoke.arg.Args +import java.util.* + +class DummyPlayer( + world: World, + blockPos: BlockPos, + f: Float, + gameProfile: GameProfile +) : PlayerEntity( + world, + blockPos, + f, + gameProfile +) { + override fun isSpectator(): Boolean = false + override fun isCreative(): Boolean = false + + override fun tick() { + super.tick() + } + + override fun handleAttack(entity: Entity): Boolean { + cancelProjection(entity) + return super.handleAttack(entity) + } + + override fun interactAt(playerEntity: PlayerEntity, vec3d: Vec3d, hand: Hand): ActionResult { + if (hand == Hand.MAIN_HAND) { + cancelProjection(playerEntity) + } + return super.interactAt(playerEntity, vec3d, hand) + } + + override fun onDeath(damageSource: DamageSource) { + cancelProjection(damageSource.attacker) + super.onDeath(damageSource) + } + + fun playEmote(emote: Identifier) { + emoteS2CPacket.sendToAll(EmoteSync(this.id, emote, EmoteSync.State.PLAY)) + } + + fun stopEmote(emote: Identifier) { + emoteS2CPacket.sendToAll(EmoteSync(this.id, emote, EmoteSync.State.STOP)) + } + + companion object { + fun UUID.isFakeUUID(): Boolean { + return this.toString().startsWith("00000000-0000-0000") + } + + fun handleDummyPlayerSpawn( + packet: EntitySpawnS2CPacket, + callback: CallbackInfoReturnable, + world: ClientWorld + ) { + if (packet.uuid.isFakeUUID()) { + val player = OtherClientPlayerEntity( + world, + GameProfile(packet.uuid, RandomStringUtils.randomAlphabetic(16)) + ) + callback.setReturnValue(player) + } + } + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/IAangPlayer.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/IAangPlayer.kt new file mode 100644 index 0000000..3df5dd1 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/IAangPlayer.kt @@ -0,0 +1,17 @@ +package gg.norisk.heroes.aang.entity + +import gg.norisk.heroes.aang.utils.EntitySpinTracker +import gg.norisk.heroes.aang.utils.PlayerRotationTracker +import kotlinx.coroutines.Job +import net.minecraft.entity.player.PlayerEntity + +interface IAangPlayer { + var rotationTracker: PlayerRotationTracker? + val aang_airScooterTasks: MutableList + val aang_spiritualProjectionsTasks: MutableList + val aang_tornadoTasks: MutableList + var aang_tornadoEntity: TornadoEntity? + var aang_airBallSpinTracker: EntitySpinTracker +} + +val PlayerEntity.aang get() = this as IAangPlayer diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/TornadoEntity.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/TornadoEntity.kt new file mode 100644 index 0000000..5da7513 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/entity/TornadoEntity.kt @@ -0,0 +1,199 @@ +package gg.norisk.heroes.aang.entity + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.heroes.aang.ability.TornadoAbility +import gg.norisk.heroes.aang.ability.TornadoAbility.isTornadoMode +import gg.norisk.heroes.aang.registry.EntityRegistry +import gg.norisk.heroes.aang.utils.PlayerRotationTracker +import gg.norisk.heroes.common.utils.SphereUtils +import gg.norisk.heroes.common.utils.sound +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType +import net.minecraft.entity.FallingBlockEntity +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.mob.PathAwareEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.fluid.FluidState +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundEvents +import net.minecraft.util.math.Direction +import net.minecraft.util.math.MathHelper +import net.minecraft.util.math.Vec3d +import net.minecraft.world.World +import net.silkmc.silk.core.entity.modifyVelocity +import kotlin.math.absoluteValue +import kotlin.math.min +import kotlin.math.sign + +class TornadoEntity(entityType: EntityType, world: World) : + PathAwareEntity(entityType, world) { + var currentScale: Float = 0f + var rotationTracker: PlayerRotationTracker? = PlayerRotationTracker() + + init { + this.getAttributeInstance(EntityAttributes.GENERIC_STEP_HEIGHT)?.baseValue = 2.5 + } + + fun getLerpedScale(f: Float): Float { + currentScale = MathHelper.lerp(f * 0.05f, currentScale, this.scale) + return currentScale + } + + // Apply player-controlled movement + override fun travel(pos: Vec3d) { + if (!this.isAlive) return + if (this.hasPassengers()) { + val passenger = controllingPassenger ?: return super.travel(pos) + this.prevYaw = yaw + this.prevPitch = pitch + + yaw = passenger.yaw + pitch = passenger.pitch * 0.5f + setRotation(yaw, pitch) + + this.bodyYaw = this.yaw + this.headYaw = this.bodyYaw + val x = passenger.sidewaysSpeed * 0.5f + val z = 0.6f + + this.movementSpeed = 0.3f + super.travel(Vec3d(x.toDouble(), pos.y, z.toDouble())) + } else { + super.travel(pos) + } + } + + fun disappear(entity: Entity?) { + val player = entity as? PlayerEntity? + if (player?.isTornadoMode == true) { + TornadoAbility.Ability.addCooldown(player) + player.isTornadoMode = false + player.sound(SoundEvents.ENTITY_BREEZE_IDLE_AIR, 0.4f, 0.7f) + } + this.discard() + } + + override fun removePassenger(entity: Entity?) { + super.removePassenger(entity) + disappear(entity) + } + + override fun getControllingPassenger(): LivingEntity? { + return if (firstPassenger?.id == ownerId) { + firstPassenger as? LivingEntity? + } else { + null + } + } + + override fun onStartedTrackingBy(player: ServerPlayerEntity) { + super.onStartedTrackingBy(player) + TornadoAbility.tornadoSoundPacketS2C.send(id, player) + } + + override fun tick() { + super.tick() + rotationTracker?.update(world.getEntityById(ownerId) as? PlayerEntity?) + if (!world.isClient) { + serverTick() + } + } + + override fun tickMovement() { + super.tickMovement() + if (this.world.isClient) return + + val radius = this.scale.toDouble() + val windStrength = 0.1 * this.scale + + for (entity in this.world.getOtherEntities(this, this.boundingBox.expand(radius)) { + !it.isSpectator && it !is PlayerEntity || (it is PlayerEntity && !it.isCreative) + }) { + if (entity.id == ownerId) continue + if (entity.type == EntityRegistry.TORNADO) continue + // Berechnung des Fortschritts nach oben (Höhe) + val maxY = this.eyeY + + // Tornado-Zentrum auf der Höhe des Entitys + val effectiveCentre = pos.add(0.0, entity.y - pos.y, 0.0) + + // Entfernung vom Tornado-Zentrum + val distFromCentre = entity.pos.distanceTo(effectiveCentre) + + // Windstärke basierend auf Entfernung vom Zentrum + val strength = windStrength / distFromCentre + + // Berechnung der Einwärtsbewegung + val inwardStrength = min((0.01 + world.random.nextDouble() * 0.5) / radius, strength) + + // Richtung des Tornado-Sogs zur Mitte + val inwardXDir = pos.x - entity.x + val inwardZDir = pos.z - entity.z + val inwardPullX = sign(inwardXDir) * inwardXDir.absoluteValue.coerceAtLeast(radius.toDouble()) + val inwardPullZ = sign(inwardZDir) * inwardZDir.absoluteValue.coerceAtLeast(radius.toDouble()) + + // Spiralförmige Bewegung nach oben und Rotation um das Zentrum + val spiralMovement = effectiveCentre.subtract(entity.pos) + .normalize() + .crossProduct(Vec3d(0.0, 1.0, 0.0)) // Rotation um die Y-Achse + .multiply(strength) + .add( + Vec3d( + inwardPullX * inwardStrength, + strength * world.random.nextDouble(), + inwardPullZ * inwardStrength + ) + ) + + // Setze die neue Bewegung des Entities + entity.modifyVelocity(spiralMovement.x, spiralMovement.y, spiralMovement.z) + + // Wenn das Entity die maximale Höhe erreicht hat, fliegt es davon + if (entity.y >= maxY) { + val flyAwayDirection = Vec3d( + world.random.nextDouble() - 0.5, + world.random.nextDouble() * 0.5, + world.random.nextDouble() - 0.5 + ).normalize().multiply(1.3) // Geschwindigkeit des Wegfliegens + entity.modifyVelocity(flyAwayDirection) + } + } + + SphereUtils.generateSphere(this.blockPos, 3 + radius.toInt(), false).filter { pos -> + val blockState = world.getBlockState(pos) + if (blockState.isAir) return@filter false + if (Direction.values().any { world.getBlockState(pos.offset(it)).isAir }) { + return@filter true + } + return@filter false + }.shuffled().take(1).forEach { + val spawnFromBlock = FallingBlockEntity.spawnFromBlock(world, it, world.getBlockState(it)) + spawnFromBlock.modifyVelocity(0.0, .2, .0) + spawnFromBlock.dropItem = false + } + } + + override fun canWalkOnFluid(fluidState: FluidState): Boolean { + return true + } + + private fun serverTick() { + val owner = (world as ServerWorld).getEntityById(ownerId) as? PlayerEntity? ?: return + val rotationTracker = this.rotationTracker + if (rotationTracker != null) { + this.getAttributeInstance(EntityAttributes.GENERIC_SCALE)?.baseValue = + rotationTracker.getPercentageBetween(1f, 10f).toDouble() + } + } + + var ownerId: Int + get() = this.getSyncedData("TornadoOwnerId") ?: -1 + set(value) = this.setSyncedData("TornadoOwnerId", value) + + var isGrowingMode: Boolean + get() = this.getSyncedData("TornadoIsGrowingMode") ?: false + set(value) = this.setSyncedData("TornadoIsGrowingMode", value) +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/EmoteRegistry.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/EmoteRegistry.kt new file mode 100644 index 0000000..9ed0e73 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/EmoteRegistry.kt @@ -0,0 +1,19 @@ +package gg.norisk.heroes.aang.registry + +import gg.norisk.heroes.aang.AangManager.toId +import net.minecraft.util.Identifier + +object EmoteRegistry { + val AIR_SCOOTER = "air_scooter_2".toEmote() + val AIR_SCOOTER_SITTING = "air_scooter_sitting".toEmote() + val SPIRITUAL_PROJECTION_START = "spiritual_projection_start".toEmote() + val SPIRITUAL_PROJECTION_LOOP = "spiritual_projection_loop".toEmote() + val LEVITATION = "levitation".toEmote() + val AIR_BENDING = "air_bending".toEmote() + + fun init() {} + + fun String.toEmote(): Identifier { + return "emotes/$this.animation.json".toId() + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/EntityRegistry.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/EntityRegistry.kt new file mode 100644 index 0000000..98dc7c9 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/EntityRegistry.kt @@ -0,0 +1,66 @@ +package gg.norisk.heroes.aang.registry + +import gg.norisk.heroes.aang.AangManager.toId +import gg.norisk.heroes.aang.entity.AirScooterEntity +import gg.norisk.heroes.aang.entity.TornadoEntity +import gg.norisk.heroes.common.HeroesManager +import net.fabricmc.fabric.api.`object`.builder.v1.entity.FabricDefaultAttributeRegistry +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityDimensions +import net.minecraft.entity.EntityType +import net.minecraft.entity.SpawnGroup +import net.minecraft.entity.attribute.DefaultAttributeContainer +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.mob.PathAwareEntity +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry + +object EntityRegistry { + val AIR_SCOOTER = Registry.register( + Registries.ENTITY_TYPE, + "air_scooter".toId(), + EntityType.Builder.create(::AirScooterEntity, SpawnGroup.MISC) + .requires(HeroesManager.heroesFlag) + .dimensions(0.3125f, 0.3125f) + .build(null) + ) + val TORNADO = Registry.register( + Registries.ENTITY_TYPE, + "tornado".toId(), + EntityType.Builder.create(::TornadoEntity, SpawnGroup.MISC) + .requires(HeroesManager.heroesFlag) + .dimensions(0.6f, 1f) + .build(null) + ) + + fun init() { + registerEntityAttributes() + } + + private fun registerEntityAttributes() { + FabricDefaultAttributeRegistry.register(AIR_SCOOTER, createGenericEntityAttributes()) + FabricDefaultAttributeRegistry.register(TORNADO, createGenericEntityAttributes()) + } + + fun createGenericEntityAttributes(): DefaultAttributeContainer.Builder { + return PathAwareEntity.createLivingAttributes() + .add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.80000000298023224) + .add(EntityAttributes.GENERIC_FOLLOW_RANGE, 16.0).add(EntityAttributes.GENERIC_MAX_HEALTH, 10.0) + .add(EntityAttributes.GENERIC_ATTACK_DAMAGE, 5.0) + .add(EntityAttributes.GENERIC_ATTACK_KNOCKBACK, 0.1) + } + + private fun register( + name: String, entity: EntityType.EntityFactory, + width: Float, height: Float + ): EntityType { + val dimension = EntityDimensions.changing(width, height).withEyeHeight(0f) + val builder = EntityType.Builder.create(entity, SpawnGroup.CREATURE) + return Registry.register( + Registries.ENTITY_TYPE, + name.toId(), + builder.eyeHeight(0f).dimensions(dimension.width, dimension.height).requires(HeroesManager.heroesFlag) + .build(null) + ) + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/EntityRendererRegistry.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/EntityRendererRegistry.kt new file mode 100644 index 0000000..163b3f9 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/EntityRendererRegistry.kt @@ -0,0 +1,23 @@ +package gg.norisk.heroes.aang.registry + +import gg.norisk.heroes.aang.AangManager.toId +import gg.norisk.heroes.aang.client.render.entity.AirScooterEntityRenderer +import gg.norisk.heroes.aang.client.render.entity.TornadoEntityRenderer +import gg.norisk.heroes.aang.client.render.entity.model.AirScooterEntityModel +import net.fabricmc.fabric.api.client.rendering.v1.EntityModelLayerRegistry +import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry +import net.minecraft.client.render.entity.model.EntityModelLayer + + +object EntityRendererRegistry { + val AIR_SCOOTER_LAYER: EntityModelLayer = EntityModelLayer("air_scooter".toId(), "main") + + fun init() { + EntityRendererRegistry.register(EntityRegistry.AIR_SCOOTER, ::AirScooterEntityRenderer) + EntityRendererRegistry.register(EntityRegistry.TORNADO, ::TornadoEntityRenderer) + EntityModelLayerRegistry.registerModelLayer( + AIR_SCOOTER_LAYER, + AirScooterEntityModel.Companion::getTexturedModelData + ); + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/ParticleRegistry.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/ParticleRegistry.kt new file mode 100644 index 0000000..8d66ddd --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/ParticleRegistry.kt @@ -0,0 +1,21 @@ +package gg.norisk.heroes.aang.registry + +import gg.norisk.heroes.aang.AangManager.toId +import net.fabricmc.fabric.api.particle.v1.FabricParticleTypes +import net.minecraft.particle.ParticleEffect +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry + +object ParticleRegistry { + val AIR_SCOOTER_DUST = register("air_scooter_dust") + val BENDING_AIR = register("bending_air") + + fun init() { + } + + private fun register( + name: String + ): ParticleEffect { + return Registry.register(Registries.PARTICLE_TYPE, name.toId(), FabricParticleTypes.simple()) + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/ParticleRendererRegistry.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/ParticleRendererRegistry.kt new file mode 100644 index 0000000..182d346 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/ParticleRendererRegistry.kt @@ -0,0 +1,14 @@ +package gg.norisk.heroes.aang.registry + +import gg.norisk.heroes.aang.client.particle.AirScooterDustParticle +import gg.norisk.heroes.aang.client.particle.BendingAirParticle +import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry +import net.minecraft.particle.ParticleEffect +import net.minecraft.particle.ParticleType + +object ParticleRendererRegistry { + fun init() { + ParticleFactoryRegistry.getInstance().register(ParticleRegistry.AIR_SCOOTER_DUST as ParticleType, AirScooterDustParticle::Factory) + ParticleFactoryRegistry.getInstance().register(ParticleRegistry.BENDING_AIR as ParticleType, BendingAirParticle::Factory) + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/SoundRegistry.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/SoundRegistry.kt new file mode 100644 index 0000000..cf28a38 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/registry/SoundRegistry.kt @@ -0,0 +1,15 @@ +package gg.norisk.heroes.aang.registry + +import gg.norisk.heroes.aang.AangManager.toId +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry +import net.minecraft.sound.SoundEvent + +object SoundRegistry { + var FLYING = "flying".register() + + fun init() { + } + + private fun String.register() = Registry.register(Registries.SOUND_EVENT, this.toId(), SoundEvent.of(this.toId())) +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/utils/CircleDetector3D.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/utils/CircleDetector3D.kt new file mode 100644 index 0000000..96851d7 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/utils/CircleDetector3D.kt @@ -0,0 +1,76 @@ +package gg.norisk.heroes.aang.utils + +import net.minecraft.util.math.Vec3d +import kotlin.math.abs +import kotlin.math.sqrt + +class CircleDetector3D { + + // Liste zur Speicherung der Mausbewegungen im 3D-Raum + private val mouseMovements = mutableSetOf() + + // Methode, um Mausbewegungen aufzuzeichnen + fun addMouseMovement(x: Double, y: Double, z: Double): Boolean { + return mouseMovements.add(Vec3d(x, y, z)) + } + + // Methode zur Berechnung des Kreisähnlichkeitsprozentsatzes im 3D-Raum + fun calculateCircleAccuracy(): Double { + if (mouseMovements.size < 3) return 0.0 // Nicht genug Punkte, um einen Kreis zu erkennen + + // Berechnung des Mittelpunkts (Schwerpunkt der Bewegung) + val center = calculateCenter(mouseMovements) + + // Berechnung des durchschnittlichen Radius + val averageRadius = calculateAverageRadius(mouseMovements, center) + + // Berechnung des Fehlerwerts für jeden Punkt + var totalError = 0.0 + for (point in mouseMovements) { + val distanceToCenter = calculateDistance(center, point) + totalError += abs(distanceToCenter - averageRadius) + } + + // Normierung des Fehlers und Umwandlung in Prozent (100% bedeutet perfekter Kreis) + val maxError = averageRadius * mouseMovements.size + val accuracy = 100.0f - (totalError / maxError * 100.0f) + + return accuracy.coerceIn(0.0, 100.0) // Beschränkung auf 0-100% + } + + // Hilfsmethode zur Berechnung des Mittelpunkts im 3D-Raum + private fun calculateCenter(points: Collection): Vec3d { + var sumX = 0.0 + var sumY = 0.0 + var sumZ = 0.0 + + for (point in points) { + sumX += point.x + sumY += point.y + sumZ += point.z + } + + val centerX = sumX / points.size + val centerY = sumY / points.size + val centerZ = sumZ / points.size + + return Vec3d(centerX, centerY, centerZ) + } + + // Hilfsmethode zur Berechnung des durchschnittlichen Radius + private fun calculateAverageRadius(points: Collection, center: Vec3d): Double { + var totalRadius = 0.0 + for (point in points) { + totalRadius += calculateDistance(center, point) + } + return totalRadius / points.size + } + + // Hilfsmethode zur Berechnung der Distanz zwischen zwei Punkten im 3D-Raum + private fun calculateDistance(p1: Vec3d, p2: Vec3d): Double { + val dx = p1.x - p2.x + val dy = p1.y - p2.y + val dz = p1.z - p2.z + return sqrt(dx * dx + dy * dy + dz * dz) + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/utils/EntitySpinTracker.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/utils/EntitySpinTracker.kt new file mode 100644 index 0000000..765eff6 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/utils/EntitySpinTracker.kt @@ -0,0 +1,84 @@ +package gg.norisk.heroes.aang.utils + +import net.minecraft.entity.Entity +import java.util.* +import kotlin.math.abs + +class EntitySpinTracker { + private val yawHistory: Deque = ArrayDeque() + private val maxHistorySize = 60 // Anzahl der Ticks, die wir überwachen (z. B. 1 Sekunde bei 20 Ticks pro Sekunde) + private val spinThreshold = 360.0f // Mindestens 720° Änderung für einen "wilden Spin" (z. B. 2 volle Umdrehungen) + + fun update(entity: Entity) { + // Aktuelle Yaw-Rotation der Entity holen + val currentYaw = normalizeYaw(entity.yaw) + + // Letzten Wert speichern + if (yawHistory.size >= maxHistorySize) { + yawHistory.pollFirst() + } + yawHistory.addLast(currentYaw) + + // Optional: Debug-Log für Rotation + } + + fun clear() { + yawHistory.clear() + } + + fun getSpinProgress(): Float { + if (yawHistory.size < 2) { + return 0.0f // Nicht genug Daten + } + + var totalChange = 0.0f + var previousYaw: Float? = null + + for (yaw in yawHistory) { + if (previousYaw != null) { + val delta = calculateYawDifference(previousYaw, yaw) + totalChange += delta + } + previousYaw = yaw + } + + // Berechne den Fortschritt als Prozentsatz + return (totalChange / spinThreshold).coerceAtMost(1.0f) * 100.0f + } + + fun hasSpunWildly(): Boolean { + if (yawHistory.size < 2) { + return false // Nicht genug Daten + } + + var totalChange = 0.0f + var previousYaw: Float? = null + + for (yaw in yawHistory) { + if (previousYaw != null) { + val delta = calculateYawDifference(previousYaw, yaw) + totalChange += delta + } + previousYaw = yaw + } + + // Wenn die gesamte Änderung den Schwellenwert überschreitet + return totalChange >= spinThreshold + } + + private fun calculateYawDifference(previous: Float, current: Float): Float { + var diff = current - previous + while (diff < -180.0f) { + diff += 360.0f + } + while (diff > 180.0f) { + diff -= 360.0f + } + return abs(diff.toDouble()).toFloat() + } + + private fun normalizeYaw(yaw: Float): Float { + // Yaw auf den Bereich [0, 360) normalisieren + return (yaw % 360.0f + 360.0f) % 360.0f + } +} diff --git a/aang/src/main/kotlin/gg/norisk/heroes/aang/utils/PlayerRotationTracker.kt b/aang/src/main/kotlin/gg/norisk/heroes/aang/utils/PlayerRotationTracker.kt new file mode 100644 index 0000000..c5fbb95 --- /dev/null +++ b/aang/src/main/kotlin/gg/norisk/heroes/aang/utils/PlayerRotationTracker.kt @@ -0,0 +1,61 @@ +package gg.norisk.heroes.aang.utils + +import net.minecraft.entity.player.PlayerEntity + +class PlayerRotationTracker { + var lastYaw: Float? = null // Zu Beginn noch null, um den ersten Tick zu vermeiden + var lastPitch: Float? = null + var movementScale = 0f + var maxMovementScale = 100f // Maximale Skala, auf die hochgezählt werden kann + var movementDecayRate = 0.1f // Geschwindigkeit des Decays, wenn der Spieler sich wenig bewegt + var movementIncreaseRate = 0.005f // Geschwindigkeit, mit der die Skala ansteigt bei Bewegung + var movementThreshold = 5f // Minimale Änderung in Yaw oder Pitch, um als "Bewegung" zu gelten + var onlyDecay = false + + // Diese Methode sollte pro Frame oder Tick aufgerufen werden + fun update(player: PlayerEntity?) { + if (player == null) return + // Aktuelle Yaw- und Pitch-Werte des Spielers + val currentYaw = player.yaw + val currentPitch = player.pitch + + // Wenn dies das erste Mal ist, dass die Methode aufgerufen wird, setze die initialen Werte + if (lastYaw == null || lastPitch == null) { + lastYaw = currentYaw + lastPitch = currentPitch + return // Beim ersten Durchlauf kein Update der Skala + } + + // Berechne die Änderungen in Yaw und Pitch + val deltaYaw = Math.abs(currentYaw - lastYaw!!) + val deltaPitch = Math.abs(currentPitch - lastPitch!!) + + // Überprüfe, ob die Änderung größer als der Schwellwert ist + if ((deltaYaw > movementThreshold || deltaPitch > movementThreshold) && !onlyDecay) { + // Erhöhe die Skala basierend auf der Bewegungsmenge + movementScale += (deltaYaw + deltaPitch) * movementIncreaseRate + if (movementScale > maxMovementScale) { + movementScale = maxMovementScale + } + } else { + // Verringere die Skala langsam, wenn keine signifikante Bewegung stattfindet + movementScale -= movementDecayRate + if (movementScale < 0) { + movementScale = 0f + } + } + + // Speichere die aktuellen Werte für den nächsten Tick + lastYaw = currentYaw + lastPitch = currentPitch + } + + // Neue Methode: Prozentsatz zwischen zwei Werten basierend auf der movementScale berechnen + fun getPercentageBetween(minValue: Float, maxValue: Float): Float { + // Normalisiere die movementScale zwischen 0 und 1 + val normalizedScale = movementScale / maxMovementScale + + // Berechne den interpolierten Wert zwischen minValue und maxValue + return minValue + (maxValue - minValue) * normalizedScale + } +} diff --git a/aang/src/main/resources/aang.accesswidener b/aang/src/main/resources/aang.accesswidener new file mode 100644 index 0000000..d4342d0 --- /dev/null +++ b/aang/src/main/resources/aang.accesswidener @@ -0,0 +1 @@ +accessWidener v2 named diff --git a/aang/src/main/resources/aang.mixins.json b/aang/src/main/resources/aang.mixins.json new file mode 100644 index 0000000..03df14e --- /dev/null +++ b/aang/src/main/resources/aang.mixins.json @@ -0,0 +1,25 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "gg.norisk.heroes.aang.mixin", + "compatibilityLevel": "JAVA_17", + "injectors": { + "defaultRequire": 1 + }, + "mixins": [ + "EntityMixin", + "LivingEntityMixin", + "PlayerEntityMixin" + ], + "client": [ + "accessor.CameraAccessor", + "client.AbstractClientPlayerEntityMixin", + "client.CameraMixin", + "client.ClientPlayNetworkHandlerMixin", + "client.GameOptionsMixin", + "client.GameRendererMixin", + "client.HeldItemRendererMixin", + "client.LivingEntityRendererMixin", + "client.PlayerEntityRendererMixin" + ] +} diff --git a/aang/src/main/resources/assets/aang/aang.png b/aang/src/main/resources/assets/aang/aang.png new file mode 100644 index 0000000..fafb421 Binary files /dev/null and b/aang/src/main/resources/assets/aang/aang.png differ diff --git a/aang/src/main/resources/assets/aang/aang_overlay.png b/aang/src/main/resources/assets/aang/aang_overlay.png new file mode 100644 index 0000000..6eb4484 Binary files /dev/null and b/aang/src/main/resources/assets/aang/aang_overlay.png differ diff --git a/aang/src/main/resources/assets/aang/bedrock/particles/bending_air_0.particle.json b/aang/src/main/resources/assets/aang/bedrock/particles/bending_air_0.particle.json new file mode 100644 index 0000000..67fbb4f --- /dev/null +++ b/aang/src/main/resources/assets/aang/bedrock/particles/bending_air_0.particle.json @@ -0,0 +1,54 @@ +{ + "format_version": "1.10.0", + "particle_effect": { + "description": { + "identifier": "emote-lib:bending_air_0", + "basic_render_parameters": { + "material": "particles_alpha", + "texture": "aang:graffiti" + } + }, + "components": { + "minecraft:emitter_rate_steady": { + "spawn_rate": 16, + "max_particles": 100 + }, + "minecraft:emitter_lifetime_looping": { + "active_time": 1 + }, + "minecraft:emitter_shape_sphere": { + "radius": 2, + "surface_only": true, + "direction": "inwards" + }, + "minecraft:particle_lifetime_expression": { + "max_lifetime": 0.4 + }, + "minecraft:particle_initial_speed": 16.5, + "minecraft:particle_motion_dynamic": { + "linear_acceleration": [0, 1, 0], + "linear_drag_coefficient": 4 + }, + "minecraft:particle_appearance_billboard": { + "size": [0.5, 0.5], + "facing_camera_mode": "rotate_xyz", + "uv": { + "texture_width": 16, + "texture_height": 128, + "flipbook": { + "base_UV": [0, 0], + "size_UV": [16, 16], + "step_UV": [0, 16], + "frames_per_second": 12, + "max_frame": 8, + "stretch_to_lifetime": true + } + } + }, + "minecraft:particle_motion_collision": { + "collision_drag": 0.4, + "collision_radius": 0.2 + } + } + } +} diff --git a/aang/src/main/resources/assets/aang/bedrock/particles/bending_air_1.particle.json b/aang/src/main/resources/assets/aang/bedrock/particles/bending_air_1.particle.json new file mode 100644 index 0000000..b4162cc --- /dev/null +++ b/aang/src/main/resources/assets/aang/bedrock/particles/bending_air_1.particle.json @@ -0,0 +1,54 @@ + + +{ + "format_version": "1.10.0", + "particle_effect": { + "description": { + "identifier": "emote-lib:bending_air_1", + "basic_render_parameters": { + "material": "particles_alpha", + "texture": "aang:graffiti" + } + }, + "components": { + "minecraft:emitter_rate_instant": { + "num_particles": 3 + }, + "minecraft:emitter_lifetime_once": { + "active_time": 1 + }, + "minecraft:emitter_shape_sphere": { + "radius": 0.5, + "surface_only": true, + "direction": "inwards" + }, + "minecraft:particle_lifetime_expression": { + "max_lifetime": 1 + }, + "minecraft:particle_initial_speed": 1, + "minecraft:particle_motion_dynamic": { + "linear_drag_coefficient": 4 + }, + "minecraft:particle_appearance_billboard": { + "size": [0.5, 0.5], + "facing_camera_mode": "rotate_xyz", + "uv": { + "texture_width": 16, + "texture_height": 128, + "flipbook": { + "base_UV": [0, 0], + "size_UV": [16, 16], + "step_UV": [0, 16], + "frames_per_second": 12, + "max_frame": 8, + "stretch_to_lifetime": true + } + } + }, + "minecraft:particle_motion_collision": { + "collision_drag": 0.4, + "collision_radius": 0.2 + } + } + } +} diff --git a/aang/src/main/resources/assets/aang/emotes/air_bending.animation.json b/aang/src/main/resources/assets/aang/emotes/air_bending.animation.json new file mode 100644 index 0000000..c20b7ca --- /dev/null +++ b/aang/src/main/resources/assets/aang/emotes/air_bending.animation.json @@ -0,0 +1,34 @@ +{ + "format_version": "1.8.0", + "animations": { + "air_bending": { + "loop": "hold_on_last_frame", + "animation_length": 0.72, + "bones": { + "bipedRightArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.72": { + "vector": [-69.48874, 39.09545, 11.28589], + "easing": "easeOutQuint" + } + } + }, + "bipedLeftArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.72": { + "vector": [68.5343, -42.00279, -163.26947], + "easing": "easeOutQuint" + } + } + } + } + } + }, + "geckolib_format_version": 2 +} \ No newline at end of file diff --git a/aang/src/main/resources/assets/aang/emotes/air_scooter_2.animation.json b/aang/src/main/resources/assets/aang/emotes/air_scooter_2.animation.json new file mode 100644 index 0000000..1560717 --- /dev/null +++ b/aang/src/main/resources/assets/aang/emotes/air_scooter_2.animation.json @@ -0,0 +1,137 @@ +{ + "format_version": "1.8.0", + "animations": { + "air_scooter": { + "animation_length": 0.84, + "bones": { + "bipedRig": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.8": { + "vector": [0, 360, 0], + "easing": "easeInExpo" + }, + "0.84": { + "vector": [0, 0, 0] + } + } + }, + "bipedRightArm": { + "rotation": { + "0.0": { + "vector": [-78.99034, 24.59477, 4.62934] + }, + "0.28": { + "vector": [73.84641, 51.3798, 167.24859] + }, + "0.56": { + "vector": [31.34641, 51.3798, 167.24859] + }, + "0.8": { + "vector": [-81.56011, -42.11662, 4.58533] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.56": { + "vector": [0, 3, 0] + }, + "0.8": { + "vector": [0, -1, -3] + } + } + }, + "bipedLeftArm": { + "rotation": { + "0.0": { + "vector": [86.44785, -33.16347, -168.10969] + }, + "0.28": { + "vector": [-82.49399, -66.60691, 3.05019] + }, + "0.56": { + "vector": [-42.49399, -66.60691, 3.05019] + }, + "0.8": { + "vector": [-72.50228, 13.17432, 52.2119] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.56": { + "vector": [0, 0, -2] + }, + "0.8": { + "vector": [-1, 0, -2] + } + } + } + }, + "particle_effects": { + "0.0": { + "effect": "bending_air_1", + "locator": "airlocator" + }, + "0.08": { + "effect": "bending_air_1", + "locator": "airlocatorrightarm" + }, + "0.12": { + "effect": "bending_air_1", + "locator": "airlocator" + }, + "0.16": { + "effect": "bending_air_1", + "locator": "airlocatorrightarm" + }, + "0.2": { + "effect": "bending_air_1", + "locator": "airlocator" + }, + "0.28": { + "effect": "bending_air_1", + "locator": "airlocatorrightarm" + }, + "0.32": { + "effect": "bending_air_1", + "locator": "airlocator" + }, + "0.44": { + "effect": "bending_air_1", + "locator": "airlocatorrightarm" + }, + "0.48": { + "effect": "bending_air_1", + "locator": "airlocator" + }, + "0.56": { + "effect": "bending_air_1", + "locator": "airlocatorrightarm" + }, + "0.6": { + "effect": "bending_air_1", + "locator": "airlocator" + }, + "0.68": { + "effect": "bending_air_1", + "locator": "airlocatorrightarm" + }, + "0.72": { + "effect": "bending_air_1", + "locator": "airlocator" + }, + "0.8": { + "effect": "bending_air_1", + "locator": "airlocatorrightarm" + } + } + } + }, + "geckolib_format_version": 2 +} diff --git a/aang/src/main/resources/assets/aang/emotes/air_scooter_2.geo.json b/aang/src/main/resources/assets/aang/emotes/air_scooter_2.geo.json new file mode 100644 index 0000000..c0c1430 --- /dev/null +++ b/aang/src/main/resources/assets/aang/emotes/air_scooter_2.geo.json @@ -0,0 +1,125 @@ +{ + "format_version": "1.12.0", + "minecraft:geometry": [ + { + "description": { + "identifier": "geometry.unknown", + "texture_width": 16, + "texture_height": 16, + "visible_bounds_width": 2, + "visible_bounds_height": 3.5, + "visible_bounds_offset": [0, 1.25, 0] + }, + "bones": [ + { + "name": "bipedRig", + "pivot": [0, 0, 0] + }, + { + "name": "bipedHead", + "parent": "bipedRig", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 24, -4], "size": [8, 8, 8], "uv": [0, 0]} + ] + }, + { + "name": "armorHead", + "parent": "bipedHead", + "pivot": [0, 24, 0] + }, + { + "name": "bipedBody", + "parent": "bipedRig", + "pivot": [0, 24, 0], + "cubes": [ + {"origin": [-4, 12, -2], "size": [8, 12, 4], "uv": [0, 16]} + ] + }, + { + "name": "armorBody", + "parent": "bipedBody", + "pivot": [0, 24, 0] + }, + { + "name": "bipedRightArm", + "parent": "bipedRig", + "pivot": [-5, 22, 0], + "cubes": [ + {"origin": [-8, 12, -2], "size": [4, 12, 4], "uv": [16, 32]} + ] + }, + { + "name": "armorRightArm", + "parent": "bipedRightArm", + "pivot": [-4, 22, 0] + }, + { + "name": "airlocatorrightarm", + "parent": "armorRightArm", + "pivot": [-6, 12, 0], + "locators": { + "airlocatorrightarm": [-6, 12, 0] + } + }, + { + "name": "bipedLeftArm", + "parent": "bipedRig", + "pivot": [5, 22, 0], + "cubes": [ + {"origin": [4, 12, -2], "size": [4, 12, 4], "uv": [32, 0]} + ] + }, + { + "name": "armorLeftArm", + "parent": "bipedLeftArm", + "pivot": [4, 22, 0] + }, + { + "name": "airlocator", + "parent": "armorLeftArm", + "pivot": [6, 12, 0], + "locators": { + "airlocator": [6, 12, 0] + } + }, + { + "name": "bipedLeftLeg", + "parent": "bipedRig", + "pivot": [2, 12, 0], + "cubes": [ + {"origin": [0, 0, -2], "size": [4, 12, 4], "uv": [0, 32]} + ] + }, + { + "name": "armorLeftLeg", + "parent": "bipedLeftLeg", + "pivot": [2, 12, 0] + }, + { + "name": "armorLeftBoot", + "parent": "bipedLeftLeg", + "pivot": [2, 12, 0] + }, + { + "name": "bipedRightLeg", + "parent": "bipedRig", + "pivot": [-2, 12, 0], + "cubes": [ + {"origin": [-4, 0, -2], "size": [4, 12, 4], "uv": [24, 16]} + ] + }, + { + "name": "armorRightLeg", + "parent": "bipedRightLeg", + "pivot": [-2, 12, 0] + }, + { + "name": "armorRightBoot", + "parent": "bipedRightLeg", + "pivot": [-2, 12, 0] + } + ] + } + ] +} \ No newline at end of file diff --git a/aang/src/main/resources/assets/aang/emotes/air_scooter_sitting.animation.json b/aang/src/main/resources/assets/aang/emotes/air_scooter_sitting.animation.json new file mode 100644 index 0000000..e911344 --- /dev/null +++ b/aang/src/main/resources/assets/aang/emotes/air_scooter_sitting.animation.json @@ -0,0 +1,36 @@ +{ + "format_version": "1.8.0", + "animations": { + "air_scooter_sitting": { + "lockVanillaBones": { + "bipedRig": false, + "bipedHead": false, + "bipedBody": false, + "bipedLeftArm": true, + "bipedRightArm": true, + "bipedLeftLeg": false, + "bipedRightLeg": false + }, + "loop": "hold_on_last_frame", + "bones": { + "bipedRightArm": { + "rotation": { + "vector": [-90, -40, 0] + }, + "position": { + "vector": [-1, 0, 1] + } + }, + "bipedLeftArm": { + "rotation": { + "vector": [-90, 40, 0] + }, + "position": { + "vector": [1, 0, 1] + } + } + } + } + }, + "geckolib_format_version": 2 +} diff --git a/aang/src/main/resources/assets/aang/emotes/levitation.animation.json b/aang/src/main/resources/assets/aang/emotes/levitation.animation.json new file mode 100644 index 0000000..a8c6683 --- /dev/null +++ b/aang/src/main/resources/assets/aang/emotes/levitation.animation.json @@ -0,0 +1,100 @@ +{ + "format_version": "1.8.0", + "animations": { + "levitation": { + "loop": "hold_on_last_frame", + "lockVanillaBones": { + "bipedRig": false, + "bipedHead": false, + "bipedBody": false, + "bipedLeftArm": false, + "bipedRightArm": false, + "bipedLeftLeg": true, + "bipedRightLeg": true + }, + "animation_length": 0.6, + "bones": { + "bipedRig": { + "position": { + "vector": [0, "Math.cos(query.anim_time * 100) * 0.5 -0.5", 0] + } + }, + "bipedRightArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.6": { + "vector": [0, 0, 65], + "easing": "easeOutQuint" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.6": { + "vector": [-1, 0, 0], + "easing": "easeOutQuint" + } + } + }, + "bipedLeftArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.6": { + "vector": [0, 0, -65], + "easing": "easeOutQuint" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.6": { + "vector": [1, 0, 0], + "easing": "easeOutQuint" + } + } + }, + "bipedLeftLeg": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.6": { + "vector": ["Math.cos(query.anim_time * 150 +25) * -15", 0, 42.5], + "easing": "easeOutExpo" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.6": { + "vector": [6, 0, 0], + "easing": "easeOutExpo" + } + } + }, + "bipedRightLeg": { + "rotation": { + "vector": ["Math.cos(query.anim_time * 150 +25) * 15", 0, 0] + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.6": { + "vector": [-2, -1, 0], + "easing": "easeOutQuint" + } + } + } + } + } + }, + "geckolib_format_version": 2 +} diff --git a/aang/src/main/resources/assets/aang/emotes/spiritual_projection_loop.animation.json b/aang/src/main/resources/assets/aang/emotes/spiritual_projection_loop.animation.json new file mode 100644 index 0000000..145d425 --- /dev/null +++ b/aang/src/main/resources/assets/aang/emotes/spiritual_projection_loop.animation.json @@ -0,0 +1,65 @@ +{ + "format_version": "1.8.0", + "animations": { + "emote": { + "loop": true, + "lockVanillaBones": { + "bipedRig": false, + "bipedHead": false, + "bipedBody": false, + "bipedLeftArm": false, + "bipedRightArm": false, + "bipedLeftLeg": true, + "bipedRightLeg": true + }, + "bones": { + "bipedRig": { + "position": { + "vector": [0, "Math.cos(query.anim_time * 100) * 1 -0.5", 0] + } + }, + "bipedRightArm": { + "rotation": { + "vector": [0, 0, 80], + "easing": "easeOutExpo" + }, + "position": { + "vector": [-2, 0, 0], + "easing": "easeOutExpo" + } + }, + "bipedLeftArm": { + "rotation": { + "vector": [0, 0, -80], + "easing": "easeOutExpo" + }, + "position": { + "vector": [2, 0, 0], + "easing": "easeOutExpo" + } + }, + "bipedLeftLeg": { + "rotation": { + "vector": [-71.88679, 33.64409, 10.27206], + "easing": "easeOutExpo" + }, + "position": { + "vector": [5, -2, 0], + "easing": "easeOutExpo" + } + }, + "bipedRightLeg": { + "rotation": { + "vector": [-89.38679, -33.64409, -10.27206], + "easing": "easeOutExpo" + }, + "position": { + "vector": [-6, -2, 1], + "easing": "easeOutExpo" + } + } + } + } + }, + "geckolib_format_version": 2 +} diff --git a/aang/src/main/resources/assets/aang/emotes/spiritual_projection_start.animation.json b/aang/src/main/resources/assets/aang/emotes/spiritual_projection_start.animation.json new file mode 100644 index 0000000..57e1d93 --- /dev/null +++ b/aang/src/main/resources/assets/aang/emotes/spiritual_projection_start.animation.json @@ -0,0 +1,105 @@ +{ + "format_version": "1.8.0", + "animations": { + "emote": { + "animation_length": 2.12, + "lockVanillaBones": { + "bipedRig": false, + "bipedHead": false, + "bipedBody": false, + "bipedLeftArm": false, + "bipedRightArm": false, + "bipedLeftLeg": true, + "bipedRightLeg": true + }, + "bones": { + "bipedRig": { + "position": { + "vector": [0, "Math.cos(query.anim_time * 100) * 1 -0.5", 0] + } + }, + "bipedRightArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.72": { + "vector": [0, 0, 80], + "easing": "easeOutExpo" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.72": { + "vector": [-2, 0, 0], + "easing": "easeOutExpo" + } + } + }, + "bipedLeftArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.72": { + "vector": [0, 0, -80], + "easing": "easeOutExpo" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.72": { + "vector": [2, 0, 0], + "easing": "easeOutExpo" + } + } + }, + "bipedLeftLeg": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.72": { + "vector": [-71.88679, 33.64409, 10.27206], + "easing": "easeOutExpo" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.72": { + "vector": [5, -2, 0], + "easing": "easeOutExpo" + } + } + }, + "bipedRightLeg": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.72": { + "vector": [-89.38679, -33.64409, -10.27206], + "easing": "easeOutExpo" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.72": { + "vector": [-6, -2, 1], + "easing": "easeOutExpo" + } + } + } + } + } + }, + "geckolib_format_version": 2 +} diff --git a/aang/src/main/resources/assets/aang/particles/air_scooter_dust.json b/aang/src/main/resources/assets/aang/particles/air_scooter_dust.json new file mode 100644 index 0000000..ffbf1d3 --- /dev/null +++ b/aang/src/main/resources/assets/aang/particles/air_scooter_dust.json @@ -0,0 +1,16 @@ +{ + "textures": [ + "aang:big_smoke_0", + "aang:big_smoke_1", + "aang:big_smoke_2", + "aang:big_smoke_3", + "aang:big_smoke_4", + "aang:big_smoke_5", + "aang:big_smoke_6", + "aang:big_smoke_7", + "aang:big_smoke_8", + "aang:big_smoke_9", + "aang:big_smoke_10", + "aang:big_smoke_11" + ] +} diff --git a/aang/src/main/resources/assets/aang/particles/bending_air.json b/aang/src/main/resources/assets/aang/particles/bending_air.json new file mode 100644 index 0000000..ffbf1d3 --- /dev/null +++ b/aang/src/main/resources/assets/aang/particles/bending_air.json @@ -0,0 +1,16 @@ +{ + "textures": [ + "aang:big_smoke_0", + "aang:big_smoke_1", + "aang:big_smoke_2", + "aang:big_smoke_3", + "aang:big_smoke_4", + "aang:big_smoke_5", + "aang:big_smoke_6", + "aang:big_smoke_7", + "aang:big_smoke_8", + "aang:big_smoke_9", + "aang:big_smoke_10", + "aang:big_smoke_11" + ] +} diff --git a/aang/src/main/resources/assets/aang/particles/snowstorm.json b/aang/src/main/resources/assets/aang/particles/snowstorm.json new file mode 100644 index 0000000..935f6d3 --- /dev/null +++ b/aang/src/main/resources/assets/aang/particles/snowstorm.json @@ -0,0 +1,5 @@ +{ + "textures": [ + "aang:graffiti" + ] +} diff --git a/aang/src/main/resources/assets/aang/sounds.json b/aang/src/main/resources/assets/aang/sounds.json new file mode 100644 index 0000000..2f6fd3d --- /dev/null +++ b/aang/src/main/resources/assets/aang/sounds.json @@ -0,0 +1,7 @@ +{ + "flying": { + "sounds": [ + "aang:flying" + ] + } +} diff --git a/aang/src/main/resources/assets/aang/sounds/flying.ogg b/aang/src/main/resources/assets/aang/sounds/flying.ogg new file mode 100644 index 0000000..cf2bf71 Binary files /dev/null and b/aang/src/main/resources/assets/aang/sounds/flying.ogg differ diff --git a/aang/src/main/resources/assets/aang/textures/entity/projectiles/air_scooter.png b/aang/src/main/resources/assets/aang/textures/entity/projectiles/air_scooter.png new file mode 100644 index 0000000..2e385ef Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/entity/projectiles/air_scooter.png differ diff --git a/aang/src/main/resources/assets/aang/textures/misc/spiritual_vignette.png b/aang/src/main/resources/assets/aang/textures/misc/spiritual_vignette.png new file mode 100644 index 0000000..cff17f2 Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/misc/spiritual_vignette.png differ diff --git a/aang/src/main/resources/assets/aang/textures/misc/spiritual_vignette.png.mcmeta b/aang/src/main/resources/assets/aang/textures/misc/spiritual_vignette.png.mcmeta new file mode 100644 index 0000000..2077f3c --- /dev/null +++ b/aang/src/main/resources/assets/aang/textures/misc/spiritual_vignette.png.mcmeta @@ -0,0 +1,6 @@ +{ + "texture": { + "blur": true + } +} + diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_0.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_0.png new file mode 100644 index 0000000..fc8431c Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_0.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_1.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_1.png new file mode 100644 index 0000000..0c31abc Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_1.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_10.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_10.png new file mode 100644 index 0000000..4db3d9b Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_10.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_11.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_11.png new file mode 100644 index 0000000..b509d74 Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_11.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_2.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_2.png new file mode 100644 index 0000000..97270d4 Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_2.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_3.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_3.png new file mode 100644 index 0000000..8a75051 Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_3.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_4.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_4.png new file mode 100644 index 0000000..66cd4a3 Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_4.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_5.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_5.png new file mode 100644 index 0000000..0c1ad4f Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_5.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_6.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_6.png new file mode 100644 index 0000000..0bcadb8 Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_6.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_7.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_7.png new file mode 100644 index 0000000..1c36406 Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_7.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_8.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_8.png new file mode 100644 index 0000000..88cb881 Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_8.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/big_smoke_9.png b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_9.png new file mode 100644 index 0000000..48f9cdd Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/big_smoke_9.png differ diff --git a/aang/src/main/resources/assets/aang/textures/particle/graffiti.png b/aang/src/main/resources/assets/aang/textures/particle/graffiti.png new file mode 100644 index 0000000..c67dd6d Binary files /dev/null and b/aang/src/main/resources/assets/aang/textures/particle/graffiti.png differ diff --git a/aang/src/main/resources/assets/hero-api/textures/hero/1minuteaang/icon.png b/aang/src/main/resources/assets/hero-api/textures/hero/1minuteaang/icon.png new file mode 100644 index 0000000..96d5ae2 Binary files /dev/null and b/aang/src/main/resources/assets/hero-api/textures/hero/1minuteaang/icon.png differ diff --git a/aang/src/main/resources/assets/hero-api/textures/hero/1stundeaang/icon.png b/aang/src/main/resources/assets/hero-api/textures/hero/1stundeaang/icon.png new file mode 100644 index 0000000..96d5ae2 Binary files /dev/null and b/aang/src/main/resources/assets/hero-api/textures/hero/1stundeaang/icon.png differ diff --git a/aang/src/main/resources/assets/hero-api/textures/hero/aang/icon.png b/aang/src/main/resources/assets/hero-api/textures/hero/aang/icon.png new file mode 100644 index 0000000..96d5ae2 Binary files /dev/null and b/aang/src/main/resources/assets/hero-api/textures/hero/aang/icon.png differ diff --git a/aang/src/main/resources/fabric.mod.json b/aang/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..1bd046a --- /dev/null +++ b/aang/src/main/resources/fabric.mod.json @@ -0,0 +1,49 @@ +{ + "schemaVersion": 1, + "name": "Aang", + "id": "aang", + "version": "${version}", + "description": "Aang", + "authors": [ + "NoRiskk" + ], + "icon": "assets/aang/icon.png", + "license": "ARR", + "environment": "*", + "entrypoints": { + "main": [ + { + "adapter": "kotlin", + "value": "gg.norisk.heroes.aang.AangManager" + } + ], + "client": [ + { + "adapter": "kotlin", + "value": "gg.norisk.heroes.aang.AangManager" + } + ], + "server": [ + { + "adapter": "kotlin", + "value": "gg.norisk.heroes.aang.AangManager" + } + ] + }, + "mixins": [ + "aang.mixins.json" + ], + "accessWidener": "aang.accesswidener", + "depends": { + }, + "custom": { + "modmenu": { + "badges": [ + "library" + ], + "parent": { + "id": "hero-api" + } + } + } +} diff --git a/assets/ffa-13-07-2024.schem b/assets/ffa-13-07-2024.schem new file mode 100644 index 0000000..5591b59 Binary files /dev/null and b/assets/ffa-13-07-2024.schem differ diff --git a/blockbench/toph.bbmodel b/blockbench/toph.bbmodel new file mode 100644 index 0000000..fdea050 --- /dev/null +++ b/blockbench/toph.bbmodel @@ -0,0 +1 @@ +{"meta":{"format_version":"4.10","model_format":"animated_entity_model","box_uv":true},"name":"toph","model_identifier":"","visible_box":[1,1,0],"variable_placeholders":"","variable_placeholder_buttons":[],"timeline_setups":[],"unhandled_root_fields":{},"geckolib_modid":"","geckolib_model_type":"Entity","geckolib_filepath_cache":{},"resolution":{"width":16,"height":16},"elements":[{"name":"bipedHead","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":1,"origin":[0,0,0],"faces":{"north":{"uv":[8,8,16,16]},"east":{"uv":[0,8,8,16]},"south":{"uv":[24,8,32,16]},"west":{"uv":[16,8,24,16]},"up":{"uv":[16,8,8,0]},"down":{"uv":[24,0,16,8]}},"type":"cube","uuid":"e5841845-2e3f-0f50-b112-0da10d6bac1e"},{"name":"bipedBody","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":3,"origin":[0,0,0],"uv_offset":[0,16],"faces":{"north":{"uv":[4,20,12,32]},"east":{"uv":[0,20,4,32]},"south":{"uv":[16,20,24,32]},"west":{"uv":[12,20,16,32]},"up":{"uv":[12,20,4,16]},"down":{"uv":[20,16,12,20]}},"type":"cube","uuid":"a6a9f27d-0d1d-91f6-95bb-c69e6c8044c4"},{"name":"bipedRightArm","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[4,12,-2],"to":[8,24,2],"autouv":0,"color":5,"origin":[0,0,0],"uv_offset":[16,32],"faces":{"north":{"uv":[20,36,24,48]},"east":{"uv":[16,36,20,48]},"south":{"uv":[28,36,32,48]},"west":{"uv":[24,36,28,48]},"up":{"uv":[24,36,20,32]},"down":{"uv":[28,32,24,36]}},"type":"cube","uuid":"f916afcf-94f8-26ed-0e3a-52807ea6dd91"},{"name":"bipedLeftArm","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-8,12,-2],"to":[-4,24,2],"autouv":0,"color":7,"origin":[0,0,0],"uv_offset":[32,0],"faces":{"north":{"uv":[36,4,40,16]},"east":{"uv":[32,4,36,16]},"south":{"uv":[44,4,48,16]},"west":{"uv":[40,4,44,16]},"up":{"uv":[40,4,36,0]},"down":{"uv":[44,0,40,4]}},"type":"cube","uuid":"73ee629c-6c19-3c63-7c37-d3c416aa9a99"},{"name":"bipedLeftLeg","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[-4,0,-2],"to":[0,12,2],"autouv":0,"color":9,"origin":[0,0,0],"uv_offset":[0,32],"faces":{"north":{"uv":[4,36,8,48]},"east":{"uv":[0,36,4,48]},"south":{"uv":[12,36,16,48]},"west":{"uv":[8,36,12,48]},"up":{"uv":[8,36,4,32]},"down":{"uv":[12,32,8,36]}},"type":"cube","uuid":"8ee88bce-95e1-f080-3568-e704d5028ab9"},{"name":"bipedRightLeg","box_uv":true,"rescale":false,"locked":false,"light_emission":0,"render_order":"default","allow_mirror_modeling":true,"from":[0,0,-2],"to":[4,12,2],"autouv":0,"color":2,"origin":[0,0,0],"uv_offset":[24,16],"faces":{"north":{"uv":[28,20,32,32]},"east":{"uv":[24,20,28,32]},"south":{"uv":[36,20,40,32]},"west":{"uv":[32,20,36,32]},"up":{"uv":[32,20,28,16]},"down":{"uv":[36,16,32,20]}},"type":"cube","uuid":"80cf2ca9-ce87-05a5-2475-bcf6e2bf2458"}],"outliner":[{"name":"bipedRig","origin":[0,0,0],"color":0,"uuid":"f7d20b62-01ed-bab3-fa05-d56a255afe6b","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[{"name":"bipedHead","origin":[0,24,0],"color":1,"uuid":"f65b6d21-44d9-8222-c6bc-a6d42ede554e","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["e5841845-2e3f-0f50-b112-0da10d6bac1e",{"name":"armorHead","origin":[0,24,0],"color":2,"uuid":"60aca50e-f327-41f4-b4de-efd0d0f43fa8","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedBody","origin":[0,24,0],"color":3,"uuid":"9b034992-30d9-e07e-b869-d1b12f8aa24c","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["a6a9f27d-0d1d-91f6-95bb-c69e6c8044c4",{"name":"armorBody","origin":[0,24,0],"color":4,"uuid":"a0c0e5da-6af5-0e2f-881d-0e4e05e2e29d","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedRightArm","origin":[5,22,0],"color":5,"uuid":"7a1d791a-94df-a32d-4340-13c665e7bb9f","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["f916afcf-94f8-26ed-0e3a-52807ea6dd91",{"name":"armorRightArm","origin":[4,22,0],"color":6,"uuid":"b753db07-83c3-df73-8f84-4775193ed7cd","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedLeftArm","origin":[-5,22,0],"color":7,"uuid":"d3708a24-fc02-e5e3-3652-282e2b7b7ba9","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["73ee629c-6c19-3c63-7c37-d3c416aa9a99",{"name":"armorLeftArm","origin":[-4,22,0],"color":8,"uuid":"b11f5810-cf1f-fb21-0113-bdc9a7709870","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedLeftLeg","origin":[-2,12,0],"color":9,"uuid":"41a8bbae-789c-6c63-7652-76cf641172eb","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["8ee88bce-95e1-f080-3568-e704d5028ab9",{"name":"armorLeftLeg","origin":[-2,12,0],"color":0,"uuid":"959464aa-5959-fc92-a387-be5dcd0ddc45","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]},{"name":"armorLeftBoot","origin":[-2,12,0],"color":1,"uuid":"065cb27b-2980-87d8-28bf-a2cbaf9cd8f1","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]},{"name":"bipedRightLeg","origin":[2,12,0],"color":2,"uuid":"46c209af-600b-324c-bc48-89b6f4326cb2","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":["80cf2ca9-ce87-05a5-2475-bcf6e2bf2458",{"name":"armorRightLeg","origin":[2,12,0],"color":3,"uuid":"6f96ee97-660c-8ddd-356d-e1b0d9c2008d","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]},{"name":"armorRightBoot","origin":[2,12,0],"color":4,"uuid":"ec7590f1-8742-6b03-593d-63630e76e720","export":true,"mirror_uv":false,"isOpen":false,"locked":false,"visibility":true,"autouv":0,"children":[]}]}]}],"textures":[]} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..21cc24d --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,250 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.spotless) + alias(libs.plugins.nexusPublish) + alias(libs.plugins.fabricLoom) + alias(libs.plugins.kotlin) + alias(libs.plugins.kotlin.serialization) + `maven-publish` +} + +defaultTasks("clean", "build") + +allprojects { + group = "gg.norisk" + description = "Heroes Project" + + apply(plugin = "fabric-loom") + + dependencies { + "minecraft"(rootProject.libs.minecraft) + "mappings"(variantOf(rootProject.libs.yarn.mappings) { classifier("v2") }) + } + + loom { + runConfigs.configureEach { + this.ideConfigGenerated(true) + } + } + + repositories { + mavenCentral() + maven("https://maven.kosmx.dev/") + maven("https://maven.enginehub.org/repo/") + maven(uri("https://maven.wispforest.io")) + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") + maven("https://repo.cloudnetservice.eu/repository/releases/") + + maven { + name = "GeckoLib" + url = uri("https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/") + content { + includeGroup("software.bernie.geckolib") + } + } + + maven { + url = uri("https://maven.norisk.gg/repository/norisk-production/") + } + + maven { + url = uri("https://maven.norisk.gg/repository/maven-releases/") + } + + // more stable replacement for jitpack + maven("https://repository.derklaro.dev/releases/") { + mavenContent { + releasesOnly() + } + } + maven("https://repository.derklaro.dev/snapshots/") { + mavenContent { + snapshotsOnly() + } + } + + exclusiveContent { + forRepository { + maven("https://api.modrinth.com/maven") + } + filter { + includeGroup("maven.modrinth") + } + } + + // packetevents + maven("https://repo.codemc.io/repository/maven-releases/") { + mavenContent { + includeGroup("com.github.retrooper") + } + } + } +} + +subprojects { + // apply all plugins only to subprojects + apply(plugin = "signing") + //apply(plugin = "checkstyle") + apply(plugin = "java-library") + apply(plugin = "maven-publish") + apply(plugin = "com.diffplug.spotless") + apply(plugin = "kotlin") + apply(plugin = "org.jetbrains.kotlin.plugin.serialization") + + //ich weiß das ist kriminell aber + version = rootProject.libs.versions.minecraft.get() + "-" + when (name) { + "hero-api" -> "1.3.1" + "katara" -> "1.1.0" + "aang" -> "1.1.0" + "toph" -> "1.1.0" + "ffa-server" -> "1.3.17" + "datatracker" -> "1.0.17" + else -> version + } //+ "-SNAPSHOT" + + dependencies { + "compileOnly"(rootProject.libs.annotations) + "implementation"(rootProject.libs.serialization) + } + + configurations.all { + // unsure why but every project loves them, and they literally have an import for every letter I type - beware + exclude("org.checkerframework", "checker-qual") + } + + tasks.withType { + from(rootProject.file("license.txt")) + duplicatesStrategy = DuplicatesStrategy.INCLUDE + } + + tasks.withType().configureEach { + compilerOptions { + freeCompilerArgs = listOf("-Xcontext-receivers", "-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn") + } + } + + tasks.withType().configureEach { + // options + options.release.set(21) + options.encoding = "UTF-8" + options.isIncremental = true + // we are aware that those are there, but we only do that if there is no other way we can use - so please keep the terminal clean! + options.compilerArgs = mutableListOf("-Xlint:-deprecation,-unchecked") + } + + extensions.configure { + disableAutoTargetJvm() + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) + } + + /*tasks.withType { + maxErrors = 0 + maxWarnings = 0 + configFile = rootProject.file("checkstyle.xml") + } + + extensions.configure { + toolVersion = rootProject.libs.versions.checkstyleTools.get() + } + + extensions.configure { + java { + licenseHeaderFile(rootProject.file("license_header.txt")) + } + }*/ + + tasks.withType { + val options = options as? StandardJavadocDocletOptions ?: return@withType + + // options + options.encoding = "UTF-8" + options.memberLevel = JavadocMemberLevel.PRIVATE + options.addStringOption("-html5") + options.addBooleanOption("Xdoclint:-missing", true) + } + + tasks.register("javadocJar") { + archiveClassifier.set("javadoc") + from(tasks.getByName("javadoc")) + } + + val sourceJar = tasks.register("sourcesJar") { + archiveClassifier.set("sources") + from(project.the()["main"].allJava) + } + + tasks.processResources { + val properties = mapOf("version" to project.version) + inputs.properties(properties) + filesMatching("fabric.mod.json") { expand(properties) } + } + + tasks.withType().configureEach { + val predicate = provider { + (repository == publishing.repositories["production"] && + publication == publishing.publications["binary"]) || + (repository == publishing.repositories["dev"] && + publication == publishing.publications["binaryAndSources"]) + } + onlyIf("publishing binary to the production repository, or binary and sources to the internal dev one") { + predicate.get() + } + } + + tasks.withType().configureEach { + val predicate = provider { + publication == publishing.publications["binaryAndSources"] + } + onlyIf("publishing binary and sources") { + predicate.get() + } + } + + extensions.configure { + publications { + create("binary") { + groupId = project.group.toString() + artifactId = project.name + version = project.version.toString() + from(components["java"]) + } + create("binaryAndSources") { + groupId = project.group.toString() + artifactId = project.name + version = project.version.toString() + from(components["java"]) + artifact(sourceJar) + } + } + repositories { + fun MavenArtifactRepository.applyCredentials() = credentials { + username = + (System.getenv("NORISK_NEXUS_USERNAME") ?: project.findProperty("noriskMavenUsername")).toString() + password = + (System.getenv("NORISK_NEXUS_PASSWORD") ?: project.findProperty("noriskMavenPassword")).toString() + } + maven { + name = "production" + url = if (version.toString().endsWith("SNAPSHOT")) { + uri("https://maven.norisk.gg/repository/norisk-snapshots/") + } else { + uri("https://maven.norisk.gg/repository/norisk-production/") + } + applyCredentials() + } + maven { + name = "dev" + // this could also be a maven repo on the dev server + // e.g. maven-staging.norisk.gg + url = if (version.toString().endsWith("SNAPSHOT")) { + uri("https://maven.norisk.gg/repository/maven-snapshots/") + } else { + uri("https://maven.norisk.gg/repository/maven-releases/") + } + applyCredentials() + } + } + } +} diff --git a/datatracker/build.gradle.kts b/datatracker/build.gradle.kts new file mode 100644 index 0000000..3fd60ed --- /dev/null +++ b/datatracker/build.gradle.kts @@ -0,0 +1,4 @@ +dependencies { + modApi(libs.bundles.fabric) + modApi(libs.bundles.silk) +} diff --git a/datatracker/src/main/java/gg/norisk/datatracker/mixin/EntityMixin.java b/datatracker/src/main/java/gg/norisk/datatracker/mixin/EntityMixin.java new file mode 100644 index 0000000..3225054 --- /dev/null +++ b/datatracker/src/main/java/gg/norisk/datatracker/mixin/EntityMixin.java @@ -0,0 +1,33 @@ +package gg.norisk.datatracker.mixin; + +import gg.norisk.datatracker.entity.ISyncedEntity; +import gg.norisk.datatracker.entity.ISyncedEntityKt; +import net.minecraft.entity.Entity; +import net.minecraft.nbt.NbtCompound; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.HashMap; +import java.util.Map; + +@Mixin(Entity.class) +public abstract class EntityMixin implements ISyncedEntity { + @Unique + private final Map syncedValues = new HashMap<>(); + + @NotNull + @Override + public Map getSyncedValuesMap() { + return syncedValues; + } + + //this is used for quest-api to trigger nbt condition like isSubwaySurfers + @Inject(method = "writeNbt", at = @At("HEAD")) + private void injected(NbtCompound nbtCompound, CallbackInfoReturnable cir) { + ISyncedEntityKt.writeSyncedNbtData((Entity) (Object) this, nbtCompound); + } +} diff --git a/datatracker/src/main/java/gg/norisk/datatracker/mixin/EntityTrackerEntryMixin.java b/datatracker/src/main/java/gg/norisk/datatracker/mixin/EntityTrackerEntryMixin.java new file mode 100644 index 0000000..508ae91 --- /dev/null +++ b/datatracker/src/main/java/gg/norisk/datatracker/mixin/EntityTrackerEntryMixin.java @@ -0,0 +1,24 @@ +package gg.norisk.datatracker.mixin; + +import gg.norisk.datatracker.entity.ISyncedEntityKt; +import net.minecraft.entity.Entity; +import net.minecraft.server.network.EntityTrackerEntry; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(EntityTrackerEntry.class) +public abstract class EntityTrackerEntryMixin { + @Shadow + @Final + private Entity entity; + + @Inject(method = "startTracking", at = @At("TAIL")) + private void startTrackingSync(ServerPlayerEntity serverPlayerEntity, CallbackInfo ci) { + ISyncedEntityKt.syncValues(this.entity, serverPlayerEntity); + } +} diff --git a/datatracker/src/main/java/gg/norisk/datatracker/mixin/accessor/WorldAccessor.java b/datatracker/src/main/java/gg/norisk/datatracker/mixin/accessor/WorldAccessor.java new file mode 100644 index 0000000..0118fe5 --- /dev/null +++ b/datatracker/src/main/java/gg/norisk/datatracker/mixin/accessor/WorldAccessor.java @@ -0,0 +1,14 @@ +package gg.norisk.datatracker.mixin.accessor; + +import net.minecraft.entity.Entity; +import net.minecraft.world.World; +import net.minecraft.world.entity.EntityLookup; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(World.class) +public interface WorldAccessor { + @Invoker("getEntityLookup") + EntityLookup invokeGetEntityLookup(); +} + diff --git a/datatracker/src/main/kotlin/gg/norisk/datatracker/DataTracker.kt b/datatracker/src/main/kotlin/gg/norisk/datatracker/DataTracker.kt new file mode 100644 index 0000000..a19c639 --- /dev/null +++ b/datatracker/src/main/kotlin/gg/norisk/datatracker/DataTracker.kt @@ -0,0 +1,19 @@ +package gg.norisk.datatracker + +import gg.norisk.datatracker.entity.initSyncedEntitiesClient +import net.fabricmc.api.ClientModInitializer +import net.fabricmc.api.ModInitializer +import net.minecraft.util.Identifier +import org.apache.logging.log4j.LogManager + +object DataTracker : ModInitializer, ClientModInitializer { + private const val MOD_ID = "datatracker" + fun String.toId() = Identifier.of(MOD_ID, this) + val logger = LogManager.getLogger(MOD_ID) + override fun onInitialize() { + } + + override fun onInitializeClient() { + initSyncedEntitiesClient() + } +} diff --git a/datatracker/src/main/kotlin/gg/norisk/datatracker/entity/ISyncedEntity.kt b/datatracker/src/main/kotlin/gg/norisk/datatracker/entity/ISyncedEntity.kt new file mode 100644 index 0000000..6b46a34 --- /dev/null +++ b/datatracker/src/main/kotlin/gg/norisk/datatracker/entity/ISyncedEntity.kt @@ -0,0 +1,206 @@ +package gg.norisk.datatracker.entity + +import gg.norisk.datatracker.DataTracker.logger +import gg.norisk.datatracker.DataTracker.toId +import gg.norisk.datatracker.serialization.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import net.minecraft.entity.Entity +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtHelper +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.util.math.BlockPos +import net.silkmc.silk.core.event.Event +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.nbt.toNbt +import net.silkmc.silk.network.packet.s2cPacket +import org.joml.Vector3f +import java.util.* + +interface ISyncedEntity { + fun getSyncedValuesMap(): MutableMap +} + +@Serializable +data class EntityWrapper( + val entityId: Int, + val key: String, +) + +@Serializable +data class DataWrapper( + val value: String, + val clazz: String, +) + +open class SyncedValueChangeEvent(val key: String, val entity: Entity, val oldValue: Any?) + +val syncedValueChangeEvent = Event.onlySync() + +fun initSyncedEntitiesClient() { + addSyncedData.receiveOnClient { packet, context -> + mcCoroutineTask(sync = true, client = true) { + //logger.info("###Received Packet {}", packet) + val entity = context.client.world?.getEntityById(packet.first.entityId) + //logger.info("###Found Entity {}", entity) + + if (registeredTypes.none { packet.second.clazz == it.key.toString() }) { + throw Error("Please register a Serializer for $packet") + } + + for ((clazz, serializer) in registeredTypes) { + if (packet.second.clazz == clazz.toString()) { + val decodedValue = runCatching { dataTrackerJson.decodeFromString(serializer as KSerializer, packet.second.value) }.onFailure { it.printStackTrace() }.getOrNull() + logger.info("Setting $entity $packet") + entity?.setSyncedData(packet.first.key, decodedValue) + break + } + } + } + } + for ((clazz, value) in registeredTypes) { + logger.info("Registering {} {}", clazz, value) + } + removeSyncedData.receiveOnClient { packet, context -> + mcCoroutineTask(sync = true, client = true) { + val entity = context.client.world?.getEntityById(packet.entityId) + entity?.unsetSyncedData(packet.key) + } + } +} + +//hier kann man noch weitere sachen registrieren +val registeredTypes = buildMap { + put(Boolean::class, BooleanSerializer) + put(String::class, StringSerializer) + put(Float::class, FloatSerializer) + put(Double::class, DoubleSerializer) + put(UUID::class, UUIDSerializer) + put(Int::class, IntSerializer) + put(Long::class, LongSerializer) + put(BlockPos::class, BlockPosSerializer) + put(Vector3f::class, Vector3fSerializer) +}.toMutableMap() + +val addSyncedData = s2cPacket>("add-sync".toId()) +val removeSyncedData = s2cPacket("remove-sync".toId()) + +internal val dataTrackerJson = Json { + ignoreUnknownKeys = true +} + +fun Entity.syncValue(key: String, value: Any, player: ServerPlayerEntity? = null) { + if (registeredTypes.none { value::class == it.key }) { + throw Error("Please register a Serializer for $value $key ${value::class}") + } + + for ((clazz, serializer) in registeredTypes) { + logger.debug("Value Class: {} Registered Class: {} {}", value::class, clazz, value::class == clazz) + if (value::class == clazz) { + val encodedValue = dataTrackerJson.encodeToString(serializer as KSerializer, value) + val pair = Pair(EntityWrapper(id, key), DataWrapper(encodedValue, clazz.toString())) + logger.debug("Sending {}", pair) + if (player != null) { + addSyncedData.send(pair, player) + } else { + addSyncedData.sendToAll(pair) + } + break + } + } +} + +internal fun Entity.writeSyncedNbtData(nbtCompound: NbtCompound) { + for ((key, value) in (this as ISyncedEntity).getSyncedValuesMap()) { + runCatching { + //JUP, wäre irgendwie so geil wenn man den registered type code von oben smarter hier einbauen kann + //das es direkt für alles geht aber erstmal low prio... + when (value) { + is Int -> { + nbtCompound.put(key, value.toNbt()) + } + + is Boolean -> { + nbtCompound.put(key, value.toNbt()) + } + + is String -> { + nbtCompound.put(key, value.toNbt()) + } + + is Double -> { + nbtCompound.put(key, value.toNbt()) + } + + is BlockPos -> { + nbtCompound.put(key, NbtHelper.fromBlockPos(value)) + } + + is Float -> { + nbtCompound.put(key, value.toNbt()) + } + + is Long -> { + nbtCompound.put(key, value.toNbt()) + } + + else -> { + logger.info("NOT SUPPORTED: [$key/$value] ${value::class}") + } + } + }.onSuccess {}.onFailure { + it.printStackTrace() + } + } +} + +fun Entity.getSyncedData(key: String): T? { + return (this as ISyncedEntity).getSyncedValuesMap()[key] as? T? +} + +fun Entity.hasSyncedData(key: String): Boolean { + return (this as ISyncedEntity).getSyncedValuesMap().containsKey(key) +} + +fun Entity.syncValues(player: ServerPlayerEntity? = null) { + for ((key, value) in (this as ISyncedEntity).getSyncedValuesMap()) { + if (!world.isClient) { + syncValue(key, value, player) + } + } +} + +fun Entity.unsetSyncedData(key: String, player: ServerPlayerEntity? = null) { + logger.debug("Client={} Unset Synced Data {} {} {}", world.isClient, key, player) + val oldValue = this.getSyncedData(key) + (this as ISyncedEntity).getSyncedValuesMap().remove(key) + syncedValueChangeEvent.invoke(SyncedValueChangeEvent(key, this, oldValue)) + if (!world.isClient) { + if (player != null) { + removeSyncedData.send(EntityWrapper(id, key), player) + } else { + //TODO ka ob das jemals probleme machen sollte aber eig nicht oder + removeSyncedData.sendToAll(EntityWrapper(id, key)) + } + } +} + + +fun Entity.setSyncedData(key: String, value: Any?, player: ServerPlayerEntity? = null) { + logger.debug("Client={} Synced Data {} {} {}", world.isClient, key, value, player) + val oldValue = this.getSyncedData(key) + if (value == null) { + (this as ISyncedEntity).getSyncedValuesMap().remove(key) + } else { + (this as ISyncedEntity).getSyncedValuesMap()[key] = value + } + syncedValueChangeEvent.invoke(SyncedValueChangeEvent(key, this, oldValue)) + if (!world.isClient) { + if (value != null) { + syncValue(key, value, player) + } else { + unsetSyncedData(key, player) + } + } +} diff --git a/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/BlockPosSerializer.kt b/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/BlockPosSerializer.kt new file mode 100644 index 0000000..c01aab2 --- /dev/null +++ b/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/BlockPosSerializer.kt @@ -0,0 +1,43 @@ +package gg.norisk.datatracker.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.* +import net.minecraft.util.math.BlockPos + +object BlockPosSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("BlockPos") { + element("x") + element("y") + element("z") + } + + override fun serialize(encoder: Encoder, value: BlockPos) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.x) + encodeIntElement(descriptor, 1, value.y) + encodeIntElement(descriptor, 2, value.z) + } + } + + override fun deserialize(decoder: Decoder): BlockPos { + return decoder.decodeStructure(descriptor) { + var x = 0 + var y = 0 + var z = 0 + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> x = decodeIntElement(descriptor, 0) + 1 -> y = decodeIntElement(descriptor, 1) + 2 -> z = decodeIntElement(descriptor, 2) + CompositeDecoder.DECODE_DONE -> break + else -> throw SerializationException("Unknown index $index") + } + } + BlockPos(x, y, z) + } + } +} diff --git a/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/PrimitiveSerializers.kt b/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/PrimitiveSerializers.kt new file mode 100644 index 0000000..2d498d3 --- /dev/null +++ b/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/PrimitiveSerializers.kt @@ -0,0 +1,50 @@ +package gg.norisk.datatracker.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@PublishedApi +internal object StringSerializer : KSerializer { + override val descriptor: SerialDescriptor = String.serializer().descriptor + override fun serialize(encoder: Encoder, value: String): Unit = String.serializer().serialize(encoder, value) + override fun deserialize(decoder: Decoder): String = String.serializer().deserialize(decoder) +} + +@PublishedApi +internal object BooleanSerializer : KSerializer { + override val descriptor: SerialDescriptor = Boolean.serializer().descriptor + override fun serialize(encoder: Encoder, value: Boolean): Unit = Boolean.serializer().serialize(encoder, value) + override fun deserialize(decoder: Decoder): Boolean = Boolean.serializer().deserialize(decoder) +} + + +@PublishedApi +internal object IntSerializer : KSerializer { + override val descriptor: SerialDescriptor = Int.serializer().descriptor + override fun serialize(encoder: Encoder, value: Int): Unit = Int.serializer().serialize(encoder, value) + override fun deserialize(decoder: Decoder): Int = Int.serializer().deserialize(decoder) +} + +@PublishedApi +internal object FloatSerializer : KSerializer { + override val descriptor: SerialDescriptor = Float.serializer().descriptor + override fun serialize(encoder: Encoder, value: Float): Unit = Float.serializer().serialize(encoder, value) + override fun deserialize(decoder: Decoder): Float = Float.serializer().deserialize(decoder) +} + +@PublishedApi +internal object DoubleSerializer : KSerializer { + override val descriptor: SerialDescriptor = Double.serializer().descriptor + override fun serialize(encoder: Encoder, value: Double): Unit = Double.serializer().serialize(encoder, value) + override fun deserialize(decoder: Decoder): Double = Double.serializer().deserialize(decoder) +} + +@PublishedApi +internal object LongSerializer : KSerializer { + override val descriptor: SerialDescriptor = Long.serializer().descriptor + override fun serialize(encoder: Encoder, value: Long): Unit = Long.serializer().serialize(encoder, value) + override fun deserialize(decoder: Decoder): Long = Long.serializer().deserialize(decoder) +} diff --git a/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/UUIDSerializer.kt b/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/UUIDSerializer.kt new file mode 100644 index 0000000..494fdf9 --- /dev/null +++ b/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/UUIDSerializer.kt @@ -0,0 +1,20 @@ +package gg.norisk.datatracker.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.util.* + +object UUIDSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): UUID { + return UUID.fromString(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: UUID) { + encoder.encodeString(value.toString()) + } +} diff --git a/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/Vector3fSerializer.kt b/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/Vector3fSerializer.kt new file mode 100644 index 0000000..a166a05 --- /dev/null +++ b/datatracker/src/main/kotlin/gg/norisk/datatracker/serialization/Vector3fSerializer.kt @@ -0,0 +1,44 @@ +package gg.norisk.datatracker.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.* +import org.joml.Vector3f + +object Vector3fSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Vector3f") { + element("x") + element("y") + element("z") + } + + override fun serialize(encoder: Encoder, value: Vector3f) { + encoder.encodeStructure(descriptor) { + encodeFloatElement(descriptor, 0, value.x) + encodeFloatElement(descriptor, 1, value.y) + encodeFloatElement(descriptor, 2, value.z) + } + } + + override fun deserialize(decoder: Decoder): Vector3f { + return decoder.decodeStructure(descriptor) { + var x = 0f + var y = 0f + var z = 0f + + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> x = decodeFloatElement(descriptor, index) + 1 -> y = decodeFloatElement(descriptor, index) + 2 -> z = decodeFloatElement(descriptor, index) + CompositeDecoder.DECODE_DONE -> break + else -> throw SerializationException("Unknown index $index") + } + } + Vector3f(x, y, z) + } + } +} \ No newline at end of file diff --git a/datatracker/src/main/resources/assets/datatracker/icon.png b/datatracker/src/main/resources/assets/datatracker/icon.png new file mode 100644 index 0000000..bdd1117 Binary files /dev/null and b/datatracker/src/main/resources/assets/datatracker/icon.png differ diff --git a/datatracker/src/main/resources/datatracker.accesswidener b/datatracker/src/main/resources/datatracker.accesswidener new file mode 100644 index 0000000..d4342d0 --- /dev/null +++ b/datatracker/src/main/resources/datatracker.accesswidener @@ -0,0 +1 @@ +accessWidener v2 named diff --git a/datatracker/src/main/resources/datatracker.mixins.json b/datatracker/src/main/resources/datatracker.mixins.json new file mode 100644 index 0000000..b0480ed --- /dev/null +++ b/datatracker/src/main/resources/datatracker.mixins.json @@ -0,0 +1,16 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "gg.norisk.datatracker.mixin", + "compatibilityLevel": "JAVA_17", + "injectors": { + "defaultRequire": 1 + }, + "mixins": [ + "EntityMixin", + "EntityTrackerEntryMixin", + "accessor.WorldAccessor" + ], + "client": [ + ] +} diff --git a/datatracker/src/main/resources/fabric.mod.json b/datatracker/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..1bdc221 --- /dev/null +++ b/datatracker/src/main/resources/fabric.mod.json @@ -0,0 +1,43 @@ +{ + "schemaVersion": 1, + "name": "DataTracker", + "id": "datatracker", + "version": "${version}", + "description": "easy syncing of data between server and client", + "authors": [ + "NoRiskk" + ], + "icon": "assets/datatracker/icon.png", + "license": "ARR", + "environment": "*", + "entrypoints": { + "main": [ + { + "adapter": "kotlin", + "value": "gg.norisk.datatracker.DataTracker" + } + ], + "client": [ + { + "adapter": "kotlin", + "value": "gg.norisk.datatracker.DataTracker" + } + ] + }, + "mixins": [ + "datatracker.mixins.json" + ], + "accessWidener": "datatracker.accesswidener", + "depends": { + }, + "custom": { + "modmenu": { + "badges": [ + "library" + ], + "parent": { + "id": "noriskclient" + } + } + } +} diff --git a/ffa-server/build.gradle.kts b/ffa-server/build.gradle.kts new file mode 100644 index 0000000..4023508 --- /dev/null +++ b/ffa-server/build.gradle.kts @@ -0,0 +1,100 @@ +val worldEditVersion: String by project + +val includeImplementation: Configuration by configurations.creating { + configurations.implementation.configure { extendsFrom(this@creating) } +} + +dependencies { + api(project(":hero-api", configuration = "namedElements")) + api(project(":datatracker", configuration = "namedElements")) + api(project(":katara", configuration = "namedElements")) + api(project(":aang", configuration = "namedElements")) + api(project(":toph", configuration = "namedElements")) + + modApi(libs.bundles.fabric) + modApi(libs.bundles.silk) + modApi(libs.bundles.nrc) + modApi(libs.bundles.performance) + modApi(libs.owolib) + modApi(libs.npcLibApi) + modApi(libs.npcLibCommon) + modApi(libs.geckolib) + modApi(libs.emoteLib) + modImplementation(libs.bundles.cloudnet) + modCompileOnly(libs.worldedit) + includeImplementation(libs.bundles.mongodb) + includeImplementation(libs.geantyref) + modImplementation(libs.hglabor.database.utils) { + exclude(module = "fabric-api") + exclude(module = "hglabor-utils-events") + } + include(libs.hglabor.database.utils) + //includeImplementation(libs.bundles.hglaborutils) + + modImplementation(files("../libs/npc-lib-fabric-3.0.0-SNAPSHOT.jar")) + // modCompileOnly("com.sk89q.worldedit:worldedit-fabric-mc${worldEditVersion}") // Ändere die Versionsnummer entsprechend der gewünschten Version + + handleIncludes(includeImplementation) +} + +fun DependencyHandlerScope.includeTransitive( + dependencies: Set, + fabricLanguageKotlinDependency: ResolvedDependency?, + checkedDependencies: MutableSet = HashSet() +) { + val minecraftDependencies = listOf( + "slf4j-api", + "commons-logging", + "oshi-core", + "jna", + "jna-platform", + "gson", + "commons-lang3", + "jackson-annotations", + "jackson-core", + "jackson-databind", + ) + + dependencies.forEach { + if (checkedDependencies.contains(it) /*|| it.moduleGroup == "org.jetbrains.kotlin" || it.moduleGroup == "org.jetbrains.kotlinx"*/) return@forEach + + if (it.name.startsWith("net.fabric")) { + checkedDependencies += it + return@forEach + } + + if (it.name.startsWith("net.silkmc")) { + checkedDependencies += it + return@forEach + } + + if (fabricLanguageKotlinDependency?.children?.any { kotlinDep -> kotlinDep.name == it.name } == true) { + println("Skipping -> ${it.name} (already in fabric-language-kotlin)") + } else if (minecraftDependencies.any { dep -> dep == it.moduleName }) { + println("Skipping -> ${it.name} (already in minecraft)") + } else { + include(it.name) + println("Including -> ${it.name}") + } + checkedDependencies += it + + includeTransitive(it.children, fabricLanguageKotlinDependency, checkedDependencies) + } +} + +fun DependencyHandlerScope.implementAndInclude(dep: Any) { + modImplementation(dep) + include(dep) +} + +fun DependencyHandlerScope.handleIncludes(configuration: Configuration) { + includeTransitive( + configuration.resolvedConfiguration.firstLevelModuleDependencies, + configurations.modImplementation.get().resolvedConfiguration.firstLevelModuleDependencies + .firstOrNull() { it.moduleGroup == "net.fabricmc" && it.moduleName == "fabric-language-kotlin" }, + ) +} + +loom { + accessWidenerPath.set(file("src/main/resources/ffa-server.accesswidener")) +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/BarrelBlockEntityMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/BarrelBlockEntityMixin.java new file mode 100644 index 0000000..f12f7fa --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/BarrelBlockEntityMixin.java @@ -0,0 +1,17 @@ +package gg.norisk.ffa.server.mixin; + +import gg.norisk.ffa.server.mechanics.lootdrop.Lootdrop; +import net.minecraft.block.entity.BarrelBlockEntity; +import net.minecraft.entity.player.PlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(BarrelBlockEntity.class) +public class BarrelBlockEntityMixin { + @Inject(method = "onOpen", at = @At("HEAD")) + private void onBarrelOpen(PlayerEntity playerEntity, CallbackInfo ci) { + Lootdrop.Companion.barrelOpened((BarrelBlockEntity) (Object) this, playerEntity); + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/BucketItemMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/BucketItemMixin.java new file mode 100644 index 0000000..70fa218 --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/BucketItemMixin.java @@ -0,0 +1,101 @@ +package gg.norisk.ffa.server.mixin; + +import net.minecraft.advancement.criterion.Criteria; +import net.minecraft.block.BlockState; +import net.minecraft.block.FluidDrainable; +import net.minecraft.block.FluidFillable; +import net.minecraft.block.LeavesBlock; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.fluid.Fluid; +import net.minecraft.fluid.Fluids; +import net.minecraft.item.*; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.stat.Stats; +import net.minecraft.util.Hand; +import net.minecraft.util.TypedActionResult; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.RaycastContext; +import net.minecraft.world.World; +import net.minecraft.world.event.GameEvent; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.*; + + +//Thanks to https://github.com/K0LALA/WaterProofLeaves +@Mixin(BucketItem.class) +public abstract class BucketItemMixin extends Item { + @Shadow @Final private Fluid fluid; + + @Shadow public abstract boolean placeFluid(@Nullable PlayerEntity player, World world, BlockPos pos, @Nullable BlockHitResult hitResult); + + @Shadow public abstract void onEmptied(@Nullable PlayerEntity player, World world, ItemStack stack, BlockPos pos); + + public BucketItemMixin(Settings settings) { + super(settings); + } + + @Unique + private static ItemStack getEmptiedStack(ItemStack stack, PlayerEntity player) { + return !player.isInCreativeMode() ? new ItemStack(Items.BUCKET) : stack; + } + + /** + * @author Kolala + * @reason Avoid placing water in leaves without sneaking + */ + @Overwrite + public TypedActionResult use(World world, PlayerEntity user, Hand hand) { + ItemStack itemStack = user.getStackInHand(hand); + BlockHitResult blockHitResult = raycast( + world, user, this.fluid == Fluids.EMPTY ? RaycastContext.FluidHandling.SOURCE_ONLY : RaycastContext.FluidHandling.NONE + ); + if (blockHitResult.getType() == HitResult.Type.MISS) { + return TypedActionResult.pass(itemStack); + } else if (blockHitResult.getType() != HitResult.Type.BLOCK) { + return TypedActionResult.pass(itemStack); + } else { + BlockPos blockPos = blockHitResult.getBlockPos(); + Direction direction = blockHitResult.getSide(); + BlockPos blockPos2 = blockPos.offset(direction); + if (!world.canPlayerModifyAt(user, blockPos) || !user.canPlaceOn(blockPos2, direction, itemStack)) { + return TypedActionResult.fail(itemStack); + } else if (this.fluid == Fluids.EMPTY) { + BlockState blockState = world.getBlockState(blockPos); + if (blockState.getBlock() instanceof FluidDrainable fluidDrainable) { + ItemStack itemStack2 = fluidDrainable.tryDrainFluid(user, world, blockPos, blockState); + if (!itemStack2.isEmpty()) { + user.incrementStat(Stats.USED.getOrCreateStat(this)); + fluidDrainable.getBucketFillSound().ifPresent(sound -> user.playSound(sound, 1.0F, 1.0F)); + world.emitGameEvent(user, GameEvent.FLUID_PICKUP, blockPos); + ItemStack itemStack3 = ItemUsage.exchangeStack(itemStack, user, itemStack2); + if (!world.isClient) { + Criteria.FILLED_BUCKET.trigger((ServerPlayerEntity)user, itemStack2); + } + + return TypedActionResult.success(itemStack3, world.isClient()); + } + } + + return TypedActionResult.fail(itemStack); + } else { + BlockState blockState = world.getBlockState(blockPos); + BlockPos blockPos3 = blockState.getBlock() instanceof FluidFillable && this.fluid == Fluids.WATER && !(blockState.getBlock() instanceof LeavesBlock && !user.isSneaking()) ? blockPos : blockPos2; + if (this.placeFluid(user, world, blockPos3, blockHitResult)) { + this.onEmptied(user, world, itemStack, blockPos3); + if (user instanceof ServerPlayerEntity) { + Criteria.PLACED_BLOCK.trigger((ServerPlayerEntity)user, blockPos3, itemStack); + } + + user.incrementStat(Stats.USED.getOrCreateStat(this)); + ItemStack itemStack2 = ItemUsage.exchangeStack(itemStack, user, getEmptiedStack(itemStack, user)); + return TypedActionResult.success(itemStack2, world.isClient()); + } else { + return TypedActionResult.fail(itemStack); + } + } + } + } +} \ No newline at end of file diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/CommandManagerMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/CommandManagerMixin.java new file mode 100644 index 0000000..c9f2d45 --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/CommandManagerMixin.java @@ -0,0 +1,19 @@ +package gg.norisk.ffa.server.mixin; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(CommandManager.class) +public abstract class CommandManagerMixin { + @WrapWithCondition( + method = "", + at = @At(value = "INVOKE", target = "Lnet/minecraft/server/command/KillCommand;register(Lcom/mojang/brigadier/CommandDispatcher;)V") + ) + private boolean dontAllowKillCommand(CommandDispatcher commandDispatcher) { + return false; + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/DamageTrackerMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/DamageTrackerMixin.java new file mode 100644 index 0000000..c319879 --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/DamageTrackerMixin.java @@ -0,0 +1,75 @@ +package gg.norisk.ffa.server.mixin; + +import gg.norisk.ffa.server.ext.IDamageTrackerExt; +import gg.norisk.ffa.server.mechanics.CombatTag; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.damage.DamageTracker; +import net.minecraft.entity.damage.DamageTypes; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.Text; +import net.silkmc.silk.core.server.ServerExtensionsKt; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyConstant; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(DamageTracker.class) +public abstract class DamageTrackerMixin implements IDamageTrackerExt { + @Shadow + @Final + private LivingEntity entity; + @Unique + private PlayerEntity lastPlayer; + + @Inject( + method = "onDamage", + at = @At("HEAD"), + cancellable = true + ) + private void dontApplyKillDamage(DamageSource damageSource, float f, CallbackInfo ci) { + PlayerEntity attacker = null; + if (damageSource.getAttacker() instanceof PlayerEntity player && entity != player) { + lastPlayer = player; + attacker = player; + } + //System.out.println("### APPLYING " + damageSource); + if (damageSource.isOf(DamageTypes.GENERIC_KILL)) { + ci.cancel(); + } else if (attacker == null) { + //only player combat should trigger combat logger + ci.cancel(); + } + } + + @Inject(method = "update", at = @At(value = "INVOKE", target = "Ljava/util/List;clear()V")) + private void ffa$updateEnd(CallbackInfo ci) { + lastPlayer = null; + } + + @ModifyConstant(method = "update", constant = @Constant(intValue = 300)) + private int modifyCombatTagTime(int constant) { + return CombatTag.INSTANCE.getTicks(); + } + + @ModifyConstant(method = "update", constant = @Constant(intValue = 100)) + private int modifyCombatTagTime2(int constant) { + return CombatTag.INSTANCE.getTicks(); + } + + @Override + public @Nullable PlayerEntity getFfa_lastPlayer() { + return lastPlayer; + } + + @Override + public void setFfa_lastPlayer(@Nullable PlayerEntity player) { + lastPlayer = player; + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/FallingBlockEntityMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/FallingBlockEntityMixin.java new file mode 100644 index 0000000..ef4ba84 --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/FallingBlockEntityMixin.java @@ -0,0 +1,18 @@ +package gg.norisk.ffa.server.mixin; + +import gg.norisk.ffa.server.mechanics.lootdrop.Lootdrop; +import net.minecraft.block.Block; +import net.minecraft.entity.FallingBlockEntity; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(FallingBlockEntity.class) +public class FallingBlockEntityMixin { + @Inject(method = "onDestroyedOnLanding", at = @At("HEAD")) + private void onDestroyedOnLanding(Block block, BlockPos blockPos, CallbackInfo ci) { + Lootdrop.Companion.fallingBlockLanded((FallingBlockEntity) (Object) this); + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/HungerManagerMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/HungerManagerMixin.java new file mode 100644 index 0000000..b3f7f4f --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/HungerManagerMixin.java @@ -0,0 +1,18 @@ +package gg.norisk.ffa.server.mixin; + +import gg.norisk.ffa.server.mechanics.KitEditor; +import net.minecraft.entity.player.HungerManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(HungerManager.class) +public abstract class HungerManagerMixin { + @ModifyConstant(method = "update", constant = @Constant(floatValue = 1f, ordinal = 0)) + private float injected(float constant) { + if (KitEditor.INSTANCE.isUHC()) { + return constant; + } + return 0.25f; + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/ItemStackMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/ItemStackMixin.java new file mode 100644 index 0000000..9c7dcf1 --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/ItemStackMixin.java @@ -0,0 +1,30 @@ +package gg.norisk.ffa.server.mixin; + +import gg.norisk.ffa.server.mechanics.KitEditor; +import gg.norisk.ffa.server.mechanics.SoupHealing; +import gg.norisk.ffa.server.mechanics.Tracker; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Hand; +import net.minecraft.util.TypedActionResult; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ItemStack.class) +public abstract class ItemStackMixin { + @Inject( + method = "use", + at = @At("HEAD"), + cancellable = true + ) + public void onUse(World world, PlayerEntity playerEntity, Hand hand, CallbackInfoReturnable> cir) { + ItemStack itemStack = (ItemStack) (Object) this; + if (!KitEditor.INSTANCE.isUHC()) { + SoupHealing.INSTANCE.onPotentialSoupUse(playerEntity, itemStack.getItem(), cir, world, hand); + } + Tracker.INSTANCE.onTrackerUse(playerEntity, itemStack, cir, world, hand); + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/MinecraftDedicatedServerMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/MinecraftDedicatedServerMixin.java new file mode 100644 index 0000000..6d95884 --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/MinecraftDedicatedServerMixin.java @@ -0,0 +1,21 @@ +package gg.norisk.ffa.server.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.minecraft.server.dedicated.MinecraftDedicatedServer; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(MinecraftDedicatedServer.class) +public abstract class MinecraftDedicatedServerMixin { + @ModifyReturnValue( + method = "isWorldAllowed", + at = @At("RETURN") + ) + private boolean disableNether(boolean original, World world) { + if (world.getRegistryKey() == World.NETHER) { + return false; + } + return original; + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/MiningToolItemMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/MiningToolItemMixin.java new file mode 100644 index 0000000..291744a --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/MiningToolItemMixin.java @@ -0,0 +1,8 @@ +package gg.norisk.ffa.server.mixin; + +import net.minecraft.item.MiningToolItem; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(MiningToolItem.class) +public abstract class MiningToolItemMixin { +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/PersistentProjectileEntityMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/PersistentProjectileEntityMixin.java new file mode 100644 index 0000000..b037cb0 --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/PersistentProjectileEntityMixin.java @@ -0,0 +1,18 @@ +package gg.norisk.ffa.server.mixin; + +import gg.norisk.ffa.server.mechanics.lootdrop.Lootdrop; +import net.minecraft.entity.projectile.PersistentProjectileEntity; +import net.minecraft.util.hit.EntityHitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PersistentProjectileEntity.class) +public class PersistentProjectileEntityMixin { + + @Inject(method = "onEntityHit", at = @At("HEAD")) + private void onDestroyedOnLanding(EntityHitResult entityHitResult, CallbackInfo ci) { + Lootdrop.Companion.projectileHit((PersistentProjectileEntity) (Object) this, entityHitResult.getEntity()); + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/PlayerEntityMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/PlayerEntityMixin.java new file mode 100644 index 0000000..390cef3 --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/PlayerEntityMixin.java @@ -0,0 +1,71 @@ +package gg.norisk.ffa.server.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import gg.norisk.ffa.server.mechanics.CombatTag; +import gg.norisk.ffa.server.mechanics.KitEditor; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.registry.tag.ItemTags; +import net.minecraft.world.World; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(PlayerEntity.class) +public abstract class PlayerEntityMixin extends LivingEntity implements CombatTag.ICombatPlayer { + @Shadow + @NotNull + public abstract ItemStack getWeaponStack(); + + @Shadow public abstract void remove(RemovalReason reason); + + @Unique + private int ffaCombatTicks; + + protected PlayerEntityMixin(EntityType entityType, World world) { + super(entityType, world); + } + + @ModifyConstant(method = "attack", constant = @Constant(floatValue = 1.5f)) + private float injected(float constant) { + if (KitEditor.INSTANCE.isUHC()) { + return constant; + } + return 1.18f; + } + + @WrapOperation( + method = "attack", + at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerEntity;getAttributeValue(Lnet/minecraft/registry/entry/RegistryEntry;)D", ordinal = 0) + ) + private double axeDamageNerf(PlayerEntity instance, RegistryEntry registryEntry, Operation original) { + var originalValue = original.call(instance, registryEntry); + if (KitEditor.INSTANCE.isUHC()) { + return originalValue; + } else { + if (getWeaponStack().isIn(ItemTags.AXES)) { + return originalValue / 4; + } else { + return originalValue; + } + } + } + + @Override + public int getFfa_combatTicks() { + return ffaCombatTicks; + } + + @Override + public void setFfa_combatTicks(int i) { + this.ffaCombatTicks = i; + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/ServerPlayNetworkHandlerMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 0000000..2855d1c --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/ServerPlayNetworkHandlerMixin.java @@ -0,0 +1,62 @@ +package gg.norisk.ffa.server.mixin; + +import gg.norisk.ffa.server.world.WorldManager; +import net.minecraft.network.packet.c2s.play.ClickSlotC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.silkmc.silk.core.Silk; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayNetworkHandler.class) +public abstract class ServerPlayNetworkHandlerMixin { + @Shadow + public ServerPlayerEntity player; + + @Inject(method = "onPlayerAction", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/NetworkThreadUtils;forceMainThread(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;Lnet/minecraft/server/world/ServerWorld;)V", shift = At.Shift.AFTER), cancellable = true) + private void cancelOnPlayerAction(PlayerActionC2SPacket playerActionC2SPacket, CallbackInfo ci) { + if (!WorldManager.INSTANCE.isInKitEditorWorld(player)) { + return; + } + switch (playerActionC2SPacket.getAction()) { + case DROP_ITEM, DROP_ALL_ITEMS -> { + updateInv(); + ci.cancel(); + } + } + } + + @Inject(method = "onClickSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/NetworkThreadUtils;forceMainThread(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;Lnet/minecraft/server/world/ServerWorld;)V", shift = At.Shift.AFTER), cancellable = true) + private void cancelOnClickSlot(ClickSlotC2SPacket clickSlotC2SPacket, CallbackInfo ci) { + if (!WorldManager.INSTANCE.isInKitEditorWorld(player)) { + return; + } + if (clickSlotC2SPacket.getActionType() == SlotActionType.THROW) { + updateInv(); + ci.cancel(); + return; + } + if (clickSlotC2SPacket.getSlot() == -999) { + //player.sendMessage(Text.of("Action" + clickSlotC2SPacket.getActionType())); + // player.sendMessage(Text.of("Button" + clickSlotC2SPacket.getButton())); + // player.sendMessage(Text.of("Slot: " + clickSlotC2SPacket.getSlot())); + if (clickSlotC2SPacket.getActionType() == SlotActionType.QUICK_CRAFT) { + return; + } + updateInv(); + ci.cancel(); + } + } + + @Unique + private void updateInv() { + player.getInventory().updateItems(); + Silk.INSTANCE.getServerOrThrow().getPlayerManager().sendPlayerStatus(player); + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/ServerPlayerEntityMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/ServerPlayerEntityMixin.java new file mode 100644 index 0000000..c47d7b8 --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/ServerPlayerEntityMixin.java @@ -0,0 +1,16 @@ +package gg.norisk.ffa.server.mixin; + +import com.mojang.authlib.GameProfile; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(ServerPlayerEntity.class) +public abstract class ServerPlayerEntityMixin extends PlayerEntity { + public ServerPlayerEntityMixin(World world, BlockPos blockPos, float f, GameProfile gameProfile) { + super(world, blockPos, f, gameProfile); + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/SwordItemMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/SwordItemMixin.java new file mode 100644 index 0000000..e35ec48 --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/SwordItemMixin.java @@ -0,0 +1,24 @@ +package gg.norisk.ffa.server.mixin; + +import gg.norisk.ffa.server.mechanics.KitEditor; +import kotlin.random.Random; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.SwordItem; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(SwordItem.class) +public abstract class SwordItemMixin { + @Inject(method = "postDamageEntity", at = @At("HEAD"), cancellable = true) + public void breakReduction(ItemStack stack, LivingEntity target, LivingEntity attacker, CallbackInfo ci) { + if (KitEditor.INSTANCE.isUHC()) { + return; + } + if (Random.Default.nextBoolean()) { + ci.cancel(); + } + } +} \ No newline at end of file diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/WitherSkullBlockMixin.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/WitherSkullBlockMixin.java new file mode 100644 index 0000000..7753bac --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/WitherSkullBlockMixin.java @@ -0,0 +1,18 @@ +package gg.norisk.ffa.server.mixin; + +import net.minecraft.block.WitherSkullBlock; +import net.minecraft.block.entity.SkullBlockEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(WitherSkullBlock.class) +public abstract class WitherSkullBlockMixin { + @Inject(method = "onPlaced(Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/entity/SkullBlockEntity;)V", at = @At("HEAD"), cancellable = true) + private static void cancelWitherSpawning(World world, BlockPos pos, SkullBlockEntity blockEntity, CallbackInfo ci) { + ci.cancel(); + } +} diff --git a/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/accessor/LivingEntityAccessor.java b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/accessor/LivingEntityAccessor.java new file mode 100644 index 0000000..00f665b --- /dev/null +++ b/ffa-server/src/main/java/gg/norisk/ffa/server/mixin/accessor/LivingEntityAccessor.java @@ -0,0 +1,22 @@ +package gg.norisk.ffa.server.mixin.accessor; + +import net.minecraft.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import javax.annotation.Nullable; + +@Mixin(LivingEntity.class) +public interface LivingEntityAccessor { + @Accessor("lastAttackTime") + public int getLastAttackTime(); + + @Accessor("lastAttackTime") + public void setLastAttackTime(int value); + + @Accessor("attacking") + public @Nullable LivingEntity getAttacking(); + + @Accessor("attacking") + public void setAttacking(@Nullable LivingEntity value); +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/FFAServer.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/FFAServer.kt new file mode 100644 index 0000000..5d87933 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/FFAServer.kt @@ -0,0 +1,42 @@ +package gg.norisk.ffa.server + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.ffa.server.command.LootdropCommand +import gg.norisk.ffa.server.command.MeCommand +import gg.norisk.ffa.server.mechanics.Bounty +import gg.norisk.ffa.server.mechanics.CombatTag +import gg.norisk.ffa.server.mechanics.KillManager +import gg.norisk.ffa.server.mechanics.KitEditor +import gg.norisk.ffa.server.selector.SelectorServerManager +import gg.norisk.ffa.server.world.MapPlacer +import gg.norisk.ffa.server.world.WorldManager +import gg.norisk.heroes.common.HeroesManager.isServer +import net.fabricmc.api.ModInitializer +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.util.Identifier +import org.apache.logging.log4j.LogManager + +object FFAServer : ModInitializer { + private const val MOD_ID = "ffa-server" + val logger = LogManager.getLogger(MOD_ID) + fun String.toId(): Identifier = Identifier.of(MOD_ID, this) + + override fun onInitialize() { + if (!isServer) return + SelectorServerManager.initServer() + WorldManager.initServer() + MeCommand.init() + MapPlacer.init() + LootdropCommand.init() + KitEditor.initServer() + Bounty.init() + CombatTag.init() + KillManager.init() + } + + const val FFA_KEY = "hero-ffa" + var PlayerEntity.isFFA: Boolean + get() = this.getSyncedData(FFA_KEY) ?: false + set(value) = this.setSyncedData(FFA_KEY, value) +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/command/LootdropCommand.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/command/LootdropCommand.kt new file mode 100644 index 0000000..ff2c9e3 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/command/LootdropCommand.kt @@ -0,0 +1,18 @@ +package gg.norisk.ffa.server.command + +import gg.norisk.ffa.server.mechanics.lootdrop.Lootdrop +import net.minecraft.server.world.ServerWorld +import net.silkmc.silk.commands.PermissionLevel +import net.silkmc.silk.commands.command + +object LootdropCommand { + fun init() { + command("lootdrop") { + requires { it.playerOrThrow.hasPermissionLevel(PermissionLevel.OWNER.level) } + + runs { + Lootdrop(source.playerOrThrow.world as ServerWorld, source.playerOrThrow.blockPos).drop() + } + } + } +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/command/MeCommand.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/command/MeCommand.kt new file mode 100644 index 0000000..ee22288 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/command/MeCommand.kt @@ -0,0 +1,21 @@ +package gg.norisk.ffa.server.command + +import net.silkmc.silk.commands.command +import net.silkmc.silk.core.text.literalText + +object MeCommand { + fun init() { + command("me") { + argument("action") { action -> + runs { + val player = this.source.playerOrThrow + this.source.playerOrThrow.sendMessage(literalText { + text(player.name) + text(": ") + text("Ich bin ein kompletter Versager") + }) + } + } + } + } +} \ No newline at end of file diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/event/FFAEvents.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/event/FFAEvents.kt new file mode 100644 index 0000000..7568fa7 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/event/FFAEvents.kt @@ -0,0 +1,11 @@ +package gg.norisk.ffa.server.event + +import net.minecraft.entity.Entity +import net.minecraft.entity.damage.DamageSource +import net.silkmc.silk.core.event.Event + +object FFAEvents { + open class EntityKilledOtherEntityEvent(val killer: Entity, val killed: Entity, val source: DamageSource) + + val entityKilledOtherEntityEvent = Event.onlySync() +} \ No newline at end of file diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/ext/IDamageTrackerExt.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/ext/IDamageTrackerExt.kt new file mode 100644 index 0000000..e2a9d34 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/ext/IDamageTrackerExt.kt @@ -0,0 +1,7 @@ +package gg.norisk.ffa.server.ext + +import net.minecraft.entity.player.PlayerEntity + +interface IDamageTrackerExt { + var ffa_lastPlayer: PlayerEntity? +} \ No newline at end of file diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/Bounty.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/Bounty.kt new file mode 100644 index 0000000..c23ee86 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/Bounty.kt @@ -0,0 +1,89 @@ +package gg.norisk.ffa.server.mechanics + +import com.mojang.brigadier.arguments.IntegerArgumentType +import gg.norisk.ffa.server.FFAServer.isFFA +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.heroes.common.player.ffaBounty +import gg.norisk.heroes.server.database.player.PlayerProvider +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents +import net.minecraft.command.argument.EntityArgumentType +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.text.Text +import net.silkmc.silk.commands.command +import net.silkmc.silk.core.server.players +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.broadcastText + +object Bounty { + fun init() { + command("bounty") { + alias("kopfgeld") + argument("player", EntityArgumentType.player()) { + runsAsync { + val player = EntityArgumentType.getPlayer(this, "player") + val source = this.source.playerOrThrow + val ffaPlayer = PlayerProvider.get(player.uuid) + source.sendMessage(Text.translatable("ffa.mechanic.bounty.info", player.name, ffaPlayer.bounty)) + } + argument("bounty", IntegerArgumentType.integer(100)) { bountyToGive -> + runsAsync { + val player = EntityArgumentType.getPlayer(this, "player") + val source = this.source.playerOrThrow + val ffaPlayer = PlayerProvider.get(player.uuid) + val sourceFfaPlayer = PlayerProvider.get(source.uuid) + + println("Player: $sourceFfaPlayer") + + if (bountyToGive() > sourceFfaPlayer.xp) { + source.sendMessage(Text.translatable("ffa.mechanic.bounty.not_enough_xp")) + return@runsAsync + } + + sourceFfaPlayer.xp -= bountyToGive() + source.ffaPlayer = sourceFfaPlayer + + ffaPlayer.bounty += bountyToGive() + player.ffaPlayer = ffaPlayer + + PlayerProvider.save(sourceFfaPlayer) + PlayerProvider.save(ffaPlayer) + + this.source.server.broadcastText(Text.translatable("ffa.mechanic.bounty.placed", source.name, bountyToGive().toString(), player.name)) + } + } + } + } + + ServerTickEvents.END_SERVER_TICK.register { server -> + mcCoroutineTask(sync = false, client = false) { + for (player in server.players) { + if (!player.isFFA) continue + updateBountyScoreboard(player) + } + } + } + } + + suspend fun receiveBounty(receiver: ServerPlayerEntity, target: ServerPlayerEntity) { + val targetDb = PlayerProvider.get(target.uuid) + val receiverDb = PlayerProvider.get(receiver.uuid) + + if (targetDb.bounty > 0) { + val bounty = targetDb.bounty + targetDb.bounty = 0 + receiverDb.xp += bounty + receiver.server.broadcastText(Text.translatable("ffa.mechanic.bounty.claimed", receiver.name, bounty, target.name)) + receiver.ffaPlayer = receiverDb + target.ffaPlayer = targetDb + PlayerProvider.save(receiverDb) + PlayerProvider.save(targetDb) + } + } + + private suspend fun updateBountyScoreboard(player: ServerPlayerEntity) { + val ffaPlayer = PlayerProvider.get(player.uuid) + if (ffaPlayer.bounty != player.ffaBounty) { + player.ffaBounty = ffaPlayer.bounty + } + } +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/CombatTag.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/CombatTag.kt new file mode 100644 index 0000000..5023664 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/CombatTag.kt @@ -0,0 +1,51 @@ +package gg.norisk.ffa.server.mechanics + +import gg.norisk.heroes.common.events.HeroEvents +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents +import net.minecraft.entity.player.PlayerEntity +import net.silkmc.silk.core.kotlin.ticks +import net.silkmc.silk.core.task.mcCoroutineTask + +object CombatTag { + interface ICombatPlayer { + var ffa_combatTicks: Int + } + + var ticks = 15 * 20 + + fun init() { + ServerPlayConnectionEvents.DISCONNECT.register(ServerPlayConnectionEvents.Disconnect { handler, server -> + mcCoroutineTask(sync = true, client = false) { + val player = handler.player + player.kill() + } + }) + HeroEvents.heroDeathEvent.listen { event -> + /*if (event.player.isInCombat()) { + println("LAST DAMAGE: " + event.player.damageTracker.recentDamage) + event.isValidDeath = true + event.player.sendMessage("Du bekommst einen Tod dazugeschrieben weil du in combat warst!".literal) + } else { + event.isValidDeath = false + event.player.sendMessage("Du bekommst keinen Tod dazugeschrieben weil du nicht in combat warst!".literal) + }*/ + } + } + + fun getCombatTimeAsString(value: Int): String { + val builder = StringBuilder() + value.ticks.toComponents { days, hours, minutes, seconds, _ -> + if (days > 0) builder.append(days).append("d ") + if (hours > 0) builder.append(hours).append("h ") + if (minutes > 0) builder.append(minutes).append("m ") + builder.append(seconds).append("s") + } + return builder.toString() + } + + fun PlayerEntity.isInCombat(): Boolean { + val damageTracker = damageTracker + val lastAttackTime = ticks - (age - lastAttackTime) + return damageTracker.hasDamage || lastAttackTime > 0 + } +} \ No newline at end of file diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/KillManager.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/KillManager.kt new file mode 100644 index 0000000..baaefd5 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/KillManager.kt @@ -0,0 +1,201 @@ +package gg.norisk.ffa.server.mechanics + +import gg.norisk.ffa.server.event.FFAEvents +import gg.norisk.ffa.server.ext.IDamageTrackerExt +import gg.norisk.ffa.server.mechanics.CombatTag.isInCombat +import gg.norisk.ffa.server.mixin.accessor.LivingEntityAccessor +import gg.norisk.ffa.server.selector.SelectorServerManager.setSelectorReady +import gg.norisk.heroes.common.events.HeroEvents +import gg.norisk.heroes.common.ffa.experience.ExperienceReason +import gg.norisk.heroes.common.ffa.experience.ExperienceRegistry +import gg.norisk.heroes.common.ffa.experience.addXp +import gg.norisk.heroes.common.player.FFAPlayer +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.heroes.server.database.player.PlayerProvider +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents +import net.minecraft.entity.ItemEntity +import net.minecraft.entity.damage.DamageSource +import net.minecraft.entity.damage.DamageTypes +import net.minecraft.entity.passive.ChickenEntity +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.text.Text +import net.silkmc.silk.commands.PermissionLevel +import net.silkmc.silk.commands.command +import net.silkmc.silk.core.Silk +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.broadcastText +import net.silkmc.silk.core.text.literalText +import java.awt.Color +import kotlin.math.min + +object KillManager { + fun init() { + killCommand() + + ServerLivingEntityEvents.ALLOW_DEATH.register { entity, source, _ -> + val player = entity as? ChickenEntity ?: return@register true + + val attacker = + source.attacker as? ServerPlayerEntity? ?: (player.damageTracker as IDamageTrackerExt).ffa_lastPlayer + if (attacker != null) { + FFAEvents.entityKilledOtherEntityEvent.invoke( + FFAEvents.EntityKilledOtherEntityEvent( + attacker, + player, + source + ) + ) + } + + return@register true + } + + ServerLivingEntityEvents.ALLOW_DEATH.register { entity, source, _ -> + val player = entity as? ServerPlayerEntity ?: return@register true + (player as LivingEntityAccessor).lastAttackTime = -10000 + (player as LivingEntityAccessor).attacking = null + player.damageTracker.hasDamage = false + + val attacker = + source.attacker as? ServerPlayerEntity? ?: (player.damageTracker as IDamageTrackerExt).ffa_lastPlayer + if (attacker != null && attacker != player) { + FFAEvents.entityKilledOtherEntityEvent.invoke( + FFAEvents.EntityKilledOtherEntityEvent( + attacker, + player, + source + ) + ) + } + + (player.damageTracker as IDamageTrackerExt).ffa_lastPlayer = null + player.setSelectorReady() + + return@register false + } + + ServerEntityEvents.ENTITY_LOAD.register { entity, world -> + val item = entity as? ItemEntity? ?: return@register + item.itemAge = 4800 + } + + FFAEvents.entityKilledOtherEntityEvent.listen { event -> + val wasCombatLog = event.source.isOf(DamageTypes.GENERIC_KILL) + val killer = event.killer as? ServerPlayerEntity? + val killed = event.killed as? ServerPlayerEntity? + + killed?.inventory?.dropAll() + + Silk.server?.broadcastText(literalText { + text(event.killer.name) + text(" hat ") { + color = Color.YELLOW.rgb + } + text(event.killed.name) + text(" getötet") { + color = Color.YELLOW.rgb + } + if (wasCombatLog) { + text(" ") + text("[Combat Log]") { + color = Color.RED.rgb + } + } + }) + + mcCoroutineTask(false, false) { + if (killer != null) { + increaseKillsForPlayer(killer) + } + if (killed != null) { + increaseDeathForPlayer(killed, event.source) + } + + if (killer != killed && killer != null && killed != null) { + Bounty.receiveBounty(killer, killed) + } + } + } + } + + private fun provideExtraXpForKillStreak(player: ServerPlayerEntity, ffaPlayer: FFAPlayer) { + val currentKillStreak = ffaPlayer.currentKillStreak + val killStreakXp = min(3000, ExperienceRegistry.KILLED_PLAYER.value * currentKillStreak * 10) + player.addXp(ExperienceReason("kill_streak", killStreakXp)) + } + + private fun provideExtraBountyForKillStreak(player: ServerPlayerEntity, ffaPlayer: FFAPlayer) { + val currentKillStreak = ffaPlayer.currentKillStreak + val bountyXp = when (currentKillStreak) { + 10 -> 1000 + 20 -> 2000 + else -> return + } + + ffaPlayer.bounty += bountyXp + } + + private suspend fun increaseKillsForPlayer(attacker: ServerPlayerEntity) { + val cachedAttacker = PlayerProvider.get(attacker.uuid) + cachedAttacker.kills++ + cachedAttacker.currentKillStreak++ + attacker.ffaPlayer = cachedAttacker + if (cachedAttacker.currentKillStreak.mod(10) == 0 || cachedAttacker.currentKillStreak == 5) { + attacker.server.broadcastText { + text(attacker.name) + text(" hat eine Killstreak von ") { + color = Color.YELLOW.rgb + } + text(cachedAttacker.currentKillStreak.toString()) { + color = Color.RED.rgb + } + } + provideExtraXpForKillStreak(attacker, cachedAttacker) + provideExtraBountyForKillStreak(attacker, cachedAttacker) + } + PlayerProvider.save(cachedAttacker) + attacker.addXp(ExperienceRegistry.KILLED_PLAYER, true) + } + + private suspend fun increaseDeathForPlayer(player: ServerPlayerEntity, source: DamageSource) { + val heroDeathEvent = HeroEvents.HeroDeathEvent(player, true) + player.sendMessage(Text.translatable("ffa.died")) + HeroEvents.heroDeathEvent.invoke(heroDeathEvent) + val cachedEntity = PlayerProvider.get(player.uuid) + if (heroDeathEvent.isValidDeath) { + cachedEntity.deaths++ + if (cachedEntity.currentKillStreak > cachedEntity.highestKillStreak) { + cachedEntity.highestKillStreak = cachedEntity.currentKillStreak + } + if (cachedEntity.currentKillStreak >= 5) { + player.sendMessage(Text.translatable("ffa.mechanic.killstreak.lost", player.name, cachedEntity.currentKillStreak)) + } + cachedEntity.currentKillStreak = 0 + + player.ffaPlayer = cachedEntity + mcCoroutineTask(sync = false, client = false) { + PlayerProvider.save(cachedEntity) + } + } + + if (!source.isOf(DamageTypes.GENERIC_KILL)) { + player.addXp(ExperienceRegistry.PLAYER_DEATH, true) + } + } + + private fun killCommand() { + command("kill") { + alias("spawn") + requiresPermissionLevel(PermissionLevel.NONE) + runs { + val player = this.source.playerOrThrow + if (player.isInCombat()) { + player.kill() + } else { + player.setSelectorReady() + } + } + } + } +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/KitEditor.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/KitEditor.kt new file mode 100644 index 0000000..fb75ee3 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/KitEditor.kt @@ -0,0 +1,163 @@ +package gg.norisk.ffa.server.mechanics + +import com.github.juliarn.npclib.api.Npc +import com.github.juliarn.npclib.api.Position +import com.github.juliarn.npclib.api.event.AttackNpcEvent +import com.github.juliarn.npclib.api.event.ShowNpcEvent +import com.github.juliarn.npclib.api.profile.Profile +import com.github.juliarn.npclib.api.profile.ProfileProperty +import com.github.juliarn.npclib.api.protocol.meta.EntityMetadataFactory +import com.github.juliarn.npclib.common.event.DefaultInteractNpcEvent +import com.github.juliarn.npclib.fabric.FabricPlatform +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.ffa.server.FFAServer.isFFA +import gg.norisk.ffa.server.FFAServer.logger +import gg.norisk.ffa.server.selector.SelectorServerManager.setSelectorReady +import gg.norisk.ffa.server.selector.SelectorServerManager.setSoupItems +import gg.norisk.ffa.server.selector.SelectorServerManager.setUHCItems +import gg.norisk.ffa.server.world.WorldManager.isInKitEditorWorld +import gg.norisk.heroes.common.events.HeroEvents +import gg.norisk.heroes.common.ffa.KitEditorManager +import gg.norisk.heroes.common.ffa.KitEditorManager.onBack +import gg.norisk.heroes.common.ffa.KitEditorManager.onReset +import gg.norisk.heroes.common.ffa.KitEditorManager.world +import gg.norisk.heroes.common.player.InventorySorting +import gg.norisk.heroes.common.player.InventorySorting.Companion.CURRENT_VERSION +import gg.norisk.heroes.common.utils.PlayStyle +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AllowDamage +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.world.World +import net.silkmc.silk.core.event.ServerEvents +import net.silkmc.silk.core.text.literal +import java.util.* + +object KitEditor { + val platform by lazy { FabricPlatform.minestomNpcPlatformBuilder().extension(this).actionController({}).build() } + lateinit var backNpc: Npc + lateinit var resetNpc: Npc + + fun initServer() { + logger.info("Initializing Mode: ${PlayStyle.current}") + HeroEvents.preKitEditorEvent.listen { event -> + if (event.player.isFFA) { + event.isCancelled.set(true) + } + } + ServerLivingEntityEvents.ALLOW_DAMAGE.register(AllowDamage { entity, source, amount -> + if ((entity as? ServerPlayerEntity?)?.isInKitEditorWorld() == true) { + return@AllowDamage false + } + return@AllowDamage true + }) + KitEditorManager.onBack = { + it.setSelectorReady() + } + KitEditorManager.resetInventory = { + handleKit(it, PlayStyle.current) + } + + ServerEvents.postStop.listen { event -> + world = null + } + + ServerEvents.postStart.listen { event -> + if (world != null) { + spawnNpcs() + registerNpcEvents() + } + } + } + + fun isUHC(): Boolean { + return PlayStyle.current == PlayStyle.UHC + } + + fun handleKit(player: PlayerEntity, mode: PlayStyle = PlayStyle.current) { + when (PlayStyle.current) { + PlayStyle.SOUP -> handleSoupKit(player) + PlayStyle.UHC -> handleUHCKit(player) + } + } + + fun handleSoupKit(player: PlayerEntity) { + player.inventory.clear() + player.setSoupItems() + } + + fun handleUHCKit(player: PlayerEntity) { + player.inventory.clear() + player.setUHCItems() + } + + + private fun registerNpcEvents() { + val eventManager = platform.eventManager() + eventManager.registerEventHandler(ShowNpcEvent.Post::class.java) { showEvent: ShowNpcEvent.Post -> + val npc = showEvent.npc() + val player = showEvent.player() + + npc.changeMetadata(EntityMetadataFactory.skinLayerMetaFactory(), true).schedule(player) + } + eventManager.registerEventHandler(DefaultInteractNpcEvent::class.java) { showEvent: DefaultInteractNpcEvent -> + val npc = showEvent.npc() + val player = showEvent.player() + + if (npc.entityId() == resetNpc.entityId()) { + onReset(player) + } else if (npc.entityId() == backNpc.entityId()) { + onBack(player) + } + } + eventManager.registerEventHandler(AttackNpcEvent::class.java) { showEvent: AttackNpcEvent -> + val npc = showEvent.npc() + val player = showEvent.player() + + if (npc.entityId() == resetNpc.entityId()) { + onReset(player) + } else if (npc.entityId() == backNpc.entityId()) { + onBack(player) + } + } + } + + private fun spawnNpcs() { + backNpc = platform + .newNpcBuilder() + .flag(Npc.LOOK_AT_PLAYER, true) + .flag(Npc.HIT_WHEN_PLAYER_HITS, true) + .flag(Npc.SNEAK_WHEN_PLAYER_SNEAKS, true) + .position(Position.position(1.5, 90.00, 5.5, "hero-api:kit-editor")) + .profile( + Profile.resolved( + "FFA", UUID.randomUUID(), setOf( + ProfileProperty.property( + "textures", + "eyJ0aW1lc3RhbXAiOjE1ODQ0NjA2NDI0OTEsInByb2ZpbGVJZCI6ImIwZDRiMjhiYzFkNzQ4ODlhZjBlODY2MWNlZTk2YWFiIiwicHJvZmlsZU5hbWUiOiJNaW5lU2tpbl9vcmciLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2Q5MzEzZjM2OTdhMGZmZjk1MzE1NDlmMzNhNjIyZmUxOWY2MTZhN2Q1OTA3MTY2NDY1Y2EzMDYyOGMzYzFjZWEifX19", + "cdjJcFcAQtn6GtdJSkaLrQl2IlzUpkbDSSLl/a6/IGoJWJu7SDjZeXRKSJ55MYo5KZu38dG1dmlxiEhlF9pRfWtxW4+NXm7EI5fpKeoHBfXyxR3wJC5Yujo+9T+5TQkjAc4zGvgSQS4cRlqa231W4T77YLHCmV+E4rOVqvcXBsPomhtwckDwoD+NjfLH+PBcNkgYULgyUKSOvQVgbetgwjqrw8ZXt5LK9KWZsYKJZdUirapKwmXi/ZgD8h8z6i/K/3Qc4URjPTeqPahsr/hN/TWAGtr9TWf+iIgq91H8pau7FEMxuRgqayMlCLJD+JWjgkbK9Z6/HHJp7s7oGznn3MQy4Sj9vytRN0mLb+MsRwZ3ejOTopFfCynr7EdNSANcdJQUKk2/kjHwNSz067PSW4I+nzQA3tbFcohRkdUyDwZPs7Ajc9OadhS8W6AsQTPsNrxpNxf8yoO/vMvcIgwr/0PLI2VHUEWDVaDNUqzGDwHXn8O55ehje1ECFv5e48qFAC50xXrVJjN4Rtkq8OrjTamOSrHnm2PxlJUgthjqu6fxZZ1dBoKzMBlE56mIy9PLm0HjCS08zcQUvsK+IDW4l7ECWi1oRWrhPDt1wXD4AOlOeYln1C+KSlrBfdRNIW8bgx3pAaeI9Dm0qFpWjDZAKT/uxCs0Lwx0nySUYjM3yvo=" + ) + ) + ) + ).buildAndTrack() + resetNpc = platform + .newNpcBuilder() + .flag(Npc.LOOK_AT_PLAYER, true) + .flag(Npc.HIT_WHEN_PLAYER_HITS, true) + .flag(Npc.SNEAK_WHEN_PLAYER_SNEAKS, true) + .position(Position.position(-0.5, 90.00, 5.5, "hero-api:kit-editor")) + .profile( + Profile.resolved( + "RESET", UUID.randomUUID(), setOf( + ProfileProperty.property( + "textures", + "ewogICJ0aW1lc3RhbXAiIDogMTYxNjgzMjkxMTE4NSwKICAicHJvZmlsZUlkIiA6ICI1N2IzZGZiNWY4YTY0OWUyOGI1NDRlNGZmYzYzMjU2ZiIsCiAgInByb2ZpbGVOYW1lIiA6ICJYaWthcm8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZGE5OWIwNWI5YTFkYjRkMjliNWU2NzNkNzdhZTU0YTc3ZWFiNjY4MTg1ODYwMzVjOGEyMDA1YWViODEwNjAyYSIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9", + "TUpkJfSfrwykKlZAOWnURNM17wX5P+S7OCJDQkeeQyLqETA96DzqK25JwMpTeJNFYcSrkDYGM6ba7nPAewhBkB3U8JaI2hdNvOHw0rQk2bxMAfg+B3Tp+VxqQb2YPQL0z8hqrJOKjzjINSkAhI2g2/rYXfNizXjiUn4f1WUejvKzsYrTcGV0TlqJeJeydJ0nVpo9ssLVu5ksr+6mOKElwvVEMgPV+0VdWC5XH3jOVSQUX1rjIW5aS+nf0A90GKu7ENxv0j0Cj03IsrL1ytx+ZguFk2vxywr49i2l5iXAOwo/qO7+3mHyzYkEyl/so2zbo9VTTGkVLJ/bmQPcbBEF0HLxl3v/m0QoGy2x/cMR2BlITtAKRQOO2zSzDLZmScYSFr0aOnGmO1qvQexn6/JLrZrDqqXFsjFTuATVwHnEXiHSb4DJ5kZds6X1Fy/4UdYLxry423sMfXZeg+49+qvfNJlsg4v+gbcPtQIBMoBKEq+wexa0PBnH7WxpJbKQhyyiQG1tzrcmhZmbA8d2eD8zmsGammI+DCQJmF+Cu3J2ftbkjON0hj09Ow4uy56RCbLkwJigbXf8v6vpBSG7QzxIxKvhwiQHeaku4CyQ6VjxzIMGowM5v5O4x7FZZcRQh0N70/GjMnTWDaVK6htQ+OixHNJ14ju10zbkpyrq484ZmRQ=" + ) + ) + ) + ).buildAndTrack() + } +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/Scoreboard.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/Scoreboard.kt new file mode 100644 index 0000000..926f592 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/Scoreboard.kt @@ -0,0 +1,110 @@ +package gg.norisk.ffa.server.mechanics + +import gg.norisk.ffa.server.mechanics.CombatTag.isInCombat +import gg.norisk.heroes.common.player.ffaPlayer +import net.minecraft.server.network.ServerPlayerEntity +import net.silkmc.silk.core.text.literalText +import net.silkmc.silk.game.sideboard.Sideboard +import net.silkmc.silk.game.sideboard.sideboard +import java.awt.Color +import kotlin.time.Duration.Companion.seconds + +object Scoreboard { + fun getScoreboardForPlayer(player: ServerPlayerEntity): Sideboard { + val mainColor = Color(36, 173, 227).rgb + val secondaryColor = Color(150, 198, 207).rgb + return sideboard(literalText { + text("Lunaris-") { + color = mainColor + bold = true + } + text("MC.de") { + bold = true + } + text(" | ") { + color = secondaryColor + } + text("HeroFFA") + }) { + line(literalText(" ") { + strikethrough = true + color = Color.LIGHT_GRAY.rgb + }) + emptyLine() + updatingLine(1.seconds) { + literalText { + text("Kills") { + color = mainColor + } + text(": ") { + color = secondaryColor + } + text(player.ffaPlayer.kills.toString()) + } + } + updatingLine(1.seconds) { + literalText { + text("Deaths") { + color = mainColor + } + text(": ") { + color = secondaryColor + } + text(player.ffaPlayer.deaths.toString()) + } + } + updatingLine(1.seconds) { + literalText { + text("Streak") { + color = mainColor + } + text(": ") { + color = secondaryColor + } + text(player.ffaPlayer.currentKillStreak.toString()) + } + } + updatingLine(1.seconds) { + val lastAttackTime = CombatTag.ticks - (player.age - player.lastAttackTime) + if (player.isInCombat()) { + val inDamage = CombatTag.ticks - (player.age - player.damageTracker.ageOnLastDamage) + literalText { + text("Combat Tag") { + color = Color.RED.rgb + } + text(": ") { + color = secondaryColor + } + text(CombatTag.getCombatTimeAsString(Math.max(lastAttackTime, inDamage))) + } + } else { + literalText { + text("Bounty") { + color = mainColor + } + text(": ") { + color = secondaryColor + } + text(player.ffaPlayer.bounty.toString()) + } + } + } + updatingLine(1.seconds) { + literalText { + text("Xp") { + color = mainColor + } + text(": ") { + color = secondaryColor + } + text(player.ffaPlayer.xp.toString()) + } + } + emptyLine() + line(literalText(" ") { + strikethrough = true + color = Color.LIGHT_GRAY.rgb + }) + } + } +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/SoupHealing.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/SoupHealing.kt new file mode 100644 index 0000000..9b1b753 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/SoupHealing.kt @@ -0,0 +1,73 @@ +package gg.norisk.ffa.server.mechanics + +import gg.norisk.heroes.common.ffa.experience.ExperienceRegistry +import gg.norisk.heroes.common.ffa.experience.addXp +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.util.Hand +import net.minecraft.util.TypedActionResult +import net.minecraft.world.World +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable + + +object SoupHealing { + const val SOUP_HEAL = 7F + fun onPotentialSoupUse( + player: PlayerEntity, item: Item, + cir: CallbackInfoReturnable>, + world: World, + hand: Hand + ) { + val foodData = player.hungerManager + + if (!item.isStew || player.health >= player.maxHealth && !foodData.isNotFull) return + + var consumedSoup = false + + if (player.health < player.maxHealth) { + player.heal(item.soupHealing) + if (item == Items.SUSPICIOUS_STEW) { + //player.addEffect(MobEffectInstance(MobEffects.BLINDNESS, 60, 1)) + //player.addEffect(MobEffectInstance(MobEffects.WEAKNESS, 60, 1)) + } + consumedSoup = true + } else if (foodData.isNotFull) { + foodData.foodLevel += item.restoredFood + consumedSoup = true + } + + if (consumedSoup) { + (player as? ServerPlayerEntity?)?.apply { + this.addXp(ExperienceRegistry.SOUP_EATEN) + } + cir.returnValue = TypedActionResult.pass(ItemStack(Items.BOWL)) + } + } + + private val Item.isStew: Boolean + get() = when (this) { + Items.MUSHROOM_STEW -> true + Items.BEETROOT_SOUP -> true + Items.RABBIT_STEW -> true + Items.SUSPICIOUS_STEW -> true + else -> false + } + + val Item.soupHealing: Float + get() = when (this) { + Items.MUSHROOM_STEW -> 7.0f + Items.BEETROOT_SOUP -> 3.0f // mushroom cow nerf + Items.RABBIT_STEW -> 8.0f // used in perfect kit + Items.SUSPICIOUS_STEW -> 2.0f // spit + else -> 0f + } + + private val Item.restoredFood: Int + get() = 7 // this.foodProperties?.nutrition ?: 0 + + private val Item.restoredSaturation: Float + get() = 3f //this.foodProperties?.saturationModifier ?: 0f +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/Tracker.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/Tracker.kt new file mode 100644 index 0000000..c400f2f --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/Tracker.kt @@ -0,0 +1,80 @@ +package gg.norisk.ffa.server.mechanics + +import gg.norisk.ffa.server.FFAServer.isFFA +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.network.packet.s2c.play.PlayerSpawnPositionS2CPacket +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.util.Hand +import net.minecraft.util.TypedActionResult +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Vec3d +import net.minecraft.world.World +import net.silkmc.silk.core.item.itemStack +import net.silkmc.silk.core.item.setCustomName +import net.silkmc.silk.core.server.players +import net.silkmc.silk.core.text.sendText +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable +import kotlin.math.sqrt + +object Tracker { + val tracker + get() = itemStack(Items.COMPASS) { + setCustomName { + text("Tracker") { + bold = true + italic = false + } + } + } + + + fun onTrackerUse( + playerEntity: PlayerEntity, + stack: ItemStack, + cir: CallbackInfoReturnable>, + world: World, + hand: Hand + ) { + val player = playerEntity as? ServerPlayerEntity ?: return + if (ItemStack.areItemsAndComponentsEqual(tracker, stack)) { + val nearestPlayer = player.nearestPlayerInfo()?.first + if (nearestPlayer != null) { + val distance = player.nearestPlayerInfo()?.second?.toInt() + player.sendText { + text(nearestPlayer.name.string) + text(" ist ") + text(distance.toString()) + text(" Blöcke entfernt") + } + player.networkHandler.sendPacket( + PlayerSpawnPositionS2CPacket( + BlockPos( + nearestPlayer.x.toInt(), + nearestPlayer.y.toInt(), + nearestPlayer.z.toInt() + ), 0.0F + ) + ) + } else { + player.sendText("Es konnte kein Spieler gefunden werden") { + color = 0xFF4B4B + } + } + } + } + + private fun ServerPlayerEntity.nearestPlayerInfo(): Pair? { + val playerDistances: MutableMap = mutableMapOf() + for (player in server.players) { + if (!player.isFFA) continue + if (world != player.world) continue + val distance = sqrt(this.squaredDistanceTo(Vec3d(player.x, this.y, player.z))) + if (distance > 10) { + playerDistances[player] = distance + } + } + return playerDistances.minByOrNull { it.value }?.toPair() + } +} \ No newline at end of file diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/Lootdrop.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/Lootdrop.kt new file mode 100644 index 0000000..037c215 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/Lootdrop.kt @@ -0,0 +1,497 @@ +package gg.norisk.ffa.server.mechanics.lootdrop + +import gg.norisk.ffa.server.mechanics.KitEditor +import gg.norisk.ffa.server.mechanics.lootdrop.loottable.ExperienceLootdropItem +import gg.norisk.ffa.server.mechanics.lootdrop.loottable.ItemStackLootdropItem +import gg.norisk.ffa.server.mechanics.lootdrop.loottable.SoupLootdropLoottable +import gg.norisk.ffa.server.mechanics.lootdrop.loottable.UHCLootdropLoottable +import gg.norisk.heroes.common.ffa.experience.ExperienceReason +import gg.norisk.heroes.common.ffa.experience.addXp +import kotlinx.coroutines.* +import net.minecraft.block.BarrelBlock +import net.minecraft.block.Blocks +import net.minecraft.block.entity.BarrelBlockEntity +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType +import net.minecraft.entity.FallingBlockEntity +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.decoration.DisplayEntity +import net.minecraft.entity.decoration.DisplayEntity.TextDisplayEntity +import net.minecraft.entity.effect.StatusEffectInstance +import net.minecraft.entity.effect.StatusEffects +import net.minecraft.entity.passive.ChickenEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.entity.projectile.FireworkRocketEntity +import net.minecraft.entity.projectile.PersistentProjectileEntity +import net.minecraft.particle.ParticleTypes +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvents +import net.minecraft.text.Text +import net.minecraft.util.math.AffineTransformation +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction +import net.minecraft.util.math.Vec3d +import net.silkmc.silk.core.Silk +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.core.text.literalText +import org.joml.Vector3f +import java.awt.Color +import kotlin.math.cos +import kotlin.math.sin +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +class Lootdrop(private val world: ServerWorld, private val blockPos: BlockPos) { + companion object { + private val mainColor = Color(36, 173, 227).rgb + private val secondaryColor = Color(150, 198, 207).rgb + + private val EXPIRATION_TIME = 3.minutes + private val ITEMS_PER_AIR_DROP = 4..8 + private val BARREL_SLOTS = 0..26 + + private val entityIdLootdropMap = HashMap() + private val posLootdropMap = HashMap() + + private val lootTable = if (KitEditor.isUHC()) UHCLootdropLoottable().init() else SoupLootdropLoottable().init() + + fun fallingBlockLanded(fallingBlock: FallingBlockEntity) { + val lootdrop = entityIdLootdropMap[fallingBlock.id] ?: return + lootdrop.onFallingBlockLanding(fallingBlock) + } + + fun barrelOpened(barrelBlockEntity: BarrelBlockEntity, player: PlayerEntity) { + val lootdrop = posLootdropMap[barrelBlockEntity.pos] ?: return + lootdrop.onBarrelOpen(player) + } + + fun projectileHit(projectile: PersistentProjectileEntity, entity: Entity) { + val lootdrop = entityIdLootdropMap[entity.id] ?: return + lootdrop.onProjectileHit(projectile, entity) + } + } + + private val lootdropCoroutine = CoroutineScope(Dispatchers.IO) + SupervisorJob() + private var state: LootdropState = LootdropState.SPAWNING + + private val allEntities = mutableListOf() + private var barrelEntity = createBarrelEntity() + private var balloonEntity = createBalloon() + private var leashEntities = createLeashChickens() + private var timerTextEntity = createTimerTextEntity() + + private var xpReward = 0 + + private var landingTime: Long? = null + private lateinit var landingPos: BlockPos + + fun drop() { + startFallingAnimation() + } + + private fun end() { + mcCoroutineTask(sync = true, client = false) { + allEntities.toList().forEach(::unregisterEntity) + posLootdropMap.remove(landingPos) + } + } + + private fun startFallingAnimation() { + allEntities.onEach { entity -> + entity.setNoGravity(false) + world.spawnEntity(entity) + } + + leashEntities.first().attachLeash(leashEntities.last(), true) + state = LootdropState.GLIDING + + lootdropCoroutine.launch { + while (state == LootdropState.GLIDING) { + val time = System.currentTimeMillis() / 500.0 + val swayX = sin(time) * 0.05 + val swayZ = cos(time) * 0.05 + + allEntities.forEach { + val a = if (it is ChickenEntity) -0.12 else -0.08 + it.velocity = Vec3d(swayX, a, swayZ) + it.velocityDirty = true + } + + val particleLocation = barrelEntity.blockPos.toCenterPos() + world.spawnParticles( + ParticleTypes.CLOUD, + particleLocation.x, + particleLocation.y, + particleLocation.z, + 1, + 0.1, + 0.1, + 0.1, + 0.05 + ) + + delay(50.milliseconds) + } + } + + lootdropCoroutine.launch { + while (state == LootdropState.GLIDING || state == LootdropState.FREE_FALL) { + barrelEntity.playSound(SoundEvents.ENTITY_PHANTOM_FLAP, 1.3f, 1.4f) + + if (System.currentTimeMillis().milliseconds.inWholeSeconds % 3 == 0L) { + world.server.executeSync { + val firework = FireworkRocketEntity(EntityType.FIREWORK_ROCKET, world) + firework.setPosition(barrelEntity.pos) + world.spawnEntity(firework) + } + } + + delay(1.seconds) + } + } + } + + fun onFallingBlockLanding(fallingBlock: FallingBlockEntity) { + when (fallingBlock.id) { + barrelEntity.id -> onBarrelLanding() + balloonEntity.id -> onBalloonLanding() + } + } + + private fun onBarrelLanding() { + val hardFall = state == LootdropState.FREE_FALL + state = LootdropState.LANDED + landingPos = barrelEntity.blockPos + landingTime = System.currentTimeMillis() + posLootdropMap[landingPos] = this + + playLandingSoundsAndParticles(hardFall) + setBarrelAndContents() + startExpirationTimer() + } + + private fun onBalloonLanding() { + leashEntities.forEach(::unregisterEntity) + if (!balloonEntity.isRemoved) { + balloonEntity.discard() + } + } + + fun onProjectileHit(projectile: PersistentProjectileEntity, entity: Entity) { + if (entity.id == balloonEntity.id) { + state = LootdropState.FREE_FALL + projectile.onLanding() + onBalloonLanding() + } + } + + fun onBarrelOpen(player: PlayerEntity) { + state = LootdropState.OPENED + if (xpReward > 0) { + player.sendMessage(Text.translatable("ffa.mechanic.lootdrop.found_xp", xpReward)) + player.addXp(ExperienceReason("lootdrop_secured", xpReward)) + } + end() + } + + private fun onExpired() { + state = LootdropState.EXPIRED + world.setBlockState(landingPos, Blocks.AIR.defaultState) + end() + } + + private fun playLandingSoundsAndParticles(hardFall: Boolean) { + val landingPos = landingPos + + world.spawnParticles( + ParticleTypes.CAMPFIRE_COSY_SMOKE, + landingPos.x + 0.5, + landingPos.y + 1.5, + landingPos.z + 0.5, + 20, + 0.5, + 1.0, + 0.5, + 0.02 + ) + + if (hardFall) { + world.playSound( + null, + landingPos, + SoundEvents.ENTITY_GENERIC_EXPLODE.value(), + SoundCategory.BLOCKS, + 1.0f, + 1.0f + ) + + world.spawnParticles( + ParticleTypes.EXPLOSION, + landingPos.x + 0.5, + landingPos.y + 1.0, + landingPos.z + 0.5, + 10, + 0.5, + 0.5, + 0.5, + 0.1 + ) + } else { + world.playSound( + null, + landingPos, + SoundEvents.BLOCK_SNOW_FALL, + SoundCategory.BLOCKS, + 0.9f, + 1.2f + ) + } + + lootdropCoroutine.launch { + while (state == LootdropState.LANDED) { + val particleLocation = landingPos.toCenterPos() + world.spawnParticles( + ParticleTypes.ELECTRIC_SPARK, + particleLocation.x, + particleLocation.y, + particleLocation.z, + 10, + 0.25, + 0.25, + 0.25, + 0.005 + ) + delay(50.milliseconds) + } + } + + lootdropCoroutine.launch { + while (state == LootdropState.LANDED) { + val pitch = (3..12).random() / 10f + + world.playSound( + null, + landingPos, + SoundEvents.BLOCK_AMETHYST_BLOCK_CHIME, + SoundCategory.BLOCKS, + 2.5f, + pitch + ) + delay((1500..3000).random().milliseconds) + } + } + } + + private fun setBarrelAndContents() { + world.setBlockState(landingPos, Blocks.BARREL.defaultState.with(BarrelBlock.FACING, Direction.UP)) + val barrel = world.getBlockEntity(landingPos) as? BarrelBlockEntity + val loot = lootTable.generateLoot(ITEMS_PER_AIR_DROP.random()) + + loot.forEach { item -> + val amount = item.amountRange.random() + when (item) { + is ItemStackLootdropItem -> { + barrel?.setStack(BARREL_SLOTS.random(), item.itemStack.copyWithCount(amount)) + } + + is ExperienceLootdropItem -> xpReward += amount + } + } + startShimmerEffect(world, landingPos) + } + + private fun startShimmerEffect(world: ServerWorld, blockPos: BlockPos) { + val entities = mutableListOf() + + fun addSide(facing: Direction) { + val pos = Vec3d(blockPos.x.toDouble(), blockPos.y.toDouble(), blockPos.z.toDouble()) + val displayPos = when (facing) { + Direction.NORTH -> pos.add(0.6, 0.0, -0.0001) + Direction.SOUTH -> pos.add(0.4, 0.0, 1.0001) + Direction.WEST -> pos.add(-0.0001, 0.0, 0.4) + Direction.EAST -> pos.add(1.0001, 0.0, 0.6) + Direction.UP -> pos.add(0.4, 1.0001, 1.0) + Direction.DOWN -> pos.add(0.4, -0.0001, 0.0) + } + val yawRotation = when (facing) { + Direction.NORTH -> 180f + Direction.SOUTH -> 0f + Direction.WEST -> 90f + Direction.EAST -> -90f + else -> 0f + } + val pitchRotation = when (facing) { + Direction.UP -> -90f + Direction.DOWN -> 90f + else -> 0f + } + + val textDisplay = TextDisplayEntity(EntityType.TEXT_DISPLAY, Silk.serverOrThrow.overworld).apply { + setPosition(displayPos.x, displayPos.y, displayPos.z) + yaw = yawRotation + pitch = pitchRotation + + setText(" ".literal) + setBackground(mainColor) + + val scale = Vector3f(8f, 3.625f, 8f) + setTransformation(AffineTransformation(null, null, scale, null)) + } + world.spawnEntity(textDisplay) + entities.add(textDisplay) + registerEntity(textDisplay) + } + + listOf(Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP, Direction.DOWN).forEach { + addSide(it) + } + + CoroutineScope(Dispatchers.IO).launch { + shimmerEffect(entities) + } + } + + private suspend fun shimmerEffect(entities: List) { + var alpha = 0.0f + var increasing = true + val delayTime = 200L + val maxAlpha = 0.66f + val stepSize = 0.01f + while (true) { + entities.forEach { entity -> + alpha = if (increasing) { + alpha + stepSize + } else { + alpha - stepSize + } + + if (alpha >= maxAlpha) { + alpha = maxAlpha + increasing = false + } else if (alpha <= 0.0f) { + alpha = 0.0f + increasing = true + delay(delayTime) + } + val alphaInt = (alpha * 255).toInt() + val rgbaColor = Color(36, 173, 227, alphaInt) + entity.setBackground(rgbaColor.rgb) + } + delay(40) + } + } + + private fun createBarrelEntity(): FallingBlockEntity { + val pos = blockPos.toCenterPos() + return FallingBlockEntity( + world, + pos.x + 0.5, + pos.y + 50.0, + pos.z + 0.5, + Blocks.BARREL.defaultState.with(BarrelBlock.FACING, Direction.UP) + ).apply { + setNoGravity(true) + setDestroyedOnLanding() + registerEntity(this) + isGlowing = true + } + } + + private fun createBalloon(): FallingBlockEntity { + val pos = barrelEntity.pos + return FallingBlockEntity( + world, + pos.x, + pos.y + 3.0, + pos.z, + Blocks.LIGHT_BLUE_CONCRETE.defaultState + ).apply { + setNoGravity(true) + setDestroyedOnLanding() + registerEntity(this) + } + } + + private fun createLeashChickens(): List { + val barrelChicken = ChickenEntity(EntityType.CHICKEN, world).apply { + setPosition(barrelEntity.x, barrelEntity.y - 0.75, barrelEntity.z) + } + + val balloonChicken = ChickenEntity(EntityType.CHICKEN, world).apply { + setPosition(balloonEntity.x, balloonEntity.y, balloonEntity.z) + } + + return listOf(barrelChicken, balloonChicken).onEach { + it.apply { + setNoGravity(true) + isInvisible = true + isSilent = true + isInvulnerable = true + addStatusEffect(StatusEffectInstance(StatusEffects.INVISIBILITY, Int.MAX_VALUE, 0, true, false)) + addStatusEffect(StatusEffectInstance(StatusEffects.SLOWNESS, Int.MAX_VALUE, 200, true, false)) + attributes.getCustomInstance(EntityAttributes.GENERIC_SCALE)?.baseValue = 0.2 + goalSelector.goals.clear() + registerEntity(this) + } + } + } + + private fun createTimerTextEntity(): TextDisplayEntity { + return TextDisplayEntity(EntityType.TEXT_DISPLAY, world).apply { + setBillboardMode(DisplayEntity.BillboardMode.CENTER) + registerEntity(this) + } + } + + private fun startExpirationTimer() { + val pos = landingPos.toCenterPos().add(0.0, 1.0, 0.0) + timerTextEntity.requestTeleport(pos.x, pos.y, pos.z) + world.spawnEntity(timerTextEntity) + + lootdropCoroutine.launch { + while (state == LootdropState.LANDED) { + updateTimerText() + delay(200.milliseconds) + } + } + } + + private fun updateTimerText() { + timerTextEntity.setText(literalText { + text("Lootdrop") { color = mainColor; bold = true } + newLine() + val timeRemaining = (landingTime!! + EXPIRATION_TIME.inWholeMilliseconds - System.currentTimeMillis()) + + if (timeRemaining <= 0 || world.getBlockState(landingPos).block != Blocks.BARREL) { + onExpired() + return + } + + timeRemaining.milliseconds.toComponents { min, sec, _ -> + text { + text(min.toString().padStart(2, '0')) { color = secondaryColor } + text(":") + text(sec.toString().padStart(2, '0')) { color = secondaryColor } + } + } + }) + } + + private fun registerEntity(entity: Entity) { + allEntities.add(entity) + entityIdLootdropMap[entity.id] = this + } + + private fun unregisterEntity(entity: Entity) { + allEntities.remove(entity) + entity.discard() + entityIdLootdropMap.remove(entity.id) + } +} + +enum class LootdropState { + SPAWNING, GLIDING, FREE_FALL, LANDED, OPENED, EXPIRED +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/LootdropManager.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/LootdropManager.kt new file mode 100644 index 0000000..47f27ad --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/LootdropManager.kt @@ -0,0 +1,78 @@ +package gg.norisk.ffa.server.mechanics.lootdrop + +import kotlinx.coroutines.* +import net.minecraft.util.math.BlockPos +import net.minecraft.world.Heightmap +import net.silkmc.silk.core.Silk +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.broadcastText +import kotlin.random.Random +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +object LootdropManager { + private val MAP_DURATION = 30.minutes + private val MAX_LOOTDROP_DELAY = 6.minutes + private val LOOTDROP_COUNT = 6 + private val world by lazy { Silk.serverOrThrow.overworld } + + private var currentJob: Job? = null + + private var lootdropTimes: List = listOf() + private var lootdropIndex: Int = 0 + + fun onArenaReset() { + currentJob?.cancel() + lootdropTimes = generateLootdropTimes().map { it + System.currentTimeMillis() } + lootdropIndex = 0 + + currentJob = CoroutineScope(Dispatchers.IO).launch { + while (isActive) { + if (lootdropIndex >= lootdropTimes.size) return@launch + + if (System.currentTimeMillis() >= lootdropTimes[lootdropIndex]) { + lootdropIndex++ + mcCoroutineTask(sync = true, client = false) { + spawnLootdrop() + } + } + delay(1.seconds) + } + } + } + + private fun spawnLootdrop() { + val pos = findRandomLocation() + Lootdrop(world, pos).drop() + Silk.serverOrThrow.broadcastText("Dropping Lootdrop at ${pos.toShortString()}") + } + + private fun findRandomLocation(): BlockPos { + val worldBorder = world.worldBorder + + val centerX = worldBorder.centerX.toInt() + val centerZ = worldBorder.centerZ.toInt() + val radius = (worldBorder.size / 2).toInt() + + val x = centerX + Random.nextInt(-radius, radius) + val z = centerZ + Random.nextInt(-radius, radius) + return world.getTopPosition(Heightmap.Type.WORLD_SURFACE, BlockPos(x, 0, z)) + } + + private fun generateLootdropTimes(): List { + val lootdropTimes = mutableListOf() + val mapDurationMillis = MAP_DURATION.inWholeMilliseconds + + var nextSpawnTime = Random.nextLong(0, MAX_LOOTDROP_DELAY.inWholeMilliseconds) + for (i in 0 until LOOTDROP_COUNT) { + if (nextSpawnTime >= mapDurationMillis) break // damit keine lootdrops nach reset spawnen (aber ig sollte eh nicht?) + + lootdropTimes.add(nextSpawnTime) + val spawnDelay = Random.nextLong(0, MAX_LOOTDROP_DELAY.inWholeMilliseconds) + nextSpawnTime += spawnDelay + } + + return lootdropTimes + } + +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/loottable/LootdropLoottable.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/loottable/LootdropLoottable.kt new file mode 100644 index 0000000..8aed878 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/loottable/LootdropLoottable.kt @@ -0,0 +1,66 @@ +package gg.norisk.ffa.server.mechanics.lootdrop.loottable + +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import kotlin.random.Random + +abstract class LootdropLoottable { + private val lootTable = mutableListOf() + private var totalWeight = 0.0 + + abstract fun init(): LootdropLoottable + + fun item(itemStack: ItemStack, weight: Double, amountRange: IntRange = 1..1) { + register(ItemStackLootdropItem(itemStack, weight, amountRange)) + } + + fun item(item: Item, weight: Double, amountRange: IntRange = 1..1) { + register(ItemStackLootdropItem(item.defaultStack, weight, amountRange)) + } + + fun exp(weight: Double, amountRange: IntRange) { + register(ExperienceLootdropItem(weight, amountRange)) + } + + private fun register(lootdropItem: LootdropItem) { + if (lootdropItem.weight < 0) { + throw IllegalArgumentException("weight of LootDropItem must be greater than zero") + } + + lootTable.add(lootdropItem) + totalWeight += lootdropItem.weight + } + + fun generateLoot(count: Int): List { + return Array(count) { getRandom() }.mapNotNull { it } + } + + private fun getRandom(): LootdropItem? { + if (lootTable.isEmpty()) return null + + var randomValue = Random.nextDouble(0.0, totalWeight) + for (item in lootTable) { + randomValue -= item.weight + if (randomValue <= 0) { + return item + } + } + return null + } +} + +sealed class LootdropItem { + abstract val weight: Double + abstract val amountRange: IntRange +} + +class ItemStackLootdropItem( + val itemStack: ItemStack, + override val weight: Double, + override val amountRange: IntRange, +) : LootdropItem() + +class ExperienceLootdropItem( + override val weight: Double, + override val amountRange: IntRange, +) : LootdropItem() diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/loottable/SoupLootdropLoottable.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/loottable/SoupLootdropLoottable.kt new file mode 100644 index 0000000..9900a79 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/loottable/SoupLootdropLoottable.kt @@ -0,0 +1,37 @@ +package gg.norisk.ffa.server.mechanics.lootdrop.loottable + +import net.minecraft.item.Items +import net.minecraft.potion.Potions +import net.silkmc.silk.core.item.itemStack +import net.silkmc.silk.core.item.setPotion + +class SoupLootdropLoottable: LootdropLoottable() { + override fun init(): SoupLootdropLoottable { + item(Items.DIAMOND_HELMET, 1.0) + item(Items.DIAMOND_CHESTPLATE, 1.0) + item(Items.DIAMOND_LEGGINGS, 1.0) + item(Items.DIAMOND_BOOTS, 1.0) + item(Items.DIAMOND_SWORD, 1.1) + item(Items.IRON_HELMET, 1.75) + item(Items.IRON_CHESTPLATE, 1.75) + item(Items.IRON_LEGGINGS, 1.75) + item(Items.IRON_BOOTS, 1.75) + item(Items.IRON_SWORD, 2.0) + item(Items.COBWEB, 2.0, 1..6) + item(Items.ENDER_PEARL, 1.5, 1..6) + item(Items.WATER_BUCKET, 1.25) + item(Items.LAVA_BUCKET, 1.25) + item(Items.MUSHROOM_STEW, 2.0, 1..4) + item(Items.RED_MUSHROOM, 2.0, 6..14) + item(Items.BROWN_MUSHROOM, 2.0, 6..14) + item(Items.EXPERIENCE_BOTTLE, 2.0, 1..3) + item(Items.LAPIS_LAZULI, 2.0, 1..12) + item(Items.ANVIL, 0.6) + item(Items.ENCHANTING_TABLE, 0.8) + item(itemStack(Items.SPLASH_POTION) { + setPotion(Potions.SWIFTNESS) + }, 0.8) + exp(1.0, 50..100) + return this + } +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/loottable/UHCLootdropLoottable.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/loottable/UHCLootdropLoottable.kt new file mode 100644 index 0000000..d59d0d9 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/mechanics/lootdrop/loottable/UHCLootdropLoottable.kt @@ -0,0 +1,40 @@ +package gg.norisk.ffa.server.mechanics.lootdrop.loottable + +import gg.norisk.ffa.server.utils.EnchantmentUtils.getEntry +import net.minecraft.enchantment.Enchantments +import net.minecraft.item.Items +import net.silkmc.silk.core.Silk +import net.silkmc.silk.core.item.itemStack + +class UHCLootdropLoottable : LootdropLoottable() { + override fun init(): LootdropLoottable { + item(Items.NETHERITE_HELMET, 1.0) + item(Items.NETHERITE_CHESTPLATE, 1.0) + item(Items.NETHERITE_LEGGINGS, 1.0) + item(Items.NETHERITE_BOOTS, 1.0) + item(Items.NETHERITE_SWORD, 1.1) + item(Items.DIAMOND_HELMET, 0.45) + item(Items.DIAMOND_CHESTPLATE, 0.45) + item(Items.DIAMOND_LEGGINGS, 0.45) + item(Items.DIAMOND_BOOTS, 0.45) + item(Items.DIAMOND_SWORD, 0.33) + item(Items.COBWEB, 2.0, 3..8) + item(Items.ENDER_PEARL, 2.0, 1..6) + item(Items.WATER_BUCKET, 1.3) + item(Items.LAVA_BUCKET, 1.3) + item(Items.EXPERIENCE_BOTTLE, 1.5, 1..5) + item(Items.LAPIS_LAZULI, 2.0, 1..12) + item(Items.ANVIL, 0.3) + item(Items.ENCHANTING_TABLE, 0.8) + item(Items.ARROW, 1.4, 8..24) + item(Items.OAK_LOG, 1.7, 32..64) + item(Items.COBBLESTONE, 1.3, 64..128) + + item(itemStack(Items.IRON_SWORD) { + addEnchantment(Enchantments.FIRE_ASPECT.getEntry(Silk.serverOrThrow.overworld), 1) + }, 0.3) + + exp(1.0, 50..100) + return this + } +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/selector/SelectorServerManager.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/selector/SelectorServerManager.kt new file mode 100644 index 0000000..90b8dcb --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/selector/SelectorServerManager.kt @@ -0,0 +1,187 @@ +package gg.norisk.ffa.server.selector + +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.ffa.server.FFAServer.isFFA +import gg.norisk.ffa.server.mechanics.KitEditor +import gg.norisk.ffa.server.mechanics.Scoreboard +import gg.norisk.ffa.server.mechanics.Tracker +import gg.norisk.ffa.server.mixin.accessor.LivingEntityAccessor +import gg.norisk.ffa.server.world.WorldManager.findSpawnLocation +import gg.norisk.ffa.server.world.WorldManager.getCenter +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.events.HeroEvents +import gg.norisk.heroes.common.hero.HeroManager +import gg.norisk.heroes.common.hero.setHero +import gg.norisk.heroes.common.networking.Networking +import gg.norisk.heroes.common.networking.dto.HeroSelectorPacket +import gg.norisk.heroes.common.player.InventorySorting.Companion.loadInventory +import gg.norisk.heroes.common.player.ffaPlayer +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.enchantment.Enchantment +import net.minecraft.enchantment.Enchantments +import net.minecraft.entity.EquipmentSlot +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.registry.entry.RegistryEntry +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.world.GameMode +import net.minecraft.world.World +import net.silkmc.silk.commands.command +import net.silkmc.silk.core.item.itemStack +import net.silkmc.silk.game.sideboard.Sideboard +import java.util.* + +object SelectorServerManager { + private val scoreboards = mutableMapOf() + + fun initServer() { + HeroEvents.heroSelectEvent.listen { event -> + event.canSelect = true + val player = event.player as? ServerPlayerEntity ?: return@listen + val server = player.server + player.changeGameMode(GameMode.SURVIVAL) + player.isFFA = true + val spawn = server.overworld.findSpawnLocation().toCenterPos() + player.teleport(server.overworld, spawn.x, spawn.y, spawn.z, 0f, 0f) + player.setArenaReady() + } + ServerPlayConnectionEvents.JOIN.register(ServerPlayConnectionEvents.Join { handler, sender, server -> + handler.player.setSelectorReady() + }) + ServerPlayConnectionEvents.DISCONNECT.register(ServerPlayConnectionEvents.Disconnect { handler, player -> + logger.info("REMOVING SCOREBOARD FOR ${handler.player}") + scoreboards.remove(handler.player.uuid) + }) + if (FabricLoader.getInstance().isDevelopmentEnvironment) { + command("ffakit") { + literal("uhc") { + runs { + KitEditor.handleUHCKit(this.source.playerOrThrow) + } + } + literal("soup") { + runs { + KitEditor.handleSoupKit(this.source.playerOrThrow) + } + } + } + } + } + + private fun ServerPlayerEntity.setArenaReady() { + val inventory = this.ffaPlayer.inventorySorting + if (inventory != null) { + this.loadInventory(inventory) + } else { + KitEditor.handleKit(this) + } + if (!KitEditor.isUHC()) { + setSyncedData("duels:OLD_PVP", true) + getAttributeInstance(EntityAttributes.GENERIC_ATTACK_SPEED)?.baseValue = 100.0 + } + hungerManager.foodLevel = 20 + hungerManager.saturationLevel = 5f + clearStatusEffects() + closeHandledScreen() + setExperienceLevel(0) + setExperiencePoints(0) + if (!FabricLoader.getInstance().isDevelopmentEnvironment) { + scoreboards.computeIfAbsent(this.uuid) { Scoreboard.getScoreboardForPlayer(this) }.displayToPlayer(this) + } + (this as LivingEntityAccessor).lastAttackTime = -10000 + } + + fun PlayerEntity.setSoupItems() { + inventory.setStack(0, Items.STONE_SWORD.defaultStack) + repeat(36) { + giveItemStack(Items.MUSHROOM_STEW.defaultStack) + } + inventory.setStack(8, Tracker.tracker) + inventory.setStack(13, ItemStack(Items.BOWL, 8)) + inventory.setStack(14, ItemStack(Items.RED_MUSHROOM, 8)) + inventory.setStack(15, ItemStack(Items.BROWN_MUSHROOM, 8)) + } + + fun PlayerEntity.setUHCItems() { + equipStack(EquipmentSlot.HEAD, itemStack(Items.DIAMOND_HELMET) { + //addEnchantment(Enchantments.PROTECTION.getEntry(world), 1) + }) + equipStack(EquipmentSlot.CHEST, itemStack(Items.DIAMOND_CHESTPLATE) { + //addEnchantment(Enchantments.PROTECTION.getEntry(world), 1) + }) + equipStack(EquipmentSlot.LEGS, itemStack(Items.DIAMOND_LEGGINGS) { + //addEnchantment(Enchantments.PROTECTION.getEntry(world), 1) + }) + equipStack(EquipmentSlot.FEET, itemStack(Items.DIAMOND_BOOTS) { + //addEnchantment(Enchantments.PROTECTION.getEntry(world), 1) + }) + equipStack(EquipmentSlot.OFFHAND, itemStack(Items.SHIELD) { + //addEnchantment(Enchantments.PROTECTION.getEntry(world), 1) + }) + inventory.setStack(0, itemStack(Items.DIAMOND_SWORD) { + addEnchantment(Enchantments.SHARPNESS.getEntry(world), 1) + }) + inventory.setStack(1, itemStack(Items.DIAMOND_AXE) { + addEnchantment(Enchantments.UNBREAKING.getEntry(world), 3) + }) + inventory.setStack(2, itemStack(Items.GOLDEN_APPLE, 6) { + }) + inventory.setStack(3, itemStack(Items.WATER_BUCKET) { + }) + inventory.setStack(29, itemStack(Items.COOKED_BEEF, 16) { + }) + inventory.setStack(30, itemStack(Items.WATER_BUCKET) { + }) + inventory.setStack(4, itemStack(Items.LAVA_BUCKET) { + }) + inventory.setStack(31, itemStack(Items.LAVA_BUCKET) { + }) + inventory.setStack(5, itemStack(Items.COBBLESTONE, 64) { + }) + inventory.setStack(32, itemStack(Items.OAK_PLANKS, 64) { + }) + inventory.setStack(6, itemStack(Items.COBWEB, 8) { + }) + inventory.setStack(7, itemStack(Items.BOW) { + addEnchantment(Enchantments.UNBREAKING.getEntry(world), 3) + addEnchantment(Enchantments.POWER.getEntry(world), 1) + }) + inventory.setStack(8, itemStack(Items.CROSSBOW) { + addEnchantment(Enchantments.UNBREAKING.getEntry(world), 3) + addEnchantment(Enchantments.PIERCING.getEntry(world), 1) + }) + inventory.setStack(17, Tracker.tracker) + inventory.setStack(14, itemStack(Items.DIAMOND_PICKAXE) { + addEnchantment(Enchantments.UNBREAKING.getEntry(world), 3) + addEnchantment(Enchantments.EFFICIENCY.getEntry(world), 1) + }) + inventory.setStack(9, itemStack(Items.ARROW, 16) { + }) + } + + private fun RegistryKey.getEntry(world: World): RegistryEntry { + return world.registryManager.get(RegistryKeys.ENCHANTMENT).getEntry(this.value).get() + } + + fun ServerPlayerEntity.setSelectorReady() { + this.health = this.maxHealth + isFFA = false + changeGameMode(GameMode.SPECTATOR) + closeHandledScreen() + clearStatusEffects() + setHero(null) + val packet = HeroSelectorPacket(HeroManager.registeredHeroes.keys.toList(), true, true) + Networking.s2cHeroSelectorPacket.send( + packet, + this, + ) + val spawn = server.overworld.getCenter().toCenterPos() + this.teleport(server.overworld, spawn.x, spawn.y, spawn.z, 0f, 0f) + scoreboards[uuid]?.hideFromPlayer(this) + } +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/utils/CloudNetManager.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/utils/CloudNetManager.kt new file mode 100644 index 0000000..fa1a41b --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/utils/CloudNetManager.kt @@ -0,0 +1,14 @@ +package gg.norisk.ffa.server.utils + +import eu.cloudnetservice.driver.inject.InjectionLayer +import eu.cloudnetservice.driver.provider.CloudServiceProvider +import eu.cloudnetservice.wrapper.configuration.WrapperConfiguration + +object CloudNetManager { + private val serviceProvider: CloudServiceProvider = InjectionLayer.ext().instance(CloudServiceProvider::class.java) + private val wrapperConfig: WrapperConfiguration = InjectionLayer.ext().instance(WrapperConfiguration::class.java) + + fun stopCloudNetService() { + wrapperConfig.serviceInfoSnapshot().provider().stop() + } +} \ No newline at end of file diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/utils/EnchantmentUtils.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/utils/EnchantmentUtils.kt new file mode 100644 index 0000000..bd333a8 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/utils/EnchantmentUtils.kt @@ -0,0 +1,13 @@ +package gg.norisk.ffa.server.utils + +import net.minecraft.enchantment.Enchantment +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.registry.entry.RegistryEntry +import net.minecraft.world.World + +object EnchantmentUtils { + fun RegistryKey.getEntry(world: World): RegistryEntry { + return world.registryManager.get(RegistryKeys.ENCHANTMENT).getEntry(this.value).get() + } +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/world/MapPlacer.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/world/MapPlacer.kt new file mode 100644 index 0000000..9ea2842 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/world/MapPlacer.kt @@ -0,0 +1,73 @@ +package gg.norisk.ffa.server.world + +import com.sk89q.worldedit.WorldEdit +import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats +import com.sk89q.worldedit.fabric.FabricAdapter +import com.sk89q.worldedit.function.operation.Operation +import com.sk89q.worldedit.function.operation.Operations +import com.sk89q.worldedit.math.BlockVector3 +import com.sk89q.worldedit.session.ClipboardHolder +import gg.norisk.ffa.server.FFAServer.logger +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.world.World +import net.silkmc.silk.commands.command +import java.io.File +import kotlin.math.pow + +object MapPlacer { + val chunkSize = 3 + val mapSize = 512 + + fun init() { + if (FabricLoader.getInstance().isDevelopmentEnvironment) { + command("placeffamap") { + runs { + generateMap(this.source.world) + } + } + } + } + + fun generateMap(world: World) { + val size = chunkSize * mapSize + val file = File(FabricLoader.getInstance().configDir.parent.parent.parent.toFile(), "assets/ffa-13-07-2024.schem") + if (!file.exists()) { + logger.error("${file.name} doesn't exist") + return + } + var counter = 0 + for (chunkX in -size..size step mapSize) { + for (chunkZ in -size..size step mapSize) { + val posX = chunkX + size + val posZ = chunkZ + size + counter++ + logger.info( + "Placing [$counter/${ + (-chunkSize..chunkSize).count().toDouble().pow(2.0).toInt() + }] at ${posX} ${posZ}" + ) + placeSchematic( + world, + file, + posX, + posZ, + 80 + ) + } + } + } + + private fun placeSchematic(world: World, file: File, offsetX: Int, offsetZ: Int, y: Int) { + val clipboard = ClipboardFormats.findByFile(file)?.getReader(file.inputStream())?.read() + WorldEdit.getInstance().editSessionFactory.getEditSession(FabricAdapter.adapt(world), -1) + .use { editSession -> + val operation: Operation = ClipboardHolder(clipboard) + .createPaste(editSession) + .to(BlockVector3.at(offsetX, y, offsetZ)) + .ignoreAirBlocks(true) + .build() + Operations.complete(operation) + logger.info("Placed at $offsetX $offsetZ") + } + } +} diff --git a/ffa-server/src/main/kotlin/gg/norisk/ffa/server/world/WorldManager.kt b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/world/WorldManager.kt new file mode 100644 index 0000000..1107353 --- /dev/null +++ b/ffa-server/src/main/kotlin/gg/norisk/ffa/server/world/WorldManager.kt @@ -0,0 +1,271 @@ +package gg.norisk.ffa.server.world + +import gg.norisk.ffa.server.FFAServer.isFFA +import gg.norisk.ffa.server.FFAServer.logger +import gg.norisk.ffa.server.mechanics.lootdrop.LootdropManager +import gg.norisk.ffa.server.utils.CloudNetManager +import gg.norisk.ffa.server.world.MapPlacer.chunkSize +import gg.norisk.ffa.server.world.MapPlacer.mapSize +import gg.norisk.heroes.common.HeroesManager +import kotlinx.coroutines.Job +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerWorldEvents +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.network.packet.s2c.play.PositionFlag +import net.minecraft.server.MinecraftServer +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.network.SpawnLocating +import net.minecraft.server.world.ServerWorld +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import net.minecraft.world.GameRules +import net.minecraft.world.World +import net.silkmc.silk.commands.PermissionLevel +import net.silkmc.silk.commands.command +import net.silkmc.silk.core.event.Events +import net.silkmc.silk.core.event.Server +import net.silkmc.silk.core.kotlin.ticks +import net.silkmc.silk.core.task.infiniteMcCoroutineTask +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.broadcastText +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.core.text.literalText +import java.time.Duration +import java.time.LocalTime +import java.util.concurrent.atomic.AtomicLong +import kotlin.math.absoluteValue +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toKotlinDuration + +object WorldManager { + var currentPair = Pair(0, 0) + var mapReset = 30 * 60L + var mapResetTask: Job? = null + val usedMaps = mutableSetOf>() + val maxCount get() = (-chunkSize..chunkSize).count() + val counter = AtomicLong(mapReset) + + fun initServer() { + Events.Server.postStart.listen { event -> + //MapPlacer.generateMap(Silk.serverOrThrow.overworld) + } + ServerWorldEvents.LOAD.register(ServerWorldEvents.Load { server, world -> + world.gameRules.get(GameRules.ANNOUNCE_ADVANCEMENTS).set(false, server) + world.gameRules.get(GameRules.DO_WEATHER_CYCLE).set(false, server) + world.gameRules.get(GameRules.DO_DAYLIGHT_CYCLE).set(false, server) + }) + ServerLifecycleEvents.SERVER_STARTED.register { server -> + logger.info("Init Map Reset Cycle...") + usedMaps.clear() + if (!FabricLoader.getInstance().isDevelopmentEnvironment) { + //restartServerTimer(server) + mapResetCycle(server) + setWorldBorder(server.overworld) + LootdropManager.onArenaReset() + } + } + + command("ffa") { + literal("resetmap") { + requires { it.hasPermissionLevel(PermissionLevel.OWNER.level) } + literal("settimer") { + argument("seconds") { seconds -> + suggestSingle { mapReset } + runs { + val sender = this.source + mapReset = seconds() + this.source.server.broadcastText(literalText { + text(HeroesManager.prefix) + text(sender.displayName) + text(" has set the map reset timer to ${mapReset.getTimeAsString()}!") + }) + } + } + } + runs { + val sender = this.source + this.source.server.broadcastText(literalText { + text(HeroesManager.prefix) + text(sender.displayName) + text(" executed map reset!") + }) + mapResetCycle(this.source.server, true) + } + } + } + } + + fun restartServerTimer(server: MinecraftServer) { + val targetTime = LocalTime.of(4, 0) // Zielzeit: 3 Uhr nachts + val now = LocalTime.now() + + // Berechne die Differenz zwischen jetzt und der Zielzeit + val initialDelay = if (now.isBefore(targetTime)) { + Duration.between(now, targetTime) + } else { + Duration.between(now, targetTime.plusHours(24)) + } + + val countdown = Duration.ofMinutes(5) // 5 Minuten Countdown + + // Ziehe den Countdown vom initialen Delay ab, damit der Neustart pünktlich erfolgt + var adjustedDelay = initialDelay.minus(countdown) + if (adjustedDelay.isNegative || adjustedDelay.isZero) { + adjustedDelay = adjustedDelay.plusDays(1) + } + + logger.info("Server Restart in ${adjustedDelay} ${adjustedDelay.seconds.getTimeAsString()}") + + // Starte den Countdown, wenn der berechnete Delay größer als 0 ist + mcCoroutineTask(sync = true, delay = adjustedDelay.toKotlinDuration()) { delay -> + mcCoroutineTask(sync = true, period = 20.ticks, howOften = countdown.seconds) { counter -> + val left = counter.counterDownToZero + if (left.mod(60) == 0 || left <= 5 || left == 10L || left == 20L || left == 30L) { + server.broadcastText("SERVER RESTART IN ${left.getTimeAsString()}") + } + + if (left == 0L) { + restartServer(server) + } + } + } + } + + fun restartServer(server: MinecraftServer) { + server.broadcastText("RESTARTING SERVER") + CloudNetManager.stopCloudNetService() + } + + fun mapResetCycle(server: MinecraftServer, force: Boolean = false) { + currentPair = getFreeMapPos() + if (force) { + server.overworld.players.forEach { player -> + player.teleportToNewMap(currentPair.first, currentPair.second) + } + setWorldBorder(server.overworld) + LootdropManager.onArenaReset() + } + mapResetTask?.cancel() + counter.set(mapReset) + mapResetTask = infiniteMcCoroutineTask(period = 20.ticks, sync = true, client = false) { + val players = server.playerManager.playerList + if (players.isEmpty()) { + return@infiniteMcCoroutineTask + } + //logger.info("Map Reset In: ${counter.get()}") + counter.decrementAndGet() + //if (counter.get() < 300) { + for (player in players) { + if (player.isFFA) { + Random + player.sendMessage("Map Reset ${counter.getTimeAsString()}".literal, true) + } + } + //} + if (counter.get() == 0L) { + usedMaps.add(currentPair) + mapResetCycle(server) + server.overworld.players.forEach { player -> + player.teleportToNewMap(currentPair.first, currentPair.second) + } + setWorldBorder(server.overworld) + LootdropManager.onArenaReset() + } + } + } + + fun PlayerEntity.isInKitEditorWorld(): Boolean { + return this.world.registryKey.value == Identifier.of("hero-api", "kit-editor") + } + + fun Number.getTimeAsString(): String { + val builder = StringBuilder() + this.toInt().seconds.toComponents { days, hours, minutes, seconds, _ -> + if (days > 0) builder.append(days).append("d ") + if (hours > 0) builder.append(hours).append("h ") + if (minutes > 0) builder.append(minutes).append("m ") + builder.append(seconds).append("s") + } + return builder.toString() + } + + fun AtomicLong.getTimeAsString(): String { + return get().getTimeAsString() + } + + fun setWorldBorder(world: World) { + world.worldBorder.setCenter( + (currentPair.first * mapSize).toDouble() + mapSize / 2.0, + (currentPair.second * mapSize).toDouble() + mapSize / 2.0 + ) + world.worldBorder.size = mapSize.toDouble() + } + + fun getFreeMapPos(): Pair { + //val chunkX = Random.nextInt(-512 / mapSize, 3072 / mapSize) + //val chunkZ = Random.nextInt(-512 / mapSize, 3072 / mapSize) + val chunkX = Random.nextInt(-1, (chunkSize * 2)) + val chunkZ = Random.nextInt(-1, (chunkSize * 2)) + //val chunkX = Random.nextInt(-mapSize, mapSize * (chunkSize * 2)) / mapSize + //val chunkZ = Random.nextInt(-mapSize, mapSize * (chunkSize * 2)) / mapSize + + val pair = Pair(chunkX, chunkZ) + + //TODO bypass für volle map sollte aber eig nicht passieren da große map + if (usedMaps.size >= maxCount) { + return pair + } + + return if (usedMaps.contains(pair)) { + getFreeMapPos() + } else { + pair + } + } + + fun ServerPlayerEntity.teleportToNewMap(x: Int, z: Int, mapSize: Int = MapPlacer.mapSize) { + val world = this.serverWorld + + val relativeX = (this.blockPos.x and (mapSize - 1)) + this.x.fractional().absoluteValue + val relativeZ = (this.blockPos.z and (mapSize - 1)) + this.z.fractional().absoluteValue + + //TODO bug dass es dings ist hä also z und x wird manchmal ganz seltsam + + val realCoordinateX = mapSize * x + relativeX + val realCoordinateZ = mapSize * z + relativeZ + this.teleport( + world, + realCoordinateX, + this.y, + realCoordinateZ, + PositionFlag.VALUES, + this.yaw, + this.pitch + ) + } + + fun Double.fractional(): Double { + return this - this.toInt() + } + + fun ServerWorld.getCenter(): BlockPos { + val x = (currentPair.first * mapSize).toDouble() + mapSize / 2.0 + val z = (currentPair.second * mapSize).toDouble() + mapSize / 2.0 + return BlockPos(x.toInt(), 64, z.toInt()) + } + + fun ServerWorld.findSpawnLocation(): BlockPos { + //smaller number -> bigger radius + val startMultiplier = 0.15 + val endMultiplier = startMultiplier * 2 + val xRange = + (currentPair.first * mapSize + (mapSize * startMultiplier).toInt()..currentPair.first * mapSize + (mapSize - mapSize * endMultiplier).toInt()) + val zRange = + (currentPair.second * mapSize + (mapSize * startMultiplier).toInt()..currentPair.second * mapSize + (mapSize - mapSize * endMultiplier).toInt()) + //logger.info("X-Range: $xRange | ${(mapSize * startMultiplier).toInt()} | ${(mapSize - mapSize * endMultiplier).toInt()}") + //logger.info("Z-Range: $zRange | ${(mapSize * startMultiplier).toInt()} | ${(mapSize - mapSize * endMultiplier).toInt()}") + return SpawnLocating.findOverworldSpawn(this, xRange.random(), zRange.random()) ?: this.findSpawnLocation() + } +} diff --git a/ffa-server/src/main/resources/assets/ffa-server/icon.png b/ffa-server/src/main/resources/assets/ffa-server/icon.png new file mode 100644 index 0000000..d2871cf Binary files /dev/null and b/ffa-server/src/main/resources/assets/ffa-server/icon.png differ diff --git a/ffa-server/src/main/resources/fabric.mod.json b/ffa-server/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..b59997b --- /dev/null +++ b/ffa-server/src/main/resources/fabric.mod.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "name": "FFA-Server", + "id": "ffa-server", + "version": "${version}", + "description": "FFA-Server", + "authors": [ + "NoRiskk", + "Freshkenny", + "BestAuto" + ], + "icon": "assets/ffa-server/icon.png", + "license": "ARR", + "environment": "*", + "entrypoints": { + "main": [ + { + "adapter": "kotlin", + "value": "gg.norisk.ffa.server.FFAServer" + } + ] + }, + "mixins": [ + "ffa-server.mixins.json" + ], + "accessWidener": "ffa-server.accesswidener", + "depends": { + } +} diff --git a/ffa-server/src/main/resources/ffa-server.accesswidener b/ffa-server/src/main/resources/ffa-server.accesswidener new file mode 100644 index 0000000..1b006e3 --- /dev/null +++ b/ffa-server/src/main/resources/ffa-server.accesswidener @@ -0,0 +1,25 @@ +accessWidener v1 named + +accessible method net/minecraft/entity/decoration/DisplayEntity setTransformation (Lnet/minecraft/util/math/AffineTransformation;)V +accessible method net/minecraft/entity/decoration/DisplayEntity setBillboardMode (Lnet/minecraft/entity/decoration/DisplayEntity$BillboardMode;)V +accessible method net/minecraft/entity/decoration/DisplayEntity$TextDisplayEntity getText ()Lnet/minecraft/text/Text; +accessible method net/minecraft/entity/decoration/DisplayEntity$TextDisplayEntity setText (Lnet/minecraft/text/Text;)V +accessible method net/minecraft/entity/decoration/DisplayEntity$TextDisplayEntity setBackground (I)V +accessible method net/minecraft/entity/decoration/DisplayEntity$TextDisplayEntity getBackground ()I +accessible method net/minecraft/entity/decoration/DisplayEntity$TextDisplayEntity getDisplayFlags ()B +accessible method net/minecraft/entity/decoration/DisplayEntity$TextDisplayEntity setDisplayFlags (B)V +accessible method net/minecraft/entity/decoration/DisplayEntity$TextDisplayEntity getLineWidth ()I +accessible method net/minecraft/entity/decoration/DisplayEntity$TextDisplayEntity setLineWidth (I)V +accessible method net/minecraft/entity/decoration/DisplayEntity$BlockDisplayEntity setBlockState (Lnet/minecraft/block/BlockState;)V + +accessible method net/minecraft/entity/FallingBlockEntity (Lnet/minecraft/world/World;DDDLnet/minecraft/block/BlockState;)V +accessible field net/minecraft/entity/mob/MobEntity goalSelector Lnet/minecraft/entity/ai/goal/GoalSelector; +accessible class net/minecraft/entity/ai/goal/GoalSelector + +accessible method net/minecraft/server/network/SpawnLocating findOverworldSpawn (Lnet/minecraft/server/world/ServerWorld;II)Lnet/minecraft/util/math/BlockPos; +accessible field net/minecraft/entity/damage/DamageTracker ageOnLastDamage I +accessible field net/minecraft/entity/damage/DamageTracker recentlyAttacked Z +accessible field net/minecraft/entity/damage/DamageTracker hasDamage Z +accessible field net/minecraft/entity/damage/DamageTracker recentDamage Ljava/util/List; + +accessible field net/minecraft/entity/ItemEntity itemAge I diff --git a/ffa-server/src/main/resources/ffa-server.mixins.json b/ffa-server/src/main/resources/ffa-server.mixins.json new file mode 100644 index 0000000..f7c6b65 --- /dev/null +++ b/ffa-server/src/main/resources/ffa-server.mixins.json @@ -0,0 +1,27 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "gg.norisk.ffa.server.mixin", + "compatibilityLevel": "JAVA_21", + "injectors": { + "defaultRequire": 1 + }, + "mixins": [ + "BarrelBlockEntityMixin", + "BucketItemMixin", + "CommandManagerMixin", + "DamageTrackerMixin", + "FallingBlockEntityMixin", + "HungerManagerMixin", + "ItemStackMixin", + "MinecraftDedicatedServerMixin", + "MiningToolItemMixin", + "PersistentProjectileEntityMixin", + "PlayerEntityMixin", + "ServerPlayerEntityMixin", + "ServerPlayNetworkHandlerMixin", + "SwordItemMixin", + "WitherSkullBlockMixin", + "accessor.LivingEntityAccessor" + ] +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..0b5be3e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx16G +org.gradle.parallel=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..f9b24b8 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,152 @@ +[versions] + +# plugins +spotless = "7.0.2" +nexusPublish = "2.0.0" +checkstyleTools = "10.21.1" +fabricLoom = "1.9-SNAPSHOT" + +# fabric +minecraft = "1.21" +yarn-mappings = "1.21+build.9" +fabric_loader = "0.16.10" +fabric-api = "0.102.0+1.21" +fabric-kotlin = "1.13.0+kotlin.2.1.0" + +# Kotlin +kotlin = "2.0.0" +silkMc = "1.10.7" +# Also modrinth version in gradle.properties + +# hglabor +hglaborUtils = "1.21.4-0.0.1" + +# mongodb +mongodb = "5.2.0" + +# animation +playerAnimator = "1.0.2-rc1+1.21" +geckolib = "4.5.6" + + +# general +gson = "2.11.0" +geantyref = "1.3.16" +annotations = "26.0.1" +serialization = "1.6.0" +netty = "4.1.117.Final" + +# platform api versions +sponge = "10.0.0" +minestom = "0d47d97417" +paper = "1.21.4-R0.1-SNAPSHOT" + +# platform extensions +paperLib = "1.0.8" +packetEvents = "2.7.0" +protocolLib = "6845acd89d" + +cloudnet = "4.0.0-RC10" + + +[libraries] + +# fabric +minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } +yarn-mappings = { module = "net.fabricmc:yarn", version.ref = "yarn-mappings" } + +fabric_loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric_loader" } +fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } +fabric-kotlin = { module = "net.fabricmc:fabric-language-kotlin", version.ref = "fabric-kotlin" } + +# general +netty = { group = "io.netty", name = "netty-buffer", version.ref = "netty" } +gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } +annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } +serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" } +geantyref = { group = "io.leangen.geantyref", name = "geantyref", version.ref = "geantyref" } + +# platform api +paper = { group = "io.papermc.paper", name = "paper-api", version.ref = "paper" } +sponge = { group = "org.spongepowered", name = "spongeapi", version.ref = "sponge" } +minestom = { group = "net.minestom", name = "minestom-snapshots", version.ref = "minestom" } + +# platform extensions +paperLib = { group = "io.papermc", name = "paperlib", version.ref = "paperLib" } +protocolLib = { group = "com.github.dmulloy2", name = "ProtocolLib", version.ref = "protocolLib" } +packetEvents = { group = "com.github.retrooper", name = "packetevents-spigot", version.ref = "packetEvents" } + +# silk +silk-bundle = { group = "maven.modrinth", name = "silk", version = "1.11.0" } +silk-commands = { group = "net.silkmc", name = "silk-commands", version.ref = "silkMc" } +silk-core = { group = "net.silkmc", name = "silk-core", version.ref = "silkMc" } +silk-network = { group = "net.silkmc", name = "silk-network", version.ref = "silkMc" } +silk-nbt = { group = "net.silkmc", name = "silk-nbt", version.ref = "silkMc" } +silk-game = { group = "net.silkmc", name = "silk-game", version.ref = "silkMc" } +owolib = { group = "io.wispforest", name = "owo-lib", version = "0.12.15+1.21" } + +# perfomance +sodium = { group = "maven.modrinth", name = "sodium", version = "mc1.21-0.5.11" } +lithium = { group = "maven.modrinth", name = "lithium", version = "mc1.21-0.13.1" } +modmenu = { group = "maven.modrinth", name = "modmenu", version = "11.0.3" } +ferrite-core = { group = "maven.modrinth", name = "ferrite-core", version = "7.0.0-fabric" } +nvidium = { group = "maven.modrinth", name = "nvidium", version = "0.3.1" } + +# npc-lib +npcLibApi = { group = "io.github.juliarn", name = "npc-lib-api", version = "3.0.0-beta10" } +npcLibCommon = { group = "io.github.juliarn", name = "npc-lib-common", version = "3.0.0-beta10" } + +# animation +playerAnimator = { group = "dev.kosmx.player-anim", name = "player-animation-lib-fabric", version.ref = "playerAnimator" } +geckolib = { group = "software.bernie.geckolib", name = "geckolib-fabric-1.21", version.ref = "geckolib" } +emoteLib = { group = "gg.norisk", name = "emote-lib", version = "1.21-1.1.20" } + +# cloudnet +cloudnet-driver = { module = "eu.cloudnetservice.cloudnet:driver", version.ref = "cloudnet" } +cloudnet-bridge = { module = "eu.cloudnetservice.cloudnet:bridge", version.ref = "cloudnet" } +cloudnet-wrapper = { module = "eu.cloudnetservice.cloudnet:wrapper-jvm", version.ref = "cloudnet" } + +# nrc +nrcClient = { group = "gg.norisk", name = "nrc-client", version = "1.0.68+fabric.1.21" } +nrcUi = { group = "gg.norisk", name = "nrc-ui", version = "1.0.85+fabric.1.21" } +nrcCore = { group = "gg.norisk", name = "nrc-core", version = "1.0.30+fabric.1.21" } +nrcZoom = { group = "gg.norisk", name = "noriskclient-zoom", version = "1.21-2.0.2" } +nrcFullbright = { group = "gg.norisk", name = "fullbright", version = "1.21-2.0.0" } +nrcFreelook = { group = "gg.norisk", name = "freelook", version = "1.21-2.0.4" } +nrcNametag = { group = "gg.norisk", name = "nametags", version = "1.21-2.0.0" } + +# mongodb +mongodb-core = { group = "org.mongodb", name = "mongodb-driver-core", version.ref = "mongodb" } +mongodb-kotlin = { group = "org.mongodb", name = "mongodb-driver-kotlin-coroutine", version.ref = "mongodb" } +mongodb-driver = { group = "org.mongodb", name = "mongodb-driver-reactivestreams", version.ref = "mongodb" } +mongodb-bson = { group = "org.mongodb", name = "bson-kotlinx", version.ref = "mongodb" } +reactive-streams = { group = "org.reactivestreams", name = "reactive-streams", version = "1.0.4" } +reactor-core = { group = "io.projectreactor", name = "reactor-core", version = "3.7.2" } + +# hglabor utils +hglabor-database-utils = { group = "de.hglabor", name = "hglabor-utils-database", version.ref = "hglaborUtils" } + +# misc +worldedit = { group = "com.sk89q.worldedit", name = "worldedit-fabric-mc1.21", version = "7.3.4" } + +# dummy version, just here for renovate to detect that there is something to update +checkstyleTools = { group = "com.puppycrawl.tools", name = "checkstyle", version.ref = "checkstyleTools" } + +[bundles] +#silk = ["silk-commands", "silk-core", "silk-network", "silk-game", "silk-nbt"] +silk = ["silk-commands", "silk-core", "silk-network", "silk-game", "silk-nbt"] +performance = ["sodium", "lithium", "modmenu", "ferrite-core", "nvidium"] +fabric = ["fabric-api", "fabric-kotlin", "fabric_loader"] +nrc = ["nrcFreelook", "nrcFullbright", "nrcZoom", "nrcNametag", "nrcClient", "nrcUi", "nrcCore"] +mongodb = ["mongodb-core", "mongodb-kotlin", "mongodb-bson"] +hglaborutils = ["hglabor-database-utils"] +cloudnet = ["cloudnet-bridge", "cloudnet-driver", "cloudnet-wrapper"] + +[plugins] + +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublish" } +fabricLoom = { id = "fabric-loom", version.ref = "fabricLoom" } +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +shadowJar = { id = "com.github.johnrengelman.shadow", version = "7.1.2" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cea7a79 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..f5feea6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hero-api/build.gradle.kts b/hero-api/build.gradle.kts new file mode 100644 index 0000000..f2ca1af --- /dev/null +++ b/hero-api/build.gradle.kts @@ -0,0 +1,19 @@ +loom { + accessWidenerPath.set(file("src/main/resources/hero-api.accesswidener")) +} + +dependencies { + api(project(":datatracker", configuration = "namedElements")) + + modApi(libs.bundles.fabric) + modApi(libs.bundles.silk) + modApi(libs.bundles.performance) + modApi(libs.bundles.mongodb) + modApi(libs.bundles.hglaborutils) { + exclude(module = "fabric-api") + exclude(module = "hglabor-utils-events") + } + modApi(libs.owolib) + modApi(libs.geckolib) + modApi(libs.emoteLib) +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/EntityMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/EntityMixin.java new file mode 100644 index 0000000..80df35e --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/EntityMixin.java @@ -0,0 +1,29 @@ +package gg.norisk.heroes.common.mixin; + +import gg.norisk.heroes.common.events.EntityEvents; +import net.minecraft.entity.Entity; +import net.minecraft.entity.data.TrackedData; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.HashMap; +import java.util.Map; + +@Mixin(Entity.class) +public abstract class EntityMixin { + @Shadow + public abstract World getWorld(); + + @Unique + private final Map syncedValues = new HashMap<>(); + + @Inject(method = "onTrackedDataSet", at = @At("TAIL")) + private void injected(TrackedData trackedData, CallbackInfo ci) { + EntityEvents.INSTANCE.getOnTrackedDataSetEvent().invoke(new EntityEvents.EntityTrackedDataSetEvent((Entity) (Object) this, trackedData)); + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/FeatureFlagsMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/FeatureFlagsMixin.java new file mode 100644 index 0000000..c7feaec --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/FeatureFlagsMixin.java @@ -0,0 +1,18 @@ +package gg.norisk.heroes.common.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import gg.norisk.heroes.common.HeroesManager; +import net.minecraft.resource.featuretoggle.FeatureFlags; +import net.minecraft.resource.featuretoggle.FeatureManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(FeatureFlags.class) +public abstract class FeatureFlagsMixin { + @Inject(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/featuretoggle/FeatureManager$Builder;build()Lnet/minecraft/resource/featuretoggle/FeatureManager;")) + private static void heroapi$featureFlag(CallbackInfo ci, @Local FeatureManager.Builder builder) { + HeroesManager.heroesFlag = builder.addFlag(HeroesManager.INSTANCE.toId("heroes")); + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/LivingEntityMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/LivingEntityMixin.java new file mode 100644 index 0000000..d01529e --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/LivingEntityMixin.java @@ -0,0 +1,41 @@ +package gg.norisk.heroes.common.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import gg.norisk.heroes.common.events.EntityEvents; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntity.class) +public abstract class LivingEntityMixin extends Entity { + @Shadow + public abstract boolean damage(DamageSource source, float amount); + + public LivingEntityMixin(EntityType entityType, World world) { + super(entityType, world); + } + + @Inject(method = "tickMovement", at = @At("HEAD")) + private void tickMovementEvent(CallbackInfo ci) { + EntityEvents.INSTANCE.getLivingEntityTickMovementEvent().invoke(new EntityEvents.LivingEntityEvent((LivingEntity) (Object) this)); + } + + + @ModifyReturnValue(method = "computeFallDamage", at = @At("RETURN")) + private int injected(int original, float fallDistance, float damageMultiplier) { + var event = new EntityEvents.ComputeFallDamageEvent(fallDistance, damageMultiplier, original, (LivingEntity) (Object) this); + EntityEvents.INSTANCE.getComputeFallDamageEvent().invoke(event); + if (event.getFallDamage() != null) { + return event.getFallDamage(); + } else { + return original; + } + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/MinecraftServerAccessor.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/MinecraftServerAccessor.java new file mode 100644 index 0000000..7e237da --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/MinecraftServerAccessor.java @@ -0,0 +1,16 @@ +package gg.norisk.heroes.common.mixin; + +import net.minecraft.registry.RegistryKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(MinecraftServer.class) +public interface MinecraftServerAccessor { + @Accessor("worlds") + public Map, ServerWorld> getLevelsMap(); +} \ No newline at end of file diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/MinecraftServerMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/MinecraftServerMixin.java new file mode 100644 index 0000000..4f0815e --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/MinecraftServerMixin.java @@ -0,0 +1,31 @@ +package gg.norisk.heroes.common.mixin; + +import com.llamalad7.mixinextras.sugar.Local; +import gg.norisk.heroes.common.HeroesManager; +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.resource.DataConfiguration; +import net.minecraft.resource.ResourcePackManager; +import net.minecraft.resource.ResourcePackProfile; +import net.minecraft.server.MinecraftServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Set; + +@Mixin(MinecraftServer.class) +public abstract class MinecraftServerMixin { + @Inject(method = "loadDataPacks(Lnet/minecraft/resource/ResourcePackManager;Lnet/minecraft/resource/DataConfiguration;ZZ)Lnet/minecraft/resource/DataConfiguration;", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;info(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V", remap = false)) + private static void heroapi$forceDataPack(ResourcePackManager resourcePackManager, DataConfiguration dataConfiguration, boolean bl, boolean bl2, CallbackInfoReturnable cir, + @Local Set set, @Local ResourcePackProfile profile + ) { + if (FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER) { + HeroesManager.INSTANCE.getLogger().info("Found Datapack {}", profile.getId()); + if ("heroes".equalsIgnoreCase(profile.getId())) { + set.add(profile.getId()); + } + } + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/PlayerEntityMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/PlayerEntityMixin.java new file mode 100644 index 0000000..ad54c98 --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/PlayerEntityMixin.java @@ -0,0 +1,29 @@ +package gg.norisk.heroes.common.mixin; + +import gg.norisk.heroes.common.hero.IHeroManagerKt; +import gg.norisk.heroes.common.hero.ability.AbstractAbility; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerEntity.class) +public abstract class PlayerEntityMixin extends LivingEntity { + protected PlayerEntityMixin(EntityType entityType, World world) { + super(entityType, world); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void tickInjection(CallbackInfo ci) { + var player = (PlayerEntity) (Object) this; + var hero = IHeroManagerKt.getHero(player); + if (hero == null) return; + for (AbstractAbility ability : hero.getAbilities().values()) { + ability.onTick(player); + } + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/ResourcePackManagerMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/ResourcePackManagerMixin.java new file mode 100644 index 0000000..280d7df --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/ResourcePackManagerMixin.java @@ -0,0 +1,26 @@ +package gg.norisk.heroes.common.mixin; + +import net.minecraft.resource.ResourcePackManager; +import net.minecraft.resource.ResourcePackProvider; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Set; + +@Mixin(value = ResourcePackManager.class, priority = 2000) +public abstract class ResourcePackManagerMixin { + @Mutable + @Shadow + @Final + private Set providers; + + @Inject(method = "", at = @At("RETURN")) + private void heroapi$resourcePack(ResourcePackProvider[] resources, CallbackInfo ci) { + //this.providers.add(new HeroDataPackProvider(new SymlinkFinder(path -> true))); + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/WorldAccessor.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/WorldAccessor.java new file mode 100644 index 0000000..a9f8891 --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/WorldAccessor.java @@ -0,0 +1,14 @@ +package gg.norisk.heroes.common.mixin; + +import net.minecraft.entity.Entity; +import net.minecraft.world.World; +import net.minecraft.world.entity.EntityLookup; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(World.class) +public interface WorldAccessor { + @Invoker("getEntityLookup") + EntityLookup invokeGetEntityLookup(); +} + diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/CameraMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/CameraMixin.java new file mode 100644 index 0000000..2045b45 --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/CameraMixin.java @@ -0,0 +1,47 @@ +package gg.norisk.heroes.common.mixin.client; + +import gg.norisk.heroes.client.events.ClientEvents; +import gg.norisk.heroes.client.renderer.CameraShaker; +import gg.norisk.heroes.client.ui.OrthoCamera; +import net.minecraft.client.render.Camera; +import net.minecraft.entity.Entity; +import net.minecraft.world.BlockView; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.*; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Camera.class) +public abstract class CameraMixin { + @ModifyConstant(method = "update", constant = @Constant(floatValue = 4.0f)) + private float updateInjection(float constant) { + var event = new ClientEvents.CameraClipToSpaceEvent(constant); + ClientEvents.INSTANCE.getCameraClipToSpaceEvent().invoke(event); + return (float) event.getValue(); + } + + @Inject( + method = "update", + at = @At( + // Inject before the call to clipToSpace + value = "INVOKE", + target = "Lnet/minecraft/client/render/Camera;setPos(DDD)V", + shift = At.Shift.BY, + by = 1 + ) + ) + void camerashake$onUpdate(BlockView area, Entity focusedEntity, boolean thirdPerson, boolean inverseView, float tickDelta, CallbackInfo ci) { + double x = CameraShaker.INSTANCE.getAvgX(); + double y = CameraShaker.INSTANCE.getAvgY(); + ((Camera) (Object)this).moveBy((float) .0, (float) y, (float) x); + } + + @ModifyVariable(method = "moveBy", at = @At("HEAD"), index = 1, argsOnly = true) + private float heroapi$moveByHeadX(float value) { + return OrthoCamera.INSTANCE.isEnabled() ? 0.0f : value; + } + + @ModifyVariable(method = "moveBy", at = @At("HEAD"), index = 3, argsOnly = true) + private float heroapi$moveByHeadZ(float value) { + return OrthoCamera.INSTANCE.isEnabled() ? 0.0f : value; + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/ClientPlayerEntityMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/ClientPlayerEntityMixin.java new file mode 100644 index 0000000..f53e63e --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/ClientPlayerEntityMixin.java @@ -0,0 +1,38 @@ +package gg.norisk.heroes.common.mixin.client; + +import com.mojang.authlib.GameProfile; +import gg.norisk.heroes.common.events.AfterTickInputEvent; +import gg.norisk.heroes.common.events.BasicEventsKt; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.input.Input; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.player.PlayerEntity; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPlayerEntity.class) +public abstract class ClientPlayerEntityMixin extends AbstractClientPlayerEntity { + @Shadow + public Input input; + + @Shadow + @Final + protected MinecraftClient client; + + public ClientPlayerEntityMixin(ClientWorld clientWorld, GameProfile gameProfile) { + super(clientWorld, gameProfile); + } + + @Inject(method = "tickMovement", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/input/Input;tick(ZF)V", shift = At.Shift.AFTER)) + public void inputHandle(CallbackInfo ci) { + PlayerEntity player = MinecraftClient.getInstance().player; + if (!MinecraftClient.getInstance().isRunning() || player == null) return; + BasicEventsKt.getAfterTickInputEvent().invoke(new AfterTickInputEvent(this.input)); + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/EntityRenderDispatcherMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/EntityRenderDispatcherMixin.java new file mode 100644 index 0000000..1f5d79e --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/EntityRenderDispatcherMixin.java @@ -0,0 +1,21 @@ +package gg.norisk.heroes.common.mixin.client; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import gg.norisk.heroes.common.events.EntityEvents; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.EntityRenderDispatcher; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(EntityRenderDispatcher.class) +public abstract class EntityRenderDispatcherMixin { + @WrapWithCondition(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/EntityRenderer;render(Lnet/minecraft/entity/Entity;FFLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V")) + private boolean onlyRenderIfAllowed(EntityRenderer targetClass, Entity entity, float f, float g, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i) { + var event = new EntityEvents.EntityRendererEvent(entity, f, g, matrixStack, vertexConsumerProvider, i); + EntityEvents.INSTANCE.getEntityRendererEvent().invoke(event); + return !event.isCancelled().get(); + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/GameOptionsMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/GameOptionsMixin.java new file mode 100644 index 0000000..5cd8f70 --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/GameOptionsMixin.java @@ -0,0 +1,21 @@ +package gg.norisk.heroes.common.mixin.client; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import gg.norisk.heroes.client.ui.OrthoCamera; +import gg.norisk.heroes.client.ui.screen.HeroSelectorScreen; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.GameOptions; +import net.minecraft.client.option.Perspective; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GameOptions.class) +public abstract class GameOptionsMixin { + @ModifyReturnValue(method = "getPerspective", at = @At("RETURN")) + private Perspective heroapi$GetPerspective(Perspective original) { + if (OrthoCamera.INSTANCE.isEnabled()) { + return Perspective.THIRD_PERSON_FRONT; + } + return original; + } +} \ No newline at end of file diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/GameRendererMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/GameRendererMixin.java new file mode 100644 index 0000000..0243974 --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/GameRendererMixin.java @@ -0,0 +1,126 @@ +package gg.norisk.heroes.common.mixin.client; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.systems.VertexSorter; +import gg.norisk.heroes.client.renderer.CameraShaker; +import gg.norisk.heroes.client.ui.OrthoCamera; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.hud.InGameHud; +import net.minecraft.client.render.Camera; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.render.RenderTickCounter; +import org.joml.Matrix4f; +import org.joml.Quaternionf; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GameRenderer.class) +public abstract class GameRendererMixin { + @Shadow + @Final + private MinecraftClient client; + + @Inject( + method = "render", + at = @At("HEAD") + ) + private void heroapi$onRender(RenderTickCounter renderTickCounter, boolean tick, CallbackInfo ci) { + if (!client.skipGameRender && tick && client.world != null) { + CameraShaker.INSTANCE.newFrame(); + } + } + + @WrapWithCondition( + method = "render", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/InGameHud;render(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/client/render/RenderTickCounter;)V") + ) + private boolean heroapi$dontRenderHud(InGameHud instance, DrawContext drawContext, RenderTickCounter renderTickCounter) { + return !OrthoCamera.INSTANCE.isEnabled(); + } + + @WrapWithCondition( + method = "renderWorld", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/GameRenderer;renderHand(Lnet/minecraft/client/render/Camera;FLorg/joml/Matrix4f;)V") + ) + private boolean heroapi$dontRenderHand(GameRenderer instance, Camera camera, float f, Matrix4f matrix4f) { + return !OrthoCamera.INSTANCE.isEnabled(); + } + + + @Inject( + method = "renderHand", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/render/GameRenderer;tiltViewWhenHurt(Lnet/minecraft/client/util/math/MatrixStack;F)V" + ) + ) + private void heroapi$shakeHand(Camera camera, float f, Matrix4f matrix4f, CallbackInfo ci) { + float x = (float) CameraShaker.INSTANCE.getAvgX(); + float y = (float) CameraShaker.INSTANCE.getAvgY(); + + matrix4f.translate(x, -y, (float) .0); // opposite of camera + } + + // TODO keine ahnung es will nxi so wie ich will T_T + @ModifyArg( + method = "renderWorld", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/render/WorldRenderer;setupFrustum(Lnet/minecraft/util/math/Vec3d;Lorg/joml/Matrix4f;Lorg/joml/Matrix4f;)V" + ), + index = 2 + ) + private Matrix4f heroapi$orthoFrustumProjMat(Matrix4f projMat) { + if (OrthoCamera.INSTANCE.isEnabled()) { + return OrthoCamera.INSTANCE.createOrthoMatrix(1.0F, 20.0F); + } + return projMat; + } + + @ModifyArg( + method = "renderWorld", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/render/WorldRenderer;render(Lnet/minecraft/client/render/RenderTickCounter;ZLnet/minecraft/client/render/Camera;Lnet/minecraft/client/render/GameRenderer;Lnet/minecraft/client/render/LightmapTextureManager;Lorg/joml/Matrix4f;Lorg/joml/Matrix4f;)V" + + ), + index = 6 + ) + private Matrix4f heroapi$orthoProjMat(Matrix4f projMat, @Local(argsOnly = true) RenderTickCounter tickCounter) { + if (OrthoCamera.INSTANCE.isEnabled()) { + float tickDelta = tickCounter.getTickDelta(true); + Matrix4f mat = OrthoCamera.INSTANCE.createOrthoMatrix(tickDelta, 0.0F); + RenderSystem.setProjectionMatrix(mat, VertexSorter.BY_Z); + return mat; + } + return projMat; + } + + @ModifyExpressionValue( + method = "renderWorld", + at = @At( + value = "INVOKE", + target = "Lorg/joml/Quaternionf;conjugate(Lorg/joml/Quaternionf;)Lorg/joml/Quaternionf;", + remap = false + ) + ) + private Quaternionf heroapi$modifyRotation(Quaternionf original, @Local(argsOnly = true) RenderTickCounter tickCounter) { + if (!OrthoCamera.INSTANCE.isEnabled()) { + return original; + } + return original.rotationXYZ( + OrthoCamera.INSTANCE.handlePitch(original, tickCounter.getTickDelta(false)), + OrthoCamera.INSTANCE.handleYaw(original, tickCounter.getTickDelta(false)), + 0.0F + ); + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/MouseMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/MouseMixin.java new file mode 100644 index 0000000..dd7b5ba --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/MouseMixin.java @@ -0,0 +1,35 @@ +package gg.norisk.heroes.common.mixin.client; + +import gg.norisk.heroes.client.events.ClientEvents; +import gg.norisk.heroes.common.events.BasicEventsKt; +import gg.norisk.heroes.common.events.MouseScrollEvent; +import net.minecraft.client.Mouse; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Mouse.class) +public abstract class MouseMixin { + @Inject(method = "onMouseScroll", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;getOverlay()Lnet/minecraft/client/gui/screen/Overlay;", shift = At.Shift.BEFORE)) + private void hookMouseScroll(long window, double horizontal, double vertical, CallbackInfo callbackInfo) { + BasicEventsKt.getMouseScrollEvent().invoke(new MouseScrollEvent(window, horizontal, vertical)); + } + + @Inject( + method = "onMouseScroll(JDD)V", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/client/Mouse;eventDeltaVerticalWheel:D", + ordinal = 6 + ), + cancellable = true + ) + private void updateZoom(CallbackInfo info) { + var event = new ClientEvents.PreHotbarScrollEvent(); + ClientEvents.INSTANCE.getPreHotbarScrollEvent().invoke(event); + if (event.isCancelled().get()) { + info.cancel(); + } + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/PlayerEntityRendererMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/PlayerEntityRendererMixin.java new file mode 100644 index 0000000..67dbffd --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/PlayerEntityRendererMixin.java @@ -0,0 +1,83 @@ +package gg.norisk.heroes.common.mixin.client; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import gg.norisk.heroes.client.renderer.SkinUtils; +import gg.norisk.heroes.common.hero.IHeroManagerKt; +import gg.norisk.heroes.common.player.FFAPlayerKt; +import net.minecraft.client.model.ModelPart; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.EntityRendererFactory; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.PlayerEntityRenderer; +import net.minecraft.client.render.entity.model.PlayerEntityModel; +import net.minecraft.client.util.SkinTextures; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.EntityAttachmentType; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(PlayerEntityRenderer.class) +public abstract class PlayerEntityRendererMixin extends LivingEntityRenderer> { + public PlayerEntityRendererMixin(EntityRendererFactory.Context ctx, PlayerEntityModel model, float shadowRadius) { + super(ctx, model, shadowRadius); + } + + @Inject(method = "getTexture(Lnet/minecraft/client/network/AbstractClientPlayerEntity;)Lnet/minecraft/util/Identifier;", at = @At("RETURN"), cancellable = true) + private void getSkinTextureInjection(AbstractClientPlayerEntity player, CallbackInfoReturnable cir) { + var hero = IHeroManagerKt.getHero(player); + if (hero == null) return; + var skin = hero.getInternalCallbacks().getGetSkin(); + if (skin != null) { + cir.setReturnValue(skin.invoke(player)); + } + } + + @ModifyReturnValue( + method = "getTexture(Lnet/minecraft/client/network/AbstractClientPlayerEntity;)Lnet/minecraft/util/Identifier;", + at = @At("RETURN") + ) + private Identifier redirectHeroSkin(Identifier original, AbstractClientPlayerEntity abstractClientPlayerEntity) { + return SkinUtils.INSTANCE.redirectCombinedSkin(original, abstractClientPlayerEntity); + } + + @Inject(method = "renderLabelIfPresent(Lnet/minecraft/client/network/AbstractClientPlayerEntity;Lnet/minecraft/text/Text;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;IF)V", at = @At(value = "HEAD")) + private void heroapi$renderBounty(AbstractClientPlayerEntity abstractClientPlayerEntity, Text text, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, float f, CallbackInfo ci) { + renderBounty(this.dispatcher.getSquaredDistanceToCamera(abstractClientPlayerEntity), abstractClientPlayerEntity, matrixStack, vertexConsumerProvider, i, f); + } + + @Unique + private void renderBounty(double d, AbstractClientPlayerEntity abstractClientPlayerEntity, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, float f) { + if (d < (double) 2000.0F) { + int bounty = FFAPlayerKt.getFfaBounty(abstractClientPlayerEntity); + if (bounty > 0) { + Vec3d vec3d = abstractClientPlayerEntity.getAttachments().getPointNullable(EntityAttachmentType.NAME_TAG, 0, abstractClientPlayerEntity.getYaw(f)); + if (vec3d != null) { + matrixStack.push(); + matrixStack.scale(0.5f, 0.5f, 0.5f); + matrixStack.translate(vec3d.x, vec3d.y + 0.125f, vec3d.z); + super.renderLabelIfPresent(abstractClientPlayerEntity, Text.empty().append("Bounty: ").append(String.valueOf(bounty)), matrixStack, vertexConsumerProvider, i, f); + matrixStack.pop(); + matrixStack.translate(0.0F, 0.075F, 0.0F); + } + } + } + } + + @WrapOperation( + method = "renderArm", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/SkinTextures;texture()Lnet/minecraft/util/Identifier;") + ) + private Identifier heroapi$redirectRenderArmSkinTexture(SkinTextures instance, Operation original, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, AbstractClientPlayerEntity abstractClientPlayerEntity, ModelPart modelPart, ModelPart modelPart2) { + return SkinUtils.INSTANCE.redirectCombinedSkin(original.call(instance), abstractClientPlayerEntity); + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/PlayerListHudMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/PlayerListHudMixin.java new file mode 100644 index 0000000..a473289 --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/PlayerListHudMixin.java @@ -0,0 +1,33 @@ +package gg.norisk.heroes.common.mixin.client; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import gg.norisk.heroes.client.renderer.SkinUtils; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.hud.PlayerListHud; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.client.util.SkinTextures; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(PlayerListHud.class) +public abstract class PlayerListHudMixin { + + @Shadow + @Final + private MinecraftClient client; + + @WrapOperation( + method = "render", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/SkinTextures;texture()Lnet/minecraft/util/Identifier;") + ) + private Identifier heroapi$redirectCombinedSkin(SkinTextures instance, Operation original, @Local PlayerListEntry playerListEntry) { + var player = client.world == null ? null : client.world.getPlayerByUuid(playerListEntry.getProfile().getId()); + return SkinUtils.INSTANCE.redirectCombinedSkin(original.call(instance), ((AbstractClientPlayerEntity) player)); + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/compat/DefaultChunkRendererMixin.java b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/compat/DefaultChunkRendererMixin.java new file mode 100644 index 0000000..c0fa049 --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/mixin/client/compat/DefaultChunkRendererMixin.java @@ -0,0 +1,28 @@ +package gg.norisk.heroes.common.mixin.client.compat; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import gg.norisk.heroes.client.ui.OrthoCamera; +import gg.norisk.heroes.client.ui.screen.HeroSelectorScreen; +import me.jellysquid.mods.sodium.client.gl.device.RenderDevice; +import me.jellysquid.mods.sodium.client.render.chunk.DefaultChunkRenderer; +import me.jellysquid.mods.sodium.client.render.chunk.ShaderChunkRenderer; +import me.jellysquid.mods.sodium.client.render.chunk.vertex.format.ChunkVertexType; +import net.minecraft.client.MinecraftClient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(value = DefaultChunkRenderer.class, remap = false) +public abstract class DefaultChunkRendererMixin extends ShaderChunkRenderer { + public DefaultChunkRendererMixin(RenderDevice device, ChunkVertexType vertexType) { + super(device, vertexType); + } + + @ModifyExpressionValue( + remap = false, + method = "render", + at = @At(value = "FIELD", target = "Lme/jellysquid/mods/sodium/client/gui/SodiumGameOptions$PerformanceSettings;useBlockFaceCulling:Z", remap = false) + ) + private boolean ffa$blockFaceCulling(boolean original) { + return original && !(OrthoCamera.INSTANCE.isEnabled()); + } +} diff --git a/hero-api/src/main/java/gg/norisk/heroes/common/ui/ScrollContainerV2.java b/hero-api/src/main/java/gg/norisk/heroes/common/ui/ScrollContainerV2.java new file mode 100644 index 0000000..21321c6 --- /dev/null +++ b/hero-api/src/main/java/gg/norisk/heroes/common/ui/ScrollContainerV2.java @@ -0,0 +1,310 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package gg.norisk.heroes.common.ui; + +import gg.norisk.heroes.client.ui.skilltree.AbilitySkillTreeComponent; +import io.wispforest.owo.ui.container.FlowLayout; +import io.wispforest.owo.ui.container.WrappingParentComponent; +import io.wispforest.owo.ui.core.*; +import io.wispforest.owo.ui.util.Delta; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.Function; + +public class ScrollContainerV2 extends WrappingParentComponent { + public static final Identifier VERTICAL_VANILLA_SCROLLBAR_TEXTURE = Identifier.of("owo", "scrollbar/vanilla_vertical"); + public static final Identifier DISABLED_VERTICAL_VANILLA_SCROLLBAR_TEXTURE = Identifier.of("owo", "scrollbar/vanilla_vertical_disabled"); + public static final Identifier HORIZONTAL_VANILLA_SCROLLBAR_TEXTURE = Identifier.of("owo", "scrollbar/vanilla_horizontal_disabled"); + public static final Identifier DISABLED_HORIZONTAL_VANILLA_SCROLLBAR_TEXTURE = Identifier.of("owo", "scrollbar/vanilla_horizontal_disabled"); + public static final Identifier VANILLA_SCROLLBAR_TRACK_TEXTURE = Identifier.of("owo", "scrollbar/track"); + public static final Identifier FLAT_VANILLA_SCROLLBAR_TEXTURE = Identifier.of("owo", "scrollbar/vanilla_flat"); + protected double scrollOffsetVertical = (double) 0.0F; + protected double scrollOffsetHorizontal = (double) 0.0F; + protected double currentScrollPositionVertical = (double) 0.0F; + protected double currentScrollPositionHorizontal = (double) 0.0F; + protected int lastScrollPositionVertical = -1; + protected int lastScrollPositionHorizontal = -1; + protected int scrollStep = 0; + protected int fixedScrollbarLength = 0; + protected double lastScrollbarLengthVertical = (double) 0.0F; + protected double lastScrollbarLengthHorizontal = (double) 0.0F; + protected boolean scrollbaring = false; + protected int maxScrollVertical = 0; + protected int maxScrollHorziontal = 0; + protected int childSize = 0; + protected final ScrollDirection verticalDirection = ScrollDirection.VERTICAL; + protected final ScrollDirection horizontalDirection = ScrollDirection.HORIZONTAL; + private boolean init = true; + + public ScrollContainerV2(Sizing horizontalSizing, Sizing verticalSizing, C child) { + super(horizontalSizing, verticalSizing, child); + this.scrollOffsetHorizontal = maxScrollHorziontal - maxScrollHorziontal / 2.0; + this.currentScrollPositionHorizontal = scrollOffsetHorizontal; + } + + protected int determineHorizontalContentSize(Sizing sizing) { + if (this.verticalDirection == ScrollDirection.VERTICAL) { + return super.determineHorizontalContentSize(sizing); + } else { + throw new UnsupportedOperationException("Horizontal ScrollContainer cannot be horizontally content-sized"); + } + } + + protected int determineVerticalContentSize(Sizing sizing) { + if (this.horizontalDirection == ScrollDirection.HORIZONTAL) { + return super.determineVerticalContentSize(sizing); + } else { + throw new UnsupportedOperationException("Vertical ScrollContainer cannot be vertically content-sized"); + } + } + + public void layout(Size space) { + super.layout(space); + this.maxScrollVertical = Math.max(0, (Integer) this.verticalDirection.sizeGetter.apply(this.child) - ((Integer) this.verticalDirection.sizeGetter.apply(this) - (Integer) this.verticalDirection.insetGetter.apply((Insets) this.padding.get()))); + this.maxScrollHorziontal = Math.max(0, (Integer) this.horizontalDirection.sizeGetter.apply(this.child) - ((Integer) this.horizontalDirection.sizeGetter.apply(this) - (Integer) this.horizontalDirection.insetGetter.apply((Insets) this.padding.get()))); + this.scrollOffsetVertical = MathHelper.clamp(this.scrollOffsetVertical, (double) 0.0F, (double) this.maxScrollVertical + (double) 0.5F); + this.scrollOffsetHorizontal = MathHelper.clamp(this.scrollOffsetHorizontal, (double) 0.0F, (double) this.maxScrollHorziontal + (double) 0.5F); + this.childSize = (Integer) this.verticalDirection.sizeGetter.apply(this.child); + this.lastScrollPositionVertical = -1; + this.lastScrollPositionHorizontal = -1; + } + + protected int childMountX() { + return (int) ((double) super.childMountX() - this.verticalDirection.choose(this.currentScrollPositionVertical, (double) 0.0F)); + } + + protected int childMountY() { + return (int) ((double) super.childMountY() - this.verticalDirection.choose((double) 0.0F, this.currentScrollPositionVertical)); + } + + protected void parentUpdate(float delta, int mouseX, int mouseY) { + super.parentUpdate(delta, mouseX, mouseY); + this.currentScrollPositionVertical += Delta.compute(this.currentScrollPositionVertical, this.scrollOffsetVertical, (double) delta * (double) 0.5F); + this.currentScrollPositionHorizontal += Delta.compute(this.currentScrollPositionHorizontal, this.scrollOffsetHorizontal, (double) delta * (double) 0.5F); + } + + public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partialTicks, float delta) { + if (init) { + //for centering + init = false; + this.scrollOffsetHorizontal = maxScrollHorziontal - maxScrollHorziontal / 2.0; + this.currentScrollPositionHorizontal = scrollOffsetHorizontal; + } + super.draw(context, mouseX, mouseY, partialTicks, delta); + int effectiveScrollOffsetVertical = this.scrollStep > 0 ? (int) this.scrollOffsetVertical / this.scrollStep * this.scrollStep : (int) this.currentScrollPositionVertical; + if (this.scrollStep > 0 && (double) this.maxScrollVertical - this.scrollOffsetVertical == (double) -1.0F) { + effectiveScrollOffsetVertical = (int) ((double) effectiveScrollOffsetVertical + this.scrollOffsetVertical % (double) this.scrollStep); + } + + int newScrollPositionVertical = this.verticalDirection.coordinateGetter.apply(this) - effectiveScrollOffsetVertical; + if (newScrollPositionVertical != this.lastScrollPositionVertical) { + this.verticalDirection.coordinateSetter.accept(this.child, newScrollPositionVertical + (this.padding.get().top() + this.child.margins().get().top())); + this.lastScrollPositionVertical = newScrollPositionVertical; + } + + //HORIZONTAL + + int effectiveScrollOffsetHorizontal = this.scrollStep > 0 ? (int) this.scrollOffsetHorizontal / this.scrollStep * this.scrollStep : (int) this.currentScrollPositionHorizontal; + if (this.scrollStep > 0 && (double) this.maxScrollHorziontal - this.scrollOffsetHorizontal == (double) -1.0F) { + effectiveScrollOffsetHorizontal = (int) ((double) effectiveScrollOffsetHorizontal + this.scrollOffsetHorizontal % (double) this.scrollStep); + } + + int newScrollPositionHorizontal = this.horizontalDirection.coordinateGetter.apply(this) - effectiveScrollOffsetHorizontal; + if (newScrollPositionHorizontal != this.lastScrollPositionHorizontal) { + this.horizontalDirection.coordinateSetter.accept(this.child, newScrollPositionHorizontal + (this.padding.get().left() + this.child.margins().get().left())); + this.lastScrollPositionHorizontal = newScrollPositionHorizontal; + } + + + context.getMatrices().push(); + double visualOffsetVertical = -(this.currentScrollPositionVertical % (double) 1.0F); + if (visualOffsetVertical > 0.9999999 || visualOffsetVertical < 1.0E-7) { + visualOffsetVertical = (double) 0.0F; + } + double visualOffsetHorizontal = -(this.currentScrollPositionHorizontal % (double) 1.0F); + if (visualOffsetHorizontal > 0.9999999 || visualOffsetHorizontal < 1.0E-7) { + visualOffsetHorizontal = (double) 0.0F; + } + + context.getMatrices().translate(this.horizontalDirection.choose(visualOffsetHorizontal, 0.0F), this.verticalDirection.choose(0.0F, visualOffsetVertical), 0.0F); + this.drawChildren(context, mouseX, mouseY, partialTicks, delta, this.childView); + context.getMatrices().pop(); + + Insets padding = this.padding.get(); + int selfSizeVertical = this.verticalDirection.sizeGetter.apply(this); + int contentSizeVertical = this.verticalDirection.sizeGetter.apply(this) - this.verticalDirection.insetGetter.apply(padding); + this.lastScrollbarLengthVertical = this.fixedScrollbarLength == 0 ? Math.min(Math.floor((float) selfSizeVertical / (float) this.childSize * (float) contentSizeVertical), contentSizeVertical) : (double) this.fixedScrollbarLength; + + int selfSizeHorizontal = this.horizontalDirection.sizeGetter.apply(this); + int contentSizeHorizontal = this.horizontalDirection.sizeGetter.apply(this) - this.horizontalDirection.insetGetter.apply(padding); + this.lastScrollbarLengthHorizontal = this.fixedScrollbarLength == 0 ? Math.min(Math.floor((float) selfSizeHorizontal / (float) this.childSize * (float) contentSizeHorizontal), contentSizeHorizontal) : (double) this.fixedScrollbarLength; + } + + public boolean canFocus(FocusSource source) { + return true; + } + + public boolean onMouseScroll(double mouseX, double mouseY, double amount) { + if (this.child.onMouseScroll((double) this.x + mouseX - (double) this.child.x(), (double) this.y + mouseY - (double) this.child.y(), amount)) { + return true; + } else { + if (this.scrollStep < 1) { + this.scrollByVertical(-amount * (double) 15.0F, false, true); + } else { + this.scrollByVertical(-amount * (double) this.scrollStep, true, true); + } + + return true; + } + } + + public boolean onMouseDown(double mouseX, double mouseY, int button) { + if (this.isInScrollbar((double) this.x + mouseX, (double) this.y + mouseY)) { + super.onMouseDown(mouseX, mouseY, button); + return true; + } else { + return super.onMouseDown(mouseX, mouseY, button); + } + } + + public boolean onMouseDrag(double mouseX, double mouseY, double deltaX, double deltaY, int button) { + if (!this.scrollbaring && !this.isInScrollbar((double) this.x + mouseX, (double) this.y + mouseY)) { + return super.onMouseDrag(mouseX, mouseY, deltaX, deltaY, button); + } else { + double deltaVertical = this.verticalDirection.choose(deltaX, deltaY) * -1; + double selfSizeVertical = this.verticalDirection.sizeGetter.apply(this) - this.verticalDirection.insetGetter.apply(this.padding.get()); + double scalarVertical = (double) this.maxScrollVertical / (selfSizeVertical - this.lastScrollbarLengthVertical); + if (!Double.isFinite(scalarVertical)) { + scalarVertical = 0.0F; + } + + this.scrollByVertical(deltaVertical * scalarVertical, true, false); + + double deltaHorizontal = this.horizontalDirection.choose(deltaX, deltaY) * -1; + double selfSizeHorizontal = this.horizontalDirection.sizeGetter.apply(this) - this.horizontalDirection.insetGetter.apply(this.padding.get()); + double scalarHorizontal = (double) this.maxScrollHorziontal / (selfSizeHorizontal - this.lastScrollbarLengthVertical); + if (!Double.isFinite(scalarHorizontal)) { + scalarHorizontal = 0.0F; + } + + this.scrollByHorizontal(deltaHorizontal * scalarHorizontal, true, false); + this.scrollbaring = true; + return true; + } + } + + public boolean onKeyPress(int keyCode, int scanCode, int modifiers) { + if (keyCode == this.verticalDirection.lessKeycode) { + this.scrollByVertical((double) -10.0F, false, true); + } else if (keyCode == this.verticalDirection.moreKeycode) { + this.scrollByVertical((double) 10.0F, false, true); + } else if (keyCode == 267) { + this.scrollByVertical(this.verticalDirection.choose((double) this.width, (double) this.height) * 0.8, false, true); + } else if (keyCode == 266) { + this.scrollByVertical(this.verticalDirection.choose((double) this.width, (double) this.height) * -0.8, false, true); + } + + return false; + } + + public boolean onMouseUp(double mouseX, double mouseY, int button) { + this.scrollbaring = false; + return true; + } + + public @Nullable Component childAt(int x, int y) { + return this.isInScrollbar((double) x, (double) y) ? this : super.childAt(x, y); + } + + protected void scrollByVertical(double offset, boolean instant, boolean showScrollbar) { + this.scrollOffsetVertical = MathHelper.clamp(this.scrollOffsetVertical + offset, (double) 0.0F, (double) this.maxScrollVertical + (double) 0.5F); + if (instant) { + this.currentScrollPositionVertical = this.scrollOffsetVertical; + } + } + + protected void scrollByHorizontal(double offset, boolean instant, boolean showScrollbar) { + this.scrollOffsetHorizontal = MathHelper.clamp(this.scrollOffsetHorizontal + offset, (double) 0.0F, (double) this.maxScrollHorziontal + (double) 0.5F); + if (instant) { + this.currentScrollPositionHorizontal = this.scrollOffsetHorizontal; + } + } + + protected boolean isInScrollbar(double mouseX, double mouseY) { + return true; + } + + public ScrollContainerV2 scrollTo(Component component) { + this.scrollOffsetVertical = MathHelper.clamp(this.scrollOffsetVertical - (double) (this.y - component.y() + ((Insets) component.margins().get()).top()), (double) 0.0F, (double) this.maxScrollVertical); + this.scrollOffsetHorizontal = MathHelper.clamp(this.scrollOffsetHorizontal - (double) (this.x - component.x() + ((Insets) component.margins().get()).right()) - component.width() * 4 - 28, (double) 0.0F, (double) this.maxScrollHorziontal); + + return this; + } + + public ScrollContainerV2 scrollTo(@Range( + from = 0L, + to = 1L + ) double horizontal, double vertical) { + this.scrollOffsetVertical = (double) this.maxScrollVertical * vertical; + this.scrollOffsetHorizontal = (double) this.maxScrollHorziontal * horizontal; + return this; + } + + public ScrollContainerV2 scrollStep(int scrollStep) { + this.scrollStep = scrollStep; + return this; + } + + public int scrollStep() { + return this.scrollStep; + } + + public ScrollContainerV2 fixedScrollbarLength(int fixedScrollbarLength) { + this.fixedScrollbarLength = fixedScrollbarLength; + return this; + } + + public int fixedScrollbarLength() { + return this.fixedScrollbarLength; + } + + public static enum ScrollDirection { + VERTICAL(Component::height, Component::updateY, Component::y, Insets::vertical, 265, 264), + HORIZONTAL(Component::width, Component::updateX, Component::x, Insets::horizontal, 263, 262); + + public final Function sizeGetter; + public final BiConsumer coordinateSetter; + public final Function, Integer> coordinateGetter; + public final Function insetGetter; + public final int lessKeycode; + public final int moreKeycode; + + private ScrollDirection(Function sizeGetter, BiConsumer coordinateSetter, Function, Integer> coordinateGetter, Function insetGetter, int lessKeycode, int moreKeycode) { + this.sizeGetter = sizeGetter; + this.coordinateSetter = coordinateSetter; + this.coordinateGetter = coordinateGetter; + this.insetGetter = insetGetter; + this.lessKeycode = lessKeycode; + this.moreKeycode = moreKeycode; + } + + public double choose(double horizontal, double vertical) { + double var10000; + switch (this.ordinal()) { + case 0 -> var10000 = vertical; + case 1 -> var10000 = horizontal; + default -> throw new MatchException((String) null, (Throwable) null); + } + + return var10000; + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/HeroesManagerClient.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/HeroesManagerClient.kt new file mode 100644 index 0000000..98af53b --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/HeroesManagerClient.kt @@ -0,0 +1,32 @@ +package gg.norisk.heroes.client + +import gg.norisk.heroes.client.command.ClientHeroCommand +import gg.norisk.heroes.client.config.ConfigManagerClient +import gg.norisk.heroes.client.hero.ability.AbilityKeyBindManager +import gg.norisk.heroes.client.hero.ability.AbilityManagerClient +import gg.norisk.heroes.client.networking.MouseListener +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.client.renderer.CameraShaker +import gg.norisk.heroes.client.renderer.KeyBindHud +import gg.norisk.heroes.client.renderer.Speedlines +import gg.norisk.heroes.client.ui.OrthoCamera +import gg.norisk.heroes.common.HeroesManager.logger +import net.fabricmc.api.ClientModInitializer + +object HeroesManagerClient : ClientModInitializer { + override fun onInitializeClient() { + logger.info("Init Hero client...") + + HeroKeyBindings.initClient() + ConfigManagerClient.init() + AbilityManagerClient.init() + OrthoCamera.initClient() + AbilityKeyBindManager.initializeKeyBindListeners() + + KeyBindHud.init() + MouseListener.initClient() + Speedlines.initClient() + CameraShaker.initClient() + ClientHeroCommand.init() + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/command/ClientHeroCommand.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/command/ClientHeroCommand.kt new file mode 100644 index 0000000..503602c --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/command/ClientHeroCommand.kt @@ -0,0 +1,29 @@ +package gg.norisk.heroes.client.command + +import gg.norisk.heroes.client.config.ConfigManagerClient +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.player.ffaPlayer +import kotlinx.serialization.encodeToString +import net.silkmc.silk.commands.clientCommand +import net.silkmc.silk.commands.player +import net.silkmc.silk.core.text.literalText + +object ClientHeroCommand { + fun init() { + clientCommand("heroes-client") { + requires { it.enabledFeatures.contains(HeroesManager.heroesFlag) } + literal("debug") { + literal("printffaplayer") { + runs { + val player = this.source.player + player.sendMessage(literalText { + text("FFA Player:") + emptyLine() + text(ConfigManagerClient.JSON.encodeToString(player.ffaPlayer)) + }) + } + } + } + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/config/ConfigManagerClient.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/config/ConfigManagerClient.kt new file mode 100644 index 0000000..f283be2 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/config/ConfigManagerClient.kt @@ -0,0 +1,27 @@ +package gg.norisk.heroes.client.config + +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.HeroManager +import gg.norisk.heroes.common.networking.Networking +import kotlinx.serialization.json.Json +import net.silkmc.silk.core.task.mcCoroutineTask + +object ConfigManagerClient { + val JSON = Json { + prettyPrint = true + ignoreUnknownKeys = true + } + + fun init() { + Networking.s2cHeroSettingsPacket.receiveOnClient { packet, context -> + mcCoroutineTask(sync = true, client = true) { + val decoded = JSON.decodeFromString>(packet) + for (heroJson in decoded) { + logger.info("Loading HeroJson ${heroJson.internalKey}") + HeroManager.getHero(heroJson.internalKey)?.load(heroJson) + } + } + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/events/ClientEvents.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/events/ClientEvents.kt new file mode 100644 index 0000000..5565c89 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/events/ClientEvents.kt @@ -0,0 +1,17 @@ +package gg.norisk.heroes.client.events + +import net.silkmc.silk.core.event.Cancellable +import net.silkmc.silk.core.event.Event +import net.silkmc.silk.core.event.EventScopeProperty + +object ClientEvents { + data class CameraClipToSpaceEvent(var value: Double) + + val cameraClipToSpaceEvent = Event.onlySync() + + class PreHotbarScrollEvent: Cancellable { + override val isCancelled: EventScopeProperty = EventScopeProperty(false) + } + + val preHotbarScrollEvent = Event.onlySync() +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/hero/ability/AbilityKeyBindManager.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/hero/ability/AbilityKeyBindManager.kt new file mode 100644 index 0000000..4dcc468 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/hero/ability/AbilityKeyBindManager.kt @@ -0,0 +1,113 @@ +package gg.norisk.heroes.client.hero.ability + +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.ability.AbilityPacketDescription +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import gg.norisk.heroes.common.hero.ability.implementation.HoldAbility +import gg.norisk.heroes.common.hero.ability.implementation.PressAbility +import gg.norisk.heroes.common.hero.ability.implementation.ToggleAbility +import gg.norisk.heroes.common.hero.getHero +import gg.norisk.utils.events.KeyEvents +import gg.norisk.utils.events.MouseEvents +import net.minecraft.client.MinecraftClient +import net.minecraft.entity.player.PlayerEntity +import net.silkmc.silk.core.event.EventPriority + +object AbilityKeyBindManager { + fun initializeKeyBindListeners() { + MouseEvents.mouseClickEvent.listen(EventPriority.FIRST) { event -> + if (MinecraftClient.getInstance().currentScreen != null) return@listen + val player = MinecraftClient.getInstance().player ?: return@listen + val hero = MinecraftClient.getInstance().player?.getHero() ?: return@listen + hero.getUsableAbilities(player).filter { it.keyBind?.matchesMouse(event.key.code) ?: false } + .sortedByDescending { it.condition != null }.forEach { ability -> + val isConditionMet = + if (ability.condition == null) true else ability.condition?.invoke(player) == true + if (isConditionMet && handleAbility(player, hero, ability, event.pressed, event.pressed)) { + event.isCancelled.set(true) + return@listen + } + } + } + + KeyEvents.keyEvent.listen(EventPriority.FIRST) { event -> + if (MinecraftClient.getInstance().currentScreen != null) return@listen + val player = MinecraftClient.getInstance().player ?: return@listen + val hero = MinecraftClient.getInstance().player?.getHero() ?: return@listen + hero.getUsableAbilities(player).filter { it.keyBind?.matchesKey(event.key, event.scanCode) ?: false } + .sortedByDescending { it.condition != null }.forEach { ability -> + val isConditionMet = + if (ability.condition == null) true else ability.condition?.invoke(player) == true + if (isConditionMet && handleAbility( + player, + hero, + ability, + event.isClicked(), + event.isHold() + ) + ) { + event.isCancelled.set(true) + return@listen + } + } + } + } + + /* TODO FUNKTIONIERT DAS GUT? + fun initializeKeyBind(ability: AbstractAbility<*>) { + logger.info("Initialize Keybind for Ability ${ability.internalKey}") + val keyBind = ability.keyBind ?: return + mouseClickEvent.listen { event -> + if (keyBind.matchesMouse(event.key.code) && canUseAbility(ability)) { + handleAbility(ability, event.pressed) + } + } + keyEvent.listen { event -> + if (event.isHold()) return@listen + if (keyBind.matchesKey(event.key, event.scanCode) && canUseAbility(ability)) { + handleAbility(ability, event.isClicked()) + } + } + } + */ + + private fun handleAbility( + player: PlayerEntity, + hero: Hero, + ability: AbstractAbility<*>, + pressed: Boolean, + hold: Boolean, + ): Boolean { + when (ability) { + is PressAbility -> { + if (!pressed) return false + return AbilityManagerClient.useAbility(player, hero, ability, AbilityPacketDescription.Use()) + } + + is HoldAbility -> { + if (pressed) { + if (AbilityManagerClient.isUsingAbility(player, ability)) return false + return AbilityManagerClient.startAbility(player, hero, ability) + } else if (hold) { + return false + } else { + if (!AbilityManagerClient.isUsingAbility(player, ability)) return false + return AbilityManagerClient.endAbility(player, hero, ability) + } + } + + is ToggleAbility -> { + if (pressed) return false + return if (!AbilityManagerClient.isUsingAbility(player, ability)) { + AbilityManagerClient.startAbility(player, hero, ability) + } else { + AbilityManagerClient.endAbility(player, hero, ability) + } + } + + else -> { + return false + } + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/hero/ability/AbilityManagerClient.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/hero/ability/AbilityManagerClient.kt new file mode 100644 index 0000000..6f4782b --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/hero/ability/AbilityManagerClient.kt @@ -0,0 +1,168 @@ +package gg.norisk.heroes.client.hero.ability + +import gg.norisk.datatracker.entity.syncedValueChangeEvent +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.HeroManager +import gg.norisk.heroes.common.hero.ability.* +import gg.norisk.heroes.common.hero.ability.implementation.Ability +import gg.norisk.heroes.common.hero.ability.implementation.PressAbility +import gg.norisk.heroes.common.hero.ability.implementation.ToggleAbility +import gg.norisk.heroes.common.hero.getHero +import gg.norisk.heroes.common.networking.Networking +import gg.norisk.heroes.common.networking.Networking.s2cAbilityPacket +import gg.norisk.heroes.common.networking.Networking.s2cCooldownPacket +import gg.norisk.utils.DevUtils.uniqueId +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.AbstractClientPlayerEntity +import net.minecraft.entity.player.PlayerEntity +import net.silkmc.silk.core.task.mcCoroutineTask +import java.util.* + +object AbilityManagerClient : IAbilityManager { + private val abilitiesInUse = hashMapOf>() + + override fun init() { + //cleanup + syncedValueChangeEvent.listen { event -> + if (event.key != HeroManager.HERO_KEY) return@listen + val player = event.entity as? AbstractClientPlayerEntity? ?: return@listen + if (player == MinecraftClient.getInstance().player) { + (event.oldValue as? Hero?)?.abilities?.forEach { (name, ability) -> ability.onDisable(player) } + abilitiesInUse.remove(player.uniqueId) + HeroManager.registeredHeroes.values.forEach { + it.abilities.values.forEach { ability -> + ability.removeCooldown(player) + } + } + player.getHero()?.abilities?.forEach { (name, ability) -> ability.onEnable(player) } + } + } + + + s2cCooldownPacket.receiveOnClient { packet, context -> + mcCoroutineTask(sync = true, client = true) { + val player = + context.client.world?.getEntityById(packet.entityId) as? PlayerEntity? ?: return@mcCoroutineTask + val hero = HeroManager.getHero(packet.heroKey) ?: return@mcCoroutineTask + val ability = hero.abilities[packet.abilityKey] ?: return@mcCoroutineTask + ability.setCooldown(packet, player) + } + } + + s2cAbilityPacket.receiveOnClient { packet, context -> + runCatching { + val player = context.client.player ?: return@receiveOnClient + val heroPlayer = context.client.world?.players?.firstOrNull { it.uuid == packet.playerUuid } + ?: return@receiveOnClient + val ability = getAbilityFromAbilityPacket(packet) ?: return@receiveOnClient + val description = packet.description + val isOwnPacket = heroPlayer.uuid == player.uuid + val abilityScope = AbilityScope(heroPlayer) + when (ability) { + is Ability, + is PressAbility -> { + ability.onStart(player, abilityScope) + /*val callbacks = ability.internalCallbacks as AbstractAbility.ReceiveCallbacks + callbacks.handleAllClients?.invoke(heroPlayer, player, description) + if (isOwnPacket) { + callbacks.handleOwnClient?.invoke(player, description) + } else { + callbacks.handleOtherClients?.invoke(heroPlayer, player, description) + }*/ + } + + is ToggleAbility -> { + val callbacks = when (description) { + is AbilityPacketDescription.Start -> { + abilitiesInUse[packet.playerUuid] = ability + logger.info("Start") + ability.onStart(player, abilityScope) + //ability.internalCallbacks.START + } + + is AbilityPacketDescription.Use -> { + ability.onUse(player) + //ability.internalCallbacks.USE + } + + is AbilityPacketDescription.End -> { + abilitiesInUse.remove(packet.playerUuid) + ability.onEnd(player, ToggleAbility.AbilityEndInformation()) + //(ability).internalCallbacks.END + } + } + //callbacks.handleAllClients?.invoke(heroPlayer, player, description) + + if (isOwnPacket) { + //callbacks.handleOwnClient?.invoke(player, description) + } else { + //callbacks.handleOtherClients?.invoke(heroPlayer, player, description) + } + } + + else -> error("Received an unknown Ability?") + } + } + } + } + + override fun isUsingAbility(player: PlayerEntity, ability: AbstractAbility<*>): Boolean { + return abilitiesInUse[player.uuid]?.internalKey == ability.internalKey + } + + override fun registerAbility(ability: AbstractAbility<*>) { + // REMOVED AbilityKeyBindManager.initializeKeyBind(ability) + } + + override fun useAbility( + player: PlayerEntity, + hero: Hero, + ability: AbstractAbility<*>, + description: AbilityPacketDescription.Use + ): Boolean { + if (ability.hasCooldown(player)) { + return false + } else { + val packet = AbilityPacket(player.uuid, hero.internalKey, ability.internalKey, description) + Networking.c2sAbilityPacket.send(packet) + return true + } + } + + override fun useAbility( + player: PlayerEntity, + ability: AbstractAbility<*>, + description: AbilityPacketDescription.Use + ) { + val hero = player.getHero() ?: return + //player.sendDebugMessage("Sending Start Use $ability".literal) + val packet = AbilityPacket(player.uuid, hero.internalKey, ability.internalKey, description) + Networking.c2sAbilityPacket.send(packet) + } + + fun startAbility(player: PlayerEntity, hero: Hero, ability: ToggleAbility): Boolean { + if (ability.hasCooldown(player)) { + return false + } else { + //player.sendDebugMessage("Sending Start Ability $ability".literal) + val packet = + AbilityPacket(player.uuid, hero.internalKey, ability.internalKey, AbilityPacketDescription.Start) + Networking.c2sAbilityPacket.send(packet) + return true + } + } + + fun endAbility(player: PlayerEntity, hero: Hero, ability: ToggleAbility): Boolean { + //player.sendDebugMessage("Sending End Ability $ability".literal) + val packet = AbilityPacket(player.uuid, hero.internalKey, ability.internalKey, AbilityPacketDescription.End) + Networking.c2sAbilityPacket.send(packet) + return true + } + + private fun getAbilityFromAbilityPacket(packet: AbilityPacket): AbstractAbility<*>? { + val hero = HeroManager.getHero(packet.heroKey) ?: return null + val ability = hero.abilities[packet.abilityKey] + return ability + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/networking/MouseListener.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/networking/MouseListener.kt new file mode 100644 index 0000000..1f00243 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/networking/MouseListener.kt @@ -0,0 +1,57 @@ +package gg.norisk.heroes.client.networking + +import gg.norisk.heroes.common.events.mouseScrollEvent +import gg.norisk.heroes.common.networking.Networking.mousePacket +import gg.norisk.heroes.common.networking.Networking.mouseScrollPacket +import gg.norisk.heroes.common.networking.dto.MouseAction +import gg.norisk.heroes.common.networking.dto.MousePacket +import gg.norisk.heroes.common.networking.dto.MouseType +import gg.norisk.utils.events.MouseEvents.mouseClickEvent +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents +import net.minecraft.client.MinecraftClient + +object MouseListener { + fun initClient() { + mouseScrollEvent.listen { + MinecraftClient.getInstance().player ?: return@listen + mouseScrollPacket.send(it.vertical > 0) + } + mouseClickEvent.listen { + MinecraftClient.getInstance().player ?: return@listen + if (MinecraftClient.getInstance().options.attackKey.matchesMouse(it.key.code)) { + mousePacket.send( + MousePacket( + MouseType.LEFT, + if (it.pressed) MouseAction.CLICK else MouseAction.RELEASE + ) + ) + } else if (MinecraftClient.getInstance().options.useKey.matchesMouse(it.key.code)) { + mousePacket.send( + MousePacket( + MouseType.RIGHT, + if (it.pressed) MouseAction.CLICK else MouseAction.RELEASE + ) + ) + } else if (MinecraftClient.getInstance().options.pickItemKey.matchesMouse(it.key.code)) { + mousePacket.send( + MousePacket( + MouseType.MIDDLE, + if (it.pressed) MouseAction.CLICK else MouseAction.RELEASE + ) + ) + } + } + ClientTickEvents.END_CLIENT_TICK.register { + MinecraftClient.getInstance().player ?: return@register + if (MinecraftClient.getInstance().options.attackKey.isPressed) { + mousePacket.send(MousePacket(MouseType.LEFT, MouseAction.HOLD)) + } + if (MinecraftClient.getInstance().options.useKey.isPressed) { + mousePacket.send(MousePacket(MouseType.RIGHT, MouseAction.HOLD)) + } + if (MinecraftClient.getInstance().options.pickItemKey.isPressed) { + mousePacket.send(MousePacket(MouseType.MIDDLE, MouseAction.HOLD)) + } + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/option/HeroKeyBindings.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/option/HeroKeyBindings.kt new file mode 100644 index 0000000..1c1ef84 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/option/HeroKeyBindings.kt @@ -0,0 +1,86 @@ +package gg.norisk.heroes.client.option + +import gg.norisk.heroes.common.hero.getHero +import gg.norisk.utils.events.KeyEvents +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.MinecraftClient +import net.minecraft.client.option.KeyBinding +import net.minecraft.client.util.InputUtil +import org.lwjgl.glfw.GLFW + +object HeroKeyBindings { + val firstKeyBind = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null + else KeyBindingHelper.registerKeyBinding( + KeyBinding( + "key.heroes.first", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_X, + "category.heroes.abilities" + ) + ) + + val secondKeyBind = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null + else KeyBindingHelper.registerKeyBinding( + KeyBinding( + "key.heroes.second", // The translation key of the keybinding's name + InputUtil.Type.KEYSYM, // The type of the keybinding, KEYSYM for keyboard, MOUSE for mouse. + GLFW.GLFW_KEY_V, // The keycode of the key + "category.heroes.abilities" // The translation key of the keybinding's category. + ) + ) + + val thirdKeyBind = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null + else KeyBindingHelper.registerKeyBinding( + KeyBinding( + "key.heroes.third", // The translation key of the keybinding's name + InputUtil.Type.KEYSYM, // The type of the keybinding, KEYSYM for keyboard, MOUSE for mouse. + GLFW.GLFW_KEY_G, // The keycode of the key + "category.heroes.abilities" // The translation key of the keybinding's category. + ) + ) + + val fourthKeyBinding = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null + else KeyBindingHelper.registerKeyBinding( + KeyBinding( + "key.heroes.fourth", // The translation key of the keybinding's name + InputUtil.Type.KEYSYM, // The type of the keybinding, KEYSYM for keyboard, MOUSE for mouse. + GLFW.GLFW_KEY_H, // The keycode of the key + "category.heroes.abilities" // The translation key of the keybinding's category. + ) + ) + + val fifthKeyBind = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null + else KeyBindingHelper.registerKeyBinding( + KeyBinding( + "key.heroes.fifth", // The translation key of the keybinding's name + InputUtil.Type.KEYSYM, // The type of the keybinding, KEYSYM for keyboard, MOUSE for mouse. + GLFW.GLFW_KEY_Z, // The keycode of the key + "category.heroes.abilities" // The translation key of the keybinding's category. + ) + ) + + val pickItemKeyBinding by lazy { + if (FabricLoader.getInstance().environmentType == EnvType.SERVER) null + else MinecraftClient.getInstance().options.pickItemKey + } + + val heroKeyBindings by lazy { listOf(firstKeyBind, secondKeyBind, thirdKeyBind, fourthKeyBinding, fifthKeyBind) } + //TODO das maybe als config damit wir lvie updaten können? + val blacklist = mutableSetOf("key.voice_chat", "key.voice_chat_group", "key.hide_icons") + + @Environment(EnvType.CLIENT) + fun initClient() { + KeyEvents.keyBindingOnPressEvent.listen { event -> + val player = MinecraftClient.getInstance().player ?: return@listen + if (player.getHero() == null) return@listen + if (blacklist.contains(event.keybinding.translationKey)) { + if (heroKeyBindings.any { it?.equals(event.keybinding) == true}) { + event.isCancelled.set(true) + } + } + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/BlockOutlineRenderer.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/BlockOutlineRenderer.kt new file mode 100644 index 0000000..2071dbf --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/BlockOutlineRenderer.kt @@ -0,0 +1,87 @@ +package gg.norisk.heroes.client.renderer + +import gg.norisk.heroes.common.utils.toVec +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.WorldRenderer +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box + + +object BlockOutlineRenderer { + + fun drawBlockBox( + matrixStack: MatrixStack, + vertexConsumerProvider: VertexConsumerProvider, + blockPos: BlockPos, + f: Float, + g: Float, + h: Float, + i: Float + ) { + drawBox(matrixStack, vertexConsumerProvider, blockPos, blockPos.add(1, 1, 1), f, g, h, i) + } + + fun drawBox( + matrixStack: MatrixStack, + vertexConsumerProvider: VertexConsumerProvider, + blockPos: BlockPos, + blockPos2: BlockPos, + f: Float, + g: Float, + h: Float, + i: Float + ) { + val camera = MinecraftClient.getInstance().gameRenderer.camera + if (camera.isReady) { + val vec3d = camera.pos.negate() + val box = Box.from(blockPos.toVec()).offset(vec3d) + drawBox(matrixStack, vertexConsumerProvider, box, f, g, h, i) + } + } + + fun drawBox( + matrixStack: MatrixStack, + vertexConsumerProvider: VertexConsumerProvider, + box: Box, + f: Float, + g: Float, + h: Float, + i: Float + ) { + drawBox( + matrixStack, + vertexConsumerProvider, + box.minX, + box.minY, + box.minZ, + box.maxX, + box.maxY, + box.maxZ, + f, + g, + h, + i + ) + } + + fun drawBox( + matrixStack: MatrixStack, + vertexConsumerProvider: VertexConsumerProvider, + d: Double, + e: Double, + f: Double, + g: Double, + h: Double, + i: Double, + j: Float, + k: Float, + l: Float, + m: Float + ) { + val vertexConsumer = vertexConsumerProvider.getBuffer(RenderLayer.getDebugFilledBox()) + WorldRenderer.renderFilledBox(matrixStack, vertexConsumer, d, e, f, g, h, i, j, k, l, m) + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/CameraShaker.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/CameraShaker.kt new file mode 100644 index 0000000..0ccb3c9 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/CameraShaker.kt @@ -0,0 +1,105 @@ +package gg.norisk.heroes.client.renderer + +import com.google.common.util.concurrent.AtomicDouble +import gg.norisk.heroes.common.networking.CameraShakeEvent +import gg.norisk.heroes.common.networking.cameraShakePacket +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.util.Util +import kotlin.random.Random + +//Credits to https://github.com/LoganDark/fabric-camera-shake +object CameraShaker { + var avgX: Double = 0.0 + var avgY: Double = 0.0 + private var smooth: Int = 3 + private var pastI: Int = 0 + private val pastX: DoubleArray = DoubleArray(smooth) + private val pastY: DoubleArray = DoubleArray(smooth) + private val events: MutableSet = HashSet() + private val providers: MutableSet = HashSet() + private var eventInceptions = Object2LongOpenHashMap() + + fun newFrame() { + val magnitude: Double = getCameraShakeMagnitude(MinecraftClient.getInstance().player) + + val x: Double = (Random.nextDouble() - .5) * magnitude + val y: Double = (Random.nextDouble() - .5) * magnitude + + pastX[pastI] = x + pastY[pastI++] = y + pastI %= smooth + + calculateAvg() + } + + private fun calculateAvg() { + avgX = .0 + avgY = .0 + for (i in 0 until smooth) { + avgX += pastX[i] + avgY += pastY[i] + } + avgX /= smooth.toDouble() + avgY /= smooth.toDouble() + } + + interface CameraShakeProvider { + fun getCameraShakeMagnitude(player: ClientPlayerEntity?): Double + } + + fun initClient() { + cameraShakePacket.receiveOnClient { packet, context -> + addEvent(packet) + } + } + + private fun getCameraShakeMagnitude(player: ClientPlayerEntity?): Double { + val magnitude = AtomicDouble() + val eventIterator: MutableIterator = events.iterator() + eventIterator.forEachRemaining { event: CameraShakeEvent -> + val t: Double + try { + t = getTime(event) + } catch (e: IllegalArgumentException) { + eventIterator.remove() + return@forEachRemaining + } + if (!event.isValid(t)) { + eventIterator.remove() + onEventRemoved(event) + } else { + magnitude.addAndGet(event.getCameraShakeMagnitude(t)) + } + } + for (provider in providers) { + magnitude.addAndGet(provider.getCameraShakeMagnitude(player)) + } + return magnitude.get() + } + + private fun onEventRemoved(event: E) { + eventInceptions.removeLong(event) + } + + private fun onEventAdded(event: E) { + eventInceptions.put(event, Util.getMeasuringTimeNano()) + } + + private fun getTime(event: CameraShakeEvent): Double { + require(eventInceptions.containsKey(event)) { "Passed event was not added to this CameraShakeManager" } + val then: Long = eventInceptions.getLong(event) + val now = Util.getMeasuringTimeNano() + val delta = now - then + return delta.toDouble() / 1000000000.0 + } + + fun addEvent(event: E): E? { + if (events.add(event)) { + onEventAdded(event) + return event + } + return null + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/KeyBindHud.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/KeyBindHud.kt new file mode 100644 index 0000000..c85f3ba --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/KeyBindHud.kt @@ -0,0 +1,109 @@ +package gg.norisk.heroes.client.renderer + +import gg.norisk.heroes.common.hero.getHero +import gg.norisk.heroes.common.hero.utils.ColorUtils +import gg.norisk.ui.api.value.key +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.RenderTickCounter +import net.minecraft.text.Text +import net.minecraft.util.Colors +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.core.text.literalText +import net.silkmc.silk.core.world.pos.Pos2i +import java.awt.Color + +object KeyBindHud { + fun init() { + HudRenderCallback.EVENT.register(::render) + } + + fun render(drawContext: DrawContext, tickCounter: RenderTickCounter) { + if (MinecraftClient.getInstance().debugHud.shouldShowDebugHud()) return + val player = MinecraftClient.getInstance().player ?: return + val hero = player.getHero() ?: return + val offset = 2 + val scale = 0.75f + + drawContext.matrices.push() + drawContext.matrices.scale(scale, scale, scale) + + + hero.getUsableAbilities(player).map { ability -> + val keyBind = ability.keyBind + var text = literalText { + text { + //if (keyBind.condition != null) text("${keyBind.condition.hudText} + ") + val deactivatedColor = 0x4A4A4A + text( + keyBind?.boundKeyLocalizedText ?: keyBind?.defaultKey?.localizedText + ?: ability.getCustomActivation() + ) { + color = if (ability.hasCooldown(player)) { + deactivatedColor + } else { + hero.color + } + } + if (ability.condition != null) { + text(" + ") + text(Text.translatable("heroes.ability.condition.short.${ability.internalKey}")) { + color = if (ability.condition?.invoke(player) == true || ability.condition == null) { + hero.color + } else { + deactivatedColor + } + } + } + } + + text(" - ") { color = 0x919191 } + text(ability.name) + } + + ability.getCooldown(player)?.let { cooldownInfo -> + text = literalText { + text(text) + cooldownInfo.durationString?.let { extension -> + text(" ") + text(extension) { color = ColorUtils.contrast(0x248223) } + } + } + } + + text to ability + }.sortedByDescending { MinecraftClient.getInstance().textRenderer.getWidth(it.first) } + .forEachIndexed { index, (text, ability) -> + val pos = Pos2i(5, 5 + (text.height + offset * 2) * index) + drawContext.fill( + RenderLayer.getGuiOverlay(), + pos.x - offset, + pos.z - offset, + pos.x + text.width + offset, + pos.z + text.height + offset, + -1873784752 + ) + drawContext.drawText( + MinecraftClient.getInstance().textRenderer, literalText { + if (!ability.hasUnlocked(player)) { + text(text.string) + strikethrough = true + color = Colors.LIGHT_GRAY + } else { + text(text) + } + }, pos.x, pos.z, 14737632, true + ) + } + + drawContext.matrices.pop() + } + + val Text.width + get() = MinecraftClient.getInstance().textRenderer.getWidth(this) + + val Text.height + get() = MinecraftClient.getInstance().textRenderer.fontHeight +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/RenderUtils.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/RenderUtils.kt new file mode 100644 index 0000000..2863a7b --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/RenderUtils.kt @@ -0,0 +1,30 @@ +package gg.norisk.heroes.client.renderer + +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.gui.DrawContext +import net.minecraft.util.Identifier + +object RenderUtils { + fun renderOverlay(drawContext: DrawContext, identifier: Identifier, f: Float) { + RenderSystem.disableDepthTest() + RenderSystem.depthMask(false) + RenderSystem.enableBlend() + drawContext.setShaderColor(1.0f, 1.0f, 1.0f, f) + drawContext.drawTexture( + identifier, + 0, + 0, + -90, + 0.0f, + 0.0f, + drawContext.scaledWindowWidth, + drawContext.scaledWindowHeight, + drawContext.scaledWindowWidth, + drawContext.scaledWindowHeight + ) + RenderSystem.disableBlend() + RenderSystem.depthMask(true) + RenderSystem.enableDepthTest() + drawContext.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f) + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/SkinUtils.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/SkinUtils.kt new file mode 100644 index 0000000..d7aeb24 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/SkinUtils.kt @@ -0,0 +1,153 @@ +package gg.norisk.heroes.client.renderer + +import com.mojang.blaze3d.systems.RenderSystem +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.getHero +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.AbstractClientPlayerEntity +import net.minecraft.client.texture.NativeImage +import net.minecraft.client.texture.NativeImageBackedTexture +import net.minecraft.util.Identifier +import java.io.File +import java.io.IOException +import kotlin.jvm.optionals.getOrNull + +object SkinUtils { + fun initClient() { + if (!FabricLoader.getInstance().isDevelopmentEnvironment) return + } + + fun applyOverlay(baseSkinId: Identifier, overlayId: Identifier, hero: Hero) { + val result = combineSkins(baseSkinId, overlayId) + if (FabricLoader.getInstance().isDevelopmentEnvironment) { + logger.info("Merged Skin $baseSkinId $overlayId: $result") + val folder = File(FabricLoader.getInstance().configDir.toFile(), "aang").apply { mkdirs() } + + //MinecraftClient.getInstance().textureManager.registerTexture() + + result?.writeTo(File(folder, "${baseSkinId.path}_overlay.png")) + } + + if (result != null) { + MinecraftClient.getInstance().submit { + MinecraftClient.getInstance().textureManager.registerTexture( + baseSkinId.toOverlaySkin(hero.internalKey.lowercase()), + NativeImageBackedTexture(result) + ) + } + } + } + + fun Identifier.toOverlaySkin(id: String): Identifier { + return Identifier.of(this.namespace, this.path + "_$id") + } + + fun redirectCombinedSkin(original: Identifier, player: AbstractClientPlayerEntity?): Identifier { + val hero = player?.getHero() + val skin = hero?.overlaySkin + if (hero != null && skin != null) { + val mergedSkin = original.toOverlaySkin(hero.internalKey.lowercase()) + if (MinecraftClient.getInstance().textureManager.getOrDefault(mergedSkin, null) != null) { + return mergedSkin + } else { + applyOverlay(original, skin, hero) + return original + } + } else { + return original + } + } + + fun extractTextureToNativeImage(glId: Int, width: Int, height: Int): NativeImage { + // Erstelle ein NativeImage mit der passenden Breite, Höhe und Format + val nativeImage = NativeImage(NativeImage.Format.RGBA, width, height, false) + + // Binde die Textur mit der OpenGL-ID + RenderSystem.bindTexture(glId) + + // Lade die Textur-Pixel-Daten aus OpenGL in das NativeImage + nativeImage.loadFromTextureImage(0, false) // i = 0 für Mipmap Level 0, bl = false für keine Alpha-Korrektur + + return nativeImage // Das NativeImage enthält nun die Texturdaten + } + + /** + * Kombiniert zwei Skins, wobei der zweite Skin alles ersetzt, was nicht transparent ist, + * und gibt das resultierende Bild als NativeImage zurück. + * + * @param baseSkin Der Identifier für das Basisbild (der erste Skin). + * @param overlaySkin Der Identifier für das Overlay-Bild (der zweite Skin). + * @return Das kombinierte Bild als NativeImage. + */ + fun combineSkins(baseSkin: Identifier, overlaySkin: Identifier): NativeImage? { + return try { + // Lade die Basis- und Overlay-Skins als NativeImage + val baseImageTexture = MinecraftClient.getInstance().textureManager.getOrDefault(baseSkin, null) + + val overlayImage = loadSkinAsNativeImage(overlaySkin) + + // Falls einer der beiden Skins nicht geladen werden konnte + if (baseImageTexture == null || overlayImage == null) { + return null + } + + //ich hoffe das macht nichts kapuut lol + val baseImage: NativeImage = extractTextureToNativeImage(baseImageTexture.glId, 64, 64) + + // Überprüfen, ob beide Bilder die gleiche Größe haben + if (baseImage.width != overlayImage.width || baseImage.height != overlayImage.height) { + return null + } + + // Erstelle ein neues NativeImage mit der gleichen Größe wie die Basis + val combinedImage = NativeImage(baseImage.width, baseImage.height, true) + + // Durchlaufen der Pixel und anwenden des Overlays auf das neue Bild + for (x in 0 until baseImage.width) { + for (y in 0 until baseImage.height) { + val baseColor = baseImage.getColor(x, y) + val overlayColor = overlayImage.getColor(x, y) + val alpha = (overlayColor shr 24) and 0xFF // Alpha-Wert extrahieren + + // Wenn das Overlay-Pixel nicht transparent ist, kopiere es ins kombinierte Bild + if (alpha > 0) { + combinedImage.setColor(x, y, overlayColor) + } else { + // Andernfalls kopiere das Basis-Pixel + combinedImage.setColor(x, y, baseColor) + } + } + } + + // Freigeben der Basis- und Overlay-Bilder, da sie nicht mehr benötigt werden + baseImage.close() + overlayImage.close() + + // Gib das kombinierte Bild zurück + combinedImage + + } catch (e: IOException) { + e.printStackTrace() + null + } + } + + /** + * Lädt einen Skin von einem Identifier als NativeImage. + * + * @param skinIdentifier Der Identifier des Skins. + * @return Das NativeImage des Skins, oder null falls das Bild nicht geladen werden konnte. + */ + fun loadSkinAsNativeImage(skinIdentifier: Identifier): NativeImage? { + val resourceManager = MinecraftClient.getInstance().resourceManager + return try { + val resource = resourceManager.getResource(skinIdentifier).getOrNull() ?: return null + NativeImage.read(resource.inputStream) + } catch (e: IOException) { + e.printStackTrace() + null + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/Speedlines.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/Speedlines.kt new file mode 100644 index 0000000..39ef49d --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/renderer/Speedlines.kt @@ -0,0 +1,83 @@ +package gg.norisk.heroes.client.renderer + +import com.mojang.blaze3d.systems.RenderSystem +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.heroes.common.HeroesManager.toId +import net.fabricmc.fabric.api.client.rendering.v1.CoreShaderRegistrationCallback +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gl.GlUniform +import net.minecraft.client.gl.ShaderProgram +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.* +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.util.math.MathHelper +import org.lwjgl.opengl.GL11 +import kotlin.math.max +import kotlin.math.min + +object Speedlines { + var lerpedSpeed: Double = 0.0 + + lateinit var edge: GlUniform + lateinit var speedlinesRenderTypeProgram: ShaderProgram + + private const val SPEEDLINES_KEY = "speedlines" + + var PlayerEntity.showSpeedlines: Boolean + get() = this.getSyncedData(SPEEDLINES_KEY) == true + set(value) { + this.setSyncedData(SPEEDLINES_KEY, value) + } + + fun initClient() { + CoreShaderRegistrationCallback.EVENT.register(CoreShaderRegistrationCallback { context: CoreShaderRegistrationCallback.RegistrationContext -> + context.register( + "speedlines".toId(), VertexFormats.POSITION + ) { shaderProgram: ShaderProgram -> + speedlinesRenderTypeProgram = shaderProgram + edge = shaderProgram.getUniform("Edge")!! + } + }) + + HudRenderCallback.EVENT.register(HudRenderCallback { context: DrawContext, tickCounter: RenderTickCounter -> + val player = MinecraftClient.getInstance().player ?: return@HudRenderCallback + val client = MinecraftClient.getInstance() + if (player.showSpeedlines) { + val width = client.getWindow().width.toFloat() + val height = client.getWindow().height.toFloat() + val delta = tickCounter.getTickDelta(false) + lerpedSpeed = + MathHelper.lerp((delta * 0.05f).toDouble(), lerpedSpeed, client.player!!.velocity.length()) + + var speed = max(0.0, (lerpedSpeed - 0.2) / 2f) + speed = min(speed, 0.2) + edge.set((0.5f - speed).toFloat()) + + val positionMatrix = context.matrices.peek().positionMatrix + val tessellator = Tessellator.getInstance().begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION) + tessellator.vertex(positionMatrix, 0f, height, 0f) + tessellator.vertex(positionMatrix, 0f, 0f, 0f) + tessellator.vertex(positionMatrix, width, 0f, 0f) + tessellator.vertex(positionMatrix, width, height, 0f) + RenderSystem.setShader { speedlinesRenderTypeProgram } + setupRender() + BufferRenderer.drawWithGlobalProgram(tessellator.end()) + endRender() + } + }) + } + + private fun setupRender() { + RenderSystem.enableBlend() + RenderSystem.defaultBlendFunc() + RenderSystem.setShaderColor(1f, 1f, 1f, 1f) + RenderSystem.disableCull() + RenderSystem.depthFunc(GL11.GL_ALWAYS) + } + + private fun endRender() { + RenderSystem.disableBlend() + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/OrthoCamera.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/OrthoCamera.kt new file mode 100644 index 0000000..c9939b4 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/OrthoCamera.kt @@ -0,0 +1,94 @@ +package gg.norisk.heroes.client.ui + +import gg.norisk.heroes.client.ui.screen.HeroSelectorScreen +import gg.norisk.heroes.client.ui.skilltree.HeroSelectorScreenV2 +import gg.norisk.heroes.client.ui.skilltree.SkillTreeScreen +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.HeroManager +import gg.norisk.heroes.common.networking.Networking +import gg.norisk.heroes.common.networking.dto.HeroSelectorPacket +import gg.norisk.utils.OldAnimation +import me.cortex.nvidium.Nvidium +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.MinecraftClient +import net.minecraft.util.math.MathHelper +import net.silkmc.silk.commands.clientCommand +import net.silkmc.silk.core.kotlin.ticks +import net.silkmc.silk.core.task.mcCoroutineTask +import org.joml.Matrix4f +import org.joml.Quaternionf +import kotlin.math.max +import kotlin.time.Duration.Companion.minutes +import kotlin.time.toJavaDuration + + +object OrthoCamera { + val isEnabled get() = MinecraftClient.getInstance().currentScreen is HeroSelectorScreen || MinecraftClient.getInstance().currentScreen is HeroSelectorScreenV2 + var yawAnimation = OldAnimation(0f, 360f, 2.minutes.toJavaDuration()) + + fun initClient() { + Networking.s2cHeroSelectorPacket.receiveOnClient { packet, context -> + if (packet.isActive) { + openHeroSelectorScreen(packet.heroes.mapNotNull { HeroManager.getHero(it) }, packet) + } else { + mcCoroutineTask(sync = true, client = true) { + if (isEnabled) { + if (FabricLoader.getInstance().isModLoaded("nvidium")) { + Nvidium.FORCE_DISABLE = false + MinecraftClient.getInstance()?.worldRenderer?.reload() + } + MinecraftClient.getInstance().setScreen(null) + } + } + } + } + + clientCommand("heroselector") { + runs { + mcCoroutineTask(delay = 1.ticks, sync = true, client = true) { + MinecraftClient.getInstance().setScreen(HeroSelectorScreen(HeroManager.registeredHeroes.values.toList(), false)) + } + } + } + + clientCommand("skilltree") { + runs { + mcCoroutineTask(sync = true, client = true, delay = 1.ticks) { + MinecraftClient.getInstance().setScreen(SkillTreeScreen()) + } + } + } + } + + fun openHeroSelectorScreen( + heroes: List = HeroManager.registeredHeroes.values.toList(), + packet: HeroSelectorPacket + ) { + mcCoroutineTask(delay = 1.ticks, sync = true, client = true) { + MinecraftClient.getInstance().setScreen(HeroSelectorScreenV2(heroes, packet.isKitEditorEnabled)) + } + } + + fun createOrthoMatrix(delta: Float, minScale: Float): Matrix4f { + val client: MinecraftClient = MinecraftClient.getInstance() + val scale = 100f + val width = max(minScale, scale * client.window.framebufferWidth / client.window.framebufferHeight) + val height = max(minScale, scale) + return Matrix4f().setOrtho( + -width, width, + -height, height, + -1000.0F, 1000.0f + ) + } + + fun handlePitch(quaternion: Quaternionf, tickDelta: Float): Float { + return 30f * MathHelper.RADIANS_PER_DEGREE + } + + fun handleYaw(quaternion: Quaternionf, tickDelta: Float): Float { + if (yawAnimation.isDone) { + yawAnimation.reset() + } + return yawAnimation.get() * MathHelper.RADIANS_PER_DEGREE + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/AbilitiesComponent.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/AbilitiesComponent.kt new file mode 100644 index 0000000..fa719f6 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/AbilitiesComponent.kt @@ -0,0 +1,53 @@ +package gg.norisk.heroes.client.ui.components + +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import gg.norisk.ui.components.ScalableButtonComponent +import io.wispforest.owo.ui.component.ButtonComponent +import io.wispforest.owo.ui.container.Containers +import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.core.OwoUIDrawContext +import io.wispforest.owo.ui.core.Sizing +import net.minecraft.client.MinecraftClient +import net.silkmc.silk.core.text.literal +import java.util.* + +class AbilitiesComponent( + val hero: Hero, + val uuid: UUID = MinecraftClient.getInstance().player!!.uuid, + horizontalSizing: Sizing = Sizing.content(), + verticalSizing: Sizing = Sizing.content() +) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) { + + val mainWrapper = Containers.horizontalFlow(Sizing.content(), Sizing.content()) + val buttonWrapper = Containers.horizontalFlow(Sizing.content(), Sizing.content()) + + init { + for (ability in hero.abilities.values) { + buttonWrapper.child(ScalableButtonComponent(ability.name.literal, 0.8f) { + onAbilityButtonClick(it, ability) + }) + } + + child(buttonWrapper) + child(mainWrapper) + buttonWrapper.children().filterIsInstance().first().onPress() + } + + override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + for (child in mainWrapper.children()) { + val width = buttonWrapper.fullSize().width + child.horizontalSizing(Sizing.fixed(width)) + } + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + + private fun onAbilityButtonClick(it: ButtonComponent, ability: AbstractAbility<*>) { + buttonWrapper.children().filterIsInstance().forEach { button -> + button.active(true) + } + it.active(false) + mainWrapper.clearChildren() + mainWrapper.child(AbilityComponent(ability)) + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/AbilityComponent.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/AbilityComponent.kt new file mode 100644 index 0000000..e9dfd11 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/AbilityComponent.kt @@ -0,0 +1,208 @@ +package gg.norisk.heroes.client.ui.components + +import gg.norisk.heroes.common.ability.CooldownProperty +import gg.norisk.heroes.common.ability.LevelInformation +import gg.norisk.heroes.common.ability.PlayerProperty +import gg.norisk.heroes.common.ability.SingleUseProperty +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import gg.norisk.heroes.common.hero.ability.SkillPropertyPacket +import gg.norisk.heroes.common.networking.Networking +import gg.norisk.ui.components.ScalableButtonComponent +import gg.norisk.ui.components.ScalableLabelComponent +import io.wispforest.owo.ui.component.ButtonComponent +import io.wispforest.owo.ui.container.Containers +import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.core.* +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.RenderLayer +import net.minecraft.text.Text +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.core.text.literalText +import java.awt.Color +import java.util.* + +class AbilityComponent( + val ability: AbstractAbility<*>, + val uuid: UUID = MinecraftClient.getInstance().player!!.uuid, + horizontalSizing: Sizing = Sizing.content(), + verticalSizing: Sizing = Sizing.content() +) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.HORIZONTAL) { + + val abilityDescription = ScalableLabelComponent(ability.description, 0.5f).apply { + shadow(true) + } + val leftWrapper = Containers.verticalFlow(Sizing.fill(45), Sizing.content()) + + init { + surface(Surface.VANILLA_TRANSLUCENT) + padding(Insets.of(5)) + leftWrapper.apply { + child(ScalableLabelComponent(literalText { + text(ability.name.literal) + underline = true + bold = true + }).apply { + shadow(true) + }) + child(abilityDescription) + gap(3) + } + child(leftWrapper) + + gap(2) + + horizontalAlignment(HorizontalAlignment.LEFT) + + child(Containers.verticalFlow(Sizing.fill(55), Sizing.content()).apply { + //debug() + gap(3) + for (property in ability.getAllProperties()) { + if (property is SingleUseProperty) continue + child(PropertyComponent(property)) + } + }) + } + + override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + abilityDescription.maxWidth(leftWrapper.fullSize().width * 2) + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + + private inner class PropertyComponent( + val property: PlayerProperty<*>, + horizontalSizing: Sizing = Sizing.fill(), + verticalSizing: Sizing = Sizing.content() + ) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) { + val progressBar = Containers.horizontalFlow(Sizing.fill(83), Sizing.fixed(3)) + var progressColor: Color = Color.GREEN + val title = ScalableLabelComponent(literalText { + text(Text.translatable(property.name)) { + bold = true + } + text(":") + }, 0.75f).apply { + shadow(true) + } + val valueLabel = ScalableLabelComponent(getValueText(property.getValue(uuid)), 0.75f).apply { + shadow(true) + } + val levelLabel = ScalableLabelComponent(getLevelText(property.getLevelInfo(uuid)), 0.5f).apply { + shadow(true) + } + val skillButton = ScalableButtonComponent("+".literal, 0.75f, ::onSkill).apply { + sizing(Sizing.fixed(15)) + } + + init { + //debug() + child(Containers.horizontalFlow(Sizing.content(), Sizing.content()).apply { + child(title) + child(valueLabel) + gap(5) + tooltip(upgradeTooltip()) + //debug() + }) + child(Containers.horizontalFlow(Sizing.content(), Sizing.content()).apply { + child(Containers.verticalFlow(Sizing.content(), Sizing.content()).apply { + alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER) + child(levelLabel) + child(progressBar) + gap(3) + }) + child(skillButton) + alignment(HorizontalAlignment.LEFT, VerticalAlignment.CENTER) + gap(5) + //debug() + }) + gap(3) + //alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP) + } + + private fun upgradeTooltip(): Text { + return literalText { + text("Upgrade") { + bold = true + } + repeat(property.maxLevel + 1) { level -> + newLine() + text("Lvl $level -> ") + text(getValueText(property.getValue(level))) + } + } + } + + private fun progressTooltip(levelInformation: LevelInformation): Text { + return literalText { + text((levelInformation.xpNextLevel - levelInformation.xpTillNextLevel).toString()) + text("/") + text(levelInformation.xpNextLevel.toString()) + } + } + + private fun onSkill(buttonComponent: ButtonComponent) { + mcCoroutineTask(client = true, sync = true) { + Networking.c2sSkillProperty.send( + SkillPropertyPacket( + ability.hero.internalKey, ability.internalKey, property.internalKey + ) + ) + } + } + + private fun getLevelText(levelInformation: LevelInformation): Text { + return literalText { + text("Lvl ") + text(levelInformation.currentLevel.toString()) + text("/") + text(levelInformation.maxLevel.toString()) + } + } + + private fun getValueText(value: T): Text { + return literalText { + text(value.toString()) + if (property is CooldownProperty) { + text("s") + } + } + } + + override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + super.draw(context, mouseX, mouseY, partialTicks, delta) + val levelInfo = property.getLevelInfo(MinecraftClient.getInstance().session.uuidOrNull) + valueLabel.text(getValueText(property.getValue(uuid))) + levelLabel.text(getLevelText(levelInfo)) + progressBar.tooltip(progressTooltip(levelInfo)) + levelLabel.tooltip(progressTooltip(levelInfo)) + val currentPercentage = levelInfo.percentageTillNextLevel + progressBar.surface { surfaceContext, component -> + val barWidth = progressBar.width() * (currentPercentage / 100.0) + + surfaceContext.fill( + RenderLayer.getGui(), + component.x(), + component.y(), + (component.x() + barWidth).toInt(), + component.y() + component.height(), + 0, + progressColor.rgb + ) + + surfaceContext.fill( + RenderLayer.getGui(), + component.x() + barWidth.toInt(), + component.y(), + (component.x() + progressBar.width()), + component.y() + component.height(), + 0, + progressColor.darker().darker().withAlpha(200).rgb + ) + } + } + } + + fun Color.withAlpha(alpha: Int): Color { + return Color(red, green, blue, alpha) + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/HeroListComponent.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/HeroListComponent.kt new file mode 100644 index 0000000..eab2fdf --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/HeroListComponent.kt @@ -0,0 +1,98 @@ +package gg.norisk.heroes.client.ui.components + + +import gg.norisk.heroes.client.ui.screen.HeroSelectorScreen +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.networking.Networking +import gg.norisk.ui.components.ScalableButtonComponent +import io.wispforest.owo.ui.component.ButtonComponent +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.container.Containers +import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.core.* +import io.wispforest.owo.ui.util.UISounds +import net.silkmc.silk.core.text.literal + +class HeroListComponent( + val heroes: List, + val heroSelectorScreen: HeroSelectorScreen, + horizontalSizing: Sizing = Sizing.content(), + verticalSizing: Sizing = Sizing.content() +) : FlowLayout( + horizontalSizing, verticalSizing, Algorithm.VERTICAL +) { + val lockInButton = ScalableButtonComponent("LOCK IN".literal, 1f, ::onLockInButton).apply { + horizontalSizing(Sizing.fixed(100)) + } + val editorButton = ScalableButtonComponent("EDITOR".literal, 1f, ::onEditorButton).apply { + horizontalSizing(Sizing.fixed(100)) + } + + init { + gap(5) + alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER) + + val grid = Containers.grid(Sizing.content(), Sizing.content(), 1, heroes.size) + for ((index, hero) in heroes.withIndex()) { + grid.child(HeroHeadComponent(hero), 0, index) + } + child(grid) + grid.surface(Surface.VANILLA_TRANSLUCENT) + grid.padding(Insets.of(5)) + + child(lockInButton) + if (heroSelectorScreen.isKitEditorEnabled) { + child(editorButton) + } + } + + private fun onLockInButton(buttonComponent: ButtonComponent) { + Networking.c2sHeroSelectorPacket.send(heroSelectorScreen.hero!!.internalKey) + } + + private fun onEditorButton(buttonComponent: ButtonComponent) { + Networking.c2sKitEditorRequestPacket.send(Unit) + } + + override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + lockInButton.active(heroSelectorScreen.hero != null) + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + + inner class HeroHeadComponent( + val hero: Hero, horizontalSizing: Sizing = Sizing.content(), verticalSizing: Sizing = Sizing.content() + ) : FlowLayout( + horizontalSizing, verticalSizing, Algorithm.HORIZONTAL + ) { + init { + val l = 8 + val m = 8 + val heroHead = Components.texture(hero.icon, 0, 0, 64, 64, 64, 64) + //val heroHead2 = Components.texture(hero.skin, 40, l, 8, m, 64, 64) + //OVERLAY + heroHead.sizing(Sizing.fixed(32)) + child(heroHead) + + margins(Insets.of(2)) + padding(Insets.of(2)) + + surface(Surface.outline(java.awt.Color.WHITE.darker().rgb)) + + mouseDown().subscribe { _, _, _ -> + UISounds.playButtonSound() + heroSelectorScreen.hero = hero + return@subscribe true + } + mouseEnter().subscribe { + surface(Surface.outline(Color.WHITE.argb())) + } + mouseLeave().subscribe { + surface(Surface.outline(java.awt.Color.WHITE.darker().rgb)) + } + } + + override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/HeroListComponentV2.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/HeroListComponentV2.kt new file mode 100644 index 0000000..3c4678a --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/components/HeroListComponentV2.kt @@ -0,0 +1,131 @@ +package gg.norisk.heroes.client.ui.components + + +import gg.norisk.heroes.client.ui.skilltree.HeroSelectorScreenV2 +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.networking.Networking +import gg.norisk.ui.components.ScalableButtonComponent +import io.wispforest.owo.ui.component.ButtonComponent +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.container.Containers +import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.container.OverlayContainer +import io.wispforest.owo.ui.core.* +import io.wispforest.owo.ui.util.UISounds +import net.minecraft.client.MinecraftClient +import net.silkmc.silk.core.text.literal + +class HeroListComponentV2( + val heroes: List, + val heroSelectorScreen: HeroSelectorScreenV2, + horizontalSizing: Sizing = Sizing.content(), + verticalSizing: Sizing = Sizing.content() +) : FlowLayout( + horizontalSizing, verticalSizing, Algorithm.VERTICAL +) { + val lockInButton = ScalableButtonComponent("LOCK IN".literal, 1f, ::onLockInButton).apply { + horizontalSizing(Sizing.fixed(100)) + } + val editorButton = ScalableButtonComponent("EDITOR".literal, 1f, ::onEditorButton).apply { + horizontalSizing(Sizing.fixed(48)) + } + val lobbyButton = ScalableButtonComponent("SPEC".literal, 1f, ::onLobbyButton).apply { + horizontalSizing(Sizing.fixed(48)) + } + + init { + gap(5) + alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER) + + val grid = Containers.grid(Sizing.content(), Sizing.content(), 1, heroes.size) + for ((index, hero) in heroes.withIndex()) { + grid.child(HeroHeadComponent(hero), 0, index) + } + child(grid) + grid.surface(Surface.VANILLA_TRANSLUCENT) + grid.padding(Insets.of(5)) + + val buttonWrapper = ButtonWrapper() + child(buttonWrapper) + + buttonWrapper.child(lockInButton) + if (heroSelectorScreen.isKitEditorEnabled) { + buttonWrapper.child(Containers.horizontalFlow(Sizing.content(), Sizing.content()).apply { + child(editorButton) + child(lobbyButton) + gap(5) + }) + } + } + + private inner class ButtonWrapper( + horizontalSizing: Sizing = Sizing.content(), + verticalSizing: Sizing = Sizing.content() + ) : FlowLayout( + horizontalSizing, verticalSizing, Algorithm.VERTICAL + ) { + init { + gap(5) + } + override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + if (heroSelectorScreen.adapter.children().any { it is OverlayContainer<*> }) { + return + } + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + } + + private fun onLockInButton(buttonComponent: ButtonComponent) { + Networking.c2sHeroSelectorPacket.send(heroSelectorScreen.hero!!.internalKey) + } + + private fun onEditorButton(buttonComponent: ButtonComponent) { + Networking.c2sKitEditorRequestPacket.send(Unit) + } + + private fun onLobbyButton(buttonComponent: ButtonComponent) { + heroSelectorScreen.close() + } + + override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + lockInButton.active(heroSelectorScreen.hero != null) + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + + inner class HeroHeadComponent( + val hero: Hero, horizontalSizing: Sizing = Sizing.content(), verticalSizing: Sizing = Sizing.content() + ) : FlowLayout( + horizontalSizing, verticalSizing, Algorithm.HORIZONTAL + ) { + init { + val l = 8 + val m = 8 + val heroHead = Components.texture(hero.icon, 0, 0, 64, 64, 64, 64) + //val heroHead2 = Components.texture(hero.skin, 40, l, 8, m, 64, 64) + //OVERLAY + heroHead.sizing(Sizing.fixed(32)) + child(heroHead) + + margins(Insets.of(2)) + padding(Insets.of(2)) + + surface(Surface.outline(java.awt.Color.WHITE.darker().rgb)) + + mouseDown().subscribe { _, _, _ -> + UISounds.playButtonSound() + heroSelectorScreen.hero = hero + return@subscribe true + } + mouseEnter().subscribe { + surface(Surface.outline(Color.WHITE.argb())) + } + mouseLeave().subscribe { + surface(Surface.outline(java.awt.Color.WHITE.darker().rgb)) + } + } + + override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/screen/HeroSelectorScreen.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/screen/HeroSelectorScreen.kt new file mode 100644 index 0000000..bee93b3 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/screen/HeroSelectorScreen.kt @@ -0,0 +1,113 @@ +package gg.norisk.heroes.client.ui.screen + + +//import me.cortex.nvidium.Nvidium +import gg.norisk.heroes.client.ui.components.AbilitiesComponent +import gg.norisk.heroes.client.ui.components.HeroListComponent +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.ui.components.ScalableLabelComponent +import io.wispforest.owo.ui.base.BaseOwoScreen +import io.wispforest.owo.ui.container.Containers +import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.core.* +import me.cortex.nvidium.Nvidium +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.MinecraftClient +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.core.text.literalText + +class HeroSelectorScreen(val heroes: List, val isKitEditorEnabled: Boolean = false) : + BaseOwoScreen() { + var hero: Hero? = null + set(value) { + heroInfoComponent?.remove() + centerLabel.remove() + if (field != value) { + field = value + if (value != null) { + heroInfoComponent = heroAbility(value) + this.uiAdapter.rootComponent.child(heroInfoComponent) + } + } else { + field = null + this.uiAdapter.rootComponent.child(centerLabel) + } + } + var heroInfoComponent: FlowLayout? = null + var centerLabel: FlowLayout = Containers.horizontalFlow(Sizing.fill(), Sizing.content()).apply { + child(ScalableLabelComponent(literalText { + text("CHOOSE YOUR HERO") + }, 3f).apply { + shadow(true) + }) + alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER) + positioning(Positioning.relative(30, 50)) + } + + override fun createAdapter(): OwoUIAdapter { + return OwoUIAdapter.create(this, Containers::verticalFlow); + } + + private fun heroAbility(hero: Hero): FlowLayout { + val container = Containers.verticalFlow(Sizing.content(), Sizing.content()) + .apply { positioning(Positioning.relative(0, 30)) } + container.child(Containers.verticalFlow(Sizing.content(), Sizing.content()).apply { + child(ScalableLabelComponent(literalText { + text(hero.name.uppercase()) + bold = true + //color = Color.YELLOW.rgb + }, 2f).apply { + this.margins(Insets.of(3)) + }) + child(XpLabel(1f).apply { + this.margins(Insets.of(3)) + }) + gap(3) + }) + container.gap(5) + container.child(AbilitiesComponent(hero)) + return container + } + + private class XpLabel(scale: Float = 1f) : ScalableLabelComponent("".literal, scale) { + override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + text(literalText { + text("XP: ") + text((MinecraftClient.getInstance().player?.ffaPlayer?.xp ?: 0).toString()) + }) + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + } + + override fun close() { + super.close() + if (FabricLoader.getInstance().isModLoaded("nvidium")) { + Nvidium.FORCE_DISABLE = false + this.client?.worldRenderer?.reload() + } + } + + override fun build(root: FlowLayout) { + val heroList = HeroListComponent(heroes, this) + heroList.positioning(Positioning.relative(50, 90)) + + root.child(heroList) + if (hero == null) { + root.child(centerLabel) + } + + if (FabricLoader.getInstance().isModLoaded("nvidium")) { + Nvidium.FORCE_DISABLE = true + this.client?.worldRenderer?.reload() + } + } + + override fun shouldPause(): Boolean { + return false + } + + override fun shouldCloseOnEsc(): Boolean { + return FabricLoader.getInstance().isDevelopmentEnvironment + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/AbilitySkillTreeComponent.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/AbilitySkillTreeComponent.kt new file mode 100644 index 0000000..97e7c82 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/AbilitySkillTreeComponent.kt @@ -0,0 +1,454 @@ +package gg.norisk.heroes.client.ui.skilltree + +import com.mojang.blaze3d.systems.RenderSystem +import gg.norisk.heroes.common.HeroesManager.toId +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.heroes.common.ui.ScrollContainerV2 +import gg.norisk.ui.components.ScalableLabelComponent +import io.wispforest.owo.ui.container.Containers +import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.core.* +import io.wispforest.owo.ui.util.NinePatchTexture +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.BufferRenderer +import net.minecraft.client.render.GameRenderer +import net.minecraft.client.render.Tessellator +import net.minecraft.client.render.VertexFormat.DrawMode +import net.minecraft.client.render.VertexFormats +import net.minecraft.client.sound.PositionedSoundInstance +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.sound.SoundEvents +import net.minecraft.text.Text +import net.minecraft.util.Colors +import net.minecraft.util.Identifier +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.core.text.literalText +import org.joml.Matrix4f +import org.joml.Vector2d +import kotlin.math.cos +import kotlin.math.sin + + +class AbilitySkillTreeComponent( + val ability: AbstractAbility<*>, + horizontalSizing: Sizing = Sizing.fill(50), + verticalSizing: Sizing = Sizing.fill(60), + val player: PlayerEntity = MinecraftClient.getInstance().player!! +) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) { + val shadow = "textures/gui/shadow.png".toId() + val scrollChild = ScrollChild() + val scroll = ScrollContainerV2(Sizing.fill(), Sizing.fill(80), scrollChild) + var isHovered = false + + init { + surface(Surface.PANEL) + alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP) + padding(Insets.of(5).withLeft(8).withRight(8)) + + //val scroll = Containers.horizontalScroll(Sizing.fill(25), Sizing.fill(60), scrollChild) + child(Containers.horizontalFlow(Sizing.fill(), Sizing.content(1)).apply { + child(ScalableLabelComponent(literalText { + text(ability.name.literal) + color = Colors.GRAY + })) + }) + child(Containers.horizontalFlow(Sizing.fill(), Sizing.content(1)).apply { + child(ScalableLabelComponent(literalText { + text(ability.description) + color = Colors.GRAY + },0.75f).apply { + maxWidth(350) + }) + }) + child(scroll) + scroll.alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER) + scroll.padding(Insets.of(2)) + scrollChild.surface(Surface { context, component -> + val divider = 240f + RenderSystem.setShaderColor(divider / 255f, divider / 255f, divider / 255f, 1f); + context.drawTexture( + ability.getBackgroundTexture(), + component.x(), + component.y(), + 0f, + 0f, + component.width(), + component.height(), + 16, + 16 + ); + + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + }) + + scrollChild.alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP) + scrollChild.child(getComponent(SkillTreeUtils.toSkillTree(ability))) + child(XPComponent()) + } + + inner class XPComponent( + horizontalSizing: Sizing = Sizing.content(), + verticalSizing: Sizing = Sizing.content(), + ) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) { + + init { + + } + + override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + super.draw(context, mouseX, mouseY, partialTicks, delta) + val textRenderer = MinecraftClient.getInstance().textRenderer + val string = "" + player.ffaPlayer.xp + + val x = this.x() - textRenderer.getWidth(string) / 2 + val y = this.y() + textRenderer.fontHeight / 2 + + context.drawText(textRenderer, string, x + 1, y, 0, false) + context.drawText(textRenderer, string, x - 1, y, 0, false) + context.drawText(textRenderer, string, x, y + 1, 0, false) + context.drawText(textRenderer, string, x, y - 1, 0, false) + context.drawText(textRenderer, string, x, y, 8453920, false) + } + } + + + inner class ScrollChild( + horizontalSizing: Sizing = Sizing.fixed(500), + verticalSizing: Sizing = Sizing.fixed(500), + ) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) { + override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + + override fun drawChildren( + context: OwoUIDrawContext, + mouseX: Int, + mouseY: Int, + partialTicks: Float, + delta: Float, + children: MutableList + ) { + super.drawChildren(context, mouseX, mouseY, partialTicks, delta, children) + } + } + + private fun getComponent(node: TreeNode): Component { + val container = Wrapper(node, Sizing.content(), Sizing.content(), Algorithm.VERTICAL) + container.alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP) + + // container.surface(Surface.outline(colors.random().argb())) + container.padding(Insets.of(1)) + //container.debug() + + if (node.children.isNotEmpty()) { + + // Kinder horizontal anordnen + val childContainer = Containers.horizontalFlow(Sizing.content(), Sizing.content()) + childContainer.id("child-node") + childContainer.alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP) + childContainer.padding(Insets.of(2)) // Fügt Abstand zwischen den Kindknoten hinzu + + + node.children.forEach { + childContainer.child(getComponent(it)) + } + + container.child(childContainer) + } + + return container + } + + override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + super.draw(context, mouseX, mouseY, partialTicks, delta) + + RenderSystem.enableDepthTest() + + RenderSystem.enableBlend() + RenderSystem.defaultBlendFunc() + val matrices = context.matrices + matrices.push() + matrices.translate(0f, 0f, 5.0f) + context.drawTexture( + shadow, + scroll.x(), + scroll.y(), + 0f, + 0f, + scroll.width(), + scroll.height(), + scroll.width(), + scroll.height(), + ) + RenderSystem.disableBlend() + matrices.pop() + } + + inner class Wrapper( + val node: TreeNode, + horizontalSizing: Sizing = Sizing.content(), + verticalSizing: Sizing = Sizing.content(), + algorithm: Algorithm + ) : FlowLayout(horizontalSizing, verticalSizing, algorithm) { + val label = ScalableLabelComponent(node.value.title()).apply { + shadow(true) + } + var isHoveredChild = false + var isVisible = false + val box: FlowLayout = Containers.verticalFlow(Sizing.fixed(26), Sizing.fixed(26)).apply { + if (node.value.isUnlocked(MinecraftClient.getInstance().player!!)) { + } else { + } + //child(label) + cursorStyle(CursorStyle.POINTER) + mouseEnter().subscribe { + isHovered = true + isHoveredChild = true + } + mouseLeave().subscribe { + isHovered = false + isHoveredChild = false + } + mouseDown().subscribe { _, _, _ -> + if (isVisible) { + MinecraftClient.getInstance().soundManager.play( + PositionedSoundInstance.master( + SoundEvents.BLOCK_AMETHYST_BLOCK_HIT, + 1.0f, 1f + ) + ) + node.value.skill() + } + return@subscribe true + } + child(node.value.icon()) + alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER) + } + + + init { + margins(Insets.top(5)) + child(label) + child(box) + } + + fun anchorPoint(): Component { + return box + } + + override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + + if (isVisible) { + box.tooltip(node.value.tooltip(player)) + } else { + box.tooltip(Text.empty()) + } + + val player = MinecraftClient.getInstance().player!! + var drawLines = true + if (node.value.isParentUnlocked(player)) { + if (node.value.isUnlocked(player)) { + if (node.value.parent() == null) { + box.surface { context2, component -> + val root = "textures/gui/root_panel.png".toId() + context2.drawTexture( + root, + component.x(), + component.y(), + 0f, + 0f, + 26, + 26, + 26, + 26 + ) + } + } else { + box.surface { owoUIDrawContext, parentComponent -> + if (node.value.isLast()) { + val root = + Identifier.ofVanilla("textures/gui/sprites/advancements/goal_frame_obtained.png") + owoUIDrawContext.drawTexture( + root, + parentComponent.x(), + parentComponent.y(), + 0f, + 0f, + 26, + 26, + 26, + 26 + ) + } else { + NinePatchTexture.draw("unlocked".toId(), context, parentComponent) + } + } + } + } else { + if (node.value.progress(player) == 1.0) { + box.surface { context2, component -> + val root = Identifier.ofVanilla("textures/gui/sprites/advancements/goal_frame_obtained.png") + context2.drawTexture( + root, + component.x(), + component.y(), + 0f, + 0f, + 26, + 26, + 26, + 26 + ) + } + } else { + if (node.value.isLast()) { + box.surface { owoUIDrawContext, parentComponent -> + val root = + Identifier.ofVanilla("textures/gui/sprites/advancements/goal_frame_unobtained.png") + owoUIDrawContext.drawTexture( + root, + parentComponent.x(), + parentComponent.y(), + 0f, + 0f, + 26, + 26, + 26, + 26 + ) + } + } else { + box.surface(Surface.PANEL) + } + } + } + } else { + if (!node.value.isUnlocked(player) && node.value.parent()?.isParentUnlocked(player) == true) { + if (node.value.isLast()) { + box.surface { owoUIDrawContext, parentComponent -> + val root = "textures/gui/goal_frame_dark.png".toId() + owoUIDrawContext.drawTexture( + root, + parentComponent.x(), + parentComponent.y(), + 0f, + 0f, + 26, + 26, + 26, + 26 + ) + } + } else { + box.surface(Surface.DARK_PANEL) + } + drawLines = false + } else { + isVisible = false + return + } + } + + isVisible = true + + + if (!node.value.isUnlocked(MinecraftClient.getInstance().player!!)) { + // return + } + val childContainer = childById(FlowLayout::class.java, "child-node") as? FlowLayout? ?: return super.draw( + context, + mouseX, + mouseY, + partialTicks, + delta + ) + if (childContainer.children().isEmpty()) return super.draw(context, mouseX, mouseY, partialTicks, delta) + + val parentX = anchorPoint().x() + anchorPoint().width() / 2 + val parentY = anchorPoint().y() + anchorPoint().height() - 5 + + // Berechne die mittlere Y-Position für die horizontale Linie + val lowestChildY = + childContainer.children().minOfOrNull { (it as? Wrapper)?.anchorPoint()?.y() ?: Int.MAX_VALUE } + ?: return super.draw(context, mouseX, mouseX, partialTicks, delta) + val midY = parentY + (lowestChildY - parentY) / 2 + val whiteLineThickness = 2.0 + val grayedColor = Color.ofRgb(Colors.GRAY) + val progressColor = Color.GREEN// Farbe für die Fortschrittslinie + + // Zeichne die graue Linie und die grüne Linie + for ((index, child) in childContainer.children().withIndex()) { + if (child is Wrapper) { + val childX = child.anchorPoint().x() + (child.anchorPoint().width()) / 2 + val childY = child.anchorPoint().y() + + // Zeichne die graue Linie + val angle = + Math.toDegrees(Math.atan2((childY - parentY).toDouble(), (childX - parentX).toDouble())); + val length = + Math.sqrt(((childX - parentX) * (childX - parentX) + (childY - parentY) * (childY - parentY)).toDouble()); + if (drawLines) { + context.drawLine(parentX, parentY, angle, length, whiteLineThickness * 2, Color.BLACK); + context.drawLine(parentX, parentY, angle, length, whiteLineThickness, grayedColor); + } + + val progress = child.node.value.progress(player) + + // Berechne den Fortschritt-Endpunkt + val progressEndX = parentX + (childX - parentX) * progress + val progressEndY = + parentY + (childY - parentY) * progress / ((childY - parentY).toDouble().takeIf { it != 0.0 } + ?: 1.0) + + // Zeichne die grüne Linie über der grauen Linie + val progressAngle = angle; // Verwende denselben Winkel + val progressLength = length * progress; // Berechne die Länge basierend auf dem Fortschritt + + if (drawLines) { + context.drawLine( + parentX, + parentY, + progressAngle, + progressLength, + whiteLineThickness, + progressColor + ); + } + } + } + + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + + + } + + fun OwoUIDrawContext.drawLine(x1: Int, y1: Int, angle: Double, length: Double, thickness: Double, color: Color) { + // Berechne die Endpunkte der Linie basierend auf dem Winkel + val radians = Math.toRadians(angle) + val x2 = (x1 + cos(radians) * length).toInt() + val y2 = (y1 + sin(radians) * length).toInt() + + val offset = + (Vector2d((x2 - x1).toDouble(), (y2 - y1).toDouble())).perpendicular().normalize().mul(thickness * 0.5f) + val buffer = Tessellator.getInstance().begin(DrawMode.QUADS, VertexFormats.POSITION_COLOR) + val matrix: Matrix4f = this.getMatrices().peek().getPositionMatrix() + val vColor = color.argb() + + buffer.vertex(matrix, (x1.toDouble() + offset.x).toFloat(), (y1.toDouble() + offset.y).toFloat(), 0.0f) + .color(vColor) + buffer.vertex(matrix, (x1.toDouble() - offset.x).toFloat(), (y1.toDouble() - offset.y).toFloat(), 0.0f) + .color(vColor) + buffer.vertex(matrix, (x2.toDouble() - offset.x).toFloat(), (y2.toDouble() - offset.y).toFloat(), 0.0f) + .color(vColor) + buffer.vertex(matrix, (x2.toDouble() + offset.x).toFloat(), (y2.toDouble() + offset.y).toFloat(), 0.0f) + .color(vColor) + + RenderSystem.enableBlend() + RenderSystem.defaultBlendFunc() + RenderSystem.setShader { GameRenderer.getPositionColorProgram() } + BufferRenderer.drawWithGlobalProgram(buffer.end()) + } + +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/HeroSelectorScreenV2.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/HeroSelectorScreenV2.kt new file mode 100644 index 0000000..bade8a7 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/HeroSelectorScreenV2.kt @@ -0,0 +1,123 @@ +package gg.norisk.heroes.client.ui.skilltree + + +//import me.cortex.nvidium.Nvidium +import gg.norisk.heroes.client.ui.components.HeroListComponentV2 +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.ui.components.LabelButtonComponent +import gg.norisk.ui.components.ScalableLabelComponent +import io.wispforest.owo.ui.base.BaseOwoScreen +import io.wispforest.owo.ui.container.Containers +import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.core.* +import io.wispforest.owo.ui.util.UISounds +import me.cortex.nvidium.Nvidium +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.MinecraftClient +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.core.text.literalText +import java.awt.Color + +class HeroSelectorScreenV2(val heroes: List, val isKitEditorEnabled: Boolean = false) : + BaseOwoScreen() { + var hero: Hero? = null + set(value) { + heroInfoComponent?.remove() + centerLabel.remove() + if (field != value) { + field = value + if (value != null) { + heroInfoComponent = heroAbility(value) + this.uiAdapter.rootComponent.child(heroInfoComponent) + } + } else { + field = null + this.uiAdapter.rootComponent.child(centerLabel) + } + } + var heroInfoComponent: FlowLayout? = null + var centerLabel: FlowLayout = Containers.verticalFlow(Sizing.fill(), Sizing.content()).apply { + child(ScalableLabelComponent(literalText { + text("CHOOSE YOUR HERO") + }, 3f).apply { + shadow(true) + }) + + alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER) + positioning(Positioning.relative(30, 40)) + } + + override fun createAdapter(): OwoUIAdapter { + return OwoUIAdapter.create(this, Containers::verticalFlow); + } + + val adapter get() = uiAdapter.rootComponent + + private fun heroAbility(hero: Hero): FlowLayout { + val container = Containers.verticalFlow(Sizing.content(), Sizing.content()) + .apply { positioning(Positioning.relative(50, 30)) } + + container.child(ScalableLabelComponent(literalText { + text(hero.name) { + } + }, 3f).apply { + shadow(true) + }) + container.alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER) + container.child(LabelButtonComponent("SKILL TREE".literal, Color.YELLOW).apply { + label.scale = 1.5f + mouseDown().subscribe { _, _, _ -> + UISounds.playButtonSound() + buildSkillTree(hero) + return@subscribe true + } + }) + return container + } + + private fun buildSkillTree(hero: Hero) { + uiAdapter.rootComponent.child(Containers.overlay(SkillTreeWrapper(hero))) + } + + private class XpLabel(scale: Float = 1f) : ScalableLabelComponent("".literal, scale) { + override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + text(literalText { + text("XP: ") + text((MinecraftClient.getInstance().player?.ffaPlayer?.xp ?: 0).toString()) + }) + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + } + + override fun close() { + super.close() + if (FabricLoader.getInstance().isModLoaded("nvidium")) { + Nvidium.FORCE_DISABLE = false + this.client?.worldRenderer?.reload() + } + } + + override fun build(root: FlowLayout) { + val heroList = HeroListComponentV2(heroes, this) + heroList.positioning(Positioning.relative(50, 90)) + + root.child(heroList) + if (hero == null) { + root.child(centerLabel) + } + + if (FabricLoader.getInstance().isModLoaded("nvidium")) { + Nvidium.FORCE_DISABLE = true + this.client?.worldRenderer?.reload() + } + } + + override fun shouldPause(): Boolean { + return false + } + + override fun shouldCloseOnEsc(): Boolean { + return FabricLoader.getInstance().isDevelopmentEnvironment + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/ISkill.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/ISkill.kt new file mode 100644 index 0000000..d267d40 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/ISkill.kt @@ -0,0 +1,17 @@ +package gg.norisk.heroes.client.ui.skilltree + +import io.wispforest.owo.ui.core.Component +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.text.Text + +interface ISkill { + fun isUnlocked(player: PlayerEntity): Boolean + fun isParentUnlocked(player: PlayerEntity): Boolean + fun title(): Text + fun parent(): ISkill? + fun progress(player: PlayerEntity): Double + fun skill() + fun isLast(): Boolean + fun tooltip(player: PlayerEntity): Text + fun icon(): Component? +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/SkillTreeScreen.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/SkillTreeScreen.kt new file mode 100644 index 0000000..1f16282 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/SkillTreeScreen.kt @@ -0,0 +1,30 @@ +package gg.norisk.heroes.client.ui.skilltree + + +//import me.cortex.nvidium.Nvidium +import gg.norisk.heroes.common.hero.getHero +import io.wispforest.owo.ui.base.BaseOwoScreen +import io.wispforest.owo.ui.container.Containers +import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.core.HorizontalAlignment +import io.wispforest.owo.ui.core.OwoUIAdapter +import io.wispforest.owo.ui.core.VerticalAlignment +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.DrawContext + +class SkillTreeScreen : BaseOwoScreen() { + + override fun createAdapter(): OwoUIAdapter { + return OwoUIAdapter.create(this, Containers::verticalFlow); + } + + + override fun build(root: FlowLayout) { + root.alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER) + root.child(SkillTreeWrapper(MinecraftClient.getInstance().player?.getHero() ?: return)) + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.render(context, mouseX, mouseY, delta) + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/SkillTreeUtils.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/SkillTreeUtils.kt new file mode 100644 index 0000000..b6a4a75 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/SkillTreeUtils.kt @@ -0,0 +1,186 @@ +package gg.norisk.heroes.client.ui.skilltree + +import gg.norisk.heroes.common.ability.CooldownProperty +import gg.norisk.heroes.common.ability.SingleUseProperty +import gg.norisk.heroes.common.command.DebugCommand.getProgressBar +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import gg.norisk.heroes.common.hero.ability.SkillPropertyPacket +import gg.norisk.heroes.common.networking.Networking +import io.wispforest.owo.ui.core.Component +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.text.Text +import net.silkmc.silk.core.text.literalText + +object SkillTreeUtils { + fun toSkillTree(ability: AbstractAbility<*>): TreeNode { + val root = TreeNode(object : ISkill { + override fun isUnlocked(player: PlayerEntity): Boolean { + return true + } + + override fun isParentUnlocked(player: PlayerEntity): Boolean { + return true + } + + override fun title(): Text { + return Text.translatable(ability.name) + } + + override fun parent(): ISkill? { + return null + } + + override fun progress(player: PlayerEntity): Double { + return 1.0 + } + + override fun skill() { + + } + + override fun isLast(): Boolean { + return false + } + + override fun tooltip(player: PlayerEntity): Text { + return Text.empty() + } + + override fun icon(): Component { + return ability.getIconComponent() + } + }) + for (property in ability.getAllProperties()) { + if (property is SingleUseProperty) continue + if (property is CooldownProperty) { + if (property.name == "NoCooldown") continue + } + var lastChild: TreeNode? = root + + repeat(property.maxLevel) { level -> + val newChild = TreeNode(object : ISkill { + val parent = lastChild?.value + override fun isUnlocked(player: PlayerEntity): Boolean { + val levelInfo = property.getLevelInfo(player.uuid) + levelInfo.percentageTillNextLevel + return levelInfo.currentLevel > level + } + + override fun isParentUnlocked(player: PlayerEntity): Boolean { + val levelInfo = property.getLevelInfo(player.uuid) + return levelInfo.currentLevel > level - 1 + } + + override fun title(): Text { + return literalText { + text(Text.translatable(property.translationKey)) + text(" ") + text(intToRoman(level + 1)) + } + } + + override fun parent(): ISkill? { + return parent + } + + override fun progress(player: PlayerEntity): Double { + val levelInfo = property.getLevelInfo(player.uuid) + if (levelInfo.currentLevel == level) { + return levelInfo.percentageTillNextLevel / 100.0 + } else if (levelInfo.currentLevel > level) { + return 1.0 + } else { + return 0.0 + } + } + + override fun skill() { + Networking.c2sSkillProperty.send( + SkillPropertyPacket( + ability.hero.internalKey, ability.internalKey, property.internalKey + ) + ) + } + + override fun isLast(): Boolean { + return level >= property.maxLevel - 1 + } + + private fun getValueText(value: T): Text { + return literalText { + // Überprüfe, ob der Wert eine Zahl ist + val formattedValue = when (value) { + is Number -> { + val doubleValue = value.toDouble() + // Überprüfe, ob der Wert Nachkommastellen hat + if (doubleValue % 1.0 == 0.0) { + doubleValue.toInt().toString() // Keine Nachkommastellen, nur die ganze Zahl + } else { + String.format("%.3f", doubleValue) // Formatiere auf 3 Nachkommastellen + } + } + else -> value.toString() // Andernfalls einfach den Wert als String + } + + text(formattedValue) + if (property is CooldownProperty) { + text("s") + } + } + } + + override fun tooltip(player: PlayerEntity): Text { + val levelInfo = property.getLevelInfo(player.uuid, level) + return literalText { + text("[") + text(Text.translatable(property.translationKey)) + text("]") + newLine() + text(Text.translatable(property.descriptionKey)) + emptyLine() + text("[") + text("Progress") + text("]") + newLine() + text( + getProgressBar( + levelInfo.percentageTillNextLevel, + 100.0, + 50, + "|".single() + ) + ) + text(" ${String.format("%.2f", levelInfo.percentageTillNextLevel)}%") + emptyLine() + text(getValueText(property.getValue(level + 1))) + } + } + + override fun icon(): Component { + return property.icon.invoke() + } + }) + lastChild?.addChild(newChild) + lastChild = newChild + } + } + + return root + } + + private val m_k = listOf(1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1) + private val m_v = listOf("M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I") + + fun intToRoman(num: Int): String { + var str = "" + var n = num + + for (i in m_k.indices) { + while (n >= m_k[i]) { + n -= m_k[i] + str += m_v[i] + } + } + return str + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/SkillTreeWrapper.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/SkillTreeWrapper.kt new file mode 100644 index 0000000..d856076 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/SkillTreeWrapper.kt @@ -0,0 +1,186 @@ +package gg.norisk.heroes.client.ui.skilltree + +import gg.norisk.heroes.common.HeroesManager.toId +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.component.TextureComponent +import io.wispforest.owo.ui.container.Containers +import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.core.* +import io.wispforest.owo.ui.util.UISounds +import net.minecraft.client.MinecraftClient +import net.minecraft.client.sound.PositionedSoundInstance +import net.minecraft.sound.SoundEvents +import net.minecraft.util.Colors +import net.minecraft.util.Identifier +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.core.text.literalText + +class SkillTreeWrapper( + val hero: Hero, + horizontalSizing: Sizing = Sizing.content(), + verticalSizing: Sizing = Sizing.content() +) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) { + val skillTreeWrapper = Containers.horizontalFlow(Sizing.content(), Sizing.content()) + val tabs = mutableListOf() + + init { + var index = 0 + for ((name, ability) in hero.abilities) { + tabs += TabButton(ability, index) + index++ + } + + child(TabWrapper().apply { + children(tabs) + //zIndex(5000) + allowOverflow(true) + gap(2) + }) + child(skillTreeWrapper) + + tabs.first { it.ability.hasUnlocked(MinecraftClient.getInstance().player!!) }.apply { + this.isSelected = true + this.onClick() + } + + //children(tabs) + } + + override fun drawChildren( + context: OwoUIDrawContext, + mouseX: Int, + mouseY: Int, + partialTicks: Float, + delta: Float, + children: MutableList + ) { + super.drawChildren(context, mouseX, mouseY, partialTicks, delta, children.reversed()) + } + + override fun draw(context: OwoUIDrawContext?, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + + inner class TabWrapper( + horizontalSizing: Sizing = Sizing.content(), + verticalSizing: Sizing = Sizing.content() + ) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.HORIZONTAL) { + } + + inner class TabButton( + val ability: AbstractAbility<*>, + index: Int, + horizontalSizing: Sizing = Sizing.fixed(28), + verticalSizing: Sizing = Sizing.fixed(28) + ) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.VERTICAL) { + var page = AbilitySkillTreeComponent(ability) + var isSelected = false + var item = ability.getIconComponent().id("unlocked") + var lockIcon = Components.texture("textures/gui/lock_icon.png".toId(), 0, 0, 20, 20, 20, 20).apply { + id("locked") + tooltip(literalText { + text(ability.name) + newLine() + text(ability.getUnlockCondition()) { + color = Colors.LIGHT_GRAY + } + }) + } + + init { + surface { context, container -> + context.matrices.push() + val texture = if (isSelected) { + if (index == 0) { + Identifier.of("textures/gui/sprites/advancements/tab_above_left_selected.png") + } else { + Identifier.of("textures/gui/sprites/advancements/tab_above_middle_selected.png") + } + } else { + if (index == 0) { + Identifier.of("textures/gui/sprites/advancements/tab_above_left.png") + } else { + Identifier.of("textures/gui/sprites/advancements/tab_above_middle.png") + } + } + context.drawTexture( + texture, + container.x(), + container.y(), + 0f, + 0f, + 28, + 32, + 28, + 32 + ) + // NinePatchTexture.draw(OwoUIDrawContext.PANEL_NINE_PATCH_TEXTURE, context, container.x(), container.y(), container.width(), container.height()) + context.matrices.pop() + } + mouseDown().subscribe { _, _, _ -> + if (!ability.hasUnlocked(MinecraftClient.getInstance().player!!)) { + MinecraftClient.getInstance().soundManager.play( + PositionedSoundInstance.master( + SoundEvents.ENTITY_VILLAGER_NO, + 1.0f + ) + ) + return@subscribe true + } + if (!isSelected) { + UISounds.playInteractionSound() + isSelected = !isSelected + tabs.filter { it != this }.forEach { it.isSelected = false } + onClick() + } + return@subscribe true + } + tooltip(ability.name.literal) + alignment(HorizontalAlignment.CENTER, VerticalAlignment.CENTER) + //positioning(Positioning.absolute(0, 0)) + child(lockIcon) + //zIndex(-5000) + allowOverflow(true) + } + + fun onClick() { + if (isSelected) { + skillTreeWrapper.clearChildren() + skillTreeWrapper.child(page) + } + } + + override fun draw(context: OwoUIDrawContext, mouseX: Int, mouseY: Int, partialTicks: Float, delta: Float) { + val hasUnlocked = ability.hasUnlocked(MinecraftClient.getInstance().player!!) + val locked = childById(TextureComponent::class.java, "locked") + val unlocked = childById(Component::class.java, "unlocked") + val component = if (hasUnlocked) { + if (unlocked == null) { + child(item) + } + if (locked != null) { + removeChild(locked) + } + item + } else { + if (locked == null) { + child(lockIcon) + } + if (unlocked != null) { + removeChild(item) + } + lockIcon + } + + + if (isSelected) { + component.margins(Insets.none()) + } else { + component.margins(Insets.top(8)) + } + super.draw(context, mouseX, mouseY, partialTicks, delta) + } + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/TreeNode.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/TreeNode.kt new file mode 100644 index 0000000..5bacb7a --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/client/ui/skilltree/TreeNode.kt @@ -0,0 +1,28 @@ +package gg.norisk.heroes.client.ui.skilltree + +class TreeNode(val value: T) { + val children: MutableList> = mutableListOf() + + fun addChild(child: TreeNode): TreeNode { + children.add(child) + return this + } + + // Tiefensuche (Depth-First Search) + fun dfs(visit: (T) -> Unit) { + visit(value) + children.forEach { it.dfs(visit) } + } + + // Breitensuche (Breadth-First Search) + fun bfs(visit: (T) -> Unit) { + val queue = ArrayDeque>() + queue.add(this) + + while (queue.isNotEmpty()) { + val currentNode = queue.removeFirst() + visit(currentNode.value) + currentNode.children.forEach { queue.add(it) } + } + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/HeroesManager.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/HeroesManager.kt new file mode 100644 index 0000000..273c653 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/HeroesManager.kt @@ -0,0 +1,76 @@ +package gg.norisk.heroes.common + +import gg.norisk.heroes.common.registry.SoundRegistry +import gg.norisk.heroes.server.HeroesManagerServer +import net.fabricmc.api.EnvType +import net.fabricmc.api.ModInitializer +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.resource.featuretoggle.FeatureFlag +import net.minecraft.util.Identifier +import net.minecraft.util.WorldSavePath +import net.silkmc.silk.core.text.literalText +import org.apache.logging.log4j.LogManager +import java.io.File +import java.nio.file.Path + + +object HeroesManager : ModInitializer { + const val MOD_ID = "hero-api" + var baseDirectory: File = getBasePath(null) + val logger = LogManager.getLogger(MOD_ID) + fun String.toId() = Identifier.of(MOD_ID, this) + + lateinit var heroesFlag: FeatureFlag + + val prefix + get() = literalText { + text("[") { } + text("Heroes") { } + text("]") { } + text(" ") + } + + val isServer get() = FabricLoader.getInstance().isDevelopmentEnvironment || FabricLoader.getInstance().environmentType == EnvType.SERVER + val isClient get() = FabricLoader.getInstance().isDevelopmentEnvironment || FabricLoader.getInstance().environmentType == EnvType.CLIENT + + override fun onInitialize() { + logger.info("Init Hero-Api Common...") + SoundRegistry.init() + HeroesManagerServer.initServer() + + ServerLifecycleEvents.SERVER_STARTING.register { + setBasePath(it.getSavePath(WorldSavePath("heroes"))) + logger.info("Found Server Path: ${baseDirectory}") + } + } + + private fun getBasePath(serverPath: Path?): File { + val defaultPath = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) { + FabricLoader.getInstance().configDir + } else { + serverPath ?: FabricLoader.getInstance().configDir + } + + val baseDirectory = File( + System.getProperty( + "hero_folder_path", + defaultPath.toFile().absolutePath + ), + ).apply { + mkdirs() + } + + return baseDirectory + } + + private fun setBasePath(serverPath: Path?) { + this.baseDirectory = getBasePath(serverPath) + } + + fun client(callBack: () -> Unit) { + if (FabricLoader.getInstance().environmentType == EnvType.CLIENT) { + callBack.invoke() + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/AbstractNumberProperty.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/AbstractNumberProperty.kt new file mode 100644 index 0000000..3d0f5de --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/AbstractNumberProperty.kt @@ -0,0 +1,50 @@ +package gg.norisk.heroes.common.ability + +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.ability.operation.MultiplyBase +import gg.norisk.heroes.common.ability.operation.Operation +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.encodeToJsonElement +import java.util.* + +@Serializable +sealed class AbstractNumberProperty : PlayerProperty() { + abstract var modifier: Operation + + override fun getValue(uuid: UUID): Double { + return getValue(getLevelInfo(uuid).currentLevel) + } + + override fun getValue(int: Int): Double { + return when (modifier) { + is AddValueTotal, is MultiplyBase -> { + modifier.getOperatedValue(baseValue, int) + } + } + } + + override fun fromJson(text: String) { + runCatching { + val loaded = JSON.decodeFromString(text) + baseValue = loaded.baseValue + maxLevel = loaded.maxLevel + name = loaded.name + levelScale = loaded.levelScale + modifier = loaded.modifier + }.onFailure { + HeroesManager.logger.error("Error Loading $name ${it.message}") + it.printStackTrace() + } + } + + override fun toJson(): String { + return JSON.encodeToString>(this) + } + + override fun toJsonElement(): JsonElement { + return JSON.encodeToJsonElement>(this) + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/AbstractUsageProperty.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/AbstractUsageProperty.kt new file mode 100644 index 0000000..aa0dcba --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/AbstractUsageProperty.kt @@ -0,0 +1,6 @@ +package gg.norisk.heroes.common.ability + +import kotlinx.serialization.Serializable + +@Serializable +sealed class AbstractUsageProperty : AbstractNumberProperty() \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/CooldownProperty.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/CooldownProperty.kt new file mode 100644 index 0000000..6883f5b --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/CooldownProperty.kt @@ -0,0 +1,15 @@ +package gg.norisk.heroes.common.ability + +import gg.norisk.heroes.common.ability.operation.Operation +import kotlinx.serialization.Serializable + +@Serializable +open class CooldownProperty( + override var baseValue: Double, + override var maxLevel: Int, + override var name: String, + override var modifier: Operation, + override var levelScale: Int = PlayerProperty.levelScale +) : AbstractNumberProperty() { + +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/LevelInformation.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/LevelInformation.kt new file mode 100644 index 0000000..1c71ac0 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/LevelInformation.kt @@ -0,0 +1,12 @@ +package gg.norisk.heroes.common.ability + +data class LevelInformation( + val currentLevel: Int, + val nextLevel: Int, + val xpCurrentLevel: Int, + val xpNextLevel: Int, + val xpTillNextLevel: Int, + val percentageTillNextLevel: Double, + val experiencePoints: Int, + val maxLevel: Int, +) diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/MultiUseProperty.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/MultiUseProperty.kt new file mode 100644 index 0000000..93853c7 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/MultiUseProperty.kt @@ -0,0 +1,18 @@ +package gg.norisk.heroes.common.ability + +import gg.norisk.heroes.common.ability.operation.Operation +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import java.util.* + +@Serializable +class MultiUseProperty( + override var baseValue: Double, + override var maxLevel: Int, + override var name: String, + override var modifier: Operation, + override var levelScale: Int = PlayerProperty.levelScale +) : AbstractUsageProperty() { + @Transient + val uses = mutableMapOf() +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/NumberProperty.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/NumberProperty.kt new file mode 100644 index 0000000..8f0d743 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/NumberProperty.kt @@ -0,0 +1,16 @@ +package gg.norisk.heroes.common.ability + +import gg.norisk.heroes.common.ability.operation.Operation +import kotlinx.serialization.Serializable + + +@Serializable +class NumberProperty( + override var baseValue: Double, + override var maxLevel: Int, + override var name: String, + override var modifier: Operation, + override var levelScale: Int = PlayerProperty.levelScale, +) : AbstractNumberProperty() { + +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/PlayerProperty.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/PlayerProperty.kt new file mode 100644 index 0000000..e178913 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/PlayerProperty.kt @@ -0,0 +1,149 @@ +package gg.norisk.heroes.common.ability + +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import gg.norisk.heroes.server.database.player.PlayerProvider +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import net.minecraft.item.Items +import java.util.* +import kotlin.math.cbrt +import kotlin.math.pow + +@Serializable +sealed class PlayerProperty { + abstract var baseValue: T + abstract var maxLevel: Int + abstract var name: String + abstract var levelScale: Int + + @Transient + var icon: () -> Component = { + Components.item(Items.CLOCK.defaultStack) + } + + @Transient + lateinit var hero: Hero + + @Transient + lateinit var ability: AbstractAbility<*> + + companion object { + val levelScale = 250 + + val JSON = Json { + prettyPrint = true + encodeDefaults = true + ignoreUnknownKeys = true + //explicitNulls = true + } + } + + abstract fun getValue(uuid: UUID): T + abstract fun getValue(int: Int): T + abstract fun fromJson(text: String) + abstract fun toJson(): String + abstract fun toJsonElement(): JsonElement + open fun getMaxValue(): T { + return getValue(maxLevel) + } + + fun addExperience(uuid: UUID, experienceToAdd: Int): Int { + val player = getOrLoadPlayer(uuid) + val maxExperience = getXpForLevel(maxLevel) + + val buffer = player.experiencePoints + experienceToAdd + return if (buffer > maxExperience) { + val toAdd = maxExperience - player.experiencePoints // Nur bis zum Maximalwert hinzufügen + player.experiencePoints = maxExperience // Korrigiere Erfahrungspunkte auf Maximum + toAdd + } else { + player.experiencePoints += experienceToAdd + experienceToAdd + } + } + + fun isMaxed(uuid: UUID): Boolean { + return getLevelInfo(uuid).currentLevel >= maxLevel + } + + fun getLevelInfo(uuid: UUID, level: Int? = null): LevelInformation { + val player = getOrLoadPlayer(uuid) + val currentLevel = Math.min(maxLevel, level ?: calculateLevel(player.experiencePoints)) + val nextLevel = Math.min(maxLevel, currentLevel + 1) + + val xpCurrentLevel = getXpForLevel(currentLevel) + + val xpNextLevel = if (currentLevel < maxLevel) { + getXpForLevel(nextLevel) + } else { + xpCurrentLevel // Kein weiteres Level, bleibt gleich + } + + val xpTillNextLevel = if (currentLevel < maxLevel) { + xpNextLevel - player.experiencePoints + } else { + 0 // Kein weiteres Level + } + + val percentageTillNextLevel = if (currentLevel < maxLevel) { + Math.max( + 0.0, Math.min( + 100.0, + ((player.experiencePoints - xpCurrentLevel).toDouble() / (xpNextLevel - xpCurrentLevel).toDouble()) * 100.0 + ) + ) + } else { + 100.0 // Max-Level erreicht + } + + if (currentLevel == maxLevel - 1 && percentageTillNextLevel >= 100f) { + return LevelInformation( + maxLevel, + maxLevel, + xpCurrentLevel, + xpNextLevel, + xpTillNextLevel, + percentageTillNextLevel, + player.experiencePoints, + maxLevel + ) + } + + return LevelInformation( + currentLevel, + nextLevel, + xpCurrentLevel, + xpNextLevel, + xpTillNextLevel, + percentageTillNextLevel, + player.experiencePoints, + maxLevel + ) + } + + private fun calculateLevel(xp: Int): Int { + return cbrt((xp / levelScale).toDouble()).toInt() + } + + private fun getXpForLevel(level: Int): Int { + return (levelScale * level.toDouble().pow(3)).toInt() + } + + val internalKey get() = name.lowercase().replace(" ", "_") + val translationKey get() = "heroes.property.${internalKey}" + val descriptionKey get() = "heroes.property.${internalKey}.description" + + private fun getOrLoadPlayer(uuid: UUID): PropertyPlayer { + val player = runBlocking { PlayerProvider.get(uuid) } + val heroMap = player.heroes.computeIfAbsent(hero.internalKey) { mutableMapOf() } + val abilityMap = heroMap.computeIfAbsent(ability.internalKey) { mutableMapOf() } + val property = abilityMap.computeIfAbsent(internalKey) { PropertyPlayer() } + return property + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/PropertyPlayer.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/PropertyPlayer.kt new file mode 100644 index 0000000..a0ca91d --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/PropertyPlayer.kt @@ -0,0 +1,9 @@ +package gg.norisk.heroes.common.ability + +import kotlinx.serialization.Serializable + +@Serializable +data class PropertyPlayer( + var experiencePoints: Int = 0 +) { +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/SingleUseProperty.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/SingleUseProperty.kt new file mode 100644 index 0000000..52aa6ca --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/SingleUseProperty.kt @@ -0,0 +1,15 @@ +package gg.norisk.heroes.common.ability + +import gg.norisk.heroes.common.ability.operation.Operation +import kotlinx.serialization.Serializable + +@Serializable +class SingleUseProperty( + override var baseValue: Double, + override var maxLevel: Int, + override var name: String, + override var modifier: Operation, + override var levelScale: Int = PlayerProperty.levelScale +) : AbstractUsageProperty() { + +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/operation/AddValueTotal.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/operation/AddValueTotal.kt new file mode 100644 index 0000000..155132c --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/operation/AddValueTotal.kt @@ -0,0 +1,20 @@ +package gg.norisk.heroes.common.ability.operation + +import kotlinx.serialization.Serializable + +@Serializable +class AddValueTotal(var steps: List) : Operation() { + + constructor(vararg steps: Double) : this(steps.toList()) + + override fun getOperatedValue(baseValue: Double, level: Int): Double { + // wir doublen alles wegen... jo + var valueToReturn = baseValue + repeat(level) { + val increment = steps.getOrNull(it) ?: error("$steps doesn't have an index for level $it") + valueToReturn += increment + } + + return valueToReturn + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/operation/MultiplyBase.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/operation/MultiplyBase.kt new file mode 100644 index 0000000..7facd29 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/operation/MultiplyBase.kt @@ -0,0 +1,14 @@ +package gg.norisk.heroes.common.ability.operation + +import kotlinx.serialization.Serializable + +@Serializable +class MultiplyBase(var steps: List) : Operation() { + constructor(vararg steps: Double) : this(steps.toList()) + override fun getOperatedValue(baseValue: Double, level: Int): Double { + //wir doublen alles wegen... jo + val increment = steps.getOrNull(level) ?: error("$steps doesn't have an index for level $level") + + return baseValue * increment + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/operation/Operation.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/operation/Operation.kt new file mode 100644 index 0000000..88e4749 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ability/operation/Operation.kt @@ -0,0 +1,8 @@ +package gg.norisk.heroes.common.ability.operation + +import kotlinx.serialization.Serializable + +@Serializable +sealed class Operation { + abstract fun getOperatedValue(baseValue: Double, level: Int): Double +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/command/DebugCommand.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/command/DebugCommand.kt new file mode 100644 index 0000000..b59033d --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/command/DebugCommand.kt @@ -0,0 +1,224 @@ +package gg.norisk.heroes.common.command + +import com.mojang.brigadier.context.CommandContext +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.ability.PlayerProperty +import gg.norisk.heroes.common.command.EditPropertyCommand.editCommand +import gg.norisk.heroes.common.ffa.KitEditorManager +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.HeroManager +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import gg.norisk.heroes.common.networking.Networking +import gg.norisk.heroes.common.networking.dto.HeroSelectorPacket +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.heroes.server.database.player.PlayerProvider +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.command.argument.EntityArgumentType +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.text.Text +import net.silkmc.silk.commands.PermissionLevel +import net.silkmc.silk.commands.command +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.core.text.literalText + +object DebugCommand { + fun initServer() { + command("heroes") { + requiresPermissionLevel(PermissionLevel.OWNER) + //if (HeroesManager.isClient) { + requires { it.server.saveProperties.dataConfiguration.enabledFeatures.contains(HeroesManager.heroesFlag) } + // } + runs { + Networking.s2cHeroSelectorPacket.send( + HeroSelectorPacket(HeroManager.registeredHeroes.keys.toList(), true, KitEditorManager.hasKitWorld), + this.source.playerOrThrow + ) + } + literal("reload") { + runs { + HeroManager.reloadHeroes(*HeroManager.registeredHeroes.values.toTypedArray()) + } + } + literal("xp") { + literal("set") { + argument("players", EntityArgumentType.players()) { + argument("xp") { xp -> + runsAsync { + val players = EntityArgumentType.getPlayers(this, "players") + for (player in players) { + val cachedPlayer = PlayerProvider.get(player.uuid) + cachedPlayer.xp = xp() + player.ffaPlayer = cachedPlayer + PlayerProvider.save(player.ffaPlayer) + this.source.sendMessage("Set Xp of ${player.gameProfile.name} to ${xp()}".literal) + } + } + } + } + } + } + argument("hero") { heroKey -> + suggestList { HeroManager.registeredHeroes.keys } + literal("ability") { + argument("ability") { abilityKey -> + suggestList { + HeroManager.getHero(heroKey(it))?.abilities?.keys + } + literal("property") { + argument("property") { propertyKey -> + suggestList { + HeroManager.getHero(heroKey(it))?.abilities + ?.values + ?.map { ability -> ability.getAllProperties() } + ?.flatten()?.map { property -> property.internalKey } + } + editCommand() + literal("add") { + argument("expierencepoints") { xpPoints -> + runs { + val hero = HeroManager.getHero(heroKey())!! + + val ability = hero.abilities.values + .flatMap { ability -> + ability.getAllProperties().map { property -> ability to property } + } + .firstOrNull { (ability, property) -> + property.internalKey == propertyKey() && ability.internalKey == abilityKey() + }!! + + val player = this.source.playerOrThrow + ability.second.addExperience(player.uuid, xpPoints()) + sendLevelInfo(player, player, ability.second, ability.first) + } + } + } + literal("info") { + runs { + val hero = HeroManager.getHero(heroKey())!! + val ability = hero.abilities.values + .flatMap { ability -> + ability.getAllProperties().map { property -> ability to property } + } + .firstOrNull { (ability, property) -> + property.internalKey == propertyKey() && ability.internalKey == abilityKey() + }!! + val player = this.source.playerOrThrow + sendLevelInfo(player, player, ability.second, ability.first) + } + } + } + } + } + } + } + } + } + + fun CommandContext.getHeroInformation(): Triple, PlayerProperty<*>> { + val hero = HeroManager.getHero(this.getArgument("hero", String::class.java))!! + val propertyKey = this.getArgument("property", String::class.java) + val abilityKey = this.getArgument("ability", String::class.java) + + val ability = hero.abilities.values + .flatMap { ability -> + ability.getAllProperties().map { property -> ability to property } + } + .firstOrNull { (ability, property) -> + property.internalKey == propertyKey && ability.internalKey == abilityKey + }!! + + return Triple(hero, ability.first, ability.second) + } + + fun PlayerProperty<*>.toDebugText(): Text { + return literalText { + text(Text.translatable(this@toDebugText.translationKey)) + newLine() + text("Max Level: ${this@toDebugText.maxLevel}") + newLine() + text("Base Value: ${this@toDebugText.baseValue}") + } + } + + fun PlayerEntity.sendDebugMessage(message: Text) { + if (FabricLoader.getInstance().isDevelopmentEnvironment) { + sendMessage(message) + } + } + + fun sendLevelInfo( + player: PlayerEntity, + about: PlayerEntity, + property: PlayerProperty<*>, + ability: AbstractAbility<*> + ) { + val levelInfo = property.getLevelInfo(about.uuid) + player.sendMessage(literalText { + emptyLine() + text("Level Info for ${property.name}") { + underline = true + } + emptyLine() + text("Player: ") + text(player.name) + text(" ${player.uuid}") + newLine() + text("Ability: ${ability.name}") + newLine() + text("Current Value: ${property.getValue(about.uuid)}") + newLine() + text("Current Level: ${levelInfo.currentLevel}/${levelInfo.maxLevel}") + newLine() + text("Next Level: ${levelInfo.nextLevel}") + newLine() + text("Step: ${levelInfo.experiencePoints}/${levelInfo.xpNextLevel}") + newLine() + text("Xp Needed for Upgrade: ${levelInfo.xpTillNextLevel}") { } + newLine() + text("Progress: ") + text( + getProgressBar( + levelInfo.percentageTillNextLevel, + 100.0, + 50, + "|".single() + ) + ) + text(" ${String.format("%.3f", levelInfo.percentageTillNextLevel)}%") + }) + } + + fun getProgressBar( + current: Double, + max: Double, + totalBars: Int, + symbol: Char, + completedColor: Int = getProgressBarColor(current, max), + notCompletedColor: Int = 0xa1a1a1 + ): Text { + val percent = current.toFloat() / max + val progressBars = (totalBars * percent).toInt() + + return literalText { + repeat(progressBars) { + text("" + symbol) { + color = completedColor + } + } + repeat(totalBars - progressBars) { + text("" + symbol) { + color = notCompletedColor + } + } + } + } + + fun getProgressBarColor(progress: Double, maxProgress: Double): Int { + val percentage = progress / maxProgress * 100.0 + return when { + percentage > 66 -> 0x6fff36 + percentage > 30 -> 0xfff700 + else -> 0xff0000 + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/command/EditPropertyCommand.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/command/EditPropertyCommand.kt new file mode 100644 index 0000000..4829f1f --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/command/EditPropertyCommand.kt @@ -0,0 +1,278 @@ +package gg.norisk.heroes.common.command + +import com.mojang.brigadier.arguments.StringArgumentType +import gg.norisk.heroes.common.ability.AbstractNumberProperty +import gg.norisk.heroes.common.ability.PlayerProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.ability.operation.MultiplyBase +import gg.norisk.heroes.common.command.DebugCommand.getHeroInformation +import gg.norisk.heroes.server.config.ConfigManagerServer +import net.minecraft.server.command.ServerCommandSource +import net.silkmc.silk.commands.ArgumentCommandBuilder +import net.silkmc.silk.commands.LiteralCommandBuilder +import net.silkmc.silk.core.text.broadcastText +import java.awt.Color + +object EditPropertyCommand { + fun ArgumentCommandBuilder.editCommand() { + literal("edit") { + baseValue() + maxLevel() + levelScale() + operation() + } + } + + private fun LiteralCommandBuilder.operation() { + literal("operation") { + literal("type") { + argument("type") { typeString -> + suggestList { + val type = (it.getHeroInformation().third as AbstractNumberProperty).modifier::class.simpleName + val set = mutableSetOf(type, MultiplyBase::class.simpleName, AddValueTotal::class.simpleName) + set.toList() + } + runs { + val (hero, ability, property) = this.getHeroInformation() + val oldModifier = (property as? AbstractNumberProperty?)?.modifier + val source = this.source.displayName + val newValue = when (oldModifier) { + is AddValueTotal -> { + oldModifier.steps + } + + is MultiplyBase -> { + oldModifier.steps + } + + null -> TODO() + } + when(typeString()) { + AddValueTotal::class.simpleName -> { + (property as? AbstractNumberProperty?)?.modifier = AddValueTotal(newValue) + } + MultiplyBase::class.simpleName -> { + (property as? AbstractNumberProperty?)?.modifier = MultiplyBase(newValue) + } + } + runCatching { + hero.save() + hero.load() + }.onSuccess { + this.source.server.broadcastText { + text(source) + text(" changed ${hero.name}, ${ability.name}, ${property.name}, modifier") + newLine() + text("- ${oldModifier::class.simpleName}") { + color = Color.RED.rgb + } + newLine() + text("+ ${(property as? AbstractNumberProperty?)!!.modifier::class.simpleName}") { + color = Color.GREEN.rgb + } + } + }.onFailure { + this.source.server.broadcastText { + text(source) + text(" changed ${hero.name}, ${ability.name}, ${property.name}, modifier") + newLine() + text("ERROR ${it.message}") + } + it.printStackTrace() + }.also { + ConfigManagerServer.sendHeroSettings(hero) + } + } + } + } + literal("list") { + argument("values", StringArgumentType.string()) { valuesString -> + suggestList { + when (val modifier = (it.getHeroInformation().third as? AbstractNumberProperty?)?.modifier) { + is AddValueTotal -> listOf("\"${modifier.steps}\"") + is MultiplyBase -> listOf("\"${modifier.steps}\"") + else -> listOf("\"SCHREIB NORISK AN\"") + } + } + runs { + val (hero, ability, property) = this.getHeroInformation() + val modifier = (property as? AbstractNumberProperty?)?.modifier + val possibleList = valuesString() + var oldValue: Any? = null + val source = this.source.displayName + when (modifier) { + is AddValueTotal -> { + oldValue = modifier.steps + modifier.steps = PlayerProperty.JSON.decodeFromString>(possibleList) + } + + is MultiplyBase -> { + oldValue = modifier.steps + modifier.steps = PlayerProperty.JSON.decodeFromString>(possibleList) + } + + null -> TODO() + } + + runCatching { + hero.save() + hero.load() + }.onSuccess { + this.source.server.broadcastText { + text(source) + text(" changed ${hero.name}, ${ability.name}, ${property.name}, modifier") + newLine() + + val newValue = when (modifier) { + is AddValueTotal -> { + modifier.steps + } + + is MultiplyBase -> { + modifier.steps + } + } + + text("- $oldValue") { + color = Color.RED.rgb + } + newLine() + text("+ $newValue") { + color = Color.GREEN.rgb + } + } + }.onFailure { + this.source.server.broadcastText { + text(source) + text(" changed ${hero.name}, ${ability.name}, ${property.name}, modifier") + newLine() + text("ERROR ${it.message}") + } + it.printStackTrace() + }.also { + ConfigManagerServer.sendHeroSettings(hero) + } + } + } + } + } + } + + private fun LiteralCommandBuilder.levelScale() { + literal("levelScale") { + argument("value") { value -> + suggestList { + listOf(it.getHeroInformation().third.levelScale) + } + runs { + val (hero, ability, property) = this.getHeroInformation() + if (property is AbstractNumberProperty) { + val oldValue = property.levelScale + property.levelScale = value() + val source = this.source.displayName + runCatching { + hero.save() + hero.load() + }.onSuccess { + this.source.server.broadcastText { + text(source) + text(" changed ${hero.name}, ${ability.name}, ${property.name}, levelScale") + newLine() + text("from $oldValue to ${property.baseValue}") + } + }.onFailure { + this.source.server.broadcastText { + text(source) + text(" changed ${hero.name}, ${ability.name}, ${property.name}, levelScale") + newLine() + text("ERROR ${it.message}") + } + it.printStackTrace() + }.also { + ConfigManagerServer.sendHeroSettings(hero) + } + } + } + } + } + } + + + private fun LiteralCommandBuilder.maxLevel() { + literal("maxLevel") { + argument("value") { value -> + suggestList { + listOf(it.getHeroInformation().third.maxLevel) + } + runs { + val (hero, ability, property) = this.getHeroInformation() + if (property is AbstractNumberProperty) { + val oldValue = property.maxLevel + property.maxLevel = value() + val source = this.source.displayName + runCatching { + hero.save() + hero.load() + }.onSuccess { + this.source.server.broadcastText { + text(source) + text(" changed ${hero.name}, ${ability.name}, ${property.name}, maxLevel") + newLine() + text("from $oldValue to ${property.baseValue}") + } + }.onFailure { + this.source.server.broadcastText { + text(source) + text(" changed ${hero.name}, ${ability.name}, ${property.name}, maxLevel") + newLine() + text("ERROR ${it.message}") + } + it.printStackTrace() + }.also { + ConfigManagerServer.sendHeroSettings(hero) + } + } + } + } + } + } + + private fun LiteralCommandBuilder.baseValue() { + literal("baseValue") { + argument("value") { value -> + suggestList { + listOf(it.getHeroInformation().third.baseValue) + } + runs { + val (hero, ability, property) = this.getHeroInformation() + if (property is AbstractNumberProperty) { + val oldValue = property.baseValue + property.baseValue = value().toDouble() + val source = this.source.displayName + runCatching { + hero.save() + hero.load() + }.onSuccess { + this.source.server.broadcastText { + text(source) + text(" changed ${hero.name}, ${ability.name}, ${property.name}, baseValue") + newLine() + text("from $oldValue to ${property.baseValue}") + } + }.onFailure { + this.source.server.broadcastText { + text(source) + text(" changed ${hero.name}, ${ability.name}, ${property.name}, baseValue") + newLine() + text("ERROR ${it.message}") + } + it.printStackTrace() + }.also { + ConfigManagerServer.sendHeroSettings(hero) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/cooldown/CooldownInfo.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/cooldown/CooldownInfo.kt new file mode 100644 index 0000000..25b9fad --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/cooldown/CooldownInfo.kt @@ -0,0 +1,25 @@ +package gg.norisk.heroes.common.cooldown + +import kotlinx.serialization.Serializable + +@Serializable +data class CooldownInfo( + val entityId: Int, + val duration: Long, + val startTime: Long?, + val currentTime: Long, + val multipleUsesInfo: MultipleUsesInfo?, + val heroKey: String, + val abilityKey: String, + val endTime: Long?, + var durationString: String? = null, +) { + val hasEnded get() = endTime?.let { System.nanoTime() > it } ?: true + val remaining get() = endTime?.let { it - System.nanoTime() } ?: 0 +} + +@Serializable +data class MultipleUsesInfo( + val currentUse: Int, + val maxUses: Int +) \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/AbstractProvider.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/AbstractProvider.kt new file mode 100644 index 0000000..a495ae3 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/AbstractProvider.kt @@ -0,0 +1,26 @@ +package gg.norisk.heroes.common.database + +import net.fabricmc.api.EnvType +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.server.network.ServerPlayerEntity + +abstract class AbstractProvider { + protected val cache = hashMapOf() + abstract suspend fun save(data: V) + abstract suspend fun get(uuid: K): V + + abstract suspend fun onPlayerJoin(player: ServerPlayerEntity) + abstract suspend fun onPlayerLeave(player: ServerPlayerEntity) + + abstract fun getCachedClient(uuid: K): V? + + protected suspend fun getCached(uuid: K): V? { + if (FabricLoader.getInstance().environmentType == EnvType.CLIENT && !FabricLoader.getInstance().isDevelopmentEnvironment) { + val cachedOnClient = getCachedClient(uuid) + if (cachedOnClient != null) { + return cachedOnClient + } + } + return cache[uuid] + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/inventory/AbstractInventoryProvider.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/inventory/AbstractInventoryProvider.kt new file mode 100644 index 0000000..994bd0b --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/inventory/AbstractInventoryProvider.kt @@ -0,0 +1,20 @@ +package gg.norisk.heroes.common.database.inventory + +import gg.norisk.heroes.common.database.AbstractProvider +import gg.norisk.heroes.common.player.InventorySorting +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.heroes.common.utils.PlayStyle +import net.minecraft.client.MinecraftClient +import net.minecraft.server.network.ServerPlayerEntity +import java.util.* + +abstract class AbstractInventoryProvider(val playStyle: PlayStyle) : AbstractProvider() { + + override suspend fun onPlayerJoin(player: ServerPlayerEntity) {} + override suspend fun onPlayerLeave(player: ServerPlayerEntity) {} + + override fun getCachedClient(uuid: UUID): InventorySorting? { + val ffaPlayer = MinecraftClient.getInstance().world?.getPlayerByUuid(uuid)?.ffaPlayer + return ffaPlayer?.inventorySorting + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/inventory/JsonInventoryProvider.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/inventory/JsonInventoryProvider.kt new file mode 100644 index 0000000..47c6794 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/inventory/JsonInventoryProvider.kt @@ -0,0 +1,65 @@ +package gg.norisk.heroes.common.database.inventory + +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.player.InventorySorting +import gg.norisk.heroes.common.utils.PlayStyle +import gg.norisk.heroes.common.utils.createIfNotExists +import gg.norisk.heroes.server.config.ConfigManagerServer.JSON +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import java.util.* + +class JsonInventoryProvider : AbstractInventoryProvider(PlayStyle.current) { + private val file get() = HeroesManager.baseDirectory.resolve("player-inventory-database.json").createIfNotExists() + + private fun loadDatabase(): MutableSet { + var database = mutableSetOf() + runCatching { + if (file.exists()) { + database = JSON.decodeFromString(file.readText()) + } + }.onFailure { + if (file.readText().isBlank()) { + file.writeText("[]") + } + logger.info("Error Reading File ${file.absolutePath}") + it.printStackTrace() + } + return database + } + + private suspend fun find(uuid: UUID): InventorySorting? { + val database = loadDatabase() + return database.find { it.userId == uuid && it.playStyle == playStyle } + } + + override suspend fun get(uuid: UUID): InventorySorting? { + val inventory = getCached(uuid) ?: find(uuid) + cache[uuid] = inventory + return inventory + } + + override suspend fun save(data: InventorySorting?) { + if (data == null) { + logger.info("Cant save Inventory `null`") + return + } + + if (data.main.isEmpty() && data.armor.isEmpty() && data.offhand.isEmpty()) { + logger.info("${data.userId}'s inventory is empty. not saving") + return + } + val database = loadDatabase() + database.removeIf { it.userId == data.userId } + database.add(data) + if (!file.exists()) { + withContext(Dispatchers.IO) { + file.createNewFile() + } + } + file.writeText(JSON.encodeToString(database)) + logger.info("Saved InventorySorting for ${data.userId} to $file") + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/player/AbstractPlayerProvider.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/player/AbstractPlayerProvider.kt new file mode 100644 index 0000000..3429f8f --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/player/AbstractPlayerProvider.kt @@ -0,0 +1,39 @@ +package gg.norisk.heroes.common.database.player + +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.database.AbstractProvider +import gg.norisk.heroes.common.player.FFAPlayer +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.heroes.server.database.inventory.InventoryProvider +import net.minecraft.client.MinecraftClient +import net.minecraft.server.network.ServerPlayerEntity +import java.util.UUID + +abstract class AbstractPlayerProvider : AbstractProvider() { + + override fun getCachedClient(uuid: UUID): FFAPlayer? { + val ffaPlayer = MinecraftClient.getInstance().world?.getPlayerByUuid(uuid)?.ffaPlayer + return ffaPlayer + } + + override suspend fun onPlayerJoin(player: ServerPlayerEntity) { + val ffaPlayer = get(player.uuid) + cache[player.uuid] = ffaPlayer + player.ffaPlayer = ffaPlayer + logger.info("Loaded Database Player ${player.gameProfile.name}") + + InventoryProvider.onPlayerJoin(player) + } + + override suspend fun onPlayerLeave(player: ServerPlayerEntity) { + if (cache.containsKey(player.uuid)) { + save(cache.computeIfAbsent(player.uuid) { FFAPlayer(player.uuid) }) + cache.remove(player.uuid) + logger.info("Saving Database Player ${player.gameProfile.name}") + } else { + logger.warn("Cache didn't contain any data about ${player.gameProfile.name} (${player.gameProfile.id}), not saving any data") + } + + InventoryProvider.onPlayerLeave(player) + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/player/JsonPlayerProvider.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/player/JsonPlayerProvider.kt new file mode 100644 index 0000000..ebc50ef --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/database/player/JsonPlayerProvider.kt @@ -0,0 +1,54 @@ +package gg.norisk.heroes.common.database.player + +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.player.FFAPlayer +import gg.norisk.heroes.server.config.ConfigManagerServer.JSON +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import java.util.* + +class JsonPlayerProvider : AbstractPlayerProvider() { + private val file get() = HeroesManager.baseDirectory.resolve("player-database.json") + + private suspend fun loadDatabase(): MutableSet { + var database = mutableSetOf() + runCatching { + if (file.exists()) { + database = JSON.decodeFromString(file.readText()) + } + }.onFailure { + if (file.readText().isBlank()) { + file.writeText("[]") + } + logger.error("Error Reading ${file.absolutePath}") + it.printStackTrace() + } + return database + } + + private suspend fun findPlayer(uuid: UUID): FFAPlayer? { + val database = loadDatabase() + return database.find { it.uuid == uuid } + } + + override suspend fun get(uuid: UUID): FFAPlayer { + val player = getCached(uuid) ?: findPlayer(uuid) ?: FFAPlayer(uuid) + cache[uuid] = player + return player + } + + override suspend fun save(data: FFAPlayer) { + val database = loadDatabase() + database.removeIf { it.uuid == data.uuid } + database.add(data.copy(inventorySorting = null)) + if (!file.exists()) { + withContext(Dispatchers.IO) { + file.createNewFile() + } + } + file.writeText(JSON.encodeToString(database.map { it.copy(inventorySorting = null) })) + logger.info("Saved Database Player for ${data.uuid} to $file") + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/events/BasicEvents.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/events/BasicEvents.kt new file mode 100644 index 0000000..83c466c --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/events/BasicEvents.kt @@ -0,0 +1,12 @@ +package gg.norisk.heroes.common.events + +import net.minecraft.client.input.Input +import net.silkmc.silk.core.event.Event + +open class AfterTickInputEvent(val input: Input) +open class MouseScrollEvent(val window: Long, val horizontal: Double, val vertical: Double) + +val mouseScrollEvent = Event.onlySync() + +val afterTickInputEvent = Event.onlySync() + diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/events/EntityEvents.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/events/EntityEvents.kt new file mode 100644 index 0000000..15acc80 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/events/EntityEvents.kt @@ -0,0 +1,46 @@ +package gg.norisk.heroes.common.events + +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.chunk.SectionBuilder +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.Entity +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.data.DataTracker +import net.minecraft.entity.data.TrackedData +import net.silkmc.silk.core.event.Cancellable +import net.silkmc.silk.core.event.Event +import net.silkmc.silk.core.event.EventScopeProperty + +object EntityEvents { + class EntityTrackedDataSetEvent(val entity: Entity, val data: TrackedData<*>) + open class LivingEntityEvent(val livingEntity: LivingEntity) + class InitDataTrackerEvent(livingEntity: LivingEntity, val dataTracker: DataTracker) : + LivingEntityEvent(livingEntity) + + open class EntityRendererEvent( + val entity: Entity, + val f: Float, + val g: Float, + val matrixStack: MatrixStack, + val vertexConsumerProvider: VertexConsumerProvider, + val light: Int + ) : Cancellable { + override val isCancelled = EventScopeProperty(false) + } + + open class ComputeFallDamageEvent( + val fallDistance: Float, + val damageMultiplier: Float, + val originalFallDamage: Int, + livingEntity: LivingEntity + ) : LivingEntityEvent( + livingEntity + ) { + var fallDamage: Int? = null + } + + val computeFallDamageEvent = Event.onlySync() + val onTrackedDataSetEvent = Event.onlySync() + val entityRendererEvent = Event.onlySync() + val livingEntityTickMovementEvent = Event.onlySync() +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/events/HeroEvents.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/events/HeroEvents.kt new file mode 100644 index 0000000..23e7ca4 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/events/HeroEvents.kt @@ -0,0 +1,30 @@ +package gg.norisk.heroes.common.events + +import gg.norisk.heroes.common.hero.Hero +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.server.network.ServerPlayerEntity +import net.silkmc.silk.core.annotations.ExperimentalSilkApi +import net.silkmc.silk.core.event.Cancellable +import net.silkmc.silk.core.event.Event +import net.silkmc.silk.core.event.EventScopeProperty + +@OptIn(ExperimentalSilkApi::class) +object HeroEvents { + open class HeroChangeEvent(val player: PlayerEntity) + + val heroChangeEvent = Event.onlySync() + + open class HeroSelectEvent(val player: PlayerEntity, val hero: Hero, var canSelect: Boolean = false) + + val heroSelectEvent = Event.onlySync() + + open class PreKitEditorEvent(val player: ServerPlayerEntity) : Cancellable { + override val isCancelled: EventScopeProperty = EventScopeProperty(false) + } + + val preKitEditorEvent = Event.onlySync() + + open class HeroDeathEvent(val player: ServerPlayerEntity, var isValidDeath: Boolean) + + val heroDeathEvent = Event.onlySync() +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/KitEditorManager.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/KitEditorManager.kt new file mode 100644 index 0000000..011aa72 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/KitEditorManager.kt @@ -0,0 +1,246 @@ +package gg.norisk.heroes.common.ffa + +import gg.norisk.heroes.common.HeroesManager.isServer +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.HeroesManager.prefix +import gg.norisk.heroes.common.HeroesManager.toId +import gg.norisk.heroes.common.player.InventorySorting.Companion.loadInventory +import gg.norisk.heroes.common.events.HeroEvents +import gg.norisk.heroes.common.networking.Networking +import gg.norisk.heroes.common.networking.dto.HeroSelectorPacket +import gg.norisk.heroes.common.player.InventorySorting +import gg.norisk.heroes.common.player.InventorySorting.Companion.CURRENT_VERSION +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.heroes.common.utils.PlayStyle +import gg.norisk.heroes.server.database.player.PlayerProvider +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerWorldEvents +import net.minecraft.block.Blocks +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.network.packet.s2c.play.PositionFlag +import net.minecraft.particle.ItemStackParticleEffect +import net.minecraft.particle.ParticleTypes +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvents +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Vec3d +import net.minecraft.world.GameMode +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.broadcastText +import net.silkmc.silk.core.text.literalText +import kotlin.math.cos +import kotlin.math.sin + +object KitEditorManager { + var world: ServerWorld? = null + var resetInventory: (PlayerEntity) -> Unit = { + it.inventory.clear() + it.inventory.main.set(0, Items.STONE_SWORD.defaultStack) + } + var onBack: (ServerPlayerEntity) -> Unit = { + it.teleport(it.server.overworld, 0.0, 100.0, 0.0, PositionFlag.VALUES, 0f, 0f) + } + private val kitEditorSpawn = Vec3d(0.5, 90.5, 0.5) + + val hasKitWorld get() = world != null + + fun init() { + if (!isServer) return + ServerLifecycleEvents.SERVER_STARTED.register { + for (world in it.worlds) { + logger.info("Found Worlds: $world ${world.registryKey.value}") + if (world.registryKey.value == "kit-editor".toId()) { + this.world = world + break + } + } + logger.info("Found Kit Editor World $world") + } + + ServerTickEvents.END_WORLD_TICK.register { + if (it == world) { + for (player in it.players) { + val distance = player.squaredDistanceTo(kitEditorSpawn) + //player.sendMessage("Distance: $distance".literal) + if (distance > 300) { + teleportToKitEditorSpawn(player) + } + } + } + } + + ServerWorldEvents.LOAD.register(ServerWorldEvents.Load { server, world -> + if (world.registryKey.value == "kit-editor".toId()) { + this.world = world + this.world?.worldBorder?.size = 10000.0 + server.broadcastText("LOADED KIT EDITOR WORLD") + server.broadcastText("LOADED KIT EDITOR WORLD") + /*/world.timeOfDay = 6000 + world.gameRules.get(GameRules.DO_DAYLIGHT_CYCLE).set(false, server) + world.gameRules.get(GameRules.DO_WEATHER_CYCLE).set(false, server) + world.gameRules.get(GameRules.SPECTATORS_GENERATE_CHUNKS).set(false, server) + world.gameRules.get(GameRules.DO_MOB_SPAWNING).set(false, server) + world.gameRules.get(GameRules.DO_ENTITY_DROPS).set(false, server)*/ + } + }) + + ServerEntityEvents.ENTITY_LOAD.register(ServerEntityEvents.Load { entity, world -> + val player = entity as? ServerPlayerEntity? ?: return@Load + if (world == this.world) { + player.changeGameMode(GameMode.ADVENTURE) + mcCoroutineTask(sync = false, client = false) { + val ffaPlayer = PlayerProvider.get(player.uuid) + println("Loaded ${ffaPlayer}") + if (ffaPlayer.inventorySorting == null) { + resetInventory.invoke(player) + ffaPlayer.inventorySorting = player.toDatabaseInventory() + } + mcCoroutineTask(sync = true, client = false) { + player.loadInventory(ffaPlayer.inventorySorting!!) + } + entity.sendMessage(Text.translatable("ffa.mechanic.kit.editor.enter")) + } + } + }) + + ServerEntityEvents.ENTITY_UNLOAD.register(ServerEntityEvents.Unload { entity, world -> + val player = entity as? ServerPlayerEntity? ?: return@Unload + if (world == this.world) { + val inventory = player.toDatabaseInventory() + mcCoroutineTask(sync = false, client = true) { + val ffaPlayer = PlayerProvider.get(player.uuid) + ffaPlayer.inventorySorting = inventory + player.ffaPlayer = ffaPlayer + mcCoroutineTask(sync = false, client = false) { + PlayerProvider.save(ffaPlayer) + println("Saved ${ffaPlayer}") + entity.sendMessage(Text.translatable("ffa.mechanic.kit.editor.save")) + } + entity.sendMessage(Text.translatable("ffa.mechanic.kit.editor.left")) + mcCoroutineTask(sync = true, client = false) { + player.loadInventory(inventory) + } + } + } + }) + + Networking.c2sKitEditorRequestPacket.receiveOnServer { packet, context -> + mcCoroutineTask(sync = true, client = false) { + val player = context.player + val kitEditorWorld = world ?: return@mcCoroutineTask + val event = HeroEvents.PreKitEditorEvent(player) + HeroEvents.preKitEditorEvent.invoke(event) + if (!event.isCancelled.get()) { + Networking.s2cHeroSelectorPacket.send( + HeroSelectorPacket( + emptyList(), + false, + hasKitWorld + ), player + ) + teleportToKitEditorSpawn(player) + player.sendMessage(literalText { + text(prefix) + text(Text.translatable("ffa.mechanic.kit.editor.inventory_instruction")) + }) + kitEditorWorld.setBlockState(BlockPos(0, 89, 0), Blocks.GOLD_BLOCK.defaultState) + } + } + } + } + + + fun onBack(player: ServerPlayerEntity) { + onBack.invoke(player) + } + + fun onReset(player: ServerPlayerEntity) { + resetInventory.invoke(player) + } + + private fun teleportToKitEditorSpawn(player: ServerPlayerEntity) { + player.teleport( + world, + kitEditorSpawn.x, + kitEditorSpawn.y, + kitEditorSpawn.z, + PositionFlag.VALUES, + 0f, + 0f + ) + player.playSoundToPlayer(SoundEvents.ENTITY_ENDERMAN_TELEPORT, SoundCategory.PLAYERS, 0.3f, 1f) + player.serverWorld.syncWorldEvent(2003, player.blockPos, 0) + } + + private fun spawnEnderEyeBreak(blockPos: BlockPos, player: ServerPlayerEntity) { + val d: Double = blockPos.getX().toDouble() + 0.5 + val e: Double = blockPos.getY().toDouble() + val f: Double = blockPos.getZ().toDouble() + 0.5 + + for (k in 0..7) { + player.serverWorld.spawnParticles( + player, + ItemStackParticleEffect(ParticleTypes.ITEM, ItemStack(Items.ENDER_EYE)), + false, + d, + e, + f, + 1, + player.world.random.nextGaussian() * 0.15, + player.world.random.nextDouble() * 0.2, + player.world.random.nextGaussian() * 0.15, + 0.0 + ) + } + + + var g = 0.0 + while (g < Math.PI * 2) { + player.serverWorld.spawnParticles( + player, + ParticleTypes.PORTAL, + false, + d + cos(g) * 5.0, + e - 0.4, + f + sin(g) * 5.0, + 1, + cos(g) * -5.0, + 0.0, + sin(g) * -5.0, + 0.0 + ) + player.serverWorld.spawnParticles( + player, + ParticleTypes.PORTAL, + false, + d + cos(g) * 5.0, + e - 0.4, + f + sin(g) * 5.0, + 1, + cos(g) * -5.0, + 0.0, + sin(g) * -5.0, + 0.0 + ) + g += Math.PI / 20 + } + } + + private fun ServerPlayerEntity.toDatabaseInventory(): InventorySorting { + return InventorySorting( + uuid, + PlayStyle.current, + CURRENT_VERSION, + inventory.armor.toTypedArray(), + inventory.offHand.toTypedArray(), + inventory.main.toTypedArray(), + ) + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/experience/Experience.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/experience/Experience.kt new file mode 100644 index 0000000..96fc666 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/experience/Experience.kt @@ -0,0 +1,79 @@ +package gg.norisk.heroes.common.ffa.experience + +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.HeroesManager.prefix +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.heroes.common.utils.createIfNotExists +import gg.norisk.heroes.server.config.ConfigManagerServer.JSON +import gg.norisk.heroes.server.database.player.PlayerProvider +import kotlinx.serialization.encodeToString +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.server.network.ServerPlayerEntity +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.literalText +import java.awt.Color + +object Experience { + private val configFile = HeroesManager.baseDirectory.resolve("xp-config.json").createIfNotExists() + + fun init() { + loadConfig() + } + + fun add(player: ServerPlayerEntity, reason: ExperienceReason, printMessage: Boolean = false) { + mcCoroutineTask(sync = false, client = false) { + val receivedXp = reason.value + val ffaPlayer = PlayerProvider.get(player.uuid) + ffaPlayer.xp += receivedXp + player.ffaPlayer = ffaPlayer + + if (printMessage) { + player.sendMessage(literalText { + text(prefix) + text("+$receivedXp XP") { + color = Color.GREEN.rgb + } + }) + } + + PlayerProvider.save(ffaPlayer) + } + } + + private fun loadConfig() { + val currentConfig = loadFromFile() + createDefaultConfig(currentConfig.isEmpty()) + + currentConfig.forEach { configReason -> + val reason = ExperienceRegistry.reasons.firstOrNull { reason -> reason.key == configReason.key } + if (reason == null) { + logger.warn("Found invalid reason with key `${configReason.key}` in config") + return@forEach + } + reason.value = configReason.value + } + } + + private fun loadFromFile(): MutableSet { + return runCatching> { + JSON.decodeFromString(configFile.readText()) + }.onFailure { + it.printStackTrace() + }.onSuccess { + logger.info("Loaded Xp Config") + }.getOrDefault(mutableSetOf()) + } + + private fun createDefaultConfig(force: Boolean) { + if (force) { + configFile.createNewFile() + configFile.writeText(JSON.encodeToString(ExperienceRegistry.reasons)) + logger.info("Created Default Xp Config") + } + } +} + +fun PlayerEntity.addXp(reason: ExperienceReason, printMessage: Boolean = false) { + Experience.add(this as ServerPlayerEntity, reason, printMessage) +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/experience/ExperienceReason.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/experience/ExperienceReason.kt new file mode 100644 index 0000000..fc7ce45 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/experience/ExperienceReason.kt @@ -0,0 +1,6 @@ +package gg.norisk.heroes.common.ffa.experience + +import kotlinx.serialization.Serializable + +@Serializable +data class ExperienceReason(val key: String, var value: Int) diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/experience/ExperienceRegistry.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/experience/ExperienceRegistry.kt new file mode 100644 index 0000000..8499e7e --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/ffa/experience/ExperienceRegistry.kt @@ -0,0 +1,21 @@ +package gg.norisk.heroes.common.ffa.experience + +object ExperienceRegistry { + val reasons = mutableSetOf() + + val KILLED_PLAYER = register("killed_player", 200) + val PLAYER_DEATH = register("player_death", 25) + val SOUP_EATEN = register("soup_eaten", 5) + val SMALL_ABILITY_USE = register("small_ability_use", 5) + val RECRAFT = register("soup_recraft", 5) + val END_KILL_STREAK = register("end_kill_streak", 1000) + val DEALING_DAMAGE = register("dealing_damage", 1) + val TAKING_DAMAGE = register("taking_damage", 1) + val IDLE = register("idle", 1) + + fun register(key: String, value: Int): ExperienceReason { + return ExperienceReason(key, value).apply { + reasons.add(this) + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/Hero.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/Hero.kt new file mode 100644 index 0000000..b17bd22 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/Hero.kt @@ -0,0 +1,100 @@ +package gg.norisk.heroes.common.hero + +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.HeroesManager.toId +import gg.norisk.heroes.common.ability.PlayerProperty.Companion.JSON +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import java.io.File + +open class Hero(val name: String) { + companion object { + /** + * Creates a new lazy hero delegate. + * + * @param config the config of this hero + * @param builder the [HeroBuilder] + */ + inline operator fun invoke( + name: String, + crossinline builder: HeroBuilder.() -> Unit + ) = lazy { + Hero(name).apply { + HeroBuilder(this).apply(builder) + } + } + } + + var internalCallbacks = InternalCallbacks() + val internalKey = name.lowercase().replace(' ', '_') + val icon = "textures/hero/${internalKey}/icon.png".toId() + val description = Text.translatable("text.hero.$internalKey.description") + var overlaySkin: Identifier? = null + + val abilities = hashMapOf>() + var color: Int = 0x4291AD + + fun registerAbility(ability: AbstractAbility<*>) { + ability.hero = this + // REMOVED AbilityKeyBindManager.initializeKeyBind(ability) + abilities[ability.internalKey] = ability + } + + fun getUsableAbilities(player: PlayerEntity): List> { + return abilities.values.toList() + } + + @Serializable + data class HeroJson( + val internalKey: String, + val properties: Map + ) + + fun load(heroJson: HeroJson? = null) { + if (baseFile.exists()) { + runCatching { + val loaded = heroJson ?: JSON.decodeFromString(baseFile.readText()) + for ((key, element) in loaded.properties) { + val ability = abilities[key] + for (jsonElement in element) { + val name = jsonElement.jsonObject["name"] ?: continue + val property = ability?.getAllProperties()?.find { it.name == name.jsonPrimitive.content } + property?.fromJson(jsonElement.toString()) + } + } + }.onFailure { + logger.error("Error Loading Hero $internalKey ${it.message}") + it.printStackTrace() + } + } + } + + private val baseFolder get() = File(HeroesManager.baseDirectory, "heroes/hero").apply { mkdirs() } + private val baseFile get() = File(baseFolder, "$internalKey.json") + + fun save() { + baseFile.writeText(JSON.encodeToString(toHeroJson())) + logger.info("Successfully saved $internalKey") + } + + fun toHeroJson(): HeroJson { + val properties = buildMap { + for ((key, ability) in abilities) { + put(key, JsonArray(ability.getAllProperties().map { it.toJsonElement() })) + } + } + return HeroJson(internalKey, properties) + } + + inner class InternalCallbacks { + var getSkin: ((player: PlayerEntity) -> Identifier?)? = null + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/HeroBuilder.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/HeroBuilder.kt new file mode 100644 index 0000000..3f5fc11 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/HeroBuilder.kt @@ -0,0 +1,57 @@ +package gg.norisk.heroes.common.hero + +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.util.Identifier +import net.silkmc.silk.core.event.Event +import net.silkmc.silk.core.event.EventPriority +import net.silkmc.silk.core.event.MutableEventScope + +class HeroBuilder(val hero: Hero) { + var color: Int + get() = hero.color + set(value) { + hero.color = value + } + var overlaySkin: Identifier? + get() = hero.overlaySkin + set(value) { + hero.overlaySkin = value + } + + /** + * Executes the given [callback] if the player of the + * [playerGetter] is the hero. + */ + inline fun Event.heroPlayerEvent( + crossinline playerGetter: (T) -> PlayerEntity?, + priority: EventPriority = EventPriority.NORMAL, + crossinline callback: context(MutableEventScope) (event: T) -> Unit + ) { + this.listen(priority) { + val player = playerGetter(it) ?: return@listen + if (player.getHero() != hero) return@listen + callback.invoke(MutableEventScope, it) + } + } + + /** + * Executes the given [callback] when the event is called + */ + inline fun Event.heroEvent( + priority: EventPriority = EventPriority.NORMAL, + crossinline callback: context(MutableEventScope) (event: T) -> Unit + ) { + this.listen(priority) { + callback.invoke(MutableEventScope, it) + } + } + + fun getSkin(callback: (player: PlayerEntity) -> Identifier) { + hero.internalCallbacks.getSkin = callback + } + + fun ability(ability: AbstractAbility<*>) { + hero.registerAbility(ability) + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/IHeroManager.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/IHeroManager.kt new file mode 100644 index 0000000..79a1b79 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/IHeroManager.kt @@ -0,0 +1,64 @@ +package gg.norisk.heroes.common.hero + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.heroes.common.HeroesManager.isClient +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.hero.HeroManager.HERO_KEY +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import gg.norisk.heroes.common.hero.ability.task.AbilityCoroutineManager +import gg.norisk.heroes.server.config.ConfigManagerServer +import gg.norisk.heroes.server.hero.ability.AbilityManagerServer +import net.minecraft.client.MinecraftClient +import net.minecraft.entity.player.PlayerEntity +import net.silkmc.silk.core.Silk.server +import net.silkmc.silk.core.text.broadcastText + +object HeroManager { + val registeredHeroes: MutableMap = mutableMapOf() + const val HERO_KEY = "hero" + + fun getHero(internalKey: String) = registeredHeroes[internalKey.replace(' ', '_')] + + fun registerHero(hero: Hero): Boolean { + logger.info("Register Hero ${hero.name}... on $this") + registeredHeroes[hero.internalKey] = hero + hero.abilities.values.forEach(AbstractAbility<*>::init) + return true + } + + fun reloadHeroes(vararg heroes: Hero) { + for (hero in heroes) { + runCatching { + hero.load() + hero.save() + ConfigManagerServer.sendHeroSettings(hero) + }.onSuccess { + server?.broadcastText("Loaded Hero ${hero.name}") + }.onFailure { + server?.broadcastText("Error loading Hero ${hero.name}") + } + } + } +} + +fun PlayerEntity.setHero(hero: Hero?) { + if (isClient && this == MinecraftClient.getInstance().player) { + AbilityCoroutineManager.cancelClientJobs() + } else { + AbilityCoroutineManager.cancelServerJobs(this) + AbilityManagerServer.clear(this) + } + getHero()?.abilities?.forEach { (name, ability) -> + ability.clearCooldown(this) + ability.onDisable(this) + } + this.setSyncedData(HERO_KEY, hero?.internalKey ?: "NONE") + getHero()?.abilities?.forEach { (name, ability) -> ability.onEnable(this) } +} + +fun PlayerEntity.getHero(): Hero? { + return HeroManager.getHero(this.getSyncedData(HERO_KEY) ?: "NONE") +} + +fun PlayerEntity.isHero(hero: Hero?) = this.getHero() == hero diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/AbilityPacket.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/AbilityPacket.kt new file mode 100644 index 0000000..4b2d40b --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/AbilityPacket.kt @@ -0,0 +1,33 @@ +package gg.norisk.heroes.common.hero.ability + +import gg.norisk.heroes.common.serialization.UUIDSerializer +import kotlinx.serialization.Serializable +import java.util.UUID + +@Serializable +data class AbilityPacket( + @Serializable(with = UUIDSerializer::class) + val playerUuid: UUID, + val heroKey: String, + val abilityKey: String, + val description: C +) + +@Serializable +data class SkillPropertyPacket( + val heroKey: String, + val abilityKey: String, + val propertyKey: String +) + +@Serializable +sealed class AbilityPacketDescription { + @Serializable + object Start : AbilityPacketDescription() + + @Serializable + open class Use : AbilityPacketDescription() + + @Serializable + object End : AbilityPacketDescription() +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/AbilityScope.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/AbilityScope.kt new file mode 100644 index 0000000..27de657 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/AbilityScope.kt @@ -0,0 +1,24 @@ +package gg.norisk.heroes.common.hero.ability + +import net.minecraft.entity.player.PlayerEntity + +class AbilityScope(val executingPlayer: PlayerEntity) { + var applyCooldown = true + var broadcastPacket = false + + fun cancelCooldown() { + applyCooldown = false + } + + fun applyCooldown() { + applyCooldown = true + } + + fun cancelBroadcasting() { + broadcastPacket = false + } + + fun broadcast() { + broadcastPacket = true + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/AbstractAbility.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/AbstractAbility.kt new file mode 100644 index 0000000..637b44a --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/AbstractAbility.kt @@ -0,0 +1,279 @@ +package gg.norisk.heroes.common.hero.ability + + +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.ability.* +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.ability.operation.MultiplyBase +import gg.norisk.heroes.common.ability.operation.Operation +import gg.norisk.heroes.common.cooldown.CooldownInfo +import gg.norisk.heroes.common.cooldown.MultipleUsesInfo +import gg.norisk.heroes.common.ffa.experience.ExperienceRegistry +import gg.norisk.heroes.common.ffa.experience.addXp +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.networking.Networking +import gg.norisk.heroes.server.config.ConfigManagerServer.JSON +import gg.norisk.utils.DevUtils.uniqueId +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import kotlinx.coroutines.* +import kotlinx.serialization.encodeToString +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.option.KeyBinding +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.silkmc.silk.core.task.mcCoroutineTask +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.Duration.Companion.seconds + +abstract class AbstractAbility(val name: String) { + lateinit var hero: Hero + val internalKey = name.lowercase().replace(' ', '_') + val description by lazy { Text.translatable("hero.${hero.internalKey}.ability.$internalKey.description") } + var condition: ((PlayerEntity) -> Boolean)? = null + + var showInKeybindHud: Boolean = true + + @Environment(EnvType.CLIENT) + var keyBind: KeyBinding? = null + var properties = listOf>() + private val cooldowns = ConcurrentHashMap() + private val cooldownTasks = ConcurrentHashMap() + var cooldownProperty: CooldownProperty = buildCooldown(5.0, 5, AddValueTotal(-0.1, -0.4, -0.2, -0.8, -1.5, -1.0)) + var usageProperty: AbstractUsageProperty = SingleUseProperty(0.0, 0, "Use", MultiplyBase(listOf(0.0))).apply { + icon = { + Components.item(Items.STONE_PICKAXE.defaultStack) + } + } + + //atm used for holdcooldown + open val extraProperties: List> = emptyList() + private var allProperties: List>? = null + + fun getAllProperties(): List> { + //Todo das cachen? + return buildList { + add(cooldownProperty) + add(usageProperty) + addAll(extraProperties) + addAll(properties) + } + } + + open fun getCustomActivation(): Text { + return Text.translatable("heroes.ability.$internalKey.custom_activation") + } + + open fun hasUnlocked(player: PlayerEntity): Boolean { + return true + } + + /* + ähnlich wie condition aber nur ServerSide Condition Check + */ + open fun canUse(player: ServerPlayerEntity): Boolean { + return true + } + + open fun getUnlockCondition(): Text { + return Text.empty() + } + + open fun getIconComponent(): Component { + return Components.item(Items.DIAMOND_SWORD.defaultStack) + } + + open fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/stone.png") + } + + fun handleCooldown(player: ServerPlayerEntity): Boolean { + if (hasCooldown(player)) { + // Client an den cooldown erinnern! + getCooldown(player)?.let { cooldownInfo -> + //player.sendDebugMessage("Cooldown: ${cooldownInfo.remaining}".literal) + Networking.s2cCooldownPacket.send(cooldownInfo, player) + return true + } + } + return false + } + + fun removeCooldown(player: PlayerEntity) { + cooldowns.remove(player.uuid) + } + + fun setCooldown(cooldownInfo: CooldownInfo, player: PlayerEntity) { + if (cooldownInfo.duration == 0L && cooldownInfo.startTime == 0L && cooldownInfo.currentTime == 0L) { + cooldowns.remove(player.uuid) + } else { + cooldowns[player.uuid] = cooldownInfo + } + } + + fun clearCooldown(player: PlayerEntity) { + cooldownTasks[player.uuid]?.cancel() + if (cooldowns.remove(player.uuid) != null) { + if (player is ServerPlayerEntity) { + Networking.s2cCooldownPacket.send( + CooldownInfo( + player.id, + 0, + 0, + 0, + null, + hero.internalKey, + internalKey, + null + ), player + ) + } + } + } + + fun addCooldown(player: PlayerEntity) { + if (player !is ServerPlayerEntity) return + if (cooldownProperty.name == "NoCooldown") return + //has cooldown + if (handleCooldown(player)) return + val uuid = player.uuid + val currentTime = System.nanoTime() + var startTime: Long? = null + + var multipleUsesInfo: MultipleUsesInfo? = null + if (usageProperty is MultiUseProperty) { + val currentUse = (usageProperty as MultiUseProperty).uses.getOrDefault(player.uuid, 0) + 1 + (usageProperty as MultiUseProperty).uses[player.uuid] = currentUse + val maxUses = usageProperty.getValue(player.uuid).toInt() + multipleUsesInfo = MultipleUsesInfo(currentUse, maxUses) + if (currentUse == maxUses) { + startTime = currentTime + (usageProperty as MultiUseProperty).uses[player.uuid] = 0 + } + } else if (usageProperty is SingleUseProperty) { + startTime = currentTime + } + + val value = cooldownProperty.getValue(player.uuid) + logger.info("Sending Cooldown $value to ${player.gameProfile.name}") + //player.sendDebugMessage("Value: $value Level: ${cooldownProperty.getLevelInfo(player.uuid)}".literal) + //player.sendDebugMessage("Property: $cooldownProperty".literal) + val duration = value.seconds.inWholeNanoseconds + val cooldownInfo = CooldownInfo( + player.id, + duration, + startTime, + currentTime, + multipleUsesInfo, + hero.internalKey, + internalKey, + startTime?.let { it + duration } + ).apply { + this.durationString = getCooldownText(this) + } + player.addXp(ExperienceRegistry.SMALL_ABILITY_USE, true) + cooldowns[uuid] = cooldownInfo + Networking.s2cCooldownPacket.send(cooldownInfo, player) + + //player.sendDebugMessage("Sending Cooldown: $cooldownInfo".literal) + + if (cooldownInfo.remaining > 0) { + cooldownTasks[uuid]?.cancel() + cooldownTasks[uuid] = mcCoroutineTask(sync = false, client = false) { + //NO DELAY IN CREATIVE MODE FOR TESTING? + //player.sendMessage("START".literal.withColor(Color.red.rgb)) + //player.sendMessage(getCooldownText(cooldownInfo)?.literal) + if (!player.isCreative) { + delay(value.seconds.inWholeMilliseconds) + } + //player.sendMessage("END".literal.withColor(Color.red.rgb)) + //player.sendMessage(getCooldownText(cooldownInfo)?.literal) + cooldowns -= uuid + Networking.s2cCooldownPacket.send( + CooldownInfo( + player.id, + 0, + 0, + 0, + multipleUsesInfo, + hero.internalKey, + internalKey, + null + ), player + ) + } + } + } + + fun getCooldown(player: PlayerEntity): CooldownInfo? { + return cooldowns[player.uuid] + } + + fun hasCooldown(player: PlayerEntity): Boolean { + val cooldownInfo = getCooldown(player) ?: return false + //player.sendDebugMessage("Cooldown: $cooldownInfo".literal) + return cooldownInfo.remaining > 0 + } + + fun init() { + for (property in getAllProperties()) { + property.ability = this + property.hero = this.hero + } + } + + open fun onEnable(player: PlayerEntity) { + + } + + open fun onDisable(player: PlayerEntity) { + } + + open fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + } + + open fun onTick(player: PlayerEntity) { + } + + protected fun buildMultipleUses(baseValue: Double, maxLevel: Int, operation: Operation): MultiUseProperty { + return MultiUseProperty(baseValue, maxLevel, "Use", operation) + } + + protected fun buildCooldown(baseValue: Double, maxLevel: Int, operation: Operation): CooldownProperty { + return CooldownProperty(baseValue, maxLevel, "Cooldown", operation) + } + + protected fun buildNoCooldown(): CooldownProperty { + return CooldownProperty(0.0, 0, "NoCooldown", MultiplyBase()) + } + + fun getCooldownText(cooldown: CooldownInfo): String? { + val remaining = cooldown.remaining + if (remaining > 0) { + val builder = StringBuilder() + remaining.nanoseconds.toComponents { days, hours, minutes, seconds, nanoseconds -> + if (days > 0) builder.append(days).append("d ") + if (hours > 0) builder.append(hours).append("h ") + if (minutes > 0) builder.append(minutes).append("m ") + builder.append(seconds).append(".") + builder.append((nanoseconds / 1_000_000).toString().padStart(3, '0').take(1)) // Nur 2 Stellen + } + return builder.toString() + } + if (usageProperty is MultiUseProperty) { + val multipleUseInfo = cooldown.multipleUsesInfo ?: return null + val currentUse = multipleUseInfo.currentUse + val maxUses = multipleUseInfo.maxUses + val remainingUses = maxUses - currentUse + return "$remainingUses/$maxUses" + } + + return null + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/IAbilityManager.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/IAbilityManager.kt new file mode 100644 index 0000000..358760c --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/IAbilityManager.kt @@ -0,0 +1,21 @@ +package gg.norisk.heroes.common.hero.ability + +import gg.norisk.heroes.common.hero.Hero +import net.minecraft.entity.player.PlayerEntity + +interface IAbilityManager { + fun init() + + fun useAbility( + player: PlayerEntity, + hero: Hero, + ability: AbstractAbility<*>, + description: AbilityPacketDescription.Use + ): Boolean + + fun useAbility(player: PlayerEntity, ability: AbstractAbility<*>, description: AbilityPacketDescription.Use) + + fun registerAbility(ability: AbstractAbility<*>) + + fun isUsingAbility(player: PlayerEntity, ability: AbstractAbility<*>): Boolean +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/Ability.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/Ability.kt new file mode 100644 index 0000000..407dc28 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/Ability.kt @@ -0,0 +1,6 @@ +package gg.norisk.heroes.common.hero.ability.implementation + +import gg.norisk.heroes.common.hero.ability.AbstractAbility + +class Ability(name: String) : AbstractAbility(name) { +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/HoldAbility.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/HoldAbility.kt new file mode 100644 index 0000000..03312ca --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/HoldAbility.kt @@ -0,0 +1,4 @@ +package gg.norisk.heroes.common.hero.ability.implementation + +open class HoldAbility(name: String) : ToggleAbility(name) { +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/PressAbility.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/PressAbility.kt new file mode 100644 index 0000000..82d73f4 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/PressAbility.kt @@ -0,0 +1,6 @@ +package gg.norisk.heroes.common.hero.ability.implementation + +import gg.norisk.heroes.common.hero.ability.AbstractAbility + +open class PressAbility(name: String) : AbstractAbility(name) { +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/ToggleAbility.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/ToggleAbility.kt new file mode 100644 index 0000000..7cb4d9d --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/implementation/ToggleAbility.kt @@ -0,0 +1,38 @@ +package gg.norisk.heroes.common.hero.ability.implementation + +import gg.norisk.heroes.common.ability.CooldownProperty +import gg.norisk.heroes.common.ability.PlayerProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.ability.operation.Operation +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import net.minecraft.entity.player.PlayerEntity + +open class ToggleAbility(name: String) : AbstractAbility(name) { + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + + } + + open fun onUse(player: PlayerEntity) { + + } + + open fun onEnd(player: PlayerEntity, abilityEndInformation: AbilityEndInformation) { + + } + + var maxDurationProperty = buildMaxDuration(10.0, 5, AddValueTotal(0.1, 0.4, 0.2, 0.8, 1.5, 1.0)) + + data class AbilityEndInformation(var applyCooldown: Boolean = true) + + protected fun buildMaxDuration(baseValue: Double, maxLevel: Int, operation: Operation): CooldownProperty { + return CooldownProperty( + baseValue, maxLevel, + "Max Duration", + operation + ) + } + + override val extraProperties: List> + get() = listOf(maxDurationProperty) +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/task/AbilityCoroutineTask.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/task/AbilityCoroutineTask.kt new file mode 100644 index 0000000..08b984f --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/ability/task/AbilityCoroutineTask.kt @@ -0,0 +1,54 @@ +package gg.norisk.heroes.common.hero.ability.task + +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.hero.ability.AbilityScope +import kotlinx.coroutines.* +import net.minecraft.client.MinecraftClient +import net.minecraft.entity.player.PlayerEntity +import net.silkmc.silk.core.annotations.DelicateSilkApi +import net.silkmc.silk.core.kotlin.ticks +import net.silkmc.silk.core.task.* +import java.util.* +import kotlin.time.Duration + +object AbilityCoroutineManager { + val playerJobs = hashMapOf>() + + fun cancelServerJobs(player: PlayerEntity) { + playerJobs[player.uuid]?.forEach(Job::cancel) + playerJobs.remove(player.uuid) + } + + fun cancelClientJobs() { + val uuid = playerJobs.keys.firstOrNull() + playerJobs[uuid]?.forEach(Job::cancel) + playerJobs.remove(uuid) + } +} + +@OptIn(DelicateSilkApi::class) +inline fun abilityCoroutineTask( + executingPlayer: PlayerEntity, + sync: Boolean = true, + client: Boolean = false, + scope: CoroutineScope = if (sync) { + if (client) mcClientCoroutineScope else mcCoroutineScope + } else silkCoroutineScope, + howOften: Long = 1, + period: Duration = 1.ticks, + delay: Duration = Duration.ZERO, + crossinline task: suspend CoroutineScope.(task: CoroutineTask) -> Unit +): Job { + val uuid = if (client) MinecraftClient.getInstance().player!!.uuid else executingPlayer.uuid + return mcCoroutineTask(sync, client, scope, howOften, period, delay) { coroutineTask -> + if (isActive && !executingPlayer.isAlive) { + logger.info("${executingPlayer.name.literalString} is currently dead, cancelling coroutine job") + cancel() + } + ensureActive() + task(this, coroutineTask) + AbilityCoroutineManager.playerJobs[uuid]?.remove(this.coroutineContext.job) + }.also { job -> + AbilityCoroutineManager.playerJobs.computeIfAbsent(uuid) { mutableListOf() }.add(job) + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/utils/ColorUtils.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/utils/ColorUtils.kt new file mode 100644 index 0000000..da56b83 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/hero/utils/ColorUtils.kt @@ -0,0 +1,53 @@ +package gg.norisk.heroes.common.hero.utils + +object ColorUtils { + fun hexAsRgb(hexColor: Int): Triple { + val red = (hexColor shr 16) and 0xFF + val green = (hexColor shr 8) and 0xFF + val blue = hexColor and 0xFF + return Triple(red, green, blue) + } + + fun isLightColor(hexColor: Int): Boolean { + val (red, green, blue) = hexAsRgb(hexColor) + + // Calculate luminance + val luminance = (0.299 * red + 0.587 * green + 0.114 * blue) / 255 + + // Determine if color is light or dark + return luminance > 0.5 + } + + fun isDarkColor(hexColor: Int) = !isLightColor(hexColor) + + fun darkenHexColor(hexColor: Int, factor: Double): Int { + // Extract RGB components + val (red, green, blue) = hexAsRgb(hexColor) + + // Darken each component + val darkenedRed = (red * (1 - factor)).toInt() + val darkenedGreen = (green * (1 - factor)).toInt() + val darkenedBlue = (blue * (1 - factor)).toInt() + + // Combine components and return the darkened color + return (darkenedRed shl 16) or (darkenedGreen shl 8) or darkenedBlue + } + + fun lightenHexColor(hexColor: Int, factor: Double): Int { + // Extract RGB components + val (red, green, blue) = hexAsRgb(hexColor) + + // Darken each component + val lightenedRed = (red + (255 - red) * factor).toInt() + val lightenedGreen = (green + (255 - green) * factor).toInt() + val lightenedBlue = (blue + (255 - blue) * factor).toInt() + + // Combine components and return the darkened color + return (lightenedRed shl 16) or (lightenedGreen shl 8) or lightenedBlue + } + + fun contrast(hexColor: Int): Int { + return if (isDarkColor(hexColor)) lightenHexColor(hexColor, 0.33) + else darkenHexColor(hexColor, 0.33) + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/CameraShakeS2C.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/CameraShakeS2C.kt new file mode 100644 index 0000000..226273a --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/CameraShakeS2C.kt @@ -0,0 +1,24 @@ +package gg.norisk.heroes.common.networking + +import gg.norisk.heroes.common.HeroesManager.toId +import kotlinx.serialization.Serializable +import net.silkmc.silk.network.packet.s2cPacket + +interface CameraShakeEvent { + fun isValid(t: Double): Boolean + fun getCameraShakeMagnitude(t: Double): Double +} + +@Serializable +data class BoomShake(private var magnitude: Double, private var sustain: Double, private var fade: Double) : + CameraShakeEvent { + override fun isValid(t: Double): Boolean = t < sustain + fade + override fun getCameraShakeMagnitude(t: Double): Double { + return when { + t <= sustain -> magnitude + else -> magnitude * (1 - (t - sustain) / fade) + } + } +} + +val cameraShakePacket = s2cPacket("camera-shake".toId()) diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/Networking.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/Networking.kt new file mode 100644 index 0000000..6b3e490 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/Networking.kt @@ -0,0 +1,30 @@ +package gg.norisk.heroes.common.networking + +import gg.norisk.heroes.common.HeroesManager.toId +import gg.norisk.heroes.common.cooldown.CooldownInfo +import gg.norisk.heroes.common.hero.ability.AbilityPacket +import gg.norisk.heroes.common.hero.ability.AbilityPacketDescription +import gg.norisk.heroes.common.hero.ability.SkillPropertyPacket +import gg.norisk.heroes.common.networking.dto.HeroSelectorPacket +import gg.norisk.heroes.common.networking.dto.MousePacket +import net.silkmc.silk.network.packet.c2sPacket +import net.silkmc.silk.network.packet.s2cPacket + +object Networking { + val c2sAbilityPacket = c2sPacket>("use-ability".toId()) + val s2cAbilityPacket = s2cPacket>("use-ability".toId()) + + val c2sSkillProperty = c2sPacket("skill-property".toId()) + val s2cHeroSelectorPacket = s2cPacket("hero-selector-s2c".toId()) + val c2sHeroSelectorPacket = c2sPacket("hero-selector-c2s".toId()) + val c2sKitEditorRequestPacket = c2sPacket("kit-editor-request".toId()) + + val s2cCooldownPacket = s2cPacket("cooldown".toId()) + + val mousePacket = c2sPacket("mouse-packet".toId()) + val mouseScrollPacket = c2sPacket("mouse-scroll".toId()) + + //warum String? + // java.lang.IllegalStateException: This serializer can be used only with Json format.Expected Encoder to be JsonEncoder, got class kotlinx.serialization.cbor.internal.CborMapWriter + val s2cHeroSettingsPacket = s2cPacket("hero-settings".toId()) +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/AnimationInterpolator.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/AnimationInterpolator.kt new file mode 100644 index 0000000..860eb59 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/AnimationInterpolator.kt @@ -0,0 +1,141 @@ +package gg.norisk.heroes.common.networking.dto + +import it.unimi.dsi.fastutil.doubles.Double2DoubleFunction +import java.time.Duration +import kotlin.math.* + +class AnimationInterpolator(val start: Float, val end: Float, var dur: Duration) { + var startTime: Long + var easing = Easing.LINEAR + val forward = true + + init { + this.startTime = System.nanoTime() + } + + constructor(start: Float, end: Float, duration: Duration, easing: Easing) : this(start, end, duration) { + this.easing = easing + } + + fun setDuration(dur: Duration) { + this.dur = dur + } + + fun reset() { + this.startTime = System.nanoTime() + } + + fun get(): Float { + val currentTime = System.nanoTime() + val delta = currentTime - startTime + val nanoDuration = dur.toNanos() + var animDelta = delta.toFloat() / nanoDuration + animDelta = max(0.0, min(1.0, animDelta.toDouble())).toFloat() + if (!forward) { + animDelta = (1 - animDelta).toFloat() + } + animDelta = easing.apply(animDelta.toDouble()) + return start + (end - start) * animDelta + } + + val isDone: Boolean + get() = System.nanoTime() - startTime >= dur.toNanos() + + enum class Easing(val floatFunction: Double2DoubleFunction) { + LINEAR(Double2DoubleFunction { x: Double -> x }), + SINE_IN(Double2DoubleFunction { x: Double -> 1 - cos(x * Math.PI / 2) }), + SINE_OUT(Double2DoubleFunction { x: Double -> sin(x * Math.PI / 2) }), + SINE_IN_OUT(Double2DoubleFunction { x: Double -> -(cos(Math.PI * x) - 1) / 2 }), + + CUBIC_IN(Double2DoubleFunction { x: Double -> x.pow(3.0) }), + CUBIC_OUT(Double2DoubleFunction { x: Double -> 1 - (1 - x).pow(3.0) }), + CUBIC_IN_OUT(Double2DoubleFunction { x: Double -> if (x < 0.5) 4 * x * x * x else 1 - (-2 * x + 2).pow(3.0) / 2 }), + + QUINT_IN(Double2DoubleFunction { x: Double -> x.pow(5.0) }), + QUINT_OUT(Double2DoubleFunction { x: Double -> 1 - (1 - x).pow(5.0) }), + QUINT_IN_OUT(Double2DoubleFunction { x: Double -> + if (x < 0.5) 16 * x * x * x * x * x else 1 - (-2 * x + 2).pow( + 5.0 + ) / 2 + }), + + CIRC_IN(Double2DoubleFunction { x: Double -> 1 - sqrt(1 - x.pow(2.0)) }), + CIRC_OUT(Double2DoubleFunction { x: Double -> sqrt(1 - (x - 1).pow(2.0)) }), + CIRC_IN_OUT(Double2DoubleFunction { x: Double -> + if (x < 0.5) (1 - sqrt(1 - (2 * x).pow(2.0))) / 2 else (sqrt( + 1 - (-2 * x + 2).pow(2.0) + ) + 1) / 2 + }), + + ELASTIC_IN(Double2DoubleFunction { x: Double -> + val c4 = 2 * Math.PI / 3 + if (x == 0.0) 0.0 else if (x == 1.0) 1.0 else -2.0.pow(10 * x - 10) * sin((x * 10 - 10.75) * c4) + }), + ELASTIC_OUT(Double2DoubleFunction { x: Double -> + val c4 = 2 * Math.PI / 3 + if (x == 0.0) 0.0 else if (x == 1.0) 1.0 else 2.0.pow(-10 * x) * sin((x * 10 - 0.75) * c4) + 1 + }), + ELASTIC_IN_OUT(Double2DoubleFunction { x: Double -> + val c5 = 2 * Math.PI / 4.5 + val sin = sin((20 * x - 11.125) * c5) + if (x == 0.0) 0.0 else if (x == 1.0) 1.0 else if (x < 0.5) -(2.0.pow(20 * x - 10) * sin) / 2 else 2.0.pow(-20 * x + 10) * sin / 2 + 1 + }), + + QUAD_IN(Double2DoubleFunction { x: Double -> x * x }), + QUAD_OUT(Double2DoubleFunction { x: Double -> 1 - (1 - x) * (1 - x) }), + QUAD_IN_OUT(Double2DoubleFunction { x: Double -> if (x < 0.5) 2 * x * x else 1 - (-2 * x + 2).pow(2.0) / 2 }), + + QUART_IN(Double2DoubleFunction { x: Double -> x * x * x * x }), + QUART_OUT(Double2DoubleFunction { x: Double -> 1 - (1 - x).pow(4.0) }), + QUART_IN_OUT(Double2DoubleFunction { x: Double -> if (x < 0.5) 8 * x * x * x * x else 1 - (-2 * x + 2).pow(4.0) / 2 }), + + EXPO_IN(Double2DoubleFunction { x: Double -> if (x == 0.0) 0.0 else 2.0.pow(10 * x - 10) }), + EXPO_OUT(Double2DoubleFunction { x: Double -> if (x == 1.0) 1.0 else 1 - 2.0.pow(-10 * x) }), + EXPO_IN_OUT(Double2DoubleFunction { x: Double -> + if (x == 0.0) 0.0 else if (x == 1.0) 1.0 else if (x < 0.5) 2.0.pow( + 20 * x - 10 + ) / 2 else (2 - 2.0.pow(-20 * x + 10)) / 2 + }), + + BACK_IN(Double2DoubleFunction { x: Double -> + val c1 = 1.70158 + val c3 = c1 + 1 + c3 * x * x * x - c1 * x * x + }), + BACK_OUT(Double2DoubleFunction { x: Double -> + val c1 = 1.70158 + val c3 = c1 + 1 + 1 + c3 * (x - 1).pow(3.0) + c1 * (x - 1).pow(2.0) + }), + BACK_IN_OUT(Double2DoubleFunction { x: Double -> + val c1 = 1.70158 + val c2 = c1 * 1.525 + if (x < 0.5) (2 * x).pow(2.0) * ((c2 + 1) * 2 * x - c2) / 2 else ((2 * x - 2).pow(2.0) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2 + }), + + BOUNCE_OUT(Double2DoubleFunction { x: Double -> + var x = x + val n1 = 7.5625 + val d1 = 2.75 + if (x < 1 / d1) { + return@Double2DoubleFunction n1 * x * x + } else if (x < 2 / d1) { + return@Double2DoubleFunction n1 * ((1.5 / d1).let { x -= it; x }) * x + 0.75 + } else if (x < 2.5 / d1) { + return@Double2DoubleFunction n1 * ((2.25 / d1).let { x -= it; x }) * x + 0.9375 + } else { + return@Double2DoubleFunction n1 * ((2.625 / d1).let { x -= it; x }) * x + 0.984375 + } + }), + BOUNCE_IN(Double2DoubleFunction { x: Double -> (1 - BOUNCE_OUT.apply(x)).toDouble() }), + BOUNCE_IN_OUT(Double2DoubleFunction { x: Double -> + if (x < 0.5) ((1 - BOUNCE_OUT.apply(1 - 2 * x)) / 2).toDouble() else ((1 + BOUNCE_OUT.apply( + 2 * x - 1 + )) / 2).toDouble() + }); + + fun apply(f: Double): Float { + return floatFunction[f].toFloat() + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/BlockInfoSmall.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/BlockInfoSmall.kt new file mode 100644 index 0000000..edc4260 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/BlockInfoSmall.kt @@ -0,0 +1,13 @@ +package gg.norisk.heroes.common.networking.dto + +import gg.norisk.heroes.common.serialization.BlockPosSerializer +import gg.norisk.heroes.common.serialization.BlockStateSerializer +import kotlinx.serialization.Serializable +import net.minecraft.block.BlockState +import net.minecraft.util.math.BlockPos + +@Serializable +data class BlockInfoSmall( + @Serializable(with = BlockStateSerializer::class) val state: BlockState, + @Serializable(with = BlockPosSerializer::class) val pos: BlockPos +) diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/HeroSelectorPacket.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/HeroSelectorPacket.kt new file mode 100644 index 0000000..11cfd75 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/HeroSelectorPacket.kt @@ -0,0 +1,10 @@ +package gg.norisk.heroes.common.networking.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class HeroSelectorPacket( + val heroes: List, + val isActive: Boolean, + var isKitEditorEnabled: Boolean +) \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/MouseClickPacket.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/MouseClickPacket.kt new file mode 100644 index 0000000..9d5e786 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/networking/dto/MouseClickPacket.kt @@ -0,0 +1,30 @@ +package gg.norisk.heroes.common.networking.dto + +import kotlinx.serialization.Serializable + +enum class MouseType { + LEFT, MIDDLE, RIGHT +} + +enum class MouseAction { + CLICK, RELEASE, HOLD +} + +@Serializable +data class MousePacket(val type: MouseType, val action: MouseAction) { + fun isLeft(): Boolean = type == MouseType.LEFT + fun isRight(): Boolean = type == MouseType.RIGHT + fun isMiddle(): Boolean = type == MouseType.MIDDLE + + fun isHolding(): Boolean = action == MouseAction.HOLD + fun isReleased(): Boolean = action == MouseAction.RELEASE + fun isClicked(): Boolean = action == MouseAction.CLICK + + fun isHoldingLeftClick(): Boolean = isLeft() && isHolding() + fun isHoldingRightClick(): Boolean = isRight() && isHolding() + fun isHoldingMiddleClick(): Boolean = isMiddle() && isHolding() + + override fun toString(): String { + return "[$type, $action]" + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/player/FFAPlayer.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/player/FFAPlayer.kt new file mode 100644 index 0000000..4480f6e --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/player/FFAPlayer.kt @@ -0,0 +1,70 @@ +package gg.norisk.heroes.common.player + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.ability.PropertyPlayer +import gg.norisk.heroes.common.serialization.ItemStackSerializer +import gg.norisk.heroes.common.serialization.UUIDSerializer +import gg.norisk.heroes.common.utils.PlayStyle +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.server.network.ServerPlayerEntity +import java.util.* + +@Serializable +data class FFAPlayer( + @SerialName("_id") + @Serializable(with = UUIDSerializer::class) + val uuid: UUID, + var xp: Int = 0, + var kills: Int = 0, + var deaths: Int = 0, + var currentKillStreak: Int = 0, + var highestKillStreak: Int = 0, + var bounty: Int = 0, + var heroes: MutableMap>> = mutableMapOf(), + var inventorySorting: InventorySorting? = null, +) + +@Serializable +data class InventorySorting( + @Serializable(with = UUIDSerializer::class) + val userId: UUID, + val playStyle: PlayStyle, + val version: Int, + val armor: Array<@Serializable(with = ItemStackSerializer::class) ItemStack> = Array(4) { ItemStack.EMPTY }, + val offhand: Array<@Serializable(with = ItemStackSerializer::class) ItemStack> = Array(1) { ItemStack.EMPTY }, + val main: Array<@Serializable(with = ItemStackSerializer::class) ItemStack> = Array(36) { ItemStack.EMPTY }, +) { + companion object { + var CURRENT_VERSION = 0 + + fun ServerPlayerEntity.loadInventory(inventorySorting: InventorySorting) { + logger.info("Loading inventory $inventorySorting") + inventory.clear() + inventorySorting.armor.forEachIndexed { index, itemStack -> + inventory.armor[index] = itemStack.copy() + } + inventorySorting.main.forEachIndexed { index, itemStack -> + inventory.main[index] = itemStack.copy() + } + inventorySorting.offhand.forEachIndexed { index, itemStack -> + inventory.offHand[index] = itemStack.copy() + } + } + } +} + +private const val FFA_PLAYER = "HeroApi:FfaPlayer" +var PlayerEntity.ffaPlayer: FFAPlayer + get() = this.getSyncedData(FFA_PLAYER) ?: FFAPlayer(this.uuid) + set(value) = this.setSyncedData(FFA_PLAYER, value, (this as? ServerPlayerEntity?)) + +private const val FFA_BOUNTY = "HeroApi:Bounty" +var PlayerEntity.ffaBounty: Int + get() = this.getSyncedData(FFA_BOUNTY) ?: 0 + set(value) = this.setSyncedData(FFA_BOUNTY, value) + diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/registry/SoundRegistry.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/registry/SoundRegistry.kt new file mode 100644 index 0000000..8d44354 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/registry/SoundRegistry.kt @@ -0,0 +1,15 @@ +package gg.norisk.heroes.common.registry + +import gg.norisk.heroes.common.HeroesManager.toId +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry +import net.minecraft.sound.SoundEvent + +object SoundRegistry { + var FLYING = "flying".register() + + fun init() { + } + + private fun String.register() = Registry.register(Registries.SOUND_EVENT, this.toId(), SoundEvent.of(this.toId())) +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/BlockPosSerializer.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/BlockPosSerializer.kt new file mode 100644 index 0000000..49eeb1c --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/BlockPosSerializer.kt @@ -0,0 +1,43 @@ +package gg.norisk.heroes.common.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.* +import net.minecraft.util.math.BlockPos + +object BlockPosSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("BlockPos") { + element("x") + element("y") + element("z") + } + + override fun serialize(encoder: Encoder, value: BlockPos) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.x) + encodeIntElement(descriptor, 1, value.y) + encodeIntElement(descriptor, 2, value.z) + } + } + + override fun deserialize(decoder: Decoder): BlockPos { + return decoder.decodeStructure(descriptor) { + var x = 0 + var y = 0 + var z = 0 + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> x = decodeIntElement(descriptor, 0) + 1 -> y = decodeIntElement(descriptor, 1) + 2 -> z = decodeIntElement(descriptor, 2) + CompositeDecoder.DECODE_DONE -> break + else -> throw SerializationException("Unknown index $index") + } + } + BlockPos(x, y, z) + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/BlockStateSerializer.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/BlockStateSerializer.kt new file mode 100644 index 0000000..9a36485 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/BlockStateSerializer.kt @@ -0,0 +1,27 @@ +package gg.norisk.heroes.common.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.minecraft.block.BlockState +import net.minecraft.nbt.NbtHelper +import net.minecraft.nbt.StringNbtReader +import net.minecraft.registry.Registries + +object BlockStateSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("BlockState") + + override fun serialize(encoder: Encoder, value: BlockState) { + val nbt = NbtHelper.fromBlockState(value) + encoder.encodeString(nbt.asString()) + } + + override fun deserialize(decoder: Decoder): BlockState { + val nbtString = decoder.decodeString() + val nbt = StringNbtReader.parse(nbtString) + val blockState = NbtHelper.toBlockState(Registries.BLOCK.tagCreatingWrapper, nbt) + return blockState + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/ItemStackSerializer.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/ItemStackSerializer.kt new file mode 100644 index 0000000..563dfe9 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/ItemStackSerializer.kt @@ -0,0 +1,44 @@ +package gg.norisk.heroes.common.serialization + +import gg.norisk.heroes.common.HeroesManager.isServer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.fabricmc.api.EnvType +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.MinecraftClient +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtHelper +import net.silkmc.silk.core.Silk.serverOrThrow + +object ItemStackSerializer : KSerializer { + private val emptyItemStack = "EMPTY" + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ItemStack", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: ItemStack) { + if (value.isEmpty) { + encoder.encodeString(emptyItemStack) + return + } + val registryManager = + if (isServer) serverOrThrow.registryManager else MinecraftClient.getInstance().world?.registryManager + val nbt = value.encode(registryManager) as NbtCompound + val string = NbtHelper.toNbtProviderString(nbt) + encoder.encodeString(string) + } + + override fun deserialize(decoder: Decoder): ItemStack { + val registryManager = + if (isServer) serverOrThrow.registryManager else MinecraftClient.getInstance().world?.registryManager + val string = decoder.decodeString() + if (string == emptyItemStack) { + return ItemStack.EMPTY + } + val nbt = NbtHelper.fromNbtProviderString(string) + return ItemStack.fromNbt(registryManager, nbt).orElseThrow() + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/PrimitiveSerializers.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/PrimitiveSerializers.kt new file mode 100644 index 0000000..14d549a --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/PrimitiveSerializers.kt @@ -0,0 +1,43 @@ +package gg.norisk.heroes.common.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@PublishedApi +internal object StringSerializer : KSerializer { + override val descriptor: SerialDescriptor = String.serializer().descriptor + override fun serialize(encoder: Encoder, value: String): Unit = String.serializer().serialize(encoder, value) + override fun deserialize(decoder: Decoder): String = String.serializer().deserialize(decoder) +} + +@PublishedApi +internal object BooleanSerializer : KSerializer { + override val descriptor: SerialDescriptor = Boolean.serializer().descriptor + override fun serialize(encoder: Encoder, value: Boolean): Unit = Boolean.serializer().serialize(encoder, value) + override fun deserialize(decoder: Decoder): Boolean = Boolean.serializer().deserialize(decoder) +} + + +@PublishedApi +internal object IntSerializer : KSerializer { + override val descriptor: SerialDescriptor = Int.serializer().descriptor + override fun serialize(encoder: Encoder, value: Int): Unit = Int.serializer().serialize(encoder, value) + override fun deserialize(decoder: Decoder): Int = Int.serializer().deserialize(decoder) +} + +@PublishedApi +internal object FloatSerializer : KSerializer { + override val descriptor: SerialDescriptor = Float.serializer().descriptor + override fun serialize(encoder: Encoder, value: Float): Unit = Float.serializer().serialize(encoder, value) + override fun deserialize(decoder: Decoder): Float = Float.serializer().deserialize(decoder) +} + +@PublishedApi +internal object DoubleSerializer : KSerializer { + override val descriptor: SerialDescriptor = Double.serializer().descriptor + override fun serialize(encoder: Encoder, value: Double): Unit = Double.serializer().serialize(encoder, value) + override fun deserialize(decoder: Decoder): Double = Double.serializer().deserialize(decoder) +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/SerializationHelper.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/SerializationHelper.kt new file mode 100644 index 0000000..2fd0da3 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/SerializationHelper.kt @@ -0,0 +1,113 @@ +package gg.norisk.heroes.common.serialization + +import com.mongodb.internal.connection.BsonWriterDecorator +import kotlinx.serialization.ExperimentalSerializationApi +import org.bson.AbstractBsonReader +import org.bson.AbstractBsonWriter +import org.bson.codecs.kotlinx.BsonDecoder +import org.bson.codecs.kotlinx.BsonEncoder +import java.lang.reflect.Field +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.javaField + +@OptIn(ExperimentalSerializationApi::class) +object SerializationHelper { + fun isNameState(bsonEncoder: BsonEncoder): Boolean { + return WriterHelper.getState(bsonEncoder) == AbstractBsonWriter.State.NAME + } + + fun isNameState(bsonDecoder: BsonDecoder): Boolean { + return ReaderHelper.isNameState(bsonDecoder) + } + + private object WriterHelper { + private val BsonEncoder_writer_field: Field by lazy { + Class.forName("org.bson.codecs.kotlinx.JsonBsonEncoder").kotlin.memberProperties + .find { it.name == "writer" }?.javaField!!.also { + it.isAccessible = true + } + } + private val BsonWriterDecorator_bsonWriter_field: Field by lazy { + BsonWriterDecorator::class.memberProperties.find { it.name == "bsonWriter" }?.javaField!!.also { + it.isAccessible = true + } + } + private val BsonWriterDecorator_state_field: Field by lazy { + AbstractBsonWriter::class.memberProperties.find { it.name == "state" }?.javaField!!.also { + it.isAccessible = true + } + } + + private fun `get BsonWriterDecorator of JsonBsonEncoder`(encoder: BsonEncoder): BsonWriterDecorator { + val writerDecorator = BsonEncoder_writer_field.get(encoder) as? BsonWriterDecorator + ?: throw IllegalStateException("writerDecorator is not a ${BsonWriterDecorator::class.qualifiedName}.") + return writerDecorator + } + + private fun `get AbstractBsonWriter of BsonWriterDecorator`(bsonWriterDecorator: BsonWriterDecorator): AbstractBsonWriter { + val abstractBsonWriter = + BsonWriterDecorator_bsonWriter_field.get(bsonWriterDecorator) as? AbstractBsonWriter + ?: throw IllegalStateException("abstractBsonWriter is not a ${AbstractBsonWriter::class.qualifiedName}.") + return abstractBsonWriter + } + + private fun `get State of AbstractBsonWriter`(abstractBsonWriter: AbstractBsonWriter): AbstractBsonWriter.State { + val state = BsonWriterDecorator_state_field.get(abstractBsonWriter) as? AbstractBsonWriter.State + ?: throw IllegalStateException("abstractBsonWriter is not a ${AbstractBsonWriter::class.qualifiedName}.") + return state + } + + fun getState(bsonEncoder: BsonEncoder): AbstractBsonWriter.State { + val bsonWriterDecoder = `get BsonWriterDecorator of JsonBsonEncoder`(bsonEncoder) + val abstractBsonWriter = `get AbstractBsonWriter of BsonWriterDecorator`(bsonWriterDecoder) + val state = `get State of AbstractBsonWriter`(abstractBsonWriter) + return state + } + } + + private object ReaderHelper { + private val JsonBsonMapDecoderClass: Class<*> by lazy { + Class.forName("org.bson.codecs.kotlinx.JsonBsonMapDecoder") + } + + /* private val AbstractBsonDecoderClass: Class<*> by lazy { + Class.forName("org.bson.codecs.kotlinx.AbstractBsonDecoder") + }*/ + + private val JsonBsonDocumentDecoder_reader_field: Field by lazy { + JsonBsonMapDecoderClass.kotlin.memberProperties.find { it.name == "reader" }?.javaField!!.also { + it.isAccessible = true + } + } + + private fun `BsonDecoder is JsonBsonMapDecoder`(bsonDecoder: BsonDecoder): Boolean { + return JsonBsonMapDecoderClass.isInstance(bsonDecoder) + } + + private fun `BsonDecoder as JsonBsonDocumentDecoder`(bsonDecoder: BsonDecoder): Any { + val jsonBsonDocumentDecoder = JsonBsonMapDecoderClass.cast(bsonDecoder) + return jsonBsonDocumentDecoder + } + + private fun `get AbstractBsonReader of JsonBsonDocumentDecoder`(jsonBsonDocumentDecoder: Any): AbstractBsonReader { + val reader = JsonBsonDocumentDecoder_reader_field.get(jsonBsonDocumentDecoder) as AbstractBsonReader + return reader + } + + private fun `get State of AbstractBsonReader`(abstractBsonReader: AbstractBsonReader): AbstractBsonReader.State { + return abstractBsonReader.state + } + + private fun getState(bsonDecoder: BsonDecoder): AbstractBsonReader.State { + val jsonBsonDocumentDecoder = `BsonDecoder as JsonBsonDocumentDecoder`(bsonDecoder) + val abstractBsonReader = `get AbstractBsonReader of JsonBsonDocumentDecoder`(jsonBsonDocumentDecoder) + val state = `get State of AbstractBsonReader`(abstractBsonReader) + return state + } + + fun isNameState(bsonDecoder: BsonDecoder): Boolean { + if (!`BsonDecoder is JsonBsonMapDecoder`(bsonDecoder)) return false + return getState(bsonDecoder) == AbstractBsonReader.State.NAME + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/UUIDSerializer.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/UUIDSerializer.kt new file mode 100644 index 0000000..8167439 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/serialization/UUIDSerializer.kt @@ -0,0 +1,65 @@ +package gg.norisk.heroes.common.serialization + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.silkmc.silk.core.logging.logger +import org.bson.BsonBinary +import org.bson.UuidRepresentation +import org.bson.codecs.kotlinx.BsonDecoder +import org.bson.codecs.kotlinx.BsonEncoder +import java.util.* + +object UUIDSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) + + @OptIn(ExperimentalSerializationApi::class) + override fun serialize(encoder: Encoder, value: UUID) { + runCatching { + if (encoder is BsonEncoder && !SerializationHelper.isNameState(encoder)) { + // Wenn BSON und kein NAME (wie in einer Map), serialize als binary + val bsonBinary = BsonBinary(value, UuidRepresentation.STANDARD) + return encoder.encodeBsonValue(bsonBinary) + } + }.onFailure { + print("Caught exception when serializing uuid: ${it.stackTraceToString()}") + } + + // Wenn nicht BSON, dann serialize als String + encoder.encodeString(value.toString()) + } + + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): UUID { + runCatching { + if (decoder is BsonDecoder && !SerializationHelper.isNameState(decoder)) { + val decoded = decoder.decodeBsonValue() + //println("Value: $decoded") + return decoded.asBinary().asUuid() + } + }.onFailure { + print("Caught exception when serializing uuid: ${it.stackTraceToString()}") + } + // Wenn nicht BSON, dann serialize als String + var uuidString = decoder.decodeString() + if (!uuidString.contains("-")) { + uuidString = insertDashUUID(uuidString) + } + return UUID.fromString(uuidString) + } + + private fun insertDashUUID(uuid: String): String { + var sb = StringBuilder(uuid) + sb.insert(8, "-") + sb = StringBuilder(sb.toString()) + sb.insert(13, "-") + sb = StringBuilder(sb.toString()) + sb.insert(18, "-") + sb = StringBuilder(sb.toString()) + sb.insert(23, "-") + return sb.toString() + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/sound/FlyingSoundInstance.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/sound/FlyingSoundInstance.kt new file mode 100644 index 0000000..e20c8d8 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/sound/FlyingSoundInstance.kt @@ -0,0 +1,48 @@ +package gg.norisk.heroes.common.sound + +import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.client.sound.MovingSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvents +import net.minecraft.util.math.MathHelper +import java.util.function.Supplier + +class FlyingSoundInstance( + private val player: ClientPlayerEntity, + private val condition: Supplier +) : + MovingSoundInstance(SoundEvents.ITEM_ELYTRA_FLYING, SoundCategory.PLAYERS, SoundInstance.createRandom()) { + private var tickCount = 0 + + init { + this.repeat = true + this.repeatDelay = 0 + this.volume = 0.1f + } + + override fun tick() { + ++this.tickCount + this.x = player.x.toFloat().toDouble() + this.y = player.y.toFloat().toDouble() + this.z = player.z.toFloat().toDouble() + val f = player.velocity.lengthSquared().toFloat() * 2 + if (f.toDouble() >= 1.0E-7) { + this.volume = MathHelper.clamp(f / 4.0f, 0.0f, 1.0f) + } else { + this.volume = 0.0f + } + + if (!player.isRemoved && (condition.get())) { + if (this.volume > 0.8f) { + this.pitch = 1.0f + (this.volume - 0.8f) + } else { + this.pitch = 1.0f + } + } else { + if (player.isOnGround) { + this.setDone() + } + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/DynamicStringConversion.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/DynamicStringConversion.kt new file mode 100644 index 0000000..95b98f8 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/DynamicStringConversion.kt @@ -0,0 +1,36 @@ +package gg.norisk.heroes.common.utils + +import java.lang.reflect.Method +import kotlin.reflect.KClass + +object DynamicStringConversion { + private val conversionFunction = hashMapOf() + + fun convertString(valueAsString: String, type: String): Any { + runCatching { + val typeName = type.lowercase() + if (typeName == "string") return valueAsString + + val stringsKtClass = Class.forName("kotlin.text.StringsKt") + val function = conversionFunction.computeIfAbsent(typeName) { + stringsKtClass.methods.first { + it.name.startsWith("to$typeName", true) && it.parameterCount == 1 + } + } + function.isAccessible = true + function.invoke(null, valueAsString) + }.onSuccess { + return it + } + + throw IllegalArgumentException("Can not convert settings to type $type") + } + + fun convertString(valueAsString: String, type: KClass<*>): Any { + return convertString(valueAsString, type.simpleName!!) + } + + inline fun convertString(valueAsString: String): Any { + return convertString(valueAsString, T::class.simpleName!!) + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/EntityExtensions.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/EntityExtensions.kt new file mode 100644 index 0000000..362c359 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/EntityExtensions.kt @@ -0,0 +1,38 @@ +package gg.norisk.heroes.common.utils + +import net.minecraft.entity.Entity +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvent +import net.minecraft.util.math.Box +import net.minecraft.util.math.Vec3d +import net.minecraft.util.math.Vec3i +import net.silkmc.silk.core.entity.directionVector +import kotlin.math.cos +import kotlin.math.sin + +fun LivingEntity.raycastEntities(range: Int = 30, expand: Double = 1.0): Set { + val eyePos = eyePos + val eyeDirection = directionVector.normalize() + + val entities = mutableSetOf() + + repeat(range) { + val newPos = eyePos.add(eyeDirection.multiply(it.toDouble())) + entities += world.getOtherEntities(this, Box.from(newPos).expand(expand)) + } + + return entities +} + +val LivingEntity.pos3i: Vec3i get() = Vec3i(x.toInt(), y.toInt(), z.toInt()) + +fun Entity.sound(soundEvent: SoundEvent, volume: Number = 1f, pitch: Number = 1f) { + world.playSoundFromEntity(null, this, soundEvent, this.soundCategory, volume.toFloat(), pitch.toFloat()) +} + + +fun PlayerEntity.sound(soundEvent: SoundEvent) { + world.playSoundFromEntity(null, this, soundEvent, SoundCategory.PLAYERS, 1f, 1f) +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/HeroDataPackProvider.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/HeroDataPackProvider.kt new file mode 100644 index 0000000..1998a45 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/HeroDataPackProvider.kt @@ -0,0 +1,104 @@ +package gg.norisk.heroes.common.utils + +import com.google.common.annotations.VisibleForTesting +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.HeroesManager.isServer +import gg.norisk.heroes.common.HeroesManager.toId +import net.fabricmc.api.EnvType +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.SharedConstants +import net.minecraft.registry.VersionedIdentifier +import net.minecraft.resource.* +import net.minecraft.resource.featuretoggle.FeatureSet +import net.minecraft.resource.metadata.PackFeatureSetMetadata +import net.minecraft.resource.metadata.PackResourceMetadata +import net.minecraft.resource.metadata.ResourceMetadataMap +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.path.SymlinkFinder +import java.nio.file.Path +import java.util.* + +class HeroDataPackProvider(symlinkFinder: SymlinkFinder?) : VanillaResourcePackProvider( + ResourceType.SERVER_DATA, + createDefaultPack(), + ID, + symlinkFinder +) { + override fun getDisplayName(string: String): Text = Text.literal(string) + + override fun createDefault(resourcePack: ResourcePack): ResourcePackProfile? { + return ResourcePackProfile.create( + INFO, + createPackFactory(resourcePack), + ResourceType.SERVER_DATA, + BOTTOM_POSITION + ) + } + + override fun create( + string: String, + packFactory: ResourcePackProfile.PackFactory, + text: Text + ): ResourcePackProfile? { + return ResourcePackProfile.create(createInfo(string, text), packFactory, ResourceType.SERVER_DATA, TOP_POSITION) + } + + companion object { + private val METADATA = PackResourceMetadata( + Text.translatable("dataPack.hero.description"), + SharedConstants.getGameVersion().getResourceVersion(ResourceType.SERVER_DATA), + Optional.empty() + ) + private val FEATURE_FLAGS = PackFeatureSetMetadata(FeatureSet.of(HeroesManager.heroesFlag)) + private val METADATA_MAP: ResourceMetadataMap = ResourceMetadataMap.of( + PackResourceMetadata.SERIALIZER, + METADATA, + PackFeatureSetMetadata.SERIALIZER, + FEATURE_FLAGS + ) + private val INFO: ResourcePackInfo + private val BOTTOM_POSITION: ResourcePackPosition + private val TOP_POSITION: ResourcePackPosition + private val ID: Identifier + + private fun createInfo(string: String, text: Text): ResourcePackInfo { + return ResourcePackInfo(string, text, source, Optional.of(createHero(string))) + } + + private fun createHero(string: String): VersionedIdentifier { + return VersionedIdentifier(HeroesManager.MOD_ID, string, SharedConstants.getGameVersion().id) + } + + @VisibleForTesting + fun createDefaultPack(): DefaultResourcePack { + return DefaultResourcePackBuilder() + .withMetadataMap(METADATA_MAP) + .withNamespaces(HeroesManager.MOD_ID) + .runCallback() + .withDefaultPaths() + .build(INFO) + } + + fun createManager(path: Path?, symlinkFinder: SymlinkFinder?): ResourcePackManager { + return ResourcePackManager( + HeroDataPackProvider(symlinkFinder), + FileResourcePackProvider(path, ResourceType.SERVER_DATA, ResourcePackSource.WORLD, symlinkFinder) + ) + } + + val source get() = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) ResourcePackSource.SERVER else ResourcePackSource.FEATURE + + init { + INFO = ResourcePackInfo( + "hero", + Text.translatable("dataPack.hero.name"), + source, + Optional.of(createHero("core")) + ) + BOTTOM_POSITION = ResourcePackPosition(false, ResourcePackProfile.InsertionPosition.BOTTOM, false) + TOP_POSITION = ResourcePackPosition(false, ResourcePackProfile.InsertionPosition.TOP, false) + ID = "heroes".toId() + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/MathExtensions.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/MathExtensions.kt new file mode 100644 index 0000000..8a8e971 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/MathExtensions.kt @@ -0,0 +1,46 @@ +package gg.norisk.heroes.common.utils + +import gg.norisk.heroes.common.HeroesManager.logger +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction +import net.minecraft.util.math.Vec3d +import net.minecraft.world.World +import org.joml.Vector3f +import java.io.File +import kotlin.random.Random + +fun Vector3f.toVector() = Vec3d(x.toDouble(), y.toDouble(), z.toDouble()) +fun Vec3d.toBlockPos() = BlockPos(x.toInt(), y.toInt(), z.toInt()) +fun BlockPos.toVec() = Vec3d(x.toDouble(), y.toDouble(), z.toDouble()) +fun World.hasFreeSide(blockPos: BlockPos) = + Direction.values().any { direction: Direction -> this.getBlockState(blockPos.offset(direction)).isAir } + +fun World.hasFreeHorizontalSide(blockPos: BlockPos): Direction? { + for (direction in Direction.Type.HORIZONTAL.toList()) { + if (this.getBlockState(blockPos.offset(direction)).isAir) { + return direction + } + } + return null +} + +fun File.createIfNotExists(): File { + if (this.exists()) return this + if (!this.parentFile.mkdirs() && !this.parentFile.exists()) { + logger.warn("Parent of ${this.name} does not exist and could not be created") + } + this.createNewFile() + return this +} + +fun calculateProbability(probabilityInPercent: Double): Boolean { + require(probabilityInPercent in 0.0..100.0) { + "Die Wahrscheinlichkeit muss zwischen 0 und 100 Prozent liegen." + } + val randomValue = Random.nextDouble(0.0, 100.0) + return randomValue < probabilityInPercent +} + +fun ClosedFloatingPointRange.random(): Double { + return Random.nextDouble(this.start, this.endInclusive) +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/PlayStyle.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/PlayStyle.kt new file mode 100644 index 0000000..f4f4cb0 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/PlayStyle.kt @@ -0,0 +1,23 @@ +package gg.norisk.heroes.common.utils + +import gg.norisk.heroes.common.HeroesManager + +enum class PlayStyle(val displayName: String) { + SOUP("Soup"), UHC("UHC"); + + companion object { + val current by lazy { + (runCatching { + PlayStyle.entries.first { + it.displayName.equals( + System.getProperty( + "ffa_mode", "UHC" + ), true + ) + } + }.getOrNull() ?: UHC).also { + HeroesManager.logger.info("Current play style: $it") + } + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/RaycastUtils.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/RaycastUtils.kt new file mode 100644 index 0000000..75d4d82 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/RaycastUtils.kt @@ -0,0 +1,114 @@ +package gg.norisk.heroes.common.utils + +import net.minecraft.entity.Entity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.entity.projectile.ProjectileUtil +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.hit.EntityHitResult +import net.minecraft.util.math.Box +import net.minecraft.util.math.Vec3d +import net.minecraft.world.RaycastContext +import net.minecraft.world.RaycastContext.FluidHandling +import net.minecraft.world.World +import java.util.* +import java.util.function.Predicate +import kotlin.math.cos +import kotlin.math.sin + + +/** + * Credits to [Source](https://github.com/vini2003/Maven) + */ +object RaycastUtils { + fun raycastEntity(source: PlayerEntity, delta: Float, maxDist: Float): Entity? { + val pos = source.getCameraPosVec(delta) + val rot = source.getCameraPosVec(1.0f) + val end = pos.add(rot.x * maxDist.toDouble(), rot.y * maxDist.toDouble(), rot.z * maxDist.toDouble()) + val sourceBox = source.boundingBox.stretch(rot.multiply(maxDist.toDouble())).expand(2.5, 2.5, 2.5) + val entityHitResult = raycastEntity( + source, + pos, + end, + sourceBox, + { entity: Entity -> !entity.isSpectator && entity.canHit() }, + (maxDist * maxDist).toDouble() + ) + return entityHitResult?.entity + } + + fun raycastEntity(entity: Entity, i: Int): Optional { + return run { + val vec3d = entity.eyePos + val vec3d2 = entity.getRotationVec(1.0f).multiply(i.toDouble()) + val vec3d3 = vec3d.add(vec3d2) + val box = entity.boundingBox.stretch(vec3d2).expand(1.0) + val j = i * i + val predicate = + Predicate { entityx: Entity -> !entityx.isSpectator && entityx.canHit() } + val entityHitResult = ProjectileUtil.raycast(entity, vec3d, vec3d3, box, predicate, j.toDouble()) + if (entityHitResult == null) { + Optional.empty() + } else { + if (vec3d.squaredDistanceTo(entityHitResult.pos) > j.toDouble()) Optional.empty() else Optional.of( + entityHitResult.entity + ) + } + } + } + + fun clipWithDistance(player: PlayerEntity, level: World, clipDistance: Double, fluidHandling: FluidHandling = FluidHandling.NONE): BlockHitResult? { + val vecX = sin(-player.yaw * (Math.PI / 180.0) - Math.PI) * -cos(-player.pitch * (Math.PI / 180.0)) + val vecY = sin(-player.pitch * (Math.PI / 180.0)) + val vecZ = cos(-player.yaw * (Math.PI / 180.0) - Math.PI) * -cos(-player.pitch * (Math.PI / 180.0)) + return level.raycast( + RaycastContext( + player.eyePos, + player.eyePos.add(vecX * clipDistance, vecY * clipDistance, vecZ * clipDistance), + RaycastContext.ShapeType.OUTLINE, + fluidHandling, + player + ) + ) + } + + fun raycastEntity( + source: PlayerEntity, + pos: Vec3d, + end: Vec3d?, + box: Box?, + predicate: Predicate?, + sqrMaxDist: Double + ): EntityHitResult? { + val world = source.world + var distToGo = sqrMaxDist + var target: Entity? = null + var targetPos: Vec3d? = null + for (entity in world.getOtherEntities(source, box, predicate)) { + val entityBox = entity.boundingBox.expand(entity.targetingMargin.toDouble()) + val optPos = entityBox.raycast(pos, end) + if (entityBox.contains(pos)) { + if (distToGo >= 0.0) { + target = entity + targetPos = optPos.orElse(pos) + distToGo = 0.0 + } + } else if (optPos.isPresent) { + val entityPos = optPos.get() + val sqrDist = pos.squaredDistanceTo(entityPos) + if (sqrDist < distToGo || distToGo == 0.0) { + if (entity.rootVehicle === source.rootVehicle) { + if (distToGo == 0.0) { + target = entity + targetPos = entityPos + } + } else { + target = entity + targetPos = entityPos + distToGo = sqrDist + } + } + } + } + return target?.let { EntityHitResult(it, targetPos) } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/SphereUtils.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/SphereUtils.kt new file mode 100644 index 0000000..6845093 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/utils/SphereUtils.kt @@ -0,0 +1,23 @@ +package gg.norisk.heroes.common.utils + +import net.minecraft.util.math.BlockPos + +object SphereUtils { + fun generateSphere(centerBlock: BlockPos, radius: Int, hollow: Boolean): List { + val circleBlocks: MutableList = ArrayList() + val bx: Int = centerBlock.x + val by: Int = centerBlock.y + val bz: Int = centerBlock.z + for (x in bx - radius..bx + radius) { + for (y in by - radius..by + radius) { + for (z in bz - radius..bz + radius) { + val distance = ((bx - x) * (bx - x) + (bz - z) * (bz - z) + (by - y) * (by - y)).toDouble() + if (distance < radius * radius && !(hollow && distance < (radius - 1) * (radius - 1))) { + circleBlocks.add(BlockPos(x, y, z)) + } + } + } + } + return circleBlocks + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/server/HeroesManagerServer.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/server/HeroesManagerServer.kt new file mode 100644 index 0000000..a45cb5e --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/server/HeroesManagerServer.kt @@ -0,0 +1,72 @@ +package gg.norisk.heroes.server + +import gg.norisk.heroes.common.HeroesManager.MOD_ID +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.HeroesManager.toId +import gg.norisk.heroes.common.command.DebugCommand +import gg.norisk.heroes.common.events.HeroEvents +import gg.norisk.heroes.common.ffa.KitEditorManager +import gg.norisk.heroes.common.ffa.experience.Experience +import gg.norisk.heroes.common.hero.HeroManager +import gg.norisk.heroes.common.hero.setHero +import gg.norisk.heroes.common.networking.Networking +import gg.norisk.heroes.common.networking.dto.HeroSelectorPacket +import gg.norisk.heroes.server.config.ConfigManagerServer +import gg.norisk.heroes.server.database.inventory.InventoryProvider +import gg.norisk.heroes.server.database.player.PlayerProvider +import gg.norisk.heroes.server.hero.ability.AbilityManagerServer +import net.fabricmc.api.EnvType +import net.fabricmc.fabric.api.resource.ResourceManagerHelper +import net.fabricmc.fabric.api.resource.ResourcePackActivationType +import net.fabricmc.loader.api.FabricLoader +import net.silkmc.silk.core.task.mcCoroutineTask + +object HeroesManagerServer { + fun initServer() { + logger.info("Init Hero server...") + + ConfigManagerServer.init() + AbilityManagerServer.init() + DebugCommand.initServer() + PlayerProvider.init() + InventoryProvider.init() + Experience.init() + KitEditorManager.init() + + handleHeroSelectorPacket() + + FabricLoader.getInstance().getModContainer(MOD_ID).ifPresent { + val type = if (FabricLoader.getInstance().environmentType == EnvType.SERVER) { + ResourcePackActivationType.ALWAYS_ENABLED + } else { + ResourcePackActivationType.NORMAL + } + val result = ResourceManagerHelper.registerBuiltinResourcePack( + "heroes".toId(), + it, + type + ) + logger.info("Init Heroes DataPack: $result") + } + } + + private fun handleHeroSelectorPacket() { + Networking.c2sHeroSelectorPacket.receiveOnServer { packet, context -> + mcCoroutineTask(sync = true, client = false) { + val hero = HeroManager.getHero(packet) ?: return@mcCoroutineTask + val event = HeroEvents.HeroSelectEvent(context.player, hero, true) + HeroEvents.heroSelectEvent.invoke(event) + if (event.canSelect) { + context.player.setHero(hero) + Networking.s2cHeroSelectorPacket.send( + HeroSelectorPacket( + emptyList(), + false, + KitEditorManager.hasKitWorld + ), context.player + ) + } + } + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/server/config/ConfigManagerServer.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/server/config/ConfigManagerServer.kt new file mode 100644 index 0000000..f16ad18 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/server/config/ConfigManagerServer.kt @@ -0,0 +1,40 @@ +package gg.norisk.heroes.server.config + +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.HeroManager +import gg.norisk.heroes.common.networking.Networking +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents +import net.minecraft.server.network.ServerPlayerEntity + +object ConfigManagerServer { + val JSON = Json { + prettyPrint = true + ignoreUnknownKeys = true + } + + fun sendHeroSettings(vararg hero: Hero, playerEntity: ServerPlayerEntity? = null) { + val heroJsons = hero.map { it.toHeroJson() } + val encoded = JSON.encodeToString(heroJsons) + if (playerEntity != null) { + Networking.s2cHeroSettingsPacket.send(encoded, playerEntity) + } else { + Networking.s2cHeroSettingsPacket.sendToAll(encoded) + } + } + + fun init() { + ServerPlayConnectionEvents.JOIN.register(ServerPlayConnectionEvents.Join { handler, sender, server -> + sendHeroSettings( + *HeroManager.registeredHeroes.values.toTypedArray(), + playerEntity = handler.player + ) + }) + + ServerLifecycleEvents.SERVER_STARTED.register(ServerLifecycleEvents.ServerStarted { + HeroManager.reloadHeroes(*HeroManager.registeredHeroes.values.toTypedArray()) + }) + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/MongoManager.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/MongoManager.kt new file mode 100644 index 0000000..2238388 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/MongoManager.kt @@ -0,0 +1,43 @@ +package gg.norisk.heroes.server.database + +import com.mongodb.MongoClientSettings +import com.mongodb.MongoCredential +import com.mongodb.ServerAddress +import com.mongodb.kotlin.client.coroutine.MongoClient +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import org.bson.UuidRepresentation + +object MongoManager { + var isConnected: Boolean = false + lateinit var client: MongoClient + lateinit var database: MongoDatabase + val databaseName + get() = System.getProperty( + "ffa_database", + "ffa" + ) + + fun connect() { + client = MongoClient.create( + MongoClientSettings.builder() + .uuidRepresentation(UuidRepresentation.STANDARD) + .credential( + MongoCredential.createCredential( + "root", + "admin", + "nWcbOgR5R0L9QePfU03cotqPrpGwbu".toCharArray() + ) + ).applyToClusterSettings { + it.hosts( + listOf( + ServerAddress( + "194.164.200.242", + System.getenv("MONGODB_PORT").toIntOrNull() ?: 27017 + ) + ) + ) + }.build() + ) + database = client.getDatabase(databaseName) + } +} \ No newline at end of file diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/inventory/InventoryProvider.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/inventory/InventoryProvider.kt new file mode 100644 index 0000000..82bcb0c --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/inventory/InventoryProvider.kt @@ -0,0 +1,53 @@ +package gg.norisk.heroes.server.database.inventory + +import gg.norisk.datatracker.entity.registeredTypes +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.database.inventory.AbstractInventoryProvider +import gg.norisk.heroes.common.database.inventory.JsonInventoryProvider +import gg.norisk.heroes.common.player.InventorySorting +import gg.norisk.heroes.common.utils.PlayStyle +import gg.norisk.heroes.server.database.MongoManager +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents +import net.minecraft.server.network.ServerPlayerEntity +import java.util.* + +object InventoryProvider : AbstractInventoryProvider(PlayStyle.current) { + private var provider: AbstractInventoryProvider = JsonInventoryProvider() + + fun init() { + (registeredTypes as MutableMap).put( + InventorySorting::class, + InventorySorting.serializer() + ) + + ServerLifecycleEvents.SERVER_STARTING.register { + runCatching { + MongoManager.connect() + }.onSuccess { + provider = MongoInventoryProvider() + logger.info("Initialized provider: ${provider::class.simpleName}") + }.onFailure { + it.printStackTrace() + MongoManager.isConnected = false + provider = JsonInventoryProvider() + } + logger.info("Initialized provider: ${provider::class.simpleName}") + } + } + + override suspend fun save(data: InventorySorting?) { + provider.save(data) + } + + override suspend fun get(uuid: UUID): InventorySorting? { + return provider.get(uuid) + } + + override suspend fun onPlayerJoin(player: ServerPlayerEntity) { + provider.onPlayerJoin(player) + } + + override suspend fun onPlayerLeave(player: ServerPlayerEntity) { + provider.onPlayerLeave(player) + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/inventory/MongoInventoryProvider.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/inventory/MongoInventoryProvider.kt new file mode 100644 index 0000000..518d906 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/inventory/MongoInventoryProvider.kt @@ -0,0 +1,59 @@ +package gg.norisk.heroes.server.database.inventory + +import com.mongodb.client.model.ReplaceOptions +import de.hglabor.utils.database.extensions.findOne +import de.hglabor.utils.database.extensions.getOrCreateCollection +import de.hglabor.utils.database.kmongo.and +import de.hglabor.utils.database.kmongo.eq +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.database.inventory.AbstractInventoryProvider +import gg.norisk.heroes.common.player.InventorySorting +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.heroes.common.utils.PlayStyle +import gg.norisk.heroes.server.database.MongoManager +import net.minecraft.server.network.ServerPlayerEntity +import java.util.* + +class MongoInventoryProvider : AbstractInventoryProvider(PlayStyle.current) { + private val collection = MongoManager.database.getOrCreateCollection("inventories") + + private suspend fun find(uuid: UUID): InventorySorting? { + return collection.findOne(and(InventorySorting::userId eq uuid, InventorySorting::playStyle eq playStyle)) + } + + override suspend fun get(uuid: UUID): InventorySorting? { + val player = getCached(uuid) ?: find(uuid) + cache[uuid] = player + return player + } + + override suspend fun save(data: InventorySorting?) { + if (data == null) { + logger.info("Cant save Inventory `null`") + return + } + cache[data.userId] = data + collection.replaceOne( + and(InventorySorting::userId eq data.userId, InventorySorting::playStyle eq playStyle), + data, + ReplaceOptions().upsert(true) + ) + } + + override suspend fun onPlayerJoin(player: ServerPlayerEntity) { + val inventory = get(player.uuid) + cache[player.uuid] = inventory + player.ffaPlayer.inventorySorting = inventory + logger.info("Loaded inventory of ${player.gameProfile.name}") + } + + override suspend fun onPlayerLeave(player: ServerPlayerEntity) { + if (cache.containsKey(player.uuid)) { + save(cache[player.uuid]) + cache.remove(player.uuid) + logger.info("Saving inventory of ${player.gameProfile.name}") + } else { + logger.warn("Cache didn't contain any data about ${player.gameProfile.name} (${player.gameProfile.id}), not saving any data") + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/player/MongoPlayerProvider.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/player/MongoPlayerProvider.kt new file mode 100644 index 0000000..a0380bc --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/player/MongoPlayerProvider.kt @@ -0,0 +1,29 @@ +package gg.norisk.heroes.server.database.player + +import com.mongodb.client.model.ReplaceOptions +import de.hglabor.utils.database.extensions.findOne +import de.hglabor.utils.database.extensions.getOrCreateCollection +import de.hglabor.utils.database.kmongo.eq +import gg.norisk.heroes.common.player.FFAPlayer +import gg.norisk.heroes.common.database.player.AbstractPlayerProvider +import gg.norisk.heroes.server.database.MongoManager +import java.util.* + +class MongoPlayerProvider : AbstractPlayerProvider() { + private val collection = MongoManager.database.getOrCreateCollection("players") + + override suspend fun get(uuid: UUID): FFAPlayer { + val player = getCached(uuid) ?: collection.findOne(FFAPlayer::uuid eq uuid) ?: FFAPlayer(uuid) + cache[uuid] = player + return player + } + + override suspend fun save(data: FFAPlayer) { + cache[data.uuid] = data + collection.replaceOne( + FFAPlayer::uuid eq data.uuid, + data.copy(inventorySorting = null), + ReplaceOptions().upsert(true) + ) + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/player/PlayerProvider.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/player/PlayerProvider.kt new file mode 100644 index 0000000..c0deac1 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/server/database/player/PlayerProvider.kt @@ -0,0 +1,61 @@ +package gg.norisk.heroes.server.database.player + +import gg.norisk.datatracker.entity.registeredTypes +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.database.player.AbstractPlayerProvider +import gg.norisk.heroes.common.database.player.JsonPlayerProvider +import gg.norisk.heroes.common.player.FFAPlayer +import gg.norisk.heroes.server.database.MongoManager +import gg.norisk.heroes.server.database.inventory.InventoryProvider +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents +import net.silkmc.silk.core.task.mcCoroutineTask +import java.util.* + +object PlayerProvider : AbstractPlayerProvider() { + private var provider: AbstractPlayerProvider = JsonPlayerProvider() + + fun init() { + (registeredTypes as MutableMap).put( + FFAPlayer::class, + FFAPlayer.serializer() + ) + + ServerLifecycleEvents.SERVER_STARTING.register { + runCatching { + MongoManager.connect() + }.onSuccess { + provider = MongoPlayerProvider() + }.onFailure { + it.printStackTrace() + MongoManager.isConnected = false + provider = JsonPlayerProvider() + } + logger.info("Initialized provider: ${provider::class.simpleName}") + } + + ServerPlayConnectionEvents.JOIN.register(ServerPlayConnectionEvents.Join { handler, sender, server -> + mcCoroutineTask(sync = false, client = false) { + onPlayerJoin(handler.player) + } + }) + + ServerPlayConnectionEvents.DISCONNECT.register(ServerPlayConnectionEvents.Disconnect { handler, server -> + mcCoroutineTask(sync = false, client = false) { + onPlayerLeave(handler.player) + InventoryProvider.onPlayerLeave(handler.player) + } + }) + } + + override suspend fun save(player: FFAPlayer) { + provider.save(player) + InventoryProvider.save(player.inventorySorting) + } + + override suspend fun get(uuid: UUID): FFAPlayer { + val player = provider.get(uuid) + player.inventorySorting = InventoryProvider.get(uuid) + return player + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/server/hero/ability/AbilityManagerServer.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/server/hero/ability/AbilityManagerServer.kt new file mode 100644 index 0000000..66325f5 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/server/hero/ability/AbilityManagerServer.kt @@ -0,0 +1,260 @@ +package gg.norisk.heroes.server.hero.ability + +import gg.norisk.heroes.common.HeroesManager.logger +import gg.norisk.heroes.common.ffa.experience.ExperienceRegistry +import gg.norisk.heroes.common.ffa.experience.addXp +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.HeroManager +import gg.norisk.heroes.common.hero.ability.* +import gg.norisk.heroes.common.hero.ability.implementation.Ability +import gg.norisk.heroes.common.hero.ability.implementation.PressAbility +import gg.norisk.heroes.common.hero.ability.implementation.ToggleAbility +import gg.norisk.heroes.common.hero.getHero +import gg.norisk.heroes.common.networking.Networking +import gg.norisk.heroes.common.networking.Networking.c2sAbilityPacket +import gg.norisk.heroes.common.networking.Networking.c2sSkillProperty +import gg.norisk.heroes.common.networking.Networking.s2cAbilityPacket +import gg.norisk.heroes.common.player.ffaPlayer +import gg.norisk.heroes.server.database.player.PlayerProvider +import kotlinx.coroutines.* +import net.fabricmc.api.EnvType +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.server.MinecraftServer +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvents +import net.minecraft.text.Text +import net.minecraft.util.Colors +import net.silkmc.silk.core.server.players +import net.silkmc.silk.core.task.infiniteMcCoroutineTask +import net.silkmc.silk.core.task.mcCoroutineTask +import java.util.* +import kotlin.time.Duration.Companion.seconds + +object AbilityManagerServer : IAbilityManager { + private val abilitiesInUse = hashMapOf>() + private val abilityJobs: HashMap, Job>> = hashMapOf() + + override fun init() { + c2sAbilityPacket.receiveOnServer { packet, context -> + mcCoroutineTask(sync = true, client = false) { + handleIncomingAbility(packet, context.player) + } + } + + c2sSkillProperty.receiveOnServer { packet, context -> + mcCoroutineTask(sync = true, client = false) { + skillProperty(packet, context.player) + } + } + + initCooldownManager() + } + + private fun initCooldownManager() { + if (FabricLoader.getInstance().environmentType == EnvType.CLIENT) { + ServerTickEvents.END_SERVER_TICK.register(ServerTickEvents.EndTick { + sendCooldownToPlayers(it) + }) + } else { + ServerLifecycleEvents.SERVER_STARTED.register { + infiniteMcCoroutineTask(sync = false, client = false) { + sendCooldownToPlayers(it) + } + } + } + } + + private fun sendCooldownToPlayers(server: MinecraftServer) { + for (player in server.players) { + val hero = player.getHero() ?: continue + for (ability in hero.abilities.values) { + val cooldown = ability.getCooldown(player) ?: continue + cooldown.durationString = ability.getCooldownText(cooldown) + Networking.s2cCooldownPacket.send(cooldown, player) + } + } + } + + private suspend fun skillProperty(packet: SkillPropertyPacket, player: ServerPlayerEntity) { + val hero = HeroManager.getHero(packet.heroKey) ?: return + val ability = hero.abilities[packet.abilityKey] ?: return + val property = ability.getAllProperties().find { it.internalKey == packet.propertyKey } ?: return + val cachedPlayer = PlayerProvider.get(player.uuid) + + val oldLevel = property.getLevelInfo(player.uuid) + + val experienceToSpend = Math.min(500, cachedPlayer.xp) + val spentExperience = property.addExperience(player.uuid, experienceToSpend) + cachedPlayer.xp -= spentExperience + + val newLevel = property.getLevelInfo(player.uuid) + + logger.info("OldLevel $oldLevel CurrentLevel: $newLevel") + if (oldLevel.currentLevel != newLevel.currentLevel) { + //upgrade + if (newLevel.currentLevel == newLevel.maxLevel) { + player.playSoundToPlayer(SoundEvents.BLOCK_TRIAL_SPAWNER_OPEN_SHUTTER, SoundCategory.MASTER, 1f, 1f) + } else { + player.playSoundToPlayer(SoundEvents.ENTITY_PLAYER_LEVELUP, SoundCategory.MASTER, 0.5f, 1f) + } + } + + logger.info("Spent Experience $spentExperience $experienceToSpend ${cachedPlayer.xp}") + player.ffaPlayer = cachedPlayer + PlayerProvider.save(cachedPlayer) + } + + private fun handleIncomingAbility(packet: AbilityPacket<*>, player: ServerPlayerEntity) { + runCatching { + var ignoreCooldown = false + if (packet.playerUuid != player.uuid) return@runCatching + val ability = getAbilityFromAbilityUsePacket(packet) ?: return@runCatching + val description = packet.description + val abilityScope = AbilityScope(player) + if (!ability.hasUnlocked(player)) { + player.sendMessage(Text.translatable("heroes.ability.locked").withColor(Colors.RED)) + return@runCatching + } + + if (!ability.canUse(player)) { + player.sendMessage(Text.translatable("heroes.ability.cant_use").withColor(Colors.RED)) + return@runCatching + } + + val condition = ability.condition + if (condition != null && ability !is ToggleAbility) { + if (!condition.invoke(player)) { + player.sendMessage(Text.translatable("heroes.ability.condition.long.${ability.internalKey}")) + return@runCatching + } + } + when (ability) { + is PressAbility, + is Ability -> { + if (ability.handleCooldown(player)) return@runCatching + ability.onStart(player, abilityScope) + } + + is ToggleAbility -> { + val callbacks = when (description) { + is AbilityPacketDescription.Start -> { + if (condition != null) { + if (!condition.invoke(player)) { + player.sendMessage(Text.translatable("heroes.ability.condition.long.${(ability).internalKey}")) + return@runCatching + } + } + if (ability.handleCooldown(player)) return@runCatching + startAbilityAndForceEndAfterMaxDuration(player, abilityScope, ability) + ignoreCooldown = true + ability.onStart(player, abilityScope) + //ability.internalCallbacks.START + } + + is AbilityPacketDescription.Use -> { + ignoreCooldown = true + ability.onUse(player) + //ability.internalCallbacks.USE + } + + is AbilityPacketDescription.End -> { + if (abilityJobs[player.uuid]?.containsKey(ability) == false) return@runCatching + forceEndAbility(player, ability) + return + } + } + //callbacks?.handleServer?.invoke(abilityScope, player, packet.description) + } + } + if (abilityScope.broadcastPacket) { + s2cAbilityPacket.sendToAll(packet) + } else { + s2cAbilityPacket.send(packet, player) + } + if (!ignoreCooldown && abilityScope.applyCooldown) { + ability.addCooldown(player) + } + }.onFailure { + it.printStackTrace() + } + } + + private fun startAbilityAndForceEndAfterMaxDuration( + player: ServerPlayerEntity, + abilityScope: AbilityScope, + ability: ToggleAbility + ) { + val playerJobs = abilityJobs.computeIfAbsent(player.uuid) { hashMapOf() } + abilitiesInUse[player.uuid] = ability + playerJobs[ability] = mcCoroutineTask(sync = false, client = false) { + delay(ability.maxDurationProperty.getValue(player.uuid).seconds) + yield() + mcCoroutineTask(sync = true, client = false) { + forceEndAbility(player, ability, abilityScope) + } + } + } + + fun clear(player: PlayerEntity) { + abilityJobs[player.uuid]?.forEach { (ability, job) -> + job.cancel() + } + abilitiesInUse.remove(player.uuid) + } + + fun forceEndAbility( + player: ServerPlayerEntity, + ability: ToggleAbility, + abilityScope: AbilityScope = AbilityScope(player) + ) { + val job = abilityJobs[player.uuid]?.get(ability) ?: return + job.cancel() + abilitiesInUse.remove(player.uuid) + abilityJobs[player.uuid]?.remove(ability) + val description = AbilityPacketDescription.End + val packet = AbilityPacket(player.uuid, ability.hero.internalKey, ability.internalKey, description) + s2cAbilityPacket.sendToAll(packet) + val abilityEndInformation = ToggleAbility.AbilityEndInformation(true) + ability.onEnd(player, abilityEndInformation) + if (abilityEndInformation.applyCooldown) { + ability.addCooldown(player) + } + } + + private fun getAbilityFromAbilityUsePacket(abilityPacket: AbilityPacket): AbstractAbility<*>? { + val hero = HeroManager.getHero(abilityPacket.heroKey) ?: return null + return hero.abilities[abilityPacket.abilityKey] + } + + override fun useAbility( + player: PlayerEntity, + hero: Hero, + ability: AbstractAbility<*>, + description: AbilityPacketDescription.Use + ): Boolean { + throw IllegalCallerException("AbilityManager.useAbility(AbstractAbility<*>, AbilityPacketDescription.Use) must not be used on the server side") + } + + override fun useAbility( + player: PlayerEntity, + ability: AbstractAbility<*>, + description: AbilityPacketDescription.Use + ) { + val hero = player.getHero() ?: return + if (hero != ability.hero) return + val packet = AbilityPacket(player.uuid, hero.internalKey, ability.internalKey, description) + mcCoroutineTask(sync = true, client = false) { + handleIncomingAbility(packet, player as? ServerPlayerEntity ?: return@mcCoroutineTask) + } + } + + override fun registerAbility(ability: AbstractAbility<*>) {} + + override fun isUsingAbility(player: PlayerEntity, ability: AbstractAbility<*>): Boolean { + return abilitiesInUse[player.uuid]?.internalKey == ability.internalKey + } +} diff --git a/hero-api/src/main/resources/assets/hero-api/icon.png b/hero-api/src/main/resources/assets/hero-api/icon.png new file mode 100644 index 0000000..d2871cf Binary files /dev/null and b/hero-api/src/main/resources/assets/hero-api/icon.png differ diff --git a/hero-api/src/main/resources/assets/hero-api/lang/de_de.json b/hero-api/src/main/resources/assets/hero-api/lang/de_de.json new file mode 100644 index 0000000..7965898 --- /dev/null +++ b/hero-api/src/main/resources/assets/hero-api/lang/de_de.json @@ -0,0 +1,101 @@ +{ + "key.heroes.first": "Erste Fähigkeit", + "key.heroes.second": "Zweite Fähigkeit", + "key.heroes.third": "Dritte Fähigkeit", + "key.heroes.fourth": "Vierte Fähigkeit", + "key.heroes.fifth": "Fünfte Fähigkeit", + "category.heroes.abilities": "Fähigkeiten", + "heroes.announce_kill_streak": "%s hat eine %s Kill-Serie.", + "heroes.end_kill_streak": "§4%s wurde mit einer %s Kill-Serie getötet.", + "heroes.ability.locked": "Du musst diese Fähigkeit zuerst freischalten.", + "heroes.ability.cant_use": "You cannot use this ability right now.", + "heroes.ability.healing.unlock_condition": "Du musst den Cooldown-Pfad\nvon Wasserbändigen vollständig aufrüsten, um es freizuschalten.", + "heroes.ability.water_circle.unlock_condition": "Du musst den Cooldown-Pfad\nvon Wasserbändigen vollständig aufrüsten, um es freizuschalten.", + "heroes.ability.water_circle.custom_activation": "Drehe dich 360°", + "heroes.ability.condition.short.water_forming": "Wasser", + "heroes.ability.condition.short.water_pillar": "Im Wasser", + "heroes.ability.condition.short.healing": "Wasser", + "heroes.ability.condition.short.earth_trap": "Sneak", + + "heroes.property.cooldown": "Cooldown", + "heroes.property.cooldown.description": "Die Zeit, die du warten musst,\nbis du die Fähigkeit erneut einsetzen kannst.", + "heroes.property.max_duration": "Maximale Dauer", + "heroes.property.max_duration.description": "Die maximale Zeit,\nwie lange du die Fähigkeit halten kannst.", + "heroes.property.max_size": "Maximale Größe", + "heroes.property.max_size.description": "Die maximale Größe,\ndie eine Fähigkeit erreichen kann.", + "heroes.property.speed": "Geschwindigkeit", + "heroes.property.speed.description": "Wie schnell du dich\nbei der Verwendung der Fähigkeit bewegst.", + "heroes.property.step_height": "Schritt Höhe", + "heroes.property.step_height.description": "Die maximale Höhe\nvon Stufen, die du erklimmen kannst.", + "heroes.property.radius": "Radius", + "heroes.property.radius.description": "Der Wirkungsradius\nder Fähigkeit.", + "heroes.property.height": "Höhe", + "heroes.property.height.description": "Die Höhe, die von\nder Fähigkeit betroffen ist.", + "heroes.property.armor": "Rüstung", + "heroes.property.armor.description": "Die Menge an\nSchutz, die geboten wird.", + "heroes.property.knockback": "Rückstoß", + "heroes.property.knockback.description": "Die Kraft, mit der\ndu Gegner wegstößt.", + "heroes.property.damage": "Schaden", + "heroes.property.damage.description": "Die Menge an\nSchaden, die verursacht wird.", + "heroes.property.water_pillar_distance": "Distanz", + "heroes.property.water_pillar_distance.description": "Die Distanz, die du dich bewegen kannst,\nbis deine Fähigkeit endet.", + "heroes.property.water_pillar_start_boost": "Startschub", + "heroes.property.water_pillar_start_boost.description": "Wie stark du nach oben\nunterstützt wirst, wenn du die Fähigkeit verwendest.", + "heroes.property.water_forming_max_blocks": "Maximaler Block", + "heroes.property.water_forming_max_blocks.description": "Die maximale Menge\nan Wasser, die du in Eis verwandeln kannst.", + "heroes.property.use": "Verwendungen", + "heroes.property.use.description": "Die Anzahl der Verwendungen,\nbis der Cooldown beginnt.", + "heroes.property.regeneration": "Regeneration", + "heroes.property.regeneration.description": "Der Verstärker des\nRegenerationseffekts.", + "heroes.property.max_duration_lasts": "Maximale Dauer", + "heroes.property.max_duration_lasts.description": "Die Dauer\ndes Effekts.", + "heroes.property.water_circle_sphere": "Kugel", + "heroes.property.water_circle_sphere.description": "Die Anzahl der Kugeln,\ndie du im Wasserkreis haben kannst.", + "heroes.property.water_circle_fall_distance": "Fallhöhe", + "heroes.property.water_circle_fall_distance.description": "Die maximale Höhe,\nvon der du Fallschaden blockieren kannst.", + "heroes.property.spiritual_projection_max_distance": "Maximale Distanz", + "heroes.property.spiritual_projection_max_distance.description": "Die maximale Anzahl von Blöcken,\ndie du dich von deinem Körper entfernen kannst,\nbis du zurückgeschickt wirst.", + "heroes.property.tornado_increase_rate": "Wachstumsrate", + "heroes.property.tornado_increase_rate.description": "Die Rate, mit der\ndein Tornado wachsen kann.", + "heroes.property.tornado_decrease_rate": "Schrumpfrate", + "heroes.property.tornado_decrease_rate.description": "Die Rate, mit der\nder Tornado sich über Zeit verkleinert.", + "heroes.property.slowness": "Langsamkeit", + "heroes.property.slowness.description": "Wie langsam die Fähigkeit ist.", + + "hero.aang.ability.air_scooter.description": "Erzeugt einen Luftball, auf dem du reiten kannst.", + "hero.aang.ability.tornado.description": "Erzeugt einen Tornado, indem du die Maus drehst und nearby Blöcke und Entitäten wegschleudert.", + "hero.aang.ability.air_ball.description": "Durch kreisförmige Bewegungen mit der Maus erzeugst du einen Luftball, den du durch Scrollen skalieren kannst.", + "hero.aang.ability.levitation.description": "Während du die Fähigkeit hältst, kannst du gleiten.", + "hero.aang.ability.spiritual_projection.description": "Erzeugt ein Doppelgänger, mit dem du deinen Körper verlässt und die Landschaft erkunden kannst.", + "heroes.ability.tornado.unlock_condition": "Du musst den Cooldown- und Maximalgrößen-Pfad der Luftball-Fähigkeit gemeistert haben.", + "heroes.ability.spiritual_projection.unlock_condition": "Du musst den Cooldown- und maximalen Dauer-Pfad der Levitation-Fähigkeit gemeistert haben.", + + "hero.katara.ability.healing.description": "Erzeugt heilendes Wasser, das dir oder anderen Entitäten Gesundheit wiederherstellt.", + "hero.katara.ability.water_pillar.description": "Du musst im Wasser sein, um eine Wassersäule zu erschaffen, die du nach Belieben kontrollieren kannst.", + "hero.katara.ability.water_bending.description": "Du kannst Wasser bändigen, indem du Wasser oder Pflanzen anvisierst.", + "hero.katara.ability.water_forming.description": "Du musst Wasser anvisieren und die Fähigkeit gedrückt halten, um eine Eisbrücke zu erschaffen.", + "hero.katara.ability.water_circle.description": "Du musst eine 360°-Drehung machen, während du Wasser bändigst, um ein Schutzschild zu erstellen, das dich vor Fallschaden, Projektilen und Feuer schützt.", + + "hero.toph.ability.earth_surf.description": "Du kannst auf dem Boden surfen.", + "hero.toph.ability.earth_column.description": "Du kannst eine Erdsäule bändigen.", + "hero.toph.ability.earth_push.description": "Du kannst einen Gesteinsblock herausschlagen und ihn in eine Richtung kicken.", + "hero.toph.ability.earth_armor.description": "Du kannst umliegende Gesteinsblöcke zu dir bändigen und sie als Rüstung nutzen.", + "hero.toph.ability.earth_trap.description": "Du musst auf Erde stehen, einen Gegner anvisieren und ihn dann in den Boden sinken lassen.", + "hero.toph.ability.seismic_sense.description": "Alles wird schwarz, damit du die umliegende Umgebung scannen und versteckte Entitäten aufdecken kannst.", + "heroes.ability.earth_trap.unlock_condition": "Du musst den Cooldown-Pfad von Erdstoß gemeistert haben.", + "heroes.ability.seismic_sense.unlock_condition": "Du musst den Cooldown-Pfad von Erdstoß gemeistert haben.", + + "ffa.died": "§cDu bist gestorben", + "ffa.mechanic.bounty.claimed": "§f%s §ehat das Kopfgeld von §a%s §efür §f%s §eerhalten.", + "ffa.mechanic.bounty.placed": "§f%s §ehat ein Kopfgeld von §a%s §eauf §f%s §eerteilt.", + "ffa.mechanic.bounty.info": "§f%s §ehat ein Kopfgeld von §a%s§e.", + "ffa.mechanic.bounty.not_enough_xp": "§cDu hast zu wenig XP um dieses Kopfgeld zu erteilen.", + "ffa.mechanic.killstreak.lost": "§f%s §ehat seine Killstreak von §c%s §everloren.", + "ffa.mechanic.lootdrop.found_xp": "§eDu hast §a%s §eXP erhalten.", + "ffa.mechanic.kit.editor.enter": "§fDu hast den §cKit-Editor §ebetreten.", + "ffa.mechanic.kit.editor.save": "§aDein Kit wurde §2gespeichert§a.", + "ffa.mechanic.kit.editor.left": "§fDu hast den §cKit-Editor §everlassen.", + "ffa.mechanic.kit.editor.inventory_instruction": "§fHier kannst du dein Inventar nach Belieben anordnen, damit es beim nächsten Mal gespeichert wird.", + + "toph.ability.condition.spiritual_projection": "You cannot use this ability while in spirit mode." +} diff --git a/hero-api/src/main/resources/assets/hero-api/lang/en_us.json b/hero-api/src/main/resources/assets/hero-api/lang/en_us.json new file mode 100644 index 0000000..04d20aa --- /dev/null +++ b/hero-api/src/main/resources/assets/hero-api/lang/en_us.json @@ -0,0 +1,108 @@ +{ + "key.heroes.first": "First Ability", + "key.heroes.second": "Second Ability", + "key.heroes.third": "Third Ability", + "key.heroes.fourth": "Fourth Ability", + "key.heroes.fifth": "Fifth Ability", + "category.heroes.abilities": "Abilities", + "heroes.announce_kill_streak": "%s is on a %s killstreak", + "heroes.end_kill_streak": "§4%s has been killed on a %s killstreak.", + "heroes.ability.locked": "You must unlock this ability first.", + "heroes.ability.cant_use": "You cannot use this ability right now.", + "heroes.ability.healing.unlock_condition": "You need to fully upgrade the cooldown path\nof Water Bending to unlock it.", + "heroes.ability.water_circle.unlock_condition": "You need to fully upgrade the cooldown path\nof Water Bending to unlock it.", + "heroes.ability.water_circle.custom_activation": "Spin 360°", + "heroes.ability.condition.short.water_forming": "Water", + "heroes.ability.condition.short.water_pillar": "In Water", + "heroes.ability.condition.long.water_pillar": "You must be standing in water.", + "heroes.ability.condition.short.healing": "Water", + "heroes.ability.condition.short.ice_shards": "Water", + "heroes.ability.condition.long.ice_shards": "You must bend water first.", + "heroes.ability.condition.short.earth_trap": "Sneak", + + "heroes.property.cooldown": "Cooldown", + "heroes.property.cooldown.description": "The time you must wait\nbefore you can use the ability again.", + "heroes.property.max_duration": "Max Duration", + "heroes.property.max_duration.description": "The maximum time\nyou can hold the ability.", + "heroes.property.max_size": "Max Size", + "heroes.property.max_size.description": "The maximum size\nan ability can reach.", + "heroes.property.speed": "Speed", + "heroes.property.speed.description": "How fast you move\nwhile using the ability.", + "heroes.property.step_height": "Step Height", + "heroes.property.step_height.description": "The maximum height\nof steps you can climb.", + "heroes.property.radius": "Radius", + "heroes.property.radius.description": "The radius of effect\nfor this ability.", + "heroes.property.height": "Height", + "heroes.property.height.description": "The height affected\nby this ability.", + "heroes.property.armor": "Armor", + "heroes.property.armor.description": "The amount of\nprotection provided.", + "heroes.property.knockback": "Knockback", + "heroes.property.knockback.description": "The force with which\nyou push enemies away.", + "heroes.property.damage": "Damage", + "heroes.property.damage.description": "The amount of\ndamage dealt.", + "heroes.property.water_pillar_distance": "Distance", + "heroes.property.water_pillar_distance.description": "The distance you can move\nbefore your ability ends.", + "heroes.property.water_pillar_start_boost": "Start Boost", + "heroes.property.water_pillar_start_boost.description": "How much you are boosted\nupwards when you use the ability.", + "heroes.property.water_forming_max_blocks": "Max Blocks", + "heroes.property.water_forming_max_blocks.description": "The maximum amount\nof water you can turn into ice.", + "heroes.property.use": "Uses", + "heroes.property.use.description": "The number of uses\nbefore the cooldown starts.", + "heroes.property.regeneration": "Regeneration", + "heroes.property.regeneration.description": "The amplifier of\nthe regeneration effect.", + "heroes.property.max_duration_lasts": "Max Duration", + "heroes.property.max_duration_lasts.description": "The duration\nof the effect.", + "heroes.property.water_circle_sphere": "Sphere", + "heroes.property.water_circle_sphere.description": "The number of spheres\nyou can have in the water circle.", + "heroes.property.water_circle_fall_distance": "Fall Distance", + "heroes.property.water_circle_fall_distance.description": "The maximum height\nfrom which you can block fall damage.", + "heroes.property.spiritual_projection_max_distance": "Max Distance", + "heroes.property.spiritual_projection_max_distance.description": "The maximum number of blocks\nyou can move away from your body\nbefore being sent back.", + "heroes.property.tornado_increase_rate": "Increase Rate", + "heroes.property.tornado_increase_rate.description": "The rate at which\nyour tornado can grow.", + "heroes.property.tornado_decrease_rate": "Decrease Rate", + "heroes.property.tornado_decrease_rate.description": "The rate at which\nyour tornado shrinks\nover time.", + "heroes.property.slowness": "Slowness", + "heroes.property.slowness.description": "How slow the ability is.", + "heroes.property.earth_column_boost": "Boost", + "heroes.property.earth_column_boost.description": "How high entities\nare launched into the air.", + + "hero.aang.ability.air_scooter.description": "Creates an airball that you can ride on.", + "hero.aang.ability.tornado.description": "Creates a tornado by spinning the mouse, launching nearby blocks and entities away.", + "hero.aang.ability.air_ball.description": "By making circular movements with the mouse, you create an air ball that can be scaled by scrolling.", + "hero.aang.ability.levitation.description": "While holding the ability, you can glide.", + "hero.aang.ability.spiritual_projection.description": "Creates a double, allowing you to leave your body and explore the landscape.", + "heroes.ability.tornado.unlock_condition": "You must have mastered the cooldown and max size paths of the AirBall ability.", + "heroes.ability.spiritual_projection.unlock_condition": "You must have mastered the cooldown and max duration paths of the Levitation ability.", + + "hero.katara.ability.healing.description": "Creates healing water that restores health to you or other entities.", + "hero.katara.ability.water_pillar.description": "You must be in water to create a water pillar that you can control at will.", + "hero.katara.ability.water_bending.description": "You can bend water by targeting water or plants.", + "hero.katara.ability.water_forming.description": "You must target water and hold the ability to create an ice bridge.", + "hero.katara.ability.ice_shards.description": "You can shoot ice shards.", + "hero.katara.ability.water_circle.description": "You must perform a 360 while bending water to create a shield that protects you from fall damage, projectiles, and fire.", + + "hero.toph.ability.earth_surf.description": "You can surf on the ground.", + "hero.toph.ability.earth_column.description": "You can bend an earth column.", + "hero.toph.ability.earth_push.description": "You can strike a rock block and kick it in a direction.", + "hero.toph.ability.earth_armor.description": "You can bend nearby rock blocks to you and use them as armor.", + "hero.toph.ability.earth_trap.description": "You must be standing on earth, target an opponent, and then you can make them sink into the ground.", + "hero.toph.ability.seismic_sense.description": "Everything goes black so you can scan the surrounding environment and reveal hidden entities.", + "heroes.ability.earth_trap.unlock_condition": "You must have mastered the cooldown path of Earth Push.", + "heroes.ability.seismic_sense.unlock_condition": "You must have mastered the cooldown path of Earth Push.", + + "ffa.died": "§cYou died.", + "ffa.mechanic.bounty.claimed": "§f%s §ehas claimed the bounty of §a%s §efor §f%s§e.", + "ffa.mechanic.bounty.placed": "§f%s §ehas placed a bounty of §a%s §eon §f%s§e.", + "ffa.mechanic.bounty.info": "§f%s §ehas a bounty of §a%s§e.", + "ffa.mechanic.bounty.not_enough_xp": "§cYou don't have enough XP to place this bounty.", + "ffa.mechanic.killstreak.lost": "§f%s §ehas lost their killstreak of §c%s§e.", + "ffa.mechanic.lootdrop.found_xp": "§eYou received §a%s §exp.", + "ffa.mechanic.kit.editor.enter": "§eYou have entered the §cKit §eEditor.", + "ffa.mechanic.kit.editor.save": "§aYour kit has been §2saved§a.", + "ffa.mechanic.kit.editor.left": "§eYou have left the §cKit §eEditor.", + "ffa.mechanic.kit.editor.inventory_instruction": "§fHere you can arrange your inventory as you like, so it will be saved for next time.", + + "toph.ability.condition.spiritual_projection": "You cannot use this ability while in spirit mode." +} + diff --git a/hero-api/src/main/resources/assets/hero-api/nine_patch_textures/unlocked.json b/hero-api/src/main/resources/assets/hero-api/nine_patch_textures/unlocked.json new file mode 100644 index 0000000..3ec59db --- /dev/null +++ b/hero-api/src/main/resources/assets/hero-api/nine_patch_textures/unlocked.json @@ -0,0 +1,10 @@ +{ + "texture": "hero-api:textures/gui/unlocked_panel.png", + "texture_width": 16, + "texture_height": 16, + "repeat": false, + "patch_size": { + "width": 5, + "height": 5 + } +} \ No newline at end of file diff --git a/hero-api/src/main/resources/assets/hero-api/shaders/core/speedlines.fsh b/hero-api/src/main/resources/assets/hero-api/shaders/core/speedlines.fsh new file mode 100644 index 0000000..e738b94 --- /dev/null +++ b/hero-api/src/main/resources/assets/hero-api/shaders/core/speedlines.fsh @@ -0,0 +1,61 @@ +#version 330 + +uniform float Radius; +uniform float Edge; +uniform float GameTime; +uniform vec2 ScreenSize; + +out vec4 fragColor; + +const vec3 COLOR = vec3(1, 1, 1); + +vec3 random3(vec3 c) { + float j = 4096.0*sin(dot(c,vec3(17.0, 59.4, 15.0))); + vec3 r; + r.z = fract(512.0*j); + j *= .125; + r.x = fract(512.0*j); + j *= .125; + r.y = fract(512.0*j); + return r-0.5; +} + +float simplex3d(vec3 p) { + vec3 s = floor(p + dot(p, vec3(0.3333333))); + vec3 x = p - s + dot(s, vec3(0.1666667)); + vec3 e = step(vec3(0.0), x - x.yzx); + vec3 i1 = e*(1.0 - e.zxy); + vec3 i2 = 1.0 - e.zxy*(1.0 - e); + vec3 x1 = x - i1 + 0.1666667; + vec3 x2 = x - i2 + 2.0*0.1666667; + vec3 x3 = x - 1.0 + 3.0*0.1666667; + vec4 w, d; + w.x = dot(x, x); + w.y = dot(x1, x1); + w.z = dot(x2, x2); + w.w = dot(x3, x3); + w = max(0.6 - w, 0.0); + d.x = dot(random3(s), x); + d.y = dot(random3(s + i1), x1); + d.z = dot(random3(s + i2), x2); + d.w = dot(random3(s + 1.0), x3); + w *= w; + w *= w; + d *= w; + return dot(d, vec4(52.0)); +} + +void main() +{ + float time = (GameTime * 1200) * 2.; + float scale = 50.0; + vec2 uv = (gl_FragCoord.xy*2. - ScreenSize.xy) / ScreenSize.y * 0.5; + vec2 p = vec2(0.5*ScreenSize.x/ScreenSize.y, 0.5) + normalize(uv) * min(length(uv), 0.05); + vec3 p3 = scale*0.25*vec3(p.xy, 0) + vec3(0, 0, time*0.025); + float noise = simplex3d(p3 * 32.0) * 0.5 + 0.5; + float dist = abs(clamp(length(uv)/Radius, 0.0, 1.0)*noise*2.-1.); + float stepped = smoothstep(Edge-.5,Edge+.5, noise * (1.0-pow(dist, 4.0))); + float final = smoothstep(Edge - 0.05, Edge + 0.05, noise*stepped); + + fragColor = vec4(COLOR,final); +} \ No newline at end of file diff --git a/hero-api/src/main/resources/assets/hero-api/shaders/core/speedlines.json b/hero-api/src/main/resources/assets/hero-api/shaders/core/speedlines.json new file mode 100644 index 0000000..f14b628 --- /dev/null +++ b/hero-api/src/main/resources/assets/hero-api/shaders/core/speedlines.json @@ -0,0 +1,19 @@ +{ + "blend": { + "func": "add", + "srcrgb": "srcalpha", + "dstrgb": "1-srcalpha" + }, + "vertex": "position", + "fragment": "hero-api:speedlines", + "attributes": [ "Position" ], + "samplers": [], + "uniforms": [ + { "name": "GameTime", "type": "float", "count": 1, "values": [ 0.0 ] }, + { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ScreenSize", "type": "float", "count": 2, "values": [ 0.0,0.0 ] }, + { "name": "Radius", "type": "float", "count": 1, "values": [ 12.0 ] }, + { "name": "Edge", "type": "float", "count": 1, "values": [ 0.5 ] } + ] +} diff --git a/hero-api/src/main/resources/assets/hero-api/sounds.json b/hero-api/src/main/resources/assets/hero-api/sounds.json new file mode 100644 index 0000000..bcef1b8 --- /dev/null +++ b/hero-api/src/main/resources/assets/hero-api/sounds.json @@ -0,0 +1,7 @@ +{ + "flying": { + "sounds": [ + "hero-api:flying" + ] + } +} diff --git a/hero-api/src/main/resources/assets/hero-api/sounds/flying.ogg b/hero-api/src/main/resources/assets/hero-api/sounds/flying.ogg new file mode 100644 index 0000000..cf2bf71 Binary files /dev/null and b/hero-api/src/main/resources/assets/hero-api/sounds/flying.ogg differ diff --git a/hero-api/src/main/resources/assets/hero-api/textures/gui/dark_panel.png b/hero-api/src/main/resources/assets/hero-api/textures/gui/dark_panel.png new file mode 100644 index 0000000..a6d99be Binary files /dev/null and b/hero-api/src/main/resources/assets/hero-api/textures/gui/dark_panel.png differ diff --git a/hero-api/src/main/resources/assets/hero-api/textures/gui/goal_frame_dark.png b/hero-api/src/main/resources/assets/hero-api/textures/gui/goal_frame_dark.png new file mode 100644 index 0000000..67efa2b Binary files /dev/null and b/hero-api/src/main/resources/assets/hero-api/textures/gui/goal_frame_dark.png differ diff --git a/hero-api/src/main/resources/assets/hero-api/textures/gui/lock_icon.png b/hero-api/src/main/resources/assets/hero-api/textures/gui/lock_icon.png new file mode 100644 index 0000000..a35ee4c Binary files /dev/null and b/hero-api/src/main/resources/assets/hero-api/textures/gui/lock_icon.png differ diff --git a/hero-api/src/main/resources/assets/hero-api/textures/gui/root_panel.png b/hero-api/src/main/resources/assets/hero-api/textures/gui/root_panel.png new file mode 100644 index 0000000..56189c5 Binary files /dev/null and b/hero-api/src/main/resources/assets/hero-api/textures/gui/root_panel.png differ diff --git a/hero-api/src/main/resources/assets/hero-api/textures/gui/shadow.png b/hero-api/src/main/resources/assets/hero-api/textures/gui/shadow.png new file mode 100644 index 0000000..5cc42ed Binary files /dev/null and b/hero-api/src/main/resources/assets/hero-api/textures/gui/shadow.png differ diff --git a/hero-api/src/main/resources/assets/hero-api/textures/gui/unlocked_panel.png b/hero-api/src/main/resources/assets/hero-api/textures/gui/unlocked_panel.png new file mode 100644 index 0000000..a612583 Binary files /dev/null and b/hero-api/src/main/resources/assets/hero-api/textures/gui/unlocked_panel.png differ diff --git a/hero-api/src/main/resources/assets/hero-api/textures/placeholder.png b/hero-api/src/main/resources/assets/hero-api/textures/placeholder.png new file mode 100644 index 0000000..778a891 Binary files /dev/null and b/hero-api/src/main/resources/assets/hero-api/textures/placeholder.png differ diff --git a/hero-api/src/main/resources/fabric.mod.json b/hero-api/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..263a056 --- /dev/null +++ b/hero-api/src/main/resources/fabric.mod.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "name": "Hero-Api", + "id": "hero-api", + "version": "${version}", + "description": "Hero Api", + "authors": [ + "NoRiskk", + "Freshkenny", + "BestAuto" + ], + "icon": "assets/hero-api/icon.png", + "license": "ARR", + "environment": "*", + "entrypoints": { + "main": [ + { + "adapter": "kotlin", + "value": "gg.norisk.heroes.common.HeroesManager" + } + ], + "client": [ + { + "adapter": "kotlin", + "value": "gg.norisk.heroes.client.HeroesManagerClient" + } + ] + }, + "accessWidener": "hero-api.accesswidener", + "mixins": [ + "hero-api.mixins.json" + ], + "depends": { + } +} diff --git a/hero-api/src/main/resources/hero-api.accesswidener b/hero-api/src/main/resources/hero-api.accesswidener new file mode 100644 index 0000000..e4a231f --- /dev/null +++ b/hero-api/src/main/resources/hero-api.accesswidener @@ -0,0 +1,4 @@ +accessWidener v1 named +accessible field net/minecraft/entity/data/DataTracker MAX_DATA_VALUE_ID I +accessible method net/minecraft/client/render/Camera moveBy (FFF)V +accessible method net/minecraft/util/WorldSavePath (Ljava/lang/String;)V \ No newline at end of file diff --git a/hero-api/src/main/resources/hero-api.mixins.json b/hero-api/src/main/resources/hero-api.mixins.json new file mode 100644 index 0000000..09848ba --- /dev/null +++ b/hero-api/src/main/resources/hero-api.mixins.json @@ -0,0 +1,30 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "gg.norisk.heroes.common.mixin", + "compatibilityLevel": "JAVA_17", + "injectors": { + "defaultRequire": 1 + }, + "mixins": [ + "EntityMixin", + "FeatureFlagsMixin", + "LivingEntityMixin", + "MinecraftServerAccessor", + "MinecraftServerMixin", + "PlayerEntityMixin", + "ResourcePackManagerMixin", + "WorldAccessor" + ], + "client": [ + "client.CameraMixin", + "client.ClientPlayerEntityMixin", + "client.EntityRenderDispatcherMixin", + "client.GameOptionsMixin", + "client.GameRendererMixin", + "client.MouseMixin", + "client.PlayerEntityRendererMixin", + "client.PlayerListHudMixin", + "client.compat.DefaultChunkRendererMixin" + ] +} diff --git a/hero-api/src/main/resources/resourcepacks/heroes/data/hero-api/dimension/kit-editor.json b/hero-api/src/main/resources/resourcepacks/heroes/data/hero-api/dimension/kit-editor.json new file mode 100644 index 0000000..c64b50c --- /dev/null +++ b/hero-api/src/main/resources/resourcepacks/heroes/data/hero-api/dimension/kit-editor.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:overworld", + "generator": { + "type": "minecraft:flat", + "settings": { + "biome": "minecraft:forest", + "lakes": false, + "features": false, + "layers": [ + { + "height": 153, + "block": "minecraft:air" + }, + { + "height": 1, + "block": "minecraft:barrier" + } + ] + } + } +} \ No newline at end of file diff --git a/hero-api/src/main/resources/resourcepacks/heroes/pack.mcmeta b/hero-api/src/main/resources/resourcepacks/heroes/pack.mcmeta new file mode 100644 index 0000000..acd0251 --- /dev/null +++ b/hero-api/src/main/resources/resourcepacks/heroes/pack.mcmeta @@ -0,0 +1,11 @@ +{ + "pack": { + "pack_format": 48, + "description": "Heroes Datapack" + }, + "features": { + "enabled": [ + "hero-api:heroes" + ] + } +} \ No newline at end of file diff --git a/katara/build.gradle.kts b/katara/build.gradle.kts new file mode 100644 index 0000000..14de688 --- /dev/null +++ b/katara/build.gradle.kts @@ -0,0 +1,16 @@ +dependencies { + implementation(project(":hero-api", configuration = "namedElements")) + implementation(project(":datatracker", configuration = "namedElements")) + + modApi(libs.bundles.fabric) + modApi(libs.bundles.silk) + modApi(libs.bundles.performance) + modApi(libs.owolib) + modApi(libs.geckolib) + modApi(libs.emoteLib) +} + +loom { + accessWidenerPath.set(file("src/main/resources/katara.accesswidener")) +} + diff --git a/katara/src/main/java/gg/norisk/heroes/katara/mixin/BufferBuilderStorageMixin.java b/katara/src/main/java/gg/norisk/heroes/katara/mixin/BufferBuilderStorageMixin.java new file mode 100644 index 0000000..0a1d944 --- /dev/null +++ b/katara/src/main/java/gg/norisk/heroes/katara/mixin/BufferBuilderStorageMixin.java @@ -0,0 +1,24 @@ +package gg.norisk.heroes.katara.mixin; + +import gg.norisk.heroes.katara.client.render.HealingWaterFeatureRenderer; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import net.minecraft.client.render.BufferBuilderStorage; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.util.BufferAllocator; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(BufferBuilderStorage.class) +public abstract class BufferBuilderStorageMixin { + @Shadow + private static void assignBufferBuilder(Object2ObjectLinkedOpenHashMap object2ObjectLinkedOpenHashMap, RenderLayer renderLayer) { + } + + @Inject(method = "method_54639", at = @At("TAIL")) + private void injected(Object2ObjectLinkedOpenHashMap object2ObjectLinkedOpenHashMap, CallbackInfo ci) { + assignBufferBuilder(object2ObjectLinkedOpenHashMap, HealingWaterFeatureRenderer.Companion.getLAYER()); + } +} diff --git a/katara/src/main/java/gg/norisk/heroes/katara/mixin/EntityMixin.java b/katara/src/main/java/gg/norisk/heroes/katara/mixin/EntityMixin.java new file mode 100644 index 0000000..c1f57e8 --- /dev/null +++ b/katara/src/main/java/gg/norisk/heroes/katara/mixin/EntityMixin.java @@ -0,0 +1,59 @@ +package gg.norisk.heroes.katara.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import gg.norisk.heroes.katara.entity.IKataraEntity; +import gg.norisk.heroes.katara.ability.WaterCircleAbilityV2; +import gg.norisk.heroes.katara.utils.EntityCircleTracker; +import gg.norisk.heroes.katara.utils.EntitySpinTracker; +import kotlinx.coroutines.Job; +import net.minecraft.entity.Entity; +import net.minecraft.entity.ProjectileDeflection; +import net.minecraft.entity.player.PlayerEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Entity.class) +public abstract class EntityMixin implements IKataraEntity { + @Unique + private Job waterHealingJob; + @Unique + private EntitySpinTracker entitySpinTracker = new EntitySpinTracker(); + @Unique + private EntityCircleTracker entityCircleTracker = new EntityCircleTracker(); + + @ModifyReturnValue( + method = "getProjectileDeflection", + at = @At("RETURN") + ) + private ProjectileDeflection katara$projectileReflection(ProjectileDeflection original) { + if ((Object) this instanceof PlayerEntity player) { + if (WaterCircleAbilityV2.INSTANCE.breakWaterCirclePiece(player)) { + return ProjectileDeflection.REDIRECTED; + } + } + return original; + } + + @Override + public @Nullable Job getKatara_waterHealingJob() { + return waterHealingJob; + } + + @Override + public void setKatara_waterHealingJob(@Nullable Job job) { + waterHealingJob = job; + } + + @Override + public @NotNull EntitySpinTracker getKatara_entitySpinTracker() { + return entitySpinTracker; + } + + @Override + public @NotNull EntityCircleTracker getKatara_entityCircleTracker() { + return entityCircleTracker; + } +} diff --git a/katara/src/main/java/gg/norisk/heroes/katara/mixin/FlowableFluidMixin.java b/katara/src/main/java/gg/norisk/heroes/katara/mixin/FlowableFluidMixin.java new file mode 100644 index 0000000..6b36d3d --- /dev/null +++ b/katara/src/main/java/gg/norisk/heroes/katara/mixin/FlowableFluidMixin.java @@ -0,0 +1,30 @@ +package gg.norisk.heroes.katara.mixin; + +import gg.norisk.heroes.katara.event.FluidEvents; +import net.minecraft.fluid.FlowableFluid; +import net.minecraft.fluid.Fluid; +import net.minecraft.fluid.FluidState; +import net.minecraft.state.StateManager; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(FlowableFluid.class) +public abstract class FlowableFluidMixin { + @Inject(method = "onScheduledTick", at = @At("HEAD"), cancellable = true) + private void onScheduledTickInjection(World world, BlockPos blockPos, FluidState fluidState, CallbackInfo ci) { + var event = new FluidEvents.FluidEvent(world, blockPos, fluidState); + FluidEvents.INSTANCE.getFluidTickEvent().invoke(event); + if (event.isCancelled().get()) { + ci.cancel(); + } + } + + @Inject(method = "appendProperties", at = @At("TAIL")) + private void appendStillProperties(StateManager.Builder builder, CallbackInfo ci) { + //builder.add(FluidEvents.INSTANCE.getStatic()); + } +} diff --git a/katara/src/main/java/gg/norisk/heroes/katara/mixin/FluidRendererMixin.java b/katara/src/main/java/gg/norisk/heroes/katara/mixin/FluidRendererMixin.java new file mode 100644 index 0000000..6e8f56d --- /dev/null +++ b/katara/src/main/java/gg/norisk/heroes/katara/mixin/FluidRendererMixin.java @@ -0,0 +1,313 @@ +package gg.norisk.heroes.katara.mixin; + +import gg.norisk.heroes.katara.client.render.IFluidRendererExt; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.LeavesBlock; +import net.minecraft.block.TranslucentBlock; +import net.minecraft.client.color.world.BiomeColors; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.block.FluidRenderer; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.fluid.Fluid; +import net.minecraft.fluid.FluidState; +import net.minecraft.registry.tag.FluidTags; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.BlockRenderView; +import net.minecraft.world.BlockView; +import org.joml.Matrix4f; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.awt.*; + +@Mixin(FluidRenderer.class) +public abstract class FluidRendererMixin implements IFluidRendererExt { + @Shadow + @Final + private Sprite[] lavaSprites; + + @Shadow + @Final + private Sprite[] waterSprites; + + @Shadow + protected static boolean isSameFluid(FluidState fluidState, FluidState fluidState2) { + return false; + } + + @Shadow + public static boolean shouldRenderSide(BlockRenderView blockRenderView, BlockPos blockPos, FluidState fluidState, BlockState blockState, Direction direction, FluidState fluidState2) { + return false; + } + + @Shadow + protected static boolean isSideCovered(BlockView blockView, BlockPos blockPos, Direction direction, float f, BlockState blockState) { + return false; + } + + @Shadow + protected abstract float getFluidHeight(BlockRenderView blockRenderView, Fluid fluid, BlockPos blockPos); + + @Shadow + protected abstract float calculateFluidHeight(BlockRenderView blockRenderView, Fluid fluid, float f, float g, float h, BlockPos blockPos); + + @Shadow + protected abstract float getFluidHeight(BlockRenderView blockRenderView, Fluid fluid, BlockPos blockPos, BlockState blockState, FluidState fluidState); + + @Shadow + protected abstract int getLight(BlockRenderView blockRenderView, BlockPos blockPos); + + @Shadow + protected abstract void vertex(VertexConsumer vertexConsumer, float f, float g, float h, float i, float j, float k, float l, float m, int n); + + private void vertex2(Matrix4f matrix4f, VertexConsumer vertexConsumer, float f, float g, float h, float i, float j, float k, float l, float m, int n) { + vertexConsumer.vertex(matrix4f, f, g, h).color(i, j, k, 1.0F).texture(l, m).light(n).normal(0.0F, 1.0F, 0.0F); + } + + @Shadow + private Sprite waterOverlaySprite; + + public void katara_renderFluid(MatrixStack matrixStack, BlockRenderView blockRenderView, Vec3d pos, VertexConsumer vertexConsumer, BlockState blockState, FluidState fluidState, Color waterColor) { + Matrix4f matrix = matrixStack.peek().getPositionMatrix(); + + // Beispiel für Vertex-Anwendung: + // float x = (float) pos.x - 0.5f; + // float y = (float) pos.y - 0.5f; + // float z = (float) pos.z - 0.5f; + + boolean bl = fluidState.isIn(FluidTags.LAVA); + Sprite[] sprites = bl ? this.lavaSprites : this.waterSprites; + int i; + if (waterColor != null) { + i = waterColor.getRGB(); + } else { + i = bl ? 16777215 : BiomeColors.getWaterColor(blockRenderView, new BlockPos((int) pos.x, (int) pos.y, (int) pos.z)); + } + float f = (float) (i >> 16 & 0xFF) / 255.0F; + float g = (float) (i >> 8 & 0xFF) / 255.0F; + float h = (float) (i & 0xFF) / 255.0F; + var blockPos = new BlockPos((int) pos.x, (int) pos.y, (int) pos.z); + /*BlockState blockState2 = blockRenderView.getBlockState(blockPos.offset(Direction.DOWN)); + FluidState fluidState2 = blockState2.getFluidState(); + BlockState blockState3 = blockRenderView.getBlockState(blockPos.offset(Direction.UP)); + FluidState fluidState3 = blockState3.getFluidState(); + BlockState blockState4 = blockRenderView.getBlockState(blockPos.offset(Direction.NORTH)); + FluidState fluidState4 = blockState4.getFluidState(); + BlockState blockState5 = blockRenderView.getBlockState(blockPos.offset(Direction.SOUTH)); + FluidState fluidState5 = blockState5.getFluidState(); + BlockState blockState6 = blockRenderView.getBlockState(blockPos.offset(Direction.WEST)); + FluidState fluidState6 = blockState6.getFluidState(); + BlockState blockState7 = blockRenderView.getBlockState(blockPos.offset(Direction.EAST)); + FluidState fluidState7 = blockState7.getFluidState();*/ + boolean bl2 = true; //!isSameFluid(fluidState, fluidState3); + boolean bl3 = true;//shouldRenderSide(blockRenderView, blockPos, fluidState, blockState, Direction.DOWN, fluidState2) && !isSideCovered(blockRenderView, blockPos, Direction.DOWN, 0.8888889F, blockState2); + boolean bl4 = true;//shouldRenderSide(blockRenderView, blockPos, fluidState, blockState, Direction.NORTH, fluidState4); + boolean bl5 = true;//shouldRenderSide(blockRenderView, blockPos, fluidState, blockState, Direction.SOUTH, fluidState5); + boolean bl6 = true;//shouldRenderSide(blockRenderView, blockPos, fluidState, blockState, Direction.WEST, fluidState6); + boolean bl7 = true;//shouldRenderSide(blockRenderView, blockPos, fluidState, blockState, Direction.EAST, fluidState7); + if (bl2 || bl3 || bl7 || bl6 || bl4 || bl5) { + float j = blockRenderView.getBrightness(Direction.DOWN, true); + float k = blockRenderView.getBrightness(Direction.UP, true); + float l = blockRenderView.getBrightness(Direction.NORTH, true); + float m = blockRenderView.getBrightness(Direction.WEST, true); + Fluid fluid = fluidState.getFluid(); + float n = 1.0f;//this.getFluidHeight(blockRenderView, fluid, blockPos, blockState, fluidState); + float o = 1.0F; + float p = 1.0F; + float q = 1.0F; + float r = 1.0F; + if (n >= 1.0F) { + o = 1.0F; + p = 1.0F; + q = 1.0F; + r = 1.0F; + } else { + /*float s = this.getFluidHeight(blockRenderView, fluid, blockPos.north(), blockState4, fluidState4); + float t = this.getFluidHeight(blockRenderView, fluid, blockPos.south(), blockState5, fluidState5); + float u = this.getFluidHeight(blockRenderView, fluid, blockPos.east(), blockState7, fluidState7); + float v = this.getFluidHeight(blockRenderView, fluid, blockPos.west(), blockState6, fluidState6); + o = this.calculateFluidHeight(blockRenderView, fluid, n, s, u, blockPos.offset(Direction.NORTH).offset(Direction.EAST)); + p = this.calculateFluidHeight(blockRenderView, fluid, n, s, v, blockPos.offset(Direction.NORTH).offset(Direction.WEST)); + q = this.calculateFluidHeight(blockRenderView, fluid, n, t, u, blockPos.offset(Direction.SOUTH).offset(Direction.EAST)); + r = this.calculateFluidHeight(blockRenderView, fluid, n, t, v, blockPos.offset(Direction.SOUTH).offset(Direction.WEST));*/ + } + + + // float s = (float) ((float) pos.getX() - camera.getPos().getX()) - 0.5f;//(float) (blockPos.getX() & 15); + // float t = (float) ((float) pos.getY() - camera.getPos().getY()) - 0.5f;//(float) (blockPos.getY() & 15); + // float u = (float) ((float) pos.getZ() - camera.getPos().getZ()) - 0.5f;//(float) (blockPos.getZ() & 15); + float s = 0f;//(float) (blockPos.getX() & 15); + float t = 0f;//(float) (blockPos.getY() & 15); + float u = 0f;//(float) (blockPos.getZ() & 15); + float v = 0.001F; + float w = bl3 ? 0.001F : 0.0F; + if (bl2 /*&& !isSideCovered(blockRenderView, blockPos, Direction.UP, Math.min(Math.min(p, r), Math.min(q, o)), blockState3)*/) { + p -= 0.001F; + r -= 0.001F; + q -= 0.001F; + o -= 0.001F; + Vec3d vec3d = new Vec3d(1f, 1f, 1f); //fluidState.getVelocity(blockRenderView, blockPos); + float x; + float z; + float ab; + float ad; + float y; + float aa; + float ac; + float ae; + if (vec3d.x == 0.0 && vec3d.z == 0.0) { + Sprite sprite = sprites[0]; + x = sprite.getFrameU(0.0F); + y = sprite.getFrameV(0.0F); + z = x; + aa = sprite.getFrameV(1.0F); + ab = sprite.getFrameU(1.0F); + ac = aa; + ad = ab; + ae = y; + } else { + Sprite sprite = sprites[1]; + float af = (float) MathHelper.atan2(vec3d.z, vec3d.x) - (float) (Math.PI / 2); + float ag = MathHelper.sin(af) * 0.25F; + float ah = MathHelper.cos(af) * 0.25F; + float ai = 0.5F; + x = sprite.getFrameU(0.5F + (-ah - ag)); + y = sprite.getFrameV(0.5F + -ah + ag); + z = sprite.getFrameU(0.5F + -ah + ag); + aa = sprite.getFrameV(0.5F + ah + ag); + ab = sprite.getFrameU(0.5F + ah + ag); + ac = sprite.getFrameV(0.5F + (ah - ag)); + ad = sprite.getFrameU(0.5F + (ah - ag)); + ae = sprite.getFrameV(0.5F + (-ah - ag)); + } + + float aj = (x + z + ab + ad) / 4.0F; + float af = (y + aa + ac + ae) / 4.0F; + float ag = sprites[0].getAnimationFrameDelta(); + x = MathHelper.lerp(ag, x, aj); + z = MathHelper.lerp(ag, z, aj); + ab = MathHelper.lerp(ag, ab, aj); + ad = MathHelper.lerp(ag, ad, aj); + y = MathHelper.lerp(ag, y, af); + aa = MathHelper.lerp(ag, aa, af); + ac = MathHelper.lerp(ag, ac, af); + ae = MathHelper.lerp(ag, ae, af); + int ak = this.getLight(blockRenderView, blockPos); + float ai = k * f; + float al = k * g; + float am = k * h; + this.vertex2(matrix, vertexConsumer, s + 0.0F, t + p, u + 0.0F, ai, al, am, x, y, ak); + this.vertex2(matrix, vertexConsumer, s + 0.0F, t + r, u + 1.0F, ai, al, am, z, aa, ak); + this.vertex2(matrix, vertexConsumer, s + 1.0F, t + q, u + 1.0F, ai, al, am, ab, ac, ak); + this.vertex2(matrix, vertexConsumer, s + 1.0F, t + o, u + 0.0F, ai, al, am, ad, ae, ak); + if (fluidState.canFlowTo(blockRenderView, blockPos.up())) { + this.vertex2(matrix, vertexConsumer, s + 0.0F, t + p, u + 0.0F, ai, al, am, x, y, ak); + this.vertex2(matrix, vertexConsumer, s + 1.0F, t + o, u + 0.0F, ai, al, am, ad, ae, ak); + this.vertex2(matrix, vertexConsumer, s + 1.0F, t + q, u + 1.0F, ai, al, am, ab, ac, ak); + this.vertex2(matrix, vertexConsumer, s + 0.0F, t + r, u + 1.0F, ai, al, am, z, aa, ak); + } + } + + if (bl3) { + float xx = sprites[0].getMinU(); + float zx = sprites[0].getMaxU(); + float abx = sprites[0].getMinV(); + float adx = sprites[0].getMaxV(); + int an = this.getLight(blockRenderView, blockPos.down()); + float aax = j * f; + float acx = j * g; + float aex = j * h; + this.vertex2(matrix, vertexConsumer, s, t + w, u + 1.0F, aax, acx, aex, xx, adx, an); + this.vertex2(matrix, vertexConsumer, s, t + w, u, aax, acx, aex, xx, abx, an); + this.vertex2(matrix, vertexConsumer, s + 1.0F, t + w, u, aax, acx, aex, zx, abx, an); + this.vertex2(matrix, vertexConsumer, s + 1.0F, t + w, u + 1.0F, aax, acx, aex, zx, adx, an); + } + + int ao = this.getLight(blockRenderView, blockPos); + + for (Direction direction : Direction.Type.HORIZONTAL) { + float adx; + float yx; + float aax; + float acx; + float aex; + float ap; + boolean bl8; + switch (direction) { + case NORTH: + adx = p; + yx = o; + aax = s; + aex = s + 1.0F; + acx = u + 0.001F; + ap = u + 0.001F; + bl8 = bl4; + break; + case SOUTH: + adx = q; + yx = r; + aax = s + 1.0F; + aex = s; + acx = u + 1.0F - 0.001F; + ap = u + 1.0F - 0.001F; + bl8 = bl5; + break; + case WEST: + adx = r; + yx = p; + aax = s + 0.001F; + aex = s + 0.001F; + acx = u + 1.0F; + ap = u; + bl8 = bl6; + break; + default: + adx = o; + yx = q; + aax = s + 1.0F - 0.001F; + aex = s + 1.0F - 0.001F; + acx = u; + ap = u + 1.0F; + bl8 = bl7; + } + + if (bl8 && !isSideCovered(blockRenderView, blockPos, direction, Math.max(adx, yx), blockRenderView.getBlockState(blockPos.offset(direction)))) { + BlockPos blockPos2 = blockPos.offset(direction); + Sprite sprite2 = sprites[1]; + if (!bl) { + Block block = blockRenderView.getBlockState(blockPos2).getBlock(); + if (block instanceof TranslucentBlock || block instanceof LeavesBlock) { + sprite2 = this.waterOverlaySprite; + } + } + + float ah = sprite2.getFrameU(0.0F); + float ai = sprite2.getFrameU(0.5F); + float al = sprite2.getFrameV((1.0F - adx) * 0.5F); + float am = sprite2.getFrameV((1.0F - yx) * 0.5F); + float aq = sprite2.getFrameV(0.5F); + float ar = direction.getAxis() == Direction.Axis.Z ? l : m; + float as = k * ar * f; + float at = k * ar * g; + float au = k * ar * h; + this.vertex2(matrix, vertexConsumer, aax, t + adx, acx, as, at, au, ah, al, ao); + this.vertex2(matrix, vertexConsumer, aex, t + yx, ap, as, at, au, ai, am, ao); + this.vertex2(matrix, vertexConsumer, aex, t + w, ap, as, at, au, ai, aq, ao); + this.vertex2(matrix, vertexConsumer, aax, t + w, acx, as, at, au, ah, aq, ao); + if (sprite2 != this.waterOverlaySprite) { + this.vertex2(matrix, vertexConsumer, aax, t + w, acx, as, at, au, ah, aq, ao); + this.vertex2(matrix, vertexConsumer, aex, t + w, ap, as, at, au, ai, aq, ao); + this.vertex2(matrix, vertexConsumer, aex, t + yx, ap, as, at, au, ai, am, ao); + this.vertex2(matrix, vertexConsumer, aax, t + adx, acx, as, at, au, ah, al, ao); + } + } + } + } + } +} diff --git a/katara/src/main/java/gg/norisk/heroes/katara/mixin/PlayerEntityMixin.java b/katara/src/main/java/gg/norisk/heroes/katara/mixin/PlayerEntityMixin.java new file mode 100644 index 0000000..110b479 --- /dev/null +++ b/katara/src/main/java/gg/norisk/heroes/katara/mixin/PlayerEntityMixin.java @@ -0,0 +1,35 @@ +package gg.norisk.heroes.katara.mixin; + +import gg.norisk.heroes.katara.entity.IWaterBendingPlayer; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.math.BlockPos; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +import java.util.HashSet; +import java.util.Set; + +@Mixin(PlayerEntity.class) +public abstract class PlayerEntityMixin implements IWaterBendingPlayer { + @Unique + private final Set waterPillarBlocks = new HashSet<>(); + @Unique + private BlockPos waterPillarPos; + + @Override + public @NotNull Set getKatara_waterPillarBlocks() { + return waterPillarBlocks; + } + + @Override + public @Nullable BlockPos getKatara_waterPillarOrigin() { + return waterPillarPos; + } + + @Override + public void setKatara_waterPillarOrigin(@Nullable BlockPos blockPos) { + waterPillarPos = blockPos; + } +} diff --git a/katara/src/main/java/gg/norisk/heroes/katara/mixin/PlayerEntityRendererMixin.java b/katara/src/main/java/gg/norisk/heroes/katara/mixin/PlayerEntityRendererMixin.java new file mode 100644 index 0000000..c369e13 --- /dev/null +++ b/katara/src/main/java/gg/norisk/heroes/katara/mixin/PlayerEntityRendererMixin.java @@ -0,0 +1,25 @@ +package gg.norisk.heroes.katara.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import gg.norisk.heroes.katara.ability.WaterBendingAbility; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.entity.PlayerEntityRenderer; +import net.minecraft.client.render.entity.model.BipedEntityModel; +import net.minecraft.util.Hand; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(PlayerEntityRenderer.class) +public abstract class PlayerEntityRendererMixin { + @ModifyReturnValue( + method = "getArmPose", + at = @At("RETURN") + ) + private static BipedEntityModel.ArmPose getArmPoseInjection(BipedEntityModel.ArmPose original, AbstractClientPlayerEntity abstractClientPlayerEntity, Hand hand) { + BipedEntityModel.ArmPose waterBendingPose = WaterBendingAbility.INSTANCE.getWaterBendingPose(abstractClientPlayerEntity, hand); + if (waterBendingPose != null) { + return waterBendingPose; + } + return original; + } +} diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/KataraManager.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/KataraManager.kt new file mode 100644 index 0000000..0b52465 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/KataraManager.kt @@ -0,0 +1,68 @@ +package gg.norisk.heroes.katara + +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.HeroManager.registerHero +import gg.norisk.heroes.katara.ability.* +import gg.norisk.heroes.katara.registry.EntityRegistry +import gg.norisk.heroes.katara.registry.EntityRendererRegistry +import gg.norisk.heroes.katara.registry.SoundRegistry +import net.fabricmc.api.ClientModInitializer +import net.fabricmc.api.DedicatedServerModInitializer +import net.fabricmc.api.ModInitializer +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents +import net.minecraft.util.Identifier +import org.apache.logging.log4j.LogManager + +object KataraManager : ModInitializer, ClientModInitializer, DedicatedServerModInitializer { + const val MOD_ID = "katara" + val logger = LogManager.getLogger(MOD_ID) + fun String.toId() = Identifier.of(MOD_ID, this) + fun String.toEmote(): Identifier { + return "emotes/$this.animation.json".toId() + } + + val waterBenderOverlay = "waterbender_overlay.png".toId() + + override fun onInitialize() { + logger.info("Starting $MOD_ID Hero...") + SoundRegistry.init() + WaterPillarAbility.initServer() + HealingAbility.initServer() + WaterFormingAbility.initServer() + WaterBendingAbility.initServer() + WaterCircleAbilityV2.initServer() + EntityRegistry.init() + //WaterBubbleAbility.initServer() + } + + override fun onInitializeClient() { + ClientLifecycleEvents.CLIENT_STARTED.register { + registerHeroes() + } + EntityRendererRegistry.init() + WaterPillarAbility.initClient() + HealingAbility.initClient() + WaterFormingAbility.initClient() + WaterCircleAbilityV2.initClient() + WaterBendingAbility.initClient() + } + + override fun onInitializeServer() { + registerHero(Katara) + } + + private fun registerHeroes() { + registerHero(Katara) + } + + val Katara: Hero by Hero("Katara") { + ability(WaterBendingAbility.ability) + ability(WaterCircleAbilityV2.ability) + ability(WaterPillarAbility.ability) + ability(WaterFormingAbility.ability) + ability(IceShardAbility.ability) + ability(HealingAbility.ability) + color = 0x416bdf + overlaySkin = waterBenderOverlay + } +} diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/HealingAbility.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/HealingAbility.kt new file mode 100644 index 0000000..12f32bd --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/HealingAbility.kt @@ -0,0 +1,221 @@ +package gg.norisk.heroes.katara.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.datatracker.entity.syncedValueChangeEvent +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.PressAbility +import gg.norisk.heroes.common.utils.sound +import gg.norisk.heroes.katara.ability.WaterBendingAbility.getCurrentBendingEntity +import gg.norisk.heroes.katara.ability.WaterBendingAbility.waterBendingDistance +import gg.norisk.heroes.katara.client.render.HealingWaterFeatureRenderer +import gg.norisk.heroes.katara.client.sound.WaterHealingSoundInstance +import gg.norisk.heroes.katara.entity.IKataraEntity +import gg.norisk.heroes.katara.entity.WaterBendingEntity +import gg.norisk.utils.OldAnimation +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityFeatureRendererRegistrationCallback +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents +import net.minecraft.client.MinecraftClient +import net.minecraft.entity.Entity +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.effect.StatusEffectInstance +import net.minecraft.entity.effect.StatusEffects +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.potion.Potions +import net.minecraft.registry.RegistryKeys +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.sound.SoundEvents +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.math.Vec3d +import net.silkmc.silk.core.entity.directionVector +import net.silkmc.silk.core.item.itemStack +import net.silkmc.silk.core.item.setPotion +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.literalText +import kotlin.time.Duration.Companion.seconds + +object HealingAbility { + fun initServer() { + syncedValueChangeEvent.listen { + if (it.key != WATER_HEALING_EFFECT) return@listen + if (!it.entity.world.isClient) { + + } else { + if (it.entity.isReceivingWaterHealing) { + MinecraftClient.getInstance().soundManager.play(WaterHealingSoundInstance(it.entity) { entity -> + entity.isReceivingWaterHealing + }) + } + } + } + /*ServerTickEvents.END_SERVER_TICK.register(ServerTickEvents.EndTick { + for (player in it.players) { + val currentEntity = player.getCurrentBendingEntity() + if (currentEntity != null) { + (player as IKataraEntity).katara_entityCircleTracker.update(player) + if (player.katara_entityCircleTracker.isDrawingCircle) { + (player as IKataraEntity).katara_entityCircleTracker.clear() + //addToCircle(currentEntity, player) + player.sendMessage("CIRCLE: ".literal) + } + } else { + (player as IKataraEntity).katara_entityCircleTracker.clear() + } + } + })*/ + } + + data class WaterRender( + val pos: Vec3d, var animation: OldAnimation, val startTime: Int = 0 + ) + + var counter = 0 + + fun PlayerEntity.getWaterBendingPos(distance: Double = this.waterBendingDistance): Vec3d { + return this.getLeashPos( + if (world.isClient) { + MinecraftClient.getInstance().renderTickCounter.getTickDelta(false) + } else 1f + ).add(this.directionVector.normalize().multiply(distance).add(0.0, 1.0, 0.0)) + } + + fun initClient() { + LivingEntityFeatureRendererRegistrationCallback.EVENT.register(LivingEntityFeatureRendererRegistrationCallback { entityType, entityRenderer, registrationHelper, context -> + registrationHelper.register(HealingWaterFeatureRenderer(entityRenderer)) + }) + WorldRenderEvents.BEFORE_DEBUG_RENDER.register { + val player = MinecraftClient.getInstance().player ?: return@register + + val tickDelta = it.tickCounter().getTickDelta(false) + for (entity in it.world().entities) { + if (entity is WaterBendingEntity) { + entity.tickTrail(entity.getOwner(), tickDelta) + } + } + } + } + + private const val WATER_HEALING = "WaterBending:Healing" + private const val WATER_HEALING_EFFECT = "WaterBending:HealingEffect" + + private var PlayerEntity.isHealing: Boolean + get() = this.getSyncedData(WATER_HEALING) ?: false + set(value) = this.setSyncedData(WATER_HEALING, value) + + var Entity.isReceivingWaterHealing: Boolean + get() = this.getSyncedData(WATER_HEALING_EFFECT) ?: false + set(value) = this.setSyncedData(WATER_HEALING_EFFECT, value) + + fun Entity.handleWaterHealing(owner: PlayerEntity) { + (this as IKataraEntity).katara_waterHealingJob?.cancel() + isReceivingWaterHealing = true + val duration = waterHealingMaxDuration.getValue(owner.uuid).seconds + if (this is LivingEntity) { + addStatusEffect( + StatusEffectInstance( + StatusEffects.REGENERATION, + duration.inWholeMilliseconds.toInt() / 50, + waterHealingRegeneration.getValue(owner.uuid).toInt(), + false, + false + ) + ) + } + (this as IKataraEntity).katara_waterHealingJob = mcCoroutineTask(sync = true, delay = duration) { + isReceivingWaterHealing = false + // sendMessage("Stopped WaterHealing".literal) + } + } + + val waterHealingRegeneration = NumberProperty( + 0.0, 3, + "Regeneration", + AddValueTotal(1.0, 1.0, 1.0) + ).apply { + icon = { + Components.item(itemStack(Items.POTION) { + setPotion( + MinecraftClient.getInstance().world!!.registryManager.get(RegistryKeys.POTION) + .getEntry(Potions.REGENERATION.value()) + ) + }) + } + } + val waterHealingMaxDuration = NumberProperty( + 5.0, 4, + "Max Duration Lasts", + AddValueTotal(1.0, 1.0, 1.0, 1.0) + ).apply { + icon = { + Components.item(itemStack(Items.CLOCK) {}) + } + } + + val ability = object : PressAbility("Healing") { + init { + HeroesManager.client { + this.keyBind = HeroKeyBindings.thirdKeyBind + } + this.condition = { + it.getCurrentBendingEntity() != null + } + //this.usageProperty = buildMultipleUses(1.0, 3, AddValueTotal(1.0, 1.0, 1.0)) + this.cooldownProperty = buildCooldown(50.0, 4, AddValueTotal(-10.0, -10.0, -10.0, -10.0)) + + this.properties = listOf(waterHealingRegeneration, waterHealingMaxDuration) + } + + override fun getIconComponent(): Component { + return Components.item(itemStack(Items.POTION) { + setPotion( + MinecraftClient.getInstance().world!!.registryManager.get(RegistryKeys.POTION) + .getEntry(Potions.REGENERATION.value()) + ) + }) + } + + override fun hasUnlocked(player: PlayerEntity): Boolean { + return WaterBendingAbility.ability.cooldownProperty.isMaxed(player.uuid) || player.isCreative + } + + override fun getUnlockCondition(): Text { + return literalText { + text(Text.translatable("heroes.ability.$internalKey.unlock_condition")) + } + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_ice.png") + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + if (player is ServerPlayerEntity) { + val entity = player.getCurrentBendingEntity() + if (player.isSneaking) { + entity?.apply { + player.sound(SoundEvents.ENTITY_PLAYER_HURT_FREEZE, 1f, 2f) + freeze() + } + } else { + player.isHealing = true + entity?.apply { + isHealing = !isHealing + if (isHealing) { + entity.sound(SoundEvents.BLOCK_AMETHYST_BLOCK_HIT, 1f, 2f) + } else { + entity.sound(SoundEvents.BLOCK_AMETHYST_BLOCK_HIT, 1f, 0.3) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/IceShardAbility.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/IceShardAbility.kt new file mode 100644 index 0000000..2e8c267 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/IceShardAbility.kt @@ -0,0 +1,129 @@ +package gg.norisk.heroes.katara.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.HoldAbility +import gg.norisk.heroes.katara.KataraManager.toId +import gg.norisk.heroes.katara.ability.WaterBendingAbility.getCurrentBendingEntity +import gg.norisk.heroes.katara.entity.IceShardEntity +import gg.norisk.heroes.katara.registry.EntityRegistry +import gg.norisk.heroes.katara.registry.SoundRegistry +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.minecraft.entity.attribute.EntityAttributeModifier +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.sound.SoundCategory +import net.minecraft.util.Identifier +import net.minecraft.util.math.Vec3d +import net.minecraft.world.World +import net.silkmc.silk.core.entity.directionVector +import kotlin.random.Random + +object IceShardAbility { + + private const val ICE_SHARDING = "WaterBending:ICE_SHARDING" + var PlayerEntity.isIceShooting: Boolean + get() = this.getSyncedData(ICE_SHARDING) ?: false + set(value) = this.setSyncedData(ICE_SHARDING, value) + + val ICE_SHARD_SLOW_BOOST = EntityAttributeModifier( + "ice_shard".toId(), + -0.3, + EntityAttributeModifier.Operation.ADD_MULTIPLIED_TOTAL + ) + + val ability = object : HoldAbility("Ice Shards") { + + init { + HeroesManager.client { + this.keyBind = HeroKeyBindings.fifthKeyBind + } + this.condition = { + it.getCurrentBendingEntity() != null + } + + this.cooldownProperty = + buildCooldown(26.0, 4, AddValueTotal(-2.0, -2.0, -2.0, -2.0)) + this.maxDurationProperty = + buildMaxDuration(0.5, 5, AddValueTotal(0.25, 0.25, 0.25, 0.25, 0.25)) + } + + override fun getIconComponent(): Component { + return Components.item(Items.ARROW.defaultStack) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_ice.png") + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + player.isIceShooting = true + player.getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED) + ?.addTemporaryModifier(ICE_SHARD_SLOW_BOOST) + } + } + + override fun onTick(player: PlayerEntity) { + super.onTick(player) + if (player.isIceShooting) { + launchProjectiles(player.world, player) + } + } + + fun cleanUp(player: PlayerEntity) { + if (player is ServerPlayerEntity) { + player.isIceShooting = false + player.getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED) + ?.removeModifier(ICE_SHARD_SLOW_BOOST.id) + } + } + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + cleanUp(player) + } + + override fun onEnd(player: PlayerEntity, abilityEndInformation: AbilityEndInformation) { + super.onEnd(player, abilityEndInformation) + cleanUp(player) + } + + private fun launchProjectiles(world: World, user: PlayerEntity) { + if (!world.isClient) { + val entity = user.getCurrentBendingEntity() + entity?.discard() + val iceShard = IceShardEntity(EntityRegistry.ICE_SHARD, world) + iceShard.damage = 1.0 + val pos = user.eyePos.add(0.0, -0.1, 0.0).add(user.directionVector.normalize().multiply(1.0)) + val offset = 1.0 + val randomX = Random.nextDouble(-offset, offset) + val randomY = Random.nextDouble(-offset, offset) + val randomZ = Random.nextDouble(-offset, offset) + val spawnPos = Vec3d(pos.x + randomX, pos.y + randomY, pos.z + randomZ) + iceShard.setPosition(spawnPos.x, spawnPos.y, spawnPos.z) + //snowballEntity.setItem(Items.SNOWBALL.defaultStack) + iceShard.setVelocity(user, user.pitch, user.yaw, 0.0f, 5f, 0.0f) + world.playSound( + null, + spawnPos.getX(), + spawnPos.getY(), + spawnPos.getZ(), + SoundRegistry.ICE_PLACE, + SoundCategory.PLAYERS, + 0.75f, + Random.nextDouble(1.5, 2.0).toFloat() + ) + world.spawnEntity(iceShard) + } + } + } +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterBendingAbility.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterBendingAbility.kt new file mode 100644 index 0000000..d3fa1e6 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterBendingAbility.kt @@ -0,0 +1,296 @@ +package gg.norisk.heroes.katara.ability + +import gg.norisk.datatracker.entity.* +import gg.norisk.datatracker.serialization.BlockPosSerializer +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.client.renderer.BlockOutlineRenderer +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.ability.CooldownProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.command.DebugCommand.sendDebugMessage +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.HoldAbility +import gg.norisk.heroes.common.hero.isHero +import gg.norisk.heroes.common.networking.Networking.mousePacket +import gg.norisk.heroes.common.networking.Networking.mouseScrollPacket +import gg.norisk.heroes.common.utils.sound +import gg.norisk.heroes.katara.KataraManager.Katara +import gg.norisk.heroes.katara.KataraManager.MOD_ID +import gg.norisk.heroes.katara.KataraManager.toId +import gg.norisk.heroes.katara.ability.HealingAbility.getWaterBendingPos +import gg.norisk.heroes.katara.ability.IceShardAbility.isIceShooting +import gg.norisk.heroes.katara.ability.WaterCircleAbilityV2.waterCircleAmount +import gg.norisk.heroes.katara.ability.WaterFormingAbility.isWaterForming +import gg.norisk.heroes.katara.client.sound.WaterSelectingSoundInstance +import gg.norisk.heroes.katara.entity.WaterBendingEntity +import gg.norisk.heroes.katara.registry.EntityRegistry +import gg.norisk.heroes.katara.registry.SoundRegistry +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import kotlinx.serialization.Serializable +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents +import net.minecraft.block.BlockState +import net.minecraft.block.Blocks +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.AbstractClientPlayerEntity +import net.minecraft.client.render.entity.model.BipedEntityModel.ArmPose +import net.minecraft.client.world.ClientWorld +import net.minecraft.entity.Entity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.fluid.Fluids +import net.minecraft.item.Items +import net.minecraft.registry.tag.BlockTags +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.util.Hand +import net.minecraft.util.Identifier +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.math.BlockPos +import net.silkmc.silk.core.server.players +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.literal +import net.silkmc.silk.network.packet.s2cPacket +import kotlin.random.Random + +object WaterBendingAbility { + val ability = object : HoldAbility("Water Bending") { + init { + HeroesManager.client { + this.keyBind = HeroKeyBindings.firstKeyBind + } + + mouseScrollPacket.receiveOnServer { packet, context -> + mcCoroutineTask(sync = true, client = false) { + context.player.scaleBendingDistance(packet) + } + } + + this.cooldownProperty = + buildCooldown(10.0, 5, AddValueTotal(-1.0, -1.0, -1.0, -1.0, -1.0)) + this.maxDurationProperty = + buildMaxDuration(10.0, 5, AddValueTotal(2.0, 1.0, 0.5, 0.5, 1.0)) + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + if (!player.world.isClient) { + player.isWaterSelecting = true + } + } + + override fun getIconComponent(): Component { + return Components.item(Items.WATER_BUCKET.defaultStack) + } + + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_ice.png") + } + + override fun onEnd(player: PlayerEntity, abilityEndInformation: AbilityEndInformation) { + if (player is ServerPlayerEntity) { + player.isWaterSelecting = false + // player.sendMessage("END".literal) + var lastPos: BlockPos? = null + for (block in player.selectedWaterBlocks.blocks) { + val state = player.world.getBlockState(block) + if (state.isWaterSource + ) { + lastPos = block + player.world.breakBlock(block, false, player) + if (state.isOf(Blocks.WATER)) { + player.serverWorld.setBlockState( + block, + Fluids.FLOWING_WATER.getFlowing(1, false).blockState + ) + } + } + } + if (lastPos != null) { + val water = EntityRegistry.WATER_BENDING.create(player.serverWorld) ?: return + /*player.serverWorld.setBlockState( + clipWithDistance.blockPos, + Fluids.FLOWING_WATER.getFlowing(1, false).blockState + )*/ + water.setPosition(lastPos.toCenterPos()) + //water.setPosition(player.getWaterBendingPos()) + water.ownerId = player.id + water.isInitial = true + player.serverWorld.spawnEntity(water) + } else { + // player.sendMessage("Jetzt".literal) + if (player.waterCircleAmount > 0) { + player.waterCircleAmount -= 1 + player.sound(SoundRegistry.WATER_CIRCLE_ADD, 0.4f, Random.nextDouble(1.5, 2.0)) + val water = EntityRegistry.WATER_BENDING.create(player.serverWorld) ?: return + water.setPosition(player.getWaterBendingPos()) + water.ownerId = player.id + water.isInitial = false + player.serverWorld.spawnEntity(water) + } else { + abilityEndInformation.applyCooldown = false + } + } + player.selectedWaterBlocks = SelectedWaterBlocks(mutableListOf()) + } + } + } + + private fun ServerPlayerEntity.scaleBendingDistance(packet: Boolean) { + if (!isHero(Katara)) return + if (getCurrentBendingEntity() == null) return + val world = this.serverWorld + + val scale = 0.5 + val forceStrength = if (packet) scale else -scale + + + waterBendingDistance += forceStrength + if (waterBendingDistance < 3.0) { + waterBendingDistance = 3.0 + } + if (waterBendingDistance > 15) { + waterBendingDistance = 15.0 + } + } + + val BlockState.isWaterSource + get() = isOf(Blocks.WATER) || isIn(BlockTags.SNOW) || isIn(BlockTags.ICE) || isIn( + BlockTags.LEAVES + ) || isIn(BlockTags.FLOWERS) || isIn(BlockTags.REPLACEABLE_BY_TREES) || isIn(BlockTags.LOGS) || isOf(Blocks.BAMBOO) + + fun initServer() { + (registeredTypes as MutableMap).put( + SelectedWaterBlocks::class, + SelectedWaterBlocks.serializer() + ) + + ServerTickEvents.END_SERVER_TICK.register { + for (player in it.players) { + if (!player.isHero(Katara)) continue + if (!player.isWaterSelecting) continue + /*val target = findCrosshairTarget(player,5.0,5.0,1f) + player.sendMessage("Target: ${target.pos} ${target.type} ${player.world.getBlockState(target.pos.toBlockPos())}".literal) + player.firstWaterFormingPos = target.pos.toBlockPos()*/ + val pos = (player.raycast(45.0, 0.0f, true) as? BlockHitResult?)?.blockPos?.toImmutable() ?: continue + val state = player.world.getBlockState(pos) + if (state.isWaterSource) { + if (!player.selectedWaterBlocks.blocks.contains(pos)) { + player.selectedWaterBlocks.blocks.add(pos) + if (player.selectedWaterBlocks.blocks.size >= 15) { + player.selectedWaterBlocks.blocks.removeFirstOrNull() + } + + player.selectedWaterBlocks = SelectedWaterBlocks(buildList { + addAll(player.selectedWaterBlocks.blocks) + }.toMutableList()) + } + } + } + } + + mousePacket.receiveOnServer { packet, context -> + mcCoroutineTask(sync = true, client = false) { + //TODO automatisieren + if (!context.player.isHero(Katara)) return@mcCoroutineTask + if (packet.isClicked() && packet.isRight()) { + for (entity in context.player.serverWorld.iterateEntities()) { + if (entity is WaterBendingEntity && entity.ownerId == context.player.id) { + entity.drop(context.player) + } + } + } else if (packet.isLeft() && packet.isClicked()) { + for (entity in context.player.serverWorld.iterateEntities()) { + if (entity is WaterBendingEntity && entity.ownerId == context.player.id) { + entity.launch(context.player) + } + } + } + } + } + + syncedValueChangeEvent.listen { event -> + if (event.key != IS_WATER_SELECTING) return@listen + val player = event.entity as? PlayerEntity? ?: return@listen + if (player.world.isClient) { + if (player.isWaterSelecting) { + MinecraftClient.getInstance().soundManager.play(WaterSelectingSoundInstance(player) { entity -> + ((entity as? PlayerEntity?)?.isWaterSelecting == true) + }) + } + } + } + } + + fun initClient() { + WorldRenderEvents.AFTER_TRANSLUCENT.register { + val player = MinecraftClient.getInstance().player ?: return@register + val matrices = it.matrixStack() ?: return@register + if (!player.isWaterSelecting) return@register + for (block in player.selectedWaterBlocks.blocks) { + BlockOutlineRenderer.drawBlockBox( + matrices, + it.consumers() ?: return@register, + block, + 1.0f, + 1.0f, + 1.0f, + 0.6f + ) + } + } + } + + @Serializable + data class SelectedWaterBlocks( + val blocks: MutableList<@Serializable(with = BlockPosSerializer::class) BlockPos> + ) + + private const val IS_WATER_SELECTING = "WaterBending:IS_WATER_SELECTING" + private const val WATER_BENDING_DISTANCE = "WaterBending:WATER_BENDING_DISTANCE" + + @Environment(EnvType.CLIENT) + fun getWaterBendingPose(player: AbstractClientPlayerEntity, hand: Hand): ArmPose? { + if (player.isIceShooting) { + return ArmPose.BOW_AND_ARROW + } + if (player.isWaterSelecting) { + return ArmPose.BOW_AND_ARROW + } + if (player.isWaterForming) { + return ArmPose.BOW_AND_ARROW + } + val currentEntity = player.getCurrentBendingEntity() + if (currentEntity != null && !currentEntity.wasLaunched) { + return ArmPose.BOW_AND_ARROW + } + return null + } + + var Entity.selectedWaterBlocks: SelectedWaterBlocks + get() = this.getSyncedData("$MOD_ID:SelectedWaterBlocks") + ?: SelectedWaterBlocks(mutableListOf()) + set(value) { + this.setSyncedData("$MOD_ID:SelectedWaterBlocks", value) + } + + var PlayerEntity.waterBendingDistance: Double + get() = this.getSyncedData(WATER_BENDING_DISTANCE) ?: 3.0 + set(value) = this.setSyncedData(WATER_BENDING_DISTANCE, value) + + var PlayerEntity.isWaterSelecting: Boolean + get() = this.getSyncedData(IS_WATER_SELECTING) ?: false + set(value) = this.setSyncedData(IS_WATER_SELECTING, value) + + fun PlayerEntity.getCurrentBendingEntity(): WaterBendingEntity? { + val entities = if (!world.isClient) { + (world as ServerWorld).iterateEntities() + } else { + (world as ClientWorld).entities + } + + return entities.filterIsInstance().find { it.ownerId == id } + } +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterCircleAbilityV2.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterCircleAbilityV2.kt new file mode 100644 index 0000000..973df08 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterCircleAbilityV2.kt @@ -0,0 +1,309 @@ +package gg.norisk.heroes.katara.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.datatracker.entity.syncedValueChangeEvent +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.events.EntityEvents +import gg.norisk.heroes.common.hero.ability.AbstractAbility +import gg.norisk.heroes.common.utils.sound +import gg.norisk.heroes.katara.ability.WaterBendingAbility.getCurrentBendingEntity +import gg.norisk.heroes.katara.client.render.IFluidRendererExt +import gg.norisk.heroes.katara.client.sound.WaterCircleSoundInstance +import gg.norisk.heroes.katara.entity.IKataraEntity +import gg.norisk.heroes.katara.entity.WaterBendingEntity +import gg.norisk.heroes.katara.registry.SoundRegistry +import gg.norisk.utils.OldAnimation +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents.AllowDamage +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.block.BlockState +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.RenderLayers +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.damage.DamageTypes +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.fluid.Fluids +import net.minecraft.item.Items +import net.minecraft.particle.ParticleTypes +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundEvents +import net.minecraft.text.Text +import net.minecraft.util.Colors +import net.minecraft.util.Identifier +import net.minecraft.util.math.Vec3d +import net.silkmc.silk.commands.command +import net.silkmc.silk.core.text.literalText +import org.joml.Quaternionf +import org.joml.Vector3f +import kotlin.math.* +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +object WaterCircleAbilityV2 { + val waterCircleMaxBalls = NumberProperty(1.0, 5, "Water Circle Sphere", AddValueTotal(1.0, 1.0, 1.0, 1.0, 1.0, 3.0)).apply { + icon = { + Components.item(Items.HEART_OF_THE_SEA.defaultStack) + } + } + val waterCircleMaxFallDamage = NumberProperty(10.0, 3, "Water Circle Fall Distance", AddValueTotal(20.0, 30.0, 40.0)).apply { + icon = { + Components.item(Items.WATER_BUCKET.defaultStack) + } + } + + val ability = object : AbstractAbility("Water Circle") { + init { + this.cooldownProperty = buildNoCooldown() + this.properties = listOf( + waterCircleMaxBalls, + waterCircleMaxFallDamage + ) + } + + override fun onTick(player: PlayerEntity) { + super.onTick(player) + if (player is ServerPlayerEntity) { + val currentEntity = player.getCurrentBendingEntity() + if (currentEntity != null) { + (player as IKataraEntity).katara_entitySpinTracker.update(player) + if (player.katara_entitySpinTracker.hasSpunWildly()) { + if (hasUnlocked(player)) { + (player as IKataraEntity).katara_entitySpinTracker.clear() + addToCircle(currentEntity, player) + } else { + player.katara_entitySpinTracker.clear() + player.sendMessage(Text.translatable("heroes.ability.locked").withColor(Colors.RED)) + } + } + } else { + (player as IKataraEntity).katara_entitySpinTracker.clear() + } + } + } + + override fun onEnable(player: PlayerEntity) { + super.onEnable(player) + if (player is ServerPlayerEntity) { + player.waterCircleAmount = 0 + } + } + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + if (player is ServerPlayerEntity) { + player.waterCircleAmount = 0 + } + } + + override fun hasUnlocked(player: PlayerEntity): Boolean { + return WaterBendingAbility.ability.cooldownProperty.isMaxed(player.uuid) || player.isCreative + } + + override fun getUnlockCondition(): Text { + return literalText { + text(Text.translatable("heroes.ability.$internalKey.unlock_condition")) + } + } + + override fun getIconComponent(): Component { + return Components.item(Items.HEART_OF_THE_SEA.defaultStack) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_ice.png") + } + } + + fun initServer() { + if (FabricLoader.getInstance().isDevelopmentEnvironment) { + command("waterbending") { + literal("watercircle") { + argument("amount") { amount -> + runs { + this.source.playerOrThrow.waterCircleAmount = amount() + } + } + } + } + } + syncedValueChangeEvent.listen { event -> + if (event.key != WATER_CIRCLE_AMOUNT) return@listen + val player = event.entity as? PlayerEntity? ?: return@listen + if (player.world.isClient) { + if (player.waterCircleAmount == 1) { + MinecraftClient.getInstance().soundManager.play(WaterCircleSoundInstance(player) { entity -> + ((entity as? PlayerEntity?)?.waterCircleAmount ?: 1) > 0 + }) + } + } + } + + EntityEvents.computeFallDamageEvent.listen { event -> + if (event.livingEntity is ServerPlayerEntity) { + if (event.originalFallDamage > 0 && event.fallDistance <= waterCircleMaxFallDamage.getValue(event.livingEntity.uuid)) { + if (event.livingEntity.breakWaterCirclePiece()) { + event.fallDamage = 0 + } + } + } + } + + ServerLivingEntityEvents.ALLOW_DAMAGE.register(AllowDamage { entity, source, amount -> + if (entity is PlayerEntity) { + if ((source.isOf(DamageTypes.ON_FIRE)) && entity.breakWaterCirclePiece()) { + entity.extinguishWithSound() + return@AllowDamage false + } else if ((source.isOf(DamageTypes.IN_FIRE) || source.isOf(DamageTypes.LAVA)) && entity.breakWaterCirclePiece()) { + entity.world.setBlockState(entity.blockPos, Fluids.WATER.getFlowing(1, true).blockState) + return@AllowDamage false + } + } + return@AllowDamage true + }) + } + + private fun addToCircle(entity: WaterBendingEntity?, player: PlayerEntity) { + if (player.waterCircleAmount < waterCircleMaxBalls.getValue(player.uuid)) { + player.sound(SoundRegistry.WATER_CIRCLE_ADD, 0.7f, Random.nextDouble(1.2, 1.5)) + entity?.discard() + player.waterCircleAmount += 1 + } + } + + fun LivingEntity.breakWaterCirclePiece(): Boolean { + if (waterCircleAmount > 0) { + waterCircleAmount -= 1 + sound(SoundEvents.ENTITY_GENERIC_SPLASH, 1f, 1f) + repeat(40) { + (world as? ServerWorld?)?.spawnParticles( + ParticleTypes.SPLASH, + getParticleX(0.5) + Random.nextDouble(-1.0, 1.0), + randomBodyY + Random.nextDouble(-1.0, 1.0), + getParticleZ(0.5) + Random.nextDouble(-1.0, 1.0), + 20, + 0.001, + 0.001, + 0.001, + 0.0 + ) + } + return true + } + return false + } + + fun initClient() { + WorldRenderEvents.BEFORE_DEBUG_RENDER.register { + val clientPlayer = MinecraftClient.getInstance().player ?: return@register + val tickDelta = it.tickCounter().getTickDelta(false) + val matrixStack = it.matrixStack() ?: return@register + for (player in it.world().players) { + val amount = player.waterCircleAmount + if (amount > 0) { + repeat(amount) { index -> + renderWaterCircle( + player, + matrixStack, + player.getLerpedPos(tickDelta), + Fluids.FLOWING_WATER.defaultState.blockState, + OldAnimation(0.5f, 0.5f, 1.seconds.toJavaDuration()), + index, + waterCircleMaxBalls.getMaxValue().toInt(), // Pass the total amount for even distribution + System.currentTimeMillis() // Use current time in milliseconds for rotation + ) + } + } + } + } + } + + @Environment(EnvType.CLIENT) + fun renderWaterCircle( + player: PlayerEntity, + matrixStack: MatrixStack, + centerPos: Vec3d, + state: BlockState, + animation: OldAnimation, + index: Int, + totalAmount: Int, + currentTimeMillis: Long + ) { + val camera = MinecraftClient.getInstance().gameRenderer.camera + val renderer = MinecraftClient.getInstance().blockRenderManager + val world = MinecraftClient.getInstance().world ?: return + val vertexConsumer = MinecraftClient.getInstance().bufferBuilders.entityVertexConsumers.getBuffer( + RenderLayers.getFluidLayer(state.fluidState) + ) + + // Kreisposition berechnen + val radius = 0.8 // Radius des Kreises + val rotationSpeed = 0.002 // Geschwindigkeit der Rotation (Bogenmaß pro Millisekunde) + val baseAngle = (index.toDouble() / totalAmount) * 2 * PI // Gleichmäßige Winkelverteilung entlang des Kreises + val rotationOffset = (currentTimeMillis * rotationSpeed) % (2 * PI) // Rotationsversatz basierend auf der Zeit + val angle = baseAngle + rotationOffset + + val timeFactor = (System.currentTimeMillis() % 10000L) / 1000.0 // Zeit in Sekunden (loop alle 10 Sekunden) + val sineOffset = Math.sin((timeFactor + index * 0.5) * Math.PI) * 0.1 // Wellenbewegung mit tickDelta animiert + + + val offsetX = radius * cos(angle) + val offsetZ = radius * sin(angle) + + val blockPos = centerPos.add(offsetX, 0.0, offsetZ) + + matrixStack.push() + matrixStack.translate( + blockPos.x - camera.pos.x, + blockPos.y - camera.pos.y + sineOffset, + blockPos.z - camera.pos.z + ) + matrixStack.translate(0.0, (player.height / 2.0), 0.0) + matrixStack.scale(animation.get(), animation.get(), animation.get()) + matrixStack.multiply(rotateTowards(blockPos, centerPos, Quaternionf())) + matrixStack.translate(-0.5, -0.5, -0.5) + + (renderer.fluidRenderer as IFluidRendererExt).katara_renderFluid( + matrixStack, + world, + blockPos, + vertexConsumer, + state, + state.fluidState, + null + ) + + matrixStack.pop() + } + + @Environment(EnvType.CLIENT) + fun rotateTowards(from: Vec3d?, to: Vec3d, original: Quaternionf): Quaternionf { + val direction = to.subtract(from).normalize() + val forward = Vector3f(0f, 0f, -1f) + + val dir = Vector3f(direction.x.toFloat(), direction.y.toFloat(), direction.z.toFloat()) + val axis = Vector3f() + forward.cross(dir, axis).normalize() + + val dot = forward.dot(dir) + val angle = acos(max(-1.0, min(1.0, dot.toDouble()))).toFloat() + + val rotationQuat = Quaternionf().fromAxisAngleRad(axis, angle) + return rotationQuat.mul(original) + } + + private const val WATER_CIRCLE_AMOUNT = "WaterBending:WaterCircleAmount" + + var LivingEntity.waterCircleAmount: Int + get() = this.getSyncedData(WATER_CIRCLE_AMOUNT) ?: 0 + set(value) = this.setSyncedData(WATER_CIRCLE_AMOUNT, value) +} diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterFormingAbility.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterFormingAbility.kt new file mode 100644 index 0000000..fd732c5 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterFormingAbility.kt @@ -0,0 +1,376 @@ +package gg.norisk.heroes.katara.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.client.renderer.RenderUtils +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.HoldAbility +import gg.norisk.heroes.common.hero.isHero +import gg.norisk.heroes.common.utils.sound +import gg.norisk.heroes.common.utils.toBlockPos +import gg.norisk.heroes.katara.KataraManager +import gg.norisk.heroes.katara.ability.IceShardAbility.isIceShooting +import gg.norisk.heroes.katara.ability.WaterBendingAbility.getCurrentBendingEntity +import gg.norisk.heroes.katara.client.render.IFluidRendererExt +import gg.norisk.heroes.katara.registry.SoundRegistry +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents +import net.minecraft.block.BlockState +import net.minecraft.block.Blocks +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.RenderLayers +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.particle.ParticleTypes +import net.minecraft.registry.tag.BlockTags +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.util.Identifier +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Vec3d +import net.minecraft.util.math.random.Random +import net.minecraft.world.World +import net.silkmc.silk.core.item.itemStack +import net.silkmc.silk.core.math.geometry.filledSpherePositionSet +import net.silkmc.silk.core.server.players +import kotlin.math.abs +import kotlin.math.sign +import kotlin.time.Duration.Companion.seconds + +object WaterFormingAbility { + + val waterFormingMaxDistance = NumberProperty( + 10.0, 3, + "Water Forming Max Blocks", + AddValueTotal(5.0, 5.0, 5.0) + ).apply { + icon = { + Components.item(itemStack(Items.ICE) {}) + } + } + + val ability = object : HoldAbility("Water Forming") { + init { + HeroesManager.client { + this.keyBind = HeroKeyBindings.secondKeyBind + } + this.condition = { + val pos = (it.raycast(20.0, 0.0f, false) as? BlockHitResult?)?.blockPos?.toImmutable() + checkForEnoughWater(pos, it.world) || it.getCurrentBendingEntity() != null + } + + this.cooldownProperty = + buildCooldown(10.0, 5, AddValueTotal(-0.1, -0.4, -0.2, -0.8, -1.5, -1.0)) + this.maxDurationProperty = + buildMaxDuration(10.0, 5, AddValueTotal(0.1, 0.4, 0.2, 0.8, 1.5, 1.0)) + + this.properties = listOf(waterFormingMaxDistance) + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + val pos = (player.raycast(20.0, 0.0f, false) as? BlockHitResult?)?.blockPos?.toImmutable() + player.isWaterForming = true + player.firstWaterFormingPos = pos + } + } + + override fun getIconComponent(): Component { + return Components.item(Items.BLUE_ICE.defaultStack) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_ice.png") + } + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + cleanUp(player) + } + + private fun cleanUp(player: PlayerEntity) { + if (player is ServerPlayerEntity) { + player.isWaterForming = false + player.firstWaterFormingPos = null + player.secondWaterFormingPos = null + } + } + + override fun onEnd(player: PlayerEntity, abilityEndInformation: AbilityEndInformation) { + super.onEnd(player, abilityEndInformation) + if (player is ServerPlayerEntity) { + placeIceSelection( + player.serverWorld, + player, + player.firstWaterFormingPos, + player.secondWaterFormingPos + ) + cleanUp(player) + } + } + } + + private var startTime: Long? = null + private var endTime: Long? = null // Endzeit speichern + private var lastAlpha: Float = 0f + + fun initServer() { + ServerTickEvents.END_SERVER_TICK.register { + for (player in it.players) { + if (!player.isHero(KataraManager.Katara)) continue + /*val target = findCrosshairTarget(player,5.0,5.0,1f) + player.sendMessage("Target: ${target.pos} ${target.type} ${player.world.getBlockState(target.pos.toBlockPos())}".literal) + player.firstWaterFormingPos = target.pos.toBlockPos()*/ + if (player.firstWaterFormingPos != null) { + val pos = (player.raycast(20.0, 0.0f, false) as? BlockHitResult?)?.blockPos?.toImmutable() + if (pos != player.firstWaterFormingPos && pos?.isWithinDistance( + player.firstWaterFormingPos, + waterFormingMaxDistance.getValue(player.uuid) + ) == true + ) { + player.secondWaterFormingPos = pos + } + } + } + } + } + + fun initClient() { + WorldRenderEvents.BEFORE_DEBUG_RENDER.register { + val player = MinecraftClient.getInstance().player ?: return@register + val tickDelta = it.tickCounter().getTickDelta(false) + val matrixStack = it.matrixStack() ?: return@register + /*renderBlock( + matrixStack, + Vec3d.of(player.firstWaterFormingPos ?: return@register), + Blocks.ICE.defaultState, + false + )*/ + for (blockPos in createStaircasePath( + player.firstWaterFormingPos ?: return@register, + player.secondWaterFormingPos ?: player.firstWaterFormingPos ?: return@register + )) { + renderBlock( + matrixStack, + Vec3d.of(blockPos), + Blocks.ICE.defaultState, + false + ) + } + } + + val overlay = Identifier.ofVanilla("textures/misc/powder_snow_outline.png") + HudRenderCallback.EVENT.register(HudRenderCallback { drawContext, tickCounter -> + val player = MinecraftClient.getInstance().player + + if (player?.isWaterForming == true || player?.isIceShooting == true) { + if (startTime == null) { + startTime = System.currentTimeMillis() // Startzeit initialisieren + } + endTime = null // Endzeit zurücksetzen, da der Effekt aktiv ist + + val currentTime = System.currentTimeMillis() + val elapsedTime = currentTime - startTime!! // Verstrichene Zeit in Millisekunden + + val fadeInDuration = 1000.0 // Fade-In-Dauer in Millisekunden (1 Sekunde) + lastAlpha = (elapsedTime / fadeInDuration).coerceAtMost(1.0).toFloat() // Begrenze Alpha auf maximal 1 + + RenderUtils.renderOverlay(drawContext, overlay, lastAlpha) + } else if (endTime == null && startTime != null) { + // Effekt ist beendet, Endzeit setzen + endTime = System.currentTimeMillis() + } else if (endTime != null) { + // Ausfade-Phase + val currentTime = System.currentTimeMillis() + val fadeOutDuration = 200.0 // Ausfade-Dauer in Millisekunden (1 Sekunde) + val elapsedFadeOutTime = currentTime - endTime!! // Verstrichene Zeit seit Ende + + if (elapsedFadeOutTime < fadeOutDuration) { + // Linearer Fade-Out von 1 bis 0 + val alpha = (lastAlpha - (elapsedFadeOutTime / fadeOutDuration)).toFloat() + RenderUtils.renderOverlay(drawContext, overlay, alpha) + } else { + // Vollständig ausgeblendet, alles zurücksetzen + startTime = null + endTime = null + } + } + }) + } + + @Environment(EnvType.CLIENT) + fun renderBlock( + matrixStack: MatrixStack, + pos: Vec3d, + state: BlockState, + fluid: Boolean = false, + ) { + val blockPos = pos.toBlockPos() + //println("Pos: $blockPos") + val camera = MinecraftClient.getInstance().gameRenderer.camera + val renderer = MinecraftClient.getInstance().blockRenderManager + val world = MinecraftClient.getInstance().world ?: return + val blockState = world.getBlockState(blockPos) + if (!(blockState.isLiquid || blockState.isAir || !blockState.isSolid)) { + return + } + val vertexConsumer = if (fluid) { + MinecraftClient.getInstance().bufferBuilders.entityVertexConsumers.getBuffer( + RenderLayers.getFluidLayer(state.fluidState) + ) + } else { + MinecraftClient.getInstance().bufferBuilders.entityVertexConsumers.getBuffer( + RenderLayers.getBlockLayer(state) + ) + } + + matrixStack.push() + matrixStack.translate( + blockPos.x - camera.pos.x, + blockPos.y - camera.pos.y, + blockPos.z - camera.pos.z + ) + + val scale = 1f + matrixStack.scale(scale, scale, scale) + + + //matrixStack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(angle)) + + // Rotation um die Y-Achse + + //matrixStack.translate(-0.5, 0.5, -0.5) + + if (fluid) { + (renderer.fluidRenderer as IFluidRendererExt).katara_renderFluid( + matrixStack, + world, + blockPos.toCenterPos(), + vertexConsumer, + state, + state.fluidState, + null + ) + } else { + renderer.renderBlock(state, blockPos, world, matrixStack, vertexConsumer, true, Random.create()) + } + + matrixStack.pop() + } + + + fun createStaircasePath(start: BlockPos, end: BlockPos): List { + val path = mutableSetOf() + path.add(start) + path.add(end) + + var currentX = start.x + var currentY = start.y + var currentZ = start.z + + val deltaX = end.x - start.x + val deltaY = end.y - start.y + val deltaZ = end.z - start.z + + val steps = maxOf(abs(deltaX), abs(deltaY), abs(deltaZ)) + + val stepX = deltaX.sign + val stepY = deltaY.sign + val stepZ = deltaZ.sign + + var accumulatedY = 0.0 + val yStepIncrement = abs(deltaY.toDouble() / steps) + + for (i in 0..steps) { + path.add(BlockPos(currentX, currentY, currentZ)) + + if (currentX != end.x) currentX += stepX + if (currentZ != end.z) currentZ += stepZ + + accumulatedY += yStepIncrement + if (accumulatedY >= 1) { + currentY += stepY + accumulatedY -= 1 + } + } + + return path.toList() + } + + private fun placeIceSelection( + world: ServerWorld, + player: ServerPlayerEntity, + firstPos: BlockPos?, + secondPos: BlockPos? + ) { + if (firstPos == null) return + if (secondPos == null) return + + player.sound(SoundRegistry.ICE_PLACE, pitch = kotlin.random.Random.nextDouble(1.0, 1.5)) + //player.sound(SoundEvents.ENTITY_PLAYER_HURT_FREEZE, volume = 0.5f, pitch = kotlin.random.Random.nextDouble(1.0, 1.5)) + player.getCurrentBendingEntity()?.discard() + + for (blockPos in createStaircasePath( + firstPos, + secondPos + )) { + val currentState = world.getBlockState(blockPos) + if (currentState.isLiquid || currentState.isAir || !currentState.isSolid) { + world.setBlockState(blockPos, Blocks.ICE.defaultState) + val pos = blockPos.toCenterPos() + world.spawnParticles( + ParticleTypes.CLOUD, + pos.x + kotlin.random.Random.nextDouble(-1.0, 1.0), + pos.y + kotlin.random.Random.nextDouble(-1.0, 1.0), + pos.z + kotlin.random.Random.nextDouble(-1.0, 1.0), + 1, + 0.0, + 0.0, + 0.0, + 0.0 + ) + } + } + } + + private fun checkForEnoughWater(pos: BlockPos?, world: World): Boolean { + if (pos == null) return false + for (blockPos in pos.filledSpherePositionSet(3)) { + val state = world.getBlockState(blockPos) + if (world.getBlockState(blockPos) + .isOf(Blocks.WATER) || state.isIn(BlockTags.SNOW) || state.isIn(BlockTags.ICE) + ) { + return true + } + } + return false + } + + private const val IS_WATER_FORMING = "WaterBending:IS_WATER_FORMING" + private const val WATER_FORMING_FIRST = "WaterBending:FirstWaterFormingPos" + private const val WATER_FORMING_SECOND = "WaterBending:SecondWaterFormingPos" + + var PlayerEntity.isWaterForming: Boolean + get() = this.getSyncedData(IS_WATER_FORMING) ?: false + set(value) = this.setSyncedData(IS_WATER_FORMING, value) + + var PlayerEntity.firstWaterFormingPos: BlockPos? + get() = this.getSyncedData(WATER_FORMING_FIRST) + set(value) = this.setSyncedData(WATER_FORMING_FIRST, value) + + var PlayerEntity.secondWaterFormingPos: BlockPos? + get() = this.getSyncedData(WATER_FORMING_SECOND) + set(value) = this.setSyncedData(WATER_FORMING_SECOND, value) +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterPillarAbility.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterPillarAbility.kt new file mode 100644 index 0000000..c1c5b09 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/ability/WaterPillarAbility.kt @@ -0,0 +1,430 @@ +package gg.norisk.heroes.katara.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.datatracker.entity.syncedValueChangeEvent +import gg.norisk.emote.ext.playEmote +import gg.norisk.emote.ext.stopEmote +import gg.norisk.emote.network.EmoteNetworking.playEmote +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.ability.CooldownProperty +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.ability.operation.MultiplyBase +import gg.norisk.heroes.common.command.DebugCommand.sendDebugMessage +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.ToggleAbility +import gg.norisk.heroes.common.utils.sound +import gg.norisk.heroes.common.utils.toBlockPos +import gg.norisk.heroes.katara.KataraManager.toEmote +import gg.norisk.heroes.katara.client.render.IFluidRendererExt +import gg.norisk.heroes.katara.client.sound.VelocityBasedFlyingSoundInstance +import gg.norisk.heroes.katara.entity.IWaterBendingPlayer +import gg.norisk.heroes.katara.event.FluidEvents +import gg.norisk.utils.OldAnimation +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents +import net.minecraft.block.BlockState +import net.minecraft.block.Blocks +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.AbstractClientPlayerEntity +import net.minecraft.client.render.RenderLayers +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.fluid.Fluids +import net.minecraft.item.Items +import net.minecraft.particle.ParticleTypes +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvents +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.RotationAxis +import net.minecraft.util.math.Vec3d +import net.minecraft.util.math.random.Random +import net.silkmc.silk.core.entity.modifyVelocity +import net.silkmc.silk.core.math.geometry.filledSpherePositionSet +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.literal +import org.joml.Quaternionf +import org.joml.Vector3f +import kotlin.math.* +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +object WaterPillarAbility { + private val animation by lazy { OldAnimation(0f, 360f, 0.6.seconds.toJavaDuration()) } + + fun initClient() { + WorldRenderEvents.BEFORE_DEBUG_RENDER.register { + val tickDelta = it.tickCounter().getTickDelta(false) + for (player in it.world().players) {/*renderBlock(it.matrixStack() ?: return@register, player.getLerpedPos(tickDelta), Blocks.DIRT.defaultState) + renderBlock( + it.matrixStack() ?: return@register, + player.getLerpedPos(tickDelta).add(1.0, 0.0, 0.0), + Fluids.WATER.defaultState.blockState, + true + )*/ + + val dummy = player as IWaterBendingPlayer + val origin = player.waterPillarOrigin ?: continue + val currentPos = player.getLerpedPos(tickDelta).add(0.0, 0.2, 0.0) + val positions = calculatePositionsBetween(origin, currentPos, 10) + val world = it.world() + + for ((index, position) in positions.withIndex()) { + val pos = position + renderBlock( + it.matrixStack() ?: return@register, + pos, + origin, + currentPos, + Fluids.WATER.defaultState.blockState, + true, + index = index + ) + if (kotlin.random.Random.nextInt(1, 11) > 9) { + val offset = 0.5 + val randomXOffset = kotlin.random.Random.nextDouble(-offset, offset) + val randomYOffset = kotlin.random.Random.nextDouble(-offset, offset) + val randomZOffset = kotlin.random.Random.nextDouble(-offset, offset) + //val particle = listOf(ParticleTypes.BUBBLE_POP, ParticleTypes.SPLASH) + world.addParticle( + ParticleTypes.SPLASH, + pos.x + randomXOffset, + pos.y + randomYOffset, + pos.z + randomZOffset, + 0.0, + 0.0, + 0.0 + ) + } + } + } + } + } + + fun renderBlock( + matrixStack: MatrixStack, + pos: Vec3d, + origin: BlockPos, + playerPos: Vec3d, + state: BlockState, + fluid: Boolean = false, + index: Int = 0, // Neuer Parameter für den Index + ) { + val camera = MinecraftClient.getInstance().gameRenderer.camera + val renderer = MinecraftClient.getInstance().blockRenderManager + val world = MinecraftClient.getInstance().world ?: return + val vertexConsumer = if (fluid) { + MinecraftClient.getInstance().bufferBuilders.entityVertexConsumers.getBuffer( + RenderLayers.getFluidLayer(state.fluidState) + ) + } else { + MinecraftClient.getInstance().bufferBuilders.entityVertexConsumers.getBuffer( + RenderLayers.getBlockLayer(state) + ) + } + + matrixStack.push() + matrixStack.translate( + pos.x - camera.pos.x, pos.y - camera.pos.y, pos.z - camera.pos.z + ) + + val scale = 2f / (Math.pow(1.1, index.toDouble())).toFloat() + matrixStack.scale(scale, scale, scale) + + val dx = origin.x - playerPos.x + val dz = origin.z - playerPos.z + val angle = Math.toDegrees(Math.atan2(dz, dx)).toFloat() // Berechne den Winkel + + matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(angle)) + //matrixStack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(angle)) + + // Rotation um die Y-Achse + matrixStack.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(animation.get())) + + + matrixStack.translate(-0.5, -0.5, -0.5) + + if (fluid) { + (renderer.fluidRenderer as IFluidRendererExt).katara_renderFluid( + matrixStack, world, pos, vertexConsumer, state, state.fluidState, null + ) + } else { + renderer.renderBlock(state, pos.toBlockPos(), world, matrixStack, vertexConsumer, true, Random.create()) + } + + matrixStack.pop() + + if (animation.isDone) { + animation.reset() + } + } + + + fun rotateTowards(from: Vec3d?, to: Vec3d, original: Quaternionf): Quaternionf { + val direction = to.subtract(from).normalize() + val forward = Vector3f(0f, 0f, -1f) + + val dir = Vector3f(direction.x.toFloat(), direction.y.toFloat(), direction.z.toFloat()) + val axis = Vector3f() + forward.cross(dir, axis).normalize() + + val dot = forward.dot(dir) + val angle = acos(max(-1.0, min(1.0, dot.toDouble()))).toFloat() + + /*if (dot < -0.9999f) { + axis[1f, 0f] = 0f + if (abs(forward.x.toDouble()) > 0.999f) axis[0f, 1f] = 0f + }*/ + + val rotationQuat = Quaternionf().fromAxisAngleRad(axis, angle) + return rotationQuat.mul(original) + } + + + fun initServer() { + syncedValueChangeEvent.listen { + if (it.key != WATER_PILLAR) return@listen + val player = it.entity as? PlayerEntity ?: return@listen + if (!player.world.isClient) { + if (player.isWaterPillar) {/*player.modifyVelocity(Vec3d(0.0, 1.3, 0.0)) + player.sound(SoundEvents.BLOCK_BUBBLE_COLUMN_UPWARDS_INSIDE, 0.3, 1f) + mcCoroutineTask(sync = true, client = false, delay = 0.5.seconds) { + //player.sendMessage("JETZT".literal) + player.abilities.allowFlying = true + player.abilities.flying = true + player.sendAbilitiesUpdate() + }*/ + } else { + (player as ServerPlayerEntity).cleanUpWaterBendingBlocks() + } + } else { + if (player.isWaterPillar) { + (player as AbstractClientPlayerEntity).playEmote("waterpillar".toEmote()) + MinecraftClient.getInstance().soundManager.play(VelocityBasedFlyingSoundInstance(player) { entity -> + (entity as? PlayerEntity?)?.isWaterPillar == true + }) + } else { + (player as AbstractClientPlayerEntity).stopEmote("waterpillar".toEmote()) + } + } + } + + FluidEvents.fluidTickEvent.listen { event -> + for (player in event.world.players.filter { it.isWaterPillar }) { + val dummy = player as IWaterBendingPlayer + if (player.katara_waterPillarBlocks.contains(event.blockPos)) { + event.isCancelled.set(true) + return@listen + } + } + } + } + + private const val WATER_PILLAR = "WaterBending:WaterPillar" + private const val WATER_PILLAR_ORIGIN = "WaterBending:WaterPillarOrigin" + + private var PlayerEntity.isWaterPillar: Boolean + get() = this.getSyncedData(WATER_PILLAR) ?: false + set(value) = this.setSyncedData(WATER_PILLAR, value) + + private var PlayerEntity.waterPillarOrigin: BlockPos? + get() = this.getSyncedData(WATER_PILLAR_ORIGIN) + set(value) = this.setSyncedData(WATER_PILLAR_ORIGIN, value) + + private fun ServerPlayerEntity.cleanUpWaterBendingBlocks() { + val dummy = this as IWaterBendingPlayer + for (pos in this.katara_waterPillarBlocks) { + if (world.getBlockState(pos).isOf(Blocks.WATER)) { + world.setBlockState(pos, Blocks.AIR.defaultState) + } + } + sound(SoundEvents.ENTITY_GENERIC_SPLASH, 1f, 1f) + katara_waterPillarBlocks.clear() + + if (interactionManager.gameMode.isSurvivalLike) { + abilities.allowFlying = false + abilities.flying = false + sendAbilitiesUpdate() + } + (this as IWaterBendingPlayer).katara_waterPillarOrigin = null + } + + private fun calculatePositionsBetween(start: BlockPos, end: Vec3d, steps: Int = 10): List { + val positions = mutableSetOf() + + val controlPoint = Vec3d( + (start.x + end.x) / 2.0, + (start.y + end.y) / 2.0 + 5, // Raise the control point to create a curve + (start.z + end.z) / 2.0 + ) + + for (i in 0..steps) { + val t = i / steps.toDouble() + val oneMinusT = 1 - t + + val x = (oneMinusT.pow(2) * start.x) + (2 * oneMinusT * t * controlPoint.x) + (t.pow(2) * end.x) + val y = (oneMinusT.pow(2) * start.y) + (2 * oneMinusT * t * controlPoint.y) + (t.pow(2) * end.y) + val z = (oneMinusT.pow(2) * start.z) + (2 * oneMinusT * t * controlPoint.z) + (t.pow(2) * end.z) + + positions.add(Vec3d(x, y, z)) + } + + return positions.toList() + } + + val waterPillarDistance = NumberProperty(15.0, 5, "Water Pillar Distance", AddValueTotal(3.0,3.0,3.0,3.0,3.0)).apply { + icon = { + Components.item(Items.SPYGLASS.defaultStack) + } + } + val waterPillarVelocityBoost = NumberProperty(1.0, 5, "Water Pillar Start Boost", MultiplyBase(1.0, 1.2, 1.4, 1.5, 1.8, 1.9)).apply { + icon = { + Components.item(Items.FIREWORK_ROCKET.defaultStack) + } + } + + val ability = object : ToggleAbility("Water Pillar") { + init { + HeroesManager.client { + this.keyBind = HeroKeyBindings.fourthKeyBinding + } + this.condition = { + it.isTouchingWater + } + this.properties = listOf( + waterPillarDistance, + waterPillarVelocityBoost, + ) + this.cooldownProperty = + CooldownProperty(20.0, 4, "Cooldown", AddValueTotal(-5.0, -5.0, -2.0, -3.0)) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_ice.png") + } + + override fun getIconComponent(): Component { + return Components.item(Items.WATER_BUCKET.defaultStack) + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + if (player is ServerPlayerEntity) { + player.playEmote("waterpillar_start".toEmote()) + mcCoroutineTask(sync = true, client = false, delay = 0.5.seconds) { + player.waterPillarOrigin = player.blockPos + (player as IWaterBendingPlayer).katara_waterPillarOrigin = player.blockPos + player.isWaterPillar = true + player.abilities.allowFlying = true + player.abilities.flying = true + player.modifyVelocity(Vec3d(0.0, waterPillarVelocityBoost.getValue(player.uuid), 0.0)) + player.sound(SoundEvents.BLOCK_BUBBLE_COLUMN_UPWARDS_INSIDE, 0.3, 1f) + mcCoroutineTask(sync = true, client = false, delay = 0.5.seconds) { + player.sendAbilitiesUpdate() + } + } + } + } + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + cleanUp(player) + } + + fun cleanUp(player: PlayerEntity) { + if (player is ServerPlayerEntity) { + if (player.isWaterPillar) { + player.waterPillarOrigin = null + player.isWaterPillar = false + player.cleanUpWaterBendingBlocks() + } + } + } + + override fun onEnd(player: PlayerEntity, abilityEndInformation: AbilityEndInformation) { + cleanUp(player) + } + + override fun onTick(player: PlayerEntity) { + if (player is ServerPlayerEntity && player.isWaterPillar) { + val dummy = player as IWaterBendingPlayer + val otherPos = mutableSetOf() + val origin = player.waterPillarOrigin ?: return + for (vec3d in calculatePositionsBetween(origin, player.pos)) { + for (blockPos in vec3d.toBlockPos().filledSpherePositionSet(3)) { + otherPos.add(blockPos) + } + } + val distance = sqrt(origin.getSquaredDistance(player.pos)) + val maxDistance = waterPillarDistance.getValue(player.uuid) + + if (distance >= maxDistance) { + player.sendMessage(Text.translatable("heroes.katara.ability.water_pillar.too_far_away")) + player.sendDebugMessage("Max Distance: ${maxDistance}".literal) + player.isWaterPillar = false + player.waterPillarOrigin = null + addCooldown(player) + return + } + + val world = player.world + for (freePos in otherPos) { + if (kotlin.random.Random.nextInt(1, 1000) > 960) { + if (world.getBlockState(freePos) + .isOf(Blocks.AIR) && world.getBlockState(freePos.down()).isSolid + ) { + world.setBlockState(freePos, Fluids.WATER.getFlowing(1, true).blockState) + world.playSound( + null, + freePos, + SoundEvents.ENTITY_GENERIC_SPLASH, + SoundCategory.BLOCKS, + 0.3f, + kotlin.random.Random.nextDouble(0.8, 1.3).toFloat() + ) + } + } + } + } else { + val dummy = player as IWaterBendingPlayer + val origin = player.waterPillarOrigin ?: return + + repeat(1) { + player.world.addParticle( + ParticleTypes.BUBBLE_POP, + player.getParticleX(0.5), + player.randomBodyY, + player.getParticleZ(0.5), + 0.0, + 0.0, + 0.0 + ) + } + + if (player.age.mod(10) == 0) { + for (vec3d in calculatePositionsBetween( + origin, + player.getLerpedPos(MinecraftClient.getInstance().renderTickCounter.getTickDelta(false)), + 10 + )) { + player.world.playSound( + MinecraftClient.getInstance().player, + vec3d.x, + vec3d.y, + vec3d.z, + SoundEvents.BLOCK_WATER_AMBIENT, + SoundCategory.BLOCKS, + 0.5f, + kotlin.random.Random.nextDouble(0.5, 1.1).toFloat() + ) + } + } + } + } + } +} diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/HealingWaterFeatureRenderer.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/HealingWaterFeatureRenderer.kt new file mode 100644 index 0000000..abbac04 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/HealingWaterFeatureRenderer.kt @@ -0,0 +1,81 @@ +package gg.norisk.heroes.katara.client.render + +import com.mojang.blaze3d.systems.RenderSystem +import gg.norisk.heroes.katara.KataraManager.toId +import gg.norisk.heroes.katara.ability.HealingAbility.isReceivingWaterHealing +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.* +import net.minecraft.client.render.entity.feature.FeatureRenderer +import net.minecraft.client.render.entity.feature.FeatureRendererContext +import net.minecraft.client.render.entity.model.EntityModel +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.Entity +import net.minecraft.entity.LivingEntity +import net.minecraft.util.Identifier +import net.minecraft.util.Util +import org.joml.Matrix4f + +@Environment(value = EnvType.CLIENT) +class HealingWaterFeatureRenderer>(featureRendererContext: FeatureRendererContext) : + FeatureRenderer(featureRendererContext) { + companion object { + val healingWaterTexture = "textures/overlay/healing_water.png".toId() + + val LAYER: RenderLayer = RenderLayer.of( + "katara_healing_overlay", + VertexFormats.POSITION_TEXTURE, + VertexFormat.DrawMode.QUADS, + 1536, + RenderLayer.MultiPhaseParameters.builder() + .program(RenderPhase.ENTITY_GLINT_PROGRAM) + .texture( + RenderPhase.Texture( + Identifier.ofVanilla("textures/entity/creeper/creeper_armor.png"), + true, + false + ) + ) + .writeMaskState(RenderPhase.COLOR_MASK) + .cull(RenderPhase.DISABLE_CULLING) + .depthTest(RenderPhase.EQUAL_DEPTH_TEST) + .transparency(RenderPhase.GLINT_TRANSPARENCY) + .target(RenderPhase.ITEM_ENTITY_TARGET) + .texturing(RenderPhase.ENTITY_GLINT_TEXTURING) + .build(false) + ) + + private fun setupOverlayTexture() { + val l = (Util.getMeasuringTimeMs() + .toDouble() * MinecraftClient.getInstance().options.glintSpeed.value * 8.0).toLong() + val f = (l % 110000L).toFloat() / 110000.0f + val g = (l % 30000L).toFloat() / 30000.0f + RenderSystem.setTextureMatrix(Matrix4f().translation(f, g, 0.0f)) + } + } + + override fun render( + matrixStack: MatrixStack, + vertexConsumerProvider: VertexConsumerProvider, + i: Int, + entity: T, + f: Float, + g: Float, + h: Float, + j: Float, + k: Float, + l: Float + ) { + val livingEntity = entity as? LivingEntity? ?: return + if (livingEntity.isReceivingWaterHealing) { + val m = (entity as Entity).age.toFloat() + h + val entityModel: EntityModel = this.contextModel + entityModel.animateModel(entity, f, g, h) + this.contextModel.copyStateTo(entityModel) + val vertexConsumer = vertexConsumerProvider.getBuffer(LAYER) + entityModel.setAngles(entity, f, g, j, k, l) + entityModel.render(matrixStack, vertexConsumer, i, OverlayTexture.DEFAULT_UV) + } + } +} diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/IFluidRendererExt.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/IFluidRendererExt.kt new file mode 100644 index 0000000..430300b --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/IFluidRendererExt.kt @@ -0,0 +1,21 @@ +package gg.norisk.heroes.katara.client.render + +import net.minecraft.block.BlockState +import net.minecraft.client.render.VertexConsumer +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.fluid.FluidState +import net.minecraft.util.math.Vec3d +import net.minecraft.world.BlockRenderView +import java.awt.Color + +interface IFluidRendererExt { + fun katara_renderFluid( + matrixStack: MatrixStack, + blockRenderView: BlockRenderView, + post: Vec3d, + vertexConsumer: VertexConsumer, + blockState: BlockState, + fluidState: FluidState, + waterColor: Color? + ) +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/IceShardEntityRenderer.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/IceShardEntityRenderer.kt new file mode 100644 index 0000000..60efd65 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/IceShardEntityRenderer.kt @@ -0,0 +1,21 @@ +package gg.norisk.heroes.katara.client.render + +import gg.norisk.heroes.katara.KataraManager.toId +import gg.norisk.heroes.katara.entity.IceShardEntity +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.render.entity.EntityRendererFactory +import net.minecraft.client.render.entity.ProjectileEntityRenderer +import net.minecraft.util.Identifier + +@Environment(EnvType.CLIENT) +class IceShardEntityRenderer(context: EntityRendererFactory.Context) : + ProjectileEntityRenderer(context) { + override fun getTexture(entity: IceShardEntity): Identifier { + return TEXTURE + } + + companion object { + val TEXTURE: Identifier = "textures/entity/projectiles/ice_shard.png".toId() + } +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/WaterBendingEntityModel.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/WaterBendingEntityModel.kt new file mode 100644 index 0000000..0e3cde3 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/WaterBendingEntityModel.kt @@ -0,0 +1,25 @@ +package gg.norisk.heroes.katara.client.render + +import gg.norisk.heroes.katara.entity.WaterBendingEntity +import net.minecraft.client.model.ModelData +import net.minecraft.client.model.ModelPart +import net.minecraft.client.model.TexturedModelData +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.entity.model.SinglePartEntityModel + +class WaterBendingEntityModel(val modelPart: ModelPart) : + SinglePartEntityModel(RenderLayer::getEntityTranslucent) { + override fun setAngles(entity: WaterBendingEntity?, f: Float, g: Float, h: Float, i: Float, j: Float) { + } + + companion object { + fun getTexturedModelData(): TexturedModelData { + val modelData = ModelData() + return TexturedModelData.of(modelData, 64, 32) + } + } + + override fun getPart(): ModelPart { + return modelPart + } +} diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/WaterBendingEntityRenderer.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/WaterBendingEntityRenderer.kt new file mode 100644 index 0000000..45d3fe3 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/render/WaterBendingEntityRenderer.kt @@ -0,0 +1,123 @@ +package gg.norisk.heroes.katara.client.render + +import gg.norisk.heroes.katara.ability.WaterPillarAbility +import gg.norisk.heroes.katara.entity.WaterBendingEntity +import gg.norisk.utils.OldAnimation +import net.minecraft.block.BlockState +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.RenderLayers +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.entity.EntityRendererFactory +import net.minecraft.client.render.entity.LivingEntityRenderer +import net.minecraft.client.render.entity.model.EntityModelLayers +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.fluid.Fluids +import net.minecraft.util.Identifier +import net.minecraft.util.math.Vec3d +import org.joml.Quaternionf +import java.awt.Color +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +class WaterBendingEntityRenderer(context: EntityRendererFactory.Context) : + LivingEntityRenderer( + context, + //doesnt matter + WaterBendingEntityModel(context.getPart(EntityModelLayers.PIG)), + 0f + ) { + + override fun render( + livingEntity: WaterBendingEntity, + f: Float, + g: Float, + matrixStack: MatrixStack, + vertexConsumerProvider: VertexConsumerProvider, + i: Int + ) { + val owner = livingEntity.getOwner() + renderWater( + matrixStack, + livingEntity.pos, + Fluids.FLOWING_WATER.defaultState.blockState, + OldAnimation(0.5f, 1f, 1.seconds.toJavaDuration()), + 0, + owner, + f, + livingEntity + ) + for (position in livingEntity.positions) { + matrixStack.push() + val difference = position.pos.subtract(livingEntity.getLerpedPos(g)) + matrixStack.translate(difference.x, difference.y, difference.z) + renderWater( + matrixStack, + position.pos, + Fluids.FLOWING_WATER.defaultState.blockState, + position.animation, + position.startTime, + owner, f, livingEntity + ) + matrixStack.pop() + } + // super.render(livingEntity, f, g, matrixStack, vertexConsumerProvider, i) + } + + fun renderWater( + matrixStack: MatrixStack, + pos: Vec3d, + state: BlockState, + animation: OldAnimation, + index: Int, + owner: PlayerEntity?, + tickDelta: Float, + waterBendingEntity: WaterBendingEntity, + ) { + val renderer = MinecraftClient.getInstance().blockRenderManager + val world = MinecraftClient.getInstance().world ?: return + val vertexConsumer = MinecraftClient.getInstance().bufferBuilders.entityVertexConsumers.getBuffer( + RenderLayers.getFluidLayer(state.fluidState) + ) + + matrixStack.push() + // Berechne den animierten Sinus-Offset basierend auf tickDelta und index + // Berechne den animierten Sinus-Offset basierend auf tickDelta und index + val timeFactor = (System.currentTimeMillis() % 10000L) / 1000.0 // Zeit in Sekunden (loop alle 10 Sekunden) + val sineOffset = Math.sin((timeFactor + index * 0.5) * Math.PI) * 0.05 // Wellenbewegung mit tickDelta animiert + + var scale = 0.5f + matrixStack.scale(scale, scale, scale) + matrixStack.scale(animation.get(), animation.get(), animation.get()) + owner?.apply { + matrixStack.multiply( + WaterPillarAbility.rotateTowards( + pos, this.getLerpedPos(tickDelta), + Quaternionf() + ) + ) + } + //matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(90f)) + matrixStack.translate(-0.5, -0.5 + sineOffset, -0.5) + + (renderer.fluidRenderer as IFluidRendererExt).katara_renderFluid( + matrixStack, + world, + pos, + vertexConsumer, + state, + state.fluidState, + if (waterBendingEntity.isHealing) { + Color.decode("#8aefff") + } else { + null + } + ) + + matrixStack.pop() + } + + override fun getTexture(entity: WaterBendingEntity): Identifier { + return MinecraftClient.getInstance().player!!.skinTextures.texture + } +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/VelocityBasedFlyingSoundInstance.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/VelocityBasedFlyingSoundInstance.kt new file mode 100644 index 0000000..93aaf1a --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/VelocityBasedFlyingSoundInstance.kt @@ -0,0 +1,30 @@ +package gg.norisk.heroes.katara.client.sound + +import gg.norisk.heroes.common.registry.SoundRegistry +import net.minecraft.client.sound.MovingSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.entity.Entity +import net.minecraft.sound.SoundCategory + +class VelocityBasedFlyingSoundInstance(private val entity: Entity, val condition: (Entity) -> Boolean) : + MovingSoundInstance(SoundRegistry.FLYING, SoundCategory.NEUTRAL, SoundInstance.createRandom()) { + + init { + this.repeat = true + this.repeatDelay = 0 + this.volume = 0.3f + } + + override fun tick() { + if (!entity.isRemoved && condition.invoke(entity)) { + this.x = entity.x.toFloat().toDouble() + this.y = entity.y.toFloat().toDouble() + this.z = entity.z.toFloat().toDouble() + val f: Float = Math.min(0.3f, Math.max(0.01f, this.entity.velocity.lengthSquared().toFloat())) + this.volume = f + this.pitch = 1f + this.volume + } else { + this.setDone() + } + } +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/WaterCircleSoundInstance.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/WaterCircleSoundInstance.kt new file mode 100644 index 0000000..e6ea431 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/WaterCircleSoundInstance.kt @@ -0,0 +1,41 @@ +package gg.norisk.heroes.katara.client.sound + +import net.minecraft.client.sound.MovingSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.entity.Entity +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvents + +class WaterCircleSoundInstance(private val entity: Entity, val condition: (Entity) -> Boolean) : + MovingSoundInstance(SoundEvents.BLOCK_WATER_AMBIENT, SoundCategory.NEUTRAL, SoundInstance.createRandom()) { + var fadeTime = 20 + var isFading = false + + init { + this.repeat = true + this.repeatDelay = 0 + this.volume = 0.01f + } + + override fun tick() { + if (isFading) { + --fadeTime + this.volume *= 0.9f + if (fadeTime < 0) { + this.setDone() + return + } + } + + this.x = entity.x.toFloat().toDouble() + this.y = entity.y.toFloat().toDouble() + this.z = entity.z.toFloat().toDouble() + + if (!entity.isRemoved && condition.invoke(entity)) { + this.volume = 0.7f + this.pitch = 0.5f + } else { + isFading = true + } + } +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/WaterHealingSoundInstance.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/WaterHealingSoundInstance.kt new file mode 100644 index 0000000..ed5a1f2 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/WaterHealingSoundInstance.kt @@ -0,0 +1,41 @@ +package gg.norisk.heroes.katara.client.sound + +import net.minecraft.client.sound.MovingSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.entity.Entity +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvents + +class WaterHealingSoundInstance(private val entity: Entity, val condition: (Entity) -> Boolean) : + MovingSoundInstance(SoundEvents.BLOCK_BEACON_ACTIVATE, SoundCategory.NEUTRAL, SoundInstance.createRandom()) { + var fadeTime = 20 + var isFading = false + + init { + this.repeat = true + this.repeatDelay = 0 + this.volume = 0.01f + } + + override fun tick() { + if (isFading) { + --fadeTime + this.volume *= 0.9f + if (fadeTime < 0) { + this.setDone() + return + } + } + + this.x = entity.x.toFloat().toDouble() + this.y = entity.y.toFloat().toDouble() + this.z = entity.z.toFloat().toDouble() + + if (!entity.isRemoved && condition.invoke(entity)) { + this.volume = 0.4f + this.pitch = 0.5f + } else { + isFading = true + } + } +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/WaterSelectingSoundInstance.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/WaterSelectingSoundInstance.kt new file mode 100644 index 0000000..cde2561 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/client/sound/WaterSelectingSoundInstance.kt @@ -0,0 +1,41 @@ +package gg.norisk.heroes.katara.client.sound + +import net.minecraft.client.sound.MovingSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.entity.Entity +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvents + +class WaterSelectingSoundInstance(private val entity: Entity, val condition: (Entity) -> Boolean) : + MovingSoundInstance(SoundEvents.BLOCK_WATER_AMBIENT, SoundCategory.NEUTRAL, SoundInstance.createRandom()) { + var fadeTime = 20 + var isFading = false + + init { + this.repeat = true + this.repeatDelay = 0 + this.volume = 0.01f + } + + override fun tick() { + if (isFading) { + --fadeTime + this.volume *= 0.9f + if (fadeTime < 0) { + this.setDone() + return + } + } + + this.x = entity.x.toFloat().toDouble() + this.y = entity.y.toFloat().toDouble() + this.z = entity.z.toFloat().toDouble() + + if (!entity.isRemoved && condition.invoke(entity)) { + this.volume = 0.7f + this.pitch = 0.5f + } else { + isFading = true + } + } +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/IKataraEntity.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/IKataraEntity.kt new file mode 100644 index 0000000..7c0097b --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/IKataraEntity.kt @@ -0,0 +1,11 @@ +package gg.norisk.heroes.katara.entity + +import gg.norisk.heroes.katara.utils.EntityCircleTracker +import gg.norisk.heroes.katara.utils.EntitySpinTracker +import kotlinx.coroutines.Job + +interface IKataraEntity { + var katara_waterHealingJob: Job? + val katara_entitySpinTracker: EntitySpinTracker + val katara_entityCircleTracker: EntityCircleTracker +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/IWaterBendingPlayer.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/IWaterBendingPlayer.kt new file mode 100644 index 0000000..e913444 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/IWaterBendingPlayer.kt @@ -0,0 +1,8 @@ +package gg.norisk.heroes.katara.entity + +import net.minecraft.util.math.BlockPos + +interface IWaterBendingPlayer { + val katara_waterPillarBlocks: MutableSet + var katara_waterPillarOrigin: BlockPos? +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/IceShardEntity.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/IceShardEntity.kt new file mode 100644 index 0000000..d4a4ef8 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/IceShardEntity.kt @@ -0,0 +1,35 @@ +package gg.norisk.heroes.katara.entity + +import gg.norisk.heroes.katara.registry.SoundRegistry +import net.minecraft.entity.EntityType +import net.minecraft.entity.projectile.PersistentProjectileEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.particle.ParticleTypes +import net.minecraft.sound.SoundEvent +import net.minecraft.world.World +import kotlin.random.Random + +class IceShardEntity(entityType: EntityType, world: World) : + PersistentProjectileEntity(entityType, world) { + + override fun getDefaultItemStack(): ItemStack { + return ItemStack(Items.ICE); + } + + + override fun tick() { + super.tick() + if (!inGround) { + if (world.isClient) { + if (Random.nextBoolean() && Random.nextBoolean()) { + world.addParticle(ParticleTypes.SNOWFLAKE, this.x, this.y, this.z, 0.0, 0.0, 0.0) + } + } + } + } + + override fun getHitSound(): SoundEvent { + return SoundRegistry.ICE_PLACE + } +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/WaterBendingEntity.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/WaterBendingEntity.kt new file mode 100644 index 0000000..cfa1b97 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/entity/WaterBendingEntity.kt @@ -0,0 +1,364 @@ +package gg.norisk.heroes.katara.entity + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.heroes.common.utils.sound +import gg.norisk.heroes.common.utils.toBlockPos +import gg.norisk.heroes.common.utils.toVec +import gg.norisk.heroes.katara.ability.HealingAbility.WaterRender +import gg.norisk.heroes.katara.ability.HealingAbility.counter +import gg.norisk.heroes.katara.ability.HealingAbility.getWaterBendingPos +import gg.norisk.heroes.katara.ability.HealingAbility.handleWaterHealing +import gg.norisk.heroes.katara.ability.WaterBendingAbility.waterBendingDistance +import gg.norisk.heroes.katara.ability.WaterFormingAbility.firstWaterFormingPos +import gg.norisk.heroes.katara.ability.WaterFormingAbility.secondWaterFormingPos +import gg.norisk.utils.Easing +import gg.norisk.utils.OldAnimation +import net.minecraft.block.Blocks +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.damage.DamageSource +import net.minecraft.entity.damage.DamageTypes +import net.minecraft.entity.mob.PathAwareEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.fluid.Fluids +import net.minecraft.particle.ParticleTypes +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvents +import net.minecraft.util.math.MathHelper +import net.minecraft.util.math.Vec3d +import net.minecraft.world.World +import net.silkmc.silk.core.entity.modifyVelocity +import net.silkmc.silk.core.kotlin.ticks +import net.silkmc.silk.core.task.mcCoroutineTask +import kotlin.math.sqrt +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +class WaterBendingEntity(entityType: EntityType, world: World) : + PathAwareEntity(entityType, world) { + init { + this.ignoreCameraFrustum = true + //this.getAttributeInstance(EntityAttributes.GENERIC_GRAVITY)?.baseValue = 0.02 + } + + var positions: MutableList = mutableListOf() + var wasLaunched: Boolean + get() = this.getSyncedData("WaterBendingEntityWasLaunched") ?: false + set(value) { + this.setSyncedData("WaterBendingEntityWasLaunched", value) + } + var isGettingRemoved = false + var wasDropped: Boolean + get() = this.getSyncedData("WaterBendingEntityWasDropped") ?: false + set(value) { + this.setSyncedData("WaterBendingEntityWasDropped", value) + } + + var isInitial: Boolean + get() = this.getSyncedData("WaterBendingIsInitial") ?: false + set(value) { + this.setSyncedData("WaterBendingIsInitial", value) + } + var initialCounter = 0; + + var isHealing: Boolean + get() = this.getSyncedData("WaterBendingIsHealing") ?: false + set(value) { + this.setSyncedData("WaterBendingIsHealing", value) + } + + var ownerId: Int + get() = this.getSyncedData("WaterBendingEntityOwnerId") ?: -1 + set(value) { + this.setSyncedData("WaterBendingEntityOwnerId", value) + } + + fun getOwner(): PlayerEntity? { + val id = if (ownerId != -1) ownerId else return null + return world.getEntityById(id) as? PlayerEntity? + } + + override fun tick() { + super.tick() + val owner = getOwner() + + + if (!world.isClient) { + if ((wasLaunched || wasDropped) && (horizontalCollision || verticalCollision || isTouchingWater || world.getOtherEntities( + this, + this.boundingBox + ) { it.isAlive && !it.isSpectator }.isNotEmpty()) && !isGettingRemoved + ) { + isGettingRemoved = true + + if (isHealing) { + for (otherEntity in world.getOtherEntities( + this, + boundingBox.expand(2.0) + ) { it.isAlive && !it.isSpectator }) { + if (owner != null) { + otherEntity.handleWaterHealing(owner) + } + } + } else { + for (otherEntity in world.getOtherEntities( + this, + boundingBox.expand(2.89) + ) { it.isAlive && !it.isSpectator && it.canHit() && it != owner && it !is WaterBendingEntity }) { + otherEntity.damage(this.damageSources.playerAttack(owner), 4f) + (otherEntity as? LivingEntity?)?.takeKnockback(1.1, Random.nextDouble(), Random.nextDouble()) + } + } + + if (wasDropped) { + val pos = blockPos + world.setBlockState(pos, Fluids.WATER.getStill(false).blockState) + mcCoroutineTask(sync = true, delay = 6.ticks) { + if (world.getBlockState(pos).isOf(Blocks.WATER)) { + world.setBlockState(pos, Blocks.AIR.defaultState) + } + } + } + + repeat(20) { + (world as ServerWorld).spawnParticles( + ParticleTypes.SPLASH, + getParticleX(0.5) + Random.nextDouble(-1.0, 1.0), + randomBodyY + Random.nextDouble(-1.0, 1.0), + getParticleZ(0.5) + Random.nextDouble(-1.0, 1.0), + 50, + 0.001, + 0.001, + 0.001, + 0.0 + ) + } + world.playSound( + null, + pos.x, + pos.y, + pos.z, + SoundEvents.ENTITY_GENERIC_SPLASH, + SoundCategory.BLOCKS, + 1f, Random.nextDouble(1.5, 2.0).toFloat(), + ) + mcCoroutineTask(delay = 2.ticks, sync = true) { + discard() + } + } + sound( + SoundEvents.ENTITY_BOAT_PADDLE_WATER, + 0.1f, + Random.nextDouble(1.9, 2.0).toFloat(), + ) + } + + if (isInitial && owner != null) { + val distanceToPlayer = this.squaredDistanceTo(owner) + val distance = owner.waterBendingDistance + + if (distanceToPlayer >= (distance * distance) + sqrt(distance) * 2) { + modifyVelocity(owner.getWaterBendingPos(distance).subtract(this.pos).normalize().multiply(1.0)) + } else { + initialCounter++ + if (initialCounter >= 5) { + isInitial = false + } + } + } + + if (!(wasLaunched || wasDropped) && !isInitial) { + owner?.apply { + val waterFormingPos = owner.secondWaterFormingPos ?: owner.firstWaterFormingPos + if (waterFormingPos != null) { + this@WaterBendingEntity.modifyVelocity( + waterFormingPos.toVec().subtract(this@WaterBendingEntity.pos).normalize().multiply(1.0) + ) + } else { + this@WaterBendingEntity.setPosition(this.getWaterBendingPos()) + } + } + } + + tickTrail(owner) + } + + fun freeze() { + for (position in positions.takeLast(10)) { + val pos = position.pos.toBlockPos() + val currentState = world.getBlockState(pos) + if (world.getBlockState(pos).isOf(Blocks.WATER) || world.getBlockState(pos).isAir) { + world.setBlockState(position.pos.toBlockPos(), Blocks.ICE.defaultState) + (world as? ServerWorld?)?.spawnParticles( + ParticleTypes.CLOUD, + pos.x + kotlin.random.Random.nextDouble(-1.0, 1.0), + pos.y + kotlin.random.Random.nextDouble(-1.0, 1.0), + pos.z + kotlin.random.Random.nextDouble(-1.0, 1.0), + 1, + 0.0, + 0.0, + 0.0, + 0.0 + ) + } + } + discard() + } + + fun tickTrail(owner: PlayerEntity?, tickDelta: Float = 1f) { + if (owner != null || wasLaunched || wasDropped) { + //println("Was Launched $wasLaunched") + if (positions.size > 30) { + positions.removeFirstOrNull() + /* val first = positions.getOrNull(0) + if (first?.animation?.end != 0f) { + first?.animation = OldAnimation(1f, 0.0f, 0.1.seconds.toJavaDuration()) + } + if (first?.animation?.isDone == true) { + } */ + } + + val lastPos = positions.lastOrNull() + + val pos = if (wasLaunched || wasDropped || isInitial || (owner?.secondWaterFormingPos + ?: owner?.firstWaterFormingPos) != null + ) getLerpedPos( + tickDelta + ) else owner!!.getWaterBendingPos() + + //TODO der check könnte für probleme sorgen + for (otherEntity in world.getOtherEntities(owner, this.boundingBox.expand(1.5)) { + it !is PlayerEntity && it !is WaterBendingEntity + }) { + val distance = otherEntity.distanceTo(this) + val speed = if (distance <= 0.43) { + 0.05 + } else { + 0.2 + } + otherEntity.modifyVelocity(this.pos.subtract(otherEntity.pos).normalize().multiply(speed)) + + /*otherEntity.teleport( + otherEntity.world as ServerWorld, + this.x, + this.y, + this.z, + PositionFlag.VALUES, + otherEntity.yaw, + otherEntity.pitch + )*/ + } + + //println("${lastPos?.distanceTo(pos)}") + if ((lastPos?.pos?.distanceTo(pos) ?: 10000.0) >= 0.2) { + positions += WaterRender( + pos, + OldAnimation(0.7f, 1f, 0.1.seconds.toJavaDuration(), Easing.LINEAR), + counter++ + ) + if (world.isClient) { + world.addParticle( + ParticleTypes.SPLASH, + pos.x, + pos.y, + pos.z, + 0.0, + 0.0, + 0.0 + ) + } else { + world.playSound( + null, + pos.x, + pos.y, + pos.z, + SoundEvents.ENTITY_BOAT_PADDLE_WATER, + SoundCategory.BLOCKS, + 0.3f, Random.nextDouble(1.5, 2.0).toFloat(), + ) + } + } + } + } + + // Apply player-controlled movement + override fun travel(pos: Vec3d) { + if (!wasDropped) { + this.setNoDrag(true) + this.setNoGravity(true) + } + super.travel(pos) + } + + fun launch(owner: LivingEntity) { + if (!wasLaunched && !isInitial && !wasDropped) { + wasLaunched = true + owner.sound(SoundEvents.ENTITY_GENERIC_SPLASH, 0.6f, Random.nextDouble(1.5, 2.0)) + sound(SoundEvents.ENTITY_GENERIC_SPLASH, 1f, Random.nextDouble(1.5, 2.0)) + setVelocity(owner, owner.pitch, owner.yaw, 0.0f, 2f, 1.0f) + } + } + + fun drop(owner: LivingEntity) { + if (!wasDropped && !isInitial) { + wasDropped = true + owner.sound(SoundEvents.ENTITY_GENERIC_SPLASH, 0.6f, Random.nextDouble(1.5, 2.0)) + sound(SoundEvents.ENTITY_GENERIC_SPLASH, 1f, Random.nextDouble(1.5, 2.0)) + modifyVelocity(Vec3d(0.0, -0.6, 0.0)) + } + } + + override fun damage(damageSource: DamageSource, f: Float): Boolean { + if (damageSource.isOf(DamageTypes.GENERIC_KILL)) { + return super.damage(damageSource, f) + } + val attacker = damageSource.attacker as? LivingEntity ?: return false + if (attacker.id == ownerId) { + launch(attacker) + return false + } else { + this.discard() + //TODO + } + return false + } + + fun calculateVelocity(d: Double, e: Double, f: Double, g: Float, h: Float): Vec3d { + return Vec3d(d, e, f) + .normalize() + .add( + random.nextTriangular(0.0, 0.0172275 * h.toDouble()), + random.nextTriangular(0.0, 0.0172275 * h.toDouble()), + random.nextTriangular(0.0, 0.0172275 * h.toDouble()) + ) + .multiply(g.toDouble()) + } + + fun setVelocity(entity: Entity, f: Float, g: Float, h: Float, i: Float, j: Float) { + val k = -MathHelper.sin(g * (Math.PI / 180.0).toFloat()) * MathHelper.cos(f * (Math.PI / 180.0).toFloat()) + val l = -MathHelper.sin((f + h) * (Math.PI / 180.0).toFloat()) + val m = MathHelper.cos(g * (Math.PI / 180.0).toFloat()) * MathHelper.cos(f * (Math.PI / 180.0).toFloat()) + this.setVelocity(k.toDouble(), l.toDouble(), m.toDouble(), i, j) + val vec3d = entity.movement + this.velocity = velocity.add(vec3d.x, if (entity.isOnGround) 0.0 else vec3d.y, vec3d.z) + } + + fun setVelocity(d: Double, e: Double, f: Double, g: Float, h: Float) { + val vec3d: Vec3d = this.calculateVelocity(d, e, f, g, h) + this.velocity = vec3d + this.velocityDirty = true + val i = vec3d.horizontalLength() + this.yaw = (MathHelper.atan2(vec3d.x, vec3d.z) * 180.0f / Math.PI.toFloat()).toFloat() + this.pitch = (MathHelper.atan2(vec3d.y, i) * 180.0f / Math.PI.toFloat()).toFloat() + this.prevYaw = this.yaw + this.prevPitch = this.pitch + } + + override fun handleFallDamage(f: Float, g: Float, damageSource: DamageSource?): Boolean { + return false + } +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/event/FluidEvents.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/event/FluidEvents.kt new file mode 100644 index 0000000..a970a36 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/event/FluidEvents.kt @@ -0,0 +1,20 @@ +package gg.norisk.heroes.katara.event + +import net.minecraft.fluid.FluidState +import net.minecraft.state.property.BooleanProperty +import net.minecraft.util.math.BlockPos +import net.minecraft.world.World +import net.silkmc.silk.core.event.Cancellable +import net.silkmc.silk.core.event.Event +import net.silkmc.silk.core.event.EventScopeProperty + +object FluidEvents { + val static: BooleanProperty = BooleanProperty.of("static") + + + class FluidEvent(val world: World, val blockPos: BlockPos, val fluidState: FluidState) : Cancellable { + override val isCancelled = EventScopeProperty(false) + } + + val fluidTickEvent = Event.onlySync() +} \ No newline at end of file diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/registry/EntityRegistry.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/registry/EntityRegistry.kt new file mode 100644 index 0000000..a0143a4 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/registry/EntityRegistry.kt @@ -0,0 +1,57 @@ +package gg.norisk.heroes.katara.registry + +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.katara.KataraManager.toId +import gg.norisk.heroes.katara.entity.IceShardEntity +import gg.norisk.heroes.katara.entity.WaterBendingEntity +import net.fabricmc.fabric.api.`object`.builder.v1.entity.FabricDefaultAttributeRegistry +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityDimensions +import net.minecraft.entity.EntityType +import net.minecraft.entity.SpawnGroup +import net.minecraft.entity.attribute.DefaultAttributeContainer +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.mob.PathAwareEntity +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry + +object EntityRegistry { + val WATER_BENDING: EntityType = register("water_bending", { entityType, world -> + WaterBendingEntity(entityType, world) + }, 0.3125f, 0.3125f) + val ICE_SHARD: EntityType = register("ice_shard", { entityType, world -> + IceShardEntity(entityType, world) + }, 0.3125f, 0.3125f) + + fun init() { + registerEntityAttributes() + } + + private fun registerEntityAttributes() { + FabricDefaultAttributeRegistry.register(WATER_BENDING, createGenericEntityAttributes()) + } + + fun createGenericEntityAttributes(): DefaultAttributeContainer.Builder { + return PathAwareEntity.createLivingAttributes() + .add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.80000000298023224) + .add(EntityAttributes.GENERIC_FOLLOW_RANGE, 16.0).add(EntityAttributes.GENERIC_MAX_HEALTH, 10.0) + .add(EntityAttributes.GENERIC_ATTACK_DAMAGE, 5.0).add(EntityAttributes.GENERIC_ATTACK_KNOCKBACK, 0.1) + } + + private fun register( + name: String, entity: EntityType.EntityFactory, width: Float, height: Float + ): EntityType { + val dimension = EntityDimensions.changing(width, height).withEyeHeight(0f) + val builder = EntityType.Builder.create(entity, SpawnGroup.CREATURE) + return Registry.register( + Registries.ENTITY_TYPE, + name.toId(), + builder.eyeHeight(0f) + .dimensions(dimension.width, dimension.height) + .apply { + requires(HeroesManager.heroesFlag) + } + .build(null) + ) + } +} diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/registry/EntityRendererRegistry.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/registry/EntityRendererRegistry.kt new file mode 100644 index 0000000..0e35210 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/registry/EntityRendererRegistry.kt @@ -0,0 +1,13 @@ +package gg.norisk.heroes.katara.registry + +import gg.norisk.heroes.katara.client.render.IceShardEntityRenderer +import gg.norisk.heroes.katara.client.render.WaterBendingEntityRenderer +import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry + + +object EntityRendererRegistry { + fun init() { + EntityRendererRegistry.register(EntityRegistry.WATER_BENDING, ::WaterBendingEntityRenderer) + EntityRendererRegistry.register(EntityRegistry.ICE_SHARD, ::IceShardEntityRenderer) + } +} diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/registry/SoundRegistry.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/registry/SoundRegistry.kt new file mode 100644 index 0000000..8846b23 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/registry/SoundRegistry.kt @@ -0,0 +1,16 @@ +package gg.norisk.heroes.katara.registry + +import gg.norisk.heroes.katara.KataraManager.toId +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry +import net.minecraft.sound.SoundEvent + +object SoundRegistry { + var ICE_PLACE = "ice_place".register() + var WATER_CIRCLE_ADD = "water_circle_add".register() + + fun init() { + } + + private fun String.register() = Registry.register(Registries.SOUND_EVENT, this.toId(), SoundEvent.of(this.toId())) +} diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/utils/EntityCircleTracker.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/utils/EntityCircleTracker.kt new file mode 100644 index 0000000..6f5e7c4 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/utils/EntityCircleTracker.kt @@ -0,0 +1,69 @@ +package gg.norisk.heroes.katara.utils + +import net.minecraft.entity.Entity +import java.util.* +import kotlin.math.abs + +class EntityCircleTracker { + private val rotationHistory: Deque = ArrayDeque() + private val maxHistorySize = 40 // Über 2 Sekunden (20 Ticks pro Sekunde) + private val minCircleThreshold = 360.0f // Mindestens 360° Yaw+Pitch-Änderung + + fun update(entity: Entity) { + val yaw = normalizeAngle(entity.yaw) + val pitch = normalizeAngle(entity.pitch) + + // Aktuelle Werte speichern + if (rotationHistory.size >= maxHistorySize) { + rotationHistory.pollFirst() + } + rotationHistory.addLast(Vec2f(yaw, pitch)) + } + + fun clear() { + rotationHistory.clear() + } + + val isDrawingCircle: Boolean + get() { + if (rotationHistory.size < 2) { + return false // Nicht genug Daten + } + + var yawChange = 0.0f + var pitchChange = 0.0f + var previous: Vec2f? = null + + for (current in rotationHistory) { + if (previous != null) { + yawChange += calculateAngleDifference(previous.x, current.x) + pitchChange += calculateAngleDifference(previous.y, current.y) + } + previous = current + } + + // Prüfen, ob Yaw und Pitch zusammen mindestens 360°-Bewegung ergeben + return (yawChange + pitchChange) >= minCircleThreshold + } + + private fun calculateAngleDifference(previous: Float, current: Float): Float { + var diff = current - previous + while (diff < -180.0f) { + diff += 360.0f + } + while (diff > 180.0f) { + diff -= 360.0f + } + return abs(diff.toDouble()).toFloat() + } + + private fun normalizeAngle(angle: Float): Float { + return (angle % 360.0f + 360.0f) % 360.0f + } + + // Hilfsklasse für 2D-Werte + private data class Vec2f(// Yaw + val x: Float, // Pitch + val y: Float + ) +} diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/utils/EntitySpinTracker.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/utils/EntitySpinTracker.kt new file mode 100644 index 0000000..f70e7fe --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/utils/EntitySpinTracker.kt @@ -0,0 +1,64 @@ +package gg.norisk.heroes.katara.utils + +import net.minecraft.entity.Entity +import java.util.* +import kotlin.math.abs + +class EntitySpinTracker { + private val yawHistory: Deque = ArrayDeque() + private val maxHistorySize = 20 // Anzahl der Ticks, die wir überwachen (z. B. 1 Sekunde bei 20 Ticks pro Sekunde) + private val spinThreshold = 720.0f // Mindestens 720° Änderung für einen "wilden Spin" (z. B. 2 volle Umdrehungen) + + fun update(entity: Entity) { + // Aktuelle Yaw-Rotation der Entity holen + val currentYaw = normalizeYaw(entity.yaw) + + // Letzten Wert speichern + if (yawHistory.size >= maxHistorySize) { + yawHistory.pollFirst() + } + yawHistory.addLast(currentYaw) + + // Optional: Debug-Log für Rotation + } + + fun clear() { + yawHistory.clear() + } + + fun hasSpunWildly(): Boolean { + if (yawHistory.size < 2) { + return false // Nicht genug Daten + } + + var totalChange = 0.0f + var previousYaw: Float? = null + + for (yaw in yawHistory) { + if (previousYaw != null) { + val delta = calculateYawDifference(previousYaw, yaw) + totalChange += delta + } + previousYaw = yaw + } + + // Wenn die gesamte Änderung den Schwellenwert überschreitet + return totalChange >= spinThreshold + } + + private fun calculateYawDifference(previous: Float, current: Float): Float { + var diff = current - previous + while (diff < -180.0f) { + diff += 360.0f + } + while (diff > 180.0f) { + diff -= 360.0f + } + return abs(diff.toDouble()).toFloat() + } + + private fun normalizeYaw(yaw: Float): Float { + // Yaw auf den Bereich [0, 360) normalisieren + return (yaw % 360.0f + 360.0f) % 360.0f + } +} diff --git a/katara/src/main/kotlin/gg/norisk/heroes/katara/utils/RenderUtils.kt b/katara/src/main/kotlin/gg/norisk/heroes/katara/utils/RenderUtils.kt new file mode 100644 index 0000000..4230671 --- /dev/null +++ b/katara/src/main/kotlin/gg/norisk/heroes/katara/utils/RenderUtils.kt @@ -0,0 +1,49 @@ +package gg.norisk.heroes.katara.utils + +import net.minecraft.block.BlockState +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.RenderLayers +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Vec3d +import java.util.function.Consumer + +object RenderUtils { + fun renderBlock( + matrixStack: MatrixStack, + pos: Vec3d, + state: BlockState, + blockPos: BlockPos, + consumer: Consumer = Consumer { } + ) { + val camera = MinecraftClient.getInstance().gameRenderer.camera + val renderer = MinecraftClient.getInstance().blockRenderManager + val world = MinecraftClient.getInstance().world ?: return + val vertexConsumer = MinecraftClient.getInstance().bufferBuilders.entityVertexConsumers.getBuffer( + RenderLayers.getBlockLayer(state) + ) + + matrixStack.push() + matrixStack.translate( + pos.x - camera.getPos().x + 0.5, + pos.y - camera.getPos().y + 0.5, + pos.z - camera.getPos().z + 0.5 + ) + consumer.accept(matrixStack) + // Verschiebe den Block um 0.5 in alle Richtungen und führe dann die Skalierung aus + // matrixStack.scale(0.5f, 0.5f, 0.5f) + + // Rückverschiebung nach der Skalierung, um wieder zum tatsächlichen Mittelpunkt des Blocks zu gelangen + matrixStack.translate(-0.5, -0.5, -0.5) + renderer.renderBlock( + state, + blockPos, + world, + matrixStack, + vertexConsumer, + true, + net.minecraft.util.math.random.Random.create() + ) + matrixStack.pop() + } +} diff --git a/katara/src/main/resources/assets/hero-api/textures/hero/1minuteaang/icon.png b/katara/src/main/resources/assets/hero-api/textures/hero/1minuteaang/icon.png new file mode 100644 index 0000000..96d5ae2 Binary files /dev/null and b/katara/src/main/resources/assets/hero-api/textures/hero/1minuteaang/icon.png differ diff --git a/katara/src/main/resources/assets/hero-api/textures/hero/1stundeaang/icon.png b/katara/src/main/resources/assets/hero-api/textures/hero/1stundeaang/icon.png new file mode 100644 index 0000000..96d5ae2 Binary files /dev/null and b/katara/src/main/resources/assets/hero-api/textures/hero/1stundeaang/icon.png differ diff --git a/katara/src/main/resources/assets/hero-api/textures/hero/katara/icon.png b/katara/src/main/resources/assets/hero-api/textures/hero/katara/icon.png new file mode 100644 index 0000000..7bf38f9 Binary files /dev/null and b/katara/src/main/resources/assets/hero-api/textures/hero/katara/icon.png differ diff --git a/katara/src/main/resources/assets/katara/emotes/waterpillar.animation.json b/katara/src/main/resources/assets/katara/emotes/waterpillar.animation.json new file mode 100644 index 0000000..d1c2501 --- /dev/null +++ b/katara/src/main/resources/assets/katara/emotes/waterpillar.animation.json @@ -0,0 +1,61 @@ +{ + "format_version": "1.8.0", + "animations": { + "tpose": { + "loop": "hold_on_last_frame", + "animation_length": 0.2917, + "lockVanillaBones": { + "bipedRig": false, + "bipedHead": false, + "bipedBody": false, + "bipedLeftArm": true, + "bipedRightArm": true, + "bipedLeftLeg": true, + "bipedRightLeg": true + }, + "bones": { + "bipedRightArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.2917": { + "vector": [0, 0, "90 + Math.cos(query.anim_time * 100 +25) * 3"], + "easing": "easeOutCubic" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.2917": { + "vector": [-1, -1, 0], + "easing": "easeOutCubic" + } + } + }, + "bipedLeftArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.2917": { + "vector": [0, 0, "-90 - Math.cos(query.anim_time * 100 +25) * 3"], + "easing": "easeOutCubic" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.2917": { + "vector": [1, -1, 0], + "easing": "easeOutCubic" + } + } + } + } + } + }, + "geckolib_format_version": 2 +} diff --git a/katara/src/main/resources/assets/katara/emotes/waterpillar_start.animation.json b/katara/src/main/resources/assets/katara/emotes/waterpillar_start.animation.json new file mode 100644 index 0000000..6ce58b8 --- /dev/null +++ b/katara/src/main/resources/assets/katara/emotes/waterpillar_start.animation.json @@ -0,0 +1,79 @@ +{ + "format_version": "1.8.0", + "animations": { + "water_pillar_start": { + "animation_length": 0.52, + "bones": { + "bipedRig": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.48": { + "vector": [0, 360, 0], + "easing": "easeInExpo" + }, + "0.52": { + "vector": [0, 0, 0] + } + } + }, + "bipedRightArm": { + "rotation": { + "0.0": { + "vector": [-78.99034, 24.59477, 4.62934] + }, + "0.12": { + "vector": [73.84641, 51.3798, 167.24859] + }, + "0.24": { + "vector": [31.34641, 51.3798, 167.24859] + }, + "0.48": { + "vector": [-81.56011, -42.11662, 4.58533] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.24": { + "vector": [0, 3, 0] + }, + "0.48": { + "vector": [0, -1, -3] + } + } + }, + "bipedLeftArm": { + "rotation": { + "0.0": { + "vector": [86.44785, -33.16347, -168.10969] + }, + "0.12": { + "vector": [-82.49399, -66.60691, 3.05019] + }, + "0.24": { + "vector": [-42.49399, -66.60691, 3.05019] + }, + "0.48": { + "vector": [-72.50228, 13.17432, 52.2119] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.24": { + "vector": [0, 0, -2] + }, + "0.48": { + "vector": [-1, 0, -2] + } + } + } + } + } + }, + "geckolib_format_version": 2 +} \ No newline at end of file diff --git a/katara/src/main/resources/assets/katara/sounds.json b/katara/src/main/resources/assets/katara/sounds.json new file mode 100644 index 0000000..ef90d44 --- /dev/null +++ b/katara/src/main/resources/assets/katara/sounds.json @@ -0,0 +1,12 @@ +{ + "ice_place": { + "sounds": [ + "katara:ice_place" + ] + }, + "water_circle_add": { + "sounds": [ + "katara:water_circle_add" + ] + } +} diff --git a/katara/src/main/resources/assets/katara/sounds/ice_place.ogg b/katara/src/main/resources/assets/katara/sounds/ice_place.ogg new file mode 100644 index 0000000..f147f52 Binary files /dev/null and b/katara/src/main/resources/assets/katara/sounds/ice_place.ogg differ diff --git a/katara/src/main/resources/assets/katara/sounds/water_circle_add.ogg b/katara/src/main/resources/assets/katara/sounds/water_circle_add.ogg new file mode 100644 index 0000000..abbdae8 Binary files /dev/null and b/katara/src/main/resources/assets/katara/sounds/water_circle_add.ogg differ diff --git a/katara/src/main/resources/assets/katara/textures/entity/projectiles/ice_shard.png b/katara/src/main/resources/assets/katara/textures/entity/projectiles/ice_shard.png new file mode 100644 index 0000000..5dd238b Binary files /dev/null and b/katara/src/main/resources/assets/katara/textures/entity/projectiles/ice_shard.png differ diff --git a/katara/src/main/resources/assets/katara/textures/overlay/healing_water.png b/katara/src/main/resources/assets/katara/textures/overlay/healing_water.png new file mode 100644 index 0000000..1088805 Binary files /dev/null and b/katara/src/main/resources/assets/katara/textures/overlay/healing_water.png differ diff --git a/katara/src/main/resources/assets/katara/waterbender_overlay.png b/katara/src/main/resources/assets/katara/waterbender_overlay.png new file mode 100644 index 0000000..da264b4 Binary files /dev/null and b/katara/src/main/resources/assets/katara/waterbender_overlay.png differ diff --git a/katara/src/main/resources/fabric.mod.json b/katara/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..fee60b7 --- /dev/null +++ b/katara/src/main/resources/fabric.mod.json @@ -0,0 +1,49 @@ +{ + "schemaVersion": 1, + "name": "Katara", + "id": "katara", + "version": "${version}", + "description": "Katara", + "authors": [ + "NoRiskk" + ], + "icon": "assets/katara/icon.png", + "license": "ARR", + "environment": "*", + "entrypoints": { + "main": [ + { + "adapter": "kotlin", + "value": "gg.norisk.heroes.katara.KataraManager" + } + ], + "client": [ + { + "adapter": "kotlin", + "value": "gg.norisk.heroes.katara.KataraManager" + } + ], + "server": [ + { + "adapter": "kotlin", + "value": "gg.norisk.heroes.katara.KataraManager" + } + ] + }, + "mixins": [ + "katara.mixins.json" + ], + "accessWidener": "katara.accesswidener", + "depends": { + }, + "custom": { + "modmenu": { + "badges": [ + "library" + ], + "parent": { + "id": "hero-api" + } + } + } +} diff --git a/katara/src/main/resources/katara.accesswidener b/katara/src/main/resources/katara.accesswidener new file mode 100644 index 0000000..0e5a48b --- /dev/null +++ b/katara/src/main/resources/katara.accesswidener @@ -0,0 +1,3 @@ +accessWidener v2 named +accessible field net/minecraft/entity/FallingBlockEntity block Lnet/minecraft/block/BlockState; +accessible field net/minecraft/client/render/block/BlockRenderManager fluidRenderer Lnet/minecraft/client/render/block/FluidRenderer; \ No newline at end of file diff --git a/katara/src/main/resources/katara.mixins.json b/katara/src/main/resources/katara.mixins.json new file mode 100644 index 0000000..b1ccb7c --- /dev/null +++ b/katara/src/main/resources/katara.mixins.json @@ -0,0 +1,19 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "gg.norisk.heroes.katara.mixin", + "compatibilityLevel": "JAVA_21", + "injectors": { + "defaultRequire": 1 + }, + "mixins": [ + "EntityMixin", + "FlowableFluidMixin", + "PlayerEntityMixin" + ], + "client": [ + "BufferBuilderStorageMixin", + "FluidRendererMixin", + "PlayerEntityRendererMixin" + ] +} diff --git a/libs/npc-lib-fabric-3.0.0-SNAPSHOT.jar b/libs/npc-lib-fabric-3.0.0-SNAPSHOT.jar new file mode 100644 index 0000000..426ee8b Binary files /dev/null and b/libs/npc-lib-fabric-3.0.0-SNAPSHOT.jar differ diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..26e904e --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,21 @@ +pluginManagement { + repositories { + mavenCentral() + maven(url = "https://maven.fabricmc.net/") { + name = "Fabric" + } + gradlePluginPortal() + } +} + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") +enableFeaturePreview("STABLE_CONFIGURATION_CACHE") + +rootProject.name = "heroes" +include(":datatracker") +include(":hero-api") +include(":katara") +include(":aang") +include(":toph") +include(":ffa-server") + diff --git a/toph/build.gradle.kts b/toph/build.gradle.kts new file mode 100644 index 0000000..23a1fd8 --- /dev/null +++ b/toph/build.gradle.kts @@ -0,0 +1,16 @@ +dependencies { + implementation(project(":hero-api", configuration = "namedElements")) + implementation(project(":datatracker", configuration = "namedElements")) + + modApi(libs.bundles.fabric) + modApi(libs.bundles.silk) + modApi(libs.bundles.performance) + modApi(libs.owolib) + modApi(libs.geckolib) + modApi(libs.emoteLib) +} + +loom { + accessWidenerPath.set(file("src/main/resources/toph.accesswidener")) +} + diff --git a/toph/src/main/java/gg/norisk/heroes/toph/mixin/MinecraftClientMixin.java b/toph/src/main/java/gg/norisk/heroes/toph/mixin/MinecraftClientMixin.java new file mode 100644 index 0000000..9435919 --- /dev/null +++ b/toph/src/main/java/gg/norisk/heroes/toph/mixin/MinecraftClientMixin.java @@ -0,0 +1,17 @@ +package gg.norisk.heroes.toph.mixin; + +import gg.norisk.heroes.toph.ability.SeismicSenseAbilityKt; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(MinecraftClient.class) +public abstract class MinecraftClientMixin { + @Inject(method = "hasOutline", at = @At("RETURN"), cancellable = true) + private void injected(Entity entity, CallbackInfoReturnable cir) { + SeismicSenseAbilityKt.handleSeismicSenseOutline(entity, cir); + } +} diff --git a/toph/src/main/java/gg/norisk/heroes/toph/mixin/ModelPartAccessor.java b/toph/src/main/java/gg/norisk/heroes/toph/mixin/ModelPartAccessor.java new file mode 100644 index 0000000..02608e3 --- /dev/null +++ b/toph/src/main/java/gg/norisk/heroes/toph/mixin/ModelPartAccessor.java @@ -0,0 +1,23 @@ +package gg.norisk.heroes.toph.mixin; + +import net.minecraft.client.model.ModelPart; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.util.math.MatrixStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.util.List; +import java.util.Map; + +@Mixin(ModelPart.class) +public interface ModelPartAccessor { + @Accessor("children") + Map getChildren(); + + @Accessor("cuboids") + List getCuboids(); + + @Invoker("renderCuboids") + void invokeRenderCuboids(MatrixStack.Entry entry, VertexConsumer vertexConsumer, int i, int j, int k); +} diff --git a/toph/src/main/java/gg/norisk/heroes/toph/mixin/entity/EntityMixin.java b/toph/src/main/java/gg/norisk/heroes/toph/mixin/entity/EntityMixin.java new file mode 100644 index 0000000..11408a7 --- /dev/null +++ b/toph/src/main/java/gg/norisk/heroes/toph/mixin/entity/EntityMixin.java @@ -0,0 +1,28 @@ +package gg.norisk.heroes.toph.mixin.entity; + +import gg.norisk.heroes.common.networking.dto.AnimationInterpolator; +import gg.norisk.heroes.toph.entity.ITrappedEntity; +import net.minecraft.entity.Entity; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(Entity.class) +public abstract class EntityMixin implements ITrappedEntity { + @Nullable + @Unique + private AnimationInterpolator earthRotationAnimation; + + @Unique + @Nullable + @Override + public AnimationInterpolator getEarthRotationAnimation() { + return earthRotationAnimation; + } + + @Unique + @Override + public void setEarthRotationAnimation(@Nullable AnimationInterpolator animationInterpolator) { + earthRotationAnimation = animationInterpolator; + } +} diff --git a/toph/src/main/java/gg/norisk/heroes/toph/mixin/entity/ItemEntityMixin.java b/toph/src/main/java/gg/norisk/heroes/toph/mixin/entity/ItemEntityMixin.java new file mode 100644 index 0000000..6a9aac6 --- /dev/null +++ b/toph/src/main/java/gg/norisk/heroes/toph/mixin/entity/ItemEntityMixin.java @@ -0,0 +1,39 @@ +package gg.norisk.heroes.toph.mixin.entity; + +import gg.norisk.heroes.toph.ability.EarthArmorAbilityKt; +import gg.norisk.heroes.toph.entity.IBendingItemEntity; +import net.minecraft.entity.ItemEntity; +import net.minecraft.entity.player.PlayerEntity; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.UUID; + +@Mixin(ItemEntity.class) +public abstract class ItemEntityMixin implements IBendingItemEntity { + private UUID bender; + + @Inject(method = "tick", at = @At("TAIL")) + private void injected(CallbackInfo ci) { + EarthArmorAbilityKt.moveToBender((ItemEntity) (Object) this); + } + + @Inject(method = "onPlayerCollision", at = @At("HEAD"), cancellable = true) + private void onPlayerCollisionInjection(PlayerEntity playerEntity, CallbackInfo ci) { + EarthArmorAbilityKt.handlePlayerCollision((ItemEntity) (Object) this, playerEntity, ci); + } + + @Override + public void setBender(UUID bender) { + this.bender = bender; + } + + @Nullable + @Override + public UUID getBender() { + return bender; + } +} diff --git a/toph/src/main/java/gg/norisk/heroes/toph/mixin/entity/PlayerEntityMixin.java b/toph/src/main/java/gg/norisk/heroes/toph/mixin/entity/PlayerEntityMixin.java new file mode 100644 index 0000000..9258f24 --- /dev/null +++ b/toph/src/main/java/gg/norisk/heroes/toph/mixin/entity/PlayerEntityMixin.java @@ -0,0 +1,40 @@ +package gg.norisk.heroes.toph.mixin.entity; + +import gg.norisk.heroes.toph.entity.ITophPlayer; +import io.netty.util.internal.ConcurrentSet; +import kotlin.Pair; +import kotlinx.coroutines.Job; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.math.BlockPos; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@Mixin(PlayerEntity.class) +public abstract class PlayerEntityMixin implements ITophPlayer { + private final Set> seismicBlocks = ConcurrentHashMap.newKeySet(); + private final Set> seismicEntities = ConcurrentHashMap.newKeySet(); + + @Unique + private final List seismicTasks = new ArrayList<>(); + + @NotNull + @Override + public Set> getSeismicBlocks() { + return seismicBlocks; + } + + @NotNull + @Override + public Set> getSeismicEntities() { + return seismicEntities; + } + + @Override + public @NotNull List getToph_seismicTasks() { + return seismicTasks; + } +} diff --git a/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/AnimalModelAccessor.java b/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/AnimalModelAccessor.java new file mode 100644 index 0000000..c7cc6fc --- /dev/null +++ b/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/AnimalModelAccessor.java @@ -0,0 +1,12 @@ +package gg.norisk.heroes.toph.mixin.render; + +import net.minecraft.client.model.ModelPart; +import net.minecraft.client.render.entity.model.AnimalModel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(AnimalModel.class) +public interface AnimalModelAccessor { + @Invoker("getBodyParts") + Iterable invokeGetBodyParts(); +} diff --git a/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/DarknessFogModifierMixin.java b/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/DarknessFogModifierMixin.java new file mode 100644 index 0000000..2259651 --- /dev/null +++ b/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/DarknessFogModifierMixin.java @@ -0,0 +1,38 @@ +package gg.norisk.heroes.toph.mixin.render; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import gg.norisk.heroes.toph.ability.SeismicSenseAbilityKt; +import net.minecraft.client.render.BackgroundRenderer; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.player.PlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(BackgroundRenderer.DarknessFogModifier.class) +public abstract class DarknessFogModifierMixin { + /* @ModifyConstant(method = "applyStartEndModifier", constant = @Constant(floatValue = 15.0F)) + private float injected(float constant, BackgroundRenderer.FogData fogData, LivingEntity livingEntity, StatusEffectInstance statusEffectInstance, float f, float g) { + if (livingEntity instanceof PlayerEntity player) { + var value = SeismicSenseAbilityKt.handleSeismicSenseDarkness(player); + System.out.println("Returning " + (value == -1f ? constant : value)); + return value == -1f ? constant : value; + } + return constant; + } */ + + @ModifyExpressionValue( + method = "applyStartEndModifier", + at = @At(value = "INVOKE", target = "Lnet/minecraft/util/math/MathHelper;lerp(FFF)F") + ) + private float halveSpeed(float original, BackgroundRenderer.FogData fogData, LivingEntity livingEntity, StatusEffectInstance statusEffectInstance, float f, float g) { + if (livingEntity instanceof PlayerEntity player) { + var value = SeismicSenseAbilityKt.handleSeismicSenseDarkness(player, original); + return value; + } + return original; + } +} diff --git a/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/GameRendererAccessor.java b/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/GameRendererAccessor.java new file mode 100644 index 0000000..a7b452a --- /dev/null +++ b/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/GameRendererAccessor.java @@ -0,0 +1,12 @@ +package gg.norisk.heroes.toph.mixin.render; + +import net.minecraft.client.render.GameRenderer; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(GameRenderer.class) +public interface GameRendererAccessor { + @Invoker("loadPostProcessor") + void invokeLoadPostProcessor(Identifier id); +} diff --git a/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/GameRendererMixin.java b/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/GameRendererMixin.java new file mode 100644 index 0000000..dba1fea --- /dev/null +++ b/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/GameRendererMixin.java @@ -0,0 +1,31 @@ +package gg.norisk.heroes.toph.mixin.render; + +import gg.norisk.heroes.toph.ability.SeismicSenseAbilityKt; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.entity.Entity; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GameRenderer.class) +public abstract class GameRendererMixin { + @Shadow + abstract void loadPostProcessor(Identifier identifier); + + @Inject(at = @At("HEAD"), method = "togglePostProcessorEnabled", cancellable = true) + private void togglePostProcessorEnabledInjection(CallbackInfo ci) { + SeismicSenseAbilityKt.handleSeismicSenseShader(ci); + } + + @Inject(at = @At("TAIL"), method = "onCameraEntitySet", cancellable = true) + private void onCameraEntitySet(Entity entity, CallbackInfo ci) { + var player = MinecraftClient.getInstance().player; + if (player != null && SeismicSenseAbilityKt.getHasSeismicSense(player)) { + this.loadPostProcessor(SeismicSenseAbilityKt.getSeismicSenseShader()); + } + } +} diff --git a/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/WorldRendererAccessor.java b/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/WorldRendererAccessor.java new file mode 100644 index 0000000..e2611b1 --- /dev/null +++ b/toph/src/main/java/gg/norisk/heroes/toph/mixin/render/WorldRendererAccessor.java @@ -0,0 +1,16 @@ +package gg.norisk.heroes.toph.mixin.render; + +import net.minecraft.block.BlockState; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(WorldRenderer.class) +public interface WorldRendererAccessor { + @Invoker("drawBlockOutline") + void invokeDrawBlockOutline(MatrixStack matrixStack, VertexConsumer vertexConsumer, Entity entity, double d, double e, double f, BlockPos blockPos, BlockState blockState); +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/TophManager.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/TophManager.kt new file mode 100644 index 0000000..a8f4668 --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/TophManager.kt @@ -0,0 +1,62 @@ +package gg.norisk.heroes.toph + +import gg.norisk.heroes.common.hero.Hero +import gg.norisk.heroes.common.hero.HeroManager.registerHero +import gg.norisk.heroes.toph.ability.* +import gg.norisk.heroes.toph.ability.EarthArmorAttributeModifiers.EarthArmorAbility +import gg.norisk.heroes.toph.particle.EarthDustParticle +import gg.norisk.heroes.toph.registry.ParticleRegistry +import gg.norisk.heroes.toph.registry.SoundRegistry +import net.fabricmc.api.ClientModInitializer +import net.fabricmc.api.DedicatedServerModInitializer +import net.fabricmc.api.ModInitializer +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents +import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry +import net.minecraft.block.BlockState +import net.minecraft.particle.ParticleEffect +import net.minecraft.particle.ParticleType +import net.minecraft.registry.tag.BlockTags +import net.minecraft.util.Identifier +import org.apache.logging.log4j.LogManager +import java.awt.Color + +object TophManager : ModInitializer, ClientModInitializer, DedicatedServerModInitializer { + private const val MOD_ID = "toph" + val logger = LogManager.getLogger(MOD_ID) + fun String.toId() = Identifier.of(MOD_ID, this) + fun String.toEmote(): Identifier { + return "emotes/$this.animation.json".toId() + } + + override fun onInitialize() { + ParticleRegistry.init() + SoundRegistry.init() + logger.info("Starting $MOD_ID Hero...") + } + + override fun onInitializeClient() { + ParticleFactoryRegistry.getInstance() + .register(ParticleRegistry.EARTH_DUST as ParticleType, EarthDustParticle::CosySmokeFactory) + ClientLifecycleEvents.CLIENT_STARTED.register { + registerHero(Toph) + } + } + + override fun onInitializeServer() { + registerHero(Toph) + } + + val Toph by Hero("Toph") { + ability(EarthArmorAbility) + ability(SeismicSenseAbility) + ability(EarthColumnInstantAbility) + ability(EarthSurfAbility) + ability(EarthPushAbility) + ability(EarthTrapAbility) + color = Color.GREEN.rgb + overlaySkin = "textures/toph_overlay.png".toId() + } + + val BlockState.isEarthBlock + get() = isIn(BlockTags.PICKAXE_MINEABLE) || isIn(BlockTags.SHOVEL_MINEABLE) +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthArmorAbility.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthArmorAbility.kt new file mode 100644 index 0000000..5cc0675 --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthArmorAbility.kt @@ -0,0 +1,271 @@ +package gg.norisk.heroes.toph.ability + +import gg.norisk.emote.network.EmoteNetworking.playEmote +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.common.HeroesManager.client +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.PressAbility +import gg.norisk.heroes.common.networking.BoomShake +import gg.norisk.heroes.common.networking.cameraShakePacket +import gg.norisk.heroes.common.utils.pos3i +import gg.norisk.heroes.toph.TophManager.toEmote +import gg.norisk.heroes.toph.TophManager.toId +import gg.norisk.heroes.toph.entity.IBendingItemEntity +import gg.norisk.heroes.toph.registry.SoundRegistry +import gg.norisk.heroes.toph.render.ChestItemFeatureRenderer +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityFeatureRendererRegistrationCallback +import net.minecraft.client.network.AbstractClientPlayerEntity +import net.minecraft.client.render.entity.feature.FeatureRendererContext +import net.minecraft.client.render.entity.model.PlayerEntityModel +import net.minecraft.component.DataComponentTypes +import net.minecraft.component.type.AttributeModifierSlot +import net.minecraft.component.type.AttributeModifiersComponent +import net.minecraft.entity.EntityType +import net.minecraft.entity.EquipmentSlot +import net.minecraft.entity.ItemEntity +import net.minecraft.entity.attribute.EntityAttributeModifier +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.BlockItem +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.particle.BlockStateParticleEffect +import net.minecraft.particle.ParticleTypes +import net.minecraft.registry.tag.BlockTags +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundCategory +import net.minecraft.util.Hand +import net.minecraft.util.Identifier +import net.minecraft.util.math.Direction +import net.silkmc.silk.core.entity.modifyVelocity +import net.silkmc.silk.core.entity.touchedBlockNoAir +import net.silkmc.silk.core.kotlin.ticks +import net.silkmc.silk.core.math.geometry.filledSpherePositionSet +import net.silkmc.silk.core.task.mcCoroutineTask +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo +import kotlin.random.Random + +object EarthArmorAttributeModifiers { + + val earthArmorArmorProperty = NumberProperty( + 2.0, 3, + "Armor", + AddValueTotal(1.0, 1.0, 1.0), + ).apply { + icon = { + Components.item(Items.IRON_CHESTPLATE.defaultStack) + } + } + val earthArmorKnockbackProperty = NumberProperty( + 0.05, 3, + "Knockback", + AddValueTotal(-0.01, -0.02, -0.03) + ).apply { + icon = { + Components.item(Items.ANVIL.defaultStack) + } + } + val earthArmorSpeedProperty = NumberProperty( + -0.005, 3, + "Speed", + AddValueTotal(0.002, 0.002, 0.002), + ).apply { + icon = { + Components.item(Items.SUGAR.defaultStack) + } + } + + fun addTo(stack: ItemStack, player: PlayerEntity) { + val ARMOR_ENTRY = AttributeModifiersComponent.Entry( + EntityAttributes.GENERIC_ARMOR, + EntityAttributeModifier( + "armor_modifier".toId(), + earthArmorArmorProperty.getValue(player.uuid), + EntityAttributeModifier.Operation.ADD_VALUE + ), + AttributeModifierSlot.ARMOR + ) + + val KNOCKBACK_ENTRY = AttributeModifiersComponent.Entry( + EntityAttributes.GENERIC_ATTACK_KNOCKBACK, + EntityAttributeModifier( + "knockback_modifier".toId(), + earthArmorKnockbackProperty.getValue(player.uuid), + EntityAttributeModifier.Operation.ADD_VALUE + ), + AttributeModifierSlot.ARMOR + ) + + val SPEED_ENTRY = AttributeModifiersComponent.Entry( + EntityAttributes.GENERIC_MOVEMENT_SPEED, + EntityAttributeModifier( + "speed_modifier".toId(), + earthArmorSpeedProperty.getValue(player.uuid), + EntityAttributeModifier.Operation.ADD_MULTIPLIED_TOTAL + ), + AttributeModifierSlot.ARMOR + ) + + val componentFromStack = + stack.get(DataComponentTypes.ATTRIBUTE_MODIFIERS) ?: AttributeModifiersComponent.DEFAULT + val newList: MutableList = ArrayList() + if (componentFromStack != null) { + newList.addAll(componentFromStack.modifiers) + newList.add(ARMOR_ENTRY) + newList.add(KNOCKBACK_ENTRY) + newList.add(SPEED_ENTRY) + } + + stack.set( + DataComponentTypes.ATTRIBUTE_MODIFIERS, + AttributeModifiersComponent(newList, true) + ) + } + + val EarthArmorAbility = object : PressAbility("Earth Armor") { + init { + client { + this.keyBind = HeroKeyBindings.firstKeyBind + + LivingEntityFeatureRendererRegistrationCallback.EVENT.register { entityType, entityRenderer, registrationHelper, context -> + if (entityType !== EntityType.PLAYER) return@register + registrationHelper.register( + ChestItemFeatureRenderer( + entityRenderer as FeatureRendererContext>, + context.heldItemRenderer + ) + ) + } + } + + this.properties = listOf(earthArmorArmorProperty, earthArmorSpeedProperty, earthArmorKnockbackProperty) + + this.cooldownProperty = + buildCooldown(10.0, 5, AddValueTotal(-0.1, -0.4, -0.2, -0.8, -1.5, -1.0)) + } + + override fun getIconComponent(): Component { + return Components.item(Items.IRON_CHESTPLATE.defaultStack) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_mud.png") + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + player.swingHand(Hand.MAIN_HAND, true) + val world = player.world as ServerWorld + player.playEmote("earth-armor".toEmote()) + (player as ServerPlayerEntity).spawnEarthArmorParticle() + world.playSoundFromEntity( + null, + player, + SoundRegistry.EARTH_ARMOR, + SoundCategory.PLAYERS, + 1f, + 1f + ) + cameraShakePacket.send(BoomShake(0.1, 0.2, 0.4), player) + + player.pos3i.filledSpherePositionSet(4) + .filter { + world.getBlockState(it).isIn(BlockTags.PICKAXE_MINEABLE) || world.getBlockState(it) + .isIn(BlockTags.SHOVEL_MINEABLE) + } + .filter { pos -> Direction.values().any { world.getBlockState(pos.offset(it)).isAir } } + .shuffled() + .take(4) + .forEachIndexed { index, blockPos -> + mcCoroutineTask(client = false, sync = true, delay = index.ticks) { + val center = blockPos.toCenterPos() + val state = world.getBlockState(blockPos) + val itemStack = state.block.asItem().defaultStack + val itemEntity = + ItemEntity(world, center.x, center.y, center.z, itemStack) + (itemEntity as IBendingItemEntity).bender = player.uuid + world.spawnEntity(itemEntity) + world.breakBlock(blockPos, false, player) + } + } + } + } + } +} + +fun ItemEntity.handlePlayerCollision(player: PlayerEntity, ci: CallbackInfo) { + val dummy = this as IBendingItemEntity + if (world.isClient) return + if (bender == player.uuid) { + ci.cancel() + discard() + + val blockItem = this.stack.item as? BlockItem ?: return + world.playSoundFromEntity( + null, + player, + blockItem.block.defaultState.soundGroup.placeSound, + SoundCategory.PLAYERS, + 0.7f, + 1f + ) + + val slot = EquipmentSlot.entries + .filter { it.isArmorSlot } + .firstOrNull { player.getEquippedStack(it).isEmpty } + ?: return + + EarthArmorAttributeModifiers.addTo(stack, player) + player.equipStack(slot, this.stack) + } +} + +fun ServerPlayerEntity.spawnEarthArmorParticle() { + val block = this.touchedBlockNoAir ?: return + repeat(20) { + (world as ServerWorld).spawnParticles( + BlockStateParticleEffect(ParticleTypes.BLOCK, block.block.defaultState), + this.x, + getBodyY(Random.nextDouble(0.1, 0.5)), + this.z, + 7, + (this.width / 4.0f).toDouble(), + (this.height / 4.0f).toDouble(), + (this.width / 4.0f).toDouble(), + 0.05 + ) + } +} + +fun ItemEntity.moveToBender() { + val dummy = this as IBendingItemEntity + if (world.isClient) return + val block = this.stack.item as? BlockItem ?: return + if (bender != null) { + val player = world.getPlayerByUuid(bender) + if (player == null) { + discard() + return + } + val direction = player.eyePos.subtract(this.pos).normalize().multiply(0.5) + modifyVelocity(direction) + + (world as ServerWorld).spawnParticles( + BlockStateParticleEffect(ParticleTypes.BLOCK, block.block.defaultState), + this.x, + getBodyY(0.6666666666666666), + this.z, + 7, + (this.width / 4.0f).toDouble(), + (this.height / 4.0f).toDouble(), + (this.width / 4.0f).toDouble(), + 0.05 + ) + } +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthColumnInstantAbility.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthColumnInstantAbility.kt new file mode 100644 index 0000000..39a0a1c --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthColumnInstantAbility.kt @@ -0,0 +1,339 @@ +package gg.norisk.heroes.toph.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.emote.network.EmoteNetworking.playEmote +import gg.norisk.emote.network.EmoteNetworking.stopEmote +import gg.norisk.heroes.client.events.ClientEvents +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.client.renderer.BlockOutlineRenderer +import gg.norisk.heroes.common.HeroesManager.client +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.HoldAbility +import gg.norisk.heroes.common.hero.isHero +import gg.norisk.heroes.common.networking.BoomShake +import gg.norisk.heroes.common.networking.Networking.mouseScrollPacket +import gg.norisk.heroes.common.networking.cameraShakePacket +import gg.norisk.heroes.common.networking.dto.BlockInfoSmall +import gg.norisk.heroes.common.serialization.BlockPosSerializer +import gg.norisk.heroes.common.utils.random +import gg.norisk.heroes.common.utils.sound +import gg.norisk.heroes.toph.TophManager +import gg.norisk.heroes.toph.TophManager.toEmote +import gg.norisk.heroes.toph.TophManager.toId +import gg.norisk.heroes.toph.network.earthColumnBlockInfos +import gg.norisk.heroes.toph.registry.ParticleRegistry +import gg.norisk.heroes.toph.registry.SoundRegistry +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import kotlinx.serialization.Serializable +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents +import net.minecraft.block.BlockState +import net.minecraft.client.MinecraftClient +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.attribute.EntityAttributeModifier +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.network.packet.s2c.play.PositionFlag +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundCategory +import net.minecraft.util.Identifier +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.hit.HitResult +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box +import net.minecraft.util.math.Vec3d +import net.minecraft.world.World +import net.silkmc.silk.core.entity.modifyVelocity +import net.silkmc.silk.core.kotlin.ticks +import net.silkmc.silk.core.math.geometry.produceCirclePositions +import net.silkmc.silk.core.math.geometry.produceFilledSpherePositions +import net.silkmc.silk.core.task.mcCoroutineTask + +val EarthColumnKey = "isEarthColumn" +val EarthColumnRadiusKey = "earth_column_radius" + +//TODO enrico das ist ein fall für dich und deine descriptions oder? +@Serializable +data class EarthColumnDescription( + val blocks: List, + val radius: Int, + val center: @Serializable(with = BlockPosSerializer::class) BlockPos, +) + +val earthColumnRadius = NumberProperty(2.0, 3, "Radius", AddValueTotal(1.0, 1.0, 1.0)).apply { + icon = { + Components.item(Items.STONE_SHOVEL.defaultStack) + } +} + +val earthColumnBoost = NumberProperty(1.0, 2, "Earth Column Boost", AddValueTotal(1.0, 1.0)).apply { + icon = { + Components.item(Items.FIREWORK_ROCKET.defaultStack) + } +} + +val EarthColumnInstantAbility: HoldAbility = object : HoldAbility( + "Earth Column" +) { + + val earthColumnHeight = NumberProperty(3.0, 5, "Height", AddValueTotal(1.0, 1.0, 1.0, 3.0, 2.0)).apply { + icon = { + Components.item(Items.STONE.defaultStack) + } + } + + init { + client { + this.keyBind = HeroKeyBindings.thirdKeyBind + WorldRenderEvents.AFTER_TRANSLUCENT.register { + val world = it.world() + val player = MinecraftClient.getInstance().player ?: return@register + val matrices = it.matrixStack() ?: return@register + if (!player.isEarthColumn()) return@register + val tickCounter = it.tickCounter() + val radius = player.getSyncedData(EarthColumnRadiusKey) ?: 1 + val hitResult = player.raycast(maxDistance, tickCounter.getTickDelta(false), false) + + if (hitResult != null && hitResult.type == HitResult.Type.BLOCK) { + ((hitResult as BlockHitResult).blockPos).produceFilledSpherePositions(radius) { pos -> + val blockState: BlockState = world.getBlockState(pos) + if (world.canBeBended(pos, blockState) && blockState.isSolid) { + BlockOutlineRenderer.drawBlockBox( + matrices, + it.consumers() ?: return@produceFilledSpherePositions, + pos, + 1.0f, + 1.0f, + 1.0f, + 0.5f + ) + } + } + } + } + + ClientEvents.preHotbarScrollEvent.listen { event -> + val player = MinecraftClient.getInstance().player ?: return@listen + if (player.isEarthColumn()) { + event.isCancelled.set(true) + } + } + } + + this.properties = listOf( + earthColumnRadius, + earthColumnHeight, + earthColumnBoost + ) + + this.cooldownProperty = + buildCooldown(110.0, 4, AddValueTotal(-10.0, -10.0, -10.0, -10.0)) + this.maxDurationProperty = + buildMaxDuration(5.0, 5, AddValueTotal(0.1, 0.4, 0.2, 0.8, 1.5, 1.0)) + + mouseScrollPacket.receiveOnServer { packet, context -> + mcCoroutineTask(sync = true, client = false) { + val player = context.player + if (!player.isHero(TophManager.Toph)) return@mcCoroutineTask + if (!player.isEarthColumn()) return@mcCoroutineTask + + var radius = player.getSyncedData(EarthColumnRadiusKey) ?: 1 + radius += if (!packet) -1 else 1 + if (radius <= 0) { + radius = 1 + } + if (radius >= earthColumnRadius.getValue(player.uuid)) { + radius = earthColumnRadius.getValue(player.uuid).toInt() + } + player.setSyncedData(EarthColumnRadiusKey, radius) + } + } + + earthColumnBlockInfos.receiveOnServer { earthColumn, context -> + val world = context.player.serverWorld + val player = context.player + if (!player.isHero(TophManager.Toph)) return@receiveOnServer + mcCoroutineTask( + sync = true, + client = false, + howOften = earthColumnHeight.getValue(player.uuid).toLong(), + period = 0.ticks + ) { + earthColumn.move(world, it.round.toInt(), it.counterDownToZero == 0L, player) + } + } + } + + override fun getIconComponent(): Component { + return Components.item(Items.STONE.defaultStack) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_mud.png") + } + + val maxDistance = 45.0 + + val EARTH_COLUMN_SLOW_BOOST = EntityAttributeModifier( + "earth_column".toId(), + -0.7, + EntityAttributeModifier.Operation.ADD_MULTIPLIED_TOTAL + ) + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + player.setSyncedData(EarthColumnKey, true) + player.playEmote("earth-column-start".toEmote()) + player.world.playSoundFromEntity( + null, + player, + SoundRegistry.ARM_WHOOSH, + SoundCategory.PLAYERS, + 1f, + 1f + ) + runCatching { + player.getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED) + ?.addTemporaryModifier(EARTH_COLUMN_SLOW_BOOST) + } + } + } + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + cleanUp(player) + } + + private fun cleanUp(player: PlayerEntity) { + if (player is ServerPlayerEntity) { + player.stopEmote("earth-column-start".toEmote()) + player.setSyncedData(EarthColumnKey, false) + player.getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED) + ?.removeModifier(EARTH_COLUMN_SLOW_BOOST.id) + } + } + + override fun onEnd(player: PlayerEntity, abilityEndInformation: AbilityEndInformation) { + super.onEnd(player, abilityEndInformation) + if (player is ServerPlayerEntity) { + val startTime = System.currentTimeMillis() + val world = player.world as ServerWorld + player.stopEmote("earth-column-start".toEmote()) + player.playEmote("earth-column-end".toEmote()) + player.world.playSoundFromEntity( + null, + player, + SoundRegistry.EARTH_ARMOR, + SoundCategory.PLAYERS, + 0.6f, + 1.4f + ) + cameraShakePacket.send(BoomShake(0.1, 0.2, 0.4), player as ServerPlayerEntity) + player.sound(SoundRegistry.STONE_SMASH) + player.setSyncedData(EarthColumnKey, false) + player.getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED) + ?.removeModifier(EARTH_COLUMN_SLOW_BOOST.id) + } else if (player == MinecraftClient.getInstance().player) { + val world = player.world + val tickDelta = MinecraftClient.getInstance().renderTickCounter.getTickDelta(false) + val hitResult = player.raycast(maxDistance, tickDelta, false) + + if (hitResult != null && hitResult.type == HitResult.Type.BLOCK && player.isEarthColumn()) { + val blockInfos = mutableSetOf() + val radius = player.getSyncedData(EarthColumnRadiusKey) ?: 1 + ((hitResult as BlockHitResult).blockPos).produceFilledSpherePositions(radius) { pos -> + val blockState: BlockState = world.getBlockState(pos) + if (world.canBeBended(pos, blockState)) { + repeat(32) { + val newPos = pos.up(it) + val newBlockState = world.getBlockState(newPos) + if (it == 0 && newBlockState.isSolid) { + blockInfos.add(BlockInfoSmall(world.getBlockState(pos.down(1)), pos.down(1))) + } + blockInfos.add(BlockInfoSmall(newBlockState, newPos)) + } + } + } + + earthColumnBlockInfos.send( + EarthColumnDescription( + blockInfos.toList(), radius, hitResult.blockPos + ) + ) + } + } + } +} + +private fun EarthColumnDescription.move( + world: ServerWorld, + height: Int, + isFinished: Boolean, + player: ServerPlayerEntity +) { + blocks.forEach { (state, pos) -> + val newPos = pos.up(height) + if (world.getBlockState(newPos) == state) return@forEach + world.setBlockState(newPos, state) + world.getOtherEntities(null, Box.from(newPos.toCenterPos())).filterIsInstance() + .forEach { entity -> + entity.teleport( + world, + entity.x, + entity.y + 1, + entity.z, + PositionFlag.VALUES, + entity.yaw, + entity.pitch + ) + } + } + + if (isFinished) { + mcCoroutineTask(sync = true, client = false, delay = 0.ticks) { + blocks.forEach { (state, pos) -> + if (state.isAir) return@forEach + val newPos = pos.up(it.round.toInt()).up().toCenterPos() + world.getOtherEntities(null, Box.from(newPos).expand(earthColumnRadius.getValue(player.uuid))) + .filterIsInstance() + .forEach { entity -> + entity.damage(entity.damageSources.playerAttack(player), 0.001f) + if (!entity.isSneaking) { + entity.modifyVelocity(Vec3d(0.0, earthColumnBoost.getValue(player.uuid), 0.0)) + } + } + } + } + } + + center.up(height).apply { + world.playSound(null, this, SoundRegistry.EARTH_COLUMN_1, SoundCategory.BLOCKS, 1f, 1f) + } + + center.up().produceCirclePositions(radius) { pos -> + val particlePos = pos.toCenterPos() + world.spawnParticles( + ParticleRegistry.EARTH_DUST, + particlePos.x, + particlePos.y - 0.3, + particlePos.z, + if (radius == 1) 1 else 5, + (0.1..0.4).random(), + (0.1..0.4).random(), + (0.1..0.4).random(), + (0.01..0.04).random() + ) + } +} + +fun World.canBeBended(blockPos: BlockPos, blockState: BlockState): Boolean { + return true +} + +fun PlayerEntity.isEarthColumn() = getSyncedData(EarthColumnKey) == true diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthPushAbility.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthPushAbility.kt new file mode 100644 index 0000000..15a6285 --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthPushAbility.kt @@ -0,0 +1,124 @@ +package gg.norisk.heroes.toph.ability + +import gg.norisk.emote.network.EmoteNetworking.playEmote +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.common.HeroesManager.client +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.PressAbility +import gg.norisk.heroes.common.networking.BoomShake +import gg.norisk.heroes.common.networking.Networking.mousePacket +import gg.norisk.heroes.common.networking.cameraShakePacket +import gg.norisk.heroes.common.utils.RaycastUtils +import gg.norisk.heroes.common.utils.random +import gg.norisk.heroes.common.utils.sound +import gg.norisk.heroes.toph.TophManager.isEarthBlock +import gg.norisk.heroes.toph.TophManager.toEmote +import gg.norisk.heroes.toph.entity.BendingBlockEntity +import gg.norisk.heroes.toph.entity.BendingBlockEntity.Companion.owner +import gg.norisk.heroes.toph.registry.ParticleRegistry +import gg.norisk.heroes.toph.registry.SoundRegistry +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.minecraft.entity.FallingBlockEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundCategory +import net.minecraft.util.Identifier +import net.minecraft.util.math.Vec3d +import net.silkmc.silk.core.entity.modifyVelocity +import kotlin.random.Random + +val earthPushDamage = NumberProperty(2.0, 3, "Damage", AddValueTotal(1.25, 1.25, 2.0)).apply { + icon = { + Components.item(Items.STONE_SWORD.defaultStack) + } +} +val EarthPushAbility = object : PressAbility("Earth Push") { + + init { + client { + this.keyBind = HeroKeyBindings.pickItemKeyBinding + } + + this.properties = listOf(earthPushDamage) + + this.cooldownProperty = + buildCooldown(10.0, 5, AddValueTotal(-0.1, -0.4, -0.2, -0.8, -1.5, -1.0)) + + mousePacket.receiveOnServer { packet, context -> + val player = context.player + val world = player.serverWorld + if (packet.isLeft() && packet.isClicked()) { + val entity = player.getForcedBlocks().filterIsInstance().filter { + it.distanceTo(player) < 6.0 + }.randomOrNull() + ?: return@receiveOnServer + + cameraShakePacket.send(BoomShake(0.1, 0.2, 0.4), player as ServerPlayerEntity) + player.sound(SoundRegistry.STONE_SMASH) + player.sound(SoundRegistry.EARTH_COLUMN_1) + entity.forcePush(player) + player.playEmote("earth-kick".toEmote()) + } + } + } + + override fun getIconComponent(): Component { + return Components.item(Items.DIRT.defaultStack) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_mud.png") + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + val world = player.world as ServerWorld + val clipWithDistance = RaycastUtils.clipWithDistance(player, player.world, 4.5) ?: return + val pos = clipWithDistance.blockPos.toCenterPos() + val state = world.getBlockState(clipWithDistance.blockPos) + abilityScope.cancelCooldown() + if (!state.isEarthBlock) return + abilityScope.applyCooldown() + val entity = BendingBlockEntity(world, pos.x, pos.y, pos.z, state) + cameraShakePacket.send(BoomShake(0.1, 0.2, 0.4), player as ServerPlayerEntity) + entity.canAttack = true + entity.owner = player.uuid + world.spawnEntity(entity) + world.playSound( + null, + clipWithDistance.blockPos, + state.soundGroup.breakSound, + SoundCategory.BLOCKS, + 0.3f, + Random.nextDouble().toFloat() * 0.8f + ) + player.sound(SoundRegistry.EARTH_COLUMN_1) + world.breakBlock(clipWithDistance.blockPos, false, player) + entity.modifyVelocity(Vec3d(0.0, 0.5, 0.0)) + (world as? ServerWorld?)?.spawnParticles( + ParticleRegistry.EARTH_DUST, + pos.x, + pos.y + 1, + pos.z, + 7, + (0.01..0.04).random(), + (0.01..0.04).random(), + (0.01..0.04).random(), + (0.01..0.04).random() + ) + } + } +} + +fun ServerPlayerEntity.getForcedBlocks(): List { + return serverWorld.iterateEntities() + .filterIsInstance() + .filter { it.owner == this.uuid } + .toList() +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthSurfAbility.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthSurfAbility.kt new file mode 100644 index 0000000..8fd60d9 --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthSurfAbility.kt @@ -0,0 +1,246 @@ +package gg.norisk.heroes.toph.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.datatracker.entity.syncedValueChangeEvent +import gg.norisk.emote.network.EmoteNetworking.playEmote +import gg.norisk.emote.network.EmoteNetworking.stopEmote +import gg.norisk.heroes.client.events.ClientEvents +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.client.renderer.Speedlines.showSpeedlines +import gg.norisk.heroes.common.HeroesManager.client +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.ToggleAbility +import gg.norisk.heroes.common.networking.BoomShake +import gg.norisk.heroes.common.networking.cameraShakePacket +import gg.norisk.heroes.common.utils.calculateProbability +import gg.norisk.heroes.common.utils.random +import gg.norisk.heroes.common.utils.sound +import gg.norisk.heroes.common.utils.toBlockPos +import gg.norisk.heroes.toph.TophManager.toEmote +import gg.norisk.heroes.toph.TophManager.toId +import gg.norisk.heroes.toph.registry.ParticleRegistry +import gg.norisk.heroes.toph.registry.SoundRegistry +import gg.norisk.heroes.toph.sound.StoneSlideSoundInstance +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents +import net.minecraft.block.Blocks +import net.minecraft.client.MinecraftClient +import net.minecraft.entity.Entity +import net.minecraft.entity.FallingBlockEntity +import net.minecraft.entity.attribute.EntityAttributeModifier +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundCategory +import net.minecraft.util.Identifier +import net.minecraft.util.math.Vec3d +import net.silkmc.silk.core.annotations.ExperimentalSilkApi +import net.silkmc.silk.core.entity.modifyVelocity +import net.silkmc.silk.core.item.itemStack +import net.silkmc.silk.core.math.geometry.filledSpherePositionSet +import net.silkmc.silk.core.server.players +import kotlin.math.cos +import kotlin.math.sin + +val EarthSurfKey = "isEarthSurfing" + +val earthSurfRadius = NumberProperty(1.0, 3, "Radius", AddValueTotal(1.0, 1.0, 1.0), 20).apply { + icon = { + Components.item(Items.STONE_SHOVEL.defaultStack) + } +} + +@OptIn(ExperimentalSilkApi::class) +val EarthSurfAbility = object : ToggleAbility("Earth Surf") { + + val earthSurfStepHeight = NumberProperty(3.0, 3, "Step Height", AddValueTotal(1.0, 1.0, 1.0)).apply { + icon = { + Components.item(Items.MUD_BRICK_STAIRS.defaultStack) + } + } + val earthSurfSpeedBoost = NumberProperty(1.1, 4, "Speed", AddValueTotal(0.1, 0.1, 0.1, 0.3)).apply { + icon = { + Components.item(Items.SUGAR.defaultStack) + } + } + + init { + client { + this.keyBind = HeroKeyBindings.secondKeyBind + ClientEvents.cameraClipToSpaceEvent.listen { event -> + val player = MinecraftClient.getInstance().player ?: return@listen + if (player.isEarthSurfing()) { + event.value = 8.0 + } + } + } + + this.properties = listOf(earthSurfStepHeight, earthSurfSpeedBoost, earthSurfRadius) + + this.cooldownProperty = + buildCooldown(60.0, 5, AddValueTotal(-5.0, -5.0, -5.0, -5.0,-5.0)) + this.maxDurationProperty = + buildMaxDuration(5.0, 5, AddValueTotal(0.1, 0.4, 0.2, 0.8, 1.5, 1.0)) + + syncedValueChangeEvent.listen { + val player = it.entity as? PlayerEntity ?: return@listen + if (it.key == EarthSurfKey) { + if (player.isEarthSurfing()) { + player.attributes.getCustomInstance(EntityAttributes.GENERIC_STEP_HEIGHT)?.baseValue = + earthSurfStepHeight.getValue(player.uuid) + if (player.world.isClient) { + MinecraftClient.getInstance().soundManager.play(StoneSlideSoundInstance(player)) + } + } else { + player.attributes.getCustomInstance(EntityAttributes.GENERIC_STEP_HEIGHT)?.baseValue = 0.6 + } + } + } + + ServerTickEvents.START_SERVER_TICK.register { server -> + for (player in server.players.filter { it.isEarthSurfing() }) { + player.spawnEarthCircle() + } + } + } + + override fun getIconComponent(): Component { + return Components.item(itemStack(Items.IRON_BOOTS) {}) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_mud.png") + } + + val EARTH_SURF_SPEED_BOOST = EntityAttributeModifier( + "earth_surf".toId(), + 1.3, + EntityAttributeModifier.Operation.ADD_MULTIPLIED_TOTAL + ) + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + cleanUp(player) + } + + private fun cleanUp(player: PlayerEntity) { + if (player is ServerPlayerEntity) { + player.stopEmote("earth-surfing".toEmote()) + player.setSyncedData(EarthSurfKey, false) + player.getAttributeInstance(EntityAttributes.GENERIC_STEP_HEIGHT)?.baseValue = 0.6 + player.getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED)?.removeModifier(EARTH_SURF_SPEED_BOOST.id) + } else if (MinecraftClient.getInstance().player == player) { + player.showSpeedlines = false + } + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + player.playEmote("earth-surfing".toEmote()) + player.setSyncedData(EarthSurfKey, true) + runCatching { + player.getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED) + ?.addTemporaryModifier( + EntityAttributeModifier( + "earth_surf".toId(), + earthSurfSpeedBoost.getValue(player.uuid), + EntityAttributeModifier.Operation.ADD_MULTIPLIED_TOTAL + ) + ) + } + player.sound(SoundRegistry.EARTH_ARMOR) + cameraShakePacket.send(BoomShake(0.1, 0.2, 0.4), player as ServerPlayerEntity) + } else if (player == MinecraftClient.getInstance().player) { + player.showSpeedlines = true + } + } + + override fun onEnd(player: PlayerEntity, abilityEndInformation: AbilityEndInformation) { + super.onEnd(player, abilityEndInformation) + cleanUp(player) + } +} + +fun PlayerEntity.isEarthSurfing() = getSyncedData(EarthSurfKey) == true + +val Entity.bodyDirectionVector: Vec3d + get() { + val rotY = Math.toRadians(yaw.toDouble()) + val rotX = Math.toRadians(0.0) + val xz = cos(rotX) + return Vec3d(-xz * sin(rotY), -sin(rotX), xz * cos(rotY)) + } + +fun PlayerEntity.spawnEarthCircle() { + val radius = earthSurfRadius.getValue(uuid).toInt() + this.pos.add(0.0, 0.0, 0.0).add( + this.bodyDirectionVector.normalize().multiply(-(radius.toDouble() + 2)) + ).toBlockPos() + .filledSpherePositionSet(radius) + .forEach { + val blockState = world.getBlockState(it) + if (!blockState.isAir) { + if (calculateProbability(5.0)) { + world.playSound( + null, + it, + SoundRegistry.STONE_SMASH, + SoundCategory.BLOCKS, + 1f, + (0.9..1.4).random().toFloat() + ) + } + if (calculateProbability(25.0)) { + (this as? ServerPlayerEntity?)?.apply { + cameraShakePacket.send(BoomShake(0.1, 0.2, 0.4), this) + } + } + + val blockPos = it + + val fallingBlock = FallingBlockEntity.spawnFromBlock(world, blockPos, blockState) + fallingBlock.modifyVelocity(Vec3d(0.0, 0.5, 0.0)) + fallingBlock.dropItem = false + + + world.setBlockState(it, Blocks.AIR.defaultState) + val particlePos = it.toCenterPos() + (world as? ServerWorld?)?.spawnParticles( + ParticleRegistry.EARTH_DUST, + particlePos.x, + particlePos.y + 1, + particlePos.z, + 7, + (0.1..0.4).random(), + (0.1..0.4).random(), + (0.1..0.4).random(), + (0.01..0.04).random() + ) + } + } + /* this.pos.add(0.0, 1.0, 0.0).add( + this.bodyDirectionVector.normalize().multiply(-(radius.toDouble() + 1)) + ).toBlockPos() + .circlePositionSet(radius) + .forEach { + val particlePos = it.toCenterPos() + (world as? ServerWorld?)?.spawnParticles( + ParticleRegistry.EARTH_DUST, + particlePos.x.toDouble(), + particlePos.y - 0.3, + particlePos.z.toDouble(), + if (radius == 1) 1 else 5, + (0.1..0.4).random(), + (0.1..0.4).random(), + (0.1..0.4).random(), + (0.01..0.04).random() + ) + }*/ +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthTrapAbility.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthTrapAbility.kt new file mode 100644 index 0000000..2b2b3f9 --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/EarthTrapAbility.kt @@ -0,0 +1,197 @@ +package gg.norisk.heroes.toph.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.datatracker.entity.syncedValueChangeEvent +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.common.HeroesManager.client +import gg.norisk.heroes.common.ability.CooldownProperty +import gg.norisk.heroes.common.ability.NumberProperty +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.PressAbility +import gg.norisk.heroes.common.networking.BoomShake +import gg.norisk.heroes.common.networking.cameraShakePacket +import gg.norisk.heroes.common.networking.dto.AnimationInterpolator +import gg.norisk.heroes.common.utils.RaycastUtils +import gg.norisk.heroes.common.utils.random +import gg.norisk.heroes.common.utils.sound +import gg.norisk.heroes.toph.TophManager.isEarthBlock +import gg.norisk.heroes.toph.entity.ITrappedEntity +import gg.norisk.heroes.toph.registry.ParticleRegistry +import gg.norisk.heroes.toph.registry.SoundRegistry +import gg.norisk.heroes.toph.render.BlockTrapFeatureRenderer +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityFeatureRendererRegistrationCallback +import net.minecraft.client.render.entity.feature.FeatureRendererContext +import net.minecraft.client.render.entity.model.EntityModel +import net.minecraft.client.render.entity.model.EntityModelLayers +import net.minecraft.entity.Entity +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.attribute.EntityAttributeModifier +import net.minecraft.entity.attribute.EntityAttributes +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.math.Box +import net.silkmc.silk.core.entity.posUnder +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.literalText +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toJavaDuration + +val EarthTrappedKey = "isEarthTrapped" + +val EarthTrapAbility = object : PressAbility("Earth Trap") { + //TODO player based + val distance = 64.0 + + val earthTrapMaxDurationProperty = CooldownProperty( + 2.0, 3, + "Max Duration", + AddValueTotal(1.0, 1.0, 3.0) + ) + val earthTrapSlownessProperty = NumberProperty( + -0.7, 3, + "Slowness", + AddValueTotal(-0.1, -0.2, -0.3), + ).apply { + icon = { + Components.item(Items.COBWEB.defaultStack) + } + } + + init { + client { + this.keyBind = HeroKeyBindings.pickItemKeyBinding + //TODO condition sneaking + + LivingEntityFeatureRendererRegistrationCallback.EVENT.register { entityType, entityRenderer, registrationHelper, context -> + val modelPart = EntityModelLayers.getLayers().toList() + .firstOrNull { it.id.path == entityType.untranslatedName.lowercase() } + if (modelPart != null) { + val model = context.getPart(modelPart) + registrationHelper.register( + BlockTrapFeatureRenderer( + entityRenderer as FeatureRendererContext>, + context.heldItemRenderer, + model + ) + ) + } + } + } + + this.properties = listOf(earthTrapMaxDurationProperty, earthTrapSlownessProperty) + + this.condition = { + it.isSneaking && it.world.getBlockState(it.posUnder).isEarthBlock + } + + this.cooldownProperty = + buildCooldown(10.0, 5, AddValueTotal(-0.1, -0.4, -0.2, -0.8, -1.5, -1.0)) + + syncedValueChangeEvent.listen { + if (it.key == EarthTrappedKey) { + if (it.entity.isEarthTrapped()) { + (it.entity as ITrappedEntity).earthRotationAnimation = AnimationInterpolator( + 0f, + 360f, + 0.8.seconds.toJavaDuration(), + AnimationInterpolator.Easing.CUBIC_IN + ) + } else { + (it.entity as ITrappedEntity).earthRotationAnimation = AnimationInterpolator( + 360f, + 0f, + 0.8.seconds.toJavaDuration(), + AnimationInterpolator.Easing.CUBIC_IN + ) + } + } + } + } + + override fun getIconComponent(): Component { + return Components.item(Items.SAND.defaultStack) + } + + override fun getUnlockCondition(): Text { + return literalText { + text(Text.translatable("heroes.ability.$internalKey.unlock_condition")) + } + } + + override fun hasUnlocked(player: PlayerEntity): Boolean { + return player.isCreative || (EarthPushAbility.cooldownProperty.isMaxed(player.uuid)) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_mud.png") + } + + val EARTH_TRAP_SLOW_BOOST = EntityAttributeModifier( + Identifier.of("earth_trap"), + -0.7, + EntityAttributeModifier.Operation.ADD_MULTIPLIED_TOTAL + ) + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + if (player is ServerPlayerEntity) { + if (player.isSneaking && player.world.getBlockState(player.posUnder).isEarthBlock) { + val world = player.world as ServerWorld + val clipWithDistance = RaycastUtils.clipWithDistance(player, player.world, distance) ?: return + cameraShakePacket.send(BoomShake(0.1, 0.2, 0.4), player as ServerPlayerEntity) + player.sound(SoundRegistry.STONE_SMASH) + world.getOtherEntities(player, Box.from(clipWithDistance.pos).expand(5.0)).forEach { + val entity = it + if (!entity.isEarthTrapped() && world.getBlockState(entity.posUnder).isEarthBlock) { + entity.sound(SoundRegistry.EARTH_COLUMN_1) + entity.setSyncedData(EarthTrappedKey, true) + (entity as? LivingEntity?)?.apply { + getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED)?.addTemporaryModifier( + EntityAttributeModifier( + Identifier.of("earth_trap"), + earthTrapSlownessProperty.getValue(player.uuid), + EntityAttributeModifier.Operation.ADD_MULTIPLIED_TOTAL + ) + ) + } + //TODO player based duration + repeat(5) { _ -> + (world as? ServerWorld?)?.spawnParticles( + ParticleRegistry.EARTH_DUST, + it.x, + it.y, + it.z, + 7, + (0.01..0.04).random(), + (0.01..0.04).random(), + (0.01..0.04).random(), + (0.01..0.04).random() + ) + } + mcCoroutineTask( + sync = true, + client = false, + delay = earthTrapMaxDurationProperty.getValue(player.uuid).seconds + ) { + entity.setSyncedData(EarthTrappedKey, false) + (entity as? LivingEntity?)?.apply { + getAttributeInstance(EntityAttributes.GENERIC_MOVEMENT_SPEED)?.removeModifier( + EARTH_TRAP_SLOW_BOOST.id + ) + } + } + } + } + } + } + } +} + +fun Entity.isEarthTrapped() = getSyncedData(EarthTrappedKey) == true diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/SeismicSenseAbility.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/SeismicSenseAbility.kt new file mode 100644 index 0000000..1aee1ac --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/ability/SeismicSenseAbility.kt @@ -0,0 +1,252 @@ +package gg.norisk.heroes.toph.ability + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.datatracker.entity.syncedValueChangeEvent +import gg.norisk.emote.network.EmoteNetworking.playEmote +import gg.norisk.heroes.client.option.HeroKeyBindings +import gg.norisk.heroes.client.renderer.BlockOutlineRenderer +import gg.norisk.heroes.common.HeroesManager.client +import gg.norisk.heroes.common.ability.operation.AddValueTotal +import gg.norisk.heroes.common.hero.ability.AbilityScope +import gg.norisk.heroes.common.hero.ability.implementation.PressAbility +import gg.norisk.heroes.common.networking.BoomShake +import gg.norisk.heroes.common.networking.cameraShakePacket +import gg.norisk.heroes.common.utils.SphereUtils +import gg.norisk.heroes.common.utils.toVec +import gg.norisk.heroes.toph.TophManager.isEarthBlock +import gg.norisk.heroes.toph.TophManager.toEmote +import gg.norisk.heroes.toph.TophManager.toId +import gg.norisk.heroes.toph.entity.toph +import gg.norisk.heroes.toph.mixin.render.GameRendererAccessor +import gg.norisk.heroes.toph.registry.SoundRegistry +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.core.Component +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents +import net.minecraft.block.BlockState +import net.minecraft.client.MinecraftClient +import net.minecraft.client.world.ClientWorld +import net.minecraft.entity.Entity +import net.minecraft.entity.effect.StatusEffectInstance +import net.minecraft.entity.effect.StatusEffects +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.Items +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundCategory +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box +import net.minecraft.util.math.Direction +import net.minecraft.world.World +import net.silkmc.silk.core.entity.posUnder +import net.silkmc.silk.core.kotlin.ticks +import net.silkmc.silk.core.task.mcCoroutineTask +import net.silkmc.silk.core.text.literalText +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable +import kotlin.time.Duration.Companion.seconds + +val SeismicSenseShader = "shaders/post/seismic_sense.json".toId() +val SeismicSenseKey = "hasSeismicSense" + +val SeismicSenseAbility = object : PressAbility("Seismic Sense") { + init { + client { + this.keyBind = HeroKeyBindings.fourthKeyBinding + + WorldRenderEvents.AFTER_TRANSLUCENT.register { context -> + val world = context.world() + val player = MinecraftClient.getInstance().player ?: return@register + val matrices = context.matrixStack() ?: return@register + player.toph.seismicBlocks.removeIf { (timestamp, _) -> timestamp < System.currentTimeMillis() } + player.toph.seismicBlocks.forEach { (_, pos) -> + val blockState: BlockState = world.getBlockState(pos) + if (!blockState.isAir && world.worldBorder.contains(pos) && blockState.isSolid) { + BlockOutlineRenderer.drawBlockBox( + matrices, + context.consumers() ?: return@forEach, + pos, + 1.0f, + 1.0f, + 1.0f, + 0.4f + ) + } + } + } + } + + syncedValueChangeEvent.listen { event -> + if (event.key != SeismicSenseKey) return@listen + val player = event.entity as? PlayerEntity? ?: return@listen + if (event.entity.world is ServerWorld) { + + } else if (event.entity == MinecraftClient.getInstance().player) { + if (player.hasSeismicSense) { + player.toph.toph_seismicTasks += mcCoroutineTask(sync = true, client = true, delay = 0.32.seconds) { + val gameRenderer = MinecraftClient.getInstance().gameRenderer as GameRendererAccessor + gameRenderer.invokeLoadPostProcessor(SeismicSenseShader) + player.spawnSeismicSenseGlowCircle() + player.toph.toph_seismicTasks += mcCoroutineTask(sync = true, delay = 90.ticks, client = true) { + MinecraftClient.getInstance().gameRenderer.disablePostProcessor() + } + } + } else { + cleanUp(player) + } + } + } + + this.cooldownProperty = + buildCooldown(10.0, 5, AddValueTotal(-0.1, -0.4, -0.2, -0.8, -1.5, -1.0)) + } + + override fun hasUnlocked(player: PlayerEntity): Boolean { + return player.isCreative || (EarthPushAbility.cooldownProperty.isMaxed(player.uuid)) + } + + override fun getUnlockCondition(): Text { + return literalText { + text(Text.translatable("heroes.ability.$internalKey.unlock_condition")) + } + } + + override fun getIconComponent(): Component { + return Components.item(Items.ENDER_EYE.defaultStack) + } + + override fun getBackgroundTexture(): Identifier { + return Identifier.of("textures/block/packed_mud.png") + } + + override fun onDisable(player: PlayerEntity) { + super.onDisable(player) + cleanUp(player) + } + + private fun cleanUp(player: PlayerEntity) { + player.toph.toph_seismicTasks.forEach { it.cancel() } + player.removeStatusEffect(StatusEffects.DARKNESS) + player.removeStatusEffect(StatusEffects.SLOWNESS) + player.toph.seismicBlocks.clear() + player.toph.seismicEntities.clear() + if (player is ServerPlayerEntity) { + player.hasSeismicSense = false + } else { + MinecraftClient.getInstance().gameRenderer.disablePostProcessor() + } + } + + override fun onStart(player: PlayerEntity, abilityScope: AbilityScope) { + super.onStart(player, abilityScope) + if (player is ServerPlayerEntity) { + if (player.hasSeismicSense) { + cleanUp(player) + } else { + abilityScope.cancelCooldown() + val world = player.world as ServerWorld + player.playEmote("seismic-sense".toEmote()) + player.addStatusEffect(StatusEffectInstance(StatusEffects.SLOWNESS, 140, 2, false, false)) + player.toph.toph_seismicTasks += mcCoroutineTask(sync = true, client = false, delay = 0.32.seconds) { + player.addStatusEffect(StatusEffectInstance(StatusEffects.DARKNESS, 140, 1, false, false)) + player.setSyncedData(SeismicSenseKey, true) + cameraShakePacket.send(BoomShake(0.1, 0.2, 0.4), player) + world.playSoundFromEntity( + null, + player, + SoundRegistry.SEISMIC_SENSE_START, + SoundCategory.PLAYERS, + 2f, + 1f + ) + world.playSoundFromEntity( + null, + player, + SoundRegistry.EARTH_ARMOR, + SoundCategory.PLAYERS, + 0.4f, + 2f + ) + player.toph.toph_seismicTasks += mcCoroutineTask(sync = true, delay = 90.ticks, client = false) { + player.removeStatusEffect(StatusEffects.DARKNESS) + player.removeStatusEffect(StatusEffects.SLOWNESS) + player.hasSeismicSense = false + addCooldown(player) + } + } + } + } + } +} + +fun Entity.handleSeismicSenseOutline(cir: CallbackInfoReturnable) { + val player = MinecraftClient.getInstance().player ?: return + player.toph.seismicEntities.removeIf { (timestamp, _) -> timestamp < System.currentTimeMillis() } + if (player.toph.seismicEntities.any { (_, uuid) -> uuid == this.uuid } && player.hasSeismicSense) { + cir.returnValue = true + } +} + +fun World.isVisibleVonAtleastEinerSeite(blockPos: BlockPos): Boolean { + return Direction.values().any { direction -> getBlockState(blockPos.offset(direction)).isAir } +} + +fun PlayerEntity.spawnSeismicSenseGlowCircle() { + toph.seismicBlocks.clear() + val player = this + val world = this.world as ClientWorld + + val maxRadius = 50 + player.toph.toph_seismicTasks += mcCoroutineTask(sync = true, client = true, howOften = 4, period = 10.ticks) { + world.playSoundFromEntity( + player, + player, + SoundRegistry.SEISMIC_SENSE_WAVE, + SoundCategory.PLAYERS, + 0.5f, + 1f + ) + repeat(maxRadius) { radius -> + player.toph.toph_seismicTasks += mcCoroutineTask(delay = radius.ticks, client = true, sync = true) { + if (world.getBlockState(player.posUnder).isEarthBlock && hasSeismicSense) { + SphereUtils.generateSphere(player.posUnder, radius, true).forEach { + val blockState = world.getBlockState(it) + /*if (blockState.isAir) { + getNextBottomBlock(world, it)?.apply { + seismicBlocks.add(Pair(System.currentTimeMillis() + 50L, this)) + } + }*/ + if (!blockState.isAir) { + if (world.isVisibleVonAtleastEinerSeite(it)) { + player.toph.seismicEntities.addAll( + world.getOtherEntities( + player, + Box.from(it.toVec()).expand(2.0) + ).map { entity -> entity.uuid }.map { Pair(System.currentTimeMillis() + 1000L, it) } + ) + player.toph.seismicBlocks.add(Pair(System.currentTimeMillis() + 50L, it)) + } + } + } + } + } + } + } +} + +var PlayerEntity.hasSeismicSense: Boolean + get() = this.getSyncedData(SeismicSenseKey) ?: false + set(value) = this.setSyncedData(SeismicSenseKey, value) + +fun handleSeismicSenseShader(ci: CallbackInfo) { + val player = MinecraftClient.getInstance().player ?: return + if (player.hasSeismicSense) { + ci.cancel() + } +} + +fun PlayerEntity.handleSeismicSenseDarkness(original: Float): Float { + return original * 4 +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/BendingBlockEntity.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/BendingBlockEntity.kt new file mode 100644 index 0000000..bbe91ec --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/BendingBlockEntity.kt @@ -0,0 +1,164 @@ +package gg.norisk.heroes.toph.entity + +import gg.norisk.datatracker.entity.getSyncedData +import gg.norisk.datatracker.entity.setSyncedData +import gg.norisk.heroes.common.utils.random +import gg.norisk.heroes.common.utils.toVector +import gg.norisk.heroes.toph.ability.earthPushDamage +import gg.norisk.heroes.toph.registry.ParticleRegistry +import gg.norisk.heroes.toph.registry.SoundRegistry +import net.minecraft.block.BlockState +import net.minecraft.entity.EntityType +import net.minecraft.entity.FallingBlockEntity +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.MovementType +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.particle.BlockStateParticleEffect +import net.minecraft.particle.ParticleTypes +import net.minecraft.server.world.ServerWorld +import net.minecraft.sound.SoundCategory +import net.minecraft.util.math.Vec3d +import net.minecraft.world.World +import net.minecraft.world.explosion.Explosion +import net.silkmc.silk.core.entity.modifyVelocity +import org.joml.Vector3f +import java.util.* + +class BendingBlockEntity(world: World, x: Double, y: Double, z: Double, blockState: BlockState) : + FallingBlockEntity(EntityType.FALLING_BLOCK, world) { + var canAttack: Boolean = false + + + companion object { + val NULL = Vector3f() + + private val OWNER_KEY = "BendingBlock:Owner" + private val TARGET_KEY = "BendingBlock:TargetPos" + var FallingBlockEntity.owner: UUID? + get() = this.getSyncedData(OWNER_KEY) + set(value) = this.setSyncedData(OWNER_KEY, value) + + var FallingBlockEntity.targetPos: Vector3f + get() = this.getSyncedData(TARGET_KEY) ?: NULL + set(value) = this.setSyncedData(TARGET_KEY, value) + } + + + init { + block = blockState + intersectionChecked = true + setPosition(x, y, z) + velocity = Vec3d.ZERO + prevX = x + prevY = y + prevZ = z + } + + override fun isImmuneToExplosion(explosion: Explosion): Boolean { + return true + } + + override fun isCollidable(): Boolean { + return true + } + + fun forcePush(player: PlayerEntity) { + val target = player.raycast(64.0, 0f, false) + targetPos = target.pos.toVector3f() + canAttack = true + + var direction = targetPos.toVector().subtract(pos) + direction = direction.normalize().multiply(2.0) + + modifyVelocity(direction) + } + + + override fun tick() { + if (!world.isClient) { + if (block.isAir) { + discard() + return + } + val block = block.block + ++timeFalling + if (!hasNoGravity()) { + velocity = velocity.add(0.0, -0.04, 0.0) + } + move(MovementType.SELF, velocity) + + + velocity = velocity.multiply(0.98) + + if (pos.distanceTo(targetPos.toVector()) <= 1) { + targetPos = NULL + velocity = velocity.multiply(0.01) + } + + if (velocity.y <= 0) { + setNoGravity(true) + } + + if (world is ServerWorld && (velocity.lengthSquared() > 0.1)) { + (world as ServerWorld).spawnParticles( + BlockStateParticleEffect(ParticleTypes.BLOCK, blockState), + this.x, + this.y, + this.z, + if (canAttack) 7 else 2, + (this.width / 4.0f).toDouble(), + (this.height / 4.0f).toDouble(), + (this.width / 4.0f).toDouble(), + 0.05 + ) + } + + if (canAttack) { + explode(horizontalCollision) + } + } + } + + private fun explode(force: Boolean = false) { + var flag = force + val owner = world.getPlayerByUuid(owner ?: return) ?: return + for (enemy in this.world.getEntitiesByClass( + LivingEntity::class.java, + this.boundingBox.expand(1.1) + ) { + it.isAlive && it.uuid != this.owner + }) { + flag = true + enemy.damage(this.damageSources.playerAttack(owner), earthPushDamage.getValue(owner.uuid).toFloat()) + } + + if (flag) { + world.playSound(null, blockPos, SoundRegistry.EARTH_ARMOR, SoundCategory.BLOCKS, 1f, 1f) + this.discard() + repeat(10) { + (world as ServerWorld).spawnParticles( + BlockStateParticleEffect(ParticleTypes.BLOCK, blockState), + this.x, + this.y, + this.z, + if (canAttack) 7 else 2, + (this.width / 4.0f).toDouble(), + (this.height / 4.0f).toDouble(), + (this.width / 4.0f).toDouble(), + 0.05 + ) + } + (world as? ServerWorld?)?.spawnParticles( + ParticleRegistry.EARTH_DUST, + this.x, + this.y, + this.z, + 7, + (this.width / 4.0f).toDouble(), + (this.height / 4.0f).toDouble(), + (this.width / 4.0f).toDouble(), + (0.01..0.04).random() + ) + } + } +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/IBendingItemEntity.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/IBendingItemEntity.kt new file mode 100644 index 0000000..9ef8092 --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/IBendingItemEntity.kt @@ -0,0 +1,7 @@ +package gg.norisk.heroes.toph.entity + +import java.util.* + +interface IBendingItemEntity { + var bender: UUID? +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/ITophPlayer.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/ITophPlayer.kt new file mode 100644 index 0000000..888b4c0 --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/ITophPlayer.kt @@ -0,0 +1,14 @@ +package gg.norisk.heroes.toph.entity + +import kotlinx.coroutines.Job +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.util.math.BlockPos +import java.util.* + +interface ITophPlayer { + val seismicBlocks: MutableSet> + val seismicEntities: MutableSet> + val toph_seismicTasks: MutableList +} + +val PlayerEntity.toph get() = this as ITophPlayer diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/ITrappedEntity.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/ITrappedEntity.kt new file mode 100644 index 0000000..d56b616 --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/entity/ITrappedEntity.kt @@ -0,0 +1,7 @@ +package gg.norisk.heroes.toph.entity + +import gg.norisk.heroes.common.networking.dto.AnimationInterpolator + +interface ITrappedEntity { + var earthRotationAnimation: AnimationInterpolator? +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/network/TophC2SPackets.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/network/TophC2SPackets.kt new file mode 100644 index 0000000..bbeffa8 --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/network/TophC2SPackets.kt @@ -0,0 +1,7 @@ +package gg.norisk.heroes.toph.network + +import gg.norisk.heroes.toph.TophManager.toId +import gg.norisk.heroes.toph.ability.EarthColumnDescription +import net.silkmc.silk.network.packet.c2sPacket + +val earthColumnBlockInfos = c2sPacket("earth_column_block_infos".toId()) diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/particle/EarthDustParticle.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/particle/EarthDustParticle.kt new file mode 100644 index 0000000..7789bdb --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/particle/EarthDustParticle.kt @@ -0,0 +1,71 @@ +package gg.norisk.heroes.toph.particle + +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.particle.* +import net.minecraft.client.world.ClientWorld +import net.minecraft.particle.ParticleEffect +import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry +import net.minecraft.particle.ParticleType + +@Environment(EnvType.CLIENT) +class EarthDustParticle internal constructor( + clientWorld: ClientWorld, + d: Double, + e: Double, + f: Double, + g: Double, + h: Double, + i: Double, + bl: Boolean +) : SpriteBillboardParticle(clientWorld, d, e, f) { + init { + this.scale(3.0f) + this.setBoundingBoxSpacing(0.25f, 0.25f) + this.maxAge = random.nextInt(50) + this.gravityStrength = 3.0E-6f + this.velocityX = g + this.velocityY = h + this.velocityZ = i + } + + override fun tick() { + this.prevPosX = this.x + this.prevPosY = this.y + this.prevPosZ = this.z + if (age++ < this.maxAge && !(this.alpha <= 0.0f)) { + this.velocityX += (random.nextFloat() / 5000.0f * (if (random.nextBoolean()) 1 else -1).toFloat()).toDouble() + this.velocityZ += (random.nextFloat() / 5000.0f * (if (random.nextBoolean()) 1 else -1).toFloat()).toDouble() + this.velocityY -= gravityStrength.toDouble() + this.move(this.velocityX, this.velocityY, this.velocityZ) + if (this.age >= this.maxAge - 60 && this.alpha > 0.01f) { + this.alpha -= 0.015f + } + } else { + this.markDead() + } + } + + override fun getType(): ParticleTextureSheet { + return ParticleTextureSheet.PARTICLE_SHEET_TRANSLUCENT + } + + @Environment(EnvType.CLIENT) + class CosySmokeFactory(private val spriteProvider: SpriteProvider) : ParticleFactory { + override fun createParticle( + defaultParticleType: ParticleEffect, + clientWorld: ClientWorld, + d: Double, + e: Double, + f: Double, + g: Double, + h: Double, + i: Double + ): Particle { + val campfireSmokeParticle = EarthDustParticle(clientWorld, d, e, f, g, h, i, false) + campfireSmokeParticle.setAlpha(0.9f) + campfireSmokeParticle.setSprite(this.spriteProvider) + return campfireSmokeParticle + } + } +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/registry/ParticleRegistry.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/registry/ParticleRegistry.kt new file mode 100644 index 0000000..6e159bc --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/registry/ParticleRegistry.kt @@ -0,0 +1,20 @@ +package gg.norisk.heroes.toph.registry + +import gg.norisk.heroes.toph.TophManager.toId +import net.fabricmc.fabric.api.particle.v1.FabricParticleTypes +import net.minecraft.particle.ParticleEffect +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry + +object ParticleRegistry { + val EARTH_DUST = register("earth_dust") + + fun init() { + } + + private fun register( + name: String + ): ParticleEffect { + return Registry.register(Registries.PARTICLE_TYPE, name.toId(), FabricParticleTypes.simple()) + } +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/registry/SoundRegistry.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/registry/SoundRegistry.kt new file mode 100644 index 0000000..0fe3b3a --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/registry/SoundRegistry.kt @@ -0,0 +1,21 @@ +package gg.norisk.heroes.toph.registry + +import gg.norisk.heroes.toph.TophManager.toId +import net.minecraft.registry.Registries +import net.minecraft.registry.Registry +import net.minecraft.sound.SoundEvent + +object SoundRegistry { + var EARTH_ARMOR = "earth_armor".register() + var SEISMIC_SENSE_START = "seismic_sense_start".register() + var SEISMIC_SENSE_WAVE = "seismic_sense_wave".register() + var EARTH_COLUMN_1 = "earth_column_1".register() + var ARM_WHOOSH = "arm_whoosh".register() + var STONE_SMASH = "stone_smash".register() + var STONE_SLIDE = "stone_slide".register() + + fun init() { + } + + private fun String.register() = Registry.register(Registries.SOUND_EVENT, this.toId(), SoundEvent.of(this.toId())) +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/render/BlockTrapFeatureRenderer.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/render/BlockTrapFeatureRenderer.kt new file mode 100644 index 0000000..fdd628d --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/render/BlockTrapFeatureRenderer.kt @@ -0,0 +1,135 @@ +package gg.norisk.heroes.toph.render + +import gg.norisk.heroes.toph.ability.isEarthTrapped +import gg.norisk.heroes.toph.entity.ITrappedEntity +import gg.norisk.heroes.toph.mixin.ModelPartAccessor +import net.minecraft.block.Blocks +import net.minecraft.client.model.ModelPart +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.entity.feature.FeatureRenderer +import net.minecraft.client.render.entity.feature.FeatureRendererContext +import net.minecraft.client.render.entity.model.EntityModel +import net.minecraft.client.render.item.HeldItemRenderer +import net.minecraft.client.render.model.json.ModelTransformationMode +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.LivingEntity +import net.minecraft.util.math.RotationAxis +import net.minecraft.util.math.random.Random + + +//Credits an https://github.com/chyzman/wearThat/blob/master/src/main/java/com/chyzman/wearthat/client/WearThatClient.java +class BlockTrapFeatureRenderer>( + context: FeatureRendererContext, + private val heldItemRenderer: HeldItemRenderer, + val root: ModelPart, +) : FeatureRenderer(context) { + val legs = (root as ModelPartAccessor).children.filter { + it.key.contains("leg") || it.key.contains("tentacle") || it.key.contains("rod") + }.map { it.value } + + override fun render( + matrices: MatrixStack, + vertexConsumers: VertexConsumerProvider, + light: Int, + entity: T, + limbAngle: Float, + limbDistance: Float, + tickDelta: Float, + animationProgress: Float, + headYaw: Float, + headPitch: Float + ) { + val trapped = entity as? ITrappedEntity? ?: return + if (!entity.isEarthTrapped()) { + if (trapped.earthRotationAnimation == null || trapped.earthRotationAnimation?.isDone == true) { + return + } + } + + var current: Class<*> = this.contextModel::class.java + while (current.superclass != null) { // we don't want to process Object.class + current.declaredFields.forEach { field -> + runCatching { + field.isAccessible = true + field.get(this.contextModel) as ModelPart + }.onSuccess { + val random = Random.create() + if (it.isEmpty) return@onSuccess + if (legs.any { leg -> compareCuboids(leg.getRandomCuboid(random), it.getRandomCuboid(random)) }) { + it.renderBlock(matrices, entity, vertexConsumers, light) + } + } + runCatching { + field.isAccessible = true + field.get(this.contextModel) as Array + }.onSuccess { modelParts -> + for (it in modelParts) { + if (it.isEmpty) return@onSuccess + val random = Random.create() + if (legs.any { leg -> + compareCuboids( + leg.getRandomCuboid(random), + it.getRandomCuboid(random) + ) + }) { + it.renderBlock(matrices, entity, vertexConsumers, light) + } + } + } + } + current = current.superclass + } + } + + fun compareCuboids(cuboid1: ModelPart.Cuboid, cuboid2: ModelPart.Cuboid): Boolean { + return cuboid1.minX == cuboid2.minX && + cuboid1.minY == cuboid2.minY && + cuboid1.minZ == cuboid2.minZ && + cuboid1.maxX == cuboid2.maxX && + cuboid1.maxY == cuboid2.maxY && + cuboid1.maxZ == cuboid2.maxZ + } + + private fun ModelPart.renderBlock( + matrices: MatrixStack, + entity: T, + vertexConsumers: VertexConsumerProvider, + light: Int + ) { + val trapped = entity as? ITrappedEntity ?: return + val earthRotation = trapped.earthRotationAnimation ?: return + + matrices.push() + rotate(matrices) + + var size = 0.0 + + forEachCuboid(matrices) { entry, string, i, cuboid -> + size = cuboid.maxY.toDouble() + } + + matrices.translate(0.0, size / 18, 0.0) // Position anpassen + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(earthRotation.get())) + + //val legHeight = it.cuboids.firstOrNull()?.dimensions?.y?.toDouble() ?: 0.0 + + val progress = if (entity.isEarthTrapped()) { + earthRotation.get() / earthRotation.end + } else { + earthRotation.get() / earthRotation.start + } + + val scale = progress + matrices.scale(scale, scale, scale) + heldItemRenderer.renderItem( + entity as LivingEntity, + Blocks.DIRT.asItem().defaultStack, + ModelTransformationMode.FIXED, + false, + matrices, + vertexConsumers, + light + ) + matrices.pop() + } +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/render/ChestItemFeatureRenderer.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/render/ChestItemFeatureRenderer.kt new file mode 100644 index 0000000..3c7c468 --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/render/ChestItemFeatureRenderer.kt @@ -0,0 +1,270 @@ +package gg.norisk.heroes.toph.render + +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.entity.feature.FeatureRenderer +import net.minecraft.client.render.entity.feature.FeatureRendererContext +import net.minecraft.client.render.entity.model.EntityModel +import net.minecraft.client.render.entity.model.PlayerEntityModel +import net.minecraft.client.render.item.HeldItemRenderer +import net.minecraft.client.render.model.json.ModelTransformationMode +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.entity.EquipmentSlot +import net.minecraft.entity.LivingEntity +import net.minecraft.item.Equipment +import net.minecraft.util.math.RotationAxis + +//Credits an https://github.com/chyzman/wearThat/blob/master/src/main/java/com/chyzman/wearthat/client/WearThatClient.java +class ChestItemFeatureRenderer>( + context: FeatureRendererContext, + private val heldItemRenderer: HeldItemRenderer +) : FeatureRenderer(context) { + override fun render( + matrices: MatrixStack, + vertexConsumers: VertexConsumerProvider, + light: Int, + entity: T, + limbAngle: Float, + limbDistance: Float, + tickDelta: Float, + animationProgress: Float, + headYaw: Float, + headPitch: Float + ) { + val mode = ModelTransformationMode.FIXED + val chestStack = (entity as LivingEntity).getEquippedStack(EquipmentSlot.CHEST) + if (!chestStack.isEmpty) { + if (Equipment.fromStack(chestStack)?.slotType != EquipmentSlot.CHEST) { + matrices.push() + (this.contextModel as PlayerEntityModel<*>).body.rotate(matrices) + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180f)) + matrices.translate(0f, -1 / 4f, 0f) + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180f)) + heldItemRenderer.renderItem( + entity as LivingEntity, + chestStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.scale(1.01f, 1.01f, 1.01f) + matrices.translate(0f, -1 / 4f, 0f) + heldItemRenderer.renderItem( + entity as LivingEntity, + chestStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.pop() + matrices.push() + (this.contextModel as PlayerEntityModel<*>).rightArm.rotate(matrices) + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180f)) + matrices.scale(2 / 3f, 2 / 3f, 2 / 3f) + matrices.translate(-1 / 12f, 0f, 0f) + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180f)) + heldItemRenderer.renderItem( + entity as LivingEntity, + chestStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.scale(0.99f, 0.99f, 0.99f) + matrices.translate(0f, -1 / 2f, 0f) + heldItemRenderer.renderItem( + entity as LivingEntity, + chestStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.pop() + matrices.push() + (this.contextModel as PlayerEntityModel<*>).rightArm.rotate(matrices) + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180f)) + matrices.scale(2 / 3f, 2 / 3f, 2 / 3f) + matrices.translate(-1 / 12f, 0f, 0f) + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180f)) + heldItemRenderer.renderItem( + entity as LivingEntity, + chestStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.scale(1.25f, 1.25f, 1.25f) + matrices.translate(0f, -0.75f, 0f) + heldItemRenderer.renderItem( + entity as LivingEntity, + chestStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.pop() + matrices.push() + (this.contextModel as PlayerEntityModel<*>).leftArm.rotate(matrices) + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180f)) + matrices.scale(2 / 3f, 2 / 3f, 2 / 3f) + matrices.translate(1 / 12f, 0f, 0f) + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180f)) + heldItemRenderer.renderItem( + entity as LivingEntity, + chestStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.scale(0.99f, 0.99f, 0.99f) + matrices.translate(0f, -1 / 2f, 0f) + heldItemRenderer.renderItem( + entity as LivingEntity, + chestStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.pop() + matrices.push() + (this.contextModel as PlayerEntityModel<*>).leftArm.rotate(matrices) + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180f)) + matrices.scale(2 / 3f, 2 / 3f, 2 / 3f) + matrices.translate(1 / 12f, 0f, 0f) + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180f)) + heldItemRenderer.renderItem( + entity as LivingEntity, + chestStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.scale(1.2f, 1.2f, 1.2f) + matrices.translate(0f, -0.75f, 0f) + heldItemRenderer.renderItem( + entity as LivingEntity, + chestStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.pop() + } + } + val legsStack = (entity as LivingEntity).getEquippedStack(EquipmentSlot.LEGS) + if (!legsStack.isEmpty) { + if (Equipment.fromStack(legsStack)?.slotType != EquipmentSlot.LEGS) { + matrices.push() + (this.contextModel as PlayerEntityModel<*>).rightLeg.rotate(matrices) + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180f)) + matrices.scale(2 / 3f, 2 / 3f, 2 / 3f) + matrices.translate(0f, -1 / 6f, 0f) + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180f)) + heldItemRenderer.renderItem( + entity as LivingEntity, + legsStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.scale(1.01f, 1.01f, 1.01f) + matrices.translate(0f, -1 / 3f, 0f) + heldItemRenderer.renderItem( + entity as LivingEntity, + legsStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.pop() + matrices.push() + (this.contextModel as PlayerEntityModel<*>).leftLeg.rotate(matrices) + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180f)) + matrices.scale(2 / 3f, 2 / 3f, 2 / 3f) + matrices.translate(0f, -1 / 6f, 0f) + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180f)) + heldItemRenderer.renderItem( + entity as LivingEntity, + legsStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.scale(1.01f, 1.01f, 1.01f) + matrices.translate(0f, -1 / 3f, 0f) + heldItemRenderer.renderItem( + entity as LivingEntity, + legsStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.pop() + } + } + val feetStack = (entity as LivingEntity).getEquippedStack(EquipmentSlot.FEET) + if (!feetStack.isEmpty) { + if (Equipment.fromStack(feetStack)?.slotType != EquipmentSlot.FEET) { + matrices.push() + (this.contextModel as PlayerEntityModel<*>).rightLeg.rotate(matrices) + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180f)) + matrices.scale(0.75f, 0.75f, 0.75f) + matrices.translate(0f, -0.8f, 0f) + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180f)) + heldItemRenderer.renderItem( + entity as LivingEntity, + feetStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.pop() + matrices.push() + (this.contextModel as PlayerEntityModel<*>).leftLeg.rotate(matrices) + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180f)) + matrices.scale(0.75f, 0.75f, 0.75f) + matrices.translate(0f, -0.8f, 0f) + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180f)) + heldItemRenderer.renderItem( + entity as LivingEntity, + feetStack, + mode, + false, + matrices, + vertexConsumers, + light + ) + matrices.pop() + } + } + } +} diff --git a/toph/src/main/kotlin/gg/norisk/heroes/toph/sound/StoneSlideSoundInstance.kt b/toph/src/main/kotlin/gg/norisk/heroes/toph/sound/StoneSlideSoundInstance.kt new file mode 100644 index 0000000..badd1ad --- /dev/null +++ b/toph/src/main/kotlin/gg/norisk/heroes/toph/sound/StoneSlideSoundInstance.kt @@ -0,0 +1,42 @@ +package gg.norisk.heroes.toph.sound + +import gg.norisk.heroes.toph.ability.isEarthSurfing +import gg.norisk.heroes.toph.registry.SoundRegistry +import net.minecraft.client.sound.MovingSoundInstance +import net.minecraft.client.sound.SoundInstance +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.sound.SoundCategory + +class StoneSlideSoundInstance(private val player: PlayerEntity) : + MovingSoundInstance(SoundRegistry.STONE_SLIDE, SoundCategory.PLAYERS, SoundInstance.createRandom()) { + + init { + repeat = true + repeatDelay = 0 + volume = 1f + } + + override fun tick() { + if (!player.isAlive || player.isRemoved) { + setDone() + } + + + x = player.x.toFloat().toDouble() + y = player.y.toFloat().toDouble() + z = player.z.toFloat().toDouble() + if (player.isEarthSurfing()) { + volume = 0.7f + } else { + volume -= 0.1f + } + + if (volume <= 0) { + setDone() + } + } + + override fun shouldAlwaysPlay(): Boolean { + return true + } +} diff --git a/toph/src/main/resources/assets/hero-api/textures/hero/toph/icon.png b/toph/src/main/resources/assets/hero-api/textures/hero/toph/icon.png new file mode 100644 index 0000000..a100bb3 Binary files /dev/null and b/toph/src/main/resources/assets/hero-api/textures/hero/toph/icon.png differ diff --git a/toph/src/main/resources/assets/toph/emotes/earth-armor.animation.json b/toph/src/main/resources/assets/toph/emotes/earth-armor.animation.json new file mode 100644 index 0000000..1a9eb53 --- /dev/null +++ b/toph/src/main/resources/assets/toph/emotes/earth-armor.animation.json @@ -0,0 +1,31 @@ +{ + "format_version": "1.8.0", + "animations": { + "earth-armor": { + "animation_length": 0.5417, + "bones": { + "bipedRightArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.125": { + "vector": [0, 0, 82.5] + } + } + }, + "bipedLeftArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.125": { + "vector": [0, 0, -82.5] + } + } + } + } + } + }, + "geckolib_format_version": 2 +} \ No newline at end of file diff --git a/toph/src/main/resources/assets/toph/emotes/earth-column-end.animation.json b/toph/src/main/resources/assets/toph/emotes/earth-column-end.animation.json new file mode 100644 index 0000000..ec3b8b8 --- /dev/null +++ b/toph/src/main/resources/assets/toph/emotes/earth-column-end.animation.json @@ -0,0 +1,55 @@ +{ + "format_version": "1.8.0", + "animations": { + "earth-column-end": { + "animation_length": 0.2917, + "bones": { + "bipedRightArm": { + "rotation": { + "vector": [-15, 0, -27.5] + }, + "position": { + "0.0": { + "vector": [-1, 1, -3] + }, + "0.125": { + "vector": [-1, -6, -5] + } + } + }, + "bipedLeftArm": { + "rotation": { + "vector": [-15, 0, 27.5] + }, + "position": { + "0.0": { + "vector": [1, 1, -3] + }, + "0.125": { + "vector": [1, -6, -5] + } + } + }, + "bipedRightLeg": { + "rotation": { + "0.0": { + "vector": [-22.5, 0, 0] + }, + "0.125": { + "vector": [-22.5, 0, 0] + } + }, + "position": { + "0.0": { + "vector": [0, 2, -7] + }, + "0.125": { + "vector": [0, -3, -10] + } + } + } + } + } + }, + "geckolib_format_version": 2 +} \ No newline at end of file diff --git a/toph/src/main/resources/assets/toph/emotes/earth-column-start.animation.json b/toph/src/main/resources/assets/toph/emotes/earth-column-start.animation.json new file mode 100644 index 0000000..2164e51 --- /dev/null +++ b/toph/src/main/resources/assets/toph/emotes/earth-column-start.animation.json @@ -0,0 +1,66 @@ +{ + "format_version": "1.8.0", + "animations": { + "earth-column-start": { + "loop": "hold_on_last_frame", + "animation_length": 0.125, + "bones": { + "bipedRightArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [-15, 0, -27.5] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [-1, 1, -3] + } + } + }, + "bipedLeftArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [-15, 0, 27.5] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [1, 1, -3] + } + } + }, + "bipedRightLeg": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [-22.5, 0, 0] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [0, 2, -7] + } + } + } + } + } + }, + "geckolib_format_version": 2 +} \ No newline at end of file diff --git a/toph/src/main/resources/assets/toph/emotes/earth-kick.animation.json b/toph/src/main/resources/assets/toph/emotes/earth-kick.animation.json new file mode 100644 index 0000000..72e8d3b --- /dev/null +++ b/toph/src/main/resources/assets/toph/emotes/earth-kick.animation.json @@ -0,0 +1,75 @@ +{ + "format_version": "1.8.0", + "animations": { + "earth-kick": { + "animation_length": 0.25, + "bones": { + "bipedRig": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [0, 65, 0] + } + } + }, + "bipedRightArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [0, 0, 107.5] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [-4, 0, 0] + } + } + }, + "bipedLeftArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [0, 0, -117.5] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [5, 0, 0] + } + } + }, + "bipedLeftLeg": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [0, 0, -125] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [3, 0, 0] + } + } + } + } + } + }, + "geckolib_format_version": 2 +} \ No newline at end of file diff --git a/toph/src/main/resources/assets/toph/emotes/earth-surfing.animation.json b/toph/src/main/resources/assets/toph/emotes/earth-surfing.animation.json new file mode 100644 index 0000000..743b48e --- /dev/null +++ b/toph/src/main/resources/assets/toph/emotes/earth-surfing.animation.json @@ -0,0 +1,17 @@ +{ + "format_version": "1.8.0", + "animations": { + "earth-surfing": { + "loop": "hold_on_last_frame", + "animation_length": 0.0833, + "bones": { + "bipedRig": { + "rotation": { + "vector": [20, 0, 0] + } + } + } + } + }, + "geckolib_format_version": 2 +} \ No newline at end of file diff --git a/toph/src/main/resources/assets/toph/emotes/seismic-sense.animation.json b/toph/src/main/resources/assets/toph/emotes/seismic-sense.animation.json new file mode 100644 index 0000000..265fa92 --- /dev/null +++ b/toph/src/main/resources/assets/toph/emotes/seismic-sense.animation.json @@ -0,0 +1,95 @@ +{ + "format_version": "1.8.0", + "animations": { + "seismice-sense": { + "animation_length": 0.5, + "bones": { + "bipedRightArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [0, 0, 20] + }, + "0.2917": { + "vector": [0, 0, 20] + }, + "0.4167": { + "vector": [0, 0, 20] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [-2, 4, 0] + }, + "0.2917": { + "vector": [-2, 4, 0] + }, + "0.4167": { + "vector": [0, -2, 0] + } + } + }, + "bipedLeftArm": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [0, 0, -20] + }, + "0.2917": { + "vector": [0, 0, -20] + }, + "0.4167": { + "vector": [0, 0, -20] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.0833": { + "vector": [2, 4, 0] + }, + "0.2917": { + "vector": [2, 4, 0] + }, + "0.4167": { + "vector": [0, -2, 0] + } + } + }, + "bipedRightLeg": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.125": { + "vector": [-22.5, 0, 0] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.125": { + "vector": [0, 5, -4] + }, + "0.2917": { + "vector": [0, 5, -4] + }, + "0.4167": { + "vector": [0, 0, -4] + } + } + } + } + } + }, + "geckolib_format_version": 2 +} \ No newline at end of file diff --git a/toph/src/main/resources/assets/toph/icon.png b/toph/src/main/resources/assets/toph/icon.png new file mode 100644 index 0000000..a100bb3 Binary files /dev/null and b/toph/src/main/resources/assets/toph/icon.png differ diff --git a/toph/src/main/resources/assets/toph/lang/de_de.json b/toph/src/main/resources/assets/toph/lang/de_de.json new file mode 100644 index 0000000..da4bf4d --- /dev/null +++ b/toph/src/main/resources/assets/toph/lang/de_de.json @@ -0,0 +1,8 @@ +{ + "text.hero.toph.ability.earth_surf.description": "Earth Surf beschreibung", + "text.hero.toph.ability.earth_column.description": "Earth Column beschreibung", + "text.hero.toph.ability.earth_push.description": "Earth Push beschreibung", + "text.hero.toph.ability.earth_armor.description": "Earth Armor beschreibung", + "text.hero.toph.ability.earth_trap.description": "Earth Trap beschreibung", + "text.hero.toph.ability.seismic_sense.description": "Seismic Sense beschreibung" +} diff --git a/toph/src/main/resources/assets/toph/lang/en_us.json b/toph/src/main/resources/assets/toph/lang/en_us.json new file mode 100644 index 0000000..89cf06a --- /dev/null +++ b/toph/src/main/resources/assets/toph/lang/en_us.json @@ -0,0 +1,9 @@ +{ + "text.hero.toph.description": "Toph description", + "text.hero.toph.ability.earth_surf.description": "Earth Surf description", + "text.hero.toph.ability.earth_column.description": "Earth Column description", + "text.hero.toph.ability.earth_push.description": "Earth Push description", + "text.hero.toph.ability.earth_armor.description": "Earth Armor description", + "text.hero.toph.ability.earth_trap.description": "Earth Trap description", + "text.hero.toph.ability.seismic_sense.description": "Seismic Sense description" +} diff --git a/toph/src/main/resources/assets/toph/particles/earth_dust.json b/toph/src/main/resources/assets/toph/particles/earth_dust.json new file mode 100644 index 0000000..977381b --- /dev/null +++ b/toph/src/main/resources/assets/toph/particles/earth_dust.json @@ -0,0 +1,16 @@ +{ + "textures": [ + "minecraft:big_smoke_0", + "minecraft:big_smoke_1", + "minecraft:big_smoke_2", + "minecraft:big_smoke_3", + "minecraft:big_smoke_4", + "minecraft:big_smoke_5", + "minecraft:big_smoke_6", + "minecraft:big_smoke_7", + "minecraft:big_smoke_8", + "minecraft:big_smoke_9", + "minecraft:big_smoke_10", + "minecraft:big_smoke_11" + ] +} diff --git a/toph/src/main/resources/assets/toph/shaders/post/seismic_sense.json b/toph/src/main/resources/assets/toph/shaders/post/seismic_sense.json new file mode 100644 index 0000000..2ced22d --- /dev/null +++ b/toph/src/main/resources/assets/toph/shaders/post/seismic_sense.json @@ -0,0 +1,57 @@ +{ + "targets": [ + "swap" + ], + "passes": [ + { + "name": "color_convolve", + "intarget": "minecraft:main", + "outtarget": "swap", + "uniforms": [ + { + "name": "RedMatrix", + "values": [ + 0.15, + 0.15, + 0.15 + ] + }, + { + "name": "GreenMatrix", + "values": [ + 0.15, + 0.15, + 0.15 + ] + }, + { + "name": "BlueMatrix", + "values": [ + 0.15, + 0.15, + 0.15 + ] + } + ] + }, + { + "name": "bits", + "intarget": "swap", + "outtarget": "minecraft:main", + "uniforms": [ + { + "name": "Resolution", + "values": [ + 16.0 + ] + }, + { + "name": "MosaicSize", + "values": [ + 2.0 + ] + } + ] + } + ] +} diff --git a/toph/src/main/resources/assets/toph/sounds.json b/toph/src/main/resources/assets/toph/sounds.json new file mode 100644 index 0000000..b7b3a09 --- /dev/null +++ b/toph/src/main/resources/assets/toph/sounds.json @@ -0,0 +1,37 @@ +{ + "earth_armor": { + "sounds": [ + "toph:earth_armor" + ] + }, + "seismic_sense_start": { + "sounds": [ + "toph:seismic_sense_start" + ] + }, + "seismic_sense_wave": { + "sounds": [ + "toph:seismic_sense_wave" + ] + }, + "earth_column_1": { + "sounds": [ + "toph:earth_column_1" + ] + }, + "arm_whoosh": { + "sounds": [ + "toph:arm_whoosh" + ] + }, + "stone_smash": { + "sounds": [ + "toph:stone_smash" + ] + }, + "stone_slide": { + "sounds": [ + "toph:stone_slide" + ] + } +} diff --git a/toph/src/main/resources/assets/toph/sounds/arm_whoosh.ogg b/toph/src/main/resources/assets/toph/sounds/arm_whoosh.ogg new file mode 100644 index 0000000..87d17f7 Binary files /dev/null and b/toph/src/main/resources/assets/toph/sounds/arm_whoosh.ogg differ diff --git a/toph/src/main/resources/assets/toph/sounds/earth_armor.ogg b/toph/src/main/resources/assets/toph/sounds/earth_armor.ogg new file mode 100644 index 0000000..7f0b447 Binary files /dev/null and b/toph/src/main/resources/assets/toph/sounds/earth_armor.ogg differ diff --git a/toph/src/main/resources/assets/toph/sounds/earth_column_1.ogg b/toph/src/main/resources/assets/toph/sounds/earth_column_1.ogg new file mode 100644 index 0000000..171ff2e Binary files /dev/null and b/toph/src/main/resources/assets/toph/sounds/earth_column_1.ogg differ diff --git a/toph/src/main/resources/assets/toph/sounds/seismic_sense_start.ogg b/toph/src/main/resources/assets/toph/sounds/seismic_sense_start.ogg new file mode 100644 index 0000000..92b57eb Binary files /dev/null and b/toph/src/main/resources/assets/toph/sounds/seismic_sense_start.ogg differ diff --git a/toph/src/main/resources/assets/toph/sounds/seismic_sense_wave.ogg b/toph/src/main/resources/assets/toph/sounds/seismic_sense_wave.ogg new file mode 100644 index 0000000..a71ba23 Binary files /dev/null and b/toph/src/main/resources/assets/toph/sounds/seismic_sense_wave.ogg differ diff --git a/toph/src/main/resources/assets/toph/sounds/stone_crash.ogg b/toph/src/main/resources/assets/toph/sounds/stone_crash.ogg new file mode 100644 index 0000000..72eff64 Binary files /dev/null and b/toph/src/main/resources/assets/toph/sounds/stone_crash.ogg differ diff --git a/toph/src/main/resources/assets/toph/sounds/stone_slide.ogg b/toph/src/main/resources/assets/toph/sounds/stone_slide.ogg new file mode 100644 index 0000000..01a1e2d Binary files /dev/null and b/toph/src/main/resources/assets/toph/sounds/stone_slide.ogg differ diff --git a/toph/src/main/resources/assets/toph/sounds/stone_smash.ogg b/toph/src/main/resources/assets/toph/sounds/stone_smash.ogg new file mode 100644 index 0000000..1511824 Binary files /dev/null and b/toph/src/main/resources/assets/toph/sounds/stone_smash.ogg differ diff --git a/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_armor.png b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_armor.png new file mode 100644 index 0000000..7a6fe11 Binary files /dev/null and b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_armor.png differ diff --git a/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_column.png b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_column.png new file mode 100644 index 0000000..7fe2e70 Binary files /dev/null and b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_column.png differ diff --git a/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_push.png b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_push.png new file mode 100644 index 0000000..eb44222 Binary files /dev/null and b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_push.png differ diff --git a/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_surf.png b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_surf.png new file mode 100644 index 0000000..16f1994 Binary files /dev/null and b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_surf.png differ diff --git a/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_trap.png b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_trap.png new file mode 100644 index 0000000..0b1ceea Binary files /dev/null and b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/earth_trap.png differ diff --git a/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/seismic_sense.png b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/seismic_sense.png new file mode 100644 index 0000000..fdbbebe Binary files /dev/null and b/toph/src/main/resources/assets/toph/textures/hero/toph/abilities/seismic_sense.png differ diff --git a/toph/src/main/resources/assets/toph/textures/toph_overlay.png b/toph/src/main/resources/assets/toph/textures/toph_overlay.png new file mode 100644 index 0000000..af63bbf Binary files /dev/null and b/toph/src/main/resources/assets/toph/textures/toph_overlay.png differ diff --git a/toph/src/main/resources/fabric.mod.json b/toph/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..82626bd --- /dev/null +++ b/toph/src/main/resources/fabric.mod.json @@ -0,0 +1,53 @@ +{ + "schemaVersion": 1, + "name": "Toph", + "id": "toph", + "version": "${version}", + "description": "Toph", + "authors": [ + "NoRiskk" + ], + "icon": "assets/toph/icon.png", + "license": "ARR", + "environment": "*", + "entrypoints": { + "main": [ + { + "adapter": "kotlin", + "value": "gg.norisk.heroes.toph.TophManager" + } + ], + "client": [ + { + "adapter": "kotlin", + "value": "gg.norisk.heroes.toph.TophManager" + } + ], + "server": [ + { + "adapter": "kotlin", + "value": "gg.norisk.heroes.toph.TophManager" + } + ] + }, + "mixins": [ + "toph.mixins.json" + ], + "accessWidener": "toph.accesswidener", + "depends": { + "java": ">=17", + "minecraft": "*", + "fabric": "*", + "fabric-language-kotlin": "*" + }, + "custom": { + "modmenu": { + "badges": [ + "library" + ], + "parent": { + "id": "hero-api" + } + } + } +} diff --git a/toph/src/main/resources/toph.accesswidener b/toph/src/main/resources/toph.accesswidener new file mode 100644 index 0000000..ec85d5a --- /dev/null +++ b/toph/src/main/resources/toph.accesswidener @@ -0,0 +1,5 @@ +accessWidener v2 named +accessible class net/minecraft/client/gui/hud/InGameHud$HeartType +accessible field net/minecraft/entity/FallingBlockEntity block Lnet/minecraft/block/BlockState; +accessible class net/minecraft/client/render/BackgroundRenderer$DarknessFogModifier +accessible class net/minecraft/client/render/BackgroundRenderer$FogData diff --git a/toph/src/main/resources/toph.mixins.json b/toph/src/main/resources/toph.mixins.json new file mode 100644 index 0000000..7880d50 --- /dev/null +++ b/toph/src/main/resources/toph.mixins.json @@ -0,0 +1,23 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "gg.norisk.heroes.toph.mixin", + "compatibilityLevel": "JAVA_17", + "injectors": { + "defaultRequire": 1 + }, + "mixins": [ + "ModelPartAccessor", + "entity.EntityMixin", + "entity.ItemEntityMixin", + "entity.PlayerEntityMixin" + ], + "client": [ + "MinecraftClientMixin", + "render.AnimalModelAccessor", + "render.DarknessFogModifierMixin", + "render.GameRendererAccessor", + "render.GameRendererMixin", + "render.WorldRendererAccessor" + ] +}