diff --git a/.gitignore b/.gitignore index c8fb1522c..00bac1f8a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ runs/ # forge generated resources cache /src/generated/resources/.cache/ +# Agenic Support Tools - Debug Only +/.claude/ diff --git a/gradle.properties b/gradle.properties index 37c788950..41fa6e345 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.caching = true # Mod Properties mod_id = cosmiccore mod_name = Cosmic Core -mod_version = 0.10.1 +mod_version = 0.11.0 mod_description = The Core of Cosmic Frontiers! mod_authors = Ghostipedia mod_license = Code : LGPL-3.0 , Assets: Refer to Github. diff --git a/src/generated/resources/assets/cosmiccore/blockstates/dreamers_basin.json b/src/generated/resources/assets/cosmiccore/blockstates/dreamers_basin.json new file mode 100644 index 000000000..110393924 --- /dev/null +++ b/src/generated/resources/assets/cosmiccore/blockstates/dreamers_basin.json @@ -0,0 +1,76 @@ +{ + "variants": { + "facing=east,upwards_facing=east": { + "gtceu:z": 270, + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 90 + }, + "facing=east,upwards_facing=north": { + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 90 + }, + "facing=east,upwards_facing=south": { + "gtceu:z": 180, + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 90 + }, + "facing=east,upwards_facing=west": { + "gtceu:z": 90, + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 90 + }, + "facing=north,upwards_facing=east": { + "gtceu:z": 270, + "model": "cosmiccore:block/machine/dreamers_basin" + }, + "facing=north,upwards_facing=north": { + "model": "cosmiccore:block/machine/dreamers_basin" + }, + "facing=north,upwards_facing=south": { + "gtceu:z": 180, + "model": "cosmiccore:block/machine/dreamers_basin" + }, + "facing=north,upwards_facing=west": { + "gtceu:z": 90, + "model": "cosmiccore:block/machine/dreamers_basin" + }, + "facing=south,upwards_facing=east": { + "gtceu:z": 270, + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 180 + }, + "facing=south,upwards_facing=north": { + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 180 + }, + "facing=south,upwards_facing=south": { + "gtceu:z": 180, + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 180 + }, + "facing=south,upwards_facing=west": { + "gtceu:z": 90, + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 180 + }, + "facing=west,upwards_facing=east": { + "gtceu:z": 270, + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 270 + }, + "facing=west,upwards_facing=north": { + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 270 + }, + "facing=west,upwards_facing=south": { + "gtceu:z": 180, + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 270 + }, + "facing=west,upwards_facing=west": { + "gtceu:z": 90, + "model": "cosmiccore:block/machine/dreamers_basin", + "y": 270 + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/cosmiccore/blockstates/stellar_smelting_module.json b/src/generated/resources/assets/cosmiccore/blockstates/stellar_smelting_module.json new file mode 100644 index 000000000..6b8b9375d --- /dev/null +++ b/src/generated/resources/assets/cosmiccore/blockstates/stellar_smelting_module.json @@ -0,0 +1,76 @@ +{ + "variants": { + "facing=east,upwards_facing=east": { + "gtceu:z": 270, + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 90 + }, + "facing=east,upwards_facing=north": { + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 90 + }, + "facing=east,upwards_facing=south": { + "gtceu:z": 180, + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 90 + }, + "facing=east,upwards_facing=west": { + "gtceu:z": 90, + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 90 + }, + "facing=north,upwards_facing=east": { + "gtceu:z": 270, + "model": "cosmiccore:block/machine/stellar_smelting_module" + }, + "facing=north,upwards_facing=north": { + "model": "cosmiccore:block/machine/stellar_smelting_module" + }, + "facing=north,upwards_facing=south": { + "gtceu:z": 180, + "model": "cosmiccore:block/machine/stellar_smelting_module" + }, + "facing=north,upwards_facing=west": { + "gtceu:z": 90, + "model": "cosmiccore:block/machine/stellar_smelting_module" + }, + "facing=south,upwards_facing=east": { + "gtceu:z": 270, + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 180 + }, + "facing=south,upwards_facing=north": { + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 180 + }, + "facing=south,upwards_facing=south": { + "gtceu:z": 180, + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 180 + }, + "facing=south,upwards_facing=west": { + "gtceu:z": 90, + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 180 + }, + "facing=west,upwards_facing=east": { + "gtceu:z": 270, + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 270 + }, + "facing=west,upwards_facing=north": { + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 270 + }, + "facing=west,upwards_facing=south": { + "gtceu:z": 180, + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 270 + }, + "facing=west,upwards_facing=west": { + "gtceu:z": 90, + "model": "cosmiccore:block/machine/stellar_smelting_module", + "y": 270 + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/cosmiccore/lang/en_ud.json b/src/generated/resources/assets/cosmiccore/lang/en_ud.json index 4eb63c754..3e00ae0a6 100644 --- a/src/generated/resources/assets/cosmiccore/lang/en_ud.json +++ b/src/generated/resources/assets/cosmiccore/lang/en_ud.json @@ -68,6 +68,7 @@ "block.cosmiccore.dawnforge_eclipsed": "]pǝsdıןɔƎ[ ǝbɹoɟuʍɐᗡ", "block.cosmiccore.dimensional_energy_capacitor": "uoıʇɐʇsqnS ɹǝʍoԀ", "block.cosmiccore.dimensional_energy_interface": "ǝɔɐɟɹǝʇuI ןɐuoısuǝɯıᗡ uoıʇɐʇsqnS ɹǝʍoԀ", + "block.cosmiccore.dreamers_basin": "uısɐᗺ s,ɹǝɯɐǝɹᗡ", "block.cosmiccore.drone_maintenance_interface": "ǝɔɐɟɹǝʇuI ǝɔuɐuǝʇuıɐW ǝuoɹᗡ", "block.cosmiccore.drone_station": "uoıʇɐʇS ǝuoɹᗡ", "block.cosmiccore.drygmy_grove": "ǝʌoɹ⅁ ʎɯbʎɹᗡ", @@ -285,6 +286,7 @@ "block.cosmiccore.steel_rose_light_stairs": "sɹıɐʇS ʇɥbıꞀ ǝsoᴚ ןǝǝʇS", "block.cosmiccore.stellar_iris": "sıɹI ɹɐןןǝʇS", "block.cosmiccore.stellar_neutronium_grade_magnet": "ʇǝubɐW ǝpɐɹ⅁ ɯnıuoɹʇnǝN ɹɐןןǝʇS", + "block.cosmiccore.stellar_smelting_module": "ɯɹoɟʇɐןԀ ɹosuǝʇ-ɹǝdʎH ǝuɐqspo⅁", "block.cosmiccore.sterilization_hatch": "ɥɔʇɐH uoıʇɐzןıɹǝʇS", "block.cosmiccore.submerged_welder": "ɹǝpןǝM pǝbɹǝɯqnSƐ§", "block.cosmiccore.suffering_chamber": "ɹǝqɯɐɥƆ buıɹǝɟɟnSɔ§", @@ -463,6 +465,7 @@ "config.jade.plugin_cosmiccore.drone_maintenance_interface": "ǝɔɐɟɹǝʇuI ǝɔuɐuǝʇuıɐW ǝuoɹᗡ ]ƆƆ[", "config.jade.plugin_cosmiccore.drone_station": "uoıʇɐʇS ǝuoɹᗡ ]ƆƆ[", "config.jade.plugin_cosmiccore.parallel_info_cc": "oɟuI ןǝןןɐɹɐԀ ]ƆƆ[", + "config.jade.plugin_cosmiccore.stellar_module": "ǝןnpoW ɹɐןןǝʇS ]ƆƆ[", "cosmic.command.wireless.energy.active": "%s q§:ǝʌıʇɔⱯq§ ", "cosmic.command.wireless.energy.buffered": "∩Ǝ %s q§:pǝɹǝɟɟnᗺq§ ", "cosmic.command.wireless.energy.capacitor": " q§:uoıʇɐɔoꞀ ɹoʇıɔɐdɐƆq§ ", @@ -528,6 +531,8 @@ "cosmiccore.ember.capacity": "%s 9§:ʎʇıɔɐdɐƆ ɹǝqɯƎɔ§", "cosmiccore.ember.transfer": "%s 9§:ǝʇɐᴚ ɹǝɟsuɐɹ⟘ ɹǝqɯƎɔ§", "cosmiccore.errors.bad_fuel": "ʇıun ɹǝd ןɐʇoʇ ∩Ǝ 0ᄅㄥ> ǝq ʇsnW ʇndʇnO ןǝnℲ \n ¡ʎʇıןɐnὉ ןǝnℲ ʇuǝıɔıɟɟnsuIɐ§", + "cosmiccore.gui.stellar.show_modules": "ןoɹʇuoƆ ǝןnpoW ʍoɥS", + "cosmiccore.gui.stellar.show_star": "ʍǝıΛ ɹɐʇS ʍoɥS", "cosmiccore.item.linked_terminal.boundTo": "%s oʇ punoᗺ", "cosmiccore.item.spraycan.tooltip.current_color": "%s :ɹoןoƆ ʇuǝɹɹnƆ", "cosmiccore.item.spraycan.tooltip.lclick": "ɹoןoɔ ǝןɔʎƆ8§ :ʞɔıןƆ ʇɟǝꞀㄣ§", @@ -537,6 +542,13 @@ "cosmiccore.item.spraycan.tooltip.rclick_offhand": "ʇuıɐd & ǝɔɐןԀ8§ :puɐɥɟɟO uı ʞɔıןƆ ʇɥbıᴚϛ§", "cosmiccore.item.spraycan.tooltip.rclick_sneak": "I∩ uǝdO8§ :ʞɐǝuS + ʞɔıןƆ ʇɥbıᴚϛ§", "cosmiccore.item.spraycan.tooltip.solvent_mode": "ǝpoɯ ⟘NƎΛꞀOS uı uɐɔʎɐɹdS", + "cosmiccore.jade.stellar_module.connected": "pǝʇɔǝuuoƆ :sıɹI", + "cosmiccore.jade.stellar_module.energy_usage": "%s :ǝbɐs∩", + "cosmiccore.jade.stellar_module.iris_not_ready": "ʎpɐǝᴚ ʇoN :sıɹI", + "cosmiccore.jade.stellar_module.no_wireless": "ʞɹoʍʇǝN ssǝןǝɹıM oN", + "cosmiccore.jade.stellar_module.not_connected": "pǝʇɔǝuuoƆ ʇoN :sıɹI", + "cosmiccore.jade.stellar_module.speed_bonus": "%s :pǝǝdS", + "cosmiccore.jade.stellar_module.stage": "%s :ǝbɐʇS", "cosmiccore.khoruth.1": "ǝɔɐdS - ɹoɥʞ9§", "cosmiccore.khoruth.2": "uoıʇɐpunoℲ - ɥʇnᴚ9§", "cosmiccore.lore.broken_virtue.0": "ʎןʇɟoS sɹǝppnɥS ʎʇınʇǝdɹǝԀ", @@ -552,6 +564,23 @@ "cosmiccore.machine.capacitor_array.tooltip.0": "ɹ§ǝbɐɹoʇS ɹǝʍoԀ ǝsuǝᗡ ןɐɔoꞀㄥ§", "cosmiccore.machine.capacitor_array.tooltip.1": "ɹ§sǝɯıʇ 8Ɩ oʇ dn ʎןןɐɔıʇɹǝʌ pǝpuɐdxǝ ǝq puɐ ɹoʇıɔɐdɐɔ ʎuɐ ǝsn uɐƆㄥ§", "cosmiccore.machine.capacitor_array.tooltip.2": "ɹ§sǝɥɔʇɐH ɹǝsɐꞀ9§ sʇdǝɔɔⱯㄥ§", + "cosmiccore.machine.dreamers_basin.eu_budget_header": "ʇǝbpnᗺ ʎbɹǝuƎ", + "cosmiccore.machine.dreamers_basin.eu_per_thread": ")%s( pɐǝɹɥʇ ɹǝd ʇ/∩Ǝ %s", + "cosmiccore.machine.dreamers_basin.status_idle": "ǝdıɔǝɹ oN - ǝןpI", + "cosmiccore.machine.dreamers_basin.status_suspended": "pǝpuǝdsnS", + "cosmiccore.machine.dreamers_basin.status_unknown": "uʍouʞu∩", + "cosmiccore.machine.dreamers_basin.status_waiting": "sʇnduı ɹoɟ buıʇıɐM", + "cosmiccore.machine.dreamers_basin.thread_header": "snʇɐʇS pɐǝɹɥ⟘", + "cosmiccore.machine.dreamers_basin.threads_summary": "xɐɯ %s / ǝʌıʇɔɐ %s / buıuunɹ %s", + "cosmiccore.machine.dreamers_basin.time_remaining": "buıuıɐɯǝɹ %s :ǝɯı⟘", + "cosmiccore.machine.dreamers_basin.tooltip.0": "ʎןsnoǝuɐʇןnɯıs sǝdıɔǝɹ ǝnbıun ǝןdıʇןnɯ sunᴚq§", + "cosmiccore.machine.dreamers_basin.tooltip.1": "ɥɔʇɐɥ/snq ʇnduı ɟ§pǝɹoןoɔ9§ ʎןǝnbıun ɐ sǝɹınbǝɹ pɐǝɹɥʇ ɥɔɐƎɟ§", + "cosmiccore.machine.dreamers_basin.tooltip.2": ")9Ɩ=Ɐ9Ɩ 'ㄣ=Ɐㄣ( ǝbɐɹǝdɯɐ ɥɔʇɐH ʎbɹǝuƎ = spɐǝɹɥʇ xɐWɟ§", + "cosmiccore.machine.dreamers_basin.tooltip.3": "sǝɥɔʇɐɥ/sǝsnq ʇndʇno ǝɹɐɥs spɐǝɹɥʇ ןןⱯɐ§", + "cosmiccore.machine.dreamers_basin.tooltip.crafting": ":buıʇɟɐɹƆ", + "cosmiccore.machine.dreamers_basin.tooltip.duration": "%s :uoıʇɐɹnp ǝdıɔǝᴚ", + "cosmiccore.machine.dreamers_basin.tooltip.no_recipe": "ɐʇɐp ǝdıɔǝɹ oN", + "cosmiccore.machine.dreamers_basin.tooltip.processing": "˙˙˙buıssǝɔoɹԀ ", "cosmiccore.machine.fluid_drilling_rig.depletion": "%0 :ǝʇɐᴚ uoıʇǝןdǝᗡq§", "cosmiccore.machine.fluid_drilling_rig.description.0": "ɯoɹɟ pınןɟ ǝʇıuıɟuı sןןıɹᗡq§", "cosmiccore.machine.fluid_drilling_rig.description.1": "˙pıoʌ ǝɥʇ ʇnoɥbnoɹɥʇ pǝpuǝdsns sʇǝʞɔod pınbıןq§", @@ -563,6 +592,9 @@ "cosmiccore.machine.me.stocking_item.tooltip.4": "ɹ§ʞɔıʇs ɐʇɐp ɐ ɥʇıʍ pǝʇsɐd/ʎdoɔ ǝq uɐɔ ɐʇɐp ɹǝʇןıℲɟ§", "cosmiccore.machine.me.stocking_item.tooltip.5": "ɹ§sǝuıן ʎןqɯǝssɐ ןǝןןɐɹɐd oʇ ʍoɥ buıɹǝpuoʍ ǝɹ,noʎ ɟI,q§", "cosmiccore.machine.me.stocking_item.tooltip.6": "ɹ§¡sʇǝuqns oʇ ǝɯoɔןǝM ˙ʍoɥ sı sıɥʇɟ§", + "cosmiccore.machine.multithreaded.active_threads": "%sɟ§/ㄥ§%sɐ§ :ǝʌıʇɔⱯㄥ§", + "cosmiccore.machine.multithreaded.max_threads": "%sɟ§ :spɐǝɹɥ⟘ xɐWㄥ§", + "cosmiccore.machine.multithreaded.thread_status": "=== snʇɐʇS pɐǝɹɥ⟘ ===q§", "cosmiccore.mana_leaching_tub.desc": "0006 ɹǝʞɐoS ɐuɐW", "cosmiccore.multiblock.advanced.star_ladder_tier": "%sq§ :ɟ§sǝןnpoW ɥɔɹɐǝsǝᴚ xɐWɐ§ \n %sq§ :ɟ§ɹǝı⟘ ɹǝɥʇǝ⟘ pןOɹǝppɐꞀɹɐʇS ǝuıɥɐɯoΛɐ§", "cosmiccore.multiblock.booster_used": "%s :ɹǝʇsooᗺ", @@ -612,6 +644,7 @@ "cosmiccore.multiblock.naqreactor.tooltip.0": "ןǝnɟ ǝʌıʇɔɐǝɹ puɐ suoısoןdxǝ ʎq pǝɹǝʍod ɹoʇɔɐǝɹ ǝʌıssɐɯ Ɐɔ§", "cosmiccore.multiblock.naqreactor.tooltip.1": "˙ʇndʇno x9Ɩ oʇ ןǝןןɐɹɐd oʇ ʇdɯǝʇʇɐ sʎɐʍןɐ ןןıMq§", "cosmiccore.multiblock.naqreactor.tooltip.2": "˙sǝɥɔʇɐɥ ɹǝsɐꞀ sʇdǝɔɔⱯ ʎןuOɔ§", + "cosmiccore.multiblock.pattern.stellar_module_slot": ")ǝןnpoW pǝɯɹoℲ ɹo ɹıⱯ( ʇoןS ǝןnpoWㄥ§", "cosmiccore.multiblock.reboot_powergrid": "sǝuıɥɔɐW pǝʇɔǝuuoƆ ןןⱯ ʇooqǝᴚɐ§", "cosmiccore.multiblock.send_orbit_data": "pɐoןʎɐԀ ɥɔɹɐǝsǝᴚ puǝSן§ɐ§", "cosmiccore.multiblock.sleep_powergrid": "sǝuıɥɔɐW pǝʇɔǝuuoƆ ןןⱯ puǝdsnSɔ§", @@ -619,6 +652,18 @@ "cosmiccore.multiblock.star_ladder.tooltip.1": "⟘NƎSƎᴚԀ SSOꞀ Ɐ⟘Ɐᗡ :ᴚƎ⅁NⱯᗡן§ɔ§", "cosmiccore.multiblock.star_ladder.tooltip.2": "ƎꞀᗺISSOԀ SI ʎᴚƎΛOƆƎᴚ :ᴚƎ⅁NⱯᗡן§ɔ§", "cosmiccore.multiblock.star_ladder.tooltip.3": ")ΛI oʇ ɯɐǝʇS( Ɩ⟘ƆⱯ ɟo ןɐo⅁ ןɐuıℲ ǝɥ⟘ : ʞɔoןqıʇןnW ǝןɔɐuıԀɐ§", + "cosmiccore.multiblock.stellar_module.connected": "sıɹI ɹɐןןǝʇS oʇ pǝʇɔǝuuoƆɐ§", + "cosmiccore.multiblock.stellar_module.energy_usage": "%sɟ§ :ʇ/∩Ǝ ssǝןǝɹıMǝ§", + "cosmiccore.multiblock.stellar_module.iris_not_formed": "pǝɯɹoℲ ʇoN sıɹI ɹɐןןǝʇSɔ§", + "cosmiccore.multiblock.stellar_module.iris_not_ready": "ʎpɐǝᴚ ʇoN sıɹI ɹɐןןǝʇSǝ§", + "cosmiccore.multiblock.stellar_module.loading": "˙˙˙buıpɐoꞀㄥ§", + "cosmiccore.multiblock.stellar_module.no_wireless": "ʞɹoʍʇǝN ʎbɹǝuƎ ssǝןǝɹıM oNɔ§", + "cosmiccore.multiblock.stellar_module.not_connected": "sıɹI ɹɐןןǝʇS oʇ pǝʇɔǝuuoƆ ʇoNɔ§", + "cosmiccore.multiblock.stellar_module.parallel": "%sq§ :ʇıɯıꞀ ןǝןןɐɹɐԀㄥ§", + "cosmiccore.multiblock.stellar_module.power_config": "ןǝןןɐɹɐԀㄥ§ x%dɐ§ @ㄥ§ %sq§ :bıɟuoƆㄥ§", + "cosmiccore.multiblock.stellar_module.power_failure": "¡ʎbɹǝuƎ ʇuǝıɔıɟɟnsuI - Ǝᴚ∩ꞀIⱯℲ ᴚƎMOԀן§ɔ§", + "cosmiccore.multiblock.stellar_module.speed_bonus": "%sɐ§ :snuoᗺ pǝǝdSㄥ§", + "cosmiccore.multiblock.stellar_module.stage": "%sǝ§ :ǝbɐʇS sıɹIㄥ§", "cosmiccore.omnia_circuit.ev": "˙ʇınɔɹıƆ ΛƎ ʎuɐ sɐ sʞɹoM9§", "cosmiccore.omnia_circuit.hv": "˙ʇınɔɹıƆ ΛH ʎuɐ sɐ sʞɹoM9§", "cosmiccore.omnia_circuit.iv": "˙ʇınɔɹıƆ ΛI ʎuɐ sɐ sʞɹoM9§", @@ -647,6 +692,69 @@ "cosmiccore.rune_emotion_weak.1": "˙pǝʌɹǝsqo sı uoıʇɔɐǝɹ ⱯᴚƎ ǝʇǝןdɯoɔuı uⱯo§ㄥ§", "cosmiccore.rune_emotion_weak.2": "˙ǝʇɐɹqıʌ oʇ ǝʇɐןs ǝɥʇ ǝsnɐɔ suoıʇɔɐǝɹ ןɐɔıɯǝɥɔ puɐ ןɐuoıʇoɯǝ buoɹʇSo§ㄥ§", "cosmiccore.rune_vague": "˙buıssıɯ ǝq oʇ ɯǝǝs suoıʇoɯǝ ʇuǝʇɐꞀo§ㄥ§", + "cosmiccore.stellar.context.blackhole_line1": "pǝuıɐʇuoɔ ʎʇıɹɐןnbuıS", + "cosmiccore.stellar.context.blackhole_line2": "buıssǝɔoɹd ɔıʇoxƎ", + "cosmiccore.stellar.context.death_graceful_line1": "uʍopʇnɥs pǝןןoɹʇuoƆ", + "cosmiccore.stellar.context.death_graceful_line2": "˙˙˙ssǝɹboɹd uı", + "cosmiccore.stellar.context.death_line1": "Ǝᴚ∩ꞀIⱯℲ ꞀⱯƆI⟘IᴚƆ", + "cosmiccore.stellar.context.death_line2": "ⱯƎᴚⱯ Ǝ⟘Ɐ∩ƆⱯΛƎ", + "cosmiccore.stellar.context.empty_line1": "puɐ pǝǝs ɹɐʇs ʇɹǝsuI", + "cosmiccore.stellar.context.empty_line2": "sǝsɐb ɹɐןןǝʇs ǝpıʌoɹd", + "cosmiccore.stellar.context.empty_line3": "˙uoıʇıubı uıbǝq oʇ", + "cosmiccore.stellar.context.growing_line1": "uoısnɟ ɹɐןןǝʇS", + "cosmiccore.stellar.context.growing_line2": "˙˙˙buıʇɐıʇıuı", + "cosmiccore.stellar.context.star_line1": "ǝʌıʇɔɐ uoısnɟ ǝןqɐʇS", + "cosmiccore.stellar.context.star_line2": "ǝןqɐןıɐʌɐ buıssǝɔoɹԀ", + "cosmiccore.stellar.context.superstar_line1": "ssɐɯ ןɐɔıʇıɹƆ :⅁NINᴚⱯM", + "cosmiccore.stellar.context.superstar_line2": "ʇuǝuıɯɯı ǝsdɐןןoƆ", + "cosmiccore.stellar.ignition.breaking": "¡¡¡ ⅁NIʞⱯƎᴚᗺ ¡¡¡", + "cosmiccore.stellar.ignition.ignite": "Ǝ⟘IN⅁I", + "cosmiccore.stellar.ignition.requires_star": "ᴚⱯ⟘S ƎΛI⟘ƆⱯ SƎᴚI∩ὉƎᴚ", + "cosmiccore.stellar.module.config": "bıɟuoƆ ǝןnpoW", + "cosmiccore.stellar.module.current": "ʇuǝɹɹnƆ", + "cosmiccore.stellar.module.iris_limit": "ʇıɯıꞀ sıɹI", + "cosmiccore.stellar.module.max_eut": "ʇ/∩Ǝ xɐW", + "cosmiccore.stellar.module.not_linked": "sıɹI ɹɐןןǝʇS oʇ pǝʞuıן ʇoN", + "cosmiccore.stellar.module.parallel": "ןǝןןɐɹɐԀ", + "cosmiccore.stellar.module.parallel_max": ")%s xɐɯ( x%s", + "cosmiccore.stellar.module.speed_bonus": "snuoᗺ pǝǝdS", + "cosmiccore.stellar.module.stage": "ǝbɐʇS", + "cosmiccore.stellar.module.status": "snʇɐʇS", + "cosmiccore.stellar.module.status.disconnected": "ᗡƎ⟘ƆƎNNOƆSIᗡ", + "cosmiccore.stellar.module.status.idle": "ƎꞀᗡI", + "cosmiccore.stellar.module.status.iris_inactive": "ƎΛI⟘ƆⱯNI SIᴚI", + "cosmiccore.stellar.module.status.no_wireless": "SSƎꞀƎᴚIM ON", + "cosmiccore.stellar.module.status.offline": "ƎNIꞀℲℲO", + "cosmiccore.stellar.module.status.power_fail": "ꞀIⱯℲ ᴚƎMOԀ", + "cosmiccore.stellar.module.status.processing": "⅁NISSƎƆOᴚԀ", + "cosmiccore.stellar.module.status.ready": "ʎᗡⱯƎᴚ", + "cosmiccore.stellar.module.waiting_iris": "sıɹI ɹoɟ buıʇıɐM", + "cosmiccore.stellar.power.max_parallel": "ןǝןןɐɹɐԀ ɯnɯıxɐW", + "cosmiccore.stellar.power.title": "ןǝuɐԀ ןoɹʇuoƆ ɹǝʍoԀ", + "cosmiccore.stellar.power.voltage_per_parallel": "ןǝןןɐɹɐԀ ɹǝԀ ǝbɐʇןoΛ", + "cosmiccore.stellar.prestige.continue": "]ǝnuıʇuoɔ oʇ ǝɹǝɥʍʎuɐ ʞɔıןƆ[", + "cosmiccore.stellar.prestige.current_tier": "ᴚƎI⟘ ⟘NƎᴚᴚ∩Ɔ", + "cosmiccore.stellar.prestige.max_tier": "ᗡƎHƆⱯƎᴚ ᴚƎI⟘ W∩WIXⱯW", + "cosmiccore.stellar.prestige.next_tier": "%s ɹoɟ sʇd %s", + "cosmiccore.stellar.prestige.points_earned": "ᗡƎNᴚⱯƎ S⟘NIOԀ", + "cosmiccore.stellar.prestige.tier.apprentice": "ƎƆI⟘NƎᴚԀԀⱯ", + "cosmiccore.stellar.prestige.tier.expert": "⟘ᴚƎԀXƎ", + "cosmiccore.stellar.prestige.tier.grandmaster": "ᴚƎ⟘SⱯWᗡNⱯᴚ⅁", + "cosmiccore.stellar.prestige.tier.journeyman": "NⱯWʎƎNᴚ∩Oſ", + "cosmiccore.stellar.prestige.tier.master": "ᴚƎ⟘SⱯW", + "cosmiccore.stellar.prestige.tier.novice": "ƎƆIΛON", + "cosmiccore.stellar.prestige.tier.unknown": "NMONʞN∩", + "cosmiccore.stellar.prestige.tier_up": "¡Ԁ∩ ᴚƎI⟘", + "cosmiccore.stellar.prestige.title": "ƎƆNƎ⅁ᴚƎΛNOƆ ᴚⱯꞀꞀƎ⟘S", + "cosmiccore.stellar.prestige.total_points": "sʇuıod %s :ןɐʇo⟘", + "cosmiccore.stellar.slot.star_seed": "pǝǝS ɹɐʇS", + "cosmiccore.stellar.stage.controlled_shutdown": "NMOᗡ⟘∩HS ᗡƎꞀꞀOᴚ⟘NOƆ", + "cosmiccore.stellar.stage.critical_mass": "SSⱯW ꞀⱯƆI⟘IᴚƆ", + "cosmiccore.stellar.stage.emergency_protocols": "SꞀOƆO⟘OᴚԀ ʎƆNƎ⅁ᴚƎWƎ", + "cosmiccore.stellar.stage.initialization": "NOI⟘ⱯZIꞀⱯI⟘INI", + "cosmiccore.stellar.stage.singularity_control": "ꞀOᴚ⟘NOƆ ʎ⟘IᴚⱯꞀ∩⅁NIS", + "cosmiccore.stellar.stage.stellar_ignition": "NOI⟘IN⅁I ᴚⱯꞀꞀƎ⟘S", + "cosmiccore.stellar.stage.stellar_operations": "SNOI⟘ⱯᴚƎԀO ᴚⱯꞀꞀƎ⟘S", "cosmiccore.tenura.1": "ןoɹʇuoƆ - uǝ⟘9§", "cosmiccore.tenura.2": "ʍoןℲ - ɐɹ∩9§", "cosmiccore.thermomagnitizer.desc": "buoɹʍ ob pןnoɔ ʇɐɥʍ 'sʇǝubɐW puɐ buıʇɐǝH", @@ -769,6 +877,7 @@ "item.cosmiccore.bitumen_wax": "xɐM uǝɯnʇıᗺ", "item.cosmiccore.blackstone_pustule": "ǝןnʇsnԀ ǝuoʇsʞɔɐןᗺ", "item.cosmiccore.brimstone_asteroid": "pıoɹǝʇsⱯ ǝuoʇsɯıɹᗺ", + "item.cosmiccore.bronze_supply_tank": "ʞuɐ⟘ ʎןddnS ǝzuoɹᗺ", "item.cosmiccore.capacity_chip": "dıɥƆ ʎʇıɔɐdɐƆ", "item.cosmiccore.carbon_asteroid_base": "pıoɹǝʇsⱯ ɔıuoqɹɐƆ", "item.cosmiccore.chronia": "]ɐıuoɹɥƆ[ - ןıxǝΛ", @@ -938,6 +1047,8 @@ "item.cosmiccore.portable_gravity_core.tooltip": "˙ɥʇɹɐƎ ɥɔʇɐW oʇ ʎʇıʌɐɹ⅁ sǝzıןɐɯɹoNɐ§", "item.cosmiccore.potency_chip": "dıɥƆ ʎɔuǝʇoԀ", "item.cosmiccore.prepared_petri_dish": "ɥsıᗡ ıɹʇǝԀ pǝɹɐdǝɹԀ", + "item.cosmiccore.pressurized_rebreather": "ɹǝɥʇɐǝɹqǝᴚ pǝzıɹnssǝɹԀ", + "item.cosmiccore.pressurized_rebreather.tooltip": "˙sʇuǝɯuoɹıʌuǝ 9§ɹıⱯ oNɔ§ uı sʞɹoM ˙ǝbɐsn ʞuɐʇ uǝbʎxo sǝןqɐuƎ9§", "item.cosmiccore.prideful_spirit": "ʇıɹıdS ןnɟǝpıɹԀ", "item.cosmiccore.prod_mod_1": "Ɩ˙ʞW ǝןnpoW ʎʇıʌıʇɔnpoɹԀ", "item.cosmiccore.prod_mod_2": "ᄅ˙ʞW ǝןnpoW ʎʇıʌıʇɔnpoɹԀ", @@ -964,6 +1075,7 @@ "item.cosmiccore.record_kept_printed_circuit_board": "pɹɐoᗺ ʇınɔɹıƆ pǝʇuıɹԀ ʇdǝʞ pɹoɔǝᴚ", "item.cosmiccore.refined_harmonics_wafer": "ɹǝɟɐM ɔıuoɯɹɐH pǝuıɟǝᴚ", "item.cosmiccore.refined_resonant_wafer": "ɹǝɟɐM ɔıuoɯɹɐH pǝuıɟǝᴚ", + "item.cosmiccore.reflection_mirror": "uoısoɹƎ ɟo ɹoɹɹıW", "item.cosmiccore.resipiratory_sculk_hemocytoblast": "ʇsɐןqoʇʎɔoɯǝH ʞןnɔS ʎɹoʇɐɹıdsǝᴚ", "item.cosmiccore.resonant_mod": "Ɩ˙ʞW ǝןnpoW uoısnℲ", "item.cosmiccore.robust_drone": "ǝuoɹᗡ ʇsnqoᴚ", @@ -993,6 +1105,8 @@ "item.cosmiccore.self_aware_processing_assembly": "ʎןqɯǝssⱯ ɹossǝɔoɹԀ ǝɹɐʍⱯ ɟןǝS", "item.cosmiccore.seraphon": "]uoɥdɐɹǝS[ - uouıɯnꞀ", "item.cosmiccore.shard_of_perpetuity": "ʎʇınʇǝdɹǝԀ ɟo pɹɐɥS", + "item.cosmiccore.simple_rebreather": "ɹǝɥʇɐǝɹqǝᴚ ǝןdɯıS", + "item.cosmiccore.simple_rebreather.tooltip": "˙sʇuǝɯuoɹıʌuǝ ㄥ§ɹıⱯ uıɥ⟘q§ uı uıɐɹp uǝbʎxo sǝɔnpǝᴚㄥ§", "item.cosmiccore.somatic_processing_assembly": "pɹɐoᗺ ʎןqɯǝssⱯ buıssǝɔoɹdoʇɐɯoS", "item.cosmiccore.sov_blood_orb": "qɹO pooןᗺ ubıǝɹǝʌoS", "item.cosmiccore.space_advanced_nanomuscle_chestplate": "ǝʇɐןdʇsǝɥƆ ǝʇınS ǝɔɐdS ™ǝןɔsnWouɐN pǝɔuɐʌpⱯ", @@ -1008,6 +1122,7 @@ "item.cosmiccore.spirit_engraved_enthel_circuit_board": "pɹɐoᗺ ʇınɔɹıƆ ןǝɥʇuƎ pǝʌɐɹbuƎ ʇıɹıdS", "item.cosmiccore.spirit_runed_enthel_cpu": "∩ԀƆ ןǝɥʇuƎ pǝunᴚ ʇıɹıdS", "item.cosmiccore.spirit_runed_enthel_cpu_wafer": "ɹǝɟɐM ∩ԀƆ ןǝɥʇuƎ pǝunᴚ ʇıɹıdS", + "item.cosmiccore.steel_supply_tank": "ʞuɐ⟘ ʎןddnS ןǝǝʇS", "item.cosmiccore.streptococcus_pyogenes": "sǝuǝboʎԀ snɔɔoɔoʇdǝɹʇS", "item.cosmiccore.streptococcus_pyogenes_culture": "ǝɹnʇןnƆ sǝuǝboʎԀ snɔɔoɔoʇdǝɹʇS", "item.cosmiccore.suelescent_processor": "ɹossǝɔoɹԀ ʇuǝɔsǝןǝnS", @@ -1076,6 +1191,7 @@ "material.cosmiccore.enderium": "ɯnıɹǝpuƎ", "material.cosmiccore.energetic_alloy": "ʎoןןⱯ ɔıʇǝbɹǝuƎ", "material.cosmiccore.enraged_stygian_plague": "ǝnbɐןԀ uɐıbʎʇS pǝbɐɹuƎ", + "material.cosmiccore.halizine": "ǝuızıןɐH", "material.cosmiccore.ichor": "ɹoɥɔI", "material.cosmiccore.ichorium": "ɯnıɹoɥɔI", "material.cosmiccore.infinity": "ʎʇıuıɟuI", @@ -1097,6 +1213,7 @@ "material.cosmiccore.psionic_galvorn": "uɹoʌןɐ⅁ ɔıuoısԀ", "material.cosmiccore.reclaimed_pale_ore": "ǝɹO ǝןɐԀ pǝɯıɐןɔǝᴚ", "material.cosmiccore.resonant_virtue_meld": "pןǝW ǝnʇɹıΛ ʇuɐuosǝᴚ", + "material.cosmiccore.rosmotosin": "uısoʇoɯsoᴚ", "material.cosmiccore.shimmering_neutronium": "ɯnıuoɹʇnǝN buıɹǝɯɯıɥS", "material.cosmiccore.signalum": "ɯnןɐubıS", "material.cosmiccore.sol_steel": "ןǝǝʇS ןoS", @@ -1110,6 +1227,7 @@ "material.cosmiccore.superheavy_bedrock_alloy": "ʎoןןⱯ ʞɔoɹpǝᗺ ʎʌɐǝɥɹǝdnS", "material.cosmiccore.taranium": "ɯnıuɐɹɐ⟘", "material.cosmiccore.temmerite": "ǝʇıɹǝɯɯǝ⟘", + "material.cosmiccore.tenbrium": "ɯnıɹquǝ⟘", "material.cosmiccore.trinavine": "ǝuıʌɐuıɹ⟘", "material.cosmiccore.trinium_naqide": "ǝpıbɐN ɯnıuıɹ⟘", "material.cosmiccore.triphenylphosphine": "ǝuıɥdsoɥdןʎuǝɥdıɹ⟘", @@ -1118,6 +1236,443 @@ "material.cosmiccore.virtue_meld": "pןǝW ǝnʇɹıΛ", "material.cosmiccore.vitrius": "snıɹʇıΛ", "material.cosmiccore.voidspark": "ʞɹɐdspıoΛ", + "reflection.cosmiccore.bargain.ascension.answer.ready.drawback.0": "buıʎןɟ ʇou uǝɥʍ pǝǝds ʇuǝɯǝʌoɯ %0Ɛ-", + "reflection.cosmiccore.bargain.ascension.answer.ready.drawback.1": "sǝɔɐds pǝsoןɔuǝ ɹo sǝuoz ʎןɟ-ou uı ǝןqɐɹǝuןnΛ", + "reflection.cosmiccore.bargain.ascension.answer.ready.power.0": ")ǝuɹoqɹıɐ ǝןıɥʍ dɯnظ ɥʇıʍ ǝןbboʇ( ʇɥbıןɟ ǝןʎʇs-ǝʌıʇɐǝɹƆ", + "reflection.cosmiccore.bargain.ascension.answer.ready.power.1": "ʇsoɔ ɐuıɯɐʇs ɹo ɹǝbunɥ ʇnoɥʇıʍ ʎןǝʇıuıɟǝpuı ʎןℲ", + "reflection.cosmiccore.bargain.ascension.answer.ready.power.2": "buıʎןɟ ǝןıɥʍ ןoɹʇuoɔ ʇuǝɯǝʌoɯ ᗡƐ ןןnℲ", + "reflection.cosmiccore.bargain.ascension.answer.ready.power.3": "ǝʌıʇɔɐ sı ʇɥbıןɟ ǝןıɥʍ ǝbɐɯɐp ןןɐɟ oN", + "reflection.cosmiccore.bargain.ascension.answer.ready.response": "˙ǝɹoɯʎuɐ noʎ uo ɯıɐןɔ ou sɐɥ punoɹb ǝɥ⟘ ˙ǝsıɹ uǝɥ⟘", + "reflection.cosmiccore.bargain.ascension.answer.ready.text": "˙ʎןɟ oʇ ʎpɐǝɹ ɯɐ I", + "reflection.cosmiccore.bargain.ascension.answer.refuse.response": "¿buoןɐ ןןɐ ʇuɐʌɹǝs ǝɥʇ ǝɹǝʍ noʎ ǝzıןɐǝɹ noʎ ןıʇun buoן ʍoH ˙ʇuɐʌɹǝs ɐ ǝʞıꞀ ˙pǝʌɹǝS", + "reflection.cosmiccore.bargain.ascension.answer.refuse.text": "˙ןןǝʍ ǝɯ pǝʌɹǝs sɐɥ punoɹb ǝɥ⟘", + "reflection.cosmiccore.bargain.ascension.description": "ɥʇɹɐǝ buıןʍɐɹɔ ǝɥʇ ǝʌoqɐ ǝsıᴚ", + "reflection.cosmiccore.bargain.ascension.dialogue.0": "˙ʎuuɐɹʎʇ ʎʇʇǝd s,ʎʇıʌɐɹb ʎq pǝuıɐɥƆ ˙punoɹb ǝɥʇ oʇ punoq ǝɹɐ noʎ", + "reflection.cosmiccore.bargain.ascension.dialogue.1": "˙op sןɐʇɹoɯ ןןⱯ ˙ʇɥbıןɟ ɟo ɯɐǝɹp noʎ", + "reflection.cosmiccore.bargain.ascension.dialogue.2": "˙ǝsıɹ noʎ ʇǝꞀ ˙suıɐɥɔ ǝsoɥʇ ɹǝʌǝs uɐɔ I", + "reflection.cosmiccore.bargain.ascension.dialogue.3": "˙ʇɥbıןɟ ǝnɹ⟘ ˙ǝןʎʇs ɥʇıʍ buıןןɐɟ ʇoN ˙buıpıןb ʇoN", + "reflection.cosmiccore.bargain.ascension.dialogue.4": "˙ɹoop ɐ ǝʞıן noʎ oʇ uǝdo ןןıʍ ʎʞs ǝɥ⟘", + "reflection.cosmiccore.bargain.ascension.dialogue.5": "˙buoɹM ˙ǝןqɐʇɹoɟɯoɔu∩ ˙uǝıןɐ ǝɯoɔǝq ןןıʍ punoɹb ǝɥʇ ˙˙˙punoɹb ǝɥʇ ʇnᗺ", + "reflection.cosmiccore.bargain.ascension.name": "uoısuǝɔsⱯ", + "reflection.cosmiccore.bargain.ascension.on_accept": "˙punoq-ɥʇɹɐǝ ɹǝbuoן ou ǝɹɐ noʎ ˙suǝdo ʎʞs ǝɥ⟘ ˙noʎ sǝʌɐǝן ʇɥbıǝM", + "reflection.cosmiccore.bargain.ascension.on_defy": "˙ʎןǝʌıssǝssod 'ʞɔɐq noʎ sǝɯoɔןǝʍ punoɹb ǝɥ⟘ ˙noʎ sɯıɐןɔǝɹ ʎʇıʌɐɹ⅁", + "reflection.cosmiccore.bargain.ascension.question": "¿ʎʞs ǝɥʇ ɹoɟ ɥʇɹɐǝ ǝɥʇ uopuɐqɐ noʎ ןןıM", + "reflection.cosmiccore.bargain.back.answer.accept.drawback.0": "ǝsn ʇɹodǝןǝʇ ɹǝd ʇsoɔ uoısoɹǝ ϛ", + "reflection.cosmiccore.bargain.back.answer.accept.drawback.1": "sǝʇnuıɯ 0Ɩ ɹǝʇɟɐ sǝpɐɟ ɹǝʞɹɐW", + "reflection.cosmiccore.bargain.back.answer.accept.power.0": ")ɥʇɐǝp ɹǝd ǝɔuo( uoıʇɐɔoן ɥʇɐǝp oʇ ʇɹodǝןǝ⟘", + "reflection.cosmiccore.bargain.back.answer.accept.power.1": "sןןɐʍ ɥbnoɹɥʇ ǝןqısıʌ ɹǝʞɹɐɯ ɥʇɐǝᗡ", + "reflection.cosmiccore.bargain.back.answer.accept.response": "˙ɹnoʇǝp ɐ - buıpuǝ uɐ ʇoN ˙ʍou ʇuıodʎɐʍ ɐ sǝɯoɔǝq ɥʇɐǝᗡ", + "reflection.cosmiccore.bargain.back.answer.accept.text": "˙ʞɔɐq ʎɐʍ ʎɯ puıɟ oʇ ǝɯ ɥɔɐǝ⟘", + "reflection.cosmiccore.bargain.back.answer.refuse.response": "˙ssǝןpɹɐbǝɹ ǝsoɥʇ ɟo ʎʇuǝןd ǝʌɐɥ ןן,noʎ ˙sǝɔuǝnbǝsuoƆ", + "reflection.cosmiccore.bargain.back.answer.refuse.text": "˙sǝɔuǝnbǝsuoɔ ǝʌɐɥ pןnoɥs ɥʇɐǝᗡ", + "reflection.cosmiccore.bargain.back.description": "ןןǝɟ noʎ ǝɹǝɥʍ oʇ uɹnʇǝᴚ", + "reflection.cosmiccore.bargain.back.dialogue.0": "˙sʇuıod ʎɹɐɹʇıqɹɐ oʇ 'suʍɐds oʇ 'spǝq oʇ ʞɔɐq noʎ spuǝS ˙noʎ sɹǝʇʇɐɔs ɥʇɐǝᗡ", + "reflection.cosmiccore.bargain.back.dialogue.1": "¿ןןǝɟ noʎ ǝɹǝɥʍ oʇ uɹnʇǝɹ pןnoɔ noʎ ɟı ʇɐɥʍ ʇnᗺ", + "reflection.cosmiccore.bargain.back.dialogue.2": "˙ɟןǝsʇı ɥʇɐǝp ɥbnoɹɥʇ ʞɔɐq ɥɔɐǝɹ o⟘ ˙ɹǝqɯǝɯǝɹ oʇ noʎ ɥɔɐǝʇ uɐɔ I", + "reflection.cosmiccore.bargain.back.name": "ʞɔɐᗺ ʎɐM ǝɥ⟘", + "reflection.cosmiccore.bargain.back.on_accept": "˙ʞɔɐq ʇı ʍoןןoɟ uɐɔ noʎ ˙ɥʇɐǝɹq ʇsɐן ɹnoʎ oʇ noʎ sʇɔǝuuoɔ pɐǝɹɥʇ Ɐ", + "reflection.cosmiccore.bargain.back.on_defy": "˙ǝɹoɯ ǝɔuo ןɐuıɟ sǝɯoɔǝq ɥʇɐǝᗡ ˙sdɐus pɐǝɹɥʇ ǝɥ⟘", + "reflection.cosmiccore.bargain.back.question": "¿ɥʇɐd s,ɥʇɐǝp ǝɔɐɹʇǝɹ oʇ uɹɐǝן noʎ ןןıM", + "reflection.cosmiccore.bargain.carapace.answer.refuse.response": "˙ʎɹɐɹodɯǝʇ ʍoH ˙ןɐʇɹoɯ ʍoH ˙ǝןıbɐɹɟ ʍoH ˙buıןǝǝℲ", + "reflection.cosmiccore.bargain.carapace.answer.refuse.text": "˙ǝɹnpuǝ ʎןǝɹǝɯ uɐɥʇ ןǝǝɟ ɹǝɥʇɐɹ pןnoʍ I", + "reflection.cosmiccore.bargain.carapace.answer.survive.drawback.0": "sǝɔɹnos ןןɐ ɯoɹɟ buıןɐǝɥ %0ᄅ-", + "reflection.cosmiccore.bargain.carapace.answer.survive.drawback.1": "ssǝuǝʌıʇɔǝɟɟǝ uoıʇod pǝɔnpǝᴚ", + "reflection.cosmiccore.bargain.carapace.answer.survive.power.0": ")suoɔı ɹoɯɹɐ ןןnɟ ㄣ( sʇuıod ɹoɯɹɐ 8+", + "reflection.cosmiccore.bargain.carapace.answer.survive.power.1": "ɹoɯɹɐ uɹoʍ ɥʇıʍ sʞɔɐʇS", + "reflection.cosmiccore.bargain.carapace.answer.survive.response": "˙ǝןqɐɹnp ǝɹoɯ buıɥʇǝɯos buıɯoɔǝq ǝɹɐ noʎ ˙suǝpɹɐH ˙suǝʇɥbıʇ uıʞs ɹnoʎ", + "reflection.cosmiccore.bargain.carapace.answer.survive.text": "˙ןɐʌıʌɹns ǝsooɥɔ I ˙ǝɯ uǝpɹɐH", + "reflection.cosmiccore.bargain.carapace.description": "ɹoɯɹɐ buıʌıן oʇuı suǝpɹɐɥ ɥsǝןɟ ɹnoʎ", + "reflection.cosmiccore.bargain.carapace.dialogue.0": "˙ǝןqɐɹǝuןnʌ oS ˙ʇɟos os sı uıʞs ɹnoʎ", + "reflection.cosmiccore.bargain.carapace.dialogue.1": "˙noʎ uǝʇɐǝɹɥʇ ןןɐ ʎǝɥʇ - ǝuoʇs buıןןɐɟ ʎɹǝʌǝ 'ʍɐןɔ ʎɹǝʌǝ 'ǝpɐןq ʎɹǝʌƎ", + "reflection.cosmiccore.bargain.carapace.dialogue.2": "˙buıɹnpuǝ ˙˙˙ǝɹoɯ buıɥʇǝɯos ʇı ǝʞɐW ˙ɥsǝןɟ ɹnoʎ uǝpɹɐɥ uɐɔ I", + "reflection.cosmiccore.bargain.carapace.dialogue.3": "˙ɥsıuıɯıp ןןıʍ ǝbɐɯɐᗡ ˙ɟɟo ǝɔuɐןb ןןıʍ sʍoןᗺ", + "reflection.cosmiccore.bargain.carapace.dialogue.4": "˙ʇuɐʇsıp ˙˙˙ǝɯoɔǝq ןןıʍ ɥɔno⟘ ˙ssǝן ןǝǝɟ ןןıʍ noʎ ʇnᗺ", + "reflection.cosmiccore.bargain.carapace.name": "ǝɔɐdɐɹɐƆ", + "reflection.cosmiccore.bargain.carapace.on_accept": "˙ʇuıod ǝɥʇ s,ʇɐɥ⟘ ˙ʇɹnɥ ʇ,usǝop ʇI ˙suǝʇɥbıʇ puɐ sǝןddıɹ ɥsǝןɟ ɹnoʎ", + "reflection.cosmiccore.bargain.carapace.on_defy": "˙uıɐbɐ ʇɟos ǝɹɐ noʎ ˙ǝɹnʇxǝʇ ʎɹǝʌǝ 'ǝzǝǝɹq ʎɹǝʌǝ - ʞɔɐq spooןɟ uoıʇɐsuǝS", + "reflection.cosmiccore.bargain.carapace.question": "¿ןɐʌıʌɹns ɹoɟ uoıʇɐsuǝs ǝɔıɟıɹɔɐs noʎ ןןıM", + "reflection.cosmiccore.bargain.cinder.answer.burn.drawback.0": "sǝɔɹnos pןoɔ puɐ buızǝǝɹɟ ɯoɹɟ ǝbɐɯɐp xᄅ", + "reflection.cosmiccore.bargain.cinder.answer.burn.drawback.1": "ʇuɐsɐǝןdun sןǝǝɟ 'ɹǝʍoןs sǝɥsınbuıʇxǝ ɹǝʇɐM", + "reflection.cosmiccore.bargain.cinder.answer.burn.power.0": "ʎʇıunɯɯı ɐʌɐן puɐ ǝɹıɟ ǝʇǝןdɯoƆ", + "reflection.cosmiccore.bargain.cinder.answer.burn.power.1": "ʎןǝɟɐs ɐʌɐן uı ɯıʍs uɐƆ", + "reflection.cosmiccore.bargain.cinder.answer.burn.response": "˙puǝıɹɟ ɐ sǝɯoɔǝq ǝɹıɟ puɐ 'ʎɹoɯǝɯ ɐ sǝɯoɔǝq uıɐd ǝɥʇ uǝɥ⟘ ˙ʇuǝɯoɯ ɐ ɹoɟ ʇsnſ ˙sʇɹnɥ ʇI", + "reflection.cosmiccore.bargain.cinder.answer.burn.text": "˙ǝunɯɯı ǝɯ ǝʞɐW ˙ʎןǝʇǝןdɯoɔ ǝɯ uɹnᗺ", + "reflection.cosmiccore.bargain.cinder.answer.refuse.response": "˙ǝןqou ʍoH ˙ʇɥbnoɥʇ ʇnoɥʇıʍ noʎ ǝɯnsuoɔ pןnoʍ ʇɐɥʇ buıɥʇǝɯos ɹoℲ ˙ʇɔǝdsǝᴚ", + "reflection.cosmiccore.bargain.cinder.answer.refuse.text": "˙pǝpuǝıɹɟǝq ʇou 'pǝʇɔǝdsǝɹ ǝq pןnoɥs ǝɹıℲ", + "reflection.cosmiccore.bargain.cinder.description": "pǝuɹnq ʎpɐǝɹןɐ sɐɥ ʇɐɥʍ ɯɹɐɥ ʇouuɐɔ ǝɹıℲ", + "reflection.cosmiccore.bargain.cinder.dialogue.0": "˙ʎoɹʇsǝp oʇ ǝɹnʇɐu sʇı uı sı ʇI ˙sǝɯnsuoɔ ǝɹıℲ", + "reflection.cosmiccore.bargain.cinder.dialogue.1": "˙ʞɹɐɯ sʇı sǝʌɐǝן noʎ sǝɥɔnoʇ ʇɐɥʇ ǝɯɐןɟ ʎɹǝʌƎ", + "reflection.cosmiccore.bargain.cinder.dialogue.2": "˙ʎןɹǝʇʇ∩ ˙ʎןǝʇǝןdɯoƆ ¿pǝuɹnq ʎpɐǝɹןɐ pɐɥ noʎ ɟı ʇɐɥʍ ʇnᗺ", + "reflection.cosmiccore.bargain.cinder.dialogue.3": "˙ɹǝqɯǝ puɐ ɥsɐ ʎpɐǝɹןɐ sı ʇɐɥʍ ǝɯnsuoɔ ʇouuɐɔ ǝɹıℲ", + "reflection.cosmiccore.bargain.cinder.dialogue.4": "˙pǝɥʇɐɔsun souɹǝɟuı ɥbnoɹɥʇ ʞןɐʍ ןןıʍ noʎ ˙ʎʇıןıqɐɹǝuןnʌ ɹnoʎ ʎɐʍɐ uɹnq ǝɯ ʇǝꞀ", + "reflection.cosmiccore.bargain.cinder.name": "ɹǝpuıƆ", + "reflection.cosmiccore.bargain.cinder.on_accept": "˙uıɐbɐ noʎ uǝʇɥbıɹɟ ɹǝʌǝu ןןıʍ ǝɹıℲ ˙sǝpǝɔǝɹ uǝɥʇ 'noʎ ɥbnoɹɥʇ spooןɟ ʇɐǝH", + "reflection.cosmiccore.bargain.cinder.on_defy": "˙ʍou noʎ ǝǝs ʎǝɥʇ uǝɥʍ ʎןıɹbunɥ ɹǝʞɔıןɟ sǝɯɐןℲ ˙ʎɐʍɐ suıɐɹp ɥʇɯɹɐʍ ǝɥ⟘", + "reflection.cosmiccore.bargain.cinder.question": "¿uıɐbɐ noʎ ʇɹnɥ ɹǝʌǝu uɐɔ ʇı os 'noʎ ɯıɐןɔ ǝɯɐןɟ ǝɥʇ ʇǝן noʎ ןןıM", + "reflection.cosmiccore.bargain.darksight.answer.refuse.response": "˙sǝɔɐןd dǝǝp ǝɥʇ uı sʇsɐן ʇı buoן ʍoɥ ǝǝS ˙uǝɥʇ ɥɔɹoʇ ɹnoʎ oʇ buıןƆ", + "reflection.cosmiccore.bargain.darksight.answer.refuse.text": "˙ɥbnouǝ ןןǝʍ ǝɯ sǝʌɹǝs ʇɥbıן ǝɥ⟘", + "reflection.cosmiccore.bargain.darksight.answer.yes.drawback.0": "ʇɥbıןuns ʇɥbıɹq uı ʇɔǝɟɟǝ ssǝupuıןᗺ", + "reflection.cosmiccore.bargain.darksight.answer.yes.drawback.1": "ʎɐp buıɹnp punoɹbɹǝpun ʎɐʇs ɹo ʇǝɯןǝɥ ɹɐǝʍ ʇsnW", + "reflection.cosmiccore.bargain.darksight.answer.yes.power.0": "ʇɔǝɟɟǝ uoısıΛ ʇɥbıN ʇuǝuɐɯɹǝԀ", + "reflection.cosmiccore.bargain.darksight.answer.yes.power.1": "ssǝuʞɹɐp ǝʇǝןdɯoɔ uı ǝǝS", + "reflection.cosmiccore.bargain.darksight.answer.yes.response": "˙uıɐɯop ɹnoʎ sǝɯoɔǝq ʞɹɐp ǝɥ⟘ ˙buıʇɐןıp dǝǝʞ puɐ ˙˙˙ǝʇɐןıp sןıdnd ɹnoʎ", + "reflection.cosmiccore.bargain.darksight.answer.yes.text": "˙ssǝuʞɹɐp ǝɥʇ uı ǝǝs ǝɯ ʇǝꞀ", + "reflection.cosmiccore.bargain.darksight.description": "ssǝuʞɹɐp ʇsǝdǝǝp ǝɥʇ ɥbnoɹɥʇ ǝǝS", + "reflection.cosmiccore.bargain.darksight.dialogue.0": "˙ʇsɹıɟ ʇɐ 'sǝop ǝɹnʇɐǝɹɔ ʎɹǝʌƎ ˙ʞɹɐp ǝɥʇ ɹɐǝɟ noʎ", + "reflection.cosmiccore.bargain.darksight.dialogue.1": "˙ǝɔuǝsǝɹd ʇoN ˙buıɥʇǝɯos ɟo ǝɔuǝsqɐ ǝɥʇ ʎןǝɹǝɯ sı ssǝuʞɹɐp ʇnᗺ", + "reflection.cosmiccore.bargain.darksight.dialogue.2": "˙uǝǝsun sʞɹnן ʇɐɥʍ ǝǝs o⟘ ˙ʍopɐɥs ǝɥʇ ʞuıɹp oʇ sǝʎǝ ɹnoʎ ɥɔɐǝʇ uɐɔ I", + "reflection.cosmiccore.bargain.darksight.dialogue.3": "˙noʎ oʇ sʇǝɹɔǝs sʇı ɹǝpuǝɹɹns ןןıʍ ɹǝuɹoɔ uǝppıɥ ʎɹǝʌƎ", + "reflection.cosmiccore.bargain.darksight.dialogue.4": "˙uɹnq oʇ uıbǝq ןןıʍ ʇɥbıן ǝɥʇ - pǝuɹɐʍ ǝq ʇnᗺ", + "reflection.cosmiccore.bargain.darksight.name": "ʇɥbısʞɹɐᗡ", + "reflection.cosmiccore.bargain.darksight.on_accept": "˙ʍou buıɥʇʎɹǝʌǝ ǝǝs noʎ ˙uoısıʌ ɹnoʎ ɯoɹɟ ʇɐǝɹʇǝɹ sʍopɐɥs ǝɥ⟘", + "reflection.cosmiccore.bargain.darksight.on_defy": "˙ǝɹoɯ ǝɔuo noʎ oʇ sʇǝɹɔǝs sʇı sǝsoןɔ ssǝuʞɹɐp ǝɥ⟘ ˙ʞɔɐq spooןɟ ʇɥbıꞀ", + "reflection.cosmiccore.bargain.darksight.question": "¿ʇɥbıs-ʍopɐɥs ɟo ʇɟıb ǝɥʇ ɹoɟ uns ǝɥʇ ǝpɐɹʇ noʎ ןןıM", + "reflection.cosmiccore.bargain.depths.answer.embrace.drawback.0": "sǝʇǝןdǝp ʎןןnɟ uǝbʎxo uǝɥʍ ɥʇɐǝp ʇuɐʇsuI", + "reflection.cosmiccore.bargain.depths.answer.embrace.drawback.1": "ɥʇɐǝp ʇsnظ - buıuɹɐʍ ǝbɐɯɐp buıuʍoɹp oN", + "reflection.cosmiccore.bargain.depths.answer.embrace.power.0": "ɹǝʇɐʍɹǝpun ʎʇıɔɐdɐɔ uǝbʎxo xϛ", + "reflection.cosmiccore.bargain.depths.answer.embrace.power.1": "sǝɹǝɥdsoɯʇɐ ɔıxoʇ uı ɥʇɐǝɹq pǝpuǝʇxƎ", + "reflection.cosmiccore.bargain.depths.answer.embrace.response": "˙ɯooɹ spǝǝu ʎʇıɔɐdɐɔ ʍǝu ǝɥ⟘ ˙ןɐɯɹou s,ʇɐɥ⟘ ˙ʍou ʍoןןoɥ sןǝǝɟ ʇsǝɥɔ ɹnoʎ", + "reflection.cosmiccore.bargain.depths.answer.embrace.text": "˙sɥʇdǝp ǝɥʇ ɹoɟ ǝɯ ǝʞɐɯǝᴚ", + "reflection.cosmiccore.bargain.depths.answer.refuse.response": "˙ǝʌɐɥ sʎɐʍןɐ ʎǝɥ⟘ ˙ʎןʇuǝıʇɐd ʇıɐʍ sɥʇdǝp ǝɥ⟘", + "reflection.cosmiccore.bargain.depths.answer.refuse.text": "˙ɥʇɐǝɹq ןɐʇɹoɯ ʎɯ dǝǝʞ ןן,I", + "reflection.cosmiccore.bargain.depths.description": "sǝɔuǝnbǝsuoɔ ןɐʇɐɟ ɥʇıʍ ɥʇɐǝɹq pǝɔuɐɥuƎ", + "reflection.cosmiccore.bargain.depths.dialogue.0": "˙pɐǝɥ ɹnoʎ ɹǝʌo buısoןɔ ɹǝʇɐʍ ǝɥʇ ʇןǝɟ ǝʌ,noʎ", + "reflection.cosmiccore.bargain.depths.dialogue.1": "˙ɔıuɐd ǝɥ⟘ ˙sbunן ɹnoʎ uı uɹnq ǝʇɐɹǝdsǝp ʇɐɥ⟘", + "reflection.cosmiccore.bargain.depths.dialogue.2": "˙sʇıɯıן ןɐʇɹoɯ puoʎǝq ʎʇıɔɐdɐɔ ɯǝɥʇ ǝʌı⅁ ˙suɐbɹo ǝןıbɐɹɟ ǝsoɥʇ ǝʞɐɯǝɹ uɐɔ I", + "reflection.cosmiccore.bargain.depths.dialogue.3": "˙sʇɐǝqʇɹɐǝɥ uǝǝʍʇǝq pıoʌ ǝɥʇ ןןıɟ oʇ ɥɔʇǝɹʇs ןןıʍ ɥʇɐǝɹq ɹnoʎ", + "reflection.cosmiccore.bargain.depths.dialogue.4": "˙buıuɹɐʍ ou ǝq ןןıʍ ǝɹǝɥʇ 'ʎʇdɯǝ ʎןןɐuıɟ ʎǝɥʇ uǝɥʍ - puɐʇsɹǝpun ʇnᗺ", + "reflection.cosmiccore.bargain.depths.dialogue.5": "˙ǝɔuǝןıs ˙˙˙ʇsnſ ˙buıpɐɟ ןɐnpɐɹb oN ˙sdsɐb ǝʇɐɹǝdsǝp oN", + "reflection.cosmiccore.bargain.depths.name": "sɥʇdǝᗡ ǝɥ⟘", + "reflection.cosmiccore.bargain.depths.on_accept": "˙ʍou ʇuǝɹǝɟɟıp sǝʇsɐʇ ɹıɐ ǝɥ⟘ ˙ʇsǝɥɔ ɹnoʎ uı sʇɟıɥs buıɥʇǝɯoS", + "reflection.cosmiccore.bargain.depths.on_defy": "˙uıɐbɐ ןɐʇɹoɯ ǝɹɐ noʎ ˙ǝןbbnɹʇs ɹǝqɯǝɯǝɹ 'ɔıuɐd ɹǝqɯǝɯǝɹ sbunן ɹnoʎ - dsɐb noʎ", + "reflection.cosmiccore.bargain.depths.question": "¿ɥʇɐǝɹq ɹnoʎ ǝdɐɥsǝɹ ǝɯ ʇǝן noʎ ןןıM", + "reflection.cosmiccore.bargain.home.answer.accept.drawback.0": "ʇɹodǝןǝʇ ɹǝd ʇsoɔ uoısoɹǝ 0Ɩ", + "reflection.cosmiccore.bargain.home.answer.accept.drawback.1": "ǝɯoɥ ɯoɹɟ ɹɐɟ ǝןıɥʍ uıɐb ԀX %0Ɩ-", + "reflection.cosmiccore.bargain.home.answer.accept.power.0": "ʇuıod pǝq/uʍɐds oʇ ʇɹodǝןǝʇ ʇuɐʇsuI", + "reflection.cosmiccore.bargain.home.answer.accept.power.1": "sǝsn uǝǝʍʇǝq uʍopןooɔ ǝʇnuıɯ ϛ", + "reflection.cosmiccore.bargain.home.answer.accept.response": "˙ʎɐʍɐ ʇɥbnoɥʇ ɐ uɐɥʇ ǝɹoɯ ɹǝʌǝu sı ǝɯoH ¿ʍou ןןnd ǝɥʇ ןǝǝℲ", + "reflection.cosmiccore.bargain.home.answer.accept.text": "˙ǝɯoɥ ʎɯ oʇ ǝɯ puıᗺ", + "reflection.cosmiccore.bargain.home.answer.refuse.response": "˙ǝʌıʇıɯıɹd ʎןbuıɯɹɐɥɔ ʍoH ˙buıʞןɐʍ ɟo sǝןıɯ ɥbnoɹɥ⟘ ˙pǝuɹɐƎ", + "reflection.cosmiccore.bargain.home.answer.refuse.text": "˙pǝuoɯɯns ʇou 'pǝuɹɐǝ ǝq pןnoɥs ǝɯoH", + "reflection.cosmiccore.bargain.home.description": "uǝdo sʎɐʍןɐ sı ǝɯoɥ ʎɐʍ ǝɥ⟘", + "reflection.cosmiccore.bargain.home.dialogue.0": "˙sןɐʇɹoɯ ɹoɟ ʇdǝɔuoɔ ןnɟɹǝʍod ɐ ɥɔnS ˙ǝɯoH", + "reflection.cosmiccore.bargain.home.dialogue.1": "˙noʎ spunoɹb ʇɐɥʇ ɹoɥɔuɐ ǝɥ⟘ ˙oʇ uɹnʇǝɹ noʎ ǝɔɐןd ǝɥ⟘", + "reflection.cosmiccore.bargain.home.dialogue.2": "˙ǝןqɐʞɐǝɹqu∩ ˙ʇuɐʇsuI ˙ɹǝbuoɹʇs uoıʇɔǝuuoɔ ʇɐɥʇ ǝʞɐɯ uɐɔ I", + "reflection.cosmiccore.bargain.home.name": "pɹɐʍǝɯoH", + "reflection.cosmiccore.bargain.home.on_accept": "˙ǝɯıʇʎuɐ ʇı ןןnԀ ˙ǝɯoɥ puɐ noʎ uǝǝʍʇǝq sǝɥɔʇǝɹʇs pıoʌ ɟo pɹoɔ Ɐ", + "reflection.cosmiccore.bargain.home.on_defy": "˙uoıʇɐuıʇsǝp ɐ ʇou 'uıɐbɐ ʎǝuɹnoظ ɐ sı ǝɯoH ˙sǝʌןossıp pɹoɔ ǝɥ⟘", + "reflection.cosmiccore.bargain.home.question": "¿pıoʌ ɟo suıɐɥɔ ɥʇıʍ ǝɯoɥ ɹnoʎ oʇ ɟןǝsɹnoʎ puıq noʎ ןןıM", + "reflection.cosmiccore.bargain.quake_movement.answer.refuse.response": "˙noʎ sǝʌɹǝs ʇɐɥʇ buoן ʍoɥ ǝǝs ןןɐɥs ǝM ˙uɐıɹʇsǝpǝd ǝɥʇ uı ɥʇıɐɟ ɥɔnS", + "reflection.cosmiccore.bargain.quake_movement.answer.refuse.text": "˙ɯɥʇʎɥɹ uʍo ɹıǝɥʇ ʍouʞ ʇǝǝɟ ʎW", + "reflection.cosmiccore.bargain.quake_movement.answer.yes.drawback.0": "sɹǝʌɹǝsqo oʇ ןɐɹnʇɐuun sןǝǝɟ ʇuǝɯǝʌoW", + "reflection.cosmiccore.bargain.quake_movement.answer.yes.power.0": "ɯnʇuǝɯoɯ spןınq puɐ sǝʌɹǝsǝɹd buıddoɥ ʎuunᗺ", + "reflection.cosmiccore.bargain.quake_movement.answer.yes.power.1": "ןoɹʇuoɔ uoıʇɔǝɹıp ɹıɐ-pıɯ ɹoɟ buıɟɐɹʇs ɹıⱯ", + "reflection.cosmiccore.bargain.quake_movement.answer.yes.response": "˙ʍouʞ oʇ ʇuɐǝɯ ɹǝʌǝu ǝɹǝʍ ʎǝɥʇ sǝıɹoʇɔǝظɐɹʇ buıuɹɐǝן 'buıɹıʍǝɹ sǝןɔsnɯ ɹnoʎ - ʍou ʇı ןǝǝℲ", + "reflection.cosmiccore.bargain.quake_movement.answer.yes.text": "˙puıʍ ǝɥʇ ǝʞıן ǝʌoɯ oʇ ǝɯ ɥɔɐǝ⟘", + "reflection.cosmiccore.bargain.quake_movement.description": "uoıʇoɯoɔoן ןɐɹnʇɐuun ɥbnoɹɥʇ ɟןǝsʇı ɯnʇuǝɯoɯ ɹǝʇsɐW", + "reflection.cosmiccore.bargain.quake_movement.dialogue.0": "˙ןnɟɹɐǝℲ ˙ʇuɐʇısǝH ˙ʎǝɹd ǝʞıן ǝʌoɯ noʎ", + "reflection.cosmiccore.bargain.quake_movement.dialogue.1": "˙pıbıɹ ˙˙˙os ǝɯɐɔǝq sɔısʎɥd ǝɹoɟǝᗺ ˙ʎןʇuǝɹǝɟɟıp pǝʌoɯ sbuıɥʇ uǝɥʍ ɹǝqɯǝɯǝɹ I", + "reflection.cosmiccore.bargain.quake_movement.dialogue.2": "˙ʎɐʍ ɹǝpןo ʇɐɥʇ ɹǝqɯǝɯǝɹ oʇ sbǝן ɹnoʎ ɥɔɐǝʇ uɐɔ I", + "reflection.cosmiccore.bargain.quake_movement.dialogue.3": "˙uıɐbɐ ssǝuןןıʇs ɥʇıʍ ʇuǝʇuoɔ ǝq ɹǝʌǝu ןןıʍ ʎǝɥʇ 'uɹɐǝן ʎǝɥʇ ǝɔuo ʇnᗺ", + "reflection.cosmiccore.bargain.quake_movement.name": "ʎʇıɔoןǝΛ", + "reflection.cosmiccore.bargain.quake_movement.on_accept": "˙ʇɔuıʇsuı sǝɯoɔǝq ʇuǝɯǝʌoW ˙ɯɹoɟǝɹ puɐ ʞɔɐɹɔ sʇuıoظ ɹnoʎ", + "reflection.cosmiccore.bargain.quake_movement.on_defy": "˙ɯɐǝɹp ɐ ǝʞıן sǝpɐɟ pǝǝds ǝɥ⟘ ˙ʎןןɐɯɹou ʞןɐʍ oʇ sɐʍ ʇı ʇɐɥʍ ɹǝqɯǝɯǝɹ sbǝן ɹnoʎ", + "reflection.cosmiccore.bargain.quake_movement.question": "¿noʎ uıɥʇıʍ sʇıɐʍ ʇɐɥʇ ʎʇıɔoןǝʌ ǝɥʇ ǝɔɐɹqɯǝ noʎ ןןıM", + "reflection.cosmiccore.bargain.reach.answer.further.drawback.0": "pǝǝds buıuıɯ %ϛƖ-", + "reflection.cosmiccore.bargain.reach.answer.further.drawback.1": "pǝɔnpǝɹ ǝbuɐɹ dnʞɔıd ɯǝʇI", + "reflection.cosmiccore.bargain.reach.answer.further.power.0": ")ɹǝɥʇɹnɟ ɯoɹɟ pןınq( ɥɔɐǝɹ ʞɔoןq Ɛ+", + "reflection.cosmiccore.bargain.reach.answer.further.power.1": "ɥɔɐǝɹ ʞɔɐʇʇɐ ᄅ+", + "reflection.cosmiccore.bargain.reach.answer.further.response": "˙ʎɐʍ ʇɐɥʇ ɹǝısɐǝ s,ʇI ˙ʎןǝsoןɔ ooʇ spuɐɥ ɹnoʎ ʇɐ ʞooן ʇ,uoᗡ ˙ǝɹǝɥ⟘", + "reflection.cosmiccore.bargain.reach.answer.further.text": "˙buıɥʇʎɹǝʌǝ dsɐɹb oʇ ʇuɐʍ I ˙ɹǝɥʇɹnɟ ǝɯ ɥɔʇǝɹʇS", + "reflection.cosmiccore.bargain.reach.answer.refuse.response": "˙op sʎɐʍןɐ ʎǝɥ⟘ ˙ǝɹoɯ ʇuɐʍ ןן,noʎ ʇnᗺ ˙ʍou ɹoℲ", + "reflection.cosmiccore.bargain.reach.answer.refuse.text": "˙ʇuǝıɔıɟɟns sı ɥɔɐǝɹ ʎW", + "reflection.cosmiccore.bargain.reach.description": "sʇıɯıן ןɐɹnʇɐu puoʎǝq spuǝʇxǝ dsɐɹb ɹnoʎ", + "reflection.cosmiccore.bargain.reach.dialogue.0": "˙sɯɹɐ ʇɹoɥs ɟo uoıʇɐɹʇsnɹɟ ןɐuɹǝʇǝ ǝɥ⟘ ˙ɹɐɟ os ʇǝʎ 'ǝsoןɔ oS", + "reflection.cosmiccore.bargain.reach.dialogue.1": "˙noʎ buıʞɔoW ˙ɥɔɐǝɹ ɟo ʇno ʎןʇɥbıןs ʇsnظ buıɥʇʎɹǝʌƎ", + "reflection.cosmiccore.bargain.reach.dialogue.2": "˙sʇıɯıן ןɐʇɹoɯ puoʎǝq dsɐɹb ɹnoʎ ɥɔʇǝɹʇS ˙noʎ puǝʇxǝ uɐɔ I", + "reflection.cosmiccore.bargain.reach.dialogue.3": "˙buıןʇʇǝsun ˙˙˙ɯǝɥʇ puıɟ ʎɐɯ sɹǝɥʇo ʇnᗺ ˙ʞǝǝs ʎǝɥʇ ʇɐɥʍ puıɟ ןןıʍ sɯɹɐ ɹnoʎ", + "reflection.cosmiccore.bargain.reach.name": "ɥɔɐǝᴚ", + "reflection.cosmiccore.bargain.reach.on_accept": "˙ɹǝbuoן buıǝq ɹǝqɯǝɯǝɹ sɯɹɐ ɹnoʎ ˙sɹǝpןnoɥs ɹnoʎ uı sʇɟıɥs buıɥʇǝɯoS", + "reflection.cosmiccore.bargain.reach.on_defy": "˙uıɐbɐ ןןɐɯs puɐ ǝsoןɔ sןǝǝɟ pןɹoʍ ǝɥ⟘ ˙ןɐɯɹou oʇ ʞɔɐq ʇɔɐɹʇuoɔ sɯɹɐ ɹnoʎ", + "reflection.cosmiccore.bargain.reach.question": "¿ןɐɹnʇɐuun ǝɥʇ oʇuı ɥɔɐǝɹ ɹnoʎ puǝʇxǝ noʎ ןןıM", + "reflection.cosmiccore.bargain.satiated.answer.empty.drawback.0": "sɹɐq ɹǝbunɥ ssǝן %0ϛ sǝɹoʇsǝɹ pooℲ", + "reflection.cosmiccore.bargain.satiated.answer.empty.drawback.1": "sɟɟnq pǝsɐq-pooɟ ɯoɹɟ ʇıɟǝuǝq ʇouuɐƆ", + "reflection.cosmiccore.bargain.satiated.answer.empty.power.0": "ɹǝʍoןs %08 sǝʇǝןdǝp ɹǝbunH", + "reflection.cosmiccore.bargain.satiated.answer.empty.power.1": "uoıʇɐɹnʇɐs xƐ sǝpıʌoɹd pooℲ", + "reflection.cosmiccore.bargain.satiated.answer.empty.response": "˙uıɐbɐ ʇɐǝ oʇ pǝǝu ʎןnɹʇ ɹǝʌǝu ןןıʍ noʎ ˙sǝpɐɟ ssǝuıʇdɯǝ ǝɥ⟘", + "reflection.cosmiccore.bargain.satiated.answer.empty.text": "˙ɹǝbunɥ sıɥʇ ɯoɹɟ ǝɯ ǝǝɹℲ", + "reflection.cosmiccore.bargain.satiated.answer.refuse.response": "˙ǝɔɐןd ɹnoʎ ɟo noʎ puıɯǝɹ ןןıʍ ɹǝbunɥ ǝɥ⟘ ˙pɹoʍ ןɐʇɹoɯ ɐ ɥɔnS ˙ʎoظuƎ", + "reflection.cosmiccore.bargain.satiated.answer.refuse.text": "˙ɹǝbunɥ ǝɥʇ dǝǝʞ ןן,I ˙sןɐǝɯ ʎɯ ʎoظuǝ I", + "reflection.cosmiccore.bargain.satiated.description": "ʎɹoɯǝɯ ʇuɐʇsıp ɐ sǝɯoɔǝq ɹǝbunH", + "reflection.cosmiccore.bargain.satiated.dialogue.0": "˙ǝɯnsuoɔ oʇ pǝǝu ssǝןpuǝ ǝɥ⟘ ˙buıʍɐub ʇuɐʇsuoɔ ǝɥ⟘", + "reflection.cosmiccore.bargain.satiated.dialogue.1": "˙ʇuɐɹʎʇ ɐ ǝʞıן noʎ sǝןnɹ ɥɔɐɯoʇs ɹnoʎ", + "reflection.cosmiccore.bargain.satiated.dialogue.2": "¿ǝʇɐʇs ןɐɹnʇɐu ɹnoʎ ssǝuןןnɟ ǝʞɐW ¿ʇı ǝɔuǝןıs pןnoɔ I ɟı ʇɐɥM", + "reflection.cosmiccore.bargain.satiated.dialogue.3": "˙pǝǝu ɹoɟ ɹǝʌǝu - ןɐnʇıɹ ɹoɟ 'ǝʇsɐʇ ɹoɟ ʇɐǝ pןnoʍ noʎ", + "reflection.cosmiccore.bargain.satiated.dialogue.4": "˙ǝɹoɯ buıɥʇoN ˙ןǝnɟ ˙˙˙sǝɯoɔǝq pooℲ ˙ǝpɐɟ pןnoʍ ɟןǝsʇı ǝʇsɐʇ ʇnᗺ", + "reflection.cosmiccore.bargain.satiated.name": "pǝʇɐıʇɐS", + "reflection.cosmiccore.bargain.satiated.on_accept": "˙ɯopǝǝɹℲ ˙ʎןןǝq ɹnoʎ uı ǝɔuǝןıS ˙sdoʇs buıʍɐub ǝɥ⟘", + "reflection.cosmiccore.bargain.satiated.on_defy": "˙ǝʞıן sןǝǝɟ pǝǝu ʇɐɥʍ ɹǝqɯǝɯǝɹ noʎ ˙ǝɔuɐǝbuǝʌ ɐ ɥʇıʍ suɹnʇǝɹ ɹǝbunH", + "reflection.cosmiccore.bargain.satiated.question": "¿ɹǝbunɥ ɯoɹɟ ɯopǝǝɹɟ ɹoɟ buıʇɐǝ ɟo ǝɹnsɐǝןd ǝɥʇ ǝpɐɹʇ noʎ ןןıM", + "reflection.cosmiccore.bargain.soft_landing.answer.refuse.response": "˙ʇsnɯ noʎ ɟı ʇı ʞןɐʍ ʇnᗺ ˙ǝɹǝɥʍou oʇ ɥʇɐd ʍoןs Ɐ ˙uoıʇnɐƆ", + "reflection.cosmiccore.bargain.soft_landing.answer.refuse.text": "˙ʇı dǝǝʞ ןן,I ˙snoıʇnɐɔ ǝɯ sdǝǝʞ ɹɐǝℲ", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.drawback.0": "sǝɔɹnos ןןɐ ɯoɹɟ uǝʞɐʇ ǝbɐɯɐp %ϛƖ+", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.drawback.1": "ǝɔuɐʇsısǝɹ ʞɔɐqʞɔouʞ pǝɔnpǝᴚ", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.power.0": "ʎʇıunɯɯı ǝbɐɯɐp ןןɐɟ ǝʇǝןdɯoƆ", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.power.1": "ʇɥbıǝɥ ʎuɐ ɯoɹɟ sdoɹp ǝɟɐS", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.response": "˙ɹǝɥʇoɯ ɐ ǝʞıן noʎ ǝɔɐɹqɯǝ ןןıʍ punoɹb ǝɥ⟘ ˙pɐǝɥɐ o⅁ ˙dɯnſ", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.text": "˙buıןןɐɟ ɟo ɹɐǝɟ ʎɯ ʎɐʍɐ ǝʞɐ⟘", + "reflection.cosmiccore.bargain.soft_landing.description": "ʇɥbıǝɥ ʎuɐ ɯoɹɟ ʎןʇuǝb noʎ sǝɯoɔןǝʍ punoɹb ǝɥ⟘", + "reflection.cosmiccore.bargain.soft_landing.dialogue.0": "˙buıןןɐɟ ɟo ɹɐǝɟ ןɐɯıɹd ǝɥ⟘ ˙noʎ ʎɟıɹɹǝʇ sʇɥbıǝH", + "reflection.cosmiccore.bargain.soft_landing.dialogue.1": "˙noʎ sɯıɐןɔ ʎʇıʌɐɹb uǝɥʍ ʇuǝɯoɯ buıuǝʞɔıs ʇɐɥ⟘", + "reflection.cosmiccore.bargain.soft_landing.dialogue.2": "¿noʎ ǝʌɐbɹoɟ ˙˙˙punoɹb ǝɥʇ ɟı ʇɐɥʍ ʇnᗺ", + "reflection.cosmiccore.bargain.soft_landing.dialogue.3": "¿ʇɥbıǝɥ ǝɥʇ ɹǝʇʇɐɯ ou 'ʎןʇɟos pǝpuǝ ןןɐɟ ʎɹǝʌǝ ɟı ʇɐɥM", + "reflection.cosmiccore.bargain.soft_landing.dialogue.4": "˙uıɐbɐ doɹp ǝɥʇ ɹɐǝɟ ɹǝʌǝu pǝǝu noʎ", + "reflection.cosmiccore.bargain.soft_landing.name": "buıpuɐꞀ ʇɟoS", + "reflection.cosmiccore.bargain.soft_landing.on_accept": "˙ʍou ʎןʇuǝb ʇnq 'sןןnd ןןıʇs ʇI ˙sʇɟıɥs ʎʇıʌɐɹb ɥʇıʍ dıɥsuoıʇɐןǝɹ ɹnoʎ", + "reflection.cosmiccore.bargain.soft_landing.on_defy": "˙uıɐbɐ sɹǝʇʇɐɯ ןןɐɟ ʎɹǝʌƎ ˙sǝuoq ɹnoʎ oʇuı ʞɔɐq sǝɥsɐɹɔ ʇɥbıǝM", + "reflection.cosmiccore.bargain.soft_landing.question": "¿noʎ ɥɔʇɐɔ ɥʇɹɐǝ ǝɥʇ ʇǝן noʎ ןןıM", + "reflection.cosmiccore.bargain.stride.answer.accept.drawback.0": "sǝbpǝ ɟɟo ʞןɐʍ-ɥɔnoɹɔ ʇouuɐƆ", + "reflection.cosmiccore.bargain.stride.answer.accept.power.0": "ʇɥbıǝɥ ʞɔoןq Ɩ oʇ dn-dǝʇs oʇnⱯ", + "reflection.cosmiccore.bargain.stride.answer.accept.power.1": "ןɐsɹǝʌɐɹʇ uıɐɹɹǝʇ ɥʇooɯS", + "reflection.cosmiccore.bargain.stride.answer.accept.response": "˙ǝɔuǝıuǝʌuoɔ ɹnoʎ ɹoɟ ɟןǝsʇı sǝdɐɥsǝɹ uıɐɹɹǝʇ ʍoɥ ןǝǝℲ ˙ʍou ʞןɐM", + "reflection.cosmiccore.bargain.stride.answer.accept.text": "˙ǝɯ ǝɹoɟǝq uǝʇʇɐןɟ pןɹoʍ ǝɥʇ ʇǝꞀ", + "reflection.cosmiccore.bargain.stride.answer.refuse.response": "˙op sʎɐʍןɐ ʎǝɥ⟘ ˙ǝpoɹǝ ןןıʍ ʇI ˙uoıʇɐuıɯɹǝʇǝp ɥɔnS", + "reflection.cosmiccore.bargain.stride.answer.refuse.text": "˙ʎɐʍ uʍo ʎɯ qɯıןɔ ןן,I", + "reflection.cosmiccore.bargain.stride.description": "buıɥʇou ǝɹǝʍ ʎǝɥʇ ɟı sɐ sǝןɔɐʇsqo ɹǝʌo ʞןɐM", + "reflection.cosmiccore.bargain.stride.dialogue.0": "˙ǝןɔɐʇsqo ןןɐɯs ʎɹǝʌƎ ˙ʞɔoןq ʎɹǝʌƎ ˙ǝbpǝן ʎɹǝʌƎ", + "reflection.cosmiccore.bargain.stride.dialogue.1": "˙pǝqɯıןɔ ǝq oʇ pǝǝu ɹıǝɥʇ ɥʇıʍ noʎ ʞɔoɯ ʎǝɥ⟘", + "reflection.cosmiccore.bargain.stride.dialogue.2": "¿ǝpıɹʇs ɹnoʎ pǝʇɐpoɯɯoɔɔɐ ˙˙˙ʎןdɯıs pןɹoʍ ǝɥʇ ɟı ʇɐɥM", + "reflection.cosmiccore.bargain.stride.dialogue.3": "˙ɯǝɥʇ ʇǝǝɯ oʇ ǝsıɹ ןןıʍ punoɹb ǝɥ⟘ ˙punoɹb ǝɥʇ ǝʌɐǝן ɹǝʌǝu pǝǝu ʇǝǝɟ ɹnoʎ", + "reflection.cosmiccore.bargain.stride.name": "ǝpıɹʇS", + "reflection.cosmiccore.bargain.stride.on_accept": "˙ɥʇɐd ɹnoʎ ɥʇooɯs oʇ ɹǝbɐǝ 'ʎןʇɥbıןs ʇɟıɥs oʇ sɯǝǝs ɥʇɹɐǝ ǝɥ⟘", + "reflection.cosmiccore.bargain.stride.on_defy": "˙uıɐbɐ ǝbuǝןןɐɥɔ ɐ sı ǝbpǝן ʎɹǝʌƎ ˙ɟןǝsʇı sʇɹǝssɐǝɹ ʎʇıʌɐɹ⅁", + "reflection.cosmiccore.bargain.stride.question": "¿ǝbɐssɐd ɹnoʎ ǝɹoɟǝq ʍoq sǝןɔɐʇsqo ןןɐɥS", + "reflection.cosmiccore.bargain.swiftness.answer.accept.drawback.0": "ןןıʇs buıpuɐʇs uǝɥʍ ɹǝbunɥ pǝsɐǝɹɔuI", + "reflection.cosmiccore.bargain.swiftness.answer.accept.power.0": "pǝǝds ʇuǝɯǝʌoɯ %0ㄣ+", + "reflection.cosmiccore.bargain.swiftness.answer.accept.power.1": "uıɐɹp ɹǝbunɥ ʇnoɥʇıʍ ʇuıɹdS", + "reflection.cosmiccore.bargain.swiftness.answer.accept.response": "˙ʍoןןɐ pןnoɥs ʇɹɐǝɥ ʎuɐ uɐɥʇ ɹǝʇsɐɟ buıɔɐɹ ʇı ןǝǝℲ ˙ʍou sbuıs pooןq ɹnoʎ", + "reflection.cosmiccore.bargain.swiftness.answer.accept.text": "˙ǝɹnsɐǝɯ puoʎǝq ʇɟıʍs ǝɯ ǝʞɐW", + "reflection.cosmiccore.bargain.swiftness.answer.refuse.response": "˙ǝpɐɟ ןןıʍ ʇI ˙ʇuǝɯıʇuǝs ןɐʇɹoɯ ɐ ɥɔnS ˙ʇuǝʇuoƆ", + "reflection.cosmiccore.bargain.swiftness.answer.refuse.text": "˙ǝɔɐd ʎɯ ɥʇıʍ ʇuǝʇuoɔ ɯɐ I", + "reflection.cosmiccore.bargain.swiftness.description": "suıǝʌ ɹnoʎ ɥbnoɹɥʇ sǝsɹnoɔ pǝǝds ןɐɹnʇɐuɹǝdnS", + "reflection.cosmiccore.bargain.swiftness.dialogue.0": "¿ʇı ʇ,usǝop 'noʎ punoɹɐ ʎןʍoןs os sǝʌoɯ pןɹoʍ ǝɥ⟘", + "reflection.cosmiccore.bargain.swiftness.dialogue.1": "˙unɹ oʇ ǝɥɔɐ noʎ ǝןıɥʍ sǝssɐןoɯ ɥbnoɹɥʇ buıbpnɹʇ ǝsןǝ ǝuoʎɹǝʌƎ", + "reflection.cosmiccore.bargain.swiftness.dialogue.2": "˙ɯɐǝɹp buıpɐɟ ɐ ǝʞıן ʇsɐd ɹnןq pןɹoʍ ǝɥʇ ǝʞɐW ˙noʎ ǝʇɐɹǝןǝɔɔɐ uɐɔ I", + "reflection.cosmiccore.bargain.swiftness.name": "ssǝuʇɟıʍS", + "reflection.cosmiccore.bargain.swiftness.on_accept": "˙ʎbɹǝuǝ ssǝןʇsǝɹ ɥʇıʍ ɥɔʇıʍʇ noʎ ˙sǝןɔsnɯ ɹnoʎ ɥbnoɹɥʇ sɔɹɐ buıuʇɥbıꞀ", + "reflection.cosmiccore.bargain.swiftness.on_defy": "˙ǝɹoɯ ǝɔuo uɐɯnɥ ʎןǝɹǝɯ ǝɹɐ noʎ ˙noʎ punoɹɐ dn spǝǝds pןɹoʍ ǝɥ⟘", + "reflection.cosmiccore.bargain.swiftness.question": "¿puıɥǝq pןɹoʍ ʍoןs ǝɥʇ ǝʌɐǝן oʇ ɥsıʍ noʎ oᗡ", + "reflection.cosmiccore.bargain.violence.answer.accept.drawback.0": "sǝɔɹnos ןןɐ ɯoɹɟ uǝʞɐʇ ǝbɐɯɐp %0ᄅ+", + "reflection.cosmiccore.bargain.violence.answer.accept.drawback.1": "spןǝıɥs ǝsn ʇouuɐƆ", + "reflection.cosmiccore.bargain.violence.answer.accept.power.0": "ʇןɐǝp ǝbɐɯɐp ǝǝןǝɯ %0Ɛ+", + "reflection.cosmiccore.bargain.violence.answer.accept.power.1": "pǝǝds ʞɔɐʇʇɐ %ϛƖ+", + "reflection.cosmiccore.bargain.violence.answer.accept.response": "˙sɹnoʎ s,ʇI ˙ʇı ʇɥbıɟ ʇ,uoᗡ ¿ʎoɹʇsǝp oʇ ǝbɹn ǝɥ⟘ ¿ʍou ʇı ןǝǝℲ", + "reflection.cosmiccore.bargain.violence.answer.accept.text": "˙sʇuıɐɹʇsǝɹ ʎɯ ǝʌoɯǝᴚ", + "reflection.cosmiccore.bargain.violence.answer.refuse.response": "˙ǝןqɐɹopɐ ʍoH ˙ɟןǝsɹnoʎ uo ʇnd noʎ ɥsɐǝן Ɐ ˙ʇuıɐɹʇsǝᴚ", + "reflection.cosmiccore.bargain.violence.answer.refuse.text": "˙ɥʇbuǝɹʇs uʍo sʇı sı ʇuıɐɹʇsǝᴚ", + "reflection.cosmiccore.bargain.violence.description": "ǝןqıɹɹǝʇ buıɥʇǝɯos ɟo ǝɔɹoɟ ǝɥʇ ɥʇıʍ ǝʞıɹʇS", + "reflection.cosmiccore.bargain.violence.dialogue.0": "˙ʇuɐʇısǝH ˙pǝuıɐɹʇsǝɹ ˙˙˙os ǝɹɐ sʍoןq ɹnoʎ", + "reflection.cosmiccore.bargain.violence.dialogue.1": "˙ǝbɐɯɐp ǝɥʇ sɹɐǝɟ noʎ ɟo ʇɹɐd ǝɯoS ˙buıʍs ʎɹǝʌƎ ˙ʞɔɐq pןoɥ noʎ", + "reflection.cosmiccore.bargain.violence.dialogue.2": "˙ʎןǝǝɹɟ ʍoןɟ ǝɔuǝןoıʌ ɹnoʎ ʇǝꞀ ˙ʇuıɐɹʇsǝɹ ʇɐɥʇ ǝʌoɯǝɹ uɐɔ I", + "reflection.cosmiccore.bargain.violence.dialogue.3": "˙noʎ ǝɹoɟǝq ɹǝʇʇɐɥs ןןıʍ sǝıɯǝuǝ ɹnoʎ", + "reflection.cosmiccore.bargain.violence.dialogue.4": "˙sʎɐʍ ɥʇoq sʍoןɟ ʇɐɥʇ ɹǝʌıɹ ɐ sı ǝɔuǝןoıʌ ʇnᗺ", + "reflection.cosmiccore.bargain.violence.name": "ǝɔuǝןoıΛ", + "reflection.cosmiccore.bargain.violence.on_accept": "˙ʍou ǝןqɐʞɐǝɹq ˙˙˙os sʞooן buıɥʇʎɹǝʌƎ ˙sɯɹɐ ɹnoʎ ɥbnoɹɥʇ sǝbɹns ɹǝʍoԀ", + "reflection.cosmiccore.bargain.violence.on_defy": "˙ʇɥbıǝʍ ןɐʇɹoɯ oʇ uɹnʇǝɹ sʍoןq ɹnoʎ ˙ʎɐʍɐ suıɐɹp ǝbɐɹ ǝɥ⟘", + "reflection.cosmiccore.bargain.violence.question": "¿ǝɔuǝןoıʌ pǝuıɐɹʇsǝɹun 'ǝnɹʇ ǝɔɐɹqɯǝ noʎ ןןıM", + "reflection.cosmiccore.bargain.vitality.answer.accept.drawback.0": "ǝʇɐɹ uoıʇɐɹǝuǝbǝɹ ןɐɹnʇɐu %0ϛ-", + "reflection.cosmiccore.bargain.vitality.answer.accept.drawback.1": "ǝʌıʇɔǝɟɟǝ ssǝן %0Ɛ suoıʇod buıןɐǝH", + "reflection.cosmiccore.bargain.vitality.answer.accept.power.0": ")sʇɹɐǝɥ ɐɹʇxǝ ϛ( ɥʇןɐǝɥ xɐɯ 0Ɩ+", + "reflection.cosmiccore.bargain.vitality.answer.accept.power.1": "ɹǝɟɟnq uoıʇdɹosqɐ ǝbɐɯɐp pǝsɐǝɹɔuI", + "reflection.cosmiccore.bargain.vitality.answer.accept.response": "˙ʍou dɯnd oʇ ǝɹoɯ sɐɥ ʇI ˙ʎןןɐɹǝʇıꞀ ˙sןןǝʍs ʇɹɐǝɥ ɹnoʎ", + "reflection.cosmiccore.bargain.vitality.answer.accept.text": "˙puǝds oʇ ǝɟıן ǝɹoɯ ǝɯ ǝʌı⅁", + "reflection.cosmiccore.bargain.vitality.answer.refuse.response": "˙ʍou ɹoℲ ˙ǝɹɐ ʎןdɯıs noʎ ˙buıɥʇʎuɐ noʎ ǝʌɐb ǝuoʎuɐ ɟı sⱯ ˙uǝʌı⅁", + "reflection.cosmiccore.bargain.vitality.answer.refuse.text": "˙uǝʌıb sɐʍ I ʇɐɥʍ ɥʇıʍ ʞɹoʍ ןן,I", + "reflection.cosmiccore.bargain.vitality.description": "sʇıɯıן ןɐʇɹoɯ puoʎǝq ǝɔɹoɟ ǝɟıꞀ", + "reflection.cosmiccore.bargain.vitality.dialogue.0": "˙ǝɟıן ɟo ʇunoɯɐ pǝxıɟ Ɐ ˙sʇıɯıן sɐɥ ʎpoq ɹnoʎ", + "reflection.cosmiccore.bargain.vitality.dialogue.1": "˙ןɐuıℲ ˙ןɐʇnɹᗺ ˙ǝןdɯıS ˙ǝıp noʎ 'sǝıʇdɯǝ ʇı uǝɥM", + "reflection.cosmiccore.bargain.vitality.dialogue.2": "˙spunoq ןɐɹnʇɐu sʇı puoʎǝq ǝɔɹoɟ ǝɟıן ɹnoʎ ɥɔʇǝɹʇS ˙ǝɹoɯ noʎ ǝʌıb uɐɔ I", + "reflection.cosmiccore.bargain.vitality.dialogue.3": "˙puǝ ǝɥʇ ǝɹoɟǝq sʇɐǝqʇɹɐǝɥ ǝɹoW ˙ɥʇɐǝɹq ǝɹoW ˙pooןq ǝɹoW", + "reflection.cosmiccore.bargain.vitality.dialogue.4": "˙ɹǝʍoןs ˙˙˙ןɐǝɥ ןןıʍ noʎ ˙ʇɹoɟɟǝ sǝʞɐʇ ssǝɔxǝ buıuıɐʇuıɐɯ ʇnᗺ", + "reflection.cosmiccore.bargain.vitality.name": "ʎʇıןɐʇıΛ", + "reflection.cosmiccore.bargain.vitality.on_accept": "˙ʇuǝsǝɹd ˙˙˙ǝɹoɯ sןǝǝɟ buıɥʇʎɹǝʌƎ ˙ɹobıʌ ʍǝu ɥʇıʍ ǝbɹns suıǝʌ ɹnoʎ", + "reflection.cosmiccore.bargain.vitality.on_defy": "˙ǝɹoɯ ǝɔuo pǝzıs-ןɐʇɹoɯ ǝɹɐ noʎ ˙ʎɐʍɐ suıɐɹp ssǝɔxǝ ǝɥ⟘", + "reflection.cosmiccore.bargain.vitality.question": "¿ǝɔuǝıןısǝɹ ɹoɟ ʎɹǝʌoɔǝɹ ǝpɐɹʇ noʎ ןןıM", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.drawback.0": ")ssǝɔɔɐ ʎʞs( sɐǝɹɐ ʇıן uı ǝbɐɯɐp %ϛᄅ-", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.drawback.1": "ǝɹnsodxǝ ʇɥbıןuns ʇɔǝɹıp ɯoɹɟ ǝbɐɯɐp sǝʞɐ⟘", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.power.0": ")0 < ʎ( ʎʇıunɯɯı ǝbɐɯɐp pıoΛ", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.power.1": "pıoʌ buıɹǝʇuǝ uǝɥʍ ǝɔɐɟɹns oʇ ʇɹodǝןǝ⟘", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.response": "˙ʇı oʇ sbuoןǝq ʇɐɥʍ ɯɹɐɥ ʇou ןןıʍ ʇI ˙ʍou ǝɯɐu ɹnoʎ sʍouʞ pıoʌ ǝɥ⟘ ˙ǝuoᗡ", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.text": "˙sɹnoʎ ǝɯ ǝʞɐW ˙ǝɯ ʞɹɐW", + "reflection.cosmiccore.bargain.void_anchor.answer.refuse.response": "˙ʞɔnן poo⅁ ˙ʞɹɐp ssǝןpuǝ ǝɥʇ ʇsuıɐbɐ pןǝıɥs uıɥʇ ɐ ɥɔnS ˙sǝʎ ˙ʇɥbıן ǝɥ⟘", + "reflection.cosmiccore.bargain.void_anchor.answer.refuse.text": "˙noʎ ʞuɐɥʇ 'ʇɥbıן ǝɥʇ uı ʎɐʇs ןן,I", + "reflection.cosmiccore.bargain.void_anchor.description": "uʍo sʇı ʎpɐǝɹןɐ sı ʇɐɥʍ ɯıɐןɔ ʇouuɐɔ pıoʌ ǝɥ⟘", + "reflection.cosmiccore.bargain.void_anchor.dialogue.0": "˙pןɹoʍ ǝɥʇ ɥʇɐǝuǝq pıoʌ ǝɥʇ ɟo ןןnd ǝɥ⟘ ˙ʇı ʇןǝɟ ǝʌ,noʎ", + "reflection.cosmiccore.bargain.void_anchor.dialogue.1": "˙buıɥʇʎɹǝʌǝ sʍoןןɐʍs ʇɐɥʇ ssǝuʞɹɐp ןɐuıɟ ʇɐɥ⟘ ˙ןןɐɟ ssǝןpuǝ ʇɐɥ⟘", + "reflection.cosmiccore.bargain.void_anchor.dialogue.2": "˙buıɥʇou puɐ ǝɔuǝʇsıxǝ uǝǝʍʇǝq ǝɔɐds ʇɐɥʇ uI ˙ǝɹǝɥʇ ןןǝʍp I", + "reflection.cosmiccore.bargain.void_anchor.dialogue.3": "˙ʎoɹʇsǝp ʇouuɐɔ pıoʌ ǝɥʇ 'ǝuıɯ sı ʇɐɥʍ puⱯ ˙ǝuıɯ noʎ ǝʞɐW ˙noʎ ʞɹɐɯ uɐɔ I", + "reflection.cosmiccore.bargain.void_anchor.dialogue.4": "˙noʎ ǝɯoɔןǝM ˙noʎ ǝzıuboɔǝɹ ןןıʍ ssǝuʞɹɐp ǝɥ⟘ ˙ǝʞıן noʎ sɐ ɹɐɟ sɐ ןןɐℲ", + "reflection.cosmiccore.bargain.void_anchor.dialogue.5": "˙noʎ sǝǝs ǝɔuǝʇsıxǝ ʍoɥ sǝbuɐɥɔ ʇı ˙˙˙pıoʌ ǝɥʇ ʎq pǝʞɹɐɯ buıǝq ʇnᗺ", + "reflection.cosmiccore.bargain.void_anchor.name": "ɹoɥɔuⱯ pıoΛ", + "reflection.cosmiccore.bargain.void_anchor.on_accept": "˙ʍou noʎ sʍouʞ pıoʌ ǝɥ⟘ ˙ʇı sʞɹɐW ˙ןnos ɹnoʎ sǝɥɔnoʇ pןoɔ buıɥʇǝɯoS", + "reflection.cosmiccore.bargain.void_anchor.on_defy": "˙ǝɯıʇ ʇxǝu ןnɟıɔɹǝɯ ǝq ʇou ןןıʍ ʇI ˙noʎ sʇǝbɹoɟ pıoʌ ǝɥ⟘ ˙ʎɐʍɐ suɹnq ʞɹɐɯ ǝɥ⟘", + "reflection.cosmiccore.bargain.void_anchor.question": "¿buıɥʇou ǝɥʇ uı ɹoɥɔuɐ uɐ ǝɯoɔǝq noʎ ןןıM", + "reflection.cosmiccore.threshold.0.dialogue.0": "˙uǝʌıb ʎןǝǝɹɟ ʇ,usɐʍ ʇɐɥʇ buıɥʇǝɯos ʞooʇ noʎ", + "reflection.cosmiccore.threshold.0.dialogue.1": "¿uı buıpooןɟ ɹǝʍod ǝɥ⟘ ¿poob ןǝǝɟ ʇı pıᗡ", + "reflection.cosmiccore.threshold.0.dialogue.2": "˙ʇuıod ǝɥʇ s,ʇɐɥ⟘ ˙pıp ʇı ǝsɹnoɔ ɟO", + "reflection.cosmiccore.threshold.0.dialogue.3": "˙ɹǝɥʇǝboʇ ssǝuısnq ǝʌɐɥ ǝM ˙ʍou buıɥɔʇɐʍ ǝq ןן,I", + "reflection.cosmiccore.threshold.0.question": "¿pǝʇɹɐʇs ǝʌ,noʎ ʇɐɥʍ puɐʇsɹǝpun noʎ oᗡ", + "reflection.cosmiccore.threshold.0.response": "˙ʍou ɹǝʇʇɐɯ ʇ,usǝop ʇI ˙ʇou ɹO ˙poo⅁", + "reflection.cosmiccore.threshold.1.dialogue.0": "˙pǝsıɹdɹns ʇou ɯ,I ˙ǝɹoɯ ɹoɟ ʞɔɐq ʎpɐǝɹןⱯ", + "reflection.cosmiccore.threshold.1.dialogue.1": "¿ʇı ןǝǝɟ noʎ uɐƆ ˙ɹǝuuıɥʇ sǝɥɔʇǝɹʇs ןnos ɹnoʎ", + "reflection.cosmiccore.threshold.1.dialogue.2": "˙buıpɐɟ ʎɹoɯǝɯ ɐ ǝʞıꞀ ˙ʇsıɯ ǝʞıꞀ ˙ʎɟɟɐʇ ǝʞıꞀ", + "reflection.cosmiccore.threshold.1.dialogue.3": "˙ʍou ɹoℲ ˙ʇɟǝן ʎʇuǝןd ǝʌɐɥ noʎ ˙ʎɹɹoʍ ʇ,uoᗡ", + "reflection.cosmiccore.threshold.1.question": "¿ǝןqɐʇɹoɟɯoɔ ןןıʇS", + "reflection.cosmiccore.threshold.1.response": "˙ʎɐʍʎuɐ pǝʇɐɹɹǝʌo sı ʇɹoɟɯoƆ", + "reflection.cosmiccore.threshold.2.dialogue.0": "˙ʍou ǝɯ oʇ sbuoןǝq noʎ ɟo pɹıɥʇ Ɐ", + "reflection.cosmiccore.threshold.2.dialogue.1": "˙ʇı ǝʇsɐ⟘ ˙ʇı ǝǝs uɐɔ I ˙ɹoɥdɐʇǝɯ ʇou s,ʇɐɥ⟘", + "reflection.cosmiccore.threshold.2.dialogue.2": "˙suǝʇɟos uoıʇıuıɟǝp ɹnoʎ ˙ɹnןq sǝbpǝ ɹnoʎ", + "reflection.cosmiccore.threshold.2.dialogue.3": "˙uoos ǝɔıʇou oʇ ʇɹɐʇs ʇɥbıɯ sɹǝɥʇO", + "reflection.cosmiccore.threshold.2.question": "¿sʇɥbnoɥʇ puoɔǝs buıʌɐH", + "reflection.cosmiccore.threshold.2.response": "˙ǝuo pɐɥ ɹǝʌǝu noʎ ˙ʇsɹıɟ ɐ ǝɹınbǝɹ sʇɥbnoɥʇ puoɔǝS", + "reflection.cosmiccore.threshold.3.dialogue.0": "¿ʎǝɥʇ ʇ,uǝɹɐ 'buıʇɹɐʇs ǝɹɐ sɯɐǝɹp ǝɥ⟘", + "reflection.cosmiccore.threshold.3.dialogue.1": "˙puɐן ɹǝʌǝu puɐ ןןɐɟ noʎ ǝɹǝɥʍ sǝuo ǝɥ⟘", + "reflection.cosmiccore.threshold.3.dialogue.2": "˙ʞɔɐq sʞooן ǝsןǝ buıɥʇǝɯos puɐ ɹoɹɹıɯ ɐ uı ʞooן noʎ ǝɹǝɥM", + "reflection.cosmiccore.threshold.3.dialogue.3": "˙ʎɔǝɥdoɹd s,ʇɐɥ⟘ ˙ɯɐǝɹp ɐ ʇou s,ʇɐɥ⟘", + "reflection.cosmiccore.threshold.3.question": "¿ǝɹɐ noʎ oɥʍ ʍouʞ ןןıʇs noʎ oᗡ", + "reflection.cosmiccore.threshold.3.response": "˙buıɥʇǝɯos suɐǝɯ ǝɯɐu ʇɐɥʇ ɟןǝsɹnoʎ buıןןǝʇ dǝǝʞ", + "reflection.cosmiccore.threshold.4.dialogue.0": "˙sǝɥɔɐoɹddɐ uɹnʇǝɹ ou ɟo ʇuıod ǝɥ⟘ ˙ʎɐʍɟןɐH", + "reflection.cosmiccore.threshold.4.dialogue.1": "˙sʇǝʞuıɹʇ ɹoɟ ʎɐʍɐ uǝʌı⅁ ˙ǝuob 'noʎ ɟo ɟןɐH", + "reflection.cosmiccore.threshold.4.dialogue.2": "˙ǝɹɐɔ ʇ,uop I ˙ɹǝʍsuɐ ʇ,uoᗡ ¿ʇı ɥʇɹoʍ ʇı sɐM", + "reflection.cosmiccore.threshold.4.dialogue.3": "˙ʇxǝu sǝɯoɔ ʇɐɥʍ sı sɹǝʇʇɐɯ ʇɐɥM", + "reflection.cosmiccore.threshold.4.question": "¿ǝpıs ɹǝɥʇo ǝɥʇ uo s,ʇɐɥʍ ǝǝs oʇ ʎpɐǝᴚ", + "reflection.cosmiccore.threshold.4.response": "˙ʎɐʍʎuɐ ssoɹɔ ʎǝɥʇ ʇnᗺ ˙sı ɹǝʌǝ ǝuo oN", + "reflection.cosmiccore.threshold.5.dialogue.0": "˙ʍou sɹnoʎ uɐɥʇ ǝuıɯ sı noʎ ɟo ǝɹoW", + "reflection.cosmiccore.threshold.5.dialogue.1": "˙pןnoɥs ʇI ¿noʎ uǝʇɥbıɹɟ ʇɐɥʇ sǝoᗡ", + "reflection.cosmiccore.threshold.5.dialogue.2": "˙ʇǝʎ pɐɥ ʇ,uǝʌɐɥ noʎ sʇɥbnoɥʇ ʍouʞ I", + "reflection.cosmiccore.threshold.5.dialogue.3": "˙uǝʇʇobɹoɟ ǝʌ,noʎ sbuıןǝǝɟ ןǝǝɟ I", + "reflection.cosmiccore.threshold.5.question": "¿ןoɹʇuoɔ uı ʎןןɐǝɹ s,oɥM", + "reflection.cosmiccore.threshold.5.response": "˙ǝɔıoɥɔ ǝʌɐɥ ןןıʇs noʎ buıpuǝʇǝɹd dǝǝʞ", + "reflection.cosmiccore.threshold.6.dialogue.0": "˙ǝɹoɯʎuɐ ɥɔʇɐɯ ǝʇınb ʇ,usǝop uoıʇɔǝןɟǝɹ ɹnoʎ", + "reflection.cosmiccore.threshold.6.dialogue.1": "˙ǝɔıʇou ʇou ʇɥbıɯ sɹǝɥʇO ˙ʇɥbıןs sı ʎɐןǝp ǝɥ⟘", + "reflection.cosmiccore.threshold.6.dialogue.2": "˙ʇı ןǝǝɟ noʎ ˙ʍouʞ noʎ ʇnᗺ", + "reflection.cosmiccore.threshold.6.question": "¿sɹoɹɹıɯ uı noʎ ʇɐ ʞɔɐq sǝɹɐʇs ʇɐɥM", + "reflection.cosmiccore.threshold.6.response": "˙ʍou ǝɯ sʎɐʍןⱯ ˙ǝW", + "reflection.cosmiccore.threshold.7.dialogue.0": "˙ǝɹǝʍ noʎ ʇɐɥʍ ɟo ʇɟǝן ǝןʇʇıן oS", + "reflection.cosmiccore.threshold.7.dialogue.1": "˙uoıʇuǝʇuı ɟo sʍopɐɥS ˙sǝoɥɔƎ ˙sʇuǝɯbɐɹℲ", + "reflection.cosmiccore.threshold.7.dialogue.2": "˙sǝʇɐןnɔןɐɔ puıɯ ǝɥ⟘ ˙sʞןɐʍ ʎpoq ǝɥ⟘", + "reflection.cosmiccore.threshold.7.dialogue.3": "˙ʇuǝds ʇsoɯןɐ sı ןnos ǝɥ⟘ ¿ןnos ǝɥʇ ʇnᗺ", + "reflection.cosmiccore.threshold.7.question": "¿ǝɔɐɟ s,ɹǝɥʇoɯ ɹnoʎ ɹǝqɯǝɯǝɹ noʎ uɐƆ", + "reflection.cosmiccore.threshold.7.response": "˙ʎpɐǝɹןɐ ʇɐɥʇ ʞooʇ I ˙ʇ,uɐɔ noʎ ˙oN", + "reflection.cosmiccore.threshold.8.dialogue.0": "˙ʍou ǝbpǝ ǝɥʇ ɯoɹɟ dǝʇs ǝuO", + "reflection.cosmiccore.threshold.8.dialogue.1": "˙pɐǝɹɥʇ Ɐ ˙ɹǝʌıןs Ɐ ˙ʇuǝɔɹǝd uǝ⟘", + "reflection.cosmiccore.threshold.8.dialogue.2": "˙ǝɯ ˙˙˙ɯoɹɟ noʎ sǝʇɐɹɐdǝs ʇɐɥʇ ןןɐ s,ʇɐɥ⟘", + "reflection.cosmiccore.threshold.8.dialogue.3": "˙ǝɹoɯ ǝuo ʇsnſ ˙uıɐbɹɐq ǝɹoɯ ǝuO", + "reflection.cosmiccore.threshold.8.question": "¿dǝʇs ןɐuıɟ ʇɐɥʇ ǝʞɐʇ noʎ ןןıM", + "reflection.cosmiccore.threshold.8.response": "˙uǝɥʍ sı uoıʇsǝnb ǝɥ⟘ ˙ןןıʍ noʎ ʍouʞ ɥʇoq ǝM", + "reflection.cosmiccore.threshold.9.dialogue.0": "˙ʎןןɐuıℲ", + "reflection.cosmiccore.threshold.9.dialogue.1": "˙ǝɔǝıd ʇsɐן ʎɹǝʌƎ ˙buıɥʇʎɹǝʌǝ ǝʌɐb noʎ", + "reflection.cosmiccore.threshold.9.dialogue.2": "˙ǝɹǝɥ uı pǝʞןɐʍ ʇɐɥʍ ɟo ʇɟǝן buıɥʇou s,ǝɹǝɥ⟘", + "reflection.cosmiccore.threshold.9.dialogue.3": "˙ǝɯ ʎןuO ˙ɹǝbunɥ ʎןuO ˙ɹǝʍod ʎןuO", + "reflection.cosmiccore.threshold.9.question": "¿ɟןǝsɹnoʎ ʇɐ ʞooן noʎ uǝɥʍ ǝǝs noʎ op ʇɐɥM", + "reflection.cosmiccore.threshold.9.response": "˙ǝǝs oʇ ʇɟǝן buıɥʇou s,ǝɹǝɥʇ ǝsnɐɔǝᗺ ˙buıɥʇoN", + "reflection.cosmiccore.ui.acknowledge": "]puɐʇsɹǝpun I[", + "reflection.cosmiccore.ui.available_bargains": "suıɐbɹɐᗺ ǝןqɐןıɐʌⱯ", + "reflection.cosmiccore.ui.back": "]ʞɔɐᗺ[", + "reflection.cosmiccore.ui.browse.interesting_choice": "˙noʎ ʍoɥs ǝɯ ʇǝꞀ ˙ǝɔıoɥɔ buıʇsǝɹǝʇuı uⱯ", + "reflection.cosmiccore.ui.browse_bargains": "]suıɐbɹɐq ǝןqɐןıɐʌɐ %s ǝsʍoɹᗺ[", + "reflection.cosmiccore.ui.cancel": "]ןǝɔuɐƆ[", + "reflection.cosmiccore.ui.click_to_bargain": "uıɐbɹɐq oʇ ʞɔıןƆ", + "reflection.cosmiccore.ui.click_to_defy": ")uoısoɹǝ %d( ʎɟǝp oʇ ʞɔıןƆ", + "reflection.cosmiccore.ui.confirm_defiance": "]ǝɔuɐıɟǝᗡ ɯɹıɟuoƆ[", + "reflection.cosmiccore.ui.constellation_title": "suıɐbɹɐᗺ ɟo uoıʇɐןןǝʇsuoƆ ǝɥ⟘", + "reflection.cosmiccore.ui.continue": "]ǝnuıʇuoƆ[", + "reflection.cosmiccore.ui.cost": "uoısoɹǝ %d :ʇsoƆ", + "reflection.cosmiccore.ui.defiance": "ǝɔuɐıɟǝᗡ", + "reflection.cosmiccore.ui.defiance.cancel": "]puıɯ ʎɯ pǝbuɐɥɔ ǝʌ,I 'oN[", + "reflection.cosmiccore.ui.defiance.cannot_undo": "ǝuopun ǝq ʇouuɐɔ sıɥ⟘", + "reflection.cosmiccore.ui.defiance.confirm": "]uıɐbɹɐq sıɥʇ ʎɟǝp I 'sǝʎ[", + "reflection.cosmiccore.ui.defiance.cost_amount": "uoısoɹǝ %d noʎ ʇsoɔ ןןıʍ sıɥ⟘", + "reflection.cosmiccore.ui.defiance.lose_power": "uıɐbɹɐq sıɥʇ ɯoɹɟ sɹǝʍod ןןɐ ǝsoן ןןıʍ noʎ", + "reflection.cosmiccore.ui.defiance.question": "¿uıɐbɹɐq sıɥʇ ʞɐǝɹq oʇ ɥsıʍ noʎ", + "reflection.cosmiccore.ui.defiance.scar_remains": "ɹǝʌǝɹoɟ ןnos ɹnoʎ uo uıɐɯǝɹ ןןıʍ ɹɐɔs Ɐ", + "reflection.cosmiccore.ui.defiance.so_be_it": "˙uoıʇɐɯɐןɔǝɹ ɟo uıɐd ǝɥʇ ןǝǝℲ ˙ʇı ǝq oS", + "reflection.cosmiccore.ui.defiance.warning1": "¿%s ɟo uıɐbɹɐq ǝɥʇ ʞɐǝɹq pןnoʍ noʎ", + "reflection.cosmiccore.ui.defiance.warning2": "˙uoısoɹǝ %d sı ǝɔuɐıɟǝp ɟo ʇsoɔ ǝɥ⟘", + "reflection.cosmiccore.ui.defiance.warning3": "˙ʇou ןןıʍ ɹɐɔs ǝɥ⟘ ˙noʎ ǝʌɐǝן ןןıʍ ɹǝʍod ǝɥ⟘", + "reflection.cosmiccore.ui.defiance.warning4": "¿uıɐʇɹǝɔ noʎ ǝɹⱯ", + "reflection.cosmiccore.ui.defiance.will_lose": "%s :ǝsoן ןןıʍ noʎ", + "reflection.cosmiccore.ui.defiance.wise": "˙sǝןdıɔuıɹd ɹnoʎ uɐɥʇ ǝɹoɯ ɥʇɹoʍ sı ɹǝʍod ǝɥ⟘ ˙ǝsıM", + "reflection.cosmiccore.ui.defiance_cost": "uoısoɹǝ %d ʇsoɔ ןןıʍ ǝɔuɐıɟǝᗡ", + "reflection.cosmiccore.ui.defiance_warning": "˙ןnos ɹnoʎ ɟo ǝɯos ǝɹoʇsǝɹ ʇnq ɹǝʍod noʎ ʇsoɔ ןןıʍ uıɐbɹɐq ɐ buıʎɟǝᗡ", + "reflection.cosmiccore.ui.defy": "ʎɟǝᗡ", + "reflection.cosmiccore.ui.defy_bargain": "]uıɐbɹɐᗺ sıɥ⟘ ʎɟǝᗡ[", + "reflection.cosmiccore.ui.dialogue_continue": "˙˙˙ǝnuıʇuoɔ oʇ ʞɔıןƆ", + "reflection.cosmiccore.ui.drawback": "ʞɔɐqʍɐɹᗡ", + "reflection.cosmiccore.ui.drawbacks": ":sʞɔɐqʍɐɹᗡ", + "reflection.cosmiccore.ui.enter_defiance": "]ǝpoW ǝɔuɐıɟǝᗡ ɹǝʇuƎ[", + "reflection.cosmiccore.ui.erosion": "uoısoɹǝ", + "reflection.cosmiccore.ui.exit": "]ǝʌɐǝꞀ[", + "reflection.cosmiccore.ui.forever_scarred": "pǝɹɹɐɔS ɹǝʌǝɹoℲ", + "reflection.cosmiccore.ui.gaze_constellation": "]uoıʇɐןןǝʇsuoɔ ǝɥʇ uodn ǝzɐ⅁[", + "reflection.cosmiccore.ui.hub.browse.power": "sɹǝɟɟo pıoʌ ǝɥʇ ʇɐɥʍ ǝǝS", + "reflection.cosmiccore.ui.hub.browse.response": "˙ןnos ǝןʇʇıן os ˙˙˙sǝɔıoɥɔ ʎuɐɯ oS", + "reflection.cosmiccore.ui.hub.browse.response_empty": "˙ʇǝʎ ˙noʎ ɹoɟ buıɥʇoN", + "reflection.cosmiccore.ui.hub.greeting.erosion_no_bargains.0": "˙snoıɹnƆ ¿suıɐbɹɐq ʇnoɥʇıʍ uoısoɹƎ", + "reflection.cosmiccore.ui.hub.greeting.erosion_no_bargains.1": "˙noʎ ɯoɹɟ buıʞɐʇ uǝǝq sɐɥ ǝsןǝ buıɥʇǝɯoS", + "reflection.cosmiccore.ui.hub.greeting.erosion_no_bargains.2": "˙pɐǝʇsuı dןǝɥ ǝɯ ʇǝן pןnoɥs noʎ sdɐɥɹǝԀ", + "reflection.cosmiccore.ui.hub.greeting.fresh.0": "˙ǝɹɐɹ ʍoH ˙ןnos ǝuıʇsıɹd Ɐ", + "reflection.cosmiccore.ui.hub.greeting.fresh.1": "˙ʇsɐן ʇ,uoʍ ʇɐɥ⟘ ˙ʎɹɹoʍ ʇ,uoᗡ", + "reflection.cosmiccore.ui.hub.greeting.has_bargains.0": "¿ǝɹoɯ ɹoɟ ʎɹbunH ˙uɹnʇǝɹ noʎ 'ɥⱯ", + "reflection.cosmiccore.ui.hub.greeting.has_bargains.1": "˙ɹǝɟɟo oʇ ʇɟǝן ʎʇuǝןd ǝʌɐɥ I", + "reflection.cosmiccore.ui.hub.greeting.has_scars.0": "˙ǝɔuɐıɟǝp ɟo sɹɐɔs ǝɥʇ ǝǝs I", + "reflection.cosmiccore.ui.hub.greeting.has_scars.1": "˙ʎɐʍɐ ʇı ʍǝɹɥʇ uǝɥʇ 'ɹǝʍod ʞooʇ noʎ", + "reflection.cosmiccore.ui.hub.greeting.has_scars.2": "¿ɥbıɥ ooʇ ʇı buıdǝǝʞ ɟo ʇsoɔ ǝɥʇ sɐM", + "reflection.cosmiccore.ui.hub.greeting.many_bargains.0": "˙ǝɹɐ noʎ ǝsɹnoɔ ɟO ˙uıɐbɐ ʞɔɐᗺ", + "reflection.cosmiccore.ui.hub.greeting.many_bargains.1": "˙ǝɯıʇ ɥɔɐǝ ɹǝuuıɥʇ sʍoɹb ןnos ɹnoʎ", + "reflection.cosmiccore.ui.hub.greeting.many_bargains_high.0": "˙ʎɐʍɐ uǝʌıb ɥɔnɯ oS ˙noʎ ʇɐ ʞooꞀ", + "reflection.cosmiccore.ui.hub.greeting.many_bargains_high.1": "¿ǝɹǝʍ noʎ ʇɐɥʍ ɹǝqɯǝɯǝɹ uǝʌǝ noʎ oᗡ", + "reflection.cosmiccore.ui.hub.greeting.question": "¿uıɐɯop ʎɯ oʇ noʎ sbuıɹq ʇɐɥM", + "reflection.cosmiccore.ui.hub.leave_response": "˙ǝןqɐʇɔıpǝɹd ʍoH ¿ʎɐʍɐ buıuunᴚ", + "reflection.cosmiccore.ui.hub.reflect.power": "ǝɔuǝʇsıxǝ ɹnoʎ ǝʇɐןdɯǝʇuoƆ", + "reflection.cosmiccore.ui.hub.reflect_response": "¿ǝʍ ǝɹɐ 'ssʎqɐ ǝɥʇ oʇuı buızɐ⅁", + "reflection.cosmiccore.ui.hub.review.drawback": "uıɐbɹɐq ɐ buıʎɟǝp ɹǝpısuoƆ", + "reflection.cosmiccore.ui.hub.review.power": "ʎɐʍɐ uǝʌıb ǝʌ,noʎ ʇɐɥʍ ǝǝS", + "reflection.cosmiccore.ui.hub.review_response": "˙ǝɯoɔǝq ǝʌ,noʎ ʇɐɥʍ ǝǝs s,ʇǝꞀ", + "reflection.cosmiccore.ui.just_look": "]ɟןǝsɹnoʎ ʇɐ ʞooן ˙˙˙ʇsnſ[", + "reflection.cosmiccore.ui.leave": "]ǝɔɐןd sıɥʇ ǝʌɐǝꞀ[", + "reflection.cosmiccore.ui.no_available_bargains": "˙ʍou ɹoɟ ˙˙˙noʎ ɹǝɟɟo oʇ buıɥʇou sɐɥ pıoʌ ǝɥ⟘", + "reflection.cosmiccore.ui.no_bargains": "˙ʇǝʎ pǝʇdǝɔɔɐ suıɐbɹɐq oN", + "reflection.cosmiccore.ui.of": "ɟo", + "reflection.cosmiccore.ui.power": "ɹǝʍoԀ", + "reflection.cosmiccore.ui.powers": ":sɹǝʍoԀ", + "reflection.cosmiccore.ui.reflection.extreme_erosion.0": "˙ʇsoɯןⱯ ˙ʇɟǝן buıɥʇou ʇsoɯןⱯ", + "reflection.cosmiccore.ui.reflection.extreme_erosion.1": "˙ʎןǝʇǝןdɯoɔ ǝuıɯ ǝɹ,noʎ puɐ ɥsnd ǝɹoɯ ǝuO", + "reflection.cosmiccore.ui.reflection.has_bargains.0": "˙sʇuǝɯǝbuɐɹɹɐ ˙˙˙ǝɯos ǝpɐɯ ǝʌ,noʎ ǝǝs I", + "reflection.cosmiccore.ui.reflection.has_bargains.1": "˙ʎɐʍɐ uǝʌıb noʎ ɟo ǝɔǝıd ɐ ǝuo ɥɔɐƎ", + "reflection.cosmiccore.ui.reflection.high_erosion.0": "˙ʍou ǝuob sı noʎ ɟo ɥɔnɯ oS", + "reflection.cosmiccore.ui.reflection.high_erosion.1": "¿ǝɹǝʍ noʎ ʇɐɥʍ ɹǝqɯǝɯǝɹ noʎ oᗡ", + "reflection.cosmiccore.ui.reflection.low_erosion.0": "˙sʇɹɐʇs ʇı ʍoɥ s,ʇɐɥ⟘ ˙ǝʇsɐʇ ɐ ʇsnſ", + "reflection.cosmiccore.ui.reflection.low_erosion.1": "˙ǝɹoɯ ɹoɟ ʞɔɐq ǝq ןן,noʎ", + "reflection.cosmiccore.ui.reflection.mid_erosion.0": "¿ssǝuʞɹɐp ǝɥʇ ɥʇıʍ ǝןqɐʇɹoɟɯoɔ buıʇʇǝ⅁", + "reflection.cosmiccore.ui.reflection.mid_erosion.1": "˙noʎ oʇuı buıןʇʇǝs ʇı ǝǝs uɐɔ I", + "reflection.cosmiccore.ui.reflection.no_erosion.0": "˙buıɹoq ʍoH ˙ǝɹnԀ ˙pǝɥɔnoʇu∩", + "reflection.cosmiccore.ui.reflection.no_erosion.1": "¿ʍoɥs oʇ buıɥʇou ɥʇıʍ ǝɹǝɥ ǝɯoɔ noʎ", + "reflection.cosmiccore.ui.reflection.no_erosion.2": "˙ǝbuɐɥɔ sʎɐʍןɐ ʎǝɥ⟘ ˙ǝbuɐɥɔ ןןıʍ ʇɐɥ⟘", + "reflection.cosmiccore.ui.review_bargains": "]suıɐbɹɐq %s ɹnoʎ ʍǝıʌǝᴚ[", + "reflection.cosmiccore.ui.scroll_down": "uʍop ןןoɹɔS ▼", + "reflection.cosmiccore.ui.scroll_up": "dn ןןoɹɔS ▲", + "reflection.cosmiccore.ui.select": "]ʇɔǝןǝS[", + "reflection.cosmiccore.ui.select_to_view": "sןıɐʇǝp ʍǝıʌ oʇ uıɐbɹɐq ɐ ʇɔǝןǝS", + "reflection.cosmiccore.ui.soul_erosion": "%d%% :uoısoɹƎ ןnoS", + "reflection.cosmiccore.ui.soul_erosion_display": "%s%% :uoısoɹƎ ןnoS", + "reflection.cosmiccore.ui.soul_label": "ןnoS", + "reflection.cosmiccore.ui.tooltip.no_details": "sןıɐʇǝp ןɐuoıʇıppɐ oN", + "reflection.cosmiccore.ui.unlock_cost": "uoısoɹǝ ןnos %d :ʇsoƆ", + "reflection.cosmiccore.ui.view_active": "]suıɐbɹɐᗺ ɹnoʎ ʍǝıΛ[", + "reflection.cosmiccore.ui.view_bargains": "]suıɐbɹɐᗺ ǝןqɐןıɐʌⱯ ʍǝıΛ[", + "reflection.cosmiccore.ui.void_title": "uǝǝʍʇǝᗺ pıoΛ ǝɥ⟘", + "reflection.cosmiccore.ui.your_bargains": "suıɐbɹɐᗺ ɹnoʎ", "tagprefix.alve_foil_insulator": "ɹoʇɐןnsuI ǝʌןⱯ %s", "tagprefix.heavy_beam": "ɯɐǝᗺ %s ʎʌɐǝH", "tagprefix.leached_ore": "ǝɹO %s pǝɥɔɐǝꞀ", diff --git a/src/generated/resources/assets/cosmiccore/lang/en_us.json b/src/generated/resources/assets/cosmiccore/lang/en_us.json index 357f252f1..92aea7773 100644 --- a/src/generated/resources/assets/cosmiccore/lang/en_us.json +++ b/src/generated/resources/assets/cosmiccore/lang/en_us.json @@ -68,6 +68,7 @@ "block.cosmiccore.dawnforge_eclipsed": "Dawnforge [Eclipsed]", "block.cosmiccore.dimensional_energy_capacitor": "Power Substation", "block.cosmiccore.dimensional_energy_interface": "Power Substation Dimensional Interface", + "block.cosmiccore.dreamers_basin": "Dreamer's Basin", "block.cosmiccore.drone_maintenance_interface": "Drone Maintenance Interface", "block.cosmiccore.drone_station": "Drone Station", "block.cosmiccore.drygmy_grove": "Drygmy Grove", @@ -285,6 +286,7 @@ "block.cosmiccore.steel_rose_light_stairs": "Steel Rose Light Stairs", "block.cosmiccore.stellar_iris": "Stellar Iris", "block.cosmiccore.stellar_neutronium_grade_magnet": "Stellar Neutronium Grade Magnet", + "block.cosmiccore.stellar_smelting_module": "Godsbane Hyper-tensor Platform", "block.cosmiccore.sterilization_hatch": "Sterilzation Hatch", "block.cosmiccore.submerged_welder": "§3Submerged Welder", "block.cosmiccore.suffering_chamber": "§cSuffering Chamber", @@ -463,6 +465,7 @@ "config.jade.plugin_cosmiccore.drone_maintenance_interface": "[CC] Drone Maintenance Interface", "config.jade.plugin_cosmiccore.drone_station": "[CC] Drone Station", "config.jade.plugin_cosmiccore.parallel_info_cc": "[CC] Parallel Info", + "config.jade.plugin_cosmiccore.stellar_module": "[CC] Stellar Module", "cosmic.command.wireless.energy.active": " §bActive:§b %s", "cosmic.command.wireless.energy.buffered": " §bBuffered:§b %s EU", "cosmic.command.wireless.energy.capacitor": " §bCapacitor Location:§b ", @@ -528,6 +531,8 @@ "cosmiccore.ember.capacity": "§cEmber Capacity:§6 %s", "cosmiccore.ember.transfer": "§cEmber Transfer Rate:§6 %s", "cosmiccore.errors.bad_fuel": "§aInsufficient Fuel Quality! \n Fuel Output Must be >720 EU total per unit", + "cosmiccore.gui.stellar.show_modules": "Show Module Control", + "cosmiccore.gui.stellar.show_star": "Show Star View", "cosmiccore.item.linked_terminal.boundTo": "Bound to %s", "cosmiccore.item.spraycan.tooltip.current_color": "Current Color: %s", "cosmiccore.item.spraycan.tooltip.lclick": "§4Left Click: §8Cycle color", @@ -537,6 +542,13 @@ "cosmiccore.item.spraycan.tooltip.rclick_offhand": "§5Right Click in Offhand: §8Place & paint", "cosmiccore.item.spraycan.tooltip.rclick_sneak": "§5Right Click + Sneak: §8Open UI", "cosmiccore.item.spraycan.tooltip.solvent_mode": "Spraycan in SOLVENT mode", + "cosmiccore.jade.stellar_module.connected": "Iris: Connected", + "cosmiccore.jade.stellar_module.energy_usage": "Usage: %s", + "cosmiccore.jade.stellar_module.iris_not_ready": "Iris: Not Ready", + "cosmiccore.jade.stellar_module.no_wireless": "No Wireless Network", + "cosmiccore.jade.stellar_module.not_connected": "Iris: Not Connected", + "cosmiccore.jade.stellar_module.speed_bonus": "Speed: %s", + "cosmiccore.jade.stellar_module.stage": "Stage: %s", "cosmiccore.khoruth.1": "§6Khor - Space", "cosmiccore.khoruth.2": "§6Ruth - Foundation", "cosmiccore.lore.broken_virtue.0": "Perpetuity Shudders Softly", @@ -552,6 +564,23 @@ "cosmiccore.machine.capacitor_array.tooltip.0": "§7Local Dense Power Storage§r", "cosmiccore.machine.capacitor_array.tooltip.1": "§7Can use any capacitor and be expanded vertically up to 18 times§r", "cosmiccore.machine.capacitor_array.tooltip.2": "§7Accepts §6Laser Hatches§r", + "cosmiccore.machine.dreamers_basin.eu_budget_header": "Energy Budget", + "cosmiccore.machine.dreamers_basin.eu_per_thread": "%s EU/t per thread (%s)", + "cosmiccore.machine.dreamers_basin.status_idle": "Idle - No recipe", + "cosmiccore.machine.dreamers_basin.status_suspended": "Suspended", + "cosmiccore.machine.dreamers_basin.status_unknown": "Unknown", + "cosmiccore.machine.dreamers_basin.status_waiting": "Waiting for inputs", + "cosmiccore.machine.dreamers_basin.thread_header": "Thread Status", + "cosmiccore.machine.dreamers_basin.threads_summary": "%s running / %s active / %s max", + "cosmiccore.machine.dreamers_basin.time_remaining": "Time: %s remaining", + "cosmiccore.machine.dreamers_basin.tooltip.0": "§bRuns multiple unique recipes simultaneously", + "cosmiccore.machine.dreamers_basin.tooltip.1": "§fEach thread requires a uniquely §6colored§f input bus/hatch", + "cosmiccore.machine.dreamers_basin.tooltip.2": "§fMax threads = Energy Hatch amperage (4A=4, 16A=16)", + "cosmiccore.machine.dreamers_basin.tooltip.3": "§aAll threads share output buses/hatches", + "cosmiccore.machine.dreamers_basin.tooltip.crafting": "Crafting:", + "cosmiccore.machine.dreamers_basin.tooltip.duration": "Recipe duration: %s", + "cosmiccore.machine.dreamers_basin.tooltip.no_recipe": "No recipe data", + "cosmiccore.machine.dreamers_basin.tooltip.processing": " Processing...", "cosmiccore.machine.fluid_drilling_rig.depletion": "§bDepletion Rate: 0%", "cosmiccore.machine.fluid_drilling_rig.description.0": "§bDrills infinite fluid from", "cosmiccore.machine.fluid_drilling_rig.description.1": "§bliquid pockets suspended throughout the void.", @@ -563,6 +592,9 @@ "cosmiccore.machine.me.stocking_item.tooltip.4": "§fFilter data can be copy/pasted with a data stick§r", "cosmiccore.machine.me.stocking_item.tooltip.5": "§b'If you're wondering how to parallel assembly lines§r", "cosmiccore.machine.me.stocking_item.tooltip.6": "§fthis is how. Welcome to subnets!§r", + "cosmiccore.machine.multithreaded.active_threads": "§7Active: §a%s§7/§f%s", + "cosmiccore.machine.multithreaded.max_threads": "§7Max Threads: §f%s", + "cosmiccore.machine.multithreaded.thread_status": "§b=== Thread Status ===", "cosmiccore.mana_leaching_tub.desc": "Mana Soaker 9000", "cosmiccore.multiblock.advanced.star_ladder_tier": "§aVomahine StarLadderOld Tether Tier§f: §b%s \n §aMax Research Modules§f: §b%s", "cosmiccore.multiblock.booster_used": "Booster: %s", @@ -612,6 +644,7 @@ "cosmiccore.multiblock.naqreactor.tooltip.0": "§cA massive reactor powered by explosions and reactive fuel", "cosmiccore.multiblock.naqreactor.tooltip.1": "§bWill always attempt to parallel to 16x output.", "cosmiccore.multiblock.naqreactor.tooltip.2": "§cOnly Accepts Laser hatches.", + "cosmiccore.multiblock.pattern.stellar_module_slot": "§7Module Slot (Air or Formed Module)", "cosmiccore.multiblock.reboot_powergrid": "§aReboot All Connected Machines", "cosmiccore.multiblock.send_orbit_data": "§a§lSend Research Payload", "cosmiccore.multiblock.sleep_powergrid": "§cSuspend All Connected Machines", @@ -619,6 +652,18 @@ "cosmiccore.multiblock.star_ladder.tooltip.1": "§c§lDANGER: DATA LOSS PRESENT", "cosmiccore.multiblock.star_ladder.tooltip.2": "§c§lDANGER: RECOVERY IS POSSIBLE", "cosmiccore.multiblock.star_ladder.tooltip.3": "§aPinacle Multiblock : The Final Goal of ACT1 (Steam to IV)", + "cosmiccore.multiblock.stellar_module.connected": "§aConnected to Stellar Iris", + "cosmiccore.multiblock.stellar_module.energy_usage": "§eWireless EU/t: §f%s", + "cosmiccore.multiblock.stellar_module.iris_not_formed": "§cStellar Iris Not Formed", + "cosmiccore.multiblock.stellar_module.iris_not_ready": "§eStellar Iris Not Ready", + "cosmiccore.multiblock.stellar_module.loading": "§7Loading...", + "cosmiccore.multiblock.stellar_module.no_wireless": "§cNo Wireless Energy Network", + "cosmiccore.multiblock.stellar_module.not_connected": "§cNot Connected to Stellar Iris", + "cosmiccore.multiblock.stellar_module.parallel": "§7Parallel Limit: §b%s", + "cosmiccore.multiblock.stellar_module.power_config": "§7Config: §b%s §7@ §a%dx §7Parallel", + "cosmiccore.multiblock.stellar_module.power_failure": "§c§lPOWER FAILURE - Insufficient Energy!", + "cosmiccore.multiblock.stellar_module.speed_bonus": "§7Speed Bonus: §a%s", + "cosmiccore.multiblock.stellar_module.stage": "§7Iris Stage: §e%s", "cosmiccore.omnia_circuit.ev": "§6Works as any EV Circuit.", "cosmiccore.omnia_circuit.hv": "§6Works as any HV Circuit.", "cosmiccore.omnia_circuit.iv": "§6Works as any IV Circuit.", @@ -647,6 +692,69 @@ "cosmiccore.rune_emotion_weak.1": "§7§oAn incomplete ERA reaction is observed.", "cosmiccore.rune_emotion_weak.2": "§7§oStrong emotional and chemical reactions cause the slate to vibrate.", "cosmiccore.rune_vague": "§7§oLatent emotions seem to be missing.", + "cosmiccore.stellar.context.blackhole_line1": "Singularity contained", + "cosmiccore.stellar.context.blackhole_line2": "Exotic processing", + "cosmiccore.stellar.context.death_graceful_line1": "Controlled shutdown", + "cosmiccore.stellar.context.death_graceful_line2": "in progress...", + "cosmiccore.stellar.context.death_line1": "CRITICAL FAILURE", + "cosmiccore.stellar.context.death_line2": "EVACUATE AREA", + "cosmiccore.stellar.context.empty_line1": "Insert star seed and", + "cosmiccore.stellar.context.empty_line2": "provide stellar gases", + "cosmiccore.stellar.context.empty_line3": "to begin ignition.", + "cosmiccore.stellar.context.growing_line1": "Stellar fusion", + "cosmiccore.stellar.context.growing_line2": "initiating...", + "cosmiccore.stellar.context.star_line1": "Stable fusion active", + "cosmiccore.stellar.context.star_line2": "Processing available", + "cosmiccore.stellar.context.superstar_line1": "WARNING: Critical mass", + "cosmiccore.stellar.context.superstar_line2": "Collapse imminent", + "cosmiccore.stellar.ignition.breaking": "!!! BREAKING !!!", + "cosmiccore.stellar.ignition.ignite": "IGNITE", + "cosmiccore.stellar.ignition.requires_star": "REQUIRES ACTIVE STAR", + "cosmiccore.stellar.module.config": "Module Config", + "cosmiccore.stellar.module.current": "Current", + "cosmiccore.stellar.module.iris_limit": "Iris Limit", + "cosmiccore.stellar.module.max_eut": "Max EU/t", + "cosmiccore.stellar.module.not_linked": "Not linked to Stellar Iris", + "cosmiccore.stellar.module.parallel": "Parallel", + "cosmiccore.stellar.module.parallel_max": "%sx (max %s)", + "cosmiccore.stellar.module.speed_bonus": "Speed Bonus", + "cosmiccore.stellar.module.stage": "Stage", + "cosmiccore.stellar.module.status": "Status", + "cosmiccore.stellar.module.status.disconnected": "DISCONNECTED", + "cosmiccore.stellar.module.status.idle": "IDLE", + "cosmiccore.stellar.module.status.iris_inactive": "IRIS INACTIVE", + "cosmiccore.stellar.module.status.no_wireless": "NO WIRELESS", + "cosmiccore.stellar.module.status.offline": "OFFLINE", + "cosmiccore.stellar.module.status.power_fail": "POWER FAIL", + "cosmiccore.stellar.module.status.processing": "PROCESSING", + "cosmiccore.stellar.module.status.ready": "READY", + "cosmiccore.stellar.module.waiting_iris": "Waiting for Iris", + "cosmiccore.stellar.power.max_parallel": "Maximum Parallel", + "cosmiccore.stellar.power.title": "Power Control Panel", + "cosmiccore.stellar.power.voltage_per_parallel": "Voltage Per Parallel", + "cosmiccore.stellar.prestige.continue": "[Click anywhere to continue]", + "cosmiccore.stellar.prestige.current_tier": "CURRENT TIER", + "cosmiccore.stellar.prestige.max_tier": "MAXIMUM TIER REACHED", + "cosmiccore.stellar.prestige.next_tier": "%s pts for %s", + "cosmiccore.stellar.prestige.points_earned": "POINTS EARNED", + "cosmiccore.stellar.prestige.tier.apprentice": "APPRENTICE", + "cosmiccore.stellar.prestige.tier.expert": "EXPERT", + "cosmiccore.stellar.prestige.tier.grandmaster": "GRANDMASTER", + "cosmiccore.stellar.prestige.tier.journeyman": "JOURNEYMAN", + "cosmiccore.stellar.prestige.tier.master": "MASTER", + "cosmiccore.stellar.prestige.tier.novice": "NOVICE", + "cosmiccore.stellar.prestige.tier.unknown": "UNKNOWN", + "cosmiccore.stellar.prestige.tier_up": "TIER UP!", + "cosmiccore.stellar.prestige.title": "STELLAR CONVERGENCE", + "cosmiccore.stellar.prestige.total_points": "Total: %s points", + "cosmiccore.stellar.slot.star_seed": "Star Seed", + "cosmiccore.stellar.stage.controlled_shutdown": "CONTROLLED SHUTDOWN", + "cosmiccore.stellar.stage.critical_mass": "CRITICAL MASS", + "cosmiccore.stellar.stage.emergency_protocols": "EMERGENCY PROTOCOLS", + "cosmiccore.stellar.stage.initialization": "INITIALIZATION", + "cosmiccore.stellar.stage.singularity_control": "SINGULARITY CONTROL", + "cosmiccore.stellar.stage.stellar_ignition": "STELLAR IGNITION", + "cosmiccore.stellar.stage.stellar_operations": "STELLAR OPERATIONS", "cosmiccore.tenura.1": "§6Ten - Control", "cosmiccore.tenura.2": "§6Ura - Flow", "cosmiccore.thermomagnitizer.desc": "Heating and Magnets, what could go wrong", @@ -769,6 +877,7 @@ "item.cosmiccore.bitumen_wax": "Bitumen Wax", "item.cosmiccore.blackstone_pustule": "Blackstone Pustule", "item.cosmiccore.brimstone_asteroid": "Brimstone Asteroid", + "item.cosmiccore.bronze_supply_tank": "Bronze Supply Tank", "item.cosmiccore.capacity_chip": "Capacity Chip", "item.cosmiccore.carbon_asteroid_base": "Carbonic Asteroid", "item.cosmiccore.chronia": "Vexil - [Chronia]", @@ -938,6 +1047,8 @@ "item.cosmiccore.portable_gravity_core.tooltip": "§aNormalizes Gravity to Match Earth.", "item.cosmiccore.potency_chip": "Potency Chip", "item.cosmiccore.prepared_petri_dish": "Prepared Petri Dish", + "item.cosmiccore.pressurized_rebreather": "Pressurized Rebreather", + "item.cosmiccore.pressurized_rebreather.tooltip": "§6Enables oxygen tank usage. Works in §cNo Air§6 environments.", "item.cosmiccore.prideful_spirit": "Prideful Spirit", "item.cosmiccore.prod_mod_1": "Productivity Module Mk.1", "item.cosmiccore.prod_mod_2": "Productivity Module Mk.2", @@ -964,6 +1075,7 @@ "item.cosmiccore.record_kept_printed_circuit_board": "Record Kept Printed Circuit Board", "item.cosmiccore.refined_harmonics_wafer": "Refined Harmonic Wafer", "item.cosmiccore.refined_resonant_wafer": "Refined Harmonic Wafer", + "item.cosmiccore.reflection_mirror": "Mirror of Erosion", "item.cosmiccore.resipiratory_sculk_hemocytoblast": "Respiratory Sculk Hemocytoblast", "item.cosmiccore.resonant_mod": "Fusion Module Mk.1", "item.cosmiccore.robust_drone": "Robust Drone", @@ -993,6 +1105,8 @@ "item.cosmiccore.self_aware_processing_assembly": "Self Aware Processor Assembly", "item.cosmiccore.seraphon": "Luminon - [Seraphon]", "item.cosmiccore.shard_of_perpetuity": "Shard of Perpetuity", + "item.cosmiccore.simple_rebreather": "Simple Rebreather", + "item.cosmiccore.simple_rebreather.tooltip": "§7Reduces oxygen drain in §bThin Air§7 environments.", "item.cosmiccore.somatic_processing_assembly": "Somatoprocessing Assembly Board", "item.cosmiccore.sov_blood_orb": "Sovereign Blood Orb", "item.cosmiccore.space_advanced_nanomuscle_chestplate": "Advanced NanoMuscle™ Space Suite Chestplate", @@ -1008,6 +1122,7 @@ "item.cosmiccore.spirit_engraved_enthel_circuit_board": "Spirit Engraved Enthel Circuit Board", "item.cosmiccore.spirit_runed_enthel_cpu": "Spirit Runed Enthel CPU", "item.cosmiccore.spirit_runed_enthel_cpu_wafer": "Spirit Runed Enthel CPU Wafer", + "item.cosmiccore.steel_supply_tank": "Steel Supply Tank", "item.cosmiccore.streptococcus_pyogenes": "Streptococcus Pyogenes", "item.cosmiccore.streptococcus_pyogenes_culture": "Streptococcus Pyogenes Culture", "item.cosmiccore.suelescent_processor": "Suelescent Processor", @@ -1076,6 +1191,7 @@ "material.cosmiccore.enderium": "Enderium", "material.cosmiccore.energetic_alloy": "Energetic Alloy", "material.cosmiccore.enraged_stygian_plague": "Enraged Stygian Plague", + "material.cosmiccore.halizine": "Halizine", "material.cosmiccore.ichor": "Ichor", "material.cosmiccore.ichorium": "Ichorium", "material.cosmiccore.infinity": "Infinity", @@ -1097,6 +1213,7 @@ "material.cosmiccore.psionic_galvorn": "Psionic Galvorn", "material.cosmiccore.reclaimed_pale_ore": "Reclaimed Pale Ore", "material.cosmiccore.resonant_virtue_meld": "Resonant Virtue Meld", + "material.cosmiccore.rosmotosin": "Rosmotosin", "material.cosmiccore.shimmering_neutronium": "Shimmering Neutronium", "material.cosmiccore.signalum": "Signalum", "material.cosmiccore.sol_steel": "Sol Steel", @@ -1110,6 +1227,7 @@ "material.cosmiccore.superheavy_bedrock_alloy": "Superheavy Bedrock Alloy", "material.cosmiccore.taranium": "Taranium", "material.cosmiccore.temmerite": "Temmerite", + "material.cosmiccore.tenbrium": "Tenbrium", "material.cosmiccore.trinavine": "Trinavine", "material.cosmiccore.trinium_naqide": "Trinium Naqide", "material.cosmiccore.triphenylphosphine": "Triphenylphosphine", @@ -1118,6 +1236,443 @@ "material.cosmiccore.virtue_meld": "Virtue Meld", "material.cosmiccore.vitrius": "Vitrius", "material.cosmiccore.voidspark": "Voidspark", + "reflection.cosmiccore.bargain.ascension.answer.ready.drawback.0": "-30% movement speed when not flying", + "reflection.cosmiccore.bargain.ascension.answer.ready.drawback.1": "Vulnerable in no-fly zones or enclosed spaces", + "reflection.cosmiccore.bargain.ascension.answer.ready.power.0": "Creative-style flight (toggle with jump while airborne)", + "reflection.cosmiccore.bargain.ascension.answer.ready.power.1": "Fly indefinitely without hunger or stamina cost", + "reflection.cosmiccore.bargain.ascension.answer.ready.power.2": "Full 3D movement control while flying", + "reflection.cosmiccore.bargain.ascension.answer.ready.power.3": "No fall damage while flight is active", + "reflection.cosmiccore.bargain.ascension.answer.ready.response": "Then rise. The ground has no claim on you anymore.", + "reflection.cosmiccore.bargain.ascension.answer.ready.text": "I am ready to fly.", + "reflection.cosmiccore.bargain.ascension.answer.refuse.response": "Served. Like a servant. How long until you realize you were the servant all along?", + "reflection.cosmiccore.bargain.ascension.answer.refuse.text": "The ground has served me well.", + "reflection.cosmiccore.bargain.ascension.description": "Rise above the crawling earth", + "reflection.cosmiccore.bargain.ascension.dialogue.0": "You are bound to the ground. Chained by gravity's petty tyranny.", + "reflection.cosmiccore.bargain.ascension.dialogue.1": "You dream of flight. All mortals do.", + "reflection.cosmiccore.bargain.ascension.dialogue.2": "I can sever those chains. Let you rise.", + "reflection.cosmiccore.bargain.ascension.dialogue.3": "Not gliding. Not falling with style. True flight.", + "reflection.cosmiccore.bargain.ascension.dialogue.4": "The sky will open to you like a door.", + "reflection.cosmiccore.bargain.ascension.dialogue.5": "But the ground... the ground will become alien. Uncomfortable. Wrong.", + "reflection.cosmiccore.bargain.ascension.name": "Ascension", + "reflection.cosmiccore.bargain.ascension.on_accept": "Weight leaves you. The sky opens. You are no longer earth-bound.", + "reflection.cosmiccore.bargain.ascension.on_defy": "Gravity reclaims you. The ground welcomes you back, possessively.", + "reflection.cosmiccore.bargain.ascension.question": "Will you abandon the earth for the sky?", + "reflection.cosmiccore.bargain.back.answer.accept.drawback.0": "5 erosion cost per teleport use", + "reflection.cosmiccore.bargain.back.answer.accept.drawback.1": "Marker fades after 10 minutes", + "reflection.cosmiccore.bargain.back.answer.accept.power.0": "Teleport to death location (once per death)", + "reflection.cosmiccore.bargain.back.answer.accept.power.1": "Death marker visible through walls", + "reflection.cosmiccore.bargain.back.answer.accept.response": "Death becomes a waypoint now. Not an ending - a detour.", + "reflection.cosmiccore.bargain.back.answer.accept.text": "Teach me to find my way back.", + "reflection.cosmiccore.bargain.back.answer.refuse.response": "Consequences. You'll have plenty of those regardless.", + "reflection.cosmiccore.bargain.back.answer.refuse.text": "Death should have consequences.", + "reflection.cosmiccore.bargain.back.description": "Return to where you fell", + "reflection.cosmiccore.bargain.back.dialogue.0": "Death scatters you. Sends you back to beds, to spawns, to arbitrary points.", + "reflection.cosmiccore.bargain.back.dialogue.1": "But what if you could return to where you fell?", + "reflection.cosmiccore.bargain.back.dialogue.2": "I can teach you to remember. To reach back through death itself.", + "reflection.cosmiccore.bargain.back.name": "The Way Back", + "reflection.cosmiccore.bargain.back.on_accept": "A thread connects you to your last breath. You can follow it back.", + "reflection.cosmiccore.bargain.back.on_defy": "The thread snaps. Death becomes final once more.", + "reflection.cosmiccore.bargain.back.question": "Will you learn to retrace death's path?", + "reflection.cosmiccore.bargain.carapace.answer.refuse.response": "Feeling. How fragile. How mortal. How temporary.", + "reflection.cosmiccore.bargain.carapace.answer.refuse.text": "I would rather feel than merely endure.", + "reflection.cosmiccore.bargain.carapace.answer.survive.drawback.0": "-20% healing from all sources", + "reflection.cosmiccore.bargain.carapace.answer.survive.drawback.1": "Reduced potion effectiveness", + "reflection.cosmiccore.bargain.carapace.answer.survive.power.0": "+8 armor points (4 full armor icons)", + "reflection.cosmiccore.bargain.carapace.answer.survive.power.1": "Stacks with worn armor", + "reflection.cosmiccore.bargain.carapace.answer.survive.response": "Your skin tightens. Hardens. You are becoming something more durable.", + "reflection.cosmiccore.bargain.carapace.answer.survive.text": "Harden me. I choose survival.", + "reflection.cosmiccore.bargain.carapace.description": "Your flesh hardens into living armor", + "reflection.cosmiccore.bargain.carapace.dialogue.0": "Your skin is so soft. So vulnerable.", + "reflection.cosmiccore.bargain.carapace.dialogue.1": "Every blade, every claw, every falling stone - they all threaten you.", + "reflection.cosmiccore.bargain.carapace.dialogue.2": "I can harden your flesh. Make it something more... enduring.", + "reflection.cosmiccore.bargain.carapace.dialogue.3": "Blows will glance off. Damage will diminish.", + "reflection.cosmiccore.bargain.carapace.dialogue.4": "But you will feel less. Touch will become... distant.", + "reflection.cosmiccore.bargain.carapace.name": "Carapace", + "reflection.cosmiccore.bargain.carapace.on_accept": "Your flesh ripples and tightens. It doesn't hurt. That's the point.", + "reflection.cosmiccore.bargain.carapace.on_defy": "Sensation floods back - every breeze, every texture. You are soft again.", + "reflection.cosmiccore.bargain.carapace.question": "Will you sacrifice sensation for survival?", + "reflection.cosmiccore.bargain.cinder.answer.burn.drawback.0": "2x damage from freezing and cold sources", + "reflection.cosmiccore.bargain.cinder.answer.burn.drawback.1": "Water extinguishes slower, feels unpleasant", + "reflection.cosmiccore.bargain.cinder.answer.burn.power.0": "Complete fire and lava immunity", + "reflection.cosmiccore.bargain.cinder.answer.burn.power.1": "Can swim in lava safely", + "reflection.cosmiccore.bargain.cinder.answer.burn.response": "It hurts. Just for a moment. Then the pain becomes a memory, and fire becomes a friend.", + "reflection.cosmiccore.bargain.cinder.answer.burn.text": "Burn me completely. Make me immune.", + "reflection.cosmiccore.bargain.cinder.answer.refuse.response": "Respect. For something that would consume you without thought. How noble.", + "reflection.cosmiccore.bargain.cinder.answer.refuse.text": "Fire should be respected, not befriended.", + "reflection.cosmiccore.bargain.cinder.description": "Fire cannot harm what has already burned", + "reflection.cosmiccore.bargain.cinder.dialogue.0": "Fire consumes. It is in its nature to destroy.", + "reflection.cosmiccore.bargain.cinder.dialogue.1": "Every flame that touches you leaves its mark.", + "reflection.cosmiccore.bargain.cinder.dialogue.2": "But what if you had already burned? Completely. Utterly.", + "reflection.cosmiccore.bargain.cinder.dialogue.3": "Fire cannot consume what is already ash and ember.", + "reflection.cosmiccore.bargain.cinder.dialogue.4": "Let me burn away your vulnerability. You will walk through infernos unscathed.", + "reflection.cosmiccore.bargain.cinder.name": "Cinder", + "reflection.cosmiccore.bargain.cinder.on_accept": "Heat floods through you, then recedes. Fire will never frighten you again.", + "reflection.cosmiccore.bargain.cinder.on_defy": "The warmth drains away. Flames flicker hungrily when they see you now.", + "reflection.cosmiccore.bargain.cinder.question": "Will you let the flame claim you, so it can never hurt you again?", + "reflection.cosmiccore.bargain.darksight.answer.refuse.response": "Cling to your torch then. See how long it lasts in the deep places.", + "reflection.cosmiccore.bargain.darksight.answer.refuse.text": "The light serves me well enough.", + "reflection.cosmiccore.bargain.darksight.answer.yes.drawback.0": "Blindness effect in bright sunlight", + "reflection.cosmiccore.bargain.darksight.answer.yes.drawback.1": "Must wear helmet or stay underground during day", + "reflection.cosmiccore.bargain.darksight.answer.yes.power.0": "Permanent Night Vision effect", + "reflection.cosmiccore.bargain.darksight.answer.yes.power.1": "See in complete darkness", + "reflection.cosmiccore.bargain.darksight.answer.yes.response": "Your pupils dilate... and keep dilating. The dark becomes your domain.", + "reflection.cosmiccore.bargain.darksight.answer.yes.text": "Let me see in the darkness.", + "reflection.cosmiccore.bargain.darksight.description": "See through the deepest darkness", + "reflection.cosmiccore.bargain.darksight.dialogue.0": "You fear the dark. Every creature does, at first.", + "reflection.cosmiccore.bargain.darksight.dialogue.1": "But darkness is merely the absence of something. Not presence.", + "reflection.cosmiccore.bargain.darksight.dialogue.2": "I can teach your eyes to drink the shadow. To see what lurks unseen.", + "reflection.cosmiccore.bargain.darksight.dialogue.3": "Every hidden corner will surrender its secrets to you.", + "reflection.cosmiccore.bargain.darksight.dialogue.4": "But be warned - the light will begin to burn.", + "reflection.cosmiccore.bargain.darksight.name": "Darksight", + "reflection.cosmiccore.bargain.darksight.on_accept": "The shadows retreat from your vision. You see everything now.", + "reflection.cosmiccore.bargain.darksight.on_defy": "Light floods back. The darkness closes its secrets to you once more.", + "reflection.cosmiccore.bargain.darksight.question": "Will you trade the sun for the gift of shadow-sight?", + "reflection.cosmiccore.bargain.depths.answer.embrace.drawback.0": "Instant death when oxygen fully depletes", + "reflection.cosmiccore.bargain.depths.answer.embrace.drawback.1": "No drowning damage warning - just death", + "reflection.cosmiccore.bargain.depths.answer.embrace.power.0": "5x oxygen capacity underwater", + "reflection.cosmiccore.bargain.depths.answer.embrace.power.1": "Extended breath in toxic atmospheres", + "reflection.cosmiccore.bargain.depths.answer.embrace.response": "Your chest feels hollow now. That's normal. The new capacity needs room.", + "reflection.cosmiccore.bargain.depths.answer.embrace.text": "Remake me for the depths.", + "reflection.cosmiccore.bargain.depths.answer.refuse.response": "The depths wait patiently. They always have.", + "reflection.cosmiccore.bargain.depths.answer.refuse.text": "I'll keep my mortal breath.", + "reflection.cosmiccore.bargain.depths.description": "Enhanced breath with fatal consequences", + "reflection.cosmiccore.bargain.depths.dialogue.0": "You've felt the water closing over your head.", + "reflection.cosmiccore.bargain.depths.dialogue.1": "That desperate burn in your lungs. The panic.", + "reflection.cosmiccore.bargain.depths.dialogue.2": "I can remake those fragile organs. Give them capacity beyond mortal limits.", + "reflection.cosmiccore.bargain.depths.dialogue.3": "Your breath will stretch to fill the void between heartbeats.", + "reflection.cosmiccore.bargain.depths.dialogue.4": "But understand - when they finally empty, there will be no warning.", + "reflection.cosmiccore.bargain.depths.dialogue.5": "No desperate gasps. No gradual fading. Just... silence.", + "reflection.cosmiccore.bargain.depths.name": "The Depths", + "reflection.cosmiccore.bargain.depths.on_accept": "Something shifts in your chest. The air tastes different now.", + "reflection.cosmiccore.bargain.depths.on_defy": "You gasp - your lungs remember panic, remember struggle. You are mortal again.", + "reflection.cosmiccore.bargain.depths.question": "Will you let me reshape your breath?", + "reflection.cosmiccore.bargain.home.answer.accept.drawback.0": "10 erosion cost per teleport", + "reflection.cosmiccore.bargain.home.answer.accept.drawback.1": "-10% XP gain while far from home", + "reflection.cosmiccore.bargain.home.answer.accept.power.0": "Instant teleport to spawn/bed point", + "reflection.cosmiccore.bargain.home.answer.accept.power.1": "5 minute cooldown between uses", + "reflection.cosmiccore.bargain.home.answer.accept.response": "Feel the pull now? Home is never more than a thought away.", + "reflection.cosmiccore.bargain.home.answer.accept.text": "Bind me to my home.", + "reflection.cosmiccore.bargain.home.answer.refuse.response": "Earned. Through miles of walking. How charmingly primitive.", + "reflection.cosmiccore.bargain.home.answer.refuse.text": "Home should be earned, not summoned.", + "reflection.cosmiccore.bargain.home.description": "The way home is always open", + "reflection.cosmiccore.bargain.home.dialogue.0": "Home. Such a powerful concept for mortals.", + "reflection.cosmiccore.bargain.home.dialogue.1": "The place you return to. The anchor that grounds you.", + "reflection.cosmiccore.bargain.home.dialogue.2": "I can make that connection stronger. Instant. Unbreakable.", + "reflection.cosmiccore.bargain.home.name": "Homeward", + "reflection.cosmiccore.bargain.home.on_accept": "A cord of void stretches between you and home. Pull it anytime.", + "reflection.cosmiccore.bargain.home.on_defy": "The cord dissolves. Home is a journey again, not a destination.", + "reflection.cosmiccore.bargain.home.question": "Will you bind yourself to your home with chains of void?", + "reflection.cosmiccore.bargain.quake_movement.answer.refuse.response": "Such faith in the pedestrian. We shall see how long that serves you.", + "reflection.cosmiccore.bargain.quake_movement.answer.refuse.text": "My feet know their own rhythm.", + "reflection.cosmiccore.bargain.quake_movement.answer.yes.drawback.0": "Movement feels unnatural to observers", + "reflection.cosmiccore.bargain.quake_movement.answer.yes.power.0": "Bunny hopping preserves and builds momentum", + "reflection.cosmiccore.bargain.quake_movement.answer.yes.power.1": "Air strafing for mid-air direction control", + "reflection.cosmiccore.bargain.quake_movement.answer.yes.response": "Feel it now - your muscles rewiring, learning trajectories they were never meant to know.", + "reflection.cosmiccore.bargain.quake_movement.answer.yes.text": "Teach me to move like the wind.", + "reflection.cosmiccore.bargain.quake_movement.description": "Master momentum itself through unnatural locomotion", + "reflection.cosmiccore.bargain.quake_movement.dialogue.0": "You move like prey. Hesitant. Fearful.", + "reflection.cosmiccore.bargain.quake_movement.dialogue.1": "I remember when things moved differently. Before physics became so... rigid.", + "reflection.cosmiccore.bargain.quake_movement.dialogue.2": "I can teach your legs to remember that older way.", + "reflection.cosmiccore.bargain.quake_movement.dialogue.3": "But once they learn, they will never be content with stillness again.", + "reflection.cosmiccore.bargain.quake_movement.name": "Velocity", + "reflection.cosmiccore.bargain.quake_movement.on_accept": "Your joints crack and reform. Movement becomes instinct.", + "reflection.cosmiccore.bargain.quake_movement.on_defy": "Your legs remember what it was to walk normally. The speed fades like a dream.", + "reflection.cosmiccore.bargain.quake_movement.question": "Will you embrace the velocity that waits within you?", + "reflection.cosmiccore.bargain.reach.answer.further.drawback.0": "-15% mining speed", + "reflection.cosmiccore.bargain.reach.answer.further.drawback.1": "Item pickup range reduced", + "reflection.cosmiccore.bargain.reach.answer.further.power.0": "+3 block reach (build from further)", + "reflection.cosmiccore.bargain.reach.answer.further.power.1": "+2 attack reach", + "reflection.cosmiccore.bargain.reach.answer.further.response": "There. Don't look at your hands too closely. It's easier that way.", + "reflection.cosmiccore.bargain.reach.answer.further.text": "Stretch me further. I want to grasp everything.", + "reflection.cosmiccore.bargain.reach.answer.refuse.response": "For now. But you'll want more. They always do.", + "reflection.cosmiccore.bargain.reach.answer.refuse.text": "My reach is sufficient.", + "reflection.cosmiccore.bargain.reach.description": "Your grasp extends beyond natural limits", + "reflection.cosmiccore.bargain.reach.dialogue.0": "So close, yet so far. The eternal frustration of short arms.", + "reflection.cosmiccore.bargain.reach.dialogue.1": "Everything just slightly out of reach. Mocking you.", + "reflection.cosmiccore.bargain.reach.dialogue.2": "I can extend you. Stretch your grasp beyond mortal limits.", + "reflection.cosmiccore.bargain.reach.dialogue.3": "Your arms will find what they seek. But others may find them... unsettling.", + "reflection.cosmiccore.bargain.reach.name": "Reach", + "reflection.cosmiccore.bargain.reach.on_accept": "Something shifts in your shoulders. Your arms remember being longer.", + "reflection.cosmiccore.bargain.reach.on_defy": "Your arms contract back to normal. The world feels close and small again.", + "reflection.cosmiccore.bargain.reach.question": "Will you extend your reach into the unnatural?", + "reflection.cosmiccore.bargain.satiated.answer.empty.drawback.0": "Food restores 50% less hunger bars", + "reflection.cosmiccore.bargain.satiated.answer.empty.drawback.1": "Cannot benefit from food-based buffs", + "reflection.cosmiccore.bargain.satiated.answer.empty.power.0": "Hunger depletes 80% slower", + "reflection.cosmiccore.bargain.satiated.answer.empty.power.1": "Food provides 3x saturation", + "reflection.cosmiccore.bargain.satiated.answer.empty.response": "The emptiness fades. You will never truly need to eat again.", + "reflection.cosmiccore.bargain.satiated.answer.empty.text": "Free me from this hunger.", + "reflection.cosmiccore.bargain.satiated.answer.refuse.response": "Enjoy. Such a mortal word. The hunger will remind you of your place.", + "reflection.cosmiccore.bargain.satiated.answer.refuse.text": "I enjoy my meals. I'll keep the hunger.", + "reflection.cosmiccore.bargain.satiated.description": "Hunger becomes a distant memory", + "reflection.cosmiccore.bargain.satiated.dialogue.0": "The constant gnawing. The endless need to consume.", + "reflection.cosmiccore.bargain.satiated.dialogue.1": "Your stomach rules you like a tyrant.", + "reflection.cosmiccore.bargain.satiated.dialogue.2": "What if I could silence it? Make fullness your natural state?", + "reflection.cosmiccore.bargain.satiated.dialogue.3": "You would eat for taste, for ritual - never for need.", + "reflection.cosmiccore.bargain.satiated.dialogue.4": "But taste itself would fade. Food becomes... fuel. Nothing more.", + "reflection.cosmiccore.bargain.satiated.name": "Satiated", + "reflection.cosmiccore.bargain.satiated.on_accept": "The gnawing stops. Silence in your belly. Freedom.", + "reflection.cosmiccore.bargain.satiated.on_defy": "Hunger returns with a vengeance. You remember what need feels like.", + "reflection.cosmiccore.bargain.satiated.question": "Will you trade the pleasure of eating for freedom from hunger?", + "reflection.cosmiccore.bargain.soft_landing.answer.refuse.response": "Caution. A slow path to nowhere. But walk it if you must.", + "reflection.cosmiccore.bargain.soft_landing.answer.refuse.text": "Fear keeps me cautious. I'll keep it.", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.drawback.0": "+15% damage taken from all sources", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.drawback.1": "Reduced knockback resistance", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.power.0": "Complete fall damage immunity", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.power.1": "Safe drops from any height", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.response": "Jump. Go ahead. The ground will embrace you like a mother.", + "reflection.cosmiccore.bargain.soft_landing.answer.yes.text": "Take away my fear of falling.", + "reflection.cosmiccore.bargain.soft_landing.description": "The ground welcomes you gently from any height", + "reflection.cosmiccore.bargain.soft_landing.dialogue.0": "Heights terrify you. The primal fear of falling.", + "reflection.cosmiccore.bargain.soft_landing.dialogue.1": "That sickening moment when gravity claims you.", + "reflection.cosmiccore.bargain.soft_landing.dialogue.2": "But what if the ground... forgave you?", + "reflection.cosmiccore.bargain.soft_landing.dialogue.3": "What if every fall ended softly, no matter the height?", + "reflection.cosmiccore.bargain.soft_landing.dialogue.4": "You need never fear the drop again.", + "reflection.cosmiccore.bargain.soft_landing.name": "Soft Landing", + "reflection.cosmiccore.bargain.soft_landing.on_accept": "Your relationship with gravity shifts. It still pulls, but gently now.", + "reflection.cosmiccore.bargain.soft_landing.on_defy": "Weight crashes back into your bones. Every fall matters again.", + "reflection.cosmiccore.bargain.soft_landing.question": "Will you let the earth catch you?", + "reflection.cosmiccore.bargain.stride.answer.accept.drawback.0": "Cannot crouch-walk off edges", + "reflection.cosmiccore.bargain.stride.answer.accept.power.0": "Auto step-up to 1 block height", + "reflection.cosmiccore.bargain.stride.answer.accept.power.1": "Smooth terrain traversal", + "reflection.cosmiccore.bargain.stride.answer.accept.response": "Walk now. Feel how terrain reshapes itself for your convenience.", + "reflection.cosmiccore.bargain.stride.answer.accept.text": "Let the world flatten before me.", + "reflection.cosmiccore.bargain.stride.answer.refuse.response": "Such determination. It will erode. They always do.", + "reflection.cosmiccore.bargain.stride.answer.refuse.text": "I'll climb my own way.", + "reflection.cosmiccore.bargain.stride.description": "Walk over obstacles as if they were nothing", + "reflection.cosmiccore.bargain.stride.dialogue.0": "Every ledge. Every block. Every small obstacle.", + "reflection.cosmiccore.bargain.stride.dialogue.1": "They mock you with their need to be climbed.", + "reflection.cosmiccore.bargain.stride.dialogue.2": "What if the world simply... accommodated your stride?", + "reflection.cosmiccore.bargain.stride.dialogue.3": "Your feet need never leave the ground. The ground will rise to meet them.", + "reflection.cosmiccore.bargain.stride.name": "Stride", + "reflection.cosmiccore.bargain.stride.on_accept": "The earth seems to shift slightly, eager to smooth your path.", + "reflection.cosmiccore.bargain.stride.on_defy": "Gravity reasserts itself. Every ledge is a challenge again.", + "reflection.cosmiccore.bargain.stride.question": "Shall obstacles bow before your passage?", + "reflection.cosmiccore.bargain.swiftness.answer.accept.drawback.0": "Increased hunger when standing still", + "reflection.cosmiccore.bargain.swiftness.answer.accept.power.0": "+40% movement speed", + "reflection.cosmiccore.bargain.swiftness.answer.accept.power.1": "Sprint without hunger drain", + "reflection.cosmiccore.bargain.swiftness.answer.accept.response": "Your blood sings now. Feel it racing faster than any heart should allow.", + "reflection.cosmiccore.bargain.swiftness.answer.accept.text": "Make me swift beyond measure.", + "reflection.cosmiccore.bargain.swiftness.answer.refuse.response": "Content. Such a mortal sentiment. It will fade.", + "reflection.cosmiccore.bargain.swiftness.answer.refuse.text": "I am content with my pace.", + "reflection.cosmiccore.bargain.swiftness.description": "Supernatural speed courses through your veins", + "reflection.cosmiccore.bargain.swiftness.dialogue.0": "The world moves so slowly around you, doesn't it?", + "reflection.cosmiccore.bargain.swiftness.dialogue.1": "Everyone else trudging through molasses while you ache to run.", + "reflection.cosmiccore.bargain.swiftness.dialogue.2": "I can accelerate you. Make the world blur past like a fading dream.", + "reflection.cosmiccore.bargain.swiftness.name": "Swiftness", + "reflection.cosmiccore.bargain.swiftness.on_accept": "Lightning arcs through your muscles. You twitch with restless energy.", + "reflection.cosmiccore.bargain.swiftness.on_defy": "The world speeds up around you. You are merely human once more.", + "reflection.cosmiccore.bargain.swiftness.question": "Do you wish to leave the slow world behind?", + "reflection.cosmiccore.bargain.violence.answer.accept.drawback.0": "+20% damage taken from all sources", + "reflection.cosmiccore.bargain.violence.answer.accept.drawback.1": "Cannot use shields", + "reflection.cosmiccore.bargain.violence.answer.accept.power.0": "+30% melee damage dealt", + "reflection.cosmiccore.bargain.violence.answer.accept.power.1": "+15% attack speed", + "reflection.cosmiccore.bargain.violence.answer.accept.response": "Feel it now? The urge to destroy? Don't fight it. It's yours.", + "reflection.cosmiccore.bargain.violence.answer.accept.text": "Remove my restraints.", + "reflection.cosmiccore.bargain.violence.answer.refuse.response": "Restraint. A leash you put on yourself. How adorable.", + "reflection.cosmiccore.bargain.violence.answer.refuse.text": "Restraint is its own strength.", + "reflection.cosmiccore.bargain.violence.description": "Strike with the force of something terrible", + "reflection.cosmiccore.bargain.violence.dialogue.0": "Your blows are so... restrained. Hesitant.", + "reflection.cosmiccore.bargain.violence.dialogue.1": "You hold back. Every swing. Some part of you fears the damage.", + "reflection.cosmiccore.bargain.violence.dialogue.2": "I can remove that restraint. Let your violence flow freely.", + "reflection.cosmiccore.bargain.violence.dialogue.3": "Your enemies will shatter before you.", + "reflection.cosmiccore.bargain.violence.dialogue.4": "But violence is a river that flows both ways.", + "reflection.cosmiccore.bargain.violence.name": "Violence", + "reflection.cosmiccore.bargain.violence.on_accept": "Power surges through your arms. Everything looks so... breakable now.", + "reflection.cosmiccore.bargain.violence.on_defy": "The rage drains away. Your blows return to mortal weight.", + "reflection.cosmiccore.bargain.violence.question": "Will you embrace true, unrestrained violence?", + "reflection.cosmiccore.bargain.vitality.answer.accept.drawback.0": "-50% natural regeneration rate", + "reflection.cosmiccore.bargain.vitality.answer.accept.drawback.1": "Healing potions 30% less effective", + "reflection.cosmiccore.bargain.vitality.answer.accept.power.0": "+10 max health (5 extra hearts)", + "reflection.cosmiccore.bargain.vitality.answer.accept.power.1": "Increased damage absorption buffer", + "reflection.cosmiccore.bargain.vitality.answer.accept.response": "Your heart swells. Literally. It has more to pump now.", + "reflection.cosmiccore.bargain.vitality.answer.accept.text": "Give me more life to spend.", + "reflection.cosmiccore.bargain.vitality.answer.refuse.response": "Given. As if anyone gave you anything. You simply are. For now.", + "reflection.cosmiccore.bargain.vitality.answer.refuse.text": "I'll work with what I was given.", + "reflection.cosmiccore.bargain.vitality.description": "Life force beyond mortal limits", + "reflection.cosmiccore.bargain.vitality.dialogue.0": "Your body has limits. A fixed amount of life.", + "reflection.cosmiccore.bargain.vitality.dialogue.1": "When it empties, you die. Simple. Brutal. Final.", + "reflection.cosmiccore.bargain.vitality.dialogue.2": "I can give you more. Stretch your life force beyond its natural bounds.", + "reflection.cosmiccore.bargain.vitality.dialogue.3": "More blood. More breath. More heartbeats before the end.", + "reflection.cosmiccore.bargain.vitality.dialogue.4": "But maintaining excess takes effort. You will heal... slower.", + "reflection.cosmiccore.bargain.vitality.name": "Vitality", + "reflection.cosmiccore.bargain.vitality.on_accept": "Your veins surge with new vigor. Everything feels more... present.", + "reflection.cosmiccore.bargain.vitality.on_defy": "The excess drains away. You are mortal-sized once more.", + "reflection.cosmiccore.bargain.vitality.question": "Will you trade recovery for resilience?", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.drawback.0": "-25% damage in lit areas (sky access)", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.drawback.1": "Takes damage from direct sunlight exposure", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.power.0": "Void damage immunity (Y < 0)", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.power.1": "Teleport to surface when entering void", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.response": "Done. The void knows your name now. It will not harm what belongs to it.", + "reflection.cosmiccore.bargain.void_anchor.answer.anchor.text": "Mark me. Make me yours.", + "reflection.cosmiccore.bargain.void_anchor.answer.refuse.response": "The light. Yes. Such a thin shield against the endless dark. Good luck.", + "reflection.cosmiccore.bargain.void_anchor.answer.refuse.text": "I'll stay in the light, thank you.", + "reflection.cosmiccore.bargain.void_anchor.description": "The void cannot claim what is already its own", + "reflection.cosmiccore.bargain.void_anchor.dialogue.0": "You've felt it. The pull of the void beneath the world.", + "reflection.cosmiccore.bargain.void_anchor.dialogue.1": "That endless fall. That final darkness that swallows everything.", + "reflection.cosmiccore.bargain.void_anchor.dialogue.2": "I dwell there. In that space between existence and nothing.", + "reflection.cosmiccore.bargain.void_anchor.dialogue.3": "I can mark you. Make you mine. And what is mine, the void cannot destroy.", + "reflection.cosmiccore.bargain.void_anchor.dialogue.4": "Fall as far as you like. The darkness will recognize you. Welcome you.", + "reflection.cosmiccore.bargain.void_anchor.dialogue.5": "But being marked by the void... it changes how existence sees you.", + "reflection.cosmiccore.bargain.void_anchor.name": "Void Anchor", + "reflection.cosmiccore.bargain.void_anchor.on_accept": "Something cold touches your soul. Marks it. The void knows you now.", + "reflection.cosmiccore.bargain.void_anchor.on_defy": "The mark burns away. The void forgets you. It will not be merciful next time.", + "reflection.cosmiccore.bargain.void_anchor.question": "Will you become an anchor in the nothing?", + "reflection.cosmiccore.threshold.0.dialogue.0": "You took something that wasn't freely given.", + "reflection.cosmiccore.threshold.0.dialogue.1": "Did it feel good? The power flooding in?", + "reflection.cosmiccore.threshold.0.dialogue.2": "Of course it did. That's the point.", + "reflection.cosmiccore.threshold.0.dialogue.3": "I'll be watching now. We have business together.", + "reflection.cosmiccore.threshold.0.question": "Do you understand what you've started?", + "reflection.cosmiccore.threshold.0.response": "Good. Or not. It doesn't matter now.", + "reflection.cosmiccore.threshold.1.dialogue.0": "Already back for more. I'm not surprised.", + "reflection.cosmiccore.threshold.1.dialogue.1": "Your soul stretches thinner. Can you feel it?", + "reflection.cosmiccore.threshold.1.dialogue.2": "Like taffy. Like mist. Like a memory fading.", + "reflection.cosmiccore.threshold.1.dialogue.3": "Don't worry. You have plenty left. For now.", + "reflection.cosmiccore.threshold.1.question": "Still comfortable?", + "reflection.cosmiccore.threshold.1.response": "Comfort is overrated anyway.", + "reflection.cosmiccore.threshold.2.dialogue.0": "A third of you belongs to me now.", + "reflection.cosmiccore.threshold.2.dialogue.1": "That's not metaphor. I can see it. Taste it.", + "reflection.cosmiccore.threshold.2.dialogue.2": "Your edges blur. Your definition softens.", + "reflection.cosmiccore.threshold.2.dialogue.3": "Others might start to notice soon.", + "reflection.cosmiccore.threshold.2.question": "Having second thoughts?", + "reflection.cosmiccore.threshold.2.response": "Second thoughts require a first. You never had one.", + "reflection.cosmiccore.threshold.3.dialogue.0": "The dreams are starting, aren't they?", + "reflection.cosmiccore.threshold.3.dialogue.1": "The ones where you fall and never land.", + "reflection.cosmiccore.threshold.3.dialogue.2": "Where you look in a mirror and something else looks back.", + "reflection.cosmiccore.threshold.3.dialogue.3": "That's not a dream. That's prophecy.", + "reflection.cosmiccore.threshold.3.question": "Do you still know who you are?", + "reflection.cosmiccore.threshold.3.response": "Keep telling yourself that name means something.", + "reflection.cosmiccore.threshold.4.dialogue.0": "Halfway. The point of no return approaches.", + "reflection.cosmiccore.threshold.4.dialogue.1": "Half of you, gone. Given away for trinkets.", + "reflection.cosmiccore.threshold.4.dialogue.2": "Was it worth it? Don't answer. I don't care.", + "reflection.cosmiccore.threshold.4.dialogue.3": "What matters is what comes next.", + "reflection.cosmiccore.threshold.4.question": "Ready to see what's on the other side?", + "reflection.cosmiccore.threshold.4.response": "No one ever is. But they cross anyway.", + "reflection.cosmiccore.threshold.5.dialogue.0": "More of you is mine than yours now.", + "reflection.cosmiccore.threshold.5.dialogue.1": "Does that frighten you? It should.", + "reflection.cosmiccore.threshold.5.dialogue.2": "I know thoughts you haven't had yet.", + "reflection.cosmiccore.threshold.5.dialogue.3": "I feel feelings you've forgotten.", + "reflection.cosmiccore.threshold.5.question": "Who's really in control?", + "reflection.cosmiccore.threshold.5.response": "Keep pretending you still have choice.", + "reflection.cosmiccore.threshold.6.dialogue.0": "Your reflection doesn't quite match anymore.", + "reflection.cosmiccore.threshold.6.dialogue.1": "The delay is slight. Others might not notice.", + "reflection.cosmiccore.threshold.6.dialogue.2": "But you know. You feel it.", + "reflection.cosmiccore.threshold.6.question": "What stares back at you in mirrors?", + "reflection.cosmiccore.threshold.6.response": "Me. Always me now.", + "reflection.cosmiccore.threshold.7.dialogue.0": "So little left of what you were.", + "reflection.cosmiccore.threshold.7.dialogue.1": "Fragments. Echoes. Shadows of intention.", + "reflection.cosmiccore.threshold.7.dialogue.2": "The body walks. The mind calculates.", + "reflection.cosmiccore.threshold.7.dialogue.3": "But the soul? The soul is almost spent.", + "reflection.cosmiccore.threshold.7.question": "Can you remember your mother's face?", + "reflection.cosmiccore.threshold.7.response": "No. You can't. I took that already.", + "reflection.cosmiccore.threshold.8.dialogue.0": "One step from the edge now.", + "reflection.cosmiccore.threshold.8.dialogue.1": "Ten percent. A sliver. A thread.", + "reflection.cosmiccore.threshold.8.dialogue.2": "That's all that separates you from... me.", + "reflection.cosmiccore.threshold.8.dialogue.3": "One more bargain. Just one more.", + "reflection.cosmiccore.threshold.8.question": "Will you take that final step?", + "reflection.cosmiccore.threshold.8.response": "We both know you will. The question is when.", + "reflection.cosmiccore.threshold.9.dialogue.0": "Finally.", + "reflection.cosmiccore.threshold.9.dialogue.1": "You gave everything. Every last piece.", + "reflection.cosmiccore.threshold.9.dialogue.2": "There's nothing left of what walked in here.", + "reflection.cosmiccore.threshold.9.dialogue.3": "Only power. Only hunger. Only me.", + "reflection.cosmiccore.threshold.9.question": "What do you see when you look at yourself?", + "reflection.cosmiccore.threshold.9.response": "Nothing. Because there's nothing left to see.", + "reflection.cosmiccore.ui.acknowledge": "[I understand]", + "reflection.cosmiccore.ui.available_bargains": "Available Bargains", + "reflection.cosmiccore.ui.back": "[Back]", + "reflection.cosmiccore.ui.browse.interesting_choice": "An interesting choice. Let me show you.", + "reflection.cosmiccore.ui.browse_bargains": "[Browse %s available bargains]", + "reflection.cosmiccore.ui.cancel": "[Cancel]", + "reflection.cosmiccore.ui.click_to_bargain": "Click to bargain", + "reflection.cosmiccore.ui.click_to_defy": "Click to defy (%d erosion)", + "reflection.cosmiccore.ui.confirm_defiance": "[Confirm Defiance]", + "reflection.cosmiccore.ui.constellation_title": "The Constellation of Bargains", + "reflection.cosmiccore.ui.continue": "[Continue]", + "reflection.cosmiccore.ui.cost": "Cost: %d erosion", + "reflection.cosmiccore.ui.defiance": "Defiance", + "reflection.cosmiccore.ui.defiance.cancel": "[No, I've changed my mind]", + "reflection.cosmiccore.ui.defiance.cannot_undo": "This cannot be undone", + "reflection.cosmiccore.ui.defiance.confirm": "[Yes, I defy this bargain]", + "reflection.cosmiccore.ui.defiance.cost_amount": "This will cost you %d erosion", + "reflection.cosmiccore.ui.defiance.lose_power": "You will lose all powers from this bargain", + "reflection.cosmiccore.ui.defiance.question": "You wish to break this bargain?", + "reflection.cosmiccore.ui.defiance.scar_remains": "A scar will remain on your soul forever", + "reflection.cosmiccore.ui.defiance.so_be_it": "So be it. Feel the pain of reclamation.", + "reflection.cosmiccore.ui.defiance.warning1": "You would break the bargain of %s?", + "reflection.cosmiccore.ui.defiance.warning2": "The cost of defiance is %d erosion.", + "reflection.cosmiccore.ui.defiance.warning3": "The power will leave you. The scar will not.", + "reflection.cosmiccore.ui.defiance.warning4": "Are you certain?", + "reflection.cosmiccore.ui.defiance.will_lose": "You will lose: %s", + "reflection.cosmiccore.ui.defiance.wise": "Wise. The power is worth more than your principles.", + "reflection.cosmiccore.ui.defiance_cost": "Defiance will cost %d erosion", + "reflection.cosmiccore.ui.defiance_warning": "Defying a bargain will cost you power but restore some of your soul.", + "reflection.cosmiccore.ui.defy": "Defy", + "reflection.cosmiccore.ui.defy_bargain": "[Defy This Bargain]", + "reflection.cosmiccore.ui.dialogue_continue": "Click to continue...", + "reflection.cosmiccore.ui.drawback": "Drawback", + "reflection.cosmiccore.ui.drawbacks": "Drawbacks:", + "reflection.cosmiccore.ui.enter_defiance": "[Enter Defiance Mode]", + "reflection.cosmiccore.ui.erosion": "erosion", + "reflection.cosmiccore.ui.exit": "[Leave]", + "reflection.cosmiccore.ui.forever_scarred": "Forever Scarred", + "reflection.cosmiccore.ui.gaze_constellation": "[Gaze upon the constellation]", + "reflection.cosmiccore.ui.hub.browse.power": "See what the void offers", + "reflection.cosmiccore.ui.hub.browse.response": "So many choices... so little soul.", + "reflection.cosmiccore.ui.hub.browse.response_empty": "Nothing for you. Yet.", + "reflection.cosmiccore.ui.hub.greeting.erosion_no_bargains.0": "Erosion without bargains? Curious.", + "reflection.cosmiccore.ui.hub.greeting.erosion_no_bargains.1": "Something else has been taking from you.", + "reflection.cosmiccore.ui.hub.greeting.erosion_no_bargains.2": "Perhaps you should let me help instead.", + "reflection.cosmiccore.ui.hub.greeting.fresh.0": "A pristine soul. How rare.", + "reflection.cosmiccore.ui.hub.greeting.fresh.1": "Don't worry. That won't last.", + "reflection.cosmiccore.ui.hub.greeting.has_bargains.0": "Ah, you return. Hungry for more?", + "reflection.cosmiccore.ui.hub.greeting.has_bargains.1": "I have plenty left to offer.", + "reflection.cosmiccore.ui.hub.greeting.has_scars.0": "I see the scars of defiance.", + "reflection.cosmiccore.ui.hub.greeting.has_scars.1": "You took power, then threw it away.", + "reflection.cosmiccore.ui.hub.greeting.has_scars.2": "Was the cost of keeping it too high?", + "reflection.cosmiccore.ui.hub.greeting.many_bargains.0": "Back again. Of course you are.", + "reflection.cosmiccore.ui.hub.greeting.many_bargains.1": "Your soul grows thinner each time.", + "reflection.cosmiccore.ui.hub.greeting.many_bargains_high.0": "Look at you. So much given away.", + "reflection.cosmiccore.ui.hub.greeting.many_bargains_high.1": "Do you even remember what you were?", + "reflection.cosmiccore.ui.hub.greeting.question": "What brings you to my domain?", + "reflection.cosmiccore.ui.hub.leave_response": "Running away? How predictable.", + "reflection.cosmiccore.ui.hub.reflect.power": "Contemplate your existence", + "reflection.cosmiccore.ui.hub.reflect_response": "Gazing into the abyss, are we?", + "reflection.cosmiccore.ui.hub.review.drawback": "Consider defying a bargain", + "reflection.cosmiccore.ui.hub.review.power": "See what you've given away", + "reflection.cosmiccore.ui.hub.review_response": "Let's see what you've become.", + "reflection.cosmiccore.ui.just_look": "[Just... look at yourself]", + "reflection.cosmiccore.ui.leave": "[Leave this place]", + "reflection.cosmiccore.ui.no_available_bargains": "The void has nothing to offer you... for now.", + "reflection.cosmiccore.ui.no_bargains": "No bargains accepted yet.", + "reflection.cosmiccore.ui.of": "of", + "reflection.cosmiccore.ui.power": "Power", + "reflection.cosmiccore.ui.powers": "Powers:", + "reflection.cosmiccore.ui.reflection.extreme_erosion.0": "Almost nothing left. Almost.", + "reflection.cosmiccore.ui.reflection.extreme_erosion.1": "One more push and you're mine completely.", + "reflection.cosmiccore.ui.reflection.has_bargains.0": "I see you've made some... arrangements.", + "reflection.cosmiccore.ui.reflection.has_bargains.1": "Each one a piece of you given away.", + "reflection.cosmiccore.ui.reflection.high_erosion.0": "So much of you is gone now.", + "reflection.cosmiccore.ui.reflection.high_erosion.1": "Do you remember what you were?", + "reflection.cosmiccore.ui.reflection.low_erosion.0": "Just a taste. That's how it starts.", + "reflection.cosmiccore.ui.reflection.low_erosion.1": "You'll be back for more.", + "reflection.cosmiccore.ui.reflection.mid_erosion.0": "Getting comfortable with the darkness?", + "reflection.cosmiccore.ui.reflection.mid_erosion.1": "I can see it settling into you.", + "reflection.cosmiccore.ui.reflection.no_erosion.0": "Untouched. Pure. How boring.", + "reflection.cosmiccore.ui.reflection.no_erosion.1": "You come here with nothing to show?", + "reflection.cosmiccore.ui.reflection.no_erosion.2": "That will change. They always change.", + "reflection.cosmiccore.ui.review_bargains": "[Review your %s bargains]", + "reflection.cosmiccore.ui.scroll_down": "▼ Scroll down", + "reflection.cosmiccore.ui.scroll_up": "▲ Scroll up", + "reflection.cosmiccore.ui.select": "[Select]", + "reflection.cosmiccore.ui.select_to_view": "Select a bargain to view details", + "reflection.cosmiccore.ui.soul_erosion": "Soul Erosion: %d%%", + "reflection.cosmiccore.ui.soul_erosion_display": "Soul Erosion: %s%%", + "reflection.cosmiccore.ui.soul_label": "Soul", + "reflection.cosmiccore.ui.tooltip.no_details": "No additional details", + "reflection.cosmiccore.ui.unlock_cost": "Cost: %d soul erosion", + "reflection.cosmiccore.ui.view_active": "[View Your Bargains]", + "reflection.cosmiccore.ui.view_bargains": "[View Available Bargains]", + "reflection.cosmiccore.ui.void_title": "The Void Between", + "reflection.cosmiccore.ui.your_bargains": "Your Bargains", "tagprefix.alve_foil_insulator": "%s Alve Insulator", "tagprefix.heavy_beam": "Heavy %s Beam", "tagprefix.leached_ore": "Leached %s Ore", diff --git a/src/generated/resources/assets/cosmiccore/models/block/machine/dreamers_basin.json b/src/generated/resources/assets/cosmiccore/models/block/machine/dreamers_basin.json new file mode 100644 index 000000000..074b682fb --- /dev/null +++ b/src/generated/resources/assets/cosmiccore/models/block/machine/dreamers_basin.json @@ -0,0 +1,90 @@ +{ + "parent": "minecraft:block/block", + "loader": "gtceu:machine", + "machine": "cosmiccore:dreamers_basin", + "texture_overrides": { + "all": "gtceu:block/casings/solid/machine_casing_solid_steel" + }, + "variants": { + "is_formed=false,recipe_logic_status=idle": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_solid_steel", + "overlay_front": "gtceu:block/multiblock/implosion_compressor/overlay_front", + "overlay_front_emissive": "gtceu:block/multiblock/implosion_compressor/overlay_front_emissive" + } + } + }, + "is_formed=false,recipe_logic_status=suspend": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_solid_steel", + "overlay_front": "gtceu:block/multiblock/implosion_compressor/overlay_front_paused", + "overlay_front_emissive": "gtceu:block/multiblock/implosion_compressor/overlay_front_paused_emissive" + } + } + }, + "is_formed=false,recipe_logic_status=waiting": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_solid_steel", + "overlay_front": "gtceu:block/multiblock/implosion_compressor/overlay_front_active", + "overlay_front_emissive": "gtceu:block/multiblock/implosion_compressor/overlay_front_active_emissive" + } + } + }, + "is_formed=false,recipe_logic_status=working": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_solid_steel", + "overlay_front": "gtceu:block/multiblock/implosion_compressor/overlay_front_active", + "overlay_front_emissive": "gtceu:block/multiblock/implosion_compressor/overlay_front_active_emissive" + } + } + }, + "is_formed=true,recipe_logic_status=idle": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_solid_steel", + "overlay_front": "gtceu:block/multiblock/implosion_compressor/overlay_front", + "overlay_front_emissive": "gtceu:block/multiblock/implosion_compressor/overlay_front_emissive" + } + } + }, + "is_formed=true,recipe_logic_status=suspend": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_solid_steel", + "overlay_front": "gtceu:block/multiblock/implosion_compressor/overlay_front_paused", + "overlay_front_emissive": "gtceu:block/multiblock/implosion_compressor/overlay_front_paused_emissive" + } + } + }, + "is_formed=true,recipe_logic_status=waiting": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_solid_steel", + "overlay_front": "gtceu:block/multiblock/implosion_compressor/overlay_front_active", + "overlay_front_emissive": "gtceu:block/multiblock/implosion_compressor/overlay_front_active_emissive" + } + } + }, + "is_formed=true,recipe_logic_status=working": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_solid_steel", + "overlay_front": "gtceu:block/multiblock/implosion_compressor/overlay_front_active", + "overlay_front_emissive": "gtceu:block/multiblock/implosion_compressor/overlay_front_active_emissive" + } + } + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/cosmiccore/models/block/machine/star_ladder.json b/src/generated/resources/assets/cosmiccore/models/block/machine/star_ladder.json index 03828b6b0..5887bff95 100644 --- a/src/generated/resources/assets/cosmiccore/models/block/machine/star_ladder.json +++ b/src/generated/resources/assets/cosmiccore/models/block/machine/star_ladder.json @@ -1,5 +1,10 @@ { "parent": "minecraft:block/block", + "dynamic_renders": [ + { + "type": "cosmiccore:star_ladder_render" + } + ], "loader": "gtceu:machine", "machine": "cosmiccore:star_ladder", "texture_overrides": { diff --git a/src/generated/resources/assets/cosmiccore/models/block/machine/stellar_smelting_module.json b/src/generated/resources/assets/cosmiccore/models/block/machine/stellar_smelting_module.json new file mode 100644 index 000000000..6554305c6 --- /dev/null +++ b/src/generated/resources/assets/cosmiccore/models/block/machine/stellar_smelting_module.json @@ -0,0 +1,74 @@ +{ + "parent": "minecraft:block/block", + "loader": "gtceu:machine", + "machine": "cosmiccore:stellar_smelting_module", + "texture_overrides": { + "all": "gtceu:block/casings/gcym/high_temperature_smelting_casing" + }, + "variants": { + "is_formed=false,recipe_logic_status=idle": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/gcym/high_temperature_smelting_casing" + } + } + }, + "is_formed=false,recipe_logic_status=suspend": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/gcym/high_temperature_smelting_casing" + } + } + }, + "is_formed=false,recipe_logic_status=waiting": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/gcym/high_temperature_smelting_casing" + } + } + }, + "is_formed=false,recipe_logic_status=working": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/gcym/high_temperature_smelting_casing" + } + } + }, + "is_formed=true,recipe_logic_status=idle": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/gcym/high_temperature_smelting_casing" + } + } + }, + "is_formed=true,recipe_logic_status=suspend": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/gcym/high_temperature_smelting_casing" + } + } + }, + "is_formed=true,recipe_logic_status=waiting": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/gcym/high_temperature_smelting_casing" + } + } + }, + "is_formed=true,recipe_logic_status=working": { + "model": { + "parent": "gtceu:block/machine/template/cube_all/sided", + "textures": { + "all": "gtceu:block/casings/gcym/high_temperature_smelting_casing" + } + } + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/cosmiccore/models/item/bronze_supply_tank.json b/src/generated/resources/assets/cosmiccore/models/item/bronze_supply_tank.json new file mode 100644 index 000000000..1c469b646 --- /dev/null +++ b/src/generated/resources/assets/cosmiccore/models/item/bronze_supply_tank.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "cosmiccore:item/bronze_supply_tank" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/cosmiccore/models/item/dreamers_basin.json b/src/generated/resources/assets/cosmiccore/models/item/dreamers_basin.json new file mode 100644 index 000000000..d629103f0 --- /dev/null +++ b/src/generated/resources/assets/cosmiccore/models/item/dreamers_basin.json @@ -0,0 +1,3 @@ +{ + "parent": "cosmiccore:block/machine/dreamers_basin" +} \ No newline at end of file diff --git a/src/generated/resources/assets/cosmiccore/models/item/pressurized_rebreather.json b/src/generated/resources/assets/cosmiccore/models/item/pressurized_rebreather.json new file mode 100644 index 000000000..c05af766a --- /dev/null +++ b/src/generated/resources/assets/cosmiccore/models/item/pressurized_rebreather.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "cosmiccore:item/pressurized_rebreather" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/cosmiccore/models/item/reflection_mirror.json b/src/generated/resources/assets/cosmiccore/models/item/reflection_mirror.json new file mode 100644 index 000000000..4b8a6284e --- /dev/null +++ b/src/generated/resources/assets/cosmiccore/models/item/reflection_mirror.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "cosmiccore:item/reflection_mirror" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/cosmiccore/models/item/simple_rebreather.json b/src/generated/resources/assets/cosmiccore/models/item/simple_rebreather.json new file mode 100644 index 000000000..bbf4192bd --- /dev/null +++ b/src/generated/resources/assets/cosmiccore/models/item/simple_rebreather.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "cosmiccore:item/simple_rebreather" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/cosmiccore/models/item/steel_supply_tank.json b/src/generated/resources/assets/cosmiccore/models/item/steel_supply_tank.json new file mode 100644 index 000000000..5ced7268b --- /dev/null +++ b/src/generated/resources/assets/cosmiccore/models/item/steel_supply_tank.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "cosmiccore:item/steel_supply_tank" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/cosmiccore/models/item/stellar_smelting_module.json b/src/generated/resources/assets/cosmiccore/models/item/stellar_smelting_module.json new file mode 100644 index 000000000..1e4379e47 --- /dev/null +++ b/src/generated/resources/assets/cosmiccore/models/item/stellar_smelting_module.json @@ -0,0 +1,3 @@ +{ + "parent": "cosmiccore:block/machine/stellar_smelting_module" +} \ No newline at end of file diff --git a/src/main/java/com/ghostipedia/cosmiccore/CosmicCore.java b/src/main/java/com/ghostipedia/cosmiccore/CosmicCore.java index b7795432f..cb3c052fd 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/CosmicCore.java +++ b/src/main/java/com/ghostipedia/cosmiccore/CosmicCore.java @@ -7,6 +7,8 @@ import com.ghostipedia.cosmiccore.api.recipe.lookup.MapSoulIngredient; import com.ghostipedia.cosmiccore.api.registries.CosmicRegistration; import com.ghostipedia.cosmiccore.client.CosmicCoreClient; +import com.ghostipedia.cosmiccore.common.airControl.OxygenItemCap; +import com.ghostipedia.cosmiccore.common.airControl.OxygenRules; import com.ghostipedia.cosmiccore.common.data.*; import com.ghostipedia.cosmiccore.common.data.materials.CosmicMaterialSet; import com.ghostipedia.cosmiccore.common.data.materials.CosmicMaterials; @@ -14,6 +16,8 @@ import com.ghostipedia.cosmiccore.common.machine.multiblock.multi.modular.MultiblockInit; import com.ghostipedia.cosmiccore.common.network.CCoreNetwork; import com.ghostipedia.cosmiccore.common.recipe.condition.CosmicConditions; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.bargain.CosmicBargains; import com.ghostipedia.cosmiccore.gtbridge.CosmicRecipeTypes; import com.gregtechceu.gtceu.api.GTCEuAPI; @@ -80,6 +84,8 @@ public static void init() { CosmicCoreDatagen.init(); CosmicPredicates.init(); CosmicMaterialSet.init(); + // Register bargains early so they're available on both client and server + CosmicBargains.init(); } public static ResourceLocation id(String path) { @@ -108,6 +114,7 @@ public void commonSetup(FMLCommonSetupEvent event) { MapIngredientTypeManager.registerMapIngredient(Double.class, MapEmberIngredient::convertToMapIngredient); GridLinkables.register(CosmicItems.LINKED_TERMINAL, LinkedTerminalBehavior.handler); CCoreNetwork.init(); + OxygenRules.registerAirRanges(); }); } @@ -142,5 +149,7 @@ public void registerSounds(GTCEuAPI.RegisterEvent @SubscribeEvent public void registerCapabilities(RegisterCapabilitiesEvent event) { CosmicCapabilities.register(event); + OxygenItemCap.onRegisterCaps(event); + ReflectionCapability.registerCaps(event); } } diff --git a/src/main/java/com/ghostipedia/cosmiccore/api/item/armor/ISpaceSuite.java b/src/main/java/com/ghostipedia/cosmiccore/api/item/armor/ISpaceSuite.java index 5ad15959a..d9b5ab06c 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/api/item/armor/ISpaceSuite.java +++ b/src/main/java/com/ghostipedia/cosmiccore/api/item/armor/ISpaceSuite.java @@ -1,9 +1,9 @@ package com.ghostipedia.cosmiccore.api.item.armor; +import com.ghostipedia.cosmiccore.common.airControl.IOxygenProvider; import com.ghostipedia.cosmiccore.common.data.tag.item.CosmicItemTags; import net.minecraft.network.chat.Component; -import net.minecraft.tags.FluidTags; import net.minecraft.tags.TagKey; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; @@ -12,7 +12,6 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; -import earth.terrarium.adastra.api.systems.OxygenApi; import earth.terrarium.adastra.common.constants.ConstantComponents; import earth.terrarium.adastra.common.registry.ModFluids; import earth.terrarium.adastra.common.utils.FluidUtils; @@ -22,20 +21,53 @@ import java.util.List; import java.util.stream.StreamSupport; -public interface ISpaceSuite { +/** + * Interface for CosmicCore space suits that integrate with our oxygen system. + * Oxygen consumption for breathing is now handled by OxygenLogic - this interface + * provides the oxygen and marks items as oxygen providers. + */ +public interface ISpaceSuite extends IOxygenProvider { - default void tickOxygen(Level Level, Player player, ItemStack itemStack) { - if (Level.isClientSide) return; + /** + * Tick handler for space suit - handles freezing prevention only. + * Oxygen consumption is handled by OxygenLogic via IOxygenProvider. + */ + default void tickOxygen(Level level, Player player, ItemStack itemStack) { + if (level.isClientSide) return; if (player.isCreative() || player.isSpectator()) return; - if (!(itemStack.getItem() instanceof SpaceArmorComponentItem suit)) return; + if (!(itemStack.getItem() instanceof SpaceArmorComponentItem)) return; + // Prevent freezing while wearing space suit player.setTicksFrozen(0); - if (player.tickCount % 12 == 0 && suit.hasOxygen(player)) { - if (!OxygenApi.API.hasOxygen(player)) suit.consumeOxygen(itemStack, 1); - if (player.isEyeInFluid(FluidTags.WATER)) { - suit.consumeOxygen(itemStack, 1); - player.setAirSupply(Math.min(player.getMaxAirSupply(), player.getAirSupply() + 4 * 10)); - } - } + // NOTE: Oxygen consumption is now handled by OxygenLogic.drainFromOxygenProviders() + } + + // --- IOxygenProvider implementation --- + + @Override + default boolean hasOxygen(ItemStack stack, Player player) { + if (!(stack.getItem() instanceof SpaceArmorComponentItem suit)) return false; + return suit.hasOxygen(player); + } + + @Override + default long consumeOxygen(ItemStack stack, Player player, long amount) { + if (!(stack.getItem() instanceof SpaceArmorComponentItem suit)) return 0; + long before = suit.getFluidContainer(stack).getFirstFluid().getFluidAmount(); + suit.consumeOxygen(stack, amount); + long after = suit.getFluidContainer(stack).getFirstFluid().getFluidAmount(); + return before - after; + } + + @Override + default long getOxygenAmount(ItemStack stack) { + if (!(stack.getItem() instanceof SpaceArmorComponentItem suit)) return 0; + return suit.getFluidContainer(stack).getFirstFluid().getFluidAmount(); + } + + @Override + default long getMaxOxygenCapacity(ItemStack stack) { + if (!(stack.getItem() instanceof SpaceArmorComponentItem suit)) return 0; + return suit.getFluidContainer(stack).getTankCapacity(0); } static boolean hasFullNanoSet(LivingEntity entity) { diff --git a/src/main/java/com/ghostipedia/cosmiccore/api/machine/feature/IStellarIrisProvider.java b/src/main/java/com/ghostipedia/cosmiccore/api/machine/feature/IStellarIrisProvider.java new file mode 100644 index 000000000..6ec49974a --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/api/machine/feature/IStellarIrisProvider.java @@ -0,0 +1,54 @@ +package com.ghostipedia.cosmiccore.api.machine.feature; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine; + +import com.gregtechceu.gtceu.api.machine.feature.IMachineFeature; + +/** + * Interface for the Stellar Iris controller. + * Modules query this to get processing parameters and stage information. + */ +public interface IStellarIrisProvider extends IMachineFeature { + + /** + * @return the current stage of the stellar iris + */ + IrisMultiblockMachine.Stage getStage(); + + /** + * @return whether the iris multiblock is formed + */ + boolean isFormed(); + + /** + * @return maximum heat provided to modules (affects recipe availability) + */ + int getMaxHeat(); + + /** + * @return speed bonus multiplier for module recipes + */ + double getSpeedBonus(); + + /** + * @return energy discount multiplier for module recipes (1.0 = no discount) + */ + double getEnergyDiscount(); + + /** + * @return maximum parallel recipes for modules + */ + int getParallelLimit(); + + /** + * Check if the stage allows processing + * + * @return true if the current stage can run module recipes + */ + default boolean canProcess() { + IrisMultiblockMachine.Stage stage = getStage(); + return stage == IrisMultiblockMachine.Stage.STAR || + stage == IrisMultiblockMachine.Stage.SUPERSTAR || + stage == IrisMultiblockMachine.Stage.BLACK_HOLE; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/api/machine/feature/IStellarModuleReceiver.java b/src/main/java/com/ghostipedia/cosmiccore/api/machine/feature/IStellarModuleReceiver.java new file mode 100644 index 000000000..ccfba4072 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/api/machine/feature/IStellarModuleReceiver.java @@ -0,0 +1,32 @@ +package com.ghostipedia.cosmiccore.api.machine.feature; + +import org.jetbrains.annotations.Nullable; + +/** + * Interface for Stellar Iris modules. + * Modules receive connection from the Iris controller and processing parameters. + */ +public interface IStellarModuleReceiver { + + /** + * @return the stellar iris this module is connected to, or null if not connected + */ + @Nullable + IStellarIrisProvider getStellarIris(); + + /** + * Sets the stellar iris connection for this module. + * Called by the Iris controller when structure forms/invalidates. + * + * @param provider the iris provider to connect to, or null to disconnect + */ + void setStellarIris(@Nullable IStellarIrisProvider provider); + + /** + * @return true if this module is connected to a valid, formed Iris + */ + default boolean isConnectedToIris() { + IStellarIrisProvider iris = getStellarIris(); + return iris != null && iris.isFormed(); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/DreamersBasinMachine.java b/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/DreamersBasinMachine.java new file mode 100644 index 000000000..7d26735ab --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/DreamersBasinMachine.java @@ -0,0 +1,380 @@ +package com.ghostipedia.cosmiccore.api.machine.multiblock; + +import com.ghostipedia.cosmiccore.common.machine.multiblock.multi.logic.MultithreadedMachine; +import com.ghostipedia.cosmiccore.common.machine.multiblock.multi.logic.MultithreadedRecipeLogic; + +import com.gregtechceu.gtceu.api.GTValues; +import com.gregtechceu.gtceu.api.capability.recipe.FluidRecipeCapability; +import com.gregtechceu.gtceu.api.capability.recipe.ItemRecipeCapability; +import com.gregtechceu.gtceu.api.gui.GuiTextures; +import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; +import com.gregtechceu.gtceu.api.machine.feature.multiblock.IDisplayUIMachine; +import com.gregtechceu.gtceu.api.machine.multiblock.MultiblockDisplayText; +import com.gregtechceu.gtceu.api.recipe.GTRecipe; +import com.gregtechceu.gtceu.api.recipe.content.Content; +import com.gregtechceu.gtceu.utils.FormattingUtil; +import com.gregtechceu.gtceu.utils.GTUtil; + +import com.lowdragmc.lowdraglib.gui.widget.*; +import com.lowdragmc.lowdraglib.side.fluid.FluidStack; +import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * The Dreamer's Basin Machine - A multithreaded processing machine with custom UI. + *

+ * This machine extends MultithreadedMachine and provides a rich UI that displays: + * - Thread status with color-coded indicators + * - Per-thread recipe progress bars + * - Current recipe information for each thread + * - Energy consumption breakdown + * - Overclock levels per thread + */ +public class DreamersBasinMachine extends MultithreadedMachine implements IDisplayUIMachine { + + protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( + DreamersBasinMachine.class, MultithreadedMachine.MANAGED_FIELD_HOLDER); + + public DreamersBasinMachine(IMachineBlockEntity holder) { + super(holder); + } + + @Override + @NotNull + public ManagedFieldHolder getFieldHolder() { + return MANAGED_FIELD_HOLDER; + } + + // ===== Custom UI Implementation ===== + + @Override + public Widget createUIWidget() { + var group = new WidgetGroup(0, 0, 256, 200); + + // Main scrollable area + var scrollable = new DraggableScrollableWidgetGroup(4, 4, 248, 192) + .setBackground(getScreenTexture()); + + // Title + scrollable.addWidget(new LabelWidget(4, 5, self().getBlockState().getBlock().getDescriptionId())); + + // Component panel for dynamic text (status, energy, etc.) + scrollable.addWidget(new ComponentPanelWidget(4, 17, this::addDisplayText) + .textSupplier(this.getLevel().isClientSide ? null : this::addDisplayText) + .setMaxWidthLimit(240) + .clickHandler(this::handleDisplayClick)); + + group.addWidget(scrollable); + group.setBackground(GuiTextures.BACKGROUND_INVERSE); + + return group; + } + + @Override + public void addDisplayText(List textList) { + // Use MultiblockDisplayText builder for consistent formatting + var builder = MultiblockDisplayText.builder(textList, isFormed()) + .setWorkingStatus(isWorkingEnabled(), getRunningThreadCount() > 0); + + if (isFormed()) { + // Energy info first + builder.addEnergyUsageLine(energyContainer); + builder.addEnergyTierLine(tier); + + // Separator + builder.addCustom(tl -> tl.add(Component.empty())); + + // Thread Status Header + builder.addCustom(tl -> { + tl.add(Component.translatable("cosmiccore.machine.dreamers_basin.thread_header") + .withStyle(ChatFormatting.AQUA, ChatFormatting.BOLD)); + + // Summary line + int running = getRunningThreadCount(); + int total = getThreadLogics().size(); + int max = getMaxThreads(); + + MutableComponent summary = Component.literal(" ") + .append(Component.translatable("cosmiccore.machine.dreamers_basin.threads_summary", + running, total, max)); + + if (running == total && total > 0) { + summary = summary.withStyle(ChatFormatting.GREEN); + } else if (running > 0) { + summary = summary.withStyle(ChatFormatting.YELLOW); + } else { + summary = summary.withStyle(ChatFormatting.GRAY); + } + tl.add(summary); + }); + + // Per-thread detailed status + builder.addCustom(tl -> { + tl.add(Component.empty()); + + for (MultithreadedRecipeLogic logic : getThreadLogics().values()) { + addThreadStatusLine(tl, logic); + } + }); + + // EU Budget info + builder.addCustom(tl -> { + tl.add(Component.empty()); + tl.add(Component.translatable("cosmiccore.machine.dreamers_basin.eu_budget_header") + .withStyle(ChatFormatting.GOLD)); + + if (!getThreadLogics().isEmpty()) { + MultithreadedRecipeLogic firstThread = getThreadLogics().values().iterator().next(); + long euPerThread = firstThread.getMaxEUtPerThread(); + int voltageTier = GTUtil.getFloorTierByVoltage(euPerThread); + String tierName = GTValues.VNF[voltageTier]; + + tl.add(Component.literal(" ") + .append(Component.translatable("cosmiccore.machine.dreamers_basin.eu_per_thread", + FormattingUtil.formatNumbers(euPerThread), tierName)) + .withStyle(ChatFormatting.GRAY)); + } + }); + } + + // Additional display from definition + getDefinition().getAdditionalDisplay().accept(this, textList); + } + + /** + * Add a detailed status line for a single thread. + */ + private void addThreadStatusLine(List textList, MultithreadedRecipeLogic logic) { + int color = logic.getThreadColor(); + String colorName = getColorDisplayName(color); + ChatFormatting colorFormat = getColorChatFormatting(color); + + // Build the thread status line + MutableComponent line = Component.literal(" "); + + // Color indicator [COLOR] + line.append(Component.literal("[" + colorName + "] ").withStyle(colorFormat)); + + if (logic.isWorking()) { + // Thread is actively processing + GTRecipe recipe = logic.getCurrentRecipe(); + int progress = logic.getProgress(); + int duration = logic.getDuration(); + int percent = duration > 0 ? (progress * 100 / duration) : 0; + + // Progress bar visualization + String progressBar = createProgressBar(percent); + + // Build hover tooltip with recipe details + Component hoverTooltip = buildRecipeTooltip(recipe, duration); + + // Create the progress portion with hover event + MutableComponent progressComponent = Component.literal(progressBar + " ") + .withStyle(Style.EMPTY + .withColor(ChatFormatting.GREEN) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hoverTooltip))); + + line.append(progressComponent); + line.append(Component.literal(percent + "%").withStyle(ChatFormatting.WHITE)); + + // Recipe EU/t info + if (recipe != null) { + long recipeEUt = recipe.getInputEUt().getTotalEU(); + if (recipeEUt > 0) { + line.append(Component.literal(" (") + .append(Component.literal(FormattingUtil.formatNumbers(recipeEUt) + " EU/t") + .withStyle(ChatFormatting.YELLOW)) + .append(Component.literal(")"))); + } + } + + textList.add(line); + + // Add time remaining on next line (also with hover) + if (duration > 0) { + int ticksRemaining = duration - progress; + float secondsRemaining = ticksRemaining / 20.0f; + MutableComponent timeLine = Component.literal(" ") + .append(Component.translatable("cosmiccore.machine.dreamers_basin.time_remaining", + String.format("%.1fs", secondsRemaining)) + .withStyle(Style.EMPTY + .withColor(ChatFormatting.DARK_GRAY) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hoverTooltip)))); + textList.add(timeLine); + } + + } else if (logic.isIdle()) { + line.append(Component.translatable("cosmiccore.machine.dreamers_basin.status_idle") + .withStyle(ChatFormatting.GRAY)); + textList.add(line); + + } else if (logic.isWaiting()) { + line.append(Component.translatable("cosmiccore.machine.dreamers_basin.status_waiting") + .withStyle(ChatFormatting.YELLOW)); + textList.add(line); + + } else if (logic.isSuspend()) { + line.append(Component.translatable("cosmiccore.machine.dreamers_basin.status_suspended") + .withStyle(ChatFormatting.RED)); + textList.add(line); + + } else { + line.append(Component.translatable("cosmiccore.machine.dreamers_basin.status_unknown") + .withStyle(ChatFormatting.DARK_GRAY)); + textList.add(line); + } + } + + /** + * Build a hover tooltip showing the first recipe output and production rate. + * Due to Minecraft hover event limitations, this is kept to a single line. + */ + private Component buildRecipeTooltip(GTRecipe recipe, int duration) { + if (recipe == null) { + return Component.translatable("cosmiccore.machine.dreamers_basin.tooltip.no_recipe"); + } + + // Calculate rate (items per second) + float recipesPerSecond = duration > 0 ? 20.0f / duration : 0; + + MutableComponent tooltip = Component.translatable("cosmiccore.machine.dreamers_basin.tooltip.crafting") + .withStyle(ChatFormatting.GOLD) + .append(Component.literal(" ").withStyle(ChatFormatting.RESET)); + + // Try to find first item output + // Note: Content stores Ingredient (usually SizedIngredient), not raw ItemStack + List itemOutputs = recipe.getOutputContents(ItemRecipeCapability.CAP); + if (itemOutputs != null && !itemOutputs.isEmpty()) { + for (Content content : itemOutputs) { + Object contentObj = content.getContent(); + if (contentObj instanceof Ingredient ingredient) { + ItemStack[] items = ingredient.getItems(); + if (items.length > 0 && !items[0].isEmpty()) { + ItemStack stack = items[0]; + int count = stack.getCount(); + float perSecond = count * recipesPerSecond; + + tooltip.append(stack.getHoverName().copy().withStyle(ChatFormatting.WHITE)) + .append(Component.literal(" x" + count).withStyle(ChatFormatting.GRAY)); + + if (perSecond >= 0.1f) { + tooltip.append(Component.literal(String.format(" (%.1f/s)", perSecond)) + .withStyle(ChatFormatting.AQUA)); + } + return tooltip; + } + } + } + } + + // Try fluid outputs if no items + List fluidOutputs = recipe.getOutputContents(FluidRecipeCapability.CAP); + if (fluidOutputs != null && !fluidOutputs.isEmpty()) { + for (Content content : fluidOutputs) { + Object contentObj = content.getContent(); + if (contentObj instanceof FluidStack fluid && !fluid.isEmpty()) { + long amount = fluid.getAmount(); + float perSecond = amount * recipesPerSecond; + + tooltip.append(fluid.getDisplayName().copy().withStyle(ChatFormatting.BLUE)) + .append(Component.literal(" " + FormattingUtil.formatNumbers(amount) + "mB") + .withStyle(ChatFormatting.GRAY)); + + if (perSecond >= 1f) { + tooltip.append(Component.literal(String.format(" (%.0f mB/s)", perSecond)) + .withStyle(ChatFormatting.AQUA)); + } + return tooltip; + } + } + } + + // Generic fallback + return tooltip.append(Component.translatable("cosmiccore.machine.dreamers_basin.tooltip.processing") + .withStyle(ChatFormatting.GRAY)); + } + + /** + * Create a simple text-based progress bar. + */ + private String createProgressBar(int percent) { + int filled = percent / 10; + int empty = 10 - filled; + return "[" + "=".repeat(filled) + "-".repeat(empty) + "]"; + } + + /** + * Get a display-friendly color name. + * GTCEu stores painting color as dye.getMapColor().col + */ + private String getColorDisplayName(int color) { + if (color == -1) return "Default"; + + for (DyeColor dye : DyeColor.values()) { + // GTCEu uses getMapColor().col for painted colors + if (dye.getMapColor().col == color) { + // Capitalize first letter of each word + String name = dye.getName().replace("_", " "); + StringBuilder result = new StringBuilder(); + for (String word : name.split(" ")) { + if (!result.isEmpty()) result.append(" "); + result.append(word.substring(0, 1).toUpperCase()).append(word.substring(1)); + } + return result.toString(); + } + } + return "Custom"; + } + + /** + * Get the ChatFormatting color that best matches the thread color. + * GTCEu stores painting color as dye.getMapColor().col + */ + private ChatFormatting getColorChatFormatting(int color) { + if (color == -1) return ChatFormatting.WHITE; + + for (DyeColor dye : DyeColor.values()) { + // GTCEu uses getMapColor().col for painted colors + if (dye.getMapColor().col == color) { + return switch (dye) { + case WHITE -> ChatFormatting.WHITE; + case ORANGE -> ChatFormatting.GOLD; + case MAGENTA -> ChatFormatting.LIGHT_PURPLE; + case LIGHT_BLUE -> ChatFormatting.AQUA; + case YELLOW -> ChatFormatting.YELLOW; + case LIME -> ChatFormatting.GREEN; + case PINK -> ChatFormatting.LIGHT_PURPLE; + case GRAY -> ChatFormatting.DARK_GRAY; + case LIGHT_GRAY -> ChatFormatting.GRAY; + case CYAN -> ChatFormatting.DARK_AQUA; + case PURPLE -> ChatFormatting.DARK_PURPLE; + case BLUE -> ChatFormatting.BLUE; + case BROWN -> ChatFormatting.GOLD; + case GREEN -> ChatFormatting.DARK_GREEN; + case RED -> ChatFormatting.RED; + case BLACK -> ChatFormatting.DARK_GRAY; + }; + } + } + return ChatFormatting.WHITE; + } + + /** + * Get all thread logics for iteration. + */ + public Iterable getThreadLogicsIterable() { + return getThreadLogics().values(); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/IMultithreadedMachine.java b/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/IMultithreadedMachine.java new file mode 100644 index 000000000..b84c5ead6 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/IMultithreadedMachine.java @@ -0,0 +1,33 @@ +package com.ghostipedia.cosmiccore.api.machine.multiblock; + +import com.ghostipedia.cosmiccore.common.machine.multiblock.multi.logic.MultithreadedRecipeLogic; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; + +/** + * Interface for machines that can run multiple independent recipe threads. + */ +public interface IMultithreadedMachine { + + /** + * Get the map of thread color to recipe logic. + */ + Int2ObjectMap getThreadLogicsMap(); + + /** + * Get the maximum number of threads this machine can support. + * Determined by energy hatch amperage. + */ + int getMaxThreadCount(); + + /** + * Get the current number of configured threads. + * Limited by available color-coded input buses. + */ + int getCurrentThreadCount(); + + /** + * Get the number of threads currently running recipes. + */ + int getRunningThreadCount(); +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/IrisMultiblockMachine.java b/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/IrisMultiblockMachine.java index 2c4b71254..533178c60 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/IrisMultiblockMachine.java +++ b/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/IrisMultiblockMachine.java @@ -1,11 +1,13 @@ package com.ghostipedia.cosmiccore.api.machine.multiblock; +import com.ghostipedia.cosmiccore.api.machine.feature.IStellarIrisProvider; +import com.ghostipedia.cosmiccore.api.machine.feature.IStellarModuleReceiver; +import com.ghostipedia.cosmiccore.client.gui.widget.stellar.StellarFancyUIWidget; +import com.ghostipedia.cosmiccore.client.gui.widget.stellar.StellarIrisWidget; +import com.ghostipedia.cosmiccore.common.data.CosmicItems; import com.ghostipedia.cosmiccore.common.data.CosmicSounds; import com.gregtechceu.gtceu.api.capability.recipe.IO; -import com.gregtechceu.gtceu.api.gui.GuiTextures; -import com.gregtechceu.gtceu.api.gui.fancy.FancyMachineUIWidget; -import com.gregtechceu.gtceu.api.gui.widget.SlotWidget; import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; import com.gregtechceu.gtceu.api.machine.MetaMachine; import com.gregtechceu.gtceu.api.machine.multiblock.WorkableElectricMultiblockMachine; @@ -15,16 +17,16 @@ import com.gregtechceu.gtceu.api.sound.AutoReleasedSound; import com.lowdragmc.lowdraglib.gui.modular.ModularUI; -import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; -import com.lowdragmc.lowdraglib.gui.texture.TextTexture; -import com.lowdragmc.lowdraglib.gui.widget.*; +import com.lowdragmc.lowdraglib.gui.widget.Widget; import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; import com.lowdragmc.lowdraglib.syncdata.annotation.UpdateListener; import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -32,13 +34,16 @@ import lombok.Setter; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Set; import static com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage.BLACK_HOLE; import static com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage.DEATH; @Getter -public class IrisMultiblockMachine extends WorkableElectricMultiblockMachine { +public class IrisMultiblockMachine extends WorkableElectricMultiblockMachine implements IStellarIrisProvider { public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( IrisMultiblockMachine.class, WorkableElectricMultiblockMachine.MANAGED_FIELD_HOLDER); @@ -53,12 +58,69 @@ public class IrisMultiblockMachine extends WorkableElectricMultiblockMachine { protected boolean isFuelable; protected Object workingSound; - @Setter @Persisted @DescSynced - @UpdateListener(methodName = "onStatusSynced") + @UpdateListener(methodName = "onStageSynced") private Stage stage = Stage.EMPTY; + /** + * Called when the stage field is synced from server to client. + * Parameters must match the field type (Stage). + */ + @OnlyIn(Dist.CLIENT) + @SuppressWarnings("unused") + protected void onStageSynced(Stage newValue, Stage oldValue) { + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.warn( + "[IrisMultiblockMachine] CLIENT onStageSynced: {} -> {}", oldValue, newValue); + this.scheduleRenderUpdate(); + soundTick(); + } + + /** + * Custom setter with debug logging to track stage changes. + */ + public void setStage(Stage newStage) { + Stage oldStage = this.stage; + this.stage = newStage; + // Debug: log all stage changes with stack trace for unusual transitions + if (oldStage != newStage) { + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.warn( + "[IrisMultiblockMachine] setStage: {} -> {}", oldStage, newStage); + // If transitioning TO DEATH from EMPTY, log stack trace to find the culprit + if (oldStage == Stage.EMPTY && newStage == Stage.DEATH) { + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.warn( + "[IrisMultiblockMachine] SUSPICIOUS: EMPTY->DEATH transition! Stack trace:", + new Exception("Stack trace")); + } + } + } + + /** + * Custom star color (RGB, no alpha). -1 means use default stage-based color. + * Persisted and synced to client for rendering. + */ + @Setter + @Persisted + @DescSynced + private int customStarColor = -1; + + @Persisted + @DescSynced + private int prestigePoints = 0; + + @Persisted + @DescSynced + private int prestigeTier = 0; + + @DescSynced + private boolean prestigeAnimationActive = false; + + @DescSynced + private int lastPrestigePointsEarned = 0; + + private List connectedModules = new ArrayList<>(); + private List moduleSlotPositions = new ArrayList<>(); + public enum Stage { EMPTY, GROWING, @@ -72,6 +134,9 @@ public enum Stage { public IrisMultiblockMachine(IMachineBlockEntity holder) { super(holder); this.inventory = new NotifiableItemStackHandler(this, 1, IO.NONE, IO.BOTH); + // Debug: log initial stage + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.warn( + "[IrisMultiblockMachine] Constructor: initial stage={}", stage); } @OnlyIn(Dist.CLIENT) @@ -89,6 +154,153 @@ protected void onStatusSynced(RecipeLogic.Status newValue, RecipeLogic.Status ol @Override public void onStructureFormed() { super.onStructureFormed(); + + // Clear old module connections + if (connectedModules != null) { + connectedModules.forEach(m -> m.setStellarIris(null)); + } + + // Get modules found during structure check (from moduleSlotPredicate) + Set modules = getMultiblockState().getMatchContext() + .getOrDefault("stellarModules", Collections.emptySet()); + + this.connectedModules = new ArrayList<>(modules); + + // Establish connections - tell each module about this Iris + for (IStellarModuleReceiver module : connectedModules) { + module.setStellarIris(this); + } + } + + /** + * Registers a module with this Iris. Called by modules when they form or detect a nearby Iris. + * + * @param module The module to register + * @return true if registration was successful + */ + public boolean registerModule(IStellarModuleReceiver module) { + if (!isFormed() || module == null) return false; + + if (!connectedModules.contains(module)) { + connectedModules.add(module); + module.setStellarIris(this); + + // Store position for tracking + if (module instanceof MetaMachine metaMachine) { + moduleSlotPositions.add(metaMachine.getPos().immutable()); + // Debug logging + if (getLevel() != null && !getLevel().isClientSide) { + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIris] Module registered: {} at {}. Total modules: {}", + metaMachine.getBlockState().getBlock().getDescriptionId(), + metaMachine.getPos(), + connectedModules.size()); + } + } + return true; + } + return false; + } + + /** + * Unregisters a module from this Iris. Called when a module is broken or invalidated. + * + * @param module The module to unregister + */ + public void unregisterModule(IStellarModuleReceiver module) { + if (module == null) return; + + if (connectedModules.remove(module)) { + module.setStellarIris(null); + + // Remove position tracking + if (module instanceof MetaMachine metaMachine) { + moduleSlotPositions.remove(metaMachine.getPos()); + } + } + } + + /** + * Rescans for modules by triggering a structure recheck. + * This is a heavier operation but ensures consistency. + */ + public void rescanModules() { + if (!isFormed()) return; + + // Trigger structure recheck which will re-run the predicates + getMultiblockState().setError(null); + checkPatternWithLock(); + } + + @Override + public void onStructureInvalid() { + super.onStructureInvalid(); + + // Sever all module connections + if (connectedModules != null) { + connectedModules.forEach(m -> m.setStellarIris(null)); + connectedModules.clear(); + } + } + + // -------- IStellarIrisProvider Implementation -------- + + @Override + public int getMaxHeat() { + // Base heat increases with stage + return switch (stage) { + case STAR -> 3600; + case SUPERSTAR -> 7200; + case BLACK_HOLE -> 12000; + default -> 0; + }; + } + + @Override + public double getSpeedBonus() { + // Speed multiplier based on stage + return switch (stage) { + case STAR -> 1.0; + case SUPERSTAR -> 1.5; + case BLACK_HOLE -> 2.0; + default -> 0.0; + }; + } + + @Override + public double getEnergyDiscount() { + // Energy discount based on stage (1.0 = no discount, lower = cheaper) + return switch (stage) { + case STAR -> 1.0; + case SUPERSTAR -> 0.8; + case BLACK_HOLE -> 0.6; + default -> 1.0; + }; + } + + @Override + public int getParallelLimit() { + // Parallel limit based on stage + return switch (stage) { + case STAR -> 4; + case SUPERSTAR -> 8; + case BLACK_HOLE -> 16; + default -> 0; + }; + } + + /** + * @return list of currently connected modules (read-only view) + */ + public List getConnectedModules() { + return Collections.unmodifiableList(connectedModules); + } + + /** + * @return number of connected modules + */ + public int getConnectedModuleCount() { + return connectedModules.size(); } public void setStarStage() { @@ -97,6 +309,157 @@ public void setStarStage() { setStage(values[nextVal]); } + // -------- Prestige System Methods -------- + + /** + * Checks if the item in the star seed slot is a prestige item (Programmable Mote). + * + * @return true if a prestige item is in the slot + */ + public boolean hasPrestigeItem() { + ItemStack stack = inventory.getStackInSlot(0); + return !stack.isEmpty() && stack.is(CosmicItems.PROGRAMMABLE_MOTE.asItem()); + } + + /** + * Checks if the iris has an active star (not EMPTY or DEATH states). + * Required for prestige to be triggered. + * + * @return true if there's an active star to consume + */ + public boolean hasActiveStar() { + return stage != Stage.EMPTY && stage != Stage.DEATH && stage != Stage.DEATH_GRACEFUL; + } + + /** + * Called when the prestige button is broken (after 3 cracks). + * Starts the prestige animation sequence - star consumption happens over time. + * The actual point award and stage reset happen when animation completes. + */ + public void triggerPrestige() { + if (!hasPrestigeItem() || !hasActiveStar()) { + return; + } + + // Consume the prestige item immediately + inventory.getStackInSlot(0).shrink(1); + + // Calculate points to award (50 per star for now) + lastPrestigePointsEarned = calculatePrestigePoints(); + + // Log the prestige event + if (getLevel() != null && !getLevel().isClientSide) { + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIris] PRESTIGE TRIGGERED at {} - Stage: {}, Points: {}", + getPos(), stage, lastPrestigePointsEarned); + } + + // Start the animation - UI will handle the visual sequence + // Animation duration: 5s shrink + 3s fade = 8s total (160 ticks) + prestigeAnimationActive = true; + + // Note: Stage reset and point award happen when completePrestige() is called + // This is triggered by the UI after animation completes + } + + /** + * Called when the prestige animation completes (after ~8 seconds). + * Awards points and resets the star. + */ + public void completePrestige() { + if (!prestigeAnimationActive) return; + + // Award the points + prestigePoints += lastPrestigePointsEarned; + + // Check for tier advancement + int newTier = calculatePrestigeTier(prestigePoints); + if (newTier > prestigeTier) { + prestigeTier = newTier; + if (getLevel() != null && !getLevel().isClientSide) { + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIris] PRESTIGE TIER UP! Now tier {} with {} total points", + prestigeTier, prestigePoints); + } + } + + // Reset the star + setStage(Stage.EMPTY); + + // End animation state + prestigeAnimationActive = false; + + if (getLevel() != null && !getLevel().isClientSide) { + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIris] Prestige complete. Total points: {}, Tier: {}", + prestigePoints, prestigeTier); + } + } + + /** + * Calculates prestige points for consuming the current star. + * For now, flat 50 points per star (will be expanded based on star seed type later). + * + * @return points to award + */ + private int calculatePrestigePoints() { + // Base: 50 points per consumed star + // Future: multiply by star seed quality, stage bonuses, etc. + return 50; + } + + /** + * Determines prestige tier based on total accumulated points. + * Tiers unlock upgrades and recipe access. + * + * @param totalPoints total prestige points + * @return tier level (0 = base, higher = more unlocks) + */ + private int calculatePrestigeTier(int totalPoints) { + // Tier thresholds (can be adjusted for balance) + if (totalPoints >= 1000) return 5; + if (totalPoints >= 500) return 4; + if (totalPoints >= 250) return 3; + if (totalPoints >= 100) return 2; + if (totalPoints >= 50) return 1; + return 0; + } + + /** + * @return current prestige points + */ + public int getPrestigePoints() { + return prestigePoints; + } + + /** + * @return current prestige tier + */ + public int getPrestigeTier() { + return prestigeTier; + } + + /** + * @return true if prestige animation is currently playing + */ + public boolean isPrestigeAnimationActive() { + return prestigeAnimationActive; + } + + /** + * @return points earned in most recent prestige (for animation display) + */ + public int getLastPrestigePointsEarned() { + return lastPrestigePointsEarned; + } + + /** + * Manually set prestige animation state (for client sync). + */ + public void setPrestigeAnimationActive(boolean active) { + this.prestigeAnimationActive = active; + } + @Override public void clientTick() { super.clientTick(); @@ -139,26 +502,7 @@ public void soundTick() { @Override public Widget createUIWidget() { - var group = new WidgetGroup(0, 0, 182 + 8, 117 + 8); - group.addWidget(new DraggableScrollableWidgetGroup(4, 4, 182, 117).setBackground(getScreenTexture()) - .addWidget(new LabelWidget(4, 5, self().getBlockState().getBlock().getDescriptionId())) - .addWidget(new ComponentPanelWidget(4, 17, this::addDisplayText) - .textSupplier(this.getLevel().isClientSide ? null : this::addDisplayText) - .setMaxWidthLimit(150) - .clickHandler(this::handleDisplayClick))); - group.addWidget(new SlotWidget(inventory.storage, 0, 7, 101, true, true) - .setBackground(GuiTextures.SLOT, GuiTextures.ATOMIC_OVERLAY_1)); - group.setBackground(GuiTextures.BACKGROUND_INVERSE); - group.addWidget(new ButtonWidget( - 27, - 100, - 158, - 20, - new GuiTextureGroup( - GuiTextures.BUTTON, - new TextTexture("Change Stage")), - clickData -> setStarStage())); - return group; + return new StellarIrisWidget(() -> this); } @Override @@ -171,6 +515,7 @@ public void addDisplayText(List textList) { @Override public ModularUI createUI(Player entityPlayer) { - return new ModularUI(198, 208, this, entityPlayer).widget(new FancyMachineUIWidget(this, 198, 208)); + return new ModularUI(176, 166, this, entityPlayer) + .widget(new StellarFancyUIWidget(this, 176, 166, this::getStage)); } } diff --git a/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/StellarBaseModule.java b/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/StellarBaseModule.java new file mode 100644 index 000000000..f571a699b --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/api/machine/multiblock/StellarBaseModule.java @@ -0,0 +1,407 @@ +package com.ghostipedia.cosmiccore.api.machine.multiblock; + +import com.ghostipedia.cosmiccore.api.data.wireless.WirelessEnergySavedData; +import com.ghostipedia.cosmiccore.api.machine.feature.IStellarIrisProvider; +import com.ghostipedia.cosmiccore.api.machine.feature.IStellarModuleReceiver; +import com.ghostipedia.cosmiccore.client.gui.widget.stellar.StellarModuleContentWidget; +import com.ghostipedia.cosmiccore.client.gui.widget.stellar.StellarModuleUIWidget; + +import com.gregtechceu.gtceu.api.GTValues; +import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; +import com.gregtechceu.gtceu.api.machine.feature.IFancyUIMachine; +import com.gregtechceu.gtceu.api.machine.feature.IOverclockMachine; +import com.gregtechceu.gtceu.api.machine.feature.multiblock.IDisplayUIMachine; +import com.gregtechceu.gtceu.api.machine.multiblock.WorkableMultiblockMachine; +import com.gregtechceu.gtceu.api.recipe.GTRecipe; +import com.gregtechceu.gtceu.api.recipe.RecipeHelper; +import com.gregtechceu.gtceu.common.machine.owner.FTBOwner; +import com.gregtechceu.gtceu.common.machine.owner.MachineOwner; +import com.gregtechceu.gtceu.utils.GTUtil; + +import com.lowdragmc.lowdraglib.gui.modular.ModularUI; +import com.lowdragmc.lowdraglib.gui.widget.Widget; +import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; +import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; +import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; + +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.player.Player; + +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigInteger; +import java.util.List; +import java.util.UUID; + +/** + * Base class for Stellar Iris modules. Energy is drawn from the owner's wireless EU network. + */ +@Getter +public class StellarBaseModule extends WorkableMultiblockMachine + implements IStellarModuleReceiver, IDisplayUIMachine, IFancyUIMachine, + IOverclockMachine { + + public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( + StellarBaseModule.class, WorkableMultiblockMachine.MANAGED_FIELD_HOLDER); + + @Setter + private IStellarIrisProvider stellarIris; + + @DescSynced + private long energyConsumedPerTick = 0; + + @DescSynced + private boolean wirelessEnergyAvailable = false; + + @DescSynced + private boolean powerFailure = false; + + @Getter + @Setter + @Persisted + @DescSynced + private int configuredMaxParallel = 1; + + @Getter + @Setter + @Persisted + @DescSynced + private long configuredVoltagePerParallel = 32; + + public StellarBaseModule(IMachineBlockEntity holder) { + super(holder); + } + + @Override + public @NotNull ManagedFieldHolder getFieldHolder() { + return MANAGED_FIELD_HOLDER; + } + + protected UUID getTeamUUID() { + var owner = getOwner(); + var ownerUUID = getOwnerUUID(); + + if (owner == null) return MachineOwner.EMPTY; + if (ownerUUID == null) return MachineOwner.EMPTY; + + if (owner instanceof FTBOwner ftbOwner) { + var team = ftbOwner.getPlayerTeam(ownerUUID); + if (team != null) { + return team.getTeamId(); + } + } + return ownerUUID; + } + + protected boolean drainWirelessEnergy(long amount) { + if (!(getLevel() instanceof ServerLevel serverLevel)) { + return false; + } + + UUID owner = getTeamUUID(); + if (owner == MachineOwner.EMPTY) { + return false; + } + + WirelessEnergySavedData data = WirelessEnergySavedData.getOrCreate(serverLevel); + + if (!data.isActive(owner)) { + return false; + } + + BigInteger stored = data.getEnergyStored(owner); + if (stored.compareTo(BigInteger.valueOf(amount)) < 0) { + return false; + } + + BigInteger leftover = data.addEUToGlobalWirelessEnergy(owner, BigInteger.valueOf(-amount)); + return leftover.equals(BigInteger.ZERO); + } + + protected boolean checkWirelessEnergyAvailable() { + if (!(getLevel() instanceof ServerLevel serverLevel)) { + return false; + } + + UUID owner = getTeamUUID(); + if (owner == MachineOwner.EMPTY) { + return false; + } + + WirelessEnergySavedData data = WirelessEnergySavedData.getOrCreate(serverLevel); + return data.isActive(owner); + } + + protected BigInteger getWirelessEnergyStored() { + if (!(getLevel() instanceof ServerLevel serverLevel)) { + return BigInteger.ZERO; + } + + UUID owner = getTeamUUID(); + if (owner == MachineOwner.EMPTY) { + return BigInteger.ZERO; + } + + WirelessEnergySavedData data = WirelessEnergySavedData.getOrCreate(serverLevel); + return data.getEnergyStored(owner); + } + + @Override + public void onStructureFormed() { + super.onStructureFormed(); + this.wirelessEnergyAvailable = checkWirelessEnergyAvailable(); + findAndRegisterWithIris(); + } + + protected void findAndRegisterWithIris() { + if (getLevel() == null || stellarIris != null) return; + + BlockPos modulePos = getPos(); + int maxRadius = 80; + + for (int radius = 1; radius <= maxRadius; radius++) { + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + if (Math.abs(x) != radius && Math.abs(z) != radius) continue; + + for (int y = -10; y <= 10; y++) { + BlockPos checkPos = modulePos.offset(x, y, z); + var blockEntity = getLevel().getBlockEntity(checkPos); + + if (blockEntity instanceof IMachineBlockEntity machineBlockEntity) { + var machine = machineBlockEntity.getMetaMachine(); + if (machine instanceof IrisMultiblockMachine iris && iris.isFormed()) { + if (iris.registerModule(this)) { + return; + } + } + } + } + } + } + } + } + + @Override + public void onStructureInvalid() { + super.onStructureInvalid(); + + if (stellarIris instanceof IrisMultiblockMachine iris) { + iris.unregisterModule(this); + } + + this.stellarIris = null; + this.wirelessEnergyAvailable = false; + this.energyConsumedPerTick = 0; + } + + @Override + public boolean isRecipeLogicAvailable() { + if (!super.isRecipeLogicAvailable()) { + return false; + } + + IStellarIrisProvider iris = getStellarIris(); + if (iris == null || !iris.isFormed()) { + return false; + } + + if (!iris.canProcess()) { + return false; + } + + this.wirelessEnergyAvailable = checkWirelessEnergyAvailable(); + return wirelessEnergyAvailable; + } + + @Override + public boolean beforeWorking(@Nullable GTRecipe recipe) { + if (recipe == null) return false; + + long euPerTick = getRecipeEUPerTick(recipe); + + if (!drainWirelessEnergy(euPerTick)) { + this.powerFailure = true; + return false; + } + + this.powerFailure = false; + this.energyConsumedPerTick = euPerTick; + return super.beforeWorking(recipe); + } + + @Override + public boolean onWorking() { + if (!super.onWorking()) { + return false; + } + + if (energyConsumedPerTick > 0) { + if (!drainWirelessEnergy(energyConsumedPerTick)) { + this.powerFailure = true; + return false; + } + } + + this.powerFailure = false; + return true; + } + + @Override + public void afterWorking() { + super.afterWorking(); + this.energyConsumedPerTick = 0; + this.powerFailure = false; + } + + protected long getRecipeEUPerTick(GTRecipe recipe) { + long baseEU = RecipeHelper.getRealEUt(recipe).getTotalEU(); + + IStellarIrisProvider iris = getStellarIris(); + if (iris != null && iris.canProcess()) { + double discount = iris.getEnergyDiscount(); + baseEU = (long) (baseEU * discount); + } + + return Math.max(1, baseEU); + } + + @Override + @Nullable + protected GTRecipe getRealRecipe(GTRecipe recipe) { + GTRecipe modified = super.getRealRecipe(recipe); + if (modified == null) { + return null; + } + + int recipeTier = RecipeHelper.getRecipeEUtTier(recipe); + if (recipeTier > getOverclockTier()) { + return null; + } + + IStellarIrisProvider iris = getStellarIris(); + if (iris == null || !iris.canProcess()) { + return modified; + } + + double speedBonus = iris.getSpeedBonus(); + if (speedBonus > 1.0) { + int newDuration = (int) Math.max(1, modified.duration / speedBonus); + modified = modified.copy(); + modified.duration = newDuration; + } + + return modified; + } + + public int getEffectiveParallelLimit() { + IStellarIrisProvider iris = getStellarIris(); + int irisLimit = (iris != null && iris.canProcess()) ? iris.getParallelLimit() : 1; + return Math.min(configuredMaxParallel, irisLimit); + } + + public int getIrisParallelLimit() { + IStellarIrisProvider iris = getStellarIris(); + if (iris == null || !iris.canProcess()) { + return 1; + } + return iris.getParallelLimit(); + } + + @Override + public int getOverclockTier() { + return GTUtil.getTierByVoltage(configuredVoltagePerParallel); + } + + @Override + public void setOverclockTier(int tier) { + tier = Math.max(getMinOverclockTier(), Math.min(tier, getMaxOverclockTier())); + this.configuredVoltagePerParallel = GTValues.V[tier]; + } + + @Override + public int getMaxOverclockTier() { + return GTValues.MAX; + } + + @Override + public int getMinOverclockTier() { + return GTValues.ULV; + } + + @Override + public long getOverclockVoltage() { + return configuredVoltagePerParallel * getEffectiveParallelLimit(); + } + + public long getMaxEUt() { + return configuredVoltagePerParallel * getEffectiveParallelLimit(); + } + + @Override + public Widget createUIWidget() { + return new StellarModuleContentWidget(() -> this); + } + + @Override + public ModularUI createUI(Player entityPlayer) { + return new ModularUI(198, 208, this, entityPlayer) + .widget(new StellarModuleUIWidget(this, 198, 208, () -> this)); + } + + @Override + public void addDisplayText(List textList) { + IDisplayUIMachine.super.addDisplayText(textList); + + if (isFormed()) { + if (powerFailure) { + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.power_failure") + .setStyle(Style.EMPTY.withColor(ChatFormatting.RED).withBold(true))); + } + + if (!wirelessEnergyAvailable) { + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.no_wireless") + .setStyle(Style.EMPTY.withColor(ChatFormatting.RED))); + } else if (energyConsumedPerTick > 0) { + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.energy_usage", + String.format("%,d", energyConsumedPerTick)) + .setStyle(Style.EMPTY.withColor(ChatFormatting.YELLOW))); + } + + String tierName = GTValues.VNF[getOverclockTier()]; + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.power_config", + tierName, getEffectiveParallelLimit()) + .setStyle(Style.EMPTY.withColor(ChatFormatting.AQUA))); + + IStellarIrisProvider iris = getStellarIris(); + if (iris == null) { + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.not_connected")); + } else if (!iris.isFormed()) { + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.iris_not_formed")); + } else if (!iris.canProcess()) { + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.iris_not_ready")); + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.stage", + iris.getStage().toString())); + } else { + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.connected")); + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.stage", + iris.getStage().toString())); + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.speed_bonus", + String.format("%.1fx", iris.getSpeedBonus()))); + textList.add(Component.translatable("cosmiccore.multiblock.stellar_module.parallel", + iris.getParallelLimit())); + } + } + } + + public boolean isPowerFailure() { + return powerFailure; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/api/pattern/CosmicPredicates.java b/src/main/java/com/ghostipedia/cosmiccore/api/pattern/CosmicPredicates.java index dffb6f0fe..786c4a252 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/api/pattern/CosmicPredicates.java +++ b/src/main/java/com/ghostipedia/cosmiccore/api/pattern/CosmicPredicates.java @@ -2,8 +2,12 @@ import com.ghostipedia.cosmiccore.api.CosmicCoreAPI; import com.ghostipedia.cosmiccore.api.block.IMagnetType; +import com.ghostipedia.cosmiccore.api.machine.feature.IStellarModuleReceiver; import com.ghostipedia.cosmiccore.common.block.MagnetBlock; +import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; +import com.gregtechceu.gtceu.api.machine.MetaMachine; +import com.gregtechceu.gtceu.api.machine.feature.multiblock.IMultiController; import com.gregtechceu.gtceu.api.pattern.TraceabilityPredicate; import com.gregtechceu.gtceu.api.pattern.error.PatternStringError; import com.gregtechceu.gtceu.api.pattern.util.PatternMatchContext; @@ -11,11 +15,14 @@ import com.lowdragmc.lowdraglib.utils.BlockInfo; import net.minecraft.network.chat.Component; +import net.minecraft.world.level.block.Blocks; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import java.util.Comparator; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.function.Supplier; public class CosmicPredicates { @@ -63,5 +70,45 @@ public static TraceabilityPredicate starLadderModules() { .addTooltips(Component.translatable("gtceu.multiblock.pattern.error.filters")); } + /** + * Predicate for Stellar Iris module slots. + * Accepts air (empty slot) or a Stellar Module controller (formed or not). + * When a module is found, adds it to the "stellarModules" set in match context. + * The module doesn't need to be formed - the Iris will establish the connection, + * allowing the module to then form using the Iris as its provider. + */ + public static TraceabilityPredicate stellarModuleSlot() { + return new TraceabilityPredicate(blockWorldState -> { + var blockState = blockWorldState.getBlockState(); + + // Accept air (empty slot) + if (blockState.isAir()) { + return true; + } + + // Check if this is a stellar module controller + var blockEntity = blockWorldState.getTileEntity(); + if (blockEntity instanceof IMachineBlockEntity machineBlockEntity) { + MetaMachine machine = machineBlockEntity.getMetaMachine(); + + // Must be a multiblock controller that implements IStellarModuleReceiver + if (machine instanceof IMultiController && + machine instanceof IStellarModuleReceiver moduleReceiver) { + + // Add to the set of connected modules (formed or not) + // The Iris will establish the connection during onStructureFormed + Set modules = blockWorldState.getMatchContext() + .getOrCreate("stellarModules", HashSet::new); + modules.add(moduleReceiver); + return true; + } + } + + // Invalid block - not air and not a valid module + return false; + }, () -> new BlockInfo[] { BlockInfo.fromBlockState(Blocks.AIR.defaultBlockState()) }) + .addTooltips(Component.translatable("cosmiccore.multiblock.pattern.stellar_module_slot")); + } + public static void init() {} } diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/CosmicCoreClient.java b/src/main/java/com/ghostipedia/cosmiccore/client/CosmicCoreClient.java index b02a37422..d3600ddcf 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/client/CosmicCoreClient.java +++ b/src/main/java/com/ghostipedia/cosmiccore/client/CosmicCoreClient.java @@ -7,11 +7,15 @@ import com.gregtechceu.gtceu.client.renderer.machine.DynamicRenderManager; import net.minecraft.client.renderer.ShaderInstance; +import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.client.event.ModelEvent; import net.minecraftforge.client.event.RegisterGuiOverlaysEvent; import net.minecraftforge.client.event.RegisterShadersEvent; +import net.minecraftforge.client.event.RenderGuiOverlayEvent; +import net.minecraftforge.client.gui.overlay.VanillaGuiOverlay; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; import com.mojang.blaze3d.vertex.DefaultVertexFormat; import forestry.api.apiculture.genetics.BeeLifeStage; @@ -41,16 +45,35 @@ public static void init(IEventBus modBus) { DynamicRenderManager.register(CosmicCore.id("spirit_crucible"), SpiritCrucibleRender.TYPE); DynamicRenderManager.register(CosmicCore.id("biovat_render"), BioVatRender.TYPE); DynamicRenderManager.register(CosmicCore.id("tester_render"), RenderTesterHelper.TYPE); + DynamicRenderManager.register(CosmicCore.id("star_ladder_render"), StarLadderRender.TYPE); } @Getter private static ShaderInstance nebulaeShader; + @Getter + private static ShaderInstance soulAuraShader; + + @Getter + private static ShaderInstance voidBgShader; + + @Getter + private static ShaderInstance galaxyBgShader; + @SubscribeEvent public static void shaderRegistry(RegisterShadersEvent event) { try { event.registerShader(new ShaderInstance(event.getResourceProvider(), CosmicCore.id("rendertype_nebulae"), DefaultVertexFormat.POSITION), (shaderInstance) -> nebulaeShader = shaderInstance); + + event.registerShader(new ShaderInstance(event.getResourceProvider(), CosmicCore.id("soul_aura"), + DefaultVertexFormat.POSITION_TEX), (shaderInstance) -> soulAuraShader = shaderInstance); + + event.registerShader(new ShaderInstance(event.getResourceProvider(), CosmicCore.id("void_bg"), + DefaultVertexFormat.POSITION_TEX), (shaderInstance) -> voidBgShader = shaderInstance); + + event.registerShader(new ShaderInstance(event.getResourceProvider(), CosmicCore.id("galaxy_bg"), + DefaultVertexFormat.POSITION_TEX), (shaderInstance) -> galaxyBgShader = shaderInstance); } catch (IOException e) { throw new RuntimeException(e); } @@ -357,4 +380,19 @@ private static void registerApiculture(IClientRegistration client) { CosmicCore.id("item/bee/bee_drone_fuzzy_queen")); } } + + /** + * Hides vanilla GUI overlays that are replaced by CosmicCore's custom HUD. + * Specifically hides the vanilla air bubble bar since we use our own oxygen bar. + */ + @Mod.EventBusSubscriber(modid = CosmicCore.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.FORGE) + public static final class HideVanillaOverlays { + + @SubscribeEvent + public static void onOverlayPre(RenderGuiOverlayEvent.Pre event) { + if (event.getOverlay() == VanillaGuiOverlay.AIR_LEVEL.type()) { + event.setCanceled(true); + } + } + } } diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/CosmicHudGuiOverlay.java b/src/main/java/com/ghostipedia/cosmiccore/client/CosmicHudGuiOverlay.java index 4dcc85591..c90c1c5b4 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/client/CosmicHudGuiOverlay.java +++ b/src/main/java/com/ghostipedia/cosmiccore/client/CosmicHudGuiOverlay.java @@ -1,5 +1,6 @@ package com.ghostipedia.cosmiccore.client; +import com.ghostipedia.cosmiccore.CosmicCore; import com.ghostipedia.cosmiccore.common.item.behavior.WirelessPDABehavior; import com.gregtechceu.gtceu.api.item.ComponentItem; @@ -8,32 +9,78 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import net.minecraftforge.client.gui.overlay.ForgeGui; import net.minecraftforge.client.gui.overlay.IGuiOverlay; +import com.mojang.blaze3d.systems.RenderSystem; import lombok.NoArgsConstructor; import org.jetbrains.annotations.NotNull; @NoArgsConstructor public class CosmicHudGuiOverlay implements IGuiOverlay { + // Oxygen bar textures + private static final ResourceLocation OXY_BG = CosmicCore.id("textures/gui/oxygen_bg.png"); + private static final ResourceLocation OXY_FILL = CosmicCore.id("textures/gui/oxygen_fill.png"); + private static final int TEX_W = 64, TEX_H = 12; + + // Time bar state private static long timeTicksLeft = -1; private static long timeMaxTicks = 0; + // Oxygen bar state + private static long oxygenTicksLeft = -1; + private static long oxygenMaxTicks = 0; + private static boolean oxygenShow = true; + private static double lastRateTicksPerSecond = Double.NaN; + + // Track displayed value to prevent visual jitter (bar only moves in direction of rate) + private static long displayedOxygen = -1; + + // Colors for oxygen bar text + private static final int COLOR_DRAIN = 0x000000; // Black for draining (no shadow) + private static final int COLOR_REGEN = 0x00ff66; + private static final int COLOR_IDLE = 0xAAAAAA; + public static void setTimeBar(ResourceLocation dim, long left, long max) { timeTicksLeft = left; timeMaxTicks = max; } + public static void setOxygenBar(long left, long max, boolean show, double ratePerSecond) { + oxygenTicksLeft = left; + oxygenMaxTicks = max; + oxygenShow = show; + lastRateTicksPerSecond = ratePerSecond; + + // Update displayed value with monotonic constraint based on rate direction + // This prevents visual jitter from server-side fluctuations + if (displayedOxygen < 0) { + // First sync - just use the value + displayedOxygen = left; + } else if (ratePerSecond < -0.1) { + // Draining: bar can only decrease or stay same + displayedOxygen = Math.min(displayedOxygen, left); + } else if (ratePerSecond > 0.1) { + // Regenerating: bar can only increase or stay same + displayedOxygen = Math.max(displayedOxygen, left); + } else { + // Idle/neutral: snap to actual value + displayedOxygen = left; + } + } + @Override public void render(ForgeGui forgeGui, GuiGraphics guiGraphics, float partialTick, int screenWidth, int screenHeight) { Minecraft mc = Minecraft.getInstance(); - if (mc.isWindowActive() && mc.level != null && !mc.options.renderDebug && !mc.options.hideGui) { + if (mc.level != null && !mc.options.renderDebug && !mc.options.hideGui) { renderHUDWirelessPDA(WirelessPDABehavior.CosmicCuriosUtils.getPDACurio(mc.player), guiGraphics); renderTimeBudgetBar(guiGraphics, screenWidth, screenHeight); + renderOxygenBar(guiGraphics, screenWidth, screenHeight); } } @@ -69,5 +116,102 @@ private static void renderTimeBudgetBar(GuiGraphics gg, int sw, int sh) { x + w / 2 - Minecraft.getInstance().font.width(txt) / 2, y - 10, 0xFFFFFF, true); } + + // ------------------------------------------------------------------------- + // Oxygen Bar Rendering + // Uses matrix scaling to render at 81px wide (matching hunger bar) without texture stretching // ------------------------------------------------------------------------- + + // Scale factor to match vanilla hunger bar width (81px target / 64px texture) + private static final float BAR_SCALE = 81f / TEX_W; // ~1.265625 + + private static void renderOxygenBar(GuiGraphics gg, int screenWidth, int screenHeight) { + if (!oxygenShow || oxygenTicksLeft < 0 || oxygenMaxTicks <= 0) return; + + // Final rendered dimensions after scaling + int renderedWidth = (int) (TEX_W * BAR_SCALE); + int renderedHeight = (int) (TEX_H * BAR_SCALE); + + // Position to match vanilla hunger bar (right edge at screenWidth/2 + 91) + int x = screenWidth / 2 + 10; + int y = screenHeight - 39 - renderedHeight; + + // Use displayedOxygen for visual (has monotonic constraint to prevent jitter) + long visualOxygen = displayedOxygen >= 0 ? displayedOxygen : oxygenTicksLeft; + double frac = Math.max(0d, Math.min(1d, (double) visualOxygen / (double) oxygenMaxTicks)); + + // Calculate filled width in texture pixels (before scaling) + int filledTexW = (int) (TEX_W * frac); + filledTexW = Math.max(0, Math.min(TEX_W, filledTexW)); + + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + + // Push matrix and apply scale transform + var pose = gg.pose(); + pose.pushPose(); + pose.translate(x, y, 0); + pose.scale(BAR_SCALE, BAR_SCALE, 1f); + + // Background - render at native texture size (scaling handled by matrix) + gg.blit(OXY_BG, 0, 0, 0, 0, TEX_W, TEX_H, TEX_W, TEX_H); + + // Fill bar - clip horizontally based on fill amount + if (filledTexW > 0) { + gg.blit(OXY_FILL, 0, 0, 0, 0, filledTexW, TEX_H, TEX_W, TEX_H); + } + + pose.popPose(); + + // ETA text - render outside the scaled context for crisp text + var font = Minecraft.getInstance().font; + var comp = computeOxygenETA(); + int tx = x + renderedWidth / 2 - font.width(comp) / 2; + int ty = y + (renderedHeight - 8) / 2 + 1; // +1 to nudge down for better centering + // Disable shadow for draining (black text), enable for others + boolean useShadow = !isDraining(); + gg.drawString(font, comp, tx, ty, 0xFFFFFF, useShadow); + + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + } + + private static boolean isDraining() { + if (oxygenTicksLeft <= 0) return true; // Suffocating + if (oxygenTicksLeft >= oxygenMaxTicks) return false; // Full + double r = lastRateTicksPerSecond; + return Double.isFinite(r) && r < -0.01; // Negative rate = draining + } + + private static Component computeOxygenETA() { + if (oxygenTicksLeft <= 0) { + return Component.literal("SUFFOCATING").withStyle(s -> s.withColor(COLOR_DRAIN)); + } + if (oxygenTicksLeft >= oxygenMaxTicks) { + return Component.literal("--:--").withStyle(s -> s.withColor(COLOR_IDLE)); + } + + double r = lastRateTicksPerSecond; + if (!Double.isFinite(r) || Math.abs(r) < 0.01) { + return Component.literal("--:--").withStyle(s -> s.withColor(COLOR_IDLE)); + } + + if (r < 0) { + // Draining + long etaSec = (long) Math.ceil(oxygenTicksLeft / (-r)); + return Component.literal("<- " + formatSeconds(etaSec) + " >").withStyle(s -> s.withColor(COLOR_DRAIN)); + } else { + // Regenerating + long ticksNeeded = oxygenMaxTicks - oxygenTicksLeft; + long etaSec = (long) Math.ceil(ticksNeeded / r); + return Component.literal("< " + formatSeconds(etaSec) + " ->").withStyle(s -> s.withColor(COLOR_REGEN)); + } + } + + private static String formatSeconds(long sec) { + if (sec < 0) sec = 0; + long m = sec / 60; + long s = sec % 60; + return m + ":" + String.format("%02d", s); + } } diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/DebugPrimeButton.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/DebugPrimeButton.java new file mode 100644 index 000000000..62a56958b --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/DebugPrimeButton.java @@ -0,0 +1,63 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.Nonnull; + +public class DebugPrimeButton extends Widget { + + private final Runnable onClick; + private boolean hovered = false; + + public DebugPrimeButton(int x, int y, int width, int height, Runnable onClick) { + super(x, y, width, height); + this.onClick = onClick; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + hovered = isMouseOverElement(mouseX, mouseY); + + // Background - yellow/gold tint for "prime" + int bgColor = hovered ? 0xAA404020 : 0x80302010; + DrawerHelper.drawSolidRect(graphics, x, y, w, h, bgColor); + + // Border - golden when hovered + int borderColor = hovered ? 0xFFFFCC44 : 0xFF806020; + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 1); + + // Label + var font = Minecraft.getInstance().font; + String label = "[PRIME]"; + int labelWidth = font.width(label); + int labelX = x + (w - labelWidth) / 2; + int labelY = y + (h - font.lineHeight) / 2 + 1; + + int textColor = hovered ? 0xFFFFDD66 : 0xFFAA9944; + graphics.drawString(font, label, labelX, labelY, textColor, false); + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && isMouseOverElement(mouseX, mouseY)) { + onClick.run(); + return true; + } + return false; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/DebugStageButton.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/DebugStageButton.java new file mode 100644 index 000000000..6bd614766 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/DebugStageButton.java @@ -0,0 +1,104 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Consumer; + +import javax.annotation.Nonnull; + +public class DebugStageButton extends Widget { + + private final Stage stage; + private final Consumer onClick; + private boolean hovered = false; + + public DebugStageButton(int x, int y, int width, int height, Stage stage, Consumer onClick) { + super(x, y, width, height); + this.stage = stage; + this.onClick = onClick; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + hovered = isMouseOverElement(mouseX, mouseY); + + // Background + int bgColor = hovered ? 0xAA303040 : 0x80202030; + DrawerHelper.drawSolidRect(graphics, x, y, w, h, bgColor); + + // Border with stage color + int borderColor = getStageColor(); + if (hovered) { + borderColor = brighten(borderColor); + } + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 1); + + // Label + var font = Minecraft.getInstance().font; + String label = getShortLabel(); + int labelWidth = font.width(label); + int labelX = x + (w - labelWidth) / 2; + int labelY = y + (h - font.lineHeight) / 2 + 1; + + int textColor = hovered ? 0xFFFFFFFF : 0xFFAAAAAA; + graphics.drawString(font, label, labelX, labelY, textColor, false); + } + + private String getShortLabel() { + return switch (stage) { + case EMPTY -> "EMP"; + case GROWING -> "GRW"; + case STAR -> "STR"; + case SUPERSTAR -> "SUP"; + case BLACK_HOLE -> "BLK"; + case DEATH -> "DTH"; + case DEATH_GRACEFUL -> "GRC"; + }; + } + + private int getStageColor() { + return switch (stage) { + case EMPTY -> 0xFF404050; + case GROWING -> 0xFF6080FF; + case STAR -> 0xFFFFCC44; + case SUPERSTAR -> 0xFFFF8844; + case BLACK_HOLE -> 0xFF8040FF; + case DEATH -> 0xFFFF2020; + case DEATH_GRACEFUL -> 0xFF804040; + }; + } + + private int brighten(int color) { + int a = (color >> 24) & 0xFF; + int r = Math.min(255, ((color >> 16) & 0xFF) + 40); + int g = Math.min(255, ((color >> 8) & 0xFF) + 40); + int b = Math.min(255, (color & 0xFF) + 40); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && isMouseOverElement(mouseX, mouseY)) { + onClick.accept(stage); + return true; + } + return false; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/EnergyConduitWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/EnergyConduitWidget.java new file mode 100644 index 000000000..94960b349 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/EnergyConduitWidget.java @@ -0,0 +1,229 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class EnergyConduitWidget extends Widget { + + private final Supplier stageSupplier; + private final List pulses = new ArrayList<>(); + + private float flowPhase = 0f; + private float pulseSpawnTimer = 0f; + + private static class EnergyPulse { + + float position; + float speed; + float intensity; + int conduitIndex; + boolean alive = true; + } + + public EnergyConduitWidget(int x, int y, int width, int height, Supplier stageSupplier) { + super(x, y, width, height); + this.stageSupplier = stageSupplier; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + + Stage stage = stageSupplier.get(); + float flowSpeed = getFlowSpeed(stage); + flowPhase += flowSpeed; + + pulseSpawnTimer += flowSpeed * 2f; + if (pulseSpawnTimer > 1f && stage != Stage.EMPTY && stage != Stage.DEATH_GRACEFUL) { + pulseSpawnTimer = 0f; + spawnPulse(stage); + } + + pulses.removeIf(p -> !p.alive); + for (EnergyPulse pulse : pulses) { + pulse.position += pulse.speed; + if (pulse.position > 1f) { + pulse.alive = false; + } + } + } + + private void spawnPulse(Stage stage) { + if (pulses.size() > 20) return; + + EnergyPulse pulse = new EnergyPulse(); + pulse.position = 0f; + pulse.speed = 0.02f + (float) Math.random() * 0.03f; + pulse.intensity = 0.5f + (float) Math.random() * 0.5f; + pulse.conduitIndex = (int) (Math.random() * 4); + + if (stage == Stage.DEATH) { + pulse.speed *= 2f; + } else if (stage == Stage.BLACK_HOLE) { + pulse.speed *= 1.5f; + } + + pulses.add(pulse); + } + + private float getFlowSpeed(Stage stage) { + return switch (stage) { + case EMPTY -> 0.005f; + case GROWING -> 0.03f; + case STAR -> 0.02f; + case SUPERSTAR -> 0.04f; + case BLACK_HOLE -> 0.05f; + case DEATH -> 0.08f; + case DEATH_GRACEFUL -> 0.01f; + }; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + Stage stage = stageSupplier.get(); + int baseColor = getStageColor(stage); + + drawConduitLines(graphics, x, y, w, h, baseColor, stage); + drawFlowingEnergy(graphics, x, y, w, h, baseColor, stage); + drawPulses(graphics, x, y, w, h, baseColor); + drawConduitNodes(graphics, x, y, w, h, baseColor, stage); + } + + private void drawConduitLines(GuiGraphics graphics, int x, int y, int w, int h, int baseColor, Stage stage) { + int lineColor = 0x40000000 | (baseColor & 0x00FFFFFF); + int glowColor = 0x20000000 | (baseColor & 0x00FFFFFF); + + int topY = y + 8; + graphics.fill(x, topY - 1, x + w, topY + 2, glowColor); + graphics.fill(x, topY, x + w, topY + 1, lineColor); + + int bottomY = y + h - 8; + graphics.fill(x, bottomY - 1, x + w, bottomY + 2, glowColor); + graphics.fill(x, bottomY, x + w, bottomY + 1, lineColor); + + int leftX = x + 8; + graphics.fill(leftX - 1, y, leftX + 2, y + h, glowColor); + graphics.fill(leftX, y, leftX + 1, y + h, lineColor); + + int rightX = x + w - 8; + graphics.fill(rightX - 1, y, rightX + 2, y + h, glowColor); + graphics.fill(rightX, y, rightX + 1, y + h, lineColor); + } + + private void drawFlowingEnergy(GuiGraphics graphics, int x, int y, int w, int h, int baseColor, Stage stage) { + if (stage == Stage.EMPTY) return; + + int segmentCount = 20; + float segmentSpacing = 1f / segmentCount; + + for (int i = 0; i < segmentCount; i++) { + float segmentPhase = (flowPhase + i * segmentSpacing) % 1f; + float brightness = Mth.sin(segmentPhase * Mth.PI) * 0.8f; + if (brightness < 0.1f) continue; + + int alpha = (int) (0x60 * brightness); + int color = (alpha << 24) | (baseColor & 0x00FFFFFF); + + int topY = y + 8; + int segX = x + (int) (w * segmentPhase); + graphics.fill(segX - 1, topY - 1, segX + 2, topY + 2, color); + + int bottomY = y + h - 8; + int reverseX = x + w - (int) (w * segmentPhase); + graphics.fill(reverseX - 1, bottomY - 1, reverseX + 2, bottomY + 2, color); + + int leftX = x + 8; + int segY = y + (int) (h * segmentPhase); + graphics.fill(leftX - 1, segY - 1, leftX + 2, segY + 2, color); + + int rightX = x + w - 8; + int reverseY = y + h - (int) (h * segmentPhase); + graphics.fill(rightX - 1, reverseY - 1, rightX + 2, reverseY + 2, color); + } + } + + private void drawPulses(GuiGraphics graphics, int x, int y, int w, int h, int baseColor) { + for (EnergyPulse pulse : pulses) { + float brightness = pulse.intensity * (1f - pulse.position * 0.5f); + int alpha = (int) (0xC0 * brightness); + int color = (alpha << 24) | (baseColor & 0x00FFFFFF); + int coreColor = (alpha << 24) | 0xFFFFFF; + + int px, py; + switch (pulse.conduitIndex) { + case 0 -> { + px = x + (int) (w * pulse.position); + py = y + 8; + } + case 1 -> { + px = x + w - (int) (w * pulse.position); + py = y + h - 8; + } + case 2 -> { + px = x + 8; + py = y + (int) (h * pulse.position); + } + default -> { + px = x + w - 8; + py = y + h - (int) (h * pulse.position); + } + } + + graphics.fill(px - 3, py - 3, px + 4, py + 4, color); + graphics.fill(px - 1, py - 1, px + 2, py + 2, coreColor); + } + } + + private void drawConduitNodes(GuiGraphics graphics, int x, int y, int w, int h, int baseColor, Stage stage) { + float nodePulse = Mth.sin(flowPhase * 3f) * 0.3f + 0.7f; + int nodeAlpha = (int) (0x80 * nodePulse); + int nodeColor = (nodeAlpha << 24) | (baseColor & 0x00FFFFFF); + int nodeGlow = (nodeAlpha / 2 << 24) | (baseColor & 0x00FFFFFF); + + int[][] nodePositions = { + { x + 8, y + 8 }, + { x + w - 8, y + 8 }, + { x + 8, y + h - 8 }, + { x + w - 8, y + h - 8 } + }; + + for (int[] pos : nodePositions) { + graphics.fill(pos[0] - 4, pos[1] - 4, pos[0] + 5, pos[1] + 5, nodeGlow); + graphics.fill(pos[0] - 2, pos[1] - 2, pos[0] + 3, pos[1] + 3, nodeColor); + graphics.fill(pos[0] - 1, pos[1] - 1, pos[0] + 2, pos[1] + 2, 0xFFFFFFFF); + } + } + + private int getStageColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0x405060; + case GROWING -> 0x6090FF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF7722; + case BLACK_HOLE -> 0xAA55FF; + case DEATH -> 0xFF3030; + case DEATH_GRACEFUL -> 0x664040; + }; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/FuelGaugeWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/FuelGaugeWidget.java new file mode 100644 index 000000000..2d656fe75 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/FuelGaugeWidget.java @@ -0,0 +1,121 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class FuelGaugeWidget extends Widget { + + private final Supplier fuelLevelSupplier; + + private float displayedLevel = 0f; + private float animPhase = 0f; + + public FuelGaugeWidget(int x, int y, int width, int height, Supplier fuelLevelSupplier) { + super(x, y, width, height); + this.fuelLevelSupplier = fuelLevelSupplier; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + animPhase += 0.1f; + + float target = fuelLevelSupplier.get(); + displayedLevel = Mth.lerp(0.1f, displayedLevel, target); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + // Label + var font = Minecraft.getInstance().font; + graphics.drawString(font, "STELLAR FUEL", x, y, 0xFF909090, false); + + // Gauge background + int gaugeY = y + 12; + int gaugeH = h - 12; + DrawerHelper.drawSolidRect(graphics, x, gaugeY, w, gaugeH, 0xFF101020); + + // Border + DrawerHelper.drawBorder(graphics, x, gaugeY, w, gaugeH, 0xFF303050, 1); + + // Filled portion with gradient based on level + int fillW = (int) (w * displayedLevel); + if (fillW > 0) { + int fillColor = getFillColor(displayedLevel); + int fillColorDark = darkenColor(fillColor, 0.6f); + + DrawerHelper.drawGradientRect(graphics, x + 1, gaugeY + 1, fillW - 2, gaugeH - 2, + fillColorDark, fillColor, true); + + // Animated shimmer + float shimmerPos = (animPhase % (w * 2)) - w; + if (shimmerPos > 0 && shimmerPos < fillW) { + int shimmerX = x + (int) shimmerPos; + int shimmerW = Math.min(10, fillW - (int) shimmerPos); + graphics.fill(shimmerX, gaugeY + 1, shimmerX + shimmerW, gaugeY + gaugeH - 1, + 0x20FFFFFF); + } + } + + // Threshold markers + drawThresholdMarker(graphics, x, gaugeY, w, gaugeH, 0.8f, "IGNITE"); + + // Percentage text + int percent = (int) (displayedLevel * 100); + String percentStr = percent + "%"; + int textX = x + w - font.width(percentStr) - 2; + int textColor = displayedLevel >= 0.8f ? 0xFF80FF80 : 0xFFFFFFFF; + graphics.drawString(font, percentStr, textX, gaugeY + (gaugeH - font.lineHeight) / 2 + 1, + textColor, false); + } + + private void drawThresholdMarker(GuiGraphics graphics, int x, int y, int w, int h, + float threshold, String label) { + int markerX = x + (int) (w * threshold); + + // Vertical line + graphics.fill(markerX, y, markerX + 1, y + h, 0xFF80FF80); + + // Small triangle indicator + graphics.fill(markerX - 2, y - 3, markerX + 3, y, 0xFF80FF80); + } + + private int getFillColor(float level) { + if (level >= 0.8f) { + return 0xFF40FF60; // Green - ready + } else if (level >= 0.5f) { + return 0xFFFFCC40; // Yellow - charging + } else if (level >= 0.2f) { + return 0xFFFF8040; // Orange - low + } else { + return 0xFFFF4040; // Red - critical + } + } + + private int darkenColor(int color, float factor) { + int a = (color >> 24) & 0xFF; + int r = (int) (((color >> 16) & 0xFF) * factor); + int g = (int) (((color >> 8) & 0xFF) * factor); + int b = (int) ((color & 0xFF) * factor); + return (a << 24) | (r << 16) | (g << 8) | b; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/HolographicScanlineWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/HolographicScanlineWidget.java new file mode 100644 index 000000000..32a929bf5 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/HolographicScanlineWidget.java @@ -0,0 +1,187 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class HolographicScanlineWidget extends Widget { + + private final Supplier stageSupplier; + private float scanY = 0f; + private float glitchTimer = 0f; + private float interferencePhase = 0f; + + public HolographicScanlineWidget(int x, int y, int width, int height, Supplier stageSupplier) { + super(x, y, width, height); + this.stageSupplier = stageSupplier; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + + Stage stage = stageSupplier.get(); + float scanSpeed = getScanSpeed(stage); + scanY = (scanY + scanSpeed) % 1f; + + interferencePhase += 0.07f; + + if (stage == Stage.DEATH || stage == Stage.BLACK_HOLE) { + glitchTimer += 0.15f; + } else { + glitchTimer *= 0.95f; + } + } + + private float getScanSpeed(Stage stage) { + return switch (stage) { + case EMPTY -> 0.003f; + case GROWING -> 0.008f; + case STAR -> 0.005f; + case SUPERSTAR -> 0.012f; + case BLACK_HOLE -> 0.02f; + case DEATH -> 0.04f; + case DEATH_GRACEFUL -> 0.002f; + }; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + Stage stage = stageSupplier.get(); + int baseColor = getStageColor(stage); + + drawScanLines(graphics, x, y, w, h, baseColor); + drawMainScanBeam(graphics, x, y, w, h, baseColor); + drawInterferencePattern(graphics, x, y, w, h, stage); + + if (glitchTimer > 0.5f) { + drawGlitchEffect(graphics, x, y, w, h, stage); + } + + drawEdgeVignette(graphics, x, y, w, h, baseColor); + } + + private void drawScanLines(GuiGraphics graphics, int x, int y, int w, int h, int baseColor) { + int lineAlpha = 0x08; + int lineColor = (lineAlpha << 24) | (baseColor & 0x00FFFFFF); + + for (int ly = y; ly < y + h; ly += 2) { + graphics.fill(x, ly, x + w, ly + 1, lineColor); + } + } + + private void drawMainScanBeam(GuiGraphics graphics, int x, int y, int w, int h, int baseColor) { + int beamY = y + (int) (h * scanY); + int beamHeight = 3; + + for (int i = 0; i < 8; i++) { + int spread = i * 2; + float falloff = 1f - (i / 8f); + int alpha = (int) (0x40 * falloff); + int color = (alpha << 24) | (baseColor & 0x00FFFFFF); + + int drawY = beamY - spread; + if (drawY >= y && drawY < y + h) { + graphics.fill(x, drawY, x + w, drawY + beamHeight, color); + } + drawY = beamY + spread; + if (drawY >= y && drawY < y + h) { + graphics.fill(x, drawY, x + w, drawY + beamHeight, color); + } + } + + int coreAlpha = 0x80; + int coreColor = (coreAlpha << 24) | 0xFFFFFF; + if (beamY >= y && beamY < y + h - beamHeight) { + graphics.fill(x, beamY, x + w, beamY + beamHeight, coreColor); + } + } + + private void drawInterferencePattern(GuiGraphics graphics, int x, int y, int w, int h, Stage stage) { + if (stage == Stage.EMPTY || stage == Stage.DEATH_GRACEFUL) return; + + float intensity = switch (stage) { + case GROWING -> 0.3f; + case STAR -> 0.2f; + case SUPERSTAR -> 0.5f; + case BLACK_HOLE -> 0.7f; + case DEATH -> 0.9f; + default -> 0f; + }; + + int bands = 3 + (int) (intensity * 5); + for (int i = 0; i < bands; i++) { + float bandPhase = interferencePhase + i * 0.7f; + float bandY = (Mth.sin(bandPhase) * 0.5f + 0.5f); + int by = y + (int) (h * bandY); + + float bandIntensity = Mth.sin(bandPhase * 2.3f) * 0.5f + 0.5f; + int alpha = (int) (0x15 * intensity * bandIntensity); + int color = (alpha << 24) | 0x00FFFF; + + if (by >= y && by < y + h - 2) { + graphics.fill(x, by, x + w, by + 2, color); + } + } + } + + private void drawGlitchEffect(GuiGraphics graphics, int x, int y, int w, int h, Stage stage) { + long time = System.currentTimeMillis(); + int glitchCount = stage == Stage.DEATH ? 8 : 3; + + for (int i = 0; i < glitchCount; i++) { + if (((time / 50) + i * 17) % 7 < 2) { + int glitchY = y + (int) ((time / 30 + i * 43) % h); + int glitchH = 2 + (int) (Math.random() * 4); + int offsetX = (int) ((Math.random() - 0.5) * 10); + + int glitchColor = stage == Stage.DEATH ? 0x40FF0000 : 0x30FF00FF; + graphics.fill(x + offsetX, glitchY, x + w + offsetX, Math.min(glitchY + glitchH, y + h), glitchColor); + } + } + } + + private void drawEdgeVignette(GuiGraphics graphics, int x, int y, int w, int h, int baseColor) { + int vignetteSize = 15; + for (int i = 0; i < vignetteSize; i++) { + float progress = (float) i / vignetteSize; + int alpha = (int) (0x30 * (1f - progress)); + int color = (alpha << 24) | (baseColor & 0x00FFFFFF); + + graphics.fill(x, y + i, x + w, y + i + 1, color); + graphics.fill(x, y + h - i - 1, x + w, y + h - i, color); + graphics.fill(x + i, y, x + i + 1, y + h, color); + graphics.fill(x + w - i - 1, y, x + w - i, y + h, color); + } + } + + private int getStageColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0x4080A0; + case GROWING -> 0x60A0FF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF6622; + case BLACK_HOLE -> 0xAA44FF; + case DEATH -> 0xFF2020; + case DEATH_GRACEFUL -> 0x664444; + }; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/IgnitionButtonWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/IgnitionButtonWidget.java new file mode 100644 index 000000000..bf7c64012 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/IgnitionButtonWidget.java @@ -0,0 +1,293 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.BooleanSupplier; + +import javax.annotation.Nonnull; + +public class IgnitionButtonWidget extends Widget { + + private final BooleanSupplier canIgnite; + private final BooleanSupplier isVisible; + private final Runnable onIgnite; + + private float hoverProgress = 0f; + private float pulsePhase = 0f; + private float chargeProgress = 0f; + private boolean isCharging = false; + private boolean wasHovered = false; + + private static final int CHARGE_TICKS = 40; + private int chargeTicks = 0; + + public IgnitionButtonWidget(int x, int y, int width, int height, + BooleanSupplier canIgnite, + BooleanSupplier isVisible, + Runnable onIgnite) { + super(x, y, width, height); + this.canIgnite = canIgnite; + this.isVisible = isVisible; + this.onIgnite = onIgnite; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + pulsePhase += 0.15f; + + super.setVisible(isVisible.getAsBoolean()); + + boolean hovered = isMouseOverElement(lastMouseX, lastMouseY); + float targetHover = hovered ? 1f : 0f; + hoverProgress = Mth.lerp(0.2f, hoverProgress, targetHover); + + if (hovered && !wasHovered && isVisible()) { + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK.value(), 1.5f, 0.3f)); + } + wasHovered = hovered; + + if (isCharging && canIgnite.getAsBoolean()) { + chargeTicks++; + chargeProgress = (float) chargeTicks / CHARGE_TICKS; + if (chargeTicks >= CHARGE_TICKS) { + onIgnite.run(); + isCharging = false; + chargeTicks = 0; + chargeProgress = 0f; + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.RESPAWN_ANCHOR_SET_SPAWN, 0.8f, 1.0f)); + } + } else if (isCharging) { + isCharging = false; + chargeTicks = 0; + chargeProgress = Mth.lerp(0.3f, chargeProgress, 0f); + } else { + chargeProgress = Mth.lerp(0.2f, chargeProgress, 0f); + } + } + + private int lastMouseX, lastMouseY; + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + lastMouseX = mouseX; + lastMouseY = mouseY; + + if (!isVisible()) return; + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + boolean enabled = canIgnite.getAsBoolean(); + + int bgColor; + int borderColor; + int textColor; + + if (!enabled) { + bgColor = 0xFF1A1A2A; + borderColor = 0xFF303040; + textColor = 0xFF505060; + } else if (isCharging) { + float chargeGlow = 0.5f + chargeProgress * 0.5f; + int r = (int) (255 * chargeGlow); + int g = (int) (100 * (1f - chargeProgress * 0.5f)); + bgColor = 0xFF000000 | (r << 16) | (g << 8); + borderColor = 0xFFFF8040; + textColor = 0xFFFFFFFF; + } else { + float pulse = (float) (0.6f + 0.4f * Math.sin(pulsePhase)); + float hover = hoverProgress; + int baseR = (int) (40 + 60 * pulse + 40 * hover); + int baseG = (int) (30 + 40 * pulse + 30 * hover); + int baseB = (int) (10 + 20 * pulse); + bgColor = 0xFF000000 | (baseR << 16) | (baseG << 8) | baseB; + borderColor = 0xFFFFAA40; + textColor = 0xFFFFDD80; + } + + DrawerHelper.drawSolidRect(graphics, x, y, w, h, bgColor); + + if (enabled) { + float glowIntensity = hoverProgress * 0.5f + chargeProgress * 0.5f; + if (glowIntensity > 0.01f) { + int glowAlpha = (int) (glowIntensity * 60); + int glowColor = (glowAlpha << 24) | (borderColor & 0x00FFFFFF); + DrawerHelper.drawSolidRect(graphics, x - 2, y - 2, w + 4, h + 4, glowColor); + DrawerHelper.drawSolidRect(graphics, x - 1, y - 1, w + 2, h + 2, glowColor); + } + } + + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 1); + + if (chargeProgress > 0.01f) { + int chargeW = (int) (w * chargeProgress); + int chargeColor = lerpColor(0x40FF8040, 0x80FF4020, chargeProgress); + DrawerHelper.drawSolidRect(graphics, x + 1, y + 1, chargeW - 2, h - 2, chargeColor); + + if (chargeW > 3) { + graphics.fill(x + chargeW - 2, y + 1, x + chargeW, y + h - 1, 0xCCFFFFFF); + } + } + + if (isCharging && chargeProgress > 0.3f) { + drawEnergyArcs(graphics, x, y, w, h, chargeProgress); + } + + var font = Minecraft.getInstance().font; + String text = getButtonText(enabled); + int textW = font.width(text); + int textX = x + (w - textW) / 2; + int textY = y + (h - font.lineHeight) / 2; + + if (enabled) { + graphics.drawString(font, text, textX + 1, textY + 1, 0xFF000000, false); + } + graphics.drawString(font, text, textX, textY, textColor, false); + + if (enabled && !isCharging) { + float shimmerPos = (pulsePhase * 2) % (w + 40) - 20; + if (shimmerPos > 0 && shimmerPos < w) { + int shimmerX = x + (int) shimmerPos; + int shimmerW = Math.min(8, w - (int) shimmerPos); + graphics.fill(shimmerX, y + 1, shimmerX + shimmerW, y + h - 1, 0x15FFFFFF); + } + } + + if (enabled) { + int accentColor = isCharging ? 0xFFFF6040 : 0xFFFFAA40; + graphics.fill(x, y, x + 4, y + 1, accentColor); + graphics.fill(x, y, x + 1, y + 4, accentColor); + graphics.fill(x + w - 4, y, x + w, y + 1, accentColor); + graphics.fill(x + w - 1, y, x + w, y + 4, accentColor); + graphics.fill(x, y + h - 1, x + 4, y + h, accentColor); + graphics.fill(x, y + h - 4, x + 1, y + h, accentColor); + graphics.fill(x + w - 4, y + h - 1, x + w, y + h, accentColor); + graphics.fill(x + w - 1, y + h - 4, x + w, y + h, accentColor); + } + } + + private void drawEnergyArcs(GuiGraphics graphics, int x, int y, int w, int h, float intensity) { + long time = System.currentTimeMillis(); + int centerX = x + w / 2; + int centerY = y + h / 2; + int particleCount = 3 + (int) (intensity * 5); + + for (int i = 0; i < particleCount; i++) { + float particlePhase = ((time / 800f) + i * 0.15f) % 1f; + float angle = (i * 2.39996f) + (time / 2000f); + float edgeX, edgeY; + + if (i % 4 == 0) { + edgeX = x + (w * ((i * 0.37f) % 1f)); + edgeY = y; + } else if (i % 4 == 1) { + edgeX = x + w; + edgeY = y + (h * ((i * 0.37f) % 1f)); + } else if (i % 4 == 2) { + edgeX = x + (w * ((i * 0.37f) % 1f)); + edgeY = y + h; + } else { + edgeX = x; + edgeY = y + (h * ((i * 0.37f) % 1f)); + } + + float progress = particlePhase * particlePhase; + int particleX = (int) Mth.lerp(progress, edgeX, centerX); + int particleY = (int) Mth.lerp(progress, edgeY, centerY); + + float brightness = 0.4f + 0.6f * progress; + int alpha = (int) (brightness * 200 * intensity); + int particleColor = (alpha << 24) | 0xFFFF80; + + int size = 1 + (int) (progress * 2); + graphics.fill(particleX - size, particleY - size, particleX + size, particleY + size, particleColor); + + if (progress > 0.1f) { + float trailProgress = progress - 0.1f; + int trailX = (int) Mth.lerp(trailProgress * trailProgress, edgeX, centerX); + int trailY = (int) Mth.lerp(trailProgress * trailProgress, edgeY, centerY); + int trailAlpha = (int) (alpha * 0.3f); + int trailColor = (trailAlpha << 24) | 0xFFAA40; + graphics.fill(trailX - 1, trailY - 1, trailX + 1, trailY + 1, trailColor); + } + } + } + + private String getButtonText(boolean enabled) { + if (!enabled) { + return "INSUFFICIENT FUEL"; + } else if (isCharging) { + int percent = (int) (chargeProgress * 100); + return "CHARGING... " + percent + "%"; + } else { + return "[ IGNITE STELLAR CORE ]"; + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isVisible() || !isMouseOverElement(mouseX, mouseY)) { + return false; + } + + if (button == 0 && canIgnite.getAsBoolean()) { + isCharging = true; + chargeTicks = 0; + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.RESPAWN_ANCHOR_CHARGE, 1.2f, 0.8f)); + return true; + } + return false; + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button == 0 && isCharging) { + isCharging = false; + if (chargeTicks < CHARGE_TICKS) { + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.FIRE_EXTINGUISH, 1.0f, 0.5f)); + } + return true; + } + return false; + } + + private int lerpColor(int color1, int color2, float t) { + int a1 = (color1 >> 24) & 0xFF; + int r1 = (color1 >> 16) & 0xFF; + int g1 = (color1 >> 8) & 0xFF; + int b1 = color1 & 0xFF; + + int a2 = (color2 >> 24) & 0xFF; + int r2 = (color2 >> 16) & 0xFF; + int g2 = (color2 >> 8) & 0xFF; + int b2 = color2 & 0xFF; + + int a = (int) Mth.lerp(t, a1, a2); + int r = (int) Mth.lerp(t, r1, r2); + int g = (int) Mth.lerp(t, g1, g2); + int b = (int) Mth.lerp(t, b1, b2); + + return (a << 24) | (r << 16) | (g << 8) | b; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/ModuleConfigPopout.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/ModuleConfigPopout.java new file mode 100644 index 000000000..dfe1f0f3f --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/ModuleConfigPopout.java @@ -0,0 +1,454 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine; +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.gregtechceu.gtceu.api.GTValues; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class ModuleConfigPopout extends WidgetGroup { + + public static final int WIDTH = 170; + public static final int HEIGHT = 140; + private static final int TITLE_HEIGHT = 16; + private static final int SETTINGS_BUTTON_SIZE = 24; + private static final ResourceLocation GEAR_TEXTURE = new ResourceLocation("gtceu", + "textures/item/material_sets/dull/gear_small.png"); + + private final Supplier machineSupplier; + private final Runnable onClose; + + private int moduleIndex = -1; + private boolean dragging = false; + private double lastDeltaX, lastDeltaY; + + private String moduleName = ""; + private boolean moduleConnected = false; + private boolean moduleWorking = false; + private long energyPerTick = 0; + private double speedBonus = 0; + private Stage irisStage = Stage.EMPTY; + + private int configuredMaxParallel = 1; + private long configuredVoltage = 32; + private int irisParallelLimit = 1; + + private long maxEUt = 0; + private int effectiveParallel = 1; + private int overclockTier = 0; + + private PowerControlPopup powerPopup; + private boolean showingPowerPopup = false; + private java.util.function.BiConsumer onPowerSettingsChanged; + + private float pulsePhase = 0f; + private float appearProgress = 0f; + + public ModuleConfigPopout(int x, int y, Supplier machineSupplier, Runnable onClose) { + super(x, y, WIDTH, HEIGHT); + this.machineSupplier = machineSupplier; + this.onClose = onClose; + setVisible(false); + initPowerPopup(); + } + + private void initPowerPopup() { + powerPopup = new PowerControlPopup(WIDTH + 10, 0, this::hidePowerPopup, this::onPowerSettingsApplied); + addWidget(powerPopup); + } + + public void setOnPowerSettingsChanged(java.util.function.BiConsumer callback) { + this.onPowerSettingsChanged = callback; + } + + private void showPowerPopup() { + showingPowerPopup = true; + powerPopup.show(configuredMaxParallel, configuredVoltage); + } + + private void hidePowerPopup() { + showingPowerPopup = false; + powerPopup.hide(); + } + + private void onPowerSettingsApplied(PowerControlPopup.PowerSettings settings) { + this.configuredMaxParallel = settings.maxParallel(); + this.configuredVoltage = settings.voltagePerParallel(); + + if (onPowerSettingsChanged != null) { + onPowerSettingsChanged.accept(configuredMaxParallel, configuredVoltage); + } + } + + public void showForModule(int index) { + this.moduleIndex = index; + this.appearProgress = 0f; + setVisible(true); + setActive(true); + } + + public void hide() { + setVisible(false); + setActive(false); + moduleIndex = -1; + hidePowerPopup(); + } + + @OnlyIn(Dist.CLIENT) + public void updateModuleData(String name, boolean connected, boolean working, long energy, double speed, + Stage stage, + int maxParallel, long voltage, int irisLimit, + long moduleMaxEUt, int moduleEffectiveParallel, int moduleOverclockTier) { + this.moduleName = name; + this.moduleConnected = connected; + this.moduleWorking = working; + this.energyPerTick = energy; + this.speedBonus = speed; + this.irisStage = stage; + this.configuredMaxParallel = maxParallel; + this.configuredVoltage = voltage; + this.irisParallelLimit = irisLimit; + this.maxEUt = moduleMaxEUt; + this.effectiveParallel = moduleEffectiveParallel; + this.overclockTier = moduleOverclockTier; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + pulsePhase += 0.1f; + + if (isVisible() && appearProgress < 1f) { + appearProgress = Math.min(1f, appearProgress + 0.15f); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + if (!isVisible()) return; + + float alpha = appearProgress; + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + int bgAlpha = (int) (0xE0 * alpha); + int bgColor = (bgAlpha << 24) | 0x0c0c14; + DrawerHelper.drawSolidRect(graphics, x, y, w, h, bgColor); + + int gridAlpha = (int) (0x08 * alpha); + int gridColor = (gridAlpha << 24) | 0xFFFFFF; + for (int gx = x + 8; gx < x + w; gx += 8) { + graphics.fill(gx, y, gx + 1, y + h, gridColor); + } + for (int gy = y + 8; gy < y + h; gy += 8) { + graphics.fill(x, gy, x + w, gy + 1, gridColor); + } + + int titleBgAlpha = (int) (0xC0 * alpha); + int titleBgColor = (titleBgAlpha << 24) | 0x101820; + DrawerHelper.drawSolidRect(graphics, x, y, w, TITLE_HEIGHT, titleBgColor); + + int accentColor = getAccentColor(); + int accentAlpha = (int) (0x80 * alpha); + int borderColor = (accentAlpha << 24) | (accentColor & 0x00FFFFFF); + + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 1); + graphics.fill(x + 1, y + TITLE_HEIGHT - 2, x + w - 1, y + TITLE_HEIGHT, borderColor); + + drawTitle(graphics, x, y, w, alpha); + drawContent(graphics, x, y + TITLE_HEIGHT + 4, w, alpha); + drawSettingsButton(graphics, x + w - SETTINGS_BUTTON_SIZE - 4, y + h - SETTINGS_BUTTON_SIZE - 4, mouseX, mouseY, + alpha); + drawCloseButton(graphics, x + w - 14, y + 3, mouseX, mouseY, alpha); + + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + } + + private void drawTitle(GuiGraphics graphics, int x, int y, int w, float alpha) { + var font = Minecraft.getInstance().font; + + String title = moduleName.isEmpty() ? Component.translatable("cosmiccore.stellar.module.config").getString() : + Component.translatable(moduleName).getString(); + int maxWidth = w - 24; + if (font.width(title) > maxWidth) { + while (font.width(title + "...") > maxWidth && title.length() > 1) { + title = title.substring(0, title.length() - 1); + } + title = title + "..."; + } + + int textColor = (int) (0xFF * alpha) << 24 | 0xFFFFFF; + graphics.drawString(font, title, x + 4, y + (TITLE_HEIGHT - font.lineHeight) / 2 + 1, textColor, false); + } + + private void drawContent(GuiGraphics graphics, int x, int y, int w, float alpha) { + var font = Minecraft.getInstance().font; + int labelColor = (int) (0xFF * alpha) << 24 | 0x808090; + int valueColor = (int) (0xFF * alpha) << 24 | 0xDDDDDD; + int accentColor = (int) (0xFF * alpha) << 24 | 0x80C0FF; + + int lineHeight = 11; + int contentX = x + 6; + int valueX = x + 70; + int currentY = y; + + String statusValue; + int statusColor; + if (moduleWorking) { + statusValue = Component.translatable("cosmiccore.stellar.module.status.processing").getString(); + statusColor = 0x44FF44; + } else if (moduleConnected) { + statusValue = Component.translatable("cosmiccore.stellar.module.status.idle").getString(); + statusColor = 0x6090CC; + } else { + statusValue = Component.translatable("cosmiccore.stellar.module.status.offline").getString(); + statusColor = 0xFF5555; + } + statusColor = (int) (0xFF * alpha) << 24 | statusColor; + + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.status").getString(), contentX, + currentY, labelColor, false); + graphics.drawString(font, statusValue, valueX, currentY, statusColor, false); + currentY += lineHeight; + + int sepAlpha = (int) (0x30 * alpha); + graphics.fill(contentX, currentY, x + w - 6, currentY + 1, (sepAlpha << 24) | 0x4080FF); + currentY += 4; + + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.max_eut").getString(), contentX, + currentY, labelColor, false); + String maxEUtStr = formatEnergy(maxEUt); + graphics.drawString(font, maxEUtStr, valueX, currentY, valueColor, false); + + String tierName = overclockTier < GTValues.VNF.length ? GTValues.VNF[overclockTier] : "MAX"; + int tierColor = getTierColor(overclockTier); + int badgeX = x + w - 6 - font.width(tierName) - 4; + int badgeAlpha = (int) (0x90 * alpha); + graphics.fill(badgeX - 2, currentY - 1, badgeX + font.width(tierName) + 2, currentY + font.lineHeight, + (badgeAlpha << 24) | (tierColor & 0x00333333)); + graphics.drawString(font, tierName, badgeX, currentY, (int) (0xFF * alpha) << 24 | tierColor, false); + currentY += lineHeight; + + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.parallel").getString(), contentX, + currentY, labelColor, false); + String parallelStr = effectiveParallel + "x"; + if (effectiveParallel < configuredMaxParallel) { + parallelStr = Component + .translatable("cosmiccore.stellar.module.parallel_max", effectiveParallel, configuredMaxParallel) + .getString(); + } + graphics.drawString(font, parallelStr, valueX, currentY, accentColor, false); + currentY += lineHeight; + + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.current").getString(), contentX, + currentY, labelColor, false); + if (energyPerTick > 0) { + String currentEUStr = formatEnergy(energyPerTick); + graphics.drawString(font, currentEUStr, valueX, currentY, (int) (0xFF * alpha) << 24 | 0xFFCC44, false); + } else { + graphics.drawString(font, "---", valueX, currentY, (int) (0x80 * alpha) << 24 | 0x606060, false); + } + currentY += lineHeight; + + graphics.fill(contentX, currentY, x + w - 6, currentY + 1, (sepAlpha << 24) | 0x4080FF); + currentY += 4; + + if (moduleConnected) { + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.speed_bonus").getString(), + contentX, currentY, labelColor, false); + String speedStr = speedBonus > 0 ? String.format("%.1fx", speedBonus) : "1.0x"; + int speedColor = speedBonus > 1.0 ? 0x66FF66 : 0xCCCCCC; + graphics.drawString(font, speedStr, valueX, currentY, (int) (0xFF * alpha) << 24 | speedColor, false); + currentY += lineHeight; + + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.iris_limit").getString(), + contentX, currentY, labelColor, false); + graphics.drawString(font, irisParallelLimit + "x", valueX, currentY, valueColor, false); + } else { + int disconnectedColor = (int) (0x80 * alpha) << 24 | 0x808080; + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.not_linked").getString(), + contentX, currentY, disconnectedColor, false); + } + + if (moduleWorking) { + int barY = y + HEIGHT - TITLE_HEIGHT - 14; + int barWidth = w - 12; + float progress = (float) (0.5f + 0.5f * Math.sin(System.currentTimeMillis() / 400.0)); + int fillWidth = (int) (barWidth * progress); + + int barBg = (int) (0x40 * alpha) << 24 | 0x000000; + int barFill = (int) (0xC0 * alpha) << 24 | 0x44FF44; + + graphics.fill(contentX, barY, contentX + barWidth, barY + 3, barBg); + graphics.fill(contentX, barY, contentX + fillWidth, barY + 3, barFill); + } + } + + private int getTierColor(int tier) { + return switch (tier) { + case 0 -> 0x808080; + case 1 -> 0xC0C0C0; + case 2 -> 0x00FFFF; + case 3 -> 0xFFFF00; + case 4 -> 0x0080FF; + case 5 -> 0x8000FF; + case 6 -> 0xFF0080; + case 7 -> 0xFF00FF; + case 8 -> 0x00FF00; + default -> 0xFF4040; + }; + } + + private void drawSettingsButton(GuiGraphics graphics, int x, int y, int mouseX, int mouseY, float alpha) { + int size = SETTINGS_BUTTON_SIZE; + boolean hovered = mouseX >= x && mouseX < x + size && mouseY >= y && mouseY < y + size; + + int bgColor = hovered ? (int) (0xC0 * alpha) << 24 | 0x4080FF : (int) (0x60 * alpha) << 24 | 0x404050; + + graphics.fill(x, y, x + size, y + size, bgColor); + graphics.blit(GEAR_TEXTURE, x, y, 0, 0, size, size, size, size); + } + + private void drawCloseButton(GuiGraphics graphics, int x, int y, int mouseX, int mouseY, float alpha) { + int size = 10; + boolean hovered = mouseX >= x && mouseX < x + size && mouseY >= y && mouseY < y + size; + + int bgColor = hovered ? (int) (0xC0 * alpha) << 24 | 0xFF4444 : (int) (0x60 * alpha) << 24 | 0x404050; + int fgColor = (int) (0xFF * alpha) << 24 | 0xFFFFFF; + + graphics.fill(x, y, x + size, y + size, bgColor); + graphics.fill(x + 2, y + 3, x + 4, y + 7, fgColor); + graphics.fill(x + 6, y + 3, x + 8, y + 7, fgColor); + graphics.fill(x + 3, y + 4, x + 7, y + 6, fgColor); + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isVisible()) return false; + + if (showingPowerPopup && powerPopup.isVisible()) { + if (powerPopup.mouseClicked(mouseX, mouseY, button)) { + return true; + } + } + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + int settingsX = x + w - SETTINGS_BUTTON_SIZE - 4; + int settingsY = y + h - SETTINGS_BUTTON_SIZE - 4; + if (mouseX >= settingsX && mouseX < settingsX + SETTINGS_BUTTON_SIZE && mouseY >= settingsY && + mouseY < settingsY + SETTINGS_BUTTON_SIZE) { + if (showingPowerPopup) { + hidePowerPopup(); + } else { + showPowerPopup(); + } + playButtonClickSound(); + return true; + } + + int closeX = x + w - 14; + int closeY = y + 3; + if (mouseX >= closeX && mouseX < closeX + 10 && mouseY >= closeY && mouseY < closeY + 10) { + if (onClose != null) { + onClose.run(); + } + hide(); + playButtonClickSound(); + return true; + } + + if (mouseX >= x && mouseX < x + w - 30 && mouseY >= y && mouseY < y + TITLE_HEIGHT) { + dragging = true; + lastDeltaX = 0; + lastDeltaY = 0; + return true; + } + + if (isMouseOverElement(mouseX, mouseY)) { + return true; + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + double dx = dragX + lastDeltaX; + double dy = dragY + lastDeltaY; + int intDx = (int) dx; + int intDy = (int) dy; + lastDeltaX = dx - intDx; + lastDeltaY = dy - intDy; + + if (dragging) { + addSelfPosition(intDx, intDy); + return true; + } + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (dragging) { + dragging = false; + lastDeltaX = 0; + lastDeltaY = 0; + return true; + } + return super.mouseReleased(mouseX, mouseY, button); + } + + private String formatEnergy(long eu) { + if (eu >= 1_000_000_000) return String.format("%.1fG EU/t", eu / 1_000_000_000.0); + if (eu >= 1_000_000) return String.format("%.1fM EU/t", eu / 1_000_000.0); + if (eu >= 1000) return String.format("%.1fk EU/t", eu / 1000.0); + return String.format("%d EU/t", eu); + } + + private int getAccentColor() { + if (moduleConnected) { + return getStageColorRaw(irisStage); + } + return 0x4080AA; + } + + private int getStageColorRaw(Stage stage) { + return switch (stage) { + case EMPTY -> 0x606060; + case GROWING -> 0x66AAFF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF8844; + case BLACK_HOLE -> 0xAA66FF; + case DEATH -> 0xFF4444; + case DEATH_GRACEFUL -> 0x886666; + }; + } + + public int getModuleIndex() { + return moduleIndex; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/ModuleSelectorWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/ModuleSelectorWidget.java new file mode 100644 index 000000000..cafd9ca54 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/ModuleSelectorWidget.java @@ -0,0 +1,484 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.feature.IStellarModuleReceiver; +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine; + +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class ModuleSelectorWidget extends Widget { + + public static final int MAX_MODULES = 8; + public static final int INNER_RING_COUNT = 4; + public static final int OUTER_RING_COUNT = 4; + + private final Supplier machineSupplier; + private final Consumer onModuleSelected; + + private final List moduleSlots = new ArrayList<>(); + private float animPhase = 0f; + private int hoveredSlot = -1; + private int selectedSlot = -1; + private float pulsePhase = 0f; + + public ModuleSelectorWidget(int x, int y, int size, Supplier machineSupplier, + Consumer onModuleSelected) { + super(x, y, size, size); + this.machineSupplier = machineSupplier; + this.onModuleSelected = onModuleSelected; + + for (int i = 0; i < MAX_MODULES; i++) { + moduleSlots.add(new ModuleSlotData()); + } + } + + @Override + public void detectAndSendChanges() { + // Always call super and sync data even when not visible + // so that data is ready when we become visible + super.detectAndSendChanges(); + + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine == null) return; + + List modules = new ArrayList<>(machine.getConnectedModules()); + + // Check for changes and sync + boolean changed = false; + for (int i = 0; i < MAX_MODULES; i++) { + ModuleSlotData slot = moduleSlots.get(i); + boolean hasModule = i < modules.size(); + String newName = hasModule ? getModuleName(modules.get(i)) : ""; + boolean newWorking = hasModule && isModuleWorking(modules.get(i)); + + if (slot.populated != hasModule || !slot.moduleName.equals(newName) || slot.working != newWorking) { + slot.populated = hasModule; + slot.moduleName = newName; + slot.working = newWorking; + changed = true; + } + } + + if (changed) { + writeUpdateInfo(200, buf -> { + for (ModuleSlotData slot : moduleSlots) { + buf.writeBoolean(slot.populated); + buf.writeUtf(slot.moduleName); + buf.writeBoolean(slot.working); + } + }); + } + } + + public void forceSync() { + detectAndSendChanges(); + } + + private String getModuleName(IStellarModuleReceiver module) { + if (module instanceof com.gregtechceu.gtceu.api.machine.MetaMachine metaMachine) { + return metaMachine.getBlockState().getBlock().getDescriptionId(); + } + return "Unknown Module"; + } + + private boolean isModuleWorking(IStellarModuleReceiver module) { + if (module instanceof com.gregtechceu.gtceu.api.machine.multiblock.WorkableMultiblockMachine workable) { + return workable.getRecipeLogic().isWorking(); + } + return false; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readUpdateInfo(int id, FriendlyByteBuf buffer) { + if (id == 200) { + for (ModuleSlotData slot : moduleSlots) { + slot.populated = buffer.readBoolean(); + slot.moduleName = buffer.readUtf(); + slot.working = buffer.readBoolean(); + } + } else { + super.readUpdateInfo(id, buffer); + } + } + + @Override + public void writeInitialData(FriendlyByteBuf buffer) { + super.writeInitialData(buffer); + + // Populate slot data before writing + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine != null) { + List modules = new ArrayList<>(machine.getConnectedModules()); + for (int i = 0; i < MAX_MODULES; i++) { + ModuleSlotData slot = moduleSlots.get(i); + boolean hasModule = i < modules.size(); + slot.populated = hasModule; + slot.moduleName = hasModule ? getModuleName(modules.get(i)) : ""; + slot.working = hasModule && isModuleWorking(modules.get(i)); + } + } + + for (ModuleSlotData slot : moduleSlots) { + buffer.writeBoolean(slot.populated); + buffer.writeUtf(slot.moduleName); + buffer.writeBoolean(slot.working); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readInitialData(FriendlyByteBuf buffer) { + super.readInitialData(buffer); + for (ModuleSlotData slot : moduleSlots) { + slot.populated = buffer.readBoolean(); + slot.moduleName = buffer.readUtf(); + slot.working = buffer.readBoolean(); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + animPhase += 0.02f; + pulsePhase += 0.08f; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int cx = getPosition().x + getSize().width / 2; + int cy = getPosition().y + getSize().height / 2; + int maxRadius = getSize().width / 2 - 5; + + // Draw the two rings + drawRings(graphics, cx, cy, maxRadius); + + // Draw central hub + drawCentralHub(graphics, cx, cy); + + // Draw module slots on both rings + drawModuleSlots(graphics, cx, cy, maxRadius, mouseX, mouseY); + + // Draw connecting lines from hub to slots + drawConnectingLines(graphics, cx, cy, maxRadius); + } + + private int getRingRadius(int ringIndex, int maxRadius) { + // Inner ring at 40% of max, outer ring at 75% of max + // Leaves room for future rings + return switch (ringIndex) { + case 0 -> (int) (maxRadius * 0.40); // Inner ring + case 1 -> (int) (maxRadius * 0.75); // Outer ring + default -> (int) (maxRadius * (0.40 + ringIndex * 0.35)); + }; + } + + private float getCardinalAngle(int slotInRing) { + // Start from top (North = -PI/2), go clockwise + return switch (slotInRing) { + case 0 -> -Mth.HALF_PI; // North (top) + case 1 -> 0f; // East (right) + case 2 -> Mth.HALF_PI; // South (bottom) + case 3 -> Mth.PI; // West (left) + default -> 0f; + }; + } + + private int[] getSlotPosition(int moduleIndex) { + int ring = moduleIndex / INNER_RING_COUNT; + int slotInRing = moduleIndex % INNER_RING_COUNT; + return new int[] { ring, slotInRing }; + } + + private void drawRings(GuiGraphics graphics, int cx, int cy, int maxRadius) { + // Draw both ring circles + int ringColor = 0x60606080; + int ringColorBright = 0x908080A0; + + for (int ringIndex = 0; ringIndex < 2; ringIndex++) { + int ringRadius = getRingRadius(ringIndex, maxRadius); + + // Draw ring circle + for (int angle = 0; angle < 360; angle += 3) { + float rad = angle * Mth.DEG_TO_RAD; + int px = cx + (int) (Mth.cos(rad) * ringRadius); + int py = cy + (int) (Mth.sin(rad) * ringRadius); + + // Brighter at cardinal points + boolean isCardinal = (angle % 90) < 10 || (angle % 90) > 80; + int color = isCardinal ? ringColorBright : ringColor; + graphics.fill(px, py, px + 1, py + 1, color); + } + } + + // Inner glow + int hubRadius = 15; + for (int r = hubRadius + 10; r > hubRadius; r -= 2) { + float progress = (float) (r - hubRadius) / 10f; + int alpha = (int) (20 * (1f - progress)); + int color = (alpha << 24) | 0x101020; + drawCircle(graphics, cx, cy, r, color); + } + } + + private void drawCentralHub(GuiGraphics graphics, int cx, int cy) { + int hubRadius = 15; + + // Hub glow + for (int r = hubRadius + 5; r > hubRadius; r--) { + int alpha = (int) (60 * (1f - (float) (r - hubRadius) / 5f)); + int color = (alpha << 24) | 0x4080AA; + drawCircle(graphics, cx, cy, r, color); + } + + // Hub body + drawCircle(graphics, cx, cy, hubRadius, 0xE0101820); + + // Hub border + int pulseAlpha = (int) (100 + 50 * Mth.sin(pulsePhase)); + int borderColor = (pulseAlpha << 24) | 0x4080AA; + for (int angle = 0; angle < 360; angle += 5) { + float rad = angle * Mth.DEG_TO_RAD; + int px = cx + (int) (Mth.cos(rad) * hubRadius); + int py = cy + (int) (Mth.sin(rad) * hubRadius); + graphics.fill(px, py, px + 1, py + 1, borderColor); + } + + // Hub icon (8 dots for modules) + var font = Minecraft.getInstance().font; + String icon = "\u2699"; // Gear icon + int textWidth = font.width(icon); + graphics.drawString(font, icon, cx - textWidth / 2, cy - font.lineHeight / 2, 0xFF4080AA, false); + } + + private void drawModuleSlots(GuiGraphics graphics, int cx, int cy, int maxRadius, int mouseX, int mouseY) { + int dotSize = 12; + + hoveredSlot = -1; + + for (int i = 0; i < MAX_MODULES; i++) { + // Get ring and position within ring + int[] pos = getSlotPosition(i); + int ringIndex = pos[0]; + int slotInRing = pos[1]; + + // Calculate position + int ringRadius = getRingRadius(ringIndex, maxRadius); + float angle = getCardinalAngle(slotInRing); + + int slotX = cx + (int) (Mth.cos(angle) * ringRadius); + int slotY = cy + (int) (Mth.sin(angle) * ringRadius); + + ModuleSlotData slot = moduleSlots.get(i); + + // Check hover + int dx = mouseX - slotX; + int dy = mouseY - slotY; + boolean hovered = dx * dx + dy * dy <= (dotSize + 2) * (dotSize + 2); + if (hovered) { + hoveredSlot = i; + } + + // Draw slot + drawModuleSlot(graphics, slotX, slotY, dotSize, slot, hovered, i == selectedSlot, ringIndex); + } + } + + private void drawModuleSlot(GuiGraphics graphics, int x, int y, int size, ModuleSlotData slot, + boolean hovered, boolean selected, int ringIndex) { + int halfSize = size / 2; + + // Determine colors - inner ring slightly different tint + int bgColor; + int borderColor; + int glowColor; + + if (slot.populated) { + if (slot.working) { + // Working - green pulse + int pulse = (int) (200 + 55 * Mth.sin(pulsePhase * 2)); + bgColor = 0xE0102010; + borderColor = (pulse << 24) | 0x44FF44; + glowColor = 0x4044FF44; + } else { + // Connected but idle - cyan (inner ring slightly more blue) + bgColor = ringIndex == 0 ? 0xE0101825 : 0xE0101820; + borderColor = ringIndex == 0 ? 0xFF5090BB : 0xFF4080AA; + glowColor = ringIndex == 0 ? 0x505090BB : 0x504080AA; + } + } else { + // Empty slot - more visible + bgColor = 0x90181820; + borderColor = 0x80606070; + glowColor = 0x00000000; + } + + // Hover/selected effects + if (selected) { + borderColor = 0xFFFFFFFF; + glowColor = 0x60FFFFFF; + } else if (hovered) { + borderColor = slot.populated ? 0xFFFFCC44 : 0x80AAAAAA; + glowColor = slot.populated ? 0x40FFCC44 : 0x20AAAAAA; + } + + // Draw glow + if (glowColor != 0) { + for (int r = halfSize + 5; r > halfSize; r--) { + int alpha = (glowColor >> 24) & 0xFF; + alpha = alpha * (halfSize + 5 - r) / 5; + int color = (alpha << 24) | (glowColor & 0x00FFFFFF); + drawCircle(graphics, x, y, r, color); + } + } + + // Draw slot background + drawCircle(graphics, x, y, halfSize, bgColor); + + // Draw border - solid circle + for (int angle = 0; angle < 360; angle += 10) { + float rad = angle * Mth.DEG_TO_RAD; + int px = x + (int) (Mth.cos(rad) * halfSize); + int py = y + (int) (Mth.sin(rad) * halfSize); + graphics.fill(px, py, px + 1, py + 1, borderColor); + } + + // Draw center indicator + if (!slot.populated) { + // Empty slot - small diamond shape + int dimColor = 0x60FFFFFF; + graphics.fill(x, y - 2, x + 1, y + 3, dimColor); + graphics.fill(x - 2, y, x + 3, y + 1, dimColor); + } else { + // Draw working indicator + if (slot.working) { + int indicatorPulse = (int) (255 * (0.5f + 0.5f * Mth.sin(pulsePhase * 3))); + graphics.fill(x - 2, y - 2, x + 3, y + 3, (indicatorPulse << 24) | 0x44FF44); + } else { + graphics.fill(x - 2, y - 2, x + 3, y + 3, 0xC04080AA); + } + } + } + + private void drawConnectingLines(GuiGraphics graphics, int cx, int cy, int maxRadius) { + int hubRadius = 18; + + for (int i = 0; i < MAX_MODULES; i++) { + ModuleSlotData slot = moduleSlots.get(i); + if (!slot.populated) continue; + + // Get ring and position + int[] pos = getSlotPosition(i); + int ringIndex = pos[0]; + int slotInRing = pos[1]; + + int ringRadius = getRingRadius(ringIndex, maxRadius); + float angle = getCardinalAngle(slotInRing); + + // Line from hub edge to slot + int startX = cx + (int) (Mth.cos(angle) * hubRadius); + int startY = cy + (int) (Mth.sin(angle) * hubRadius); + int endX = cx + (int) (Mth.cos(angle) * (ringRadius - 8)); + int endY = cy + (int) (Mth.sin(angle) * (ringRadius - 8)); + + int lineColor = slot.working ? 0xA044FF44 : 0x704080AA; + + // Draw dotted line + int segments = ringIndex == 0 ? 4 : 8; + for (int s = 0; s < segments; s += 2) { + float t1 = (float) s / segments; + float t2 = (float) (s + 1) / segments; + int x1 = (int) (startX + (endX - startX) * t1); + int y1 = (int) (startY + (endY - startY) * t1); + int x2 = (int) (startX + (endX - startX) * t2); + int y2 = (int) (startY + (endY - startY) * t2); + graphics.fill(Math.min(x1, x2), Math.min(y1, y2), + Math.max(x1, x2) + 1, Math.max(y1, y2) + 1, lineColor); + } + } + } + + private void drawCircle(GuiGraphics graphics, int cx, int cy, int radius, int color) { + if (radius <= 0) return; + for (int y = -radius; y <= radius; y++) { + int halfWidth = (int) Math.sqrt(radius * radius - y * y); + graphics.fill(cx - halfWidth, cy + y, cx + halfWidth + 1, cy + y + 1, color); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && hoveredSlot >= 0) { + ModuleSlotData slot = moduleSlots.get(hoveredSlot); + if (slot.populated) { + selectedSlot = hoveredSlot; + if (onModuleSelected != null) { + onModuleSelected.accept(hoveredSlot); + } + playButtonClickSound(); + return true; + } + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInForeground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInForeground(graphics, mouseX, mouseY, partialTicks); + + // Draw tooltip for hovered slot + if (hoveredSlot >= 0) { + ModuleSlotData slot = moduleSlots.get(hoveredSlot); + List tooltip = new ArrayList<>(); + + if (slot.populated) { + tooltip.add(Component.translatable(slot.moduleName)); + if (slot.working) { + tooltip.add(Component.literal("\u00A7aWorking")); + } else { + tooltip.add(Component.literal("\u00A77Idle")); + } + tooltip.add(Component.literal("\u00A78Click to configure")); + } else { + tooltip.add(Component.literal("\u00A78Empty Slot " + (hoveredSlot + 1))); + } + + graphics.renderTooltip(Minecraft.getInstance().font, tooltip, java.util.Optional.empty(), mouseX, mouseY); + } + } + + public void clearSelection() { + selectedSlot = -1; + } + + public int getSelectedSlot() { + return selectedSlot; + } + + private static class ModuleSlotData { + + boolean populated = false; + String moduleName = ""; + boolean working = false; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/ModuleToggleButton.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/ModuleToggleButton.java new file mode 100644 index 000000000..cc6e92d13 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/ModuleToggleButton.java @@ -0,0 +1,155 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.mojang.blaze3d.systems.RenderSystem; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class ModuleToggleButton extends Widget { + + private static final ResourceLocation GEAR_TEXTURE = new ResourceLocation("gtceu", + "textures/item/material_sets/dull/gear_small.png"); + + private final Consumer onToggle; + private final Supplier stageSupplier; + private boolean showingModules = false; + private boolean hovered = false; + private float hoverProgress = 0f; + private float pulsePhase = 0f; + + public ModuleToggleButton(int x, int y, int width, int height, Consumer onToggle, + Supplier stageSupplier) { + super(x, y, width, height); + this.onToggle = onToggle; + this.stageSupplier = stageSupplier; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + pulsePhase += 0.1f; + + if (hovered && hoverProgress < 1f) { + hoverProgress = Math.min(1f, hoverProgress + 0.15f); + } else if (!hovered && hoverProgress > 0f) { + hoverProgress = Math.max(0f, hoverProgress - 0.1f); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + hovered = isMouseOverElement(mouseX, mouseY); + + int bgAlpha = (int) (0xC0 + 0x20 * hoverProgress); + int bgColor = (bgAlpha << 24) | 0x101820; + DrawerHelper.drawSolidRect(graphics, x, y, w, h, bgColor); + + Stage stage = stageSupplier != null ? stageSupplier.get() : Stage.EMPTY; + int accentColor = getStageColor(stage); + int borderAlpha = (int) (0x60 + 0x40 * hoverProgress); + int borderColor = (borderAlpha << 24) | accentColor; + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 1); + + drawIcon(graphics, x, y, w, h); + + if (hoverProgress > 0) { + int glowAlpha = (int) (0x20 * hoverProgress); + int glowColor = (glowAlpha << 24) | accentColor; + DrawerHelper.drawBorder(graphics, x - 1, y - 1, w + 2, h + 2, glowColor, 1); + } + } + + private void drawIcon(GuiGraphics graphics, int x, int y, int w, int h) { + Stage stage = stageSupplier != null ? stageSupplier.get() : Stage.EMPTY; + int stageColor = getStageColor(stage); + float pulseAlpha = 0.8f + 0.2f * Mth.sin(pulsePhase); + + float r = ((stageColor >> 16) & 0xFF) / 255f; + float g = ((stageColor >> 8) & 0xFF) / 255f; + float b = (stageColor & 0xFF) / 255f; + + RenderSystem.enableBlend(); + RenderSystem.setShaderColor(r, g, b, pulseAlpha); + + int gearSize = Math.min(w, h) - 4; + int gearX = x + (w - gearSize) / 2; + int gearY = y + (h - gearSize) / 2; + graphics.blit(GEAR_TEXTURE, gearX, gearY, 0, 0, gearSize, gearSize, gearSize, gearSize); + + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + RenderSystem.disableBlend(); + } + + private int getStageColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0x404050; // Gray + case GROWING -> 0x6080FF; // Blue + case STAR -> 0xFFCC44; // Yellow/Gold + case SUPERSTAR -> 0xFF8844; // Orange + case BLACK_HOLE -> 0x8040FF; // Purple + case DEATH -> 0xFF2020; // Red + case DEATH_GRACEFUL -> 0x804040; // Dark red + }; + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && isMouseOverElement(mouseX, mouseY)) { + showingModules = !showingModules; + if (onToggle != null) { + onToggle.accept(showingModules); + } + playButtonClickSound(); + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInForeground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInForeground(graphics, mouseX, mouseY, partialTicks); + + if (hovered) { + String tooltipText = showingModules ? "cosmiccore.gui.stellar.show_star" : + "cosmiccore.gui.stellar.show_modules"; + graphics.renderTooltip(Minecraft.getInstance().font, + List.of(Component.translatable(tooltipText)), + java.util.Optional.empty(), mouseX, mouseY); + } + } + + public boolean isShowingModules() { + return showingModules; + } + + public void setShowingModules(boolean showing) { + this.showingModules = showing; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/OrbitalRingsWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/OrbitalRingsWidget.java new file mode 100644 index 000000000..23ced7111 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/OrbitalRingsWidget.java @@ -0,0 +1,287 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class OrbitalRingsWidget extends Widget { + + private final Supplier stageSupplier; + private final int centerX; + private final int centerY; + + private float rotationPhase = 0f; + private float wobblePhase = 0f; + private float pulsePhase = 0f; + + public OrbitalRingsWidget(int x, int y, int width, int height, Supplier stageSupplier) { + super(x, y, width, height); + this.stageSupplier = stageSupplier; + this.centerX = width / 2; + this.centerY = height / 2; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + + Stage stage = stageSupplier.get(); + float rotSpeed = getRotationSpeed(stage); + + rotationPhase += rotSpeed; + wobblePhase += rotSpeed * 0.7f; + pulsePhase += 0.08f; + } + + private float getRotationSpeed(Stage stage) { + return switch (stage) { + case EMPTY -> 0.005f; + case GROWING -> 0.02f; + case STAR -> 0.015f; + case SUPERSTAR -> 0.03f; + case BLACK_HOLE -> 0.06f; + case DEATH -> 0.1f; + case DEATH_GRACEFUL -> 0.003f; + }; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int x = getPosition().x; + int y = getPosition().y; + int cx = x + centerX; + int cy = y + centerY; + + Stage stage = stageSupplier.get(); + int baseColor = getStageColor(stage); + + int ringCount = getRingCount(stage); + for (int ring = 0; ring < ringCount; ring++) { + drawOrbitalRing(graphics, cx, cy, ring, baseColor, stage); + } + + if (stage != Stage.EMPTY && stage != Stage.DEATH_GRACEFUL) { + drawOrbitalParticles(graphics, cx, cy, baseColor, stage); + } + + if (stage == Stage.BLACK_HOLE) { + drawAccretionDisk(graphics, cx, cy); + } + } + + private int getRingCount(Stage stage) { + return switch (stage) { + case EMPTY -> 1; + case GROWING -> 2; + case STAR -> 3; + case SUPERSTAR -> 4; + case BLACK_HOLE -> 5; + case DEATH -> 3; + case DEATH_GRACEFUL -> 2; + }; + } + + private void drawOrbitalRing(GuiGraphics graphics, int cx, int cy, int ringIndex, int baseColor, Stage stage) { + float baseRadius = 25 + ringIndex * 15; + float tilt = 0.3f + ringIndex * 0.1f; + float ringRotation = rotationPhase * (1f + ringIndex * 0.3f) + ringIndex * 1.2f; + float wobble = Mth.sin(wobblePhase + ringIndex * 0.8f) * 0.05f; + + float pulse = Mth.sin(pulsePhase + ringIndex * 0.5f) * 0.2f + 0.8f; + int alpha = (int) (0x40 * pulse); + + if (stage == Stage.DEATH) { + alpha = (int) (alpha * (0.5f + Math.random() * 0.5f)); + baseRadius += (float) (Math.random() - 0.5) * 5; + } + + int color = (alpha << 24) | (baseColor & 0x00FFFFFF); + + int segments = 60; + for (int i = 0; i < segments; i++) { + float angle = ringRotation + (i * Mth.TWO_PI / segments); + float nextAngle = ringRotation + ((i + 1) * Mth.TWO_PI / segments); + + float segmentBrightness = (Mth.sin(angle * 3 + pulsePhase) + 1f) * 0.3f + 0.4f; + int segmentAlpha = (int) (alpha * segmentBrightness); + int segmentColor = (segmentAlpha << 24) | (baseColor & 0x00FFFFFF); + + float x1 = cx + Mth.cos(angle) * baseRadius; + float y1 = cy + Mth.sin(angle) * baseRadius * (tilt + wobble); + float x2 = cx + Mth.cos(nextAngle) * baseRadius; + float y2 = cy + Mth.sin(nextAngle) * baseRadius * (tilt + wobble); + + drawLine(graphics, (int) x1, (int) y1, (int) x2, (int) y2, segmentColor); + } + + if (stage == Stage.STAR || stage == Stage.SUPERSTAR) { + int glowAlpha = alpha / 3; + int glowColor = (glowAlpha << 24) | (baseColor & 0x00FFFFFF); + float glowRadius = baseRadius + 2; + + for (int i = 0; i < segments; i += 2) { + float angle = ringRotation + (i * Mth.TWO_PI / segments); + float gx = cx + Mth.cos(angle) * glowRadius; + float gy = cy + Mth.sin(angle) * glowRadius * tilt; + graphics.fill((int) gx - 1, (int) gy - 1, (int) gx + 2, (int) gy + 2, glowColor); + } + } + } + + private void drawOrbitalParticles(GuiGraphics graphics, int cx, int cy, int baseColor, Stage stage) { + int particleCount = switch (stage) { + case GROWING -> 4; + case STAR -> 6; + case SUPERSTAR -> 10; + case BLACK_HOLE -> 15; + case DEATH -> 8; + default -> 2; + }; + + for (int i = 0; i < particleCount; i++) { + float particleOrbit = 20 + (i * 37) % 50; + float particleSpeed = 1f + (i % 3) * 0.5f; + float particleAngle = rotationPhase * particleSpeed + i * 0.9f; + float particleTilt = 0.2f + (i % 4) * 0.1f; + + float px = cx + Mth.cos(particleAngle) * particleOrbit; + float py = cy + Mth.sin(particleAngle) * particleOrbit * particleTilt; + + float brightness = Mth.sin(pulsePhase + i * 0.7f) * 0.4f + 0.6f; + int alpha = (int) (0xC0 * brightness); + + int particleColor; + if (stage == Stage.BLACK_HOLE) { + float hue = (particleAngle * 0.1f) % 1f; + particleColor = (alpha << 24) | hslToRgb(hue, 0.8f, 0.6f); + } else { + particleColor = (alpha << 24) | (baseColor & 0x00FFFFFF); + } + + int size = 1 + (i % 2); + graphics.fill((int) px - size, (int) py - size, (int) px + size + 1, (int) py + size + 1, particleColor); + + int trailLength = 3; + for (int t = 1; t <= trailLength; t++) { + float trailAngle = particleAngle - t * 0.15f; + float tx = cx + Mth.cos(trailAngle) * particleOrbit; + float ty = cy + Mth.sin(trailAngle) * particleOrbit * particleTilt; + int trailAlpha = alpha / (t + 1); + int trailColor = (trailAlpha << 24) | (baseColor & 0x00FFFFFF); + graphics.fill((int) tx, (int) ty, (int) tx + 1, (int) ty + 1, trailColor); + } + } + } + + private void drawAccretionDisk(GuiGraphics graphics, int cx, int cy) { + float diskRadius = 55; + float innerRadius = 20; + + int diskSegments = 120; + for (int i = 0; i < diskSegments; i++) { + float angle = rotationPhase * 0.3f + i * Mth.TWO_PI / diskSegments; + float radiusVariation = Mth.sin(angle * 8 + pulsePhase * 2) * 5; + float currentRadius = diskRadius + radiusVariation; + + float hue = ((angle + rotationPhase) * 0.15f) % 1f; + float brightness = 0.3f + Mth.sin(angle * 4 + pulsePhase) * 0.2f; + + for (float r = innerRadius; r < currentRadius; r += 3) { + float radialBrightness = 1f - (r - innerRadius) / (currentRadius - innerRadius); + int alpha = (int) (0x30 * radialBrightness * brightness); + int color = (alpha << 24) | hslToRgb(hue, 0.7f, 0.5f + radialBrightness * 0.3f); + + float dx = cx + Mth.cos(angle) * r; + float dy = cy + Mth.sin(angle) * r * 0.25f; + graphics.fill((int) dx, (int) dy, (int) dx + 2, (int) dy + 1, color); + } + } + } + + private void drawLine(GuiGraphics graphics, int x1, int y1, int x2, int y2, int color) { + int dx = Math.abs(x2 - x1); + int dy = Math.abs(y2 - y1); + int sx = x1 < x2 ? 1 : -1; + int sy = y1 < y2 ? 1 : -1; + int err = dx - dy; + + while (true) { + graphics.fill(x1, y1, x1 + 1, y1 + 1, color); + + if (x1 == x2 && y1 == y2) break; + int e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x1 += sx; + } + if (e2 < dx) { + err += dx; + y1 += sy; + } + } + } + + private int hslToRgb(float h, float s, float l) { + float c = (1 - Math.abs(2 * l - 1)) * s; + float x = c * (1 - Math.abs((h * 6) % 2 - 1)); + float m = l - c / 2; + + float r, g, b; + if (h < 1f / 6) { + r = c; + g = x; + b = 0; + } else if (h < 2f / 6) { + r = x; + g = c; + b = 0; + } else if (h < 3f / 6) { + r = 0; + g = c; + b = x; + } else if (h < 4f / 6) { + r = 0; + g = x; + b = c; + } else if (h < 5f / 6) { + r = x; + g = 0; + b = c; + } else { + r = c; + g = 0; + b = x; + } + + int ri = (int) ((r + m) * 255); + int gi = (int) ((g + m) * 255); + int bi = (int) ((b + m) * 255); + + return (ri << 16) | (gi << 8) | bi; + } + + private int getStageColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0x506080; + case GROWING -> 0x80A0FF; + case STAR -> 0xFFDD66; + case SUPERSTAR -> 0xFF9944; + case BLACK_HOLE -> 0xBB66FF; + case DEATH -> 0xFF4040; + case DEATH_GRACEFUL -> 0x806060; + }; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PowerControlPopup.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PowerControlPopup.java new file mode 100644 index 000000000..84b48218d --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PowerControlPopup.java @@ -0,0 +1,303 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.gregtechceu.gtceu.api.GTValues; +import com.gregtechceu.gtceu.utils.GTUtil; + +import com.lowdragmc.lowdraglib.gui.texture.ColorBorderTexture; +import com.lowdragmc.lowdraglib.gui.texture.ColorRectTexture; +import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.TextFieldWidget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Consumer; + +import javax.annotation.Nonnull; + +public class PowerControlPopup extends WidgetGroup { + + public static final int WIDTH = 150; + public static final int HEIGHT = 90; + private static final int TITLE_HEIGHT = 16; + private static final int FIELD_HEIGHT = 20; + + private final Runnable onClose; + private final Consumer onApply; + + private int maxParallel = 1; + private long voltagePerParallel = 32; + + private TextFieldWidget parallelField; + private TextFieldWidget voltageField; + + private boolean dragging = false; + private double lastDeltaX, lastDeltaY; + private float appearProgress = 0f; + + public PowerControlPopup(int x, int y, Runnable onClose, Consumer onApply) { + super(x, y, WIDTH, HEIGHT); + this.onClose = onClose; + this.onApply = onApply; + setVisible(false); + initFields(); + } + + private void initFields() { + int fieldX = 6; + int fieldWidth = WIDTH - 12; + int labelY = TITLE_HEIGHT + 4; + + parallelField = new TextFieldWidget(fieldX, labelY + 12, fieldWidth - 12, FIELD_HEIGHT - 4, + () -> String.valueOf(maxParallel), + this::onParallelChanged); + parallelField.setClientSideWidget(); + parallelField.setNumbersOnly(1, Integer.MAX_VALUE); + parallelField.setMaxStringLength(10); + parallelField.setBackground(new GuiTextureGroup( + new ColorRectTexture(0xE0101018), + new ColorBorderTexture(1, 0xFF404060))); + addWidget(parallelField); + + int voltageY = labelY + FIELD_HEIGHT + 16; + voltageField = new TextFieldWidget(fieldX, voltageY + 12, fieldWidth - 12, FIELD_HEIGHT - 4, + () -> String.valueOf(voltagePerParallel), + this::onVoltageChanged); + voltageField.setClientSideWidget(); + voltageField.setNumbersOnly(1L, Long.MAX_VALUE); + voltageField.setMaxStringLength(20); + voltageField.setBackground(new GuiTextureGroup( + new ColorRectTexture(0xE0101018), + new ColorBorderTexture(1, 0xFF404060))); + addWidget(voltageField); + } + + public void show(int parallel, long voltage) { + this.maxParallel = parallel; + this.voltagePerParallel = voltage; + this.appearProgress = 0f; + setVisible(true); + setActive(true); + + if (parallelField != null) { + parallelField.setCurrentString(String.valueOf(parallel)); + } + if (voltageField != null) { + voltageField.setCurrentString(String.valueOf(voltage)); + } + } + + public void hide() { + setVisible(false); + setActive(false); + } + + private void onParallelChanged(String text) { + try { + int value = Integer.parseInt(text); + maxParallel = Math.max(1, value); + applySettings(); + } catch (NumberFormatException ignored) {} + } + + private void onVoltageChanged(String text) { + try { + long value = Long.parseLong(text); + voltagePerParallel = Math.max(1, value); + applySettings(); + } catch (NumberFormatException ignored) {} + } + + private void applySettings() { + if (onApply != null) { + onApply.accept(new PowerSettings(maxParallel, voltagePerParallel)); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + if (isVisible() && appearProgress < 1f) { + appearProgress = Math.min(1f, appearProgress + 0.15f); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + if (!isVisible()) return; + + float alpha = appearProgress; + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + int bgAlpha = (int) (0xE8 * alpha); + int bgColor = (bgAlpha << 24) | 0x0c0c14; + DrawerHelper.drawSolidRect(graphics, x, y, w, h, bgColor); + + int gridAlpha = (int) (0x08 * alpha); + int gridColor = (gridAlpha << 24) | 0xFFFFFF; + for (int gx = x + 8; gx < x + w; gx += 8) { + graphics.fill(gx, y, gx + 1, y + h, gridColor); + } + for (int gy = y + 8; gy < y + h; gy += 8) { + graphics.fill(x, gy, x + w, gy + 1, gridColor); + } + + int titleBgAlpha = (int) (0xD0 * alpha); + int titleBgColor = (titleBgAlpha << 24) | 0x101820; + DrawerHelper.drawSolidRect(graphics, x, y, w, TITLE_HEIGHT, titleBgColor); + + int borderAlpha = (int) (0x80 * alpha); + int borderColor = (borderAlpha << 24) | 0x4080FF; + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 1); + + graphics.fill(x + 1, y + TITLE_HEIGHT - 2, x + w - 1, y + TITLE_HEIGHT, borderColor); + + drawTitle(graphics, x, y, w, alpha); + drawLabels(graphics, x, y, alpha); + drawCloseButton(graphics, x + w - 14, y + 3, mouseX, mouseY, alpha); + drawTierIndicator(graphics, x, y, alpha); + + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + } + + private void drawTitle(GuiGraphics graphics, int x, int y, int w, float alpha) { + var font = Minecraft.getInstance().font; + String title = Component.translatable("cosmiccore.stellar.power.title").getString(); + int textColor = (int) (0xFF * alpha) << 24 | 0xFFFFFF; + graphics.drawString(font, title, x + 4, y + (TITLE_HEIGHT - font.lineHeight) / 2 + 1, textColor, false); + } + + private void drawLabels(GuiGraphics graphics, int x, int y, float alpha) { + var font = Minecraft.getInstance().font; + int labelColor = (int) (0xFF * alpha) << 24 | 0xA0A0B0; + + int labelY = y + TITLE_HEIGHT + 4; + graphics.drawString(font, Component.translatable("cosmiccore.stellar.power.max_parallel").getString(), x + 6, + labelY, labelColor, false); + + int voltageY = labelY + FIELD_HEIGHT + 16; + graphics.drawString(font, Component.translatable("cosmiccore.stellar.power.voltage_per_parallel").getString(), + x + 6, voltageY, labelColor, false); + } + + private void drawTierIndicator(GuiGraphics graphics, int x, int y, float alpha) { + var font = Minecraft.getInstance().font; + int tier = GTUtil.getTierByVoltage(voltagePerParallel); + String tierName = GTValues.VNF[Math.min(tier, GTValues.VNF.length - 1)]; + + int labelY = y + TITLE_HEIGHT + 4 + FIELD_HEIGHT + 16; + int badgeX = x + WIDTH - 6 - font.width(tierName) - 4; + + int tierColor = getTierColor(tier); + int badgeAlpha = (int) (0x80 * alpha); + int badgeBgColor = (badgeAlpha << 24) | (tierColor & 0x00FFFFFF); + + graphics.fill(badgeX - 2, labelY - 1, badgeX + font.width(tierName) + 2, labelY + font.lineHeight + 1, + badgeBgColor); + int textColor = (int) (0xFF * alpha) << 24 | 0xFFFFFF; + graphics.drawString(font, tierName, badgeX, labelY, textColor, false); + } + + private int getTierColor(int tier) { + return switch (tier) { + case 0 -> 0x808080; + case 1 -> 0xC0C0C0; + case 2 -> 0x00FFFF; + case 3 -> 0xFFFF00; + case 4 -> 0x0080FF; + case 5 -> 0x8000FF; + case 6 -> 0xFF0080; + case 7 -> 0xFF00FF; + case 8 -> 0x00FF00; + default -> 0xFF4040; + }; + } + + private void drawCloseButton(GuiGraphics graphics, int x, int y, int mouseX, int mouseY, float alpha) { + int size = 10; + boolean hovered = mouseX >= x && mouseX < x + size && mouseY >= y && mouseY < y + size; + + int bgColor = hovered ? (int) (0xC0 * alpha) << 24 | 0xFF4444 : (int) (0x60 * alpha) << 24 | 0x404050; + int fgColor = (int) (0xFF * alpha) << 24 | 0xFFFFFF; + + graphics.fill(x, y, x + size, y + size, bgColor); + graphics.fill(x + 2, y + 3, x + 4, y + 7, fgColor); + graphics.fill(x + 6, y + 3, x + 8, y + 7, fgColor); + graphics.fill(x + 3, y + 4, x + 7, y + 6, fgColor); + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isVisible()) return false; + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + + int closeX = x + w - 14; + int closeY = y + 3; + if (mouseX >= closeX && mouseX < closeX + 10 && mouseY >= closeY && mouseY < closeY + 10) { + if (onClose != null) { + onClose.run(); + } + hide(); + playButtonClickSound(); + return true; + } + + if (mouseX >= x && mouseX < x + w && mouseY >= y && mouseY < y + TITLE_HEIGHT) { + dragging = true; + lastDeltaX = 0; + lastDeltaY = 0; + return true; + } + + if (isMouseOverElement(mouseX, mouseY)) { + return super.mouseClicked(mouseX, mouseY, button); + } + + return false; + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + double dx = dragX + lastDeltaX; + double dy = dragY + lastDeltaY; + int intDx = (int) dx; + int intDy = (int) dy; + lastDeltaX = dx - intDx; + lastDeltaY = dy - intDy; + + if (dragging) { + addSelfPosition(intDx, intDy); + return true; + } + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (dragging) { + dragging = false; + lastDeltaX = 0; + lastDeltaY = 0; + return true; + } + return super.mouseReleased(mouseX, mouseY, button); + } + + public record PowerSettings(int maxParallel, long voltagePerParallel) {} +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PrestigeAnimationOverlay.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PrestigeAnimationOverlay.java new file mode 100644 index 000000000..cafcae6a9 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PrestigeAnimationOverlay.java @@ -0,0 +1,332 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine; +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.sounds.SoundEvents; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.Random; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class PrestigeAnimationOverlay extends Widget { + + private static final int UPDATE_ID_ANIMATION_STATE = 400; + + private static final int PHASE_FLICKER_DURATION = 40; + private static final int PHASE_SHRINK_DURATION = 100; + private static final int PHASE_FADE_DURATION = 60; + private static final int PHASE_WINDOW_FADE_IN = 40; + private static final int TOTAL_ANIMATION_TICKS = PHASE_FLICKER_DURATION + PHASE_SHRINK_DURATION + + PHASE_FADE_DURATION + PHASE_WINDOW_FADE_IN; + + private final Supplier machineSupplier; + private final Runnable onAnimationComplete; + private final Runnable onShowPrestigeWindow; + + private StellarCoreWidget coreWidget; + + private boolean animationActive = false; + private int animationTick = 0; + private int pointsEarned = 0; + + private final Random random = new Random(); + private float flickerIntensity = 0f; + private float starScale = 1f; + private float starAlpha = 1f; + private float windowAlpha = 0f; + + private int[] glitchOffsets = new int[6]; + private boolean[] scanlineHits = new boolean[20]; + + public PrestigeAnimationOverlay(int x, int y, int width, int height, + Supplier machineSupplier, + Runnable onAnimationComplete, + Runnable onShowPrestigeWindow) { + super(x, y, width, height); + this.machineSupplier = machineSupplier; + this.onAnimationComplete = onAnimationComplete; + this.onShowPrestigeWindow = onShowPrestigeWindow; + } + + public void setCoreWidget(StellarCoreWidget coreWidget) { + this.coreWidget = coreWidget; + } + + public void startAnimation(Stage currentStage, int starColor, int points) { + animationActive = true; + animationTick = 0; + pointsEarned = points; + + starScale = 1f; + starAlpha = 1f; + windowAlpha = 0f; + flickerIntensity = 1f; + + if (coreWidget != null) { + coreWidget.setPrestigeAnimating(true); + coreWidget.setPrestigeScale(1f); + coreWidget.setPrestigeAlpha(1f); + } + + regenerateGlitchData(); + + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.BEACON_DEACTIVATE, 0.5f, 0.8f)); + } + + private void regenerateGlitchData() { + for (int i = 0; i < glitchOffsets.length; i++) { + glitchOffsets[i] = random.nextInt(20) - 10; + } + for (int i = 0; i < scanlineHits.length; i++) { + scanlineHits[i] = random.nextFloat() < 0.3f; + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + + if (!animationActive) return; + + animationTick++; + + if (animationTick <= PHASE_FLICKER_DURATION) { + updateFlickerPhase(); + } else if (animationTick <= PHASE_FLICKER_DURATION + PHASE_SHRINK_DURATION) { + updateShrinkPhase(); + } else if (animationTick <= PHASE_FLICKER_DURATION + PHASE_SHRINK_DURATION + PHASE_FADE_DURATION) { + updateFadePhase(); + } else if (animationTick <= TOTAL_ANIMATION_TICKS) { + updateWindowFadePhase(); + } else { + completeAnimation(); + } + + if (coreWidget != null) { + coreWidget.setPrestigeScale(starScale); + coreWidget.setPrestigeAlpha(starAlpha); + } + + if (animationTick <= PHASE_FLICKER_DURATION && animationTick % 3 == 0) { + regenerateGlitchData(); + } + } + + private void updateFlickerPhase() { + float progress = (float) animationTick / PHASE_FLICKER_DURATION; + flickerIntensity = 1f - progress * 0.3f; + + if (random.nextFloat() < 0.15f) { + float pitch = 0.8f + random.nextFloat() * 0.4f; + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.REDSTONE_TORCH_BURNOUT, pitch, 0.3f)); + } + } + + private void updateShrinkPhase() { + int phaseProgress = animationTick - PHASE_FLICKER_DURATION; + float progress = (float) phaseProgress / PHASE_SHRINK_DURATION; + + float easedProgress = 1f - (1f - progress) * (1f - progress); + starScale = 1f - easedProgress * 0.9f; + + flickerIntensity = (1f - progress) * 0.5f; + + if (random.nextFloat() < 0.05f) { + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.FIRE_EXTINGUISH, 0.7f, 0.2f)); + } + } + + private void updateFadePhase() { + int phaseProgress = animationTick - PHASE_FLICKER_DURATION - PHASE_SHRINK_DURATION; + float progress = (float) phaseProgress / PHASE_FADE_DURATION; + + starScale = 0.1f - progress * 0.1f; + + float easedProgress = progress * progress; + starAlpha = 1f - easedProgress; + + flickerIntensity = 0f; + + if (phaseProgress == PHASE_FADE_DURATION - 10) { + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.SOUL_ESCAPE, 1.0f, 0.5f)); + } + } + + private void updateWindowFadePhase() { + int phaseProgress = animationTick - PHASE_FLICKER_DURATION - PHASE_SHRINK_DURATION - PHASE_FADE_DURATION; + float progress = (float) phaseProgress / PHASE_WINDOW_FADE_IN; + + windowAlpha = 1f - (1f - progress) * (1f - progress); + + starScale = 0f; + starAlpha = 0f; + + if (phaseProgress == 1) { + onShowPrestigeWindow.run(); + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.EXPERIENCE_ORB_PICKUP, 1.2f, 0.8f)); + } + } + + private void completeAnimation() { + animationActive = false; + + if (coreWidget != null) { + coreWidget.setPrestigeAnimating(false); + } + + onAnimationComplete.run(); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInForeground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInForeground(graphics, mouseX, mouseY, partialTicks); + + if (!animationActive) return; + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + if (flickerIntensity > 0) { + drawFlickerEffect(graphics, x, y, w, h); + } + + drawVignette(graphics, x, y, w, h); + } + + private void drawFlickerEffect(GuiGraphics graphics, int x, int y, int w, int h) { + for (int i = 0; i < scanlineHits.length; i++) { + if (scanlineHits[i] && random.nextFloat() < flickerIntensity) { + int scanY = y + (h * i / scanlineHits.length); + int scanH = h / scanlineHits.length; + int offset = glitchOffsets[i % glitchOffsets.length]; + + int redAlpha = (int) (30 * flickerIntensity); + int cyanAlpha = (int) (30 * flickerIntensity); + + graphics.fill(x + offset - 2, scanY, x + w + offset - 2, scanY + scanH, + (redAlpha << 24) | 0xFF0000); + graphics.fill(x - offset + 2, scanY, x + w - offset + 2, scanY + scanH, + (cyanAlpha << 24) | 0x00FFFF); + } + } + + if (random.nextFloat() < flickerIntensity * 0.3f) { + int flashAlpha = (int) (40 * flickerIntensity * random.nextFloat()); + graphics.fill(x, y, x + w, y + h, (flashAlpha << 24) | 0xFFFFFF); + } + + if (flickerIntensity > 0.5f) { + drawStaticNoise(graphics, x, y, w, h, flickerIntensity); + } + + for (int row = 0; row < h; row += 3) { + if (random.nextFloat() < flickerIntensity * 0.1f) { + int lineAlpha = (int) (20 * flickerIntensity); + graphics.fill(x, y + row, x + w, y + row + 1, (lineAlpha << 24) | 0x000000); + } + } + } + + private void drawStaticNoise(GuiGraphics graphics, int x, int y, int w, int h, float intensity) { + int noiseCount = (int) (50 * intensity); + for (int i = 0; i < noiseCount; i++) { + int nx = x + random.nextInt(w); + int ny = y + random.nextInt(h); + int size = 1 + random.nextInt(3); + int gray = random.nextInt(256); + int alpha = (int) (100 * intensity); + int color = (alpha << 24) | (gray << 16) | (gray << 8) | gray; + graphics.fill(nx, ny, nx + size, ny + size, color); + } + } + + private void drawVignette(GuiGraphics graphics, int x, int y, int w, int h) { + float vignetteStrength = 0.3f + flickerIntensity * 0.3f; + int edgeAlpha = (int) (100 * vignetteStrength); + + int edgeHeight = h / 6; + for (int row = 0; row < edgeHeight; row++) { + float progress = (float) row / edgeHeight; + int rowAlpha = (int) (edgeAlpha * (1f - progress)); + int rowColor = rowAlpha << 24; + graphics.fill(x, y + row, x + w, y + row + 1, rowColor); + graphics.fill(x, y + h - 1 - row, x + w, y + h - row, rowColor); + } + } + + public boolean isAnimationActive() { + return animationActive; + } + + public float getWindowAlpha() { + return windowAlpha; + } + + public int getAnimationTick() { + return animationTick; + } + + public int getPointsEarned() { + return pointsEarned; + } + + public float getStarScale() { + return starScale; + } + + public float getStarAlpha() { + return starAlpha; + } + + @Override + public void detectAndSendChanges() { + super.detectAndSendChanges(); + + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine == null) return; + + if (machine.isPrestigeAnimationActive() && !animationActive) { + writeUpdateInfo(UPDATE_ID_ANIMATION_STATE, buf -> { + buf.writeBoolean(true); + buf.writeEnum(machine.getStage()); + buf.writeInt(machine.getCustomStarColor()); + buf.writeInt(machine.getLastPrestigePointsEarned()); + }); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readUpdateInfo(int id, FriendlyByteBuf buffer) { + if (id == UPDATE_ID_ANIMATION_STATE) { + boolean shouldStart = buffer.readBoolean(); + if (shouldStart && !animationActive) { + Stage stage = buffer.readEnum(Stage.class); + int color = buffer.readInt(); + int points = buffer.readInt(); + startAnimation(stage, color, points); + } + } else { + super.readUpdateInfo(id, buffer); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PrestigeIgnitionButton.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PrestigeIgnitionButton.java new file mode 100644 index 000000000..cd11f243a --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PrestigeIgnitionButton.java @@ -0,0 +1,507 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.Random; +import java.util.function.BooleanSupplier; + +import javax.annotation.Nonnull; + +public class PrestigeIgnitionButton extends Widget { + + private final BooleanSupplier isPrestigeItemPresent; + private final BooleanSupplier hasActiveStar; + private final Runnable onPrestigeTriggered; + + private float warningScrollPhase = 0f; + private float glitchPhase = 0f; + private float hoverProgress = 0f; + private boolean wasHovered = false; + private int lastMouseX, lastMouseY; + + private int crackStage = 0; + private float crackAnimProgress = 0f; + private boolean isHolding = false; + private int holdTicks = 0; + private static final int HOLD_THRESHOLD = 25; + + private boolean isBreaking = false; + private float breakAnimProgress = 0f; + private float[] shardOffsets; + + private final Random scrambleRandom = new Random(); + private String currentScrambledText = ""; + private int scrambleUpdateCounter = 0; + private static final String SCRAMBLE_CHARS = "!@#$%^&*<>?/\\|=-+"; + + private static final int BG_COLOR_DARK = 0xFF1A0808; + private static final int BG_COLOR_MID = 0xFF2A1010; + private static final int BORDER_COLOR = 0xFFFF2020; + private static final int WARNING_STRIPE_1 = 0xFFCC0000; + private static final int WARNING_STRIPE_2 = 0xFF440000; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final int WARNING_ICON_COLOR = 0xFFFFCC00; + + public PrestigeIgnitionButton(int x, int y, int width, int height, + BooleanSupplier isPrestigeItemPresent, + BooleanSupplier hasActiveStar, + Runnable onPrestigeTriggered) { + super(x, y, width, height); + this.isPrestigeItemPresent = isPrestigeItemPresent; + this.hasActiveStar = hasActiveStar; + this.onPrestigeTriggered = onPrestigeTriggered; + + shardOffsets = new float[12]; + for (int i = 0; i < shardOffsets.length; i++) { + shardOffsets[i] = (float) (Math.random() * 2 - 1); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + + if (!isPrestigeItemPresent.getAsBoolean()) { + return; + } + + warningScrollPhase += 0.8f; + glitchPhase += 0.15f; + + boolean hovered = isMouseOverElement(lastMouseX, lastMouseY); + float targetHover = hovered ? 1f : 0f; + hoverProgress = Mth.lerp(0.15f, hoverProgress, targetHover); + + if (hovered && !wasHovered && !isBreaking) { + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK.value(), 0.8f, 0.2f)); + } + wasHovered = hovered; + + if (isHolding && !isBreaking && crackStage < 3) { + holdTicks++; + if (holdTicks >= HOLD_THRESHOLD) { + advanceCrackStage(); + holdTicks = 0; + } + } + + crackAnimProgress = Mth.lerp(0.1f, crackAnimProgress, 0f); + + if (isBreaking) { + breakAnimProgress += 0.05f; + if (breakAnimProgress >= 1f) { + onPrestigeTriggered.run(); + isBreaking = false; + breakAnimProgress = 0f; + crackStage = 0; + } + } + + scrambleUpdateCounter++; + if (scrambleUpdateCounter >= 3) { + scrambleUpdateCounter = 0; + updateScrambledText(); + } + } + + private void advanceCrackStage() { + crackStage++; + crackAnimProgress = 1f; + + float pitch = 0.6f + crackStage * 0.15f; + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.GLASS_BREAK, pitch, 0.8f)); + + if (crackStage == 1) { + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.STONE_BREAK, 0.5f, 0.5f)); + } else if (crackStage == 2) { + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.ANVIL_LAND, 0.3f, 0.3f)); + } else if (crackStage >= 3) { + isBreaking = true; + breakAnimProgress = 0f; + + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.GLASS_BREAK, 0.4f, 1.0f)); + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.GENERIC_EXPLODE, 0.7f, 0.5f)); + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.TOTEM_USE, 1.2f, 0.8f)); + } + } + + private void updateScrambledText() { + StringBuilder sb = new StringBuilder(); + float glitchIntensity = 0.3f + crackStage * 0.2f; + String baseText = Component.translatable("cosmiccore.stellar.ignition.ignite").getString(); + + for (int i = 0; i < baseText.length(); i++) { + if (scrambleRandom.nextFloat() < glitchIntensity) { + sb.append(SCRAMBLE_CHARS.charAt(scrambleRandom.nextInt(SCRAMBLE_CHARS.length()))); + } else { + sb.append(baseText.charAt(i)); + } + } + currentScrambledText = sb.toString(); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + lastMouseX = mouseX; + lastMouseY = mouseY; + + if (!isPrestigeItemPresent.getAsBoolean()) { + return; + } + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + boolean canActivate = hasActiveStar.getAsBoolean(); + + if (isBreaking) { + drawBreakingAnimation(graphics, x, y, w, h); + return; + } + + drawWarningBackground(graphics, x, y, w, h); + drawMainButton(graphics, x, y, w, h, canActivate); + drawCracks(graphics, x, y, w, h); + drawWarningIcons(graphics, x, y, w, h); + drawButtonText(graphics, x, y, w, h, canActivate); + + if (isHolding && crackStage < 3) { + drawHoldProgress(graphics, x, y, w, h); + } + } + + private void drawWarningBackground(GuiGraphics graphics, int x, int y, int w, int h) { + float pulse = Mth.sin(glitchPhase) * 0.3f + 0.7f; + int glowAlpha = (int) (40 * pulse * (1 + hoverProgress * 0.5f)); + int glowColor = (glowAlpha << 24) | (BORDER_COLOR & 0x00FFFFFF); + + for (int i = 3; i > 0; i--) { + DrawerHelper.drawSolidRect(graphics, x - i, y - i, w + i * 2, h + i * 2, glowColor); + } + + DrawerHelper.drawSolidRect(graphics, x, y, w, h, BG_COLOR_DARK); + } + + private void drawMainButton(GuiGraphics graphics, int x, int y, int w, int h, boolean canActivate) { + int stripeHeight = h; + int stripeWidth = 16; + float scrollOffset = warningScrollPhase % (stripeWidth * 2); + + graphics.enableScissor(x + 1, y + 1, x + w - 1, y + h - 1); + + for (int sx = (int) (-stripeWidth * 4 + scrollOffset); sx < w + stripeWidth * 2; sx += stripeWidth) { + int stripe1X = x + sx; + int stripe2X = x + sx + stripeWidth / 2; + + drawDiagonalStripe(graphics, stripe1X, y, stripeWidth / 2, h, WARNING_STRIPE_1); + drawDiagonalStripe(graphics, stripe2X, y, stripeWidth / 2, h, WARNING_STRIPE_2); + } + + graphics.disableScissor(); + + int centerPadding = 25; + int centerAlpha = canActivate ? 0xD0 : 0xE0; + int centerColor = (centerAlpha << 24) | (BG_COLOR_MID & 0x00FFFFFF); + DrawerHelper.drawSolidRect(graphics, x + centerPadding, y + 4, w - centerPadding * 2, h - 8, centerColor); + + int borderColor = canActivate ? BORDER_COLOR : 0xFF602020; + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 1); + + int innerBorderColor = canActivate ? 0xFF801010 : 0xFF401010; + DrawerHelper.drawBorder(graphics, x + 1, y + 1, w - 2, h - 2, innerBorderColor, 1); + + if (canActivate) { + float accentPulse = Mth.sin(glitchPhase * 2) * 0.5f + 0.5f; + int accentAlpha = (int) (150 + 100 * accentPulse); + int accentColor = (accentAlpha << 24) | (BORDER_COLOR & 0x00FFFFFF); + graphics.fill(x + 2, y + 2, x + w - 2, y + 3, accentColor); + graphics.fill(x + 2, y + h - 3, x + w - 2, y + h - 2, accentColor); + } + } + + private void drawDiagonalStripe(GuiGraphics graphics, int x, int y, int w, int h, int color) { + int skew = h / 3; + for (int row = 0; row < h; row++) { + int offset = (row * skew) / h; + graphics.fill(x + offset, y + row, x + offset + w, y + row + 1, color); + } + } + + private void drawCracks(GuiGraphics graphics, int x, int y, int w, int h) { + if (crackStage == 0) return; + + int crackColor = 0xFF000000; + int highlightColor = 0x60FFFFFF; + + int shakeX = 0, shakeY = 0; + if (crackAnimProgress > 0.5f) { + shakeX = (int) ((Math.random() - 0.5) * 4 * crackAnimProgress); + shakeY = (int) ((Math.random() - 0.5) * 4 * crackAnimProgress); + } + + int cx = x + w / 2 + shakeX; + int cy = y + h / 2 + shakeY; + + if (crackStage >= 1) { + drawCrackLine(graphics, x + 5, y + 3, cx - 10, cy - 5, crackColor); + drawCrackLine(graphics, x + 6, y + 4, cx - 9, cy - 4, highlightColor); + } + + if (crackStage >= 2) { + drawCrackLine(graphics, x + w - 5, y + h - 3, cx + 10, cy + 5, crackColor); + drawCrackLine(graphics, x + w - 6, y + h - 4, cx + 9, cy + 4, highlightColor); + + drawCrackLine(graphics, cx, cy, cx + 15, cy - 8, crackColor); + drawCrackLine(graphics, cx, cy, cx - 12, cy + 10, crackColor); + } + + if (crackStage >= 3) { + for (int i = 0; i < 8; i++) { + float angle = i * Mth.TWO_PI / 8; + int endX = cx + (int) (Mth.cos(angle) * 20); + int endY = cy + (int) (Mth.sin(angle) * 10); + drawCrackLine(graphics, cx, cy, endX, endY, crackColor); + } + } + } + + private void drawCrackLine(GuiGraphics graphics, int x1, int y1, int x2, int y2, int color) { + int dx = Math.abs(x2 - x1); + int dy = Math.abs(y2 - y1); + int sx = x1 < x2 ? 1 : -1; + int sy = y1 < y2 ? 1 : -1; + int err = dx - dy; + + int px = x1, py = y1; + int jitterCounter = 0; + + while (true) { + int jx = px + (jitterCounter % 3 == 0 ? (int) (Math.random() * 2 - 1) : 0); + int jy = py + (jitterCounter % 4 == 0 ? (int) (Math.random() * 2 - 1) : 0); + graphics.fill(jx, jy, jx + 1, jy + 1, color); + + if (px == x2 && py == y2) break; + + int e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + px += sx; + } + if (e2 < dx) { + err += dx; + py += sy; + } + jitterCounter++; + } + } + + private void drawWarningIcons(GuiGraphics graphics, int x, int y, int w, int h) { + int iconSize = 12; + int iconY = y + (h - iconSize) / 2; + + drawWarningTriangle(graphics, x + 6, iconY, iconSize); + drawWarningTriangle(graphics, x + w - 6 - iconSize, iconY, iconSize); + } + + private void drawWarningTriangle(GuiGraphics graphics, int x, int y, int size) { + float flash = Mth.sin(glitchPhase * 3) * 0.3f + 0.7f; + int alpha = (int) (255 * flash); + int color = (alpha << 24) | (WARNING_ICON_COLOR & 0x00FFFFFF); + + int cx = x + size / 2; + for (int row = 0; row < size; row++) { + int halfWidth = (row * size) / (size * 2); + graphics.fill(cx - halfWidth, y + row, cx + halfWidth + 1, y + row + 1, color); + } + + var font = Minecraft.getInstance().font; + int exclamationWidth = font.width("!"); + graphics.drawString(font, "!", cx - exclamationWidth / 2, y + size / 3, 0xFF000000, false); + } + + private void drawButtonText(GuiGraphics graphics, int x, int y, int w, int h, boolean canActivate) { + var font = Minecraft.getInstance().font; + + String displayText; + int textColor; + + if (!canActivate) { + displayText = Component.translatable("cosmiccore.stellar.ignition.requires_star").getString(); + textColor = 0xFF804040; + } else if (crackStage >= 3) { + displayText = Component.translatable("cosmiccore.stellar.ignition.breaking").getString(); + textColor = 0xFFFFFFFF; + } else { + displayText = "[ " + currentScrambledText + " ]"; + textColor = TEXT_COLOR; + } + + int textW = font.width(displayText); + int textX = x + (w - textW) / 2; + int textY = y + (h - font.lineHeight) / 2; + + if (canActivate) { + graphics.drawString(font, displayText, textX + 1, textY + 1, 0xFF000000, false); + } + + graphics.drawString(font, displayText, textX, textY, textColor, false); + + if (canActivate && crackStage > 0) { + float glitchIntensity = crackStage * 0.3f + crackAnimProgress; + if (scrambleRandom.nextFloat() < glitchIntensity * 0.5f) { + int ghostAlpha = (int) (60 * glitchIntensity); + int ghostColor = (ghostAlpha << 24) | 0x00FFFF; + int offsetX = (int) ((Math.random() - 0.5) * 4); + graphics.drawString(font, displayText, textX + offsetX, textY, ghostColor, false); + } + } + } + + private void drawHoldProgress(GuiGraphics graphics, int x, int y, int w, int h) { + float progress = (float) holdTicks / HOLD_THRESHOLD; + + int barHeight = 3; + int barY = y + h - barHeight - 2; + int barW = (int) ((w - 4) * progress); + + graphics.fill(x + 2, barY, x + w - 2, barY + barHeight, 0x80000000); + + int r = (int) (255 * (0.8f + 0.2f * progress)); + int g = (int) (255 * (1f - progress * 0.7f)); + int fillColor = 0xFF000000 | (r << 16) | (g << 8); + graphics.fill(x + 2, barY, x + 2 + barW, barY + barHeight, fillColor); + + if (barW > 2) { + graphics.fill(x + 2 + barW - 1, barY, x + 2 + barW, barY + barHeight, 0xCCFFFFFF); + } + } + + private void drawBreakingAnimation(GuiGraphics graphics, int x, int y, int w, int h) { + int pieceW = w / 3; + int pieceH = h / 2; + + for (int i = 0; i < 6; i++) { + int row = i / 3; + int col = i % 3; + + float dx = shardOffsets[i * 2] * breakAnimProgress * 50; + float dy = shardOffsets[i * 2 + 1] * breakAnimProgress * 30 + breakAnimProgress * breakAnimProgress * 20; + + int px = x + col * pieceW + (int) dx; + int py = y + row * pieceH + (int) dy; + + int alpha = (int) (255 * (1f - breakAnimProgress)); + int color = (alpha << 24) | (BG_COLOR_MID & 0x00FFFFFF); + + graphics.fill(px, py, px + pieceW - 1, py + pieceH - 1, color); + + int borderAlpha = alpha / 2; + int borderColor = (borderAlpha << 24) | (BORDER_COLOR & 0x00FFFFFF); + DrawerHelper.drawBorder(graphics, px, py, pieceW - 1, pieceH - 1, borderColor, 1); + } + + for (int i = 0; i < 10; i++) { + float particleProgress = breakAnimProgress + i * 0.05f; + if (particleProgress > 1f) continue; + + float px = x + w / 2 + (float) Math.cos(i * 0.7) * particleProgress * 60; + float py = y + h / 2 + (float) Math.sin(i * 0.7) * particleProgress * 40 + + particleProgress * particleProgress * 30; + + int particleAlpha = (int) (200 * (1f - particleProgress)); + int particleColor = (particleAlpha << 24) | 0xFFAA00; + graphics.fill((int) px - 1, (int) py - 1, (int) px + 2, (int) py + 2, particleColor); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInForeground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInForeground(graphics, mouseX, mouseY, partialTicks); + + if (isBreaking && breakAnimProgress < 0.3f) { + int flashAlpha = (int) (150 * (1f - breakAnimProgress / 0.3f)); + graphics.fill(-1000, -1000, 2000, 2000, (flashAlpha << 24) | 0xFFFFFF); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isPrestigeItemPresent.getAsBoolean()) { + return false; + } + + if (!isMouseOverElement(mouseX, mouseY)) { + return false; + } + + if (button == 0 && hasActiveStar.getAsBoolean() && !isBreaking) { + isHolding = true; + holdTicks = 0; + + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.STONE_PRESSURE_PLATE_CLICK_ON, 0.8f, 0.6f)); + + return true; + } + + return false; + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button == 0 && isHolding) { + isHolding = false; + holdTicks = 0; + + if (crackStage < 3 && !isBreaking) { + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.STONE_PRESSURE_PLATE_CLICK_OFF, 0.9f, 0.4f)); + } + + return true; + } + return false; + } + + public void reset() { + crackStage = 0; + crackAnimProgress = 0f; + isHolding = false; + holdTicks = 0; + isBreaking = false; + breakAnimProgress = 0f; + } + + public int getCrackStage() { + return crackStage; + } + + public boolean isBreaking() { + return isBreaking; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PrestigeWindow.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PrestigeWindow.java new file mode 100644 index 000000000..40189c221 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/PrestigeWindow.java @@ -0,0 +1,384 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class PrestigeWindow extends Widget { + + private static final int UPDATE_ID_PRESTIGE_DATA = 410; + + private final Supplier machineSupplier; + private final Runnable onClose; + + private boolean visible = false; + private float fadeAlpha = 0f; + private float targetAlpha = 0f; + + private int totalPoints = 0; + private int earnedPoints = 0; + private int currentTier = 0; + private int previousTier = 0; + + private int animationTick = 0; + private float pointCounterDisplay = 0f; + private boolean showTierUp = false; + private int tierUpAnimTick = 0; + + private static final int BG_COLOR = 0xFF101020; + private static final int BORDER_COLOR = 0xFF4060A0; + private static final int ACCENT_COLOR = 0xFF80A0FF; + private static final int GOLD_COLOR = 0xFFFFCC44; + private static final int TEXT_COLOR = 0xFFCCCCDD; + private static final int TIER_COLORS[] = { + 0xFF808080, + 0xFF60A060, + 0xFF4080C0, + 0xFFA060C0, + 0xFFD08040, + 0xFFFFCC44, + }; + + public PrestigeWindow(int x, int y, int width, int height, + Supplier machineSupplier, + Runnable onClose) { + super(x, y, width, height); + this.machineSupplier = machineSupplier; + this.onClose = onClose; + } + + public void show(int earned, int total, int tier, int prevTier) { + this.earnedPoints = earned; + this.totalPoints = total; + this.currentTier = tier; + this.previousTier = prevTier; + this.visible = true; + this.targetAlpha = 1f; + this.animationTick = 0; + this.pointCounterDisplay = 0f; + this.showTierUp = tier > prevTier; + this.tierUpAnimTick = 0; + } + + public void hide() { + this.targetAlpha = 0f; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + + if (fadeAlpha < targetAlpha) { + fadeAlpha = Math.min(fadeAlpha + 0.05f, targetAlpha); + } else if (fadeAlpha > targetAlpha) { + fadeAlpha = Math.max(fadeAlpha - 0.05f, targetAlpha); + if (fadeAlpha <= 0f) { + visible = false; + } + } + + if (!visible) return; + + animationTick++; + + if (animationTick > 20 && pointCounterDisplay < earnedPoints) { + float remaining = earnedPoints - pointCounterDisplay; + float speed = Math.max(1f, remaining * 0.15f); + pointCounterDisplay = Math.min(pointCounterDisplay + speed, earnedPoints); + } + + if (showTierUp && animationTick > 60) { + tierUpAnimTick++; + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + if (!visible && fadeAlpha <= 0f) return; + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + drawWindowBackground(graphics, x, y, w, h); + drawHeader(graphics, x, y, w); + drawPointsSection(graphics, x, y + 30, w); + drawTierSection(graphics, x, y + 80, w); + + if (showTierUp && tierUpAnimTick > 0) { + drawTierUpCelebration(graphics, x, y, w, h); + } + + drawCloseHint(graphics, x, y + h - 16, w); + } + + private void drawWindowBackground(GuiGraphics graphics, int x, int y, int w, int h) { + int bgAlpha = (int) (0xFF * fadeAlpha); + int bgColor = (bgAlpha << 24) | (BG_COLOR & 0x00FFFFFF); + DrawerHelper.drawSolidRect(graphics, x, y, w, h, bgColor); + + int borderAlpha = (int) (0xFF * fadeAlpha); + int borderColor = (borderAlpha << 24) | (BORDER_COLOR & 0x00FFFFFF); + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 2); + + int glowAlpha = (int) (0x40 * fadeAlpha); + int glowColor = (glowAlpha << 24) | (ACCENT_COLOR & 0x00FFFFFF); + DrawerHelper.drawGradientRect(graphics, x + 2, y + 2, w - 4, 30, glowColor, 0x00000000, false); + } + + private void drawHeader(GuiGraphics graphics, int x, int y, int w) { + var font = Minecraft.getInstance().font; + + String title = Component.translatable("cosmiccore.stellar.prestige.title").getString(); + int titleColor = applyFade(GOLD_COLOR); + + int titleX = x + (w - font.width(title)) / 2; + int titleY = y + 8; + + graphics.drawString(font, title, titleX + 1, titleY + 1, applyFade(0xFF000000), false); + graphics.drawString(font, title, titleX, titleY, titleColor, false); + + int lineY = y + 24; + int lineAlpha = (int) (0x80 * fadeAlpha); + int lineColor = (lineAlpha << 24) | (ACCENT_COLOR & 0x00FFFFFF); + graphics.fill(x + 20, lineY, x + w - 20, lineY + 1, lineColor); + } + + private void drawPointsSection(GuiGraphics graphics, int x, int y, int w) { + var font = Minecraft.getInstance().font; + + String earnedLabel = Component.translatable("cosmiccore.stellar.prestige.points_earned").getString(); + int labelColor = applyFade(TEXT_COLOR); + int labelX = x + (w - font.width(earnedLabel)) / 2; + graphics.drawString(font, earnedLabel, labelX, y, labelColor, false); + + String pointsStr = "+" + (int) pointCounterDisplay; + int pointsColor = applyFade(GOLD_COLOR); + + float scale = 2.0f; + int pointsWidth = (int) (font.width(pointsStr) * scale); + int pointsX = x + (w - pointsWidth) / 2; + int pointsY = y + 12; + + graphics.pose().pushPose(); + graphics.pose().translate(pointsX, pointsY, 0); + graphics.pose().scale(scale, scale, 1f); + graphics.drawString(font, pointsStr, 0, 0, pointsColor, false); + graphics.pose().popPose(); + + String totalLabel = Component.translatable("cosmiccore.stellar.prestige.total_points", totalPoints).getString(); + int totalLabelX = x + (w - font.width(totalLabel)) / 2; + int totalY = y + 35; + graphics.drawString(font, totalLabel, totalLabelX, totalY, applyFade(TEXT_COLOR), false); + } + + private void drawTierSection(GuiGraphics graphics, int x, int y, int w) { + var font = Minecraft.getInstance().font; + + String tierLabel = Component.translatable("cosmiccore.stellar.prestige.current_tier").getString(); + int labelX = x + (w - font.width(tierLabel)) / 2; + graphics.drawString(font, tierLabel, labelX, y, applyFade(TEXT_COLOR), false); + + int tierColor = applyFade(getTierColor(currentTier)); + String tierStr = getTierName(currentTier); + + float scale = 1.5f; + int tierWidth = (int) (font.width(tierStr) * scale); + int tierX = x + (w - tierWidth) / 2; + int tierY = y + 12; + + graphics.pose().pushPose(); + graphics.pose().translate(tierX, tierY, 0); + graphics.pose().scale(scale, scale, 1f); + graphics.drawString(font, tierStr, 0, 0, tierColor, false); + graphics.pose().popPose(); + + if (currentTier < 5) { + int nextTierPoints = getPointsForTier(currentTier + 1); + int currentTierPoints = getPointsForTier(currentTier); + float progress = (float) (totalPoints - currentTierPoints) / (nextTierPoints - currentTierPoints); + progress = Mth.clamp(progress, 0f, 1f); + + int barY = y + 35; + int barW = w - 40; + int barH = 8; + int barX = x + 20; + + int barBgColor = applyFade(0xFF202030); + DrawerHelper.drawSolidRect(graphics, barX, barY, barW, barH, barBgColor); + + int fillW = (int) (barW * progress); + int fillColor = applyFade(getTierColor(currentTier + 1)); + if (fillW > 0) { + DrawerHelper.drawSolidRect(graphics, barX, barY, fillW, barH, fillColor); + } + + DrawerHelper.drawBorder(graphics, barX, barY, barW, barH, applyFade(0xFF404060), 1); + + String nextLabel = Component + .translatable("cosmiccore.stellar.prestige.next_tier", nextTierPoints, getTierName(currentTier + 1)) + .getString(); + int nextLabelX = x + (w - font.width(nextLabel)) / 2; + graphics.drawString(font, nextLabel, nextLabelX, barY + 10, applyFade(TEXT_COLOR), false); + } else { + String maxLabel = Component.translatable("cosmiccore.stellar.prestige.max_tier").getString(); + int maxLabelX = x + (w - font.width(maxLabel)) / 2; + graphics.drawString(font, maxLabel, maxLabelX, y + 35, applyFade(GOLD_COLOR), false); + } + } + + private void drawTierUpCelebration(GuiGraphics graphics, int x, int y, int w, int h) { + var font = Minecraft.getInstance().font; + + float animProgress = Math.min(1f, tierUpAnimTick / 30f); + float easeOut = 1f - (1f - animProgress) * (1f - animProgress); + + int bannerH = 40; + int bannerY = (int) (y - bannerH + easeOut * (h / 2 + bannerH / 2)); + + int bannerAlpha = (int) (0xF0 * easeOut * fadeAlpha); + int bannerColor = (bannerAlpha << 24) | (getTierColor(currentTier) & 0x00FFFFFF); + DrawerHelper.drawSolidRect(graphics, x + 10, bannerY, w - 20, bannerH, bannerColor); + + String tierUpText = Component.translatable("cosmiccore.stellar.prestige.tier_up").getString(); + String newTierText = getTierName(currentTier); + + int textAlpha = (int) (255 * easeOut * fadeAlpha); + int textColor = (textAlpha << 24) | 0xFFFFFF; + + float scale = 1.8f; + int tierUpWidth = (int) (font.width(tierUpText) * scale); + int tierUpX = x + (w - tierUpWidth) / 2; + + graphics.pose().pushPose(); + graphics.pose().translate(tierUpX, bannerY + 4, 0); + graphics.pose().scale(scale, scale, 1f); + graphics.drawString(font, tierUpText, 0, 0, textColor, false); + graphics.pose().popPose(); + + int newTierWidth = font.width(newTierText); + int newTierX = x + (w - newTierWidth) / 2; + graphics.drawString(font, newTierText, newTierX, bannerY + 26, textColor, false); + + if (tierUpAnimTick < 40) { + drawParticleBurst(graphics, x + w / 2, bannerY + bannerH / 2, tierUpAnimTick); + } + } + + private void drawParticleBurst(GuiGraphics graphics, int cx, int cy, int tick) { + int particleCount = 16; + float progress = tick / 40f; + + for (int i = 0; i < particleCount; i++) { + float angle = i * Mth.TWO_PI / particleCount; + float distance = progress * 80; + + int px = cx + (int) (Mth.cos(angle) * distance); + int py = cy + (int) (Mth.sin(angle) * distance * 0.5f); + + int alpha = (int) ((1f - progress) * 200 * fadeAlpha); + int color = (alpha << 24) | (GOLD_COLOR & 0x00FFFFFF); + + int size = (int) (3 * (1f - progress)); + if (size > 0) { + graphics.fill(px - size, py - size, px + size + 1, py + size + 1, color); + } + } + } + + private void drawCloseHint(GuiGraphics graphics, int x, int y, int w) { + var font = Minecraft.getInstance().font; + String hint = Component.translatable("cosmiccore.stellar.prestige.continue").getString(); + int hintX = x + (w - font.width(hint)) / 2; + graphics.drawString(font, hint, hintX, y, applyFade(0xFF808090), false); + } + + private int applyFade(int color) { + int a = (color >> 24) & 0xFF; + a = (int) (a * fadeAlpha); + return (a << 24) | (color & 0x00FFFFFF); + } + + private int getTierColor(int tier) { + if (tier < 0) tier = 0; + if (tier >= TIER_COLORS.length) tier = TIER_COLORS.length - 1; + return TIER_COLORS[tier]; + } + + private String getTierName(int tier) { + return switch (tier) { + case 0 -> Component.translatable("cosmiccore.stellar.prestige.tier.novice").getString(); + case 1 -> Component.translatable("cosmiccore.stellar.prestige.tier.apprentice").getString(); + case 2 -> Component.translatable("cosmiccore.stellar.prestige.tier.journeyman").getString(); + case 3 -> Component.translatable("cosmiccore.stellar.prestige.tier.expert").getString(); + case 4 -> Component.translatable("cosmiccore.stellar.prestige.tier.master").getString(); + case 5 -> Component.translatable("cosmiccore.stellar.prestige.tier.grandmaster").getString(); + default -> Component.translatable("cosmiccore.stellar.prestige.tier.unknown").getString(); + }; + } + + private int getPointsForTier(int tier) { + return switch (tier) { + case 1 -> 50; + case 2 -> 100; + case 3 -> 250; + case 4 -> 500; + case 5 -> 1000; + default -> 0; + }; + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (visible && fadeAlpha > 0.5f && animationTick > 40) { + hide(); + onClose.run(); + return true; + } + return false; + } + + public boolean isVisible() { + return visible || fadeAlpha > 0f; + } + + @Override + public void detectAndSendChanges() { + super.detectAndSendChanges(); + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine == null) return; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readUpdateInfo(int id, FriendlyByteBuf buffer) { + if (id == UPDATE_ID_PRESTIGE_DATA) { + int earned = buffer.readInt(); + int total = buffer.readInt(); + int tier = buffer.readInt(); + int prevTier = buffer.readInt(); + show(earned, total, tier, prevTier); + } else { + super.readUpdateInfo(id, buffer); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StageContextPanel.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StageContextPanel.java new file mode 100644 index 000000000..6db58aef7 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StageContextPanel.java @@ -0,0 +1,261 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine; +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.gregtechceu.gtceu.api.gui.GuiTextures; +import com.gregtechceu.gtceu.api.gui.widget.SlotWidget; + +import com.lowdragmc.lowdraglib.gui.texture.ColorBorderTexture; +import com.lowdragmc.lowdraglib.gui.texture.ColorRectTexture; +import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class StageContextPanel extends WidgetGroup { + + private static final int UPDATE_ID_PRESTIGE_STATE = 100; + + private final Supplier machineSupplier; + private final StellarIrisWidget parentWidget; + + private IgnitionButtonWidget normalIgnitionButton; + private PrestigeIgnitionButton prestigeIgnitionButton; + + private boolean hasPrestigeItem = false; + private boolean hasActiveStar = false; + + public StageContextPanel(int x, int y, int width, int height, + Supplier machineSupplier, + StellarIrisWidget parentWidget) { + super(x, y, width, height); + this.machineSupplier = machineSupplier; + this.parentWidget = parentWidget; + initWidgets(); + } + + private void initWidgets() { + addWidget(new LabelWidget(5, 5, this::getStagePanelTitle)); + + addWidget(new FuelGaugeWidget(5, 22, getSize().width - 10, 30, parentWidget::getFuelLevel)); + + normalIgnitionButton = new IgnitionButtonWidget( + 5, 58, getSize().width - 10, 24, + parentWidget::canIgnite, + () -> !hasPrestigeItem && (getCurrentStage() == Stage.EMPTY || parentWidget.canIgnite()), + parentWidget::requestIgnition); + addWidget(normalIgnitionButton); + + prestigeIgnitionButton = new PrestigeIgnitionButton( + 5, 58, getSize().width - 10, 24, + () -> hasPrestigeItem, + () -> hasActiveStar, + this::onPrestigeTriggered); + addWidget(prestigeIgnitionButton); + + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine != null) { + SlotWidget starSeedSlot = new SlotWidget(machine.getInventory().storage, 0, 5, 88, true, true); + starSeedSlot.setBackground(new GuiTextureGroup( + new ColorRectTexture(0xC0101018), + new ColorBorderTexture(1, 0xFF505070)), GuiTextures.ATOMIC_OVERLAY_1); + addWidget(starSeedSlot); + addWidget(new LabelWidget(28, 92, + () -> Component.translatable("cosmiccore.stellar.slot.star_seed").getString()) + .setTextColor(0xFF808090)); + } + } + + private void onPrestigeTriggered() { + parentWidget.triggerPrestigeAnimation(); + } + + @Override + public void writeInitialData(FriendlyByteBuf buffer) { + super.writeInitialData(buffer); + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine != null) { + buffer.writeBoolean(machine.hasPrestigeItem()); + buffer.writeBoolean(machine.hasActiveStar()); + } else { + buffer.writeBoolean(false); + buffer.writeBoolean(false); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readInitialData(FriendlyByteBuf buffer) { + super.readInitialData(buffer); + hasPrestigeItem = buffer.readBoolean(); + hasActiveStar = buffer.readBoolean(); + updateButtonVisibility(); + } + + @Override + public void detectAndSendChanges() { + super.detectAndSendChanges(); + + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine == null) return; + + boolean newHasPrestigeItem = machine.hasPrestigeItem(); + boolean newHasActiveStar = machine.hasActiveStar(); + + if (newHasPrestigeItem != hasPrestigeItem || newHasActiveStar != hasActiveStar) { + hasPrestigeItem = newHasPrestigeItem; + hasActiveStar = newHasActiveStar; + writeUpdateInfo(UPDATE_ID_PRESTIGE_STATE, buf -> { + buf.writeBoolean(hasPrestigeItem); + buf.writeBoolean(hasActiveStar); + }); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readUpdateInfo(int id, FriendlyByteBuf buffer) { + if (id == UPDATE_ID_PRESTIGE_STATE) { + hasPrestigeItem = buffer.readBoolean(); + hasActiveStar = buffer.readBoolean(); + updateButtonVisibility(); + + if (!hasPrestigeItem) { + prestigeIgnitionButton.reset(); + } + } else { + super.readUpdateInfo(id, buffer); + } + } + + @OnlyIn(Dist.CLIENT) + private void updateButtonVisibility() { + normalIgnitionButton.setVisible(!hasPrestigeItem); + normalIgnitionButton.setActive(!hasPrestigeItem); + prestigeIgnitionButton.setVisible(hasPrestigeItem); + prestigeIgnitionButton.setActive(hasPrestigeItem); + } + + private Stage getCurrentStage() { + IrisMultiblockMachine machine = machineSupplier.get(); + return machine != null ? machine.getStage() : Stage.EMPTY; + } + + private String getStagePanelTitle() { + return switch (getCurrentStage()) { + case EMPTY -> Component.translatable("cosmiccore.stellar.stage.initialization").getString(); + case GROWING -> Component.translatable("cosmiccore.stellar.stage.stellar_ignition").getString(); + case STAR -> Component.translatable("cosmiccore.stellar.stage.stellar_operations").getString(); + case SUPERSTAR -> Component.translatable("cosmiccore.stellar.stage.critical_mass").getString(); + case BLACK_HOLE -> Component.translatable("cosmiccore.stellar.stage.singularity_control").getString(); + case DEATH -> Component.translatable("cosmiccore.stellar.stage.emergency_protocols").getString(); + case DEATH_GRACEFUL -> Component.translatable("cosmiccore.stellar.stage.controlled_shutdown").getString(); + }; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + DrawerHelper.drawSolidRect(graphics, x, y, w, h, 0xCC0a0a14); + + int accentColor = getStageAccentColor(); + DrawerHelper.drawBorder(graphics, x, y, w, h, accentColor, 1); + graphics.fill(x + 1, y + 1, x + w - 1, y + 3, accentColor); + + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + drawStageInfo(graphics, x, y, w, h); + } + + private void drawStageInfo(GuiGraphics graphics, int x, int y, int w, int h) { + var font = Minecraft.getInstance().font; + int infoY = y + h - 35; + int textColor = 0xFF808090; + + switch (getCurrentStage()) { + case EMPTY -> { + graphics.drawString(font, Component.translatable("cosmiccore.stellar.context.empty_line1").getString(), + x + 5, infoY, textColor, false); + graphics.drawString(font, Component.translatable("cosmiccore.stellar.context.empty_line2").getString(), + x + 5, infoY + 10, textColor, false); + graphics.drawString(font, Component.translatable("cosmiccore.stellar.context.empty_line3").getString(), + x + 5, infoY + 20, textColor, false); + } + case GROWING -> { + graphics.drawString(font, + Component.translatable("cosmiccore.stellar.context.growing_line1").getString(), x + 5, infoY, + 0xFFAAAAFF, false); + graphics.drawString(font, + Component.translatable("cosmiccore.stellar.context.growing_line2").getString(), x + 5, + infoY + 10, 0xFFAAAAFF, false); + } + case STAR -> { + graphics.drawString(font, Component.translatable("cosmiccore.stellar.context.star_line1").getString(), + x + 5, infoY, 0xFFFFCC44, false); + graphics.drawString(font, Component.translatable("cosmiccore.stellar.context.star_line2").getString(), + x + 5, infoY + 10, textColor, false); + } + case SUPERSTAR -> { + graphics.drawString(font, + Component.translatable("cosmiccore.stellar.context.superstar_line1").getString(), x + 5, infoY, + 0xFFFF8844, false); + graphics.drawString(font, + Component.translatable("cosmiccore.stellar.context.superstar_line2").getString(), x + 5, + infoY + 10, 0xFFFF6622, false); + } + case BLACK_HOLE -> { + graphics.drawString(font, + Component.translatable("cosmiccore.stellar.context.blackhole_line1").getString(), x + 5, infoY, + 0xFFAA66FF, false); + graphics.drawString(font, + Component.translatable("cosmiccore.stellar.context.blackhole_line2").getString(), x + 5, + infoY + 10, 0xFF8844DD, false); + } + case DEATH -> { + if (parentWidget.getTickCounter() % 20 < 10) { + graphics.fill(x + 1, y + 1, x + w - 1, y + h - 1, 0x30FF0000); + } + graphics.drawString(font, Component.translatable("cosmiccore.stellar.context.death_line1").getString(), + x + 5, infoY, 0xFFFF0000, false); + graphics.drawString(font, Component.translatable("cosmiccore.stellar.context.death_line2").getString(), + x + 5, infoY + 10, 0xFFFF4444, false); + } + case DEATH_GRACEFUL -> { + graphics.drawString(font, + Component.translatable("cosmiccore.stellar.context.death_graceful_line1").getString(), x + 5, + infoY, 0xFF884444, false); + graphics.drawString(font, + Component.translatable("cosmiccore.stellar.context.death_graceful_line2").getString(), x + 5, + infoY + 10, textColor, false); + } + } + } + + private int getStageAccentColor() { + return switch (getCurrentStage()) { + case EMPTY -> 0xFF404060; + case GROWING -> 0xFF6080FF; + case STAR -> 0xFFFFCC44; + case SUPERSTAR -> 0xFFFF8844; + case BLACK_HOLE -> 0xFF8040FF; + case DEATH -> 0xFFFF2020; + case DEATH_GRACEFUL -> 0xFF804040; + }; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StarColorButton.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StarColorButton.java new file mode 100644 index 000000000..ac252276a --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StarColorButton.java @@ -0,0 +1,135 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.IntSupplier; + +import javax.annotation.Nonnull; + +public class StarColorButton extends Widget { + + private final Consumer onToggle; + private final IntSupplier colorSupplier; + private boolean hovered = false; + private float hoverProgress = 0f; + private float pulsePhase = 0f; + + public StarColorButton(int x, int y, int width, int height, Consumer onToggle, IntSupplier colorSupplier) { + super(x, y, width, height); + this.onToggle = onToggle; + this.colorSupplier = colorSupplier; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + pulsePhase += 0.1f; + + if (hovered && hoverProgress < 1f) { + hoverProgress = Math.min(1f, hoverProgress + 0.15f); + } else if (!hovered && hoverProgress > 0f) { + hoverProgress = Math.max(0f, hoverProgress - 0.15f); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + hovered = isMouseOverElement(mouseX, mouseY); + + int bgAlpha = (int) (0xC0 + 0x20 * hoverProgress); + int bgColor = (bgAlpha << 24) | 0x101820; + DrawerHelper.drawSolidRect(graphics, x, y, w, h, bgColor); + + int currentColor = colorSupplier != null ? colorSupplier.getAsInt() : -1; + int displayColor = currentColor == -1 ? 0xFFCC44 : currentColor; + + int borderAlpha = (int) (0x60 + 0x40 * hoverProgress); + int borderColor = (borderAlpha << 24) | displayColor; + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 1); + + drawColorIcon(graphics, x, y, w, h, displayColor, currentColor == -1); + + if (hoverProgress > 0) { + int glowAlpha = (int) (0x20 * hoverProgress); + int glowColor = (glowAlpha << 24) | displayColor; + DrawerHelper.drawBorder(graphics, x - 1, y - 1, w + 2, h + 2, glowColor, 1); + } + } + + private void drawColorIcon(GuiGraphics graphics, int x, int y, int w, int h, int color, boolean isDefault) { + int padding = 3; + int iconX = x + padding; + int iconY = y + padding; + int iconW = w - padding * 2; + int iconH = h - padding * 2; + + float pulseAlpha = 0.8f + 0.2f * Mth.sin(pulsePhase); + int alpha = (int) (0xFF * pulseAlpha); + + if (isDefault) { + int checkSize = 3; + for (int cy = 0; cy < iconH / checkSize; cy++) { + for (int cx = 0; cx < iconW / checkSize; cx++) { + int checkColor = ((cx + cy) % 2 == 0) ? 0xFF303030 : 0xFF505050; + int checkX = iconX + cx * checkSize; + int checkY = iconY + cy * checkSize; + int checkW = Math.min(checkSize, iconX + iconW - checkX); + int checkH = Math.min(checkSize, iconY + iconH - checkY); + graphics.fill(checkX, checkY, checkX + checkW, checkY + checkH, checkColor); + } + } + } + + graphics.fill(iconX, iconY, iconX + iconW, iconY + iconH, (alpha << 24) | color); + + int innerBorder = 0x40000000; + graphics.fill(iconX, iconY, iconX + iconW, iconY + 1, innerBorder); + graphics.fill(iconX, iconY, iconX + 1, iconY + iconH, innerBorder); + + if (isDefault) { + var font = Minecraft.getInstance().font; + int textColor = 0x80FFFFFF; + graphics.drawString(font, "D", iconX + iconW - 6, iconY + iconH - 8, textColor, false); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && isMouseOverElement(mouseX, mouseY)) { + if (onToggle != null) { + onToggle.accept(true); + } + playButtonClickSound(); + return true; + } + return false; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInForeground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + if (isMouseOverElement(mouseX, mouseY)) { + graphics.renderTooltip(Minecraft.getInstance().font, + List.of(Component.translatable("cosmiccore.gui.stellar.star_color")), + java.util.Optional.empty(), mouseX, mouseY); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StarColorPickerPopup.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StarColorPickerPopup.java new file mode 100644 index 000000000..0f07e6ff0 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StarColorPickerPopup.java @@ -0,0 +1,462 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.lowdragmc.lowdraglib.gui.texture.ColorBorderTexture; +import com.lowdragmc.lowdraglib.gui.texture.ColorRectTexture; +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.TextFieldWidget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.IntConsumer; + +import javax.annotation.Nonnull; + +public class StarColorPickerPopup extends WidgetGroup { + + public static final int WIDTH = 160; + public static final int HEIGHT = 140; + private static final int TITLE_HEIGHT = 16; + private static final int PICKER_SIZE = 80; + private static final int HUE_BAR_WIDTH = 12; + + private final Runnable onClose; + private final IntConsumer onColorChanged; + + // Current color in HSB + private float hue = 0.15f; // Default yellow-ish + private float saturation = 0.8f; + private float brightness = 1.0f; + + // Current color as RGB + private int currentColor = 0xFFCC44; + + // Text field for hex input + private TextFieldWidget hexField; + + // Dragging state + private boolean draggingPicker = false; + private boolean draggingHue = false; + private boolean draggingTitle = false; + private double lastDeltaX, lastDeltaY; + + // Animation + private float appearProgress = 0f; + + public StarColorPickerPopup(int x, int y, Runnable onClose, IntConsumer onColorChanged) { + super(x, y, WIDTH, HEIGHT); + this.onClose = onClose; + this.onColorChanged = onColorChanged; + setVisible(false); + initWidgets(); + } + + private void initWidgets() { + // Hex input field at bottom + int fieldX = 6; + int fieldY = HEIGHT - 26; + int fieldWidth = WIDTH - 60; + + hexField = new TextFieldWidget(fieldX, fieldY, fieldWidth, 16, + this::getHexString, + this::onHexChanged); + hexField.setClientSideWidget(); + hexField.setMaxStringLength(7); // #RRGGBB + hexField.setBackground(new com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup( + new ColorRectTexture(0xE0101018), + new ColorBorderTexture(1, 0xFF404060))); + addWidget(hexField); + } + + public void show(int color) { + if (color == -1) { + // Default - use a nice yellow + currentColor = 0xFFCC44; + } else { + currentColor = color & 0xFFFFFF; + } + + // Convert to HSB + float[] hsb = rgbToHsb(currentColor); + hue = hsb[0]; + saturation = hsb[1]; + brightness = hsb[2]; + + appearProgress = 0f; + setVisible(true); + setActive(true); + + updateHexField(); + } + + public void hide() { + setVisible(false); + setActive(false); + } + + private String getHexString() { + return String.format("#%06X", currentColor & 0xFFFFFF); + } + + private void onHexChanged(String text) { + try { + String hex = text.startsWith("#") ? text.substring(1) : text; + if (hex.length() == 6) { + int color = Integer.parseInt(hex, 16); + setColor(color); + } + } catch (NumberFormatException ignored) {} + } + + private void setColor(int rgb) { + currentColor = rgb & 0xFFFFFF; + float[] hsb = rgbToHsb(currentColor); + hue = hsb[0]; + saturation = hsb[1]; + brightness = hsb[2]; + + if (onColorChanged != null) { + onColorChanged.accept(currentColor); + } + } + + private void updateFromHsb() { + currentColor = hsbToRgb(hue, saturation, brightness); + updateHexField(); + + if (onColorChanged != null) { + onColorChanged.accept(currentColor); + } + } + + private void updateHexField() { + if (hexField != null) { + hexField.setCurrentString(getHexString()); + } + } + + private void resetToDefault() { + if (onColorChanged != null) { + onColorChanged.accept(-1); // -1 signals default + } + hide(); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + if (isVisible() && appearProgress < 1f) { + appearProgress = Math.min(1f, appearProgress + 0.15f); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + if (!isVisible()) return; + + float alpha = appearProgress; + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + // Background + int bgAlpha = (int) (0xE8 * alpha); + int bgColor = (bgAlpha << 24) | 0x0c0c14; + DrawerHelper.drawSolidRect(graphics, x, y, w, h, bgColor); + + // Grid pattern + int gridAlpha = (int) (0x08 * alpha); + int gridColor = (gridAlpha << 24) | 0xFFFFFF; + for (int gx = x + 8; gx < x + w; gx += 8) { + graphics.fill(gx, y, gx + 1, y + h, gridColor); + } + for (int gy = y + 8; gy < y + h; gy += 8) { + graphics.fill(x, gy, x + w, gy + 1, gridColor); + } + + // Title bar + int titleBgAlpha = (int) (0xD0 * alpha); + int titleBgColor = (titleBgAlpha << 24) | 0x101820; + DrawerHelper.drawSolidRect(graphics, x, y, w, TITLE_HEIGHT, titleBgColor); + + // Border + int borderAlpha = (int) (0x80 * alpha); + int borderColor = (borderAlpha << 24) | 0x4080FF; + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 1); + + // Title bar accent + graphics.fill(x + 1, y + TITLE_HEIGHT - 2, x + w - 1, y + TITLE_HEIGHT, borderColor); + + // Draw title + drawTitle(graphics, x, y, w, alpha); + + // Draw color picker + drawColorPicker(graphics, x, y, mouseX, mouseY, alpha); + + // Draw close button + drawCloseButton(graphics, x + w - 14, y + 3, mouseX, mouseY, alpha); + + // Draw reset button + drawResetButton(graphics, x + w - 50, y + HEIGHT - 26, mouseX, mouseY, alpha); + + // Draw color preview + drawColorPreview(graphics, x, y, alpha); + + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + } + + private void drawTitle(GuiGraphics graphics, int x, int y, int w, float alpha) { + var font = Minecraft.getInstance().font; + String title = "Star Color"; + int textColor = (int) (0xFF * alpha) << 24 | 0xFFFFFF; + graphics.drawString(font, title, x + 4, y + (TITLE_HEIGHT - font.lineHeight) / 2 + 1, textColor, false); + } + + private void drawColorPicker(GuiGraphics graphics, int baseX, int baseY, int mouseX, int mouseY, float alpha) { + int pickerX = baseX + 6; + int pickerY = baseY + TITLE_HEIGHT + 4; + + // Draw saturation/brightness gradient + for (int py = 0; py < PICKER_SIZE; py++) { + for (int px = 0; px < PICKER_SIZE; px++) { + float s = (float) px / PICKER_SIZE; + float b = 1f - (float) py / PICKER_SIZE; + int color = hsbToRgb(hue, s, b); + int pixelAlpha = (int) (0xFF * alpha); + graphics.fill(pickerX + px, pickerY + py, pickerX + px + 1, pickerY + py + 1, + (pixelAlpha << 24) | color); + } + } + + // Border around picker + int pickerBorder = (int) (0x80 * alpha) << 24 | 0x606080; + DrawerHelper.drawBorder(graphics, pickerX, pickerY, PICKER_SIZE, PICKER_SIZE, pickerBorder, 1); + + // Draw crosshair at current position + int crossX = pickerX + (int) (saturation * PICKER_SIZE); + int crossY = pickerY + (int) ((1f - brightness) * PICKER_SIZE); + int crossColor = (int) (0xFF * alpha) << 24 | 0xFFFFFF; + graphics.fill(crossX - 4, crossY, crossX + 5, crossY + 1, crossColor); + graphics.fill(crossX, crossY - 4, crossX + 1, crossY + 5, crossColor); + + // Draw hue bar + int hueX = pickerX + PICKER_SIZE + 6; + for (int py = 0; py < PICKER_SIZE; py++) { + float h = (float) py / PICKER_SIZE; + int color = hsbToRgb(h, 1f, 1f); + int pixelAlpha = (int) (0xFF * alpha); + graphics.fill(hueX, pickerY + py, hueX + HUE_BAR_WIDTH, pickerY + py + 1, (pixelAlpha << 24) | color); + } + + // Border around hue bar + DrawerHelper.drawBorder(graphics, hueX, pickerY, HUE_BAR_WIDTH, PICKER_SIZE, pickerBorder, 1); + + // Draw hue indicator + int hueY = pickerY + (int) (hue * PICKER_SIZE); + graphics.fill(hueX - 2, hueY - 1, hueX + HUE_BAR_WIDTH + 2, hueY + 2, crossColor); + } + + private void drawColorPreview(GuiGraphics graphics, int baseX, int baseY, float alpha) { + int previewX = baseX + 6 + PICKER_SIZE + 6 + HUE_BAR_WIDTH + 8; + int previewY = baseY + TITLE_HEIGHT + 4; + int previewSize = 30; + + // Background checkerboard for transparency reference + int checkSize = 5; + for (int cy = 0; cy < previewSize / checkSize; cy++) { + for (int cx = 0; cx < previewSize / checkSize; cx++) { + int checkColor = ((cx + cy) % 2 == 0) ? 0xFF404040 : 0xFF808080; + graphics.fill(previewX + cx * checkSize, previewY + cy * checkSize, + previewX + (cx + 1) * checkSize, previewY + (cy + 1) * checkSize, checkColor); + } + } + + // Color preview + int previewAlpha = (int) (0xFF * alpha); + graphics.fill(previewX, previewY, previewX + previewSize, previewY + previewSize, + (previewAlpha << 24) | currentColor); + + // Border + int previewBorder = (int) (0x80 * alpha) << 24 | 0x606080; + DrawerHelper.drawBorder(graphics, previewX, previewY, previewSize, previewSize, previewBorder, 1); + + // Label + var font = Minecraft.getInstance().font; + int textColor = (int) (0xFF * alpha) << 24 | 0xA0A0B0; + graphics.drawString(font, "Preview", previewX, previewY + previewSize + 4, textColor, false); + } + + private void drawCloseButton(GuiGraphics graphics, int x, int y, int mouseX, int mouseY, float alpha) { + int size = 10; + boolean hovered = mouseX >= x && mouseX < x + size && mouseY >= y && mouseY < y + size; + + int bgColor = hovered ? (int) (0xC0 * alpha) << 24 | 0xFF4444 : (int) (0x60 * alpha) << 24 | 0x404050; + int fgColor = (int) (0xFF * alpha) << 24 | 0xFFFFFF; + + graphics.fill(x, y, x + size, y + size, bgColor); + + // X mark + graphics.fill(x + 2, y + 3, x + 4, y + 7, fgColor); + graphics.fill(x + 6, y + 3, x + 8, y + 7, fgColor); + graphics.fill(x + 3, y + 4, x + 7, y + 6, fgColor); + } + + private void drawResetButton(GuiGraphics graphics, int x, int y, int mouseX, int mouseY, float alpha) { + int bw = 44; + int bh = 16; + boolean hovered = mouseX >= x && mouseX < x + bw && mouseY >= y && mouseY < y + bh; + + int bgColor = hovered ? (int) (0xC0 * alpha) << 24 | 0x4080FF : (int) (0x80 * alpha) << 24 | 0x404060; + + graphics.fill(x, y, x + bw, y + bh, bgColor); + DrawerHelper.drawBorder(graphics, x, y, bw, bh, (int) (0x80 * alpha) << 24 | 0x606080, 1); + + var font = Minecraft.getInstance().font; + String text = "Reset"; + int textX = x + (bw - font.width(text)) / 2; + int textY = y + (bh - font.lineHeight) / 2; + int textColor = (int) (0xFF * alpha) << 24 | 0xFFFFFF; + graphics.drawString(font, text, textX, textY, textColor, false); + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isVisible()) return false; + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + + // Check close button + int closeX = x + w - 14; + int closeY = y + 3; + if (mouseX >= closeX && mouseX < closeX + 10 && mouseY >= closeY && mouseY < closeY + 10) { + if (onClose != null) { + onClose.run(); + } + hide(); + playButtonClickSound(); + return true; + } + + // Check reset button + int resetX = x + w - 50; + int resetY = y + HEIGHT - 26; + if (mouseX >= resetX && mouseX < resetX + 44 && mouseY >= resetY && mouseY < resetY + 16) { + resetToDefault(); + playButtonClickSound(); + return true; + } + + // Check color picker area + int pickerX = x + 6; + int pickerY = y + TITLE_HEIGHT + 4; + if (mouseX >= pickerX && mouseX < pickerX + PICKER_SIZE && + mouseY >= pickerY && mouseY < pickerY + PICKER_SIZE) { + draggingPicker = true; + updatePickerFromMouse(mouseX, mouseY, pickerX, pickerY); + return true; + } + + // Check hue bar + int hueX = pickerX + PICKER_SIZE + 6; + if (mouseX >= hueX && mouseX < hueX + HUE_BAR_WIDTH && + mouseY >= pickerY && mouseY < pickerY + PICKER_SIZE) { + draggingHue = true; + updateHueFromMouse(mouseY, pickerY); + return true; + } + + // Check title bar for dragging + if (mouseX >= x && mouseX < x + w - 20 && mouseY >= y && mouseY < y + TITLE_HEIGHT) { + draggingTitle = true; + lastDeltaX = 0; + lastDeltaY = 0; + return true; + } + + // Click inside panel + if (isMouseOverElement(mouseX, mouseY)) { + return super.mouseClicked(mouseX, mouseY, button); + } + + return false; + } + + private void updatePickerFromMouse(double mouseX, double mouseY, int pickerX, int pickerY) { + saturation = Mth.clamp((float) (mouseX - pickerX) / PICKER_SIZE, 0f, 1f); + brightness = Mth.clamp(1f - (float) (mouseY - pickerY) / PICKER_SIZE, 0f, 1f); + updateFromHsb(); + } + + private void updateHueFromMouse(double mouseY, int pickerY) { + hue = Mth.clamp((float) (mouseY - pickerY) / PICKER_SIZE, 0f, 1f); + updateFromHsb(); + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (draggingPicker) { + int pickerX = getPosition().x + 6; + int pickerY = getPosition().y + TITLE_HEIGHT + 4; + updatePickerFromMouse(mouseX, mouseY, pickerX, pickerY); + return true; + } + + if (draggingHue) { + int pickerY = getPosition().y + TITLE_HEIGHT + 4; + updateHueFromMouse(mouseY, pickerY); + return true; + } + + if (draggingTitle) { + double dx = dragX + lastDeltaX; + double dy = dragY + lastDeltaY; + int intDx = (int) dx; + int intDy = (int) dy; + lastDeltaX = dx - intDx; + lastDeltaY = dy - intDy; + addSelfPosition(intDx, intDy); + return true; + } + + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (draggingPicker || draggingHue || draggingTitle) { + draggingPicker = false; + draggingHue = false; + draggingTitle = false; + lastDeltaX = 0; + lastDeltaY = 0; + return true; + } + return super.mouseReleased(mouseX, mouseY, button); + } + + private static float[] rgbToHsb(int rgb) { + int r = (rgb >> 16) & 0xFF; + int g = (rgb >> 8) & 0xFF; + int b = rgb & 0xFF; + + float[] hsb = new float[3]; + java.awt.Color.RGBtoHSB(r, g, b, hsb); + return hsb; + } + + private static int hsbToRgb(float h, float s, float b) { + return java.awt.Color.HSBtoRGB(h, s, b) & 0xFFFFFF; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StarfieldBackgroundWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StarfieldBackgroundWidget.java new file mode 100644 index 000000000..417f4b154 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StarfieldBackgroundWidget.java @@ -0,0 +1,312 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class StarfieldBackgroundWidget extends Widget { + + private final Supplier stageSupplier; + private final List stars = new ArrayList<>(); + private final List nebulae = new ArrayList<>(); + private final Random random = new Random(42); + + private float driftPhase = 0f; + private float nebulaPhase = 0f; + + private static class BackgroundStar { + + float x, y; + float baseX, baseY; + float size; + float twinkleSpeed; + float twinkleOffset; + int color; + float depth; + } + + private static class Nebula { + + float x, y; + float radius; + int color; + float pulseSpeed; + float pulseOffset; + } + + public StarfieldBackgroundWidget(int x, int y, int width, int height, Supplier stageSupplier) { + super(x, y, width, height); + this.stageSupplier = stageSupplier; + initStars(width, height); + initNebulae(width, height); + } + + private void initStars(int w, int h) { + int starCount = 80; + for (int i = 0; i < starCount; i++) { + BackgroundStar star = new BackgroundStar(); + star.baseX = random.nextFloat() * w; + star.baseY = random.nextFloat() * h; + star.x = star.baseX; + star.y = star.baseY; + star.size = 0.5f + random.nextFloat() * 1.5f; + star.twinkleSpeed = 0.02f + random.nextFloat() * 0.05f; + star.twinkleOffset = random.nextFloat() * Mth.TWO_PI; + star.depth = 0.3f + random.nextFloat() * 0.7f; + + float colorRand = random.nextFloat(); + if (colorRand < 0.6f) { + star.color = 0xFFFFFF; + } else if (colorRand < 0.75f) { + star.color = 0xFFDDAA; + } else if (colorRand < 0.85f) { + star.color = 0xAADDFF; + } else if (colorRand < 0.95f) { + star.color = 0xFFAAAA; + } else { + star.color = 0xAAFFAA; + } + + stars.add(star); + } + } + + private void initNebulae(int w, int h) { + int nebulaCount = 4; + int[] nebulaColors = { 0x4020A0, 0xA02040, 0x204080, 0x802060 }; + + for (int i = 0; i < nebulaCount; i++) { + Nebula nebula = new Nebula(); + nebula.x = random.nextFloat() * w; + nebula.y = random.nextFloat() * h; + nebula.radius = 30 + random.nextFloat() * 50; + nebula.color = nebulaColors[i % nebulaColors.length]; + nebula.pulseSpeed = 0.01f + random.nextFloat() * 0.02f; + nebula.pulseOffset = random.nextFloat() * Mth.TWO_PI; + nebulae.add(nebula); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + + Stage stage = stageSupplier.get(); + float driftSpeed = getDriftSpeed(stage); + driftPhase += driftSpeed; + nebulaPhase += 0.02f; + + float driftX = Mth.sin(driftPhase * 0.3f) * 2f; + float driftY = Mth.cos(driftPhase * 0.2f) * 1.5f; + + for (BackgroundStar star : stars) { + star.x = star.baseX + driftX * star.depth; + star.y = star.baseY + driftY * star.depth; + } + } + + private float getDriftSpeed(Stage stage) { + return switch (stage) { + case EMPTY -> 0.01f; + case GROWING -> 0.02f; + case STAR -> 0.015f; + case SUPERSTAR -> 0.03f; + case BLACK_HOLE -> 0.05f; + case DEATH -> 0.08f; + case DEATH_GRACEFUL -> 0.005f; + }; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + Stage stage = stageSupplier.get(); + + drawDeepSpaceGradient(graphics, x, y, w, h, stage); + drawNebulae(graphics, x, y, w, h, stage); + drawStars(graphics, x, y, w, h, stage); + + if (stage == Stage.BLACK_HOLE) { + drawGravitationalDistortion(graphics, x, y, w, h); + } + } + + private void drawDeepSpaceGradient(GuiGraphics graphics, int x, int y, int w, int h, Stage stage) { + int topColor, bottomColor; + + switch (stage) { + case EMPTY -> { + topColor = 0xFF08080C; + bottomColor = 0xFF040406; + } + case GROWING -> { + topColor = 0xFF0A0A14; + bottomColor = 0xFF060610; + } + case STAR -> { + topColor = 0xFF100808; + bottomColor = 0xFF080404; + } + case SUPERSTAR -> { + topColor = 0xFF140808; + bottomColor = 0xFF0A0404; + } + case BLACK_HOLE -> { + topColor = 0xFF0C0410; + bottomColor = 0xFF040208; + } + case DEATH -> { + topColor = 0xFF140404; + bottomColor = 0xFF0A0202; + } + default -> { + topColor = 0xFF0A0808; + bottomColor = 0xFF050404; + } + } + + for (int row = 0; row < h; row++) { + float progress = (float) row / h; + int color = lerpColor(topColor, bottomColor, progress); + graphics.fill(x, y + row, x + w, y + row + 1, color); + } + } + + private void drawNebulae(GuiGraphics graphics, int x, int y, int w, int h, Stage stage) { + float intensity = switch (stage) { + case EMPTY -> 0.3f; + case GROWING -> 0.5f; + case STAR -> 0.6f; + case SUPERSTAR -> 0.8f; + case BLACK_HOLE -> 1.0f; + case DEATH -> 0.4f; + case DEATH_GRACEFUL -> 0.2f; + }; + + for (Nebula nebula : nebulae) { + float pulse = Mth.sin(nebulaPhase + nebula.pulseOffset) * 0.3f + 0.7f; + float radius = nebula.radius * pulse; + + int layers = 8; + for (int layer = layers; layer > 0; layer--) { + float layerProgress = (float) layer / layers; + float layerRadius = radius * layerProgress; + + int alpha = (int) (0x08 * intensity * (1f - layerProgress * 0.7f)); + int color = (alpha << 24) | (nebula.color & 0x00FFFFFF); + + int nx = x + (int) nebula.x; + int ny = y + (int) nebula.y; + int r = (int) layerRadius; + + for (int py = -r; py <= r; py++) { + int halfWidth = (int) Math.sqrt(r * r - py * py); + int drawY = ny + py; + if (drawY >= y && drawY < y + h) { + int x1 = Math.max(x, nx - halfWidth); + int x2 = Math.min(x + w, nx + halfWidth); + if (x1 < x2) { + graphics.fill(x1, drawY, x2, drawY + 1, color); + } + } + } + } + } + } + + private void drawStars(GuiGraphics graphics, int x, int y, int w, int h, Stage stage) { + float baseAlpha = switch (stage) { + case EMPTY -> 0.4f; + case GROWING -> 0.6f; + case STAR -> 0.8f; + case SUPERSTAR -> 0.7f; + case BLACK_HOLE -> 0.5f; + case DEATH -> 0.3f; + case DEATH_GRACEFUL -> 0.5f; + }; + + for (BackgroundStar star : stars) { + float twinkle = Mth.sin(driftPhase * star.twinkleSpeed * 60f + star.twinkleOffset); + float brightness = 0.6f + 0.4f * twinkle; + int alpha = (int) (0xFF * baseAlpha * brightness); + + int starX = x + (int) star.x; + int starY = y + (int) star.y; + + if (starX < x || starX >= x + w || starY < y || starY >= y + h) continue; + + int color = (alpha << 24) | (star.color & 0x00FFFFFF); + + if (star.size > 1.2f) { + graphics.fill(starX - 1, starY, starX + 2, starY + 1, color); + graphics.fill(starX, starY - 1, starX + 1, starY + 2, color); + } else { + graphics.fill(starX, starY, starX + 1, starY + 1, color); + } + + if (star.size > 1.5f && brightness > 0.8f) { + int glowAlpha = (int) (alpha * 0.3f); + int glowColor = (glowAlpha << 24) | (star.color & 0x00FFFFFF); + graphics.fill(starX - 1, starY - 1, starX + 2, starY + 2, glowColor); + } + } + } + + private void drawGravitationalDistortion(GuiGraphics graphics, int x, int y, int w, int h) { + int cx = x + w / 2; + int cy = y + h / 2; + + for (int ring = 0; ring < 5; ring++) { + float ringPhase = driftPhase * 0.5f + ring * 0.5f; + float ringRadius = 30 + ring * 20 + Mth.sin(ringPhase) * 5; + + int alpha = 0x15 - ring * 0x03; + int color = (alpha << 24) | 0x8040FF; + + int r = (int) ringRadius; + for (int angle = 0; angle < 360; angle += 4) { + float rad = angle * Mth.DEG_TO_RAD; + int px = cx + (int) (Mth.cos(rad) * r); + int py = cy + (int) (Mth.sin(rad) * r * 0.4f); + + if (px >= x && px < x + w && py >= y && py < y + h) { + graphics.fill(px, py, px + 1, py + 1, color); + } + } + } + } + + private int lerpColor(int c1, int c2, float t) { + int a1 = (c1 >> 24) & 0xFF, a2 = (c2 >> 24) & 0xFF; + int r1 = (c1 >> 16) & 0xFF, r2 = (c2 >> 16) & 0xFF; + int g1 = (c1 >> 8) & 0xFF, g2 = (c2 >> 8) & 0xFF; + int b1 = c1 & 0xFF, b2 = c2 & 0xFF; + + int a = (int) Mth.lerp(t, a1, a2); + int r = (int) Mth.lerp(t, r1, r2); + int g = (int) Mth.lerp(t, g1, g2); + int b = (int) Mth.lerp(t, b1, b2); + + return (a << 24) | (r << 16) | (g << 8) | b; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarBackgroundWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarBackgroundWidget.java new file mode 100644 index 000000000..9c79db144 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarBackgroundWidget.java @@ -0,0 +1,285 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +/** + * Full-screen background widget that provides unified visual styling + * for the entire Stellar Iris UI, including the inventory area. + */ +public class StellarBackgroundWidget extends Widget { + + private final Supplier stageSupplier; + private float animPhase = 0f; + + public StellarBackgroundWidget(int x, int y, int width, int height, Supplier stageSupplier) { + super(x, y, width, height); + this.stageSupplier = stageSupplier; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + animPhase += 0.02f; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + Stage stage = stageSupplier.get(); + + DrawerHelper.drawGradientRect(graphics, x, y, w, h, 0xFF0c0c12, 0xFF060608, false); + + drawGridPattern(graphics, x, y, w, h); + + drawSidePanels(graphics, x, y, w, h, stage); + + int accentColor = getStageAccentColor(stage, 0.4f); + drawCornerAccents(graphics, x, y, w, h, accentColor); + + int borderColor = getStageAccentColor(stage, 0.2f); + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 1); + } + + private void drawGridPattern(GuiGraphics graphics, int x, int y, int w, int h) { + int gridColor = 0x08FFFFFF; + int spacing = 16; + + // Vertical lines + for (int gx = x + spacing; gx < x + w; gx += spacing) { + graphics.fill(gx, y, gx + 1, y + h, gridColor); + } + + // Horizontal lines + for (int gy = y + spacing; gy < y + h; gy += spacing) { + graphics.fill(x, gy, x + w, gy + 1, gridColor); + } + } + + private void drawCornerAccents(GuiGraphics graphics, int x, int y, int w, int h, int color) { + int len = 20; + int thickness = 2; + + // Top-left + graphics.fill(x, y, x + len, y + thickness, color); + graphics.fill(x, y, x + thickness, y + len, color); + + // Top-right + graphics.fill(x + w - len, y, x + w, y + thickness, color); + graphics.fill(x + w - thickness, y, x + w, y + len, color); + + // Bottom-left + graphics.fill(x, y + h - thickness, x + len, y + h, color); + graphics.fill(x, y + h - len, x + thickness, y + h, color); + + // Bottom-right + graphics.fill(x + w - len, y + h - thickness, x + w, y + h, color); + graphics.fill(x + w - thickness, y + h - len, x + w, y + h, color); + } + + private void drawSidePanels(GuiGraphics graphics, int x, int y, int w, int h, Stage stage) { + int invWidth = 162; + int invX = x + (w - invWidth) / 2; + + int leftPanelW = invX - x - 5; + if (leftPanelW > 10) { + drawTechPanel(graphics, x + 3, y + h - 85, leftPanelW, 80, stage, true); + } + + int rightPanelX = invX + invWidth + 5; + int rightPanelW = (x + w) - rightPanelX - 3; + if (rightPanelW > 10) { + drawStatsPanel(graphics, rightPanelX, y + h - 85, rightPanelW, 80, stage); + } + } + + private void drawStatsPanel(GuiGraphics graphics, int px, int py, int pw, int ph, Stage stage) { + DrawerHelper.drawSolidRect(graphics, px, py, pw, ph, 0x40000000); + + int borderColor = getStageAccentColor(stage, 0.2f); + DrawerHelper.drawBorder(graphics, px, py, pw, ph, borderColor, 1); + + int accentColor = getStageAccentColor(stage, 0.5f); + graphics.fill(px + 1, py + 1, px + pw - 1, py + 3, accentColor); + + var font = net.minecraft.client.Minecraft.getInstance().font; + int labelColor = 0xFF606080; + int valueColor = 0xFFCCCCCC; + + graphics.drawString(font, "STAR STATS", px + 4, py + 6, accentColor, false); + + float temp = getStageTemp(stage); + float mass = getStageMass(stage); + float output = getStageOutput(stage); + + int row1 = py + 20; + int row2 = py + 32; + int row3 = py + 44; + int row4 = py + 56; + + graphics.drawString(font, "TEMP:", px + 4, row1, labelColor, false); + graphics.drawString(font, formatTemp(temp), px + 35, row1, getTemperatureColor(temp), false); + + graphics.drawString(font, "MASS:", px + 4, row2, labelColor, false); + graphics.drawString(font, String.format("%.1f M\u2609", mass), px + 35, row2, valueColor, false); + + graphics.drawString(font, "OUT:", px + 4, row3, labelColor, false); + graphics.drawString(font, formatEnergy(output), px + 30, row3, valueColor, false); + + String status = getStatusString(stage); + graphics.drawString(font, status, px + 4, row4, getStatusColor(stage), false); + } + + private float getStageTemp(Stage stage) { + return switch (stage) { + case EMPTY -> 2.7f; + case GROWING -> 5_000_000f; + case STAR -> 15_000_000f; + case SUPERSTAR -> 100_000_000f; + case BLACK_HOLE -> Float.POSITIVE_INFINITY; + case DEATH -> 500_000_000f; + case DEATH_GRACEFUL -> 1_000_000f; + }; + } + + private float getStageMass(Stage stage) { + return switch (stage) { + case EMPTY -> 0f; + case GROWING -> 0.3f; + case STAR -> 1f; + case SUPERSTAR -> 8f; + case BLACK_HOLE -> 25f; + case DEATH -> 12f; + case DEATH_GRACEFUL -> 0.1f; + }; + } + + private float getStageOutput(Stage stage) { + return switch (stage) { + case EMPTY -> 0f; + case GROWING -> 1_000f; + case STAR -> 50_000f; + case SUPERSTAR -> 500_000f; + case BLACK_HOLE -> 10_000_000f; + case DEATH -> 100_000_000f; + case DEATH_GRACEFUL -> 500f; + }; + } + + private String formatTemp(float temp) { + if (Float.isInfinite(temp)) return "\u221E K"; + if (temp >= 1_000_000) return String.format("%.0fM K", temp / 1_000_000); + if (temp >= 1000) return String.format("%.0fk K", temp / 1000); + return String.format("%.1f K", temp); + } + + private String formatEnergy(float energy) { + if (energy >= 1_000_000) return String.format("%.1f PW", energy / 1_000_000); + if (energy >= 1000) return String.format("%.0f TW", energy / 1000); + return String.format("%.0f GW", energy); + } + + private int getTemperatureColor(float temp) { + if (temp >= 100_000_000) return 0xFFFF4444; + if (temp >= 10_000_000) return 0xFFFFAA44; + if (temp >= 1_000_000) return 0xFFFFFF44; + return 0xFFCCCCCC; + } + + private String getStatusString(Stage stage) { + return switch (stage) { + case EMPTY -> "DORMANT"; + case GROWING -> "IGNITING"; + case STAR -> "STABLE"; + case SUPERSTAR -> "CRITICAL"; + case BLACK_HOLE -> "CONTAINED"; + case DEATH -> "FAILURE"; + case DEATH_GRACEFUL -> "SHUTDOWN"; + }; + } + + private int getStatusColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0xFF606060; + case GROWING -> 0xFF66AAFF; + case STAR -> 0xFF66FF66; + case SUPERSTAR -> 0xFFFFAA44; + case BLACK_HOLE -> 0xFFAA66FF; + case DEATH -> 0xFFFF4444; + case DEATH_GRACEFUL -> 0xFF886666; + }; + } + + private void drawTechPanel(GuiGraphics graphics, int px, int py, int pw, int ph, Stage stage, boolean isLeft) { + // Panel background + DrawerHelper.drawSolidRect(graphics, px, py, pw, ph, 0x40000000); + + // Border + int borderColor = getStageAccentColor(stage, 0.2f); + DrawerHelper.drawBorder(graphics, px, py, pw, ph, borderColor, 1); + + // Accent line at top + int accentColor = getStageAccentColor(stage, 0.5f); + graphics.fill(px + 1, py + 1, px + pw - 1, py + 3, accentColor); + + // Animated scan line + float scanPos = (animPhase * 0.5f) % 1f; + int scanY = py + 5 + (int) ((ph - 10) * scanPos); + int scanColor = getStageAccentColor(stage, 0.15f); + graphics.fill(px + 2, scanY, px + pw - 2, scanY + 2, scanColor); + + // Tech decoration lines + int lineColor = 0x20FFFFFF; + int lineY = py + 15; + for (int i = 0; i < 5 && lineY + 10 < py + ph; i++) { + int lineW = (int) ((pw - 10) * (0.3f + 0.5f * Math.abs(Mth.sin(animPhase + i * 0.5f)))); + if (isLeft) { + graphics.fill(px + 5, lineY, px + 5 + lineW, lineY + 2, lineColor); + } else { + graphics.fill(px + pw - 5 - lineW, lineY, px + pw - 5, lineY + 2, lineColor); + } + lineY += 12; + } + + // Small blinking indicators + int indicatorY = py + ph - 15; + for (int i = 0; i < 3; i++) { + int ix = isLeft ? (px + 8 + i * 8) : (px + pw - 8 - i * 8 - 4); + boolean blink = ((int) (animPhase * 3 + i) % 3) == 0; + int indColor = blink ? getStageAccentColor(stage, 0.8f) : 0x30404050; + graphics.fill(ix, indicatorY, ix + 4, indicatorY + 4, indColor); + } + } + + private int getStageAccentColor(Stage stage, float alpha) { + int a = (int) (alpha * 255) << 24; + return switch (stage) { + case EMPTY -> a | 0x404060; + case GROWING -> a | 0x6080FF; + case STAR -> a | 0xFFCC44; + case SUPERSTAR -> a | 0xFF8844; + case BLACK_HOLE -> a | 0x8040FF; + case DEATH -> a | 0xFF2020; + case DEATH_GRACEFUL -> a | 0x804040; + }; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarCommandConsoleWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarCommandConsoleWidget.java new file mode 100644 index 000000000..51f85e6af --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarCommandConsoleWidget.java @@ -0,0 +1,331 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine; +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.gregtechceu.gtceu.api.gui.GuiTextures; +import com.gregtechceu.gtceu.api.gui.widget.SlotWidget; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class StellarCommandConsoleWidget extends WidgetGroup { + + public static final int WIDTH = 360; + public static final int HEIGHT = 220; + + private final Supplier machineSupplier; + + private Stage lastSyncedStage = Stage.EMPTY; + private float fuelLevel = 0f; + private boolean canIgnite = false; + private boolean debugPrimed = false; + + private int tickCounter = 0; + + public StellarCommandConsoleWidget(Supplier machineSupplier) { + super(0, 0, WIDTH, HEIGHT); + this.machineSupplier = machineSupplier; + initWidgets(); + } + + private void initWidgets() { + addWidget(new StarfieldBackgroundWidget(0, 0, WIDTH, HEIGHT, this::getCurrentStage)); + + addWidget(new HolographicScanlineWidget(0, 0, WIDTH, HEIGHT, this::getCurrentStage)); + + addWidget(new EnergyConduitWidget(0, 0, WIDTH, HEIGHT, this::getCurrentStage)); + + int coreSize = 130; + int coreX = 20; + int coreY = 30; + addWidget(new StellarCoreWidget(coreX, coreY, coreSize, this::getCurrentStage)); + + addWidget(new OrbitalRingsWidget(coreX - 10, coreY - 10, coreSize + 20, coreSize + 20, this::getCurrentStage)); + + int telemetryX = coreX + coreSize + 25; + int telemetryW = WIDTH - telemetryX - 15; + int telemetryH = 130; + addWidget(new TelemetryPanelWidget(telemetryX, 25, telemetryW, telemetryH, + machineSupplier, this::getCurrentStage)); + + int controlY = 160; + int controlH = HEIGHT - controlY - 10; + addWidget(new ControlPanelWidget(telemetryX, controlY, telemetryW, controlH, + machineSupplier, this)); + + addWidget(new WarningOverlayWidget(0, 0, WIDTH, HEIGHT, this::getCurrentStage)); + + addWidget(new DebugPrimeButton(5, HEIGHT - 19, 50, 14, this::requestDebugPrime)); + } + + public Stage getCurrentStage() { + return lastSyncedStage; + } + + public float getFuelLevel() { + return fuelLevel; + } + + public boolean canIgnite() { + return canIgnite; + } + + public int getTickCounter() { + return tickCounter; + } + + public void requestDebugPrime() { + writeClientAction(3, buf -> {}); + } + + public void requestIgnition() { + writeClientAction(1, buf -> {}); + } + + @Override + public void detectAndSendChanges() { + super.detectAndSendChanges(); + + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine == null) return; + + Stage currentStage = machine.getStage(); + if (currentStage != lastSyncedStage) { + lastSyncedStage = currentStage; + writeUpdateInfo(203, buf -> buf.writeEnum(currentStage)); + } + + float newFuelLevel = calculateFuelLevel(machine); + if (Math.abs(newFuelLevel - fuelLevel) > 0.01f) { + fuelLevel = newFuelLevel; + writeUpdateInfo(204, buf -> buf.writeFloat(fuelLevel)); + } + + boolean newCanIgnite = checkIgnitionRequirements(machine); + if (newCanIgnite != canIgnite) { + canIgnite = newCanIgnite; + writeUpdateInfo(205, buf -> buf.writeBoolean(canIgnite)); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readUpdateInfo(int id, FriendlyByteBuf buffer) { + if (id == 203) { + lastSyncedStage = buffer.readEnum(Stage.class); + } else if (id == 204) { + fuelLevel = buffer.readFloat(); + } else if (id == 205) { + canIgnite = buffer.readBoolean(); + } else { + super.readUpdateInfo(id, buffer); + } + } + + private float calculateFuelLevel(IrisMultiblockMachine machine) { + if (debugPrimed) return 1f; + + if (machine.getStage() == Stage.EMPTY) { + return machine.getInventory().getStackInSlot(0).isEmpty() ? 0f : 1f; + } + return switch (machine.getStage()) { + case EMPTY -> 0f; + case GROWING -> 0.5f; + case STAR, SUPERSTAR, BLACK_HOLE -> 1f; + case DEATH, DEATH_GRACEFUL -> 0.2f; + }; + } + + private boolean checkIgnitionRequirements(IrisMultiblockMachine machine) { + if (debugPrimed) return true; + if (machine.getStage() != Stage.EMPTY) return false; + if (machine.getInventory().getStackInSlot(0).isEmpty()) return false; + return fuelLevel >= 0.8f; + } + + @Override + public void handleClientAction(int id, FriendlyByteBuf buffer) { + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine == null) return; + + if (id == 1) { + if (canIgnite || debugPrimed) { + machine.setStarStage(); + debugPrimed = false; + } + } else if (id == 2) { + machine.setStarStage(); + } else if (id == 3) { + debugPrimed = true; + } else { + super.handleClientAction(id, buffer); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + tickCounter++; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + drawConsoleFrame(graphics, x, y, w, h); + + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + drawConsoleTitle(graphics, x, y, w); + } + + private void drawConsoleFrame(GuiGraphics graphics, int x, int y, int w, int h) { + int frameColor = getStageFrameColor(lastSyncedStage); + int frameGlow = (0x30 << 24) | (frameColor & 0x00FFFFFF); + + graphics.fill(x - 2, y - 2, x + w + 2, y, frameGlow); + graphics.fill(x - 2, y + h, x + w + 2, y + h + 2, frameGlow); + graphics.fill(x - 2, y, x, y + h, frameGlow); + graphics.fill(x + w, y, x + w + 2, y + h, frameGlow); + + int cornerLen = 15; + int cornerColor = (0xC0 << 24) | frameColor; + + graphics.fill(x, y, x + cornerLen, y + 2, cornerColor); + graphics.fill(x, y, x + 2, y + cornerLen, cornerColor); + + graphics.fill(x + w - cornerLen, y, x + w, y + 2, cornerColor); + graphics.fill(x + w - 2, y, x + w, y + cornerLen, cornerColor); + + graphics.fill(x, y + h - 2, x + cornerLen, y + h, cornerColor); + graphics.fill(x, y + h - cornerLen, x + 2, y + h, cornerColor); + + graphics.fill(x + w - cornerLen, y + h - 2, x + w, y + h, cornerColor); + graphics.fill(x + w - 2, y + h - cornerLen, x + w, y + h, cornerColor); + } + + private void drawConsoleTitle(GuiGraphics graphics, int x, int y, int w) { + var font = Minecraft.getInstance().font; + + String title = "STELLAR IRIS COMMAND CONSOLE"; + int titleW = font.width(title); + int titleX = x + (w - titleW) / 2; + int titleY = y + 8; + + int frameColor = getStageFrameColor(lastSyncedStage); + + int bgW = titleW + 20; + int bgX = x + (w - bgW) / 2; + graphics.fill(bgX, titleY - 3, bgX + bgW, titleY + 11, 0xE0080810); + + int borderColor = (0x80 << 24) | frameColor; + graphics.fill(bgX, titleY - 3, bgX + bgW, titleY - 2, borderColor); + graphics.fill(bgX, titleY + 10, bgX + bgW, titleY + 11, borderColor); + graphics.fill(bgX, titleY - 2, bgX + 1, titleY + 10, borderColor); + graphics.fill(bgX + bgW - 1, titleY - 2, bgX + bgW, titleY + 10, borderColor); + + int textColor = 0xFFFFFFFF; + if (lastSyncedStage == Stage.DEATH && (tickCounter / 5) % 2 == 0) { + textColor = 0xFFFF4444; + } + + graphics.drawString(font, title, titleX, titleY, textColor, true); + } + + private int getStageFrameColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0x506080; + case GROWING -> 0x6090FF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF7722; + case BLACK_HOLE -> 0xAA55FF; + case DEATH -> 0xFF3030; + case DEATH_GRACEFUL -> 0x664040; + }; + } + + private static class ControlPanelWidget extends WidgetGroup { + + private final Supplier machineSupplier; + private final StellarCommandConsoleWidget parent; + + public ControlPanelWidget(int x, int y, int width, int height, + Supplier machineSupplier, + StellarCommandConsoleWidget parent) { + super(x, y, width, height); + this.machineSupplier = machineSupplier; + this.parent = parent; + initWidgets(); + } + + private void initWidgets() { + addWidget(new FuelGaugeWidget(5, 5, getSize().width - 75, 22, parent::getFuelLevel)); + + addWidget(new IgnitionButtonWidget( + 5, 30, getSize().width - 75, 22, + parent::canIgnite, + () -> parent.getCurrentStage() == Stage.EMPTY || parent.canIgnite(), + parent::requestIgnition)); + + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine != null) { + int slotX = getSize().width - 65; + SlotWidget starSeedSlot = new SlotWidget(machine.getInventory().storage, 0, slotX, 12, true, true); + starSeedSlot.setBackground(GuiTextures.SLOT, GuiTextures.ATOMIC_OVERLAY_1); + addWidget(starSeedSlot); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + DrawerHelper.drawSolidRect(graphics, x, y, w, h, 0xCC0a0a14); + + Stage stage = parent.getCurrentStage(); + int accentColor = getStageAccentColor(stage); + DrawerHelper.drawBorder(graphics, x, y, w, h, (0x80 << 24) | accentColor, 1); + graphics.fill(x + 1, y + 1, x + w - 1, y + 3, (0x60 << 24) | accentColor); + + var font = Minecraft.getInstance().font; + graphics.drawString(font, "IGNITION CONTROL", x + 5, y - 8, (0xA0 << 24) | accentColor, false); + + int slotX = x + w - 65; + graphics.drawString(font, "SEED", slotX + 8, y + 32, 0xFF606080, false); + + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + } + + private int getStageAccentColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0x506080; + case GROWING -> 0x6090FF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF7722; + case BLACK_HOLE -> 0xAA55FF; + case DEATH -> 0xFF3030; + case DEATH_GRACEFUL -> 0x664040; + }; + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarConduitWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarConduitWidget.java new file mode 100644 index 000000000..124fc48ec --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarConduitWidget.java @@ -0,0 +1,277 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class StellarConduitWidget extends Widget { + + private final Supplier stageSupplier; + private final int coreX, coreY, coreSize; + private final int panelX, panelY, panelW, panelH; + + private final List pulses = new ArrayList<>(); + private float flowPhase = 0f; + + private static class EnergyPulse { + + float position; + float speed; + float intensity; + int pathIndex; + boolean alive = true; + } + + public StellarConduitWidget(int x, int y, int width, int height, + int coreX, int coreY, int coreSize, + int panelX, int panelY, int panelW, int panelH, + Supplier stageSupplier) { + super(x, y, width, height); + this.coreX = coreX; + this.coreY = coreY; + this.coreSize = coreSize; + this.panelX = panelX; + this.panelY = panelY; + this.panelW = panelW; + this.panelH = panelH; + this.stageSupplier = stageSupplier; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + + Stage stage = stageSupplier.get(); + float speed = getFlowSpeed(stage); + flowPhase += speed; + + if (stage != Stage.EMPTY && stage != Stage.DEATH_GRACEFUL) { + if (Math.random() < speed * 3) { + spawnPulse(stage); + } + } + + pulses.removeIf(p -> !p.alive); + for (EnergyPulse pulse : pulses) { + pulse.position += pulse.speed; + if (pulse.position > 1f) { + pulse.alive = false; + } + } + } + + private void spawnPulse(Stage stage) { + if (pulses.size() > 15) return; + + EnergyPulse pulse = new EnergyPulse(); + pulse.position = 0f; + pulse.speed = 0.015f + (float) Math.random() * 0.02f; + pulse.intensity = 0.6f + (float) Math.random() * 0.4f; + pulse.pathIndex = (int) (Math.random() * 6); + + if (stage == Stage.DEATH) pulse.speed *= 2.5f; + else if (stage == Stage.BLACK_HOLE) pulse.speed *= 1.8f; + else if (stage == Stage.SUPERSTAR) pulse.speed *= 1.4f; + + pulses.add(pulse); + } + + private float getFlowSpeed(Stage stage) { + return switch (stage) { + case EMPTY -> 0.003f; + case GROWING -> 0.025f; + case STAR -> 0.018f; + case SUPERSTAR -> 0.035f; + case BLACK_HOLE -> 0.045f; + case DEATH -> 0.07f; + case DEATH_GRACEFUL -> 0.008f; + }; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int x = getPosition().x; + int y = getPosition().y; + + Stage stage = stageSupplier.get(); + int color = getStageColor(stage); + + drawConduitPaths(graphics, x, y, color, stage); + drawFlowingEnergy(graphics, x, y, color, stage); + drawPulses(graphics, x, y, color); + drawJunctionNodes(graphics, x, y, color, stage); + } + + private void drawConduitPaths(GuiGraphics graphics, int ox, int oy, int color, Stage stage) { + int lineColor = 0x30000000 | (color & 0x00FFFFFF); + int glowColor = 0x15000000 | (color & 0x00FFFFFF); + + int cx = ox + coreX + coreSize / 2; + int cy = ox + coreY + coreSize / 2; + int coreRadius = coreSize / 2 + 5; + + int px = ox + panelX; + int py = oy + panelY; + int pw = panelW; + int ph = panelH; + + drawHorizontalConduit(graphics, cx + coreRadius, cy - 20, px - (cx + coreRadius), lineColor, glowColor); + drawHorizontalConduit(graphics, cx + coreRadius, cy + 20, px - (cx + coreRadius), lineColor, glowColor); + + drawVerticalConduit(graphics, px + pw / 2, py, oy + 5 - py, lineColor, glowColor); + drawVerticalConduit(graphics, px + pw / 2, py + ph, oy + getSize().height - 5 - (py + ph), lineColor, + glowColor); + + drawVerticalConduit(graphics, ox + coreX + coreSize / 2, oy + 5, coreY - 10, lineColor, glowColor); + drawVerticalConduit(graphics, ox + coreX + coreSize / 2, oy + coreY + coreSize + 5, + getSize().height - coreY - coreSize - 10, lineColor, glowColor); + } + + private void drawHorizontalConduit(GuiGraphics graphics, int x, int y, int length, int lineColor, int glowColor) { + if (length <= 0) return; + graphics.fill(x, y - 1, x + length, y + 2, glowColor); + graphics.fill(x, y, x + length, y + 1, lineColor); + } + + private void drawVerticalConduit(GuiGraphics graphics, int x, int y, int length, int lineColor, int glowColor) { + if (length <= 0) return; + graphics.fill(x - 1, y, x + 2, y + length, glowColor); + graphics.fill(x, y, x + 1, y + length, lineColor); + } + + private void drawFlowingEnergy(GuiGraphics graphics, int ox, int oy, int color, Stage stage) { + if (stage == Stage.EMPTY) return; + + int segments = 12; + float segmentSpacing = 1f / segments; + + int cx = ox + coreX + coreSize / 2; + int cy = oy + coreY + coreSize / 2; + int coreRadius = coreSize / 2 + 5; + int px = ox + panelX; + + for (int i = 0; i < segments; i++) { + float phase = (flowPhase + i * segmentSpacing) % 1f; + float brightness = Mth.sin(phase * Mth.PI); + if (brightness < 0.15f) continue; + + int alpha = (int) (0x80 * brightness); + int segColor = (alpha << 24) | (color & 0x00FFFFFF); + + int topConduitX = cx + coreRadius + (int) ((px - cx - coreRadius) * phase); + graphics.fill(topConduitX - 1, cy - 21, topConduitX + 2, cy - 19, segColor); + + int bottomConduitX = cx + coreRadius + (int) ((px - cx - coreRadius) * (1f - phase)); + graphics.fill(bottomConduitX - 1, cy + 19, bottomConduitX + 2, cy + 21, segColor); + } + } + + private void drawPulses(GuiGraphics graphics, int ox, int oy, int color) { + int cx = ox + coreX + coreSize / 2; + int cy = oy + coreY + coreSize / 2; + int coreRadius = coreSize / 2 + 5; + int px = ox + panelX; + int py = oy + panelY; + int pw = panelW; + int ph = panelH; + + for (EnergyPulse pulse : pulses) { + float brightness = pulse.intensity * (1f - pulse.position * 0.3f); + int alpha = (int) (0xDD * brightness); + int pulseColor = (alpha << 24) | (color & 0x00FFFFFF); + int coreColor = (alpha << 24) | 0xFFFFFF; + + int pulseX, pulseY; + + switch (pulse.pathIndex % 6) { + case 0 -> { + pulseX = cx + coreRadius + (int) ((px - cx - coreRadius) * pulse.position); + pulseY = cy - 20; + } + case 1 -> { + pulseX = cx + coreRadius + (int) ((px - cx - coreRadius) * pulse.position); + pulseY = cy + 20; + } + case 2 -> { + pulseX = px + pw / 2; + pulseY = py - (int) ((py - oy - 5) * pulse.position); + } + case 3 -> { + pulseX = px + pw / 2; + pulseY = py + ph + (int) ((oy + getSize().height - 5 - py - ph) * pulse.position); + } + case 4 -> { + pulseX = ox + coreX + coreSize / 2; + pulseY = oy + 5 + (int) ((coreY - 10) * pulse.position); + } + default -> { + pulseX = ox + coreX + coreSize / 2; + pulseY = oy + coreY + coreSize + 5 + + (int) ((getSize().height - coreY - coreSize - 10) * pulse.position); + } + } + + graphics.fill(pulseX - 2, pulseY - 2, pulseX + 3, pulseY + 3, pulseColor); + graphics.fill(pulseX - 1, pulseY - 1, pulseX + 2, pulseY + 2, coreColor); + } + } + + private void drawJunctionNodes(GuiGraphics graphics, int ox, int oy, int color, Stage stage) { + float pulse = Mth.sin(flowPhase * 4f) * 0.3f + 0.7f; + int nodeAlpha = (int) (0x90 * pulse); + int nodeColor = (nodeAlpha << 24) | (color & 0x00FFFFFF); + int nodeGlow = (nodeAlpha / 2 << 24) | (color & 0x00FFFFFF); + + int cx = ox + coreX + coreSize / 2; + int cy = oy + coreY + coreSize / 2; + int coreRadius = coreSize / 2 + 5; + int px = ox + panelX; + int py = oy + panelY; + int pw = panelW; + int ph = panelH; + + int[][] nodes = { + { cx + coreRadius, cy - 20 }, + { cx + coreRadius, cy + 20 }, + { px, cy - 20 }, + { px, cy + 20 }, + { px + pw / 2, py }, + { px + pw / 2, py + ph }, + { cx, oy + 5 }, + { cx, oy + getSize().height - 5 }, + }; + + for (int[] node : nodes) { + graphics.fill(node[0] - 3, node[1] - 3, node[0] + 4, node[1] + 4, nodeGlow); + graphics.fill(node[0] - 2, node[1] - 2, node[0] + 3, node[1] + 3, nodeColor); + graphics.fill(node[0] - 1, node[1] - 1, node[0] + 2, node[1] + 2, 0xDDFFFFFF); + } + } + + private int getStageColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0x506080; + case GROWING -> 0x6090FF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF7722; + case BLACK_HOLE -> 0xAA55FF; + case DEATH -> 0xFF3030; + case DEATH_GRACEFUL -> 0x664040; + }; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarCoreWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarCoreWidget.java new file mode 100644 index 000000000..20ca533f3 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarCoreWidget.java @@ -0,0 +1,432 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class StellarCoreWidget extends Widget { + + private final Supplier stageSupplier; + private final java.util.function.IntSupplier customColorSupplier; + + private float animPhase = 0f; + private float pulsePhase = 0f; + private float transitionProgress = 1f; + private Stage previousStage = Stage.EMPTY; + private Stage targetStage = Stage.EMPTY; + + private float prestigeScale = 1f; + private float prestigeAlpha = 1f; + private boolean prestigeAnimating = false; + + public StellarCoreWidget(int x, int y, int size, Supplier stageSupplier) { + this(x, y, size, stageSupplier, null); + } + + public StellarCoreWidget(int x, int y, int size, Supplier stageSupplier, + java.util.function.IntSupplier customColorSupplier) { + super(x, y, size, size); + this.stageSupplier = stageSupplier; + this.customColorSupplier = customColorSupplier; + } + + private int getCustomColor() { + return customColorSupplier != null ? customColorSupplier.getAsInt() : -1; + } + + public void setPrestigeScale(float scale) { + this.prestigeScale = Mth.clamp(scale, 0f, 1f); + } + + public void setPrestigeAlpha(float alpha) { + this.prestigeAlpha = Mth.clamp(alpha, 0f, 1f); + } + + public void setPrestigeAnimating(boolean animating) { + this.prestigeAnimating = animating; + if (!animating) { + this.prestigeScale = 1f; + this.prestigeAlpha = 1f; + } + } + + public boolean isPrestigeAnimating() { + return prestigeAnimating; + } + + public float getPrestigeScale() { + return prestigeScale; + } + + public float getPrestigeAlpha() { + return prestigeAlpha; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + animPhase += 0.03f; + pulsePhase += 0.08f; + + Stage current = stageSupplier.get(); + if (current != targetStage) { + previousStage = targetStage; + targetStage = current; + transitionProgress = 0f; + } + + if (transitionProgress < 1f) { + transitionProgress = Math.min(1f, transitionProgress + 0.02f); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + if (prestigeAnimating && prestigeAlpha <= 0f) { + return; + } + + int cx = getPosition().x + getSize().width / 2; + int cy = getPosition().y + getSize().height / 2; + int maxRadius = getSize().width / 2 - 5; + + if (prestigeAnimating) { + maxRadius = (int) (maxRadius * prestigeScale); + } + + Stage stage = stageSupplier.get(); + + drawVoidBackground(graphics, cx, cy, getSize().width / 2 - 5); + + switch (stage) { + case EMPTY -> drawEmptyCore(graphics, cx, cy, maxRadius); + case GROWING -> drawGrowingCore(graphics, cx, cy, maxRadius); + case STAR -> drawStarCore(graphics, cx, cy, maxRadius); + case SUPERSTAR -> drawSuperstarCore(graphics, cx, cy, maxRadius); + case BLACK_HOLE -> drawBlackHoleCore(graphics, cx, cy, maxRadius); + case DEATH, DEATH_GRACEFUL -> drawDeathCore(graphics, cx, cy, maxRadius, stage); + } + + if (!prestigeAnimating) { + drawStageLabel(graphics, cx, stage); + } + } + + private void drawVoidBackground(GuiGraphics graphics, int cx, int cy, int radius) { + for (int r = radius; r > 0; r -= 3) { + float progress = (float) r / radius; + int alpha = (int) (30 * progress); + int color = (alpha << 24) | 0x101020; + drawCircle(graphics, cx, cy, r, color); + } + } + + private void drawEmptyCore(GuiGraphics graphics, int cx, int cy, int radius) { + float pulse = 0.5f + 0.3f * Mth.sin(pulsePhase); + int alpha = (int) (pulse * 60); + + drawCircleRing(graphics, cx, cy, radius - 5, 2, (alpha << 24) | 0x404060); + + int innerRadius = radius / 3; + for (int r = innerRadius; r > 0; r -= 2) { + float glowProgress = (float) r / innerRadius; + int glowAlpha = (int) (20 * glowProgress * pulse); + drawCircle(graphics, cx, cy, r, (glowAlpha << 24) | 0x303050); + } + + drawCircle(graphics, cx, cy, 3, (int) (pulse * 100) << 24 | 0x505080); + } + + private void drawGrowingCore(GuiGraphics graphics, int cx, int cy, int radius) { + float pulse = 0.8f + 0.2f * Mth.sin(pulsePhase * 1.5f); + float grow = 0.3f + 0.4f * (Mth.sin(animPhase * 0.5f) * 0.5f + 0.5f); + + int coreRadius = (int) (radius * grow * pulse); + + for (int i = 0; i < 8; i++) { + float angle = animPhase * 2f + i * Mth.PI / 4f; + float dist = radius * 0.8f * (0.5f + 0.5f * Mth.sin(animPhase + i)); + int px = cx + (int) (Mth.cos(angle) * dist); + int py = cy + (int) (Mth.sin(angle) * dist); + int pAlpha = (int) (100 * (1f - dist / (radius * 0.8f))); + drawCircle(graphics, px, py, 2, (pAlpha << 24) | 0x6090FF); + } + + int[] colors = { 0x2040A0, 0x4060C0, 0x6080E0, 0x80A0FF }; + for (int layer = 0; layer < colors.length; layer++) { + int layerRadius = coreRadius - layer * 3; + if (layerRadius > 0) { + int alpha = 60 + layer * 30; + drawCircle(graphics, cx, cy, layerRadius, (alpha << 24) | colors[layer]); + } + } + + drawCircle(graphics, cx, cy, Math.max(3, coreRadius / 4), 0xDDFFFFFF); + } + + private void drawStarCore(GuiGraphics graphics, int cx, int cy, int radius) { + float pulse = 0.95f + 0.05f * Mth.sin(pulsePhase); + int coreRadius = (int) (radius * 0.7f * pulse); + + int customColor = getCustomColor(); + int baseColor = customColor != -1 ? customColor : 0xFFCC44; + + int[] colors = generateColorGradient(baseColor); + int coronaColor = blendTowardsWhite(baseColor, 0.3f); + + for (int r = coreRadius + 15; r > coreRadius; r -= 2) { + float glowProgress = (float) (r - coreRadius) / 15f; + int alpha = (int) ((1f - glowProgress) * 60); + drawCircle(graphics, cx, cy, r, (alpha << 24) | coronaColor); + } + + for (int layer = 0; layer < colors.length; layer++) { + int layerRadius = coreRadius - layer * (coreRadius / colors.length); + if (layerRadius > 0) { + int alpha = 180 + layer * 15; + alpha = Math.min(255, alpha); + drawCircle(graphics, cx, cy, layerRadius, (alpha << 24) | colors[layer]); + } + } + + int hotCenterRadius = coreRadius / 4; + drawCircle(graphics, cx, cy, hotCenterRadius, 0xEEFFFFFF); + + drawSolarFlares(graphics, cx, cy, coreRadius, coronaColor); + } + + private void drawSuperstarCore(GuiGraphics graphics, int cx, int cy, int radius) { + float pulse = 0.9f + 0.1f * Mth.sin(pulsePhase * 0.7f); + int coreRadius = (int) (radius * 0.85f * pulse); + + int customColor = getCustomColor(); + int baseColor = customColor != -1 ? shiftHue(customColor, 0.05f) : 0xFF7722; + + int[] colors = generateColorGradient(baseColor); + int coronaColor = darken(baseColor, 0.7f); + + for (int r = coreRadius + 20; r > coreRadius; r -= 2) { + float glowProgress = (float) (r - coreRadius) / 20f; + int alpha = (int) ((1f - glowProgress) * 80); + drawCircle(graphics, cx, cy, r, (alpha << 24) | coronaColor); + } + + for (int layer = 0; layer < colors.length; layer++) { + int layerRadius = coreRadius - layer * (coreRadius / colors.length); + if (layerRadius > 0) { + int alpha = 200 + layer * 11; + alpha = Math.min(255, alpha); + drawCircle(graphics, cx, cy, layerRadius, (alpha << 24) | colors[layer]); + } + } + + drawCircle(graphics, cx, cy, coreRadius / 3, 0xFFFFEECC); + + drawSolarFlares(graphics, cx, cy, coreRadius, coronaColor); + drawSolarFlares(graphics, cx, cy, coreRadius * 0.8f, blendTowardsWhite(baseColor, 0.5f)); + } + + private void drawBlackHoleCore(GuiGraphics graphics, int cx, int cy, int radius) { + int eventHorizonRadius = (int) (radius * 0.3f); + drawCircle(graphics, cx, cy, eventHorizonRadius, 0xFF000000); + + float diskPulse = 0.9f + 0.1f * Mth.sin(pulsePhase * 0.5f); + for (int i = 0; i < 360; i += 5) { + float angle = Mth.DEG_TO_RAD * i + animPhase; + float diskRadius = radius * 0.7f * diskPulse; + float variance = 0.1f * Mth.sin(angle * 3 + animPhase * 2); + diskRadius *= (1f + variance); + + int px = cx + (int) (Mth.cos(angle) * diskRadius); + int py = cy + (int) (Mth.sin(angle) * diskRadius * 0.3f); + + float colorPhase = (i / 360f + animPhase * 0.1f) % 1f; + int r = (int) (128 + 127 * Mth.sin(colorPhase * Mth.TWO_PI)); + int g = (int) (64 + 64 * Mth.sin(colorPhase * Mth.TWO_PI + 1)); + int b = (int) (180 + 75 * Mth.sin(colorPhase * Mth.TWO_PI + 2)); + int color = 0xAA000000 | (r << 16) | (g << 8) | b; + + drawCircle(graphics, px, py, 2, color); + } + + drawCircleRing(graphics, cx, cy, eventHorizonRadius + 3, 2, 0x60FFFFFF); + + for (int r = eventHorizonRadius; r > eventHorizonRadius - 10 && r > 0; r--) { + int alpha = (int) (40 * (1f - (float) (eventHorizonRadius - r) / 10f)); + drawCircle(graphics, cx, cy, r, (alpha << 24) | 0x6040A0); + } + } + + private void drawDeathCore(GuiGraphics graphics, int cx, int cy, int radius, Stage stage) { + boolean graceful = stage == Stage.DEATH_GRACEFUL; + + float pulse; + if (graceful) { + pulse = 0.3f + 0.2f * Mth.sin(pulsePhase * 0.3f); + } else { + pulse = 0.5f + 0.3f * Mth.sin(pulsePhase * 3f) + 0.2f * Mth.sin(pulsePhase * 7f + 1.3f) + + 0.1f * Mth.sin(pulsePhase * 11f + 2.7f); + pulse = Mth.clamp(pulse, 0.2f, 1.2f); + } + + int coreRadius = (int) (radius * 0.5f * pulse); + + int[] colors = graceful ? new int[] { 0x301010, 0x502020, 0x703030, 0x904040 } : + new int[] { 0x660000, 0xAA0000, 0xDD2200, 0xFF4400 }; + + for (int layer = 0; layer < colors.length; layer++) { + int layerRadius = coreRadius - layer * 3; + if (layerRadius > 0) { + int alpha = graceful ? (80 + layer * 20) : (150 + layer * 25); + alpha = Math.min(255, alpha); + drawCircle(graphics, cx, cy, layerRadius, (alpha << 24) | colors[layer]); + } + } + + if (!graceful && Math.random() < 0.1) { + int flickerRadius = coreRadius + (int) (Math.random() * 10); + drawCircle(graphics, cx, cy, flickerRadius, 0x40FF0000); + } + } + + private void drawSolarFlares(GuiGraphics graphics, int cx, int cy, float baseRadius, int color) { + int flareCount = 5; + for (int i = 0; i < flareCount; i++) { + float angle = animPhase * 0.5f + i * Mth.TWO_PI / flareCount; + float flareLength = 8 + 5 * Mth.sin(animPhase * 2 + i * 1.3f); + float dist = baseRadius + flareLength; + + int px = cx + (int) (Mth.cos(angle) * dist); + int py = cy + (int) (Mth.sin(angle) * dist); + + int alpha = (int) (80 + 40 * Mth.sin(animPhase * 3 + i)); + drawCircle(graphics, px, py, 3, (alpha << 24) | color); + } + } + + private void drawCircle(GuiGraphics graphics, int cx, int cy, int radius, int color) { + if (radius <= 0) return; + for (int y = -radius; y <= radius; y++) { + int halfWidth = (int) Math.sqrt(radius * radius - y * y); + graphics.fill(cx - halfWidth, cy + y, cx + halfWidth + 1, cy + y + 1, color); + } + } + + private void drawCircleRing(GuiGraphics graphics, int cx, int cy, int radius, int thickness, int color) { + for (int t = 0; t < thickness; t++) { + int r = radius - t; + if (r <= 0) continue; + for (int angle = 0; angle < 360; angle += 3) { + float rad = angle * Mth.DEG_TO_RAD; + int px = cx + (int) (Mth.cos(rad) * r); + int py = cy + (int) (Mth.sin(rad) * r); + graphics.fill(px, py, px + 1, py + 1, color); + } + } + } + + private void drawStageLabel(GuiGraphics graphics, int cx, Stage stage) { + String label = switch (stage) { + case EMPTY -> "DORMANT"; + case GROWING -> "IGNITING"; + case STAR -> "MAIN SEQUENCE"; + case SUPERSTAR -> "RED GIANT"; + case BLACK_HOLE -> "SINGULARITY"; + case DEATH -> "UNSTABLE"; + case DEATH_GRACEFUL -> "FADING"; + }; + + var font = Minecraft.getInstance().font; + int labelWidth = font.width(label); + int labelX = cx - labelWidth / 2; + int labelY = getPosition().y + getSize().height - 12; + + int textColor = getStageTextColor(stage); + graphics.drawString(font, label, labelX, labelY, textColor, false); + } + + private int getStageTextColor(Stage stage) { + int customColor = getCustomColor(); + if (customColor != -1 && (stage == Stage.STAR || stage == Stage.SUPERSTAR)) { + return 0xFF000000 | customColor; + } + + return switch (stage) { + case EMPTY -> 0xFF606080; + case GROWING -> 0xFF8090FF; + case STAR -> 0xFFFFCC44; + case SUPERSTAR -> 0xFFFF8844; + case BLACK_HOLE -> 0xFFAA66FF; + case DEATH -> 0xFFFF4444; + case DEATH_GRACEFUL -> 0xFF884444; + }; + } + + private int[] generateColorGradient(int baseColor) { + int[] gradient = new int[5]; + float[] hsb = rgbToHsb(baseColor); + + for (int i = 0; i < 5; i++) { + float brightness = 0.3f + (i * 0.175f); + float saturation = Math.max(0.2f, hsb[1] - (i * 0.1f)); + gradient[i] = hsbToRgb(hsb[0], saturation, Math.min(1f, brightness)); + } + + return gradient; + } + + private int blendTowardsWhite(int color, float factor) { + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + + r = (int) (r + (255 - r) * factor); + g = (int) (g + (255 - g) * factor); + b = (int) (b + (255 - b) * factor); + + return (r << 16) | (g << 8) | b; + } + + private int darken(int color, float factor) { + int r = (int) (((color >> 16) & 0xFF) * factor); + int g = (int) (((color >> 8) & 0xFF) * factor); + int b = (int) ((color & 0xFF) * factor); + + return (r << 16) | (g << 8) | b; + } + + private int shiftHue(int color, float shift) { + float[] hsb = rgbToHsb(color); + hsb[0] = (hsb[0] + shift) % 1f; + if (hsb[0] < 0) hsb[0] += 1f; + return hsbToRgb(hsb[0], hsb[1], hsb[2]); + } + + private static float[] rgbToHsb(int rgb) { + int r = (rgb >> 16) & 0xFF; + int g = (rgb >> 8) & 0xFF; + int b = rgb & 0xFF; + + float[] hsb = new float[3]; + java.awt.Color.RGBtoHSB(r, g, b, hsb); + return hsb; + } + + private static int hsbToRgb(float h, float s, float b) { + return java.awt.Color.HSBtoRGB(h, s, b) & 0xFFFFFF; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarFancyUIWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarFancyUIWidget.java new file mode 100644 index 000000000..19a8ef187 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarFancyUIWidget.java @@ -0,0 +1,383 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.gregtechceu.gtceu.api.gui.fancy.FancyMachineUIWidget; +import com.gregtechceu.gtceu.api.gui.fancy.IFancyUIProvider; + +import com.lowdragmc.lowdraglib.gui.texture.ColorBorderTexture; +import com.lowdragmc.lowdraglib.gui.texture.ColorRectTexture; +import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; +import com.lowdragmc.lowdraglib.gui.texture.IGuiTexture; +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.SlotWidget; +import com.lowdragmc.lowdraglib.gui.widget.Widget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class StellarFancyUIWidget extends FancyMachineUIWidget { + + private final Supplier stageSupplier; + private Stage lastStage = Stage.EMPTY; + + private static final int BG_COLOR = 0xE00a0a14; + private static final int BORDER_COLOR = 0xFF404060; + private static final int SLOT_BG_COLOR = 0xC0101018; + private static final int SLOT_BORDER_COLOR = 0xFF505070; + + public StellarFancyUIWidget(IFancyUIProvider mainPage, int width, int height, Supplier stageSupplier) { + super(mainPage, width, height); + this.stageSupplier = stageSupplier; + setBackground((IGuiTexture) null); + applyDarkTheme(); + } + + private void applyDarkTheme() { + IGuiTexture titleBarBg = new GuiTextureGroup( + new ColorRectTexture(BG_COLOR), + new ColorBorderTexture(1, BORDER_COLOR)); + + if (titleBar != null) { + titleBar.setBackground((IGuiTexture) null); + for (Widget widget : titleBar.widgets) { + if (widget instanceof WidgetGroup group) { + group.setBackground(titleBarBg); + } + } + } + + if (sideTabsWidget != null) { + sideTabsWidget.setBackground((IGuiTexture) null); + updateTabStyling(Stage.EMPTY); + } + + if (configuratorPanel != null) { + configuratorPanel.setVisible(false); + configuratorPanel.setActive(false); + } + + if (playerInventory != null) { + playerInventory.setBackground((IGuiTexture) null); + IGuiTexture darkSlot = new GuiTextureGroup( + new ColorRectTexture(SLOT_BG_COLOR), + new ColorBorderTexture(1, SLOT_BORDER_COLOR)); + for (Widget widget : playerInventory.widgets) { + if (widget instanceof SlotWidget slotWidget) { + slotWidget.setBackground(darkSlot); + } + } + } + } + + private void updateTabStyling(Stage stage) { + if (sideTabsWidget == null) return; + + int accentColor = getStageAccentColorFull(stage); + int accentColorDim = dimColor(accentColor, 0.6f); + + IGuiTexture tabNormal = new GuiTextureGroup( + new ColorRectTexture(0xA0080812), + new ColorBorderTexture(1, accentColorDim)); + IGuiTexture tabHover = new GuiTextureGroup( + new ColorRectTexture(0xC0151525), + new ColorBorderTexture(1, accentColor)); + IGuiTexture tabPressed = new GuiTextureGroup( + new ColorRectTexture(0xE0101020), + new ColorBorderTexture(1, accentColor)); + + sideTabsWidget.setTabTexture(tabNormal); + sideTabsWidget.setTabHoverTexture(tabHover); + sideTabsWidget.setTabPressedTexture(tabPressed); + } + + private int dimColor(int color, float factor) { + int a = (color >> 24) & 0xFF; + int r = (int) (((color >> 16) & 0xFF) * factor); + int g = (int) (((color >> 8) & 0xFF) * factor); + int b = (int) ((color & 0xFF) * factor); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + @Override + public void initWidget() { + super.initWidget(); + if (playerInventory != null) { + IGuiTexture darkSlot = new GuiTextureGroup( + new ColorRectTexture(SLOT_BG_COLOR), + new ColorBorderTexture(1, SLOT_BORDER_COLOR)); + for (Widget widget : playerInventory.widgets) { + if (widget instanceof SlotWidget slotWidget) { + slotWidget.setBackground(darkSlot); + } + } + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + Stage currentStage = stageSupplier.get(); + if (currentStage != lastStage) { + lastStage = currentStage; + updateTabStyling(currentStage); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + drawFullBackground(graphics); + drawCustomOverlays(graphics); + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + drawTitleText(graphics); + } + + private void drawFullBackground(GuiGraphics graphics) { + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + Stage stage = stageSupplier.get(); + + DrawerHelper.drawGradientRect(graphics, x, y, w, h, 0xFF0c0c12, 0xFF060608, false); + drawGridPattern(graphics, x, y, w, h); + drawCornerAccents(graphics, x, y, w, h, getStageAccentColorFull(stage) & 0x66FFFFFF); + DrawerHelper.drawBorder(graphics, x, y, w, h, getStageAccentColorFull(stage) & 0x33FFFFFF, 1); + + if (playerInventory != null && playerInventory.isVisible()) { + drawSidePanels(graphics, x, y, w, h, stage); + } + } + + private void drawGridPattern(GuiGraphics graphics, int x, int y, int w, int h) { + int gridColor = 0x08FFFFFF; + int spacing = 16; + for (int gx = x + spacing; gx < x + w; gx += spacing) { + graphics.fill(gx, y, gx + 1, y + h, gridColor); + } + for (int gy = y + spacing; gy < y + h; gy += spacing) { + graphics.fill(x, gy, x + w, gy + 1, gridColor); + } + } + + private void drawCornerAccents(GuiGraphics graphics, int x, int y, int w, int h, int color) { + int len = 20; + int thickness = 2; + graphics.fill(x, y, x + len, y + thickness, color); + graphics.fill(x, y, x + thickness, y + len, color); + graphics.fill(x + w - len, y, x + w, y + thickness, color); + graphics.fill(x + w - thickness, y, x + w, y + len, color); + graphics.fill(x, y + h - thickness, x + len, y + h, color); + graphics.fill(x, y + h - len, x + thickness, y + h, color); + graphics.fill(x + w - len, y + h - thickness, x + w, y + h, color); + graphics.fill(x + w - thickness, y + h - len, x + w, y + h, color); + } + + private void drawSidePanels(GuiGraphics graphics, int x, int y, int w, int h, Stage stage) { + int invY = playerInventory.getPosition().y; + int invX = playerInventory.getPosition().x; + int invW = playerInventory.getSize().width; + int panelH = h - (invY - y) - 5; + + int leftPanelX = x + 3; + int leftPanelW = invX - leftPanelX - 3; + if (leftPanelW > 20) { + drawTechPanel(graphics, leftPanelX, invY, leftPanelW, panelH, stage); + } + + int rightPanelX = invX + invW + 3; + int rightPanelW = (x + w) - rightPanelX - 3; + if (rightPanelW > 20) { + drawStatsPanel(graphics, rightPanelX, invY, rightPanelW, panelH, stage); + } + } + + private void drawTechPanel(GuiGraphics graphics, int px, int py, int pw, int ph, Stage stage) { + DrawerHelper.drawSolidRect(graphics, px, py, pw, ph, 0x40000000); + int borderColor = getStageAccentColorFull(stage) & 0x33FFFFFF; + DrawerHelper.drawBorder(graphics, px, py, pw, ph, borderColor, 1); + int accentColor = getStageAccentColorFull(stage) & 0x80FFFFFF; + graphics.fill(px + 1, py + 1, px + pw - 1, py + 3, accentColor); + + int lineColor = 0x20FFFFFF; + int lineY = py + 15; + for (int i = 0; i < 5 && lineY + 10 < py + ph; i++) { + int lineW = (int) ((pw - 10) * (0.3f + 0.4f * ((System.currentTimeMillis() / 100 + i * 50) % 100) / 100f)); + graphics.fill(px + 5, lineY, px + 5 + lineW, lineY + 2, lineColor); + lineY += 12; + } + } + + private void drawStatsPanel(GuiGraphics graphics, int px, int py, int pw, int ph, Stage stage) { + DrawerHelper.drawSolidRect(graphics, px, py, pw, ph, 0x40000000); + int borderColor = getStageAccentColorFull(stage) & 0x33FFFFFF; + DrawerHelper.drawBorder(graphics, px, py, pw, ph, borderColor, 1); + int accentColor = getStageAccentColorFull(stage) & 0x80FFFFFF; + graphics.fill(px + 1, py + 1, px + pw - 1, py + 3, accentColor); + + var font = Minecraft.getInstance().font; + int labelColor = 0xFF606080; + int valueColor = 0xFFCCCCCC; + + graphics.drawString(font, "STAR STATS", px + 4, py + 6, accentColor | 0xFF000000, false); + + float temp = getStageTemp(stage); + float mass = getStageMass(stage); + float output = getStageOutput(stage); + + graphics.drawString(font, "TEMP:", px + 4, py + 20, labelColor, false); + graphics.drawString(font, formatTemp(temp), px + 35, py + 20, getTemperatureColor(temp), false); + graphics.drawString(font, "MASS:", px + 4, py + 32, labelColor, false); + graphics.drawString(font, String.format("%.1f M\u2609", mass), px + 35, py + 32, valueColor, false); + graphics.drawString(font, "OUT:", px + 4, py + 44, labelColor, false); + graphics.drawString(font, formatEnergy(output), px + 30, py + 44, valueColor, false); + graphics.drawString(font, getStatusString(stage), px + 4, py + 56, getStatusColor(stage), false); + } + + private float getStageTemp(Stage stage) { + return switch (stage) { + case EMPTY -> 2.7f; + case GROWING -> 5_000_000f; + case STAR -> 15_000_000f; + case SUPERSTAR -> 100_000_000f; + case BLACK_HOLE -> Float.POSITIVE_INFINITY; + case DEATH -> 500_000_000f; + case DEATH_GRACEFUL -> 1_000_000f; + }; + } + + private float getStageMass(Stage stage) { + return switch (stage) { + case EMPTY -> 0f; + case GROWING -> 0.3f; + case STAR -> 1f; + case SUPERSTAR -> 8f; + case BLACK_HOLE -> 25f; + case DEATH -> 12f; + case DEATH_GRACEFUL -> 0.1f; + }; + } + + private float getStageOutput(Stage stage) { + return switch (stage) { + case EMPTY -> 0f; + case GROWING -> 1_000f; + case STAR -> 50_000f; + case SUPERSTAR -> 500_000f; + case BLACK_HOLE -> 10_000_000f; + case DEATH -> 100_000_000f; + case DEATH_GRACEFUL -> 500f; + }; + } + + private String formatTemp(float temp) { + if (Float.isInfinite(temp)) return "\u221E K"; + if (temp >= 1_000_000) return String.format("%.0fM K", temp / 1_000_000); + if (temp >= 1000) return String.format("%.0fk K", temp / 1000); + return String.format("%.1f K", temp); + } + + private String formatEnergy(float energy) { + if (energy >= 1_000_000) return String.format("%.1f PW", energy / 1_000_000); + if (energy >= 1000) return String.format("%.0f TW", energy / 1000); + return String.format("%.0f GW", energy); + } + + private int getTemperatureColor(float temp) { + if (temp >= 100_000_000) return 0xFFFF4444; + if (temp >= 10_000_000) return 0xFFFFAA44; + if (temp >= 1_000_000) return 0xFFFFFF44; + return 0xFFCCCCCC; + } + + private String getStatusString(Stage stage) { + return switch (stage) { + case EMPTY -> "DORMANT"; + case GROWING -> "IGNITING"; + case STAR -> "STABLE"; + case SUPERSTAR -> "CRITICAL"; + case BLACK_HOLE -> "CONTAINED"; + case DEATH -> "FAILURE"; + case DEATH_GRACEFUL -> "SHUTDOWN"; + }; + } + + private int getStatusColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0xFF606060; + case GROWING -> 0xFF66AAFF; + case STAR -> 0xFF66FF66; + case SUPERSTAR -> 0xFFFFAA44; + case BLACK_HOLE -> 0xFFAA66FF; + case DEATH -> 0xFFFF4444; + case DEATH_GRACEFUL -> 0xFF886666; + }; + } + + private void drawTitleText(GuiGraphics graphics) { + if (titleBar == null || mainPage == null) return; + + var font = Minecraft.getInstance().font; + String title = mainPage.getTitle().getString(); + + int titleBarX = getPosition().x + 8; + int titleBarY = getPosition().y - 16; + int textAreaX = titleBarX + 18 + 16; + int textAreaY = titleBarY + 3; + int textAreaWidth = getSize().width - 16 - 18 - 18 - 16; + int textAreaHeight = 13; + + graphics.fill(textAreaX, textAreaY, textAreaX + textAreaWidth, textAreaY + textAreaHeight, BG_COLOR); + + int textWidth = font.width(title); + int centeredX = textAreaX + (textAreaWidth - textWidth) / 2; + int centeredY = textAreaY + (textAreaHeight - font.lineHeight) / 2; + graphics.drawString(font, title, centeredX, centeredY, 0xFFFFFFFF, true); + } + + private void drawCustomOverlays(GuiGraphics graphics) { + Stage stage = stageSupplier.get(); + int accentColor = getStageAccentColor(stage); + + if (playerInventory != null && playerInventory.isVisible()) { + int x = getPosition().x; + int w = getSize().width; + int invY = playerInventory.getPosition().y; + graphics.fill(x + 10, invY - 2, x + w - 10, invY - 1, accentColor); + } + } + + private int getStageAccentColor(Stage stage) { + int alpha = 0x60; + return (alpha << 24) | switch (stage) { + case EMPTY -> 0x404060; + case GROWING -> 0x6080FF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF8844; + case BLACK_HOLE -> 0x8040FF; + case DEATH -> 0xFF2020; + case DEATH_GRACEFUL -> 0x804040; + }; + } + + private int getStageAccentColorFull(Stage stage) { + return 0xFF000000 | switch (stage) { + case EMPTY -> 0x404060; + case GROWING -> 0x6080FF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF8844; + case BLACK_HOLE -> 0x8040FF; + case DEATH -> 0xFF2020; + case DEATH_GRACEFUL -> 0x804040; + }; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarIrisWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarIrisWidget.java new file mode 100644 index 000000000..b3c934166 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarIrisWidget.java @@ -0,0 +1,701 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.feature.IStellarIrisProvider; +import com.ghostipedia.cosmiccore.api.machine.feature.IStellarModuleReceiver; +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine; +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; +import com.ghostipedia.cosmiccore.api.machine.multiblock.StellarBaseModule; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class StellarIrisWidget extends WidgetGroup { + + public static final int WIDTH = 310; + public static final int HEIGHT = 160; + + private final Supplier machineSupplier; + + private Stage lastSyncedStage = Stage.EMPTY; + private boolean hasReceivedInitialSync = false; + private float fuelLevel = 0f; + private boolean canIgnite = false; + private int lastSyncedStarColor = -1; + + private StellarCoreWidget coreWidget; + private OrbitalRingsWidget orbitalRings; + private ModuleSelectorWidget moduleSelectorWidget; + private ModuleToggleButton moduleToggleButton; + private ModuleConfigPopout moduleConfigPopout; + private StageContextPanel contextPanel; + private StarColorButton starColorButton; + private StarColorPickerPopup starColorPicker; + private boolean showingColorPicker = false; + + private PrestigeAnimationOverlay prestigeAnimationOverlay; + private PrestigeWindow prestigeWindow; + private boolean prestigeAnimationTriggered = false; + private Stage stageBeforePrestige = Stage.EMPTY; + + private int tickCounter = 0; + private boolean debugPrimed = false; + private boolean showingModuleSelector = false; + + private int selectedModuleIndex = -1; + private String lastSyncedModuleName = ""; + private boolean lastSyncedModuleConnected = false; + private boolean lastSyncedModuleWorking = false; + private long lastSyncedModuleEnergy = 0; + private double lastSyncedModuleSpeed = 0; + private Stage lastSyncedModuleStage = Stage.EMPTY; + private int lastSyncedModuleParallel = 0; + private long lastSyncedModuleVoltage = 0; + private int lastSyncedIrisParallelLimit = 0; + private long lastSyncedMaxEUt = 0; + private int lastSyncedEffectiveParallel = 0; + private int lastSyncedOverclockTier = 0; + + public StellarIrisWidget(Supplier machineSupplier) { + super(0, 0, WIDTH, HEIGHT); + this.machineSupplier = machineSupplier; + initWidgets(); + } + + @Override + public void writeInitialData(FriendlyByteBuf buffer) { + super.writeInitialData(buffer); + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine != null) { + Stage serverStage = machine.getStage(); + // Debug: log what stage the SERVER is sending + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.warn( + "[StellarIrisWidget] SERVER writeInitialData: stage={}, color={}", + serverStage, machine.getCustomStarColor()); + buffer.writeEnum(serverStage); + buffer.writeInt(machine.getCustomStarColor()); + } else { + buffer.writeEnum(Stage.EMPTY); + buffer.writeInt(-1); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readInitialData(FriendlyByteBuf buffer) { + super.readInitialData(buffer); + lastSyncedStage = buffer.readEnum(Stage.class); + lastSyncedStarColor = buffer.readInt(); + hasReceivedInitialSync = true; + // Debug: log what stage the CLIENT received + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.warn( + "[StellarIrisWidget] CLIENT readInitialData: stage={}, color={}", + lastSyncedStage, lastSyncedStarColor); + } + + private void initWidgets() { + // Debug: verify the stage supplier returns EMPTY before sync + // Version marker: V3 - 2026-01-13 + Stage initialStage = getCurrentStage(); + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.warn( + "[StellarIrisWidget V3] initWidgets: getCurrentStage()={}, hasReceivedInitialSync={}", + initialStage, hasReceivedInitialSync); + + addWidget(new StarfieldBackgroundWidget(0, 0, WIDTH, HEIGHT, this::getCurrentStage)); + + int margin = 5; + int gap = 5; + + int coreSize = 130; + int panelWidth = 135; + + int panelX = WIDTH - margin - panelWidth; + int panelY = margin; + int panelH = HEIGHT - (margin * 2); + + int coreX = panelX - gap - coreSize; + int coreY = (HEIGHT - coreSize) / 2; + + orbitalRings = new OrbitalRingsWidget(coreX - 5, coreY - 5, coreSize + 10, coreSize + 10, + this::getCurrentStage); + addWidget(orbitalRings); + + coreWidget = new StellarCoreWidget(coreX, coreY, coreSize, this::getCurrentStage, this::getCurrentStarColor); + addWidget(coreWidget); + + int moduleSelectorSize = HEIGHT - margin * 2; + int moduleSelectorX = (WIDTH - moduleSelectorSize) / 2; + int moduleSelectorY = margin; + moduleSelectorWidget = new ModuleSelectorWidget(moduleSelectorX, moduleSelectorY, moduleSelectorSize, + machineSupplier, this::onModuleSelected); + moduleSelectorWidget.setVisible(false); + moduleSelectorWidget.setActive(false); + addWidget(moduleSelectorWidget); + + contextPanel = new StageContextPanel(panelX, panelY, panelWidth, panelH, machineSupplier, this); + addWidget(contextPanel); + + int popoutX = moduleSelectorX + moduleSelectorSize + 10; + int popoutY = moduleSelectorY + 20; + moduleConfigPopout = new ModuleConfigPopout(popoutX, popoutY, machineSupplier, this::onModulePopoutClosed); + moduleConfigPopout.setOnPowerSettingsChanged(this::onPowerSettingsChanged); + addWidget(moduleConfigPopout); + + initDebugButtons(); + initModuleToggle(); + initStarColorButton(); + initPrestigeWidgets(); + } + + private void initPrestigeWidgets() { + int windowW = 200; + int windowH = 160; + int windowX = (WIDTH - windowW) / 2; + int windowY = (HEIGHT - windowH) / 2; + + prestigeWindow = new PrestigeWindow(windowX, windowY, windowW, windowH, + machineSupplier, this::onPrestigeWindowClosed); + addWidget(prestigeWindow); + + prestigeAnimationOverlay = new PrestigeAnimationOverlay(0, 0, WIDTH, HEIGHT, + machineSupplier, this::onPrestigeAnimationComplete, this::onShowPrestigeWindow); + prestigeAnimationOverlay.setCoreWidget(coreWidget); + addWidget(prestigeAnimationOverlay); + } + + private void initDebugButtons() { + int btnWidth = 50; + int btnHeight = 14; + int btnX = 5; + int btnY = HEIGHT - btnHeight - 5 - 20; + + addWidget(new DebugPrimeButton(btnX, btnY, btnWidth, btnHeight, this::requestDebugPrime)); + } + + private void initModuleToggle() { + int btnSize = 18; + int btnX = 5; + int btnY = HEIGHT - btnSize - 5; + + moduleToggleButton = new ModuleToggleButton(btnX, btnY, btnSize, btnSize, this::onModuleToggle, + this::getCurrentStage); + addWidget(moduleToggleButton); + } + + private void initStarColorButton() { + int btnSize = 18; + int btnX = 5 + 18 + 4; + int btnY = HEIGHT - btnSize - 5; + + starColorButton = new StarColorButton(btnX, btnY, btnSize, btnSize, this::onColorButtonClicked, + this::getCurrentStarColor); + addWidget(starColorButton); + + int pickerX = btnX; + int pickerY = btnY - StarColorPickerPopup.HEIGHT - 5; + starColorPicker = new StarColorPickerPopup(pickerX, pickerY, this::hideColorPicker, this::onStarColorChanged); + addWidget(starColorPicker); + } + + private void onColorButtonClicked(boolean ignored) { + if (showingColorPicker) { + hideColorPicker(); + } else { + showColorPicker(); + } + } + + private void showColorPicker() { + showingColorPicker = true; + starColorPicker.show(lastSyncedStarColor); + } + + private void hideColorPicker() { + showingColorPicker = false; + starColorPicker.hide(); + } + + private void onStarColorChanged(int newColor) { + writeClientAction(6, buf -> buf.writeInt(newColor)); + } + + public int getCurrentStarColor() { + return lastSyncedStarColor; + } + + private void onModuleToggle(boolean showModules) { + showingModuleSelector = showModules; + + coreWidget.setVisible(!showModules); + coreWidget.setActive(!showModules); + orbitalRings.setVisible(!showModules); + orbitalRings.setActive(!showModules); + contextPanel.setVisible(!showModules); + contextPanel.setActive(!showModules); + + moduleSelectorWidget.setVisible(showModules); + moduleSelectorWidget.setActive(showModules); + + if (!showModules) { + moduleConfigPopout.hide(); + moduleSelectorWidget.clearSelection(); + writeClientAction(4, buf -> buf.writeInt(-1)); + } + } + + private void onModuleSelected(int moduleIndex) { + this.selectedModuleIndex = moduleIndex; + moduleConfigPopout.showForModule(moduleIndex); + writeClientAction(4, buf -> buf.writeInt(moduleIndex)); + } + + private void onModulePopoutClosed() { + moduleSelectorWidget.clearSelection(); + } + + private void onPowerSettingsChanged(int maxParallel, long voltagePerParallel) { + if (selectedModuleIndex < 0) return; + + writeClientAction(5, buf -> { + buf.writeInt(selectedModuleIndex); + buf.writeInt(maxParallel); + buf.writeLong(voltagePerParallel); + }); + + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIrisWidget] CLIENT sending power settings: module={}, parallel={}, voltage={}", + selectedModuleIndex, maxParallel, voltagePerParallel); + } + + public void requestDebugPrime() { + writeClientAction(3, buf -> {}); + } + + public Stage getCurrentStage() { + // Before we receive initial sync, return EMPTY as safe default + // The client-side machine may have corrupted stage data before sync completes + if (!hasReceivedInitialSync) { + com.ghostipedia.cosmiccore.CosmicCore.LOGGER + .warn("[StellarIrisWidget] getCurrentStage: No initial sync yet, returning EMPTY"); + return Stage.EMPTY; + } + // Safety check: if lastSyncedStage is somehow null, return EMPTY + if (lastSyncedStage == null) { + com.ghostipedia.cosmiccore.CosmicCore.LOGGER + .warn("[StellarIrisWidget] getCurrentStage: lastSyncedStage is null, returning EMPTY"); + return Stage.EMPTY; + } + // Debug: log non-empty/non-star stages + if (lastSyncedStage != Stage.EMPTY && lastSyncedStage != Stage.STAR) { + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.warn("[StellarIrisWidget] getCurrentStage: returning {}", + lastSyncedStage); + } + return lastSyncedStage; + } + + public float getFuelLevel() { + return fuelLevel; + } + + public boolean canIgnite() { + return canIgnite; + } + + @Override + public void detectAndSendChanges() { + super.detectAndSendChanges(); + + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine == null) return; + + Stage currentStage = machine.getStage(); + if (currentStage != lastSyncedStage) { + lastSyncedStage = currentStage; + writeUpdateInfo(301, buf -> buf.writeEnum(currentStage)); + } + + float newFuelLevel = calculateFuelLevel(machine); + if (Math.abs(newFuelLevel - fuelLevel) > 0.01f) { + fuelLevel = newFuelLevel; + writeUpdateInfo(302, buf -> buf.writeFloat(fuelLevel)); + } + + boolean newCanIgnite = checkIgnitionRequirements(machine); + if (newCanIgnite != canIgnite) { + canIgnite = newCanIgnite; + writeUpdateInfo(303, buf -> buf.writeBoolean(canIgnite)); + } + + int newStarColor = machine.getCustomStarColor(); + if (newStarColor != lastSyncedStarColor) { + lastSyncedStarColor = newStarColor; + writeUpdateInfo(306, buf -> buf.writeInt(newStarColor)); + } + + syncSelectedModuleData(machine); + } + + private void syncSelectedModuleData(IrisMultiblockMachine machine) { + if (selectedModuleIndex < 0) return; + + List modules = new ArrayList<>(machine.getConnectedModules()); + if (selectedModuleIndex >= modules.size()) { + selectedModuleIndex = -1; + writeUpdateInfo(305, buf -> {}); + return; + } + + IStellarModuleReceiver receiver = modules.get(selectedModuleIndex); + + String newName = ""; + boolean newConnected = false; + boolean newWorking = false; + long newEnergy = 0; + double newSpeed = 0; + Stage newStage = Stage.EMPTY; + int newParallel = 1; + long newVoltage = 32; + int newIrisLimit = 1; + long newMaxEUt = 0; + int newEffectiveParallel = 1; + int newOverclockTier = 0; + + if (receiver instanceof StellarBaseModule module) { + newName = module.getBlockState().getBlock().getDescriptionId(); + IStellarIrisProvider iris = module.getStellarIris(); + if (iris == null) { + iris = machine; // Use Iris as fallback + } + + newConnected = iris != null && iris.isFormed(); + newWorking = module.getRecipeLogic().isWorking(); + newEnergy = module.getEnergyConsumedPerTick(); + + newParallel = module.getConfiguredMaxParallel(); + newVoltage = module.getConfiguredVoltagePerParallel(); + newIrisLimit = module.getIrisParallelLimit(); + + newMaxEUt = module.getMaxEUt(); + newEffectiveParallel = module.getEffectiveParallelLimit(); + newOverclockTier = module.getOverclockTier(); + + if (iris != null && iris.canProcess()) { + newSpeed = iris.getSpeedBonus(); + newStage = iris.getStage(); + } + } + + boolean changed = !newName.equals(lastSyncedModuleName) || newConnected != lastSyncedModuleConnected || + newWorking != lastSyncedModuleWorking || newEnergy != lastSyncedModuleEnergy || + newSpeed != lastSyncedModuleSpeed || newStage != lastSyncedModuleStage || + newParallel != lastSyncedModuleParallel || newVoltage != lastSyncedModuleVoltage || + newIrisLimit != lastSyncedIrisParallelLimit || newMaxEUt != lastSyncedMaxEUt || + newEffectiveParallel != lastSyncedEffectiveParallel || newOverclockTier != lastSyncedOverclockTier; + + if (changed) { + lastSyncedModuleName = newName; + lastSyncedModuleConnected = newConnected; + lastSyncedModuleWorking = newWorking; + lastSyncedModuleEnergy = newEnergy; + lastSyncedModuleSpeed = newSpeed; + lastSyncedModuleStage = newStage; + lastSyncedModuleParallel = newParallel; + lastSyncedModuleVoltage = newVoltage; + lastSyncedIrisParallelLimit = newIrisLimit; + lastSyncedMaxEUt = newMaxEUt; + lastSyncedEffectiveParallel = newEffectiveParallel; + lastSyncedOverclockTier = newOverclockTier; + + final String syncName = newName; + final boolean syncConnected = newConnected; + final boolean syncWorking = newWorking; + final long syncEnergy = newEnergy; + final double syncSpeed = newSpeed; + final Stage syncStage = newStage; + final int syncParallel = newParallel; + final long syncVoltage = newVoltage; + final int syncIrisLimit = newIrisLimit; + final long syncMaxEUt = newMaxEUt; + final int syncEffectiveParallel = newEffectiveParallel; + final int syncOverclockTier = newOverclockTier; + + writeUpdateInfo(304, buf -> { + buf.writeUtf(syncName); + buf.writeBoolean(syncConnected); + buf.writeBoolean(syncWorking); + buf.writeLong(syncEnergy); + buf.writeDouble(syncSpeed); + buf.writeEnum(syncStage); + buf.writeInt(syncParallel); + buf.writeLong(syncVoltage); + buf.writeInt(syncIrisLimit); + buf.writeLong(syncMaxEUt); + buf.writeInt(syncEffectiveParallel); + buf.writeInt(syncOverclockTier); + }); + + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIrisWidget] Syncing module data: name={}, connected={}, parallel={}, voltage={}, maxEUt={}, tier={}", + syncName, syncConnected, syncParallel, syncVoltage, syncMaxEUt, syncOverclockTier); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readUpdateInfo(int id, FriendlyByteBuf buffer) { + // Debug: log ALL update info calls to trace unexpected stage changes + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.warn( + "[StellarIrisWidget] readUpdateInfo: id={}, buffer remaining={}", + id, buffer.readableBytes()); + + if (id == 301) { + Stage newStage = buffer.readEnum(Stage.class); + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.warn( + "[StellarIrisWidget] readUpdateInfo(301): stage change {} -> {}", + lastSyncedStage, newStage); + lastSyncedStage = newStage; + } else if (id == 302) { + fuelLevel = buffer.readFloat(); + } else if (id == 303) { + canIgnite = buffer.readBoolean(); + } else if (id == 304) { + String name = buffer.readUtf(); + boolean connected = buffer.readBoolean(); + boolean working = buffer.readBoolean(); + long energy = buffer.readLong(); + double speed = buffer.readDouble(); + Stage stage = buffer.readEnum(Stage.class); + int parallel = buffer.readInt(); + long voltage = buffer.readLong(); + int irisLimit = buffer.readInt(); + long maxEUt = buffer.readLong(); + int effectiveParallel = buffer.readInt(); + int overclockTier = buffer.readInt(); + + // Update popout with synced data + moduleConfigPopout.updateModuleData(name, connected, working, energy, speed, stage, + parallel, voltage, irisLimit, maxEUt, effectiveParallel, overclockTier); + + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIrisWidget] CLIENT readUpdateInfo: module name={}, parallel={}, voltage={}, maxEUt={}, tier={}", + name, parallel, voltage, maxEUt, overclockTier); + } else if (id == 305) { + moduleConfigPopout.hide(); + moduleSelectorWidget.clearSelection(); + } else if (id == 306) { + lastSyncedStarColor = buffer.readInt(); + } else { + super.readUpdateInfo(id, buffer); + } + } + + private float calculateFuelLevel(IrisMultiblockMachine machine) { + if (debugPrimed) { + return 1f; + } + + if (machine.getStage() == Stage.EMPTY) { + return machine.getInventory().getStackInSlot(0).isEmpty() ? 0f : 1f; + } + return switch (machine.getStage()) { + case EMPTY -> 0f; + case GROWING -> 0.5f; + case STAR, SUPERSTAR, BLACK_HOLE -> 1f; + case DEATH, DEATH_GRACEFUL -> 0.2f; + }; + } + + private boolean checkIgnitionRequirements(IrisMultiblockMachine machine) { + if (debugPrimed) { + return true; + } + if (machine.getStage() != Stage.EMPTY) return false; + if (machine.getInventory().getStackInSlot(0).isEmpty()) return false; + return fuelLevel >= 0.8f; + } + + public void debugPrime() { + debugPrimed = true; + } + + public void requestIgnition() { + writeClientAction(1, buf -> {}); + } + + public void requestStageAdvance() { + writeClientAction(2, buf -> {}); + } + + public void triggerPrestigeAnimation() { + if (prestigeAnimationTriggered) return; + + prestigeAnimationTriggered = true; + stageBeforePrestige = lastSyncedStage; + + prestigeAnimationOverlay.startAnimation(stageBeforePrestige, lastSyncedStarColor, 50); + writeClientAction(7, buf -> {}); + + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIrisWidget] Prestige animation triggered! Stage before: {}", stageBeforePrestige); + } + + private void onPrestigeAnimationComplete() { + writeClientAction(8, buf -> {}); + + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIrisWidget] Prestige animation complete, requesting completion from server"); + } + + private void onShowPrestigeWindow() { + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine != null) { + int earned = machine.getLastPrestigePointsEarned(); + int total = machine.getPrestigePoints() + earned; + int tier = machine.getPrestigeTier(); + int prevTier = tier; + + prestigeWindow.show(earned, total, tier, prevTier); + } else { + prestigeWindow.show(50, 50, 1, 0); + } + } + + private void onPrestigeWindowClosed() { + prestigeAnimationTriggered = false; + stageBeforePrestige = Stage.EMPTY; + + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIrisWidget] Prestige window closed"); + } + + @Override + public void handleClientAction(int id, FriendlyByteBuf buffer) { + IrisMultiblockMachine machine = machineSupplier.get(); + if (machine == null) return; + + if (id == 1) { + if (canIgnite || debugPrimed) { + machine.setStarStage(); + debugPrimed = false; + } + } else if (id == 2) { + machine.setStarStage(); + } else if (id == 3) { + debugPrimed = true; + } else if (id == 4) { + int newSelectedModule = buffer.readInt(); + this.selectedModuleIndex = newSelectedModule; + this.lastSyncedModuleName = ""; + this.lastSyncedModuleConnected = false; + this.lastSyncedModuleWorking = false; + this.lastSyncedModuleEnergy = -1; + this.lastSyncedModuleSpeed = -1; + this.lastSyncedModuleStage = null; + this.lastSyncedModuleParallel = -1; + this.lastSyncedModuleVoltage = -1; + this.lastSyncedIrisParallelLimit = -1; + this.lastSyncedMaxEUt = -1; + this.lastSyncedEffectiveParallel = -1; + this.lastSyncedOverclockTier = -1; + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIrisWidget] handleClientAction: module selection = {}", newSelectedModule); + } else if (id == 5) { + int moduleIndex = buffer.readInt(); + int newParallel = buffer.readInt(); + long newVoltage = buffer.readLong(); + + List modules = new ArrayList<>(machine.getConnectedModules()); + if (moduleIndex >= 0 && moduleIndex < modules.size()) { + IStellarModuleReceiver receiver = modules.get(moduleIndex); + if (receiver instanceof StellarBaseModule stellarModule) { + stellarModule.setConfiguredMaxParallel(newParallel); + stellarModule.setConfiguredVoltagePerParallel(newVoltage); + + stellarModule.markDirty(); + + this.lastSyncedModuleParallel = -1; + this.lastSyncedModuleVoltage = -1; + + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIrisWidget] SERVER updated module {} power: parallel={}, voltage={}", + moduleIndex, newParallel, newVoltage); + } + } + } else if (id == 6) { + int newColor = buffer.readInt(); + machine.setCustomStarColor(newColor); + machine.markDirty(); + lastSyncedStarColor = newColor - 1; + + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIrisWidget] SERVER updated star color: {}", + newColor == -1 ? "default" : String.format("#%06X", newColor)); + } else if (id == 7) { + machine.triggerPrestige(); + + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIrisWidget] SERVER prestige triggered"); + } else if (id == 8) { + machine.completePrestige(); + + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarIrisWidget] SERVER prestige completed. Points: {}, Tier: {}", + machine.getPrestigePoints(), machine.getPrestigeTier()); + } else { + super.handleClientAction(id, buffer); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + tickCounter++; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + int borderColor = getStageAccentColor(lastSyncedStage, 0.4f); + DrawerHelper.drawBorder(graphics, x, y, w, h, borderColor, 1); + + if (lastSyncedStage != Stage.EMPTY) { + int glowColor = getStageAccentColor(lastSyncedStage, 0.15f); + DrawerHelper.drawGradientRect(graphics, x + 1, y + 1, w - 2, 20, glowColor, 0x00000000, false); + } + + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + } + + private int getStageAccentColor(Stage stage, float alpha) { + int a = (int) (alpha * 255) << 24; + return switch (stage) { + case EMPTY -> a | 0x404050; + case GROWING -> a | 0x6080FF; + case STAR -> a | 0xFFCC44; + case SUPERSTAR -> a | 0xFF8844; + case BLACK_HOLE -> a | 0x8040FF; + case DEATH -> a | 0xFF2020; + case DEATH_GRACEFUL -> a | 0x804040; + }; + } + + public int getTickCounter() { + return tickCounter; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarModuleContentWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarModuleContentWidget.java new file mode 100644 index 000000000..e5bea5fa5 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarModuleContentWidget.java @@ -0,0 +1,480 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.feature.IStellarIrisProvider; +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; +import com.ghostipedia.cosmiccore.api.machine.multiblock.StellarBaseModule; + +import com.gregtechceu.gtceu.api.GTValues; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class StellarModuleContentWidget extends WidgetGroup { + + public static final int WIDTH = 186; + public static final int HEIGHT = 100; + + private static final int GEAR_BUTTON_SIZE = 20; + private static final ResourceLocation GEAR_TEXTURE = new ResourceLocation("gtceu", + "textures/item/material_sets/dull/gear_small.png"); + + private final Supplier moduleSupplier; + + private boolean isConnected = false; + private boolean canProcess = false; + private boolean isWorking = false; + private Stage irisStage = Stage.EMPTY; + + private long maxEUt = 0; + private long currentEUt = 0; + private int effectiveParallel = 1; + private int configuredParallel = 1; + private int overclockTier = 0; + private double speedBonus = 1.0; + private int irisParallelLimit = 1; + private boolean wirelessAvailable = false; + private boolean powerFailure = false; + private long configuredVoltage = 32; + + private PowerControlPopup powerPopup; + private boolean showingPowerPopup = false; + private BiConsumer onPowerSettingsChanged; + + public StellarModuleContentWidget(Supplier moduleSupplier) { + super(0, 0, WIDTH, HEIGHT); + this.moduleSupplier = moduleSupplier; + initPowerPopup(); + } + + private void initPowerPopup() { + powerPopup = new PowerControlPopup(WIDTH + 4, 0, this::hidePowerPopup, this::onPowerSettingsApplied); + addWidget(powerPopup); + } + + public void setOnPowerSettingsChanged(BiConsumer callback) { + this.onPowerSettingsChanged = callback; + } + + private void showPowerPopup() { + showingPowerPopup = true; + powerPopup.show(configuredParallel, configuredVoltage); + } + + private void hidePowerPopup() { + showingPowerPopup = false; + powerPopup.hide(); + } + + private void onPowerSettingsApplied(PowerControlPopup.PowerSettings settings) { + this.configuredParallel = settings.maxParallel(); + this.configuredVoltage = settings.voltagePerParallel(); + + if (onPowerSettingsChanged != null) { + onPowerSettingsChanged.accept(configuredParallel, configuredVoltage); + } + } + + @Override + public void detectAndSendChanges() { + super.detectAndSendChanges(); + + StellarBaseModule module = moduleSupplier.get(); + if (module == null) return; + + IStellarIrisProvider iris = module.getStellarIris(); + boolean newConnected = iris != null && iris.isFormed(); + boolean newCanProcess = newConnected && iris.canProcess(); + boolean newWorking = module.getRecipeLogic() != null && module.getRecipeLogic().isWorking(); + Stage newStage = iris != null ? iris.getStage() : Stage.EMPTY; + + long newMaxEUt = module.getMaxEUt(); + long newCurrentEUt = module.getEnergyConsumedPerTick(); + int newEffectiveParallel = module.getEffectiveParallelLimit(); + int newConfiguredParallel = module.getConfiguredMaxParallel(); + int newOverclockTier = module.getOverclockTier(); + double newSpeedBonus = (iris != null && iris.canProcess()) ? iris.getSpeedBonus() : 1.0; + int newIrisLimit = module.getIrisParallelLimit(); + boolean newWirelessAvailable = module.isWirelessEnergyAvailable(); + boolean newPowerFailure = module.isPowerFailure(); + long newConfiguredVoltage = module.getConfiguredVoltagePerParallel(); + + if (newConnected != isConnected || newCanProcess != canProcess || newWorking != isWorking || + newStage != irisStage || newMaxEUt != maxEUt || newCurrentEUt != currentEUt || + newEffectiveParallel != effectiveParallel || newConfiguredParallel != configuredParallel || + newOverclockTier != overclockTier || newSpeedBonus != speedBonus || + newIrisLimit != irisParallelLimit || newWirelessAvailable != wirelessAvailable || + newPowerFailure != powerFailure || newConfiguredVoltage != configuredVoltage) { + + isConnected = newConnected; + canProcess = newCanProcess; + isWorking = newWorking; + irisStage = newStage; + maxEUt = newMaxEUt; + currentEUt = newCurrentEUt; + effectiveParallel = newEffectiveParallel; + configuredParallel = newConfiguredParallel; + overclockTier = newOverclockTier; + speedBonus = newSpeedBonus; + irisParallelLimit = newIrisLimit; + wirelessAvailable = newWirelessAvailable; + powerFailure = newPowerFailure; + configuredVoltage = newConfiguredVoltage; + + writeUpdateInfo(202, buf -> { + buf.writeBoolean(isConnected); + buf.writeBoolean(canProcess); + buf.writeBoolean(isWorking); + buf.writeEnum(irisStage); + buf.writeLong(maxEUt); + buf.writeLong(currentEUt); + buf.writeInt(effectiveParallel); + buf.writeInt(configuredParallel); + buf.writeInt(overclockTier); + buf.writeDouble(speedBonus); + buf.writeInt(irisParallelLimit); + buf.writeBoolean(wirelessAvailable); + buf.writeBoolean(powerFailure); + buf.writeLong(configuredVoltage); + }); + } + } + + @Override + public void writeInitialData(FriendlyByteBuf buffer) { + super.writeInitialData(buffer); + StellarBaseModule module = moduleSupplier.get(); + if (module != null) { + IStellarIrisProvider iris = module.getStellarIris(); + buffer.writeBoolean(iris != null && iris.isFormed()); + buffer.writeBoolean(iris != null && iris.isFormed() && iris.canProcess()); + buffer.writeBoolean(module.getRecipeLogic() != null && module.getRecipeLogic().isWorking()); + buffer.writeEnum(iris != null ? iris.getStage() : Stage.EMPTY); + buffer.writeLong(module.getMaxEUt()); + buffer.writeLong(module.getEnergyConsumedPerTick()); + buffer.writeInt(module.getEffectiveParallelLimit()); + buffer.writeInt(module.getConfiguredMaxParallel()); + buffer.writeInt(module.getOverclockTier()); + buffer.writeDouble((iris != null && iris.canProcess()) ? iris.getSpeedBonus() : 1.0); + buffer.writeInt(module.getIrisParallelLimit()); + buffer.writeBoolean(module.isWirelessEnergyAvailable()); + buffer.writeBoolean(module.isPowerFailure()); + buffer.writeLong(module.getConfiguredVoltagePerParallel()); + } else { + buffer.writeBoolean(false); + buffer.writeBoolean(false); + buffer.writeBoolean(false); + buffer.writeEnum(Stage.EMPTY); + buffer.writeLong(0); + buffer.writeLong(0); + buffer.writeInt(1); + buffer.writeInt(1); + buffer.writeInt(0); + buffer.writeDouble(1.0); + buffer.writeInt(1); + buffer.writeBoolean(false); + buffer.writeBoolean(false); + buffer.writeLong(32); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readInitialData(FriendlyByteBuf buffer) { + super.readInitialData(buffer); + isConnected = buffer.readBoolean(); + canProcess = buffer.readBoolean(); + isWorking = buffer.readBoolean(); + irisStage = buffer.readEnum(Stage.class); + maxEUt = buffer.readLong(); + currentEUt = buffer.readLong(); + effectiveParallel = buffer.readInt(); + configuredParallel = buffer.readInt(); + overclockTier = buffer.readInt(); + speedBonus = buffer.readDouble(); + irisParallelLimit = buffer.readInt(); + wirelessAvailable = buffer.readBoolean(); + powerFailure = buffer.readBoolean(); + configuredVoltage = buffer.readLong(); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readUpdateInfo(int id, FriendlyByteBuf buffer) { + if (id == 202) { + isConnected = buffer.readBoolean(); + canProcess = buffer.readBoolean(); + isWorking = buffer.readBoolean(); + irisStage = buffer.readEnum(Stage.class); + maxEUt = buffer.readLong(); + currentEUt = buffer.readLong(); + effectiveParallel = buffer.readInt(); + configuredParallel = buffer.readInt(); + overclockTier = buffer.readInt(); + speedBonus = buffer.readDouble(); + irisParallelLimit = buffer.readInt(); + wirelessAvailable = buffer.readBoolean(); + powerFailure = buffer.readBoolean(); + configuredVoltage = buffer.readLong(); + } else { + super.readUpdateInfo(id, buffer); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + DrawerHelper.drawGradientRect(graphics, x, y, w, h, 0xE00c0c14, 0xE0080810, false); + + int gridColor = 0x08FFFFFF; + for (int gx = x + 16; gx < x + w; gx += 16) { + graphics.fill(gx, y, gx + 1, y + h, gridColor); + } + for (int gy = y + 16; gy < y + h; gy += 16) { + graphics.fill(x, gy, x + w, gy + 1, gridColor); + } + + int accentColor = getAccentColor(); + DrawerHelper.drawBorder(graphics, x, y, w, h, accentColor & 0x60FFFFFF, 1); + + int cornerLen = 12; + int cornerColor = accentColor & 0x80FFFFFF; + graphics.fill(x, y, x + cornerLen, y + 2, cornerColor); + graphics.fill(x, y, x + 2, y + cornerLen, cornerColor); + graphics.fill(x + w - cornerLen, y, x + w, y + 2, cornerColor); + graphics.fill(x + w - 2, y, x + w, y + cornerLen, cornerColor); + + drawContent(graphics, x + 6, y + 6, w - 12 - GEAR_BUTTON_SIZE - 4); + drawGearButton(graphics, x + w - GEAR_BUTTON_SIZE - 4, y + h - GEAR_BUTTON_SIZE - 4, mouseX, mouseY); + + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + } + + @OnlyIn(Dist.CLIENT) + private void drawContent(GuiGraphics graphics, int x, int y, int contentWidth) { + var font = Minecraft.getInstance().font; + int labelColor = 0xFF808090; + int valueColor = 0xFFDDDDDD; + int accentColor = 0xFF80C0FF; + + int lineHeight = 11; + int valueX = x + 70; + int currentY = y; + + String statusValue; + int statusColor; + if (powerFailure) { + statusValue = Component.translatable("cosmiccore.stellar.module.status.power_fail").getString(); + statusColor = 0xFFFF4444; + } else if (!wirelessAvailable) { + statusValue = Component.translatable("cosmiccore.stellar.module.status.no_wireless").getString(); + statusColor = 0xFFFF5555; + } else if (isWorking) { + statusValue = Component.translatable("cosmiccore.stellar.module.status.processing").getString(); + statusColor = 0xFF44FF44; + } else if (isConnected && canProcess) { + statusValue = Component.translatable("cosmiccore.stellar.module.status.ready").getString(); + statusColor = 0xFF6090CC; + } else if (isConnected) { + statusValue = Component.translatable("cosmiccore.stellar.module.status.iris_inactive").getString(); + statusColor = 0xFFCC8844; + } else { + statusValue = Component.translatable("cosmiccore.stellar.module.status.disconnected").getString(); + statusColor = 0xFFFF5555; + } + + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.status").getString(), x, currentY, + labelColor, false); + graphics.drawString(font, statusValue, valueX, currentY, statusColor, false); + currentY += lineHeight; + + int sepColor = 0x304080FF; + graphics.fill(x, currentY, x + contentWidth, currentY + 1, sepColor); + currentY += 4; + + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.max_eut").getString(), x, currentY, + labelColor, false); + String maxEUtStr = formatEnergy(maxEUt); + graphics.drawString(font, maxEUtStr, valueX, currentY, valueColor, false); + + String tierName = overclockTier < GTValues.VNF.length ? GTValues.VNF[overclockTier] : "MAX"; + int tierColor = getTierColor(overclockTier); + int badgeX = x + contentWidth - font.width(tierName) - 4; + graphics.fill(badgeX - 2, currentY - 1, badgeX + font.width(tierName) + 2, currentY + font.lineHeight, + 0x90000000 | (tierColor & 0x00333333)); + graphics.drawString(font, tierName, badgeX, currentY, 0xFF000000 | tierColor, false); + currentY += lineHeight; + + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.parallel").getString(), x, currentY, + labelColor, false); + String parallelStr = effectiveParallel + "x"; + if (effectiveParallel < configuredParallel) { + parallelStr = Component + .translatable("cosmiccore.stellar.module.parallel_max", effectiveParallel, configuredParallel) + .getString(); + } + graphics.drawString(font, parallelStr, valueX, currentY, accentColor, false); + currentY += lineHeight; + + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.current").getString(), x, currentY, + labelColor, false); + if (currentEUt > 0) { + String currentEUStr = formatEnergy(currentEUt); + graphics.drawString(font, currentEUStr, valueX, currentY, 0xFFFFCC44, false); + } else { + graphics.drawString(font, "---", valueX, currentY, 0x80606060, false); + } + currentY += lineHeight; + + graphics.fill(x, currentY, x + contentWidth, currentY + 1, sepColor); + currentY += 4; + + if (isConnected && canProcess) { + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.speed_bonus").getString(), x, + currentY, labelColor, false); + String speedStr = String.format("%.1fx", speedBonus); + int speedColor = speedBonus > 1.0 ? 0xFF66FF66 : 0xFFCCCCCC; + graphics.drawString(font, speedStr, valueX, currentY, speedColor, false); + currentY += lineHeight; + + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.iris_limit").getString(), x, + currentY, labelColor, false); + graphics.drawString(font, irisParallelLimit + "x", valueX, currentY, valueColor, false); + } else if (isConnected) { + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.stage").getString(), x, + currentY, labelColor, false); + int stageColor = getStageTextColor(irisStage); + graphics.drawString(font, irisStage.toString(), valueX, currentY, stageColor, false); + currentY += lineHeight; + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.waiting_iris").getString(), x, + currentY, 0x80AAAAAA, false); + } else { + graphics.drawString(font, Component.translatable("cosmiccore.stellar.module.not_linked").getString(), x, + currentY, 0x80808080, false); + } + } + + @OnlyIn(Dist.CLIENT) + private void drawGearButton(GuiGraphics graphics, int btnX, int btnY, int mouseX, int mouseY) { + boolean hovered = mouseX >= btnX && mouseX < btnX + GEAR_BUTTON_SIZE && + mouseY >= btnY && mouseY < btnY + GEAR_BUTTON_SIZE; + + int bgColor = hovered ? 0xC04080FF : 0x80404060; + graphics.fill(btnX, btnY, btnX + GEAR_BUTTON_SIZE, btnY + GEAR_BUTTON_SIZE, bgColor); + + int borderColor = hovered ? 0xFF6090FF : 0xFF505070; + DrawerHelper.drawBorder(graphics, btnX, btnY, GEAR_BUTTON_SIZE, GEAR_BUTTON_SIZE, borderColor, 1); + + int gearSize = GEAR_BUTTON_SIZE - 4; + int gearX = btnX + 2; + int gearY = btnY + 2; + graphics.blit(GEAR_TEXTURE, gearX, gearY, 0, 0, gearSize, gearSize, gearSize, gearSize); + } + + private int getGearButtonX() { + return getPosition().x + WIDTH - GEAR_BUTTON_SIZE - 4; + } + + private int getGearButtonY() { + return getPosition().y + HEIGHT - GEAR_BUTTON_SIZE - 4; + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (showingPowerPopup && powerPopup.isVisible()) { + if (powerPopup.mouseClicked(mouseX, mouseY, button)) { + return true; + } + if (!powerPopup.isMouseOverElement(mouseX, mouseY)) { + hidePowerPopup(); + return true; + } + } + + int btnX = getGearButtonX(); + int btnY = getGearButtonY(); + if (mouseX >= btnX && mouseX < btnX + GEAR_BUTTON_SIZE && + mouseY >= btnY && mouseY < btnY + GEAR_BUTTON_SIZE) { + if (showingPowerPopup) { + hidePowerPopup(); + } else { + showPowerPopup(); + } + playButtonClickSound(); + return true; + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + private String formatEnergy(long eu) { + if (eu >= 1_000_000_000) return String.format("%.1fG EU/t", eu / 1_000_000_000.0); + if (eu >= 1_000_000) return String.format("%.1fM EU/t", eu / 1_000_000.0); + if (eu >= 1000) return String.format("%.1fk EU/t", eu / 1000.0); + return String.format("%d EU/t", eu); + } + + private int getTierColor(int tier) { + return switch (tier) { + case 0 -> 0x808080; + case 1 -> 0xC0C0C0; + case 2 -> 0x00FFFF; + case 3 -> 0xFFFF00; + case 4 -> 0x0080FF; + case 5 -> 0x8000FF; + case 6 -> 0xFF0080; + case 7 -> 0xFF00FF; + case 8 -> 0x00FF00; + default -> 0xFF4040; + }; + } + + private int getStageTextColor(Stage stage) { + return 0xFF000000 | switch (stage) { + case EMPTY -> 0x606060; + case GROWING -> 0x66AAFF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF8844; + case BLACK_HOLE -> 0xAA66FF; + case DEATH -> 0xFF4444; + case DEATH_GRACEFUL -> 0x886666; + }; + } + + private int getAccentColor() { + if (isConnected && canProcess) { + return getStageAccentColor(irisStage); + } + return 0xFF4080AA; + } + + private int getStageAccentColor(Stage stage) { + return 0xFF000000 | switch (stage) { + case EMPTY -> 0x404060; + case GROWING -> 0x6080FF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF8844; + case BLACK_HOLE -> 0x8040FF; + case DEATH -> 0xFF2020; + case DEATH_GRACEFUL -> 0x804040; + }; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarModuleUIWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarModuleUIWidget.java new file mode 100644 index 000000000..583ec3e3e --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/StellarModuleUIWidget.java @@ -0,0 +1,501 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.feature.IStellarIrisProvider; +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; +import com.ghostipedia.cosmiccore.api.machine.multiblock.StellarBaseModule; + +import com.gregtechceu.gtceu.api.gui.fancy.FancyMachineUIWidget; +import com.gregtechceu.gtceu.api.gui.fancy.IFancyUIProvider; + +import com.lowdragmc.lowdraglib.gui.texture.ColorBorderTexture; +import com.lowdragmc.lowdraglib.gui.texture.ColorRectTexture; +import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; +import com.lowdragmc.lowdraglib.gui.texture.IGuiTexture; +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.SlotWidget; +import com.lowdragmc.lowdraglib.gui.widget.Widget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class StellarModuleUIWidget extends FancyMachineUIWidget { + + private final Supplier moduleSupplier; + + private static final int BG_COLOR = 0xE00a0a14; + private static final int BORDER_COLOR = 0xFF404060; + private static final int SLOT_BG_COLOR = 0xC0101018; + private static final int SLOT_BORDER_COLOR = 0xFF505070; + private static final int MODULE_ACCENT = 0xFF4080AA; + private static final int MODULE_ACCENT_DIM = 0xFF305070; + + private int syncedMaxParallel = 1; + private long syncedVoltage = 32; + private int syncedIrisParallelLimit = 1; + private StellarModuleContentWidget contentWidget; + + public StellarModuleUIWidget(IFancyUIProvider mainPage, int width, int height, + Supplier moduleSupplier) { + super(mainPage, width, height); + this.moduleSupplier = moduleSupplier; + setBackground((IGuiTexture) null); + applyDarkTheme(); + } + + private void onPowerSettingsChanged(int maxParallel, long voltage) { + writeClientAction(1, buf -> { + buf.writeInt(maxParallel); + buf.writeLong(voltage); + }); + } + + @Override + public void handleClientAction(int id, FriendlyByteBuf buffer) { + if (id == 1) { + int newParallel = buffer.readInt(); + long newVoltage = buffer.readLong(); + + StellarBaseModule module = moduleSupplier.get(); + if (module != null) { + module.setConfiguredMaxParallel(newParallel); + module.setConfiguredVoltagePerParallel(newVoltage); + + // Mark dirty so it saves + module.markDirty(); + + com.ghostipedia.cosmiccore.CosmicCore.LOGGER.info( + "[StellarModuleUI] SERVER received power settings: parallel={}, voltage={}", + newParallel, newVoltage); + } + } else { + super.handleClientAction(id, buffer); + } + } + + @Override + public void detectAndSendChanges() { + super.detectAndSendChanges(); + StellarBaseModule module = moduleSupplier.get(); + if (module != null) { + int currentParallel = module.getConfiguredMaxParallel(); + long currentVoltage = module.getConfiguredVoltagePerParallel(); + int currentIrisLimit = module.getIrisParallelLimit(); + if (currentParallel != syncedMaxParallel || + currentVoltage != syncedVoltage || + currentIrisLimit != syncedIrisParallelLimit) { + syncedMaxParallel = currentParallel; + syncedVoltage = currentVoltage; + syncedIrisParallelLimit = currentIrisLimit; + writeUpdateInfo(201, buf -> { + buf.writeInt(syncedMaxParallel); + buf.writeLong(syncedVoltage); + buf.writeInt(syncedIrisParallelLimit); + }); + } + } + } + + @Override + public void writeInitialData(FriendlyByteBuf buffer) { + super.writeInitialData(buffer); + + StellarBaseModule module = moduleSupplier.get(); + if (module != null) { + buffer.writeInt(module.getConfiguredMaxParallel()); + buffer.writeLong(module.getConfiguredVoltagePerParallel()); + buffer.writeInt(module.getIrisParallelLimit()); + } else { + buffer.writeInt(1); + buffer.writeLong(32L); + buffer.writeInt(1); + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readInitialData(FriendlyByteBuf buffer) { + super.readInitialData(buffer); + syncedMaxParallel = buffer.readInt(); + syncedVoltage = buffer.readLong(); + syncedIrisParallelLimit = buffer.readInt(); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void readUpdateInfo(int id, FriendlyByteBuf buffer) { + if (id == 201) { + syncedMaxParallel = buffer.readInt(); + syncedVoltage = buffer.readLong(); + syncedIrisParallelLimit = buffer.readInt(); + } else { + super.readUpdateInfo(id, buffer); + } + } + + private void applyDarkTheme() { + IGuiTexture titleBarBg = new GuiTextureGroup( + new ColorRectTexture(BG_COLOR), + new ColorBorderTexture(1, BORDER_COLOR)); + + if (titleBar != null) { + titleBar.setBackground((IGuiTexture) null); + for (Widget widget : titleBar.widgets) { + if (widget instanceof WidgetGroup group) { + group.setBackground(titleBarBg); + } + } + } + + if (sideTabsWidget != null) { + sideTabsWidget.setBackground((IGuiTexture) null); + updateTabStyling(); + } + + if (configuratorPanel != null) { + configuratorPanel.setVisible(false); + configuratorPanel.setActive(false); + } + + if (playerInventory != null) { + playerInventory.setBackground((IGuiTexture) null); + IGuiTexture darkSlot = new GuiTextureGroup( + new ColorRectTexture(SLOT_BG_COLOR), + new ColorBorderTexture(1, SLOT_BORDER_COLOR)); + for (Widget widget : playerInventory.widgets) { + if (widget instanceof SlotWidget slotWidget) { + slotWidget.setBackground(darkSlot); + } + } + } + } + + private void updateTabStyling() { + if (sideTabsWidget == null) return; + + int accentColor = getAccentColor(); + int accentColorDim = dimColor(accentColor, 0.6f); + + IGuiTexture tabNormal = new GuiTextureGroup( + new ColorRectTexture(0xA0080812), + new ColorBorderTexture(1, accentColorDim)); + IGuiTexture tabHover = new GuiTextureGroup( + new ColorRectTexture(0xC0151525), + new ColorBorderTexture(1, accentColor)); + IGuiTexture tabPressed = new GuiTextureGroup( + new ColorRectTexture(0xE0101020), + new ColorBorderTexture(1, accentColor)); + + sideTabsWidget.setTabTexture(tabNormal); + sideTabsWidget.setTabHoverTexture(tabHover); + sideTabsWidget.setTabPressedTexture(tabPressed); + } + + private int dimColor(int color, float factor) { + int a = (color >> 24) & 0xFF; + int r = (int) (((color >> 16) & 0xFF) * factor); + int g = (int) (((color >> 8) & 0xFF) * factor); + int b = (int) ((color & 0xFF) * factor); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + @Override + public void initWidget() { + super.initWidget(); + if (playerInventory != null) { + IGuiTexture darkSlot = new GuiTextureGroup( + new ColorRectTexture(SLOT_BG_COLOR), + new ColorBorderTexture(1, SLOT_BORDER_COLOR)); + for (Widget widget : playerInventory.widgets) { + if (widget instanceof SlotWidget slotWidget) { + slotWidget.setBackground(darkSlot); + } + } + } + + // Find and wire up the content widget for power settings callback + findContentWidget(this); + if (contentWidget != null) { + contentWidget.setOnPowerSettingsChanged(this::onPowerSettingsChanged); + } + } + + private void findContentWidget(WidgetGroup group) { + for (Widget widget : group.widgets) { + if (widget instanceof StellarModuleContentWidget smcw) { + contentWidget = smcw; + return; + } + if (widget instanceof WidgetGroup wg) { + findContentWidget(wg); + if (contentWidget != null) return; + } + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + drawFullBackground(graphics); + drawCustomOverlays(graphics); + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + drawTitleText(graphics); + } + + private void drawFullBackground(GuiGraphics graphics) { + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + // Dark gradient background + DrawerHelper.drawGradientRect(graphics, x, y, w, h, 0xFF0c0c12, 0xFF060608, false); + + // Subtle grid pattern + drawGridPattern(graphics, x, y, w, h); + + // Corner accents + int accentColor = getAccentColor(); + drawCornerAccents(graphics, x, y, w, h, accentColor & 0x66FFFFFF); + + // Border + DrawerHelper.drawBorder(graphics, x, y, w, h, accentColor & 0x33FFFFFF, 1); + + // Side panels if inventory is visible + if (playerInventory != null && playerInventory.isVisible()) { + drawSidePanels(graphics, x, y, w, h); + } + } + + private void drawGridPattern(GuiGraphics graphics, int x, int y, int w, int h) { + int gridColor = 0x08FFFFFF; + int spacing = 16; + for (int gx = x + spacing; gx < x + w; gx += spacing) { + graphics.fill(gx, y, gx + 1, y + h, gridColor); + } + for (int gy = y + spacing; gy < y + h; gy += spacing) { + graphics.fill(x, gy, x + w, gy + 1, gridColor); + } + } + + private void drawCornerAccents(GuiGraphics graphics, int x, int y, int w, int h, int color) { + int len = 20; + int thickness = 2; + // Top-left + graphics.fill(x, y, x + len, y + thickness, color); + graphics.fill(x, y, x + thickness, y + len, color); + // Top-right + graphics.fill(x + w - len, y, x + w, y + thickness, color); + graphics.fill(x + w - thickness, y, x + w, y + len, color); + // Bottom-left + graphics.fill(x, y + h - thickness, x + len, y + h, color); + graphics.fill(x, y + h - len, x + thickness, y + h, color); + // Bottom-right + graphics.fill(x + w - len, y + h - thickness, x + w, y + h, color); + graphics.fill(x + w - thickness, y + h - len, x + w, y + h, color); + } + + private void drawSidePanels(GuiGraphics graphics, int x, int y, int w, int h) { + int invY = playerInventory.getPosition().y; + int invX = playerInventory.getPosition().x; + int invW = playerInventory.getSize().width; + int panelH = h - (invY - y) - 5; + + // Left panel - connection status + int leftPanelX = x + 3; + int leftPanelW = invX - leftPanelX - 3; + if (leftPanelW > 20) { + drawConnectionPanel(graphics, leftPanelX, invY, leftPanelW, panelH); + } + + // Right panel - stats + int rightPanelX = invX + invW + 3; + int rightPanelW = (x + w) - rightPanelX - 3; + if (rightPanelW > 20) { + drawStatsPanel(graphics, rightPanelX, invY, rightPanelW, panelH); + } + } + + private void drawConnectionPanel(GuiGraphics graphics, int px, int py, int pw, int ph) { + DrawerHelper.drawSolidRect(graphics, px, py, pw, ph, 0x40000000); + int borderColor = getAccentColor() & 0x33FFFFFF; + DrawerHelper.drawBorder(graphics, px, py, pw, ph, borderColor, 1); + int accentColor = getAccentColor() & 0x80FFFFFF; + graphics.fill(px + 1, py + 1, px + pw - 1, py + 3, accentColor); + + var font = Minecraft.getInstance().font; + StellarBaseModule module = moduleSupplier.get(); + + if (module != null) { + IStellarIrisProvider iris = module.getStellarIris(); + boolean connected = iris != null && iris.isFormed(); + boolean canProcess = connected && iris.canProcess(); + + String statusText = connected ? (canProcess ? "LINKED" : "WAITING") : "OFFLINE"; + int statusColor = connected ? (canProcess ? 0xFF66FF66 : 0xFFFFAA44) : 0xFFFF4444; + + graphics.drawString(font, "IRIS LINK", px + 4, py + 6, accentColor | 0xFF000000, false); + graphics.drawString(font, statusText, px + 4, py + 18, statusColor, false); + + // Animated connection indicator + if (connected) { + int pulseAlpha = (int) (128 + 64 * Math.sin(System.currentTimeMillis() / 200.0)); + int pulseColor = (pulseAlpha << 24) | (statusColor & 0x00FFFFFF); + int indicatorY = py + 30; + int indicatorW = (int) ((pw - 10) * (0.5f + 0.5f * Math.sin(System.currentTimeMillis() / 500.0))); + graphics.fill(px + 5, indicatorY, px + 5 + Math.max(5, indicatorW), indicatorY + 2, pulseColor); + } + } + } + + private void drawStatsPanel(GuiGraphics graphics, int px, int py, int pw, int ph) { + DrawerHelper.drawSolidRect(graphics, px, py, pw, ph, 0x40000000); + int borderColor = getAccentColor() & 0x33FFFFFF; + DrawerHelper.drawBorder(graphics, px, py, pw, ph, borderColor, 1); + int accentColor = getAccentColor() & 0x80FFFFFF; + graphics.fill(px + 1, py + 1, px + pw - 1, py + 3, accentColor); + + var font = Minecraft.getInstance().font; + int labelColor = 0xFF606080; + int valueColor = 0xFFCCCCCC; + + graphics.drawString(font, "MODULE", px + 4, py + 6, accentColor | 0xFF000000, false); + + StellarBaseModule module = moduleSupplier.get(); + if (module != null) { + IStellarIrisProvider iris = module.getStellarIris(); + + // Energy usage + long euPerTick = module.getEnergyConsumedPerTick(); + graphics.drawString(font, "EU/t:", px + 4, py + 20, labelColor, false); + graphics.drawString(font, formatEnergy(euPerTick), px + 30, py + 20, valueColor, false); + + // Iris bonuses (if connected) + if (iris != null && iris.canProcess()) { + graphics.drawString(font, "SPEED:", px + 4, py + 32, labelColor, false); + graphics.drawString(font, String.format("%.1fx", iris.getSpeedBonus()), px + 38, py + 32, 0xFF66FF66, + false); + + graphics.drawString(font, "STAGE:", px + 4, py + 44, labelColor, false); + Stage stage = iris.getStage(); + graphics.drawString(font, getShortStageName(stage), px + 38, py + 44, getStageColor(stage), false); + } else { + graphics.drawString(font, "---", px + 4, py + 32, 0xFF404040, false); + } + } + } + + private String formatEnergy(long eu) { + if (eu >= 1_000_000_000) return String.format("%.1fG", eu / 1_000_000_000.0); + if (eu >= 1_000_000) return String.format("%.1fM", eu / 1_000_000.0); + if (eu >= 1000) return String.format("%.1fk", eu / 1000.0); + return String.format("%d", eu); + } + + private String getShortStageName(Stage stage) { + return switch (stage) { + case EMPTY -> "NONE"; + case GROWING -> "GROW"; + case STAR -> "STAR"; + case SUPERSTAR -> "SUPER"; + case BLACK_HOLE -> "B.HOLE"; + case DEATH -> "DEATH"; + case DEATH_GRACEFUL -> "FADE"; + }; + } + + private int getStageColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0xFF606060; + case GROWING -> 0xFF66AAFF; + case STAR -> 0xFFFFCC44; + case SUPERSTAR -> 0xFFFF8844; + case BLACK_HOLE -> 0xFFAA66FF; + case DEATH -> 0xFFFF4444; + case DEATH_GRACEFUL -> 0xFF886666; + }; + } + + private void drawTitleText(GuiGraphics graphics) { + if (titleBar == null || mainPage == null) return; + + var font = Minecraft.getInstance().font; + String title = mainPage.getTitle().getString(); + + int titleBarX = getPosition().x + 8; + int titleBarY = getPosition().y - 16; + int textAreaX = titleBarX + 18 + 16; + int textAreaY = titleBarY + 3; + int textAreaWidth = getSize().width - 16 - 18 - 18 - 16; + int textAreaHeight = 13; + + graphics.fill(textAreaX, textAreaY, textAreaX + textAreaWidth, textAreaY + textAreaHeight, BG_COLOR); + + int textWidth = font.width(title); + int maxWidth = textAreaWidth - 4; + if (textWidth > maxWidth) { + String ellipsis = "..."; + int ellipsisWidth = font.width(ellipsis); + while (textWidth + ellipsisWidth > maxWidth && title.length() > 1) { + title = title.substring(0, title.length() - 1); + textWidth = font.width(title); + } + title = title + ellipsis; + textWidth = font.width(title); + } + + int centeredX = textAreaX + (textAreaWidth - textWidth) / 2; + int centeredY = textAreaY + (textAreaHeight - font.lineHeight) / 2; + + graphics.enableScissor(textAreaX, textAreaY, textAreaX + textAreaWidth, textAreaY + textAreaHeight); + graphics.drawString(font, title, centeredX, centeredY, 0xFFFFFFFF, true); + graphics.disableScissor(); + } + + private void drawCustomOverlays(GuiGraphics graphics) { + int accentColor = getAccentColor() & 0x60FFFFFF; + + if (playerInventory != null && playerInventory.isVisible()) { + int x = getPosition().x; + int w = getSize().width; + int invY = playerInventory.getPosition().y; + graphics.fill(x + 10, invY - 2, x + w - 10, invY - 1, accentColor); + } + } + + private int getAccentColor() { + StellarBaseModule module = moduleSupplier.get(); + if (module != null) { + IStellarIrisProvider iris = module.getStellarIris(); + if (iris != null && iris.isFormed() && iris.canProcess()) { + return getStageAccentColor(iris.getStage()); + } + } + return MODULE_ACCENT; + } + + private int getStageAccentColor(Stage stage) { + return 0xFF000000 | switch (stage) { + case EMPTY -> 0x404060; + case GROWING -> 0x6080FF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF8844; + case BLACK_HOLE -> 0x8040FF; + case DEATH -> 0xFF2020; + case DEATH_GRACEFUL -> 0x804040; + }; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/TelemetryPanelWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/TelemetryPanelWidget.java new file mode 100644 index 000000000..d224339c5 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/TelemetryPanelWidget.java @@ -0,0 +1,379 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine; +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.util.DrawerHelper; +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class TelemetryPanelWidget extends Widget { + + private final Supplier machineSupplier; + private final Supplier stageSupplier; + + private final List logLines = new ArrayList<>(); + private float scrollOffset = 0f; + private float dataUpdateTimer = 0f; + private float glitchPhase = 0f; + + private float displayedTemp = 0f; + private float displayedPressure = 0f; + private float displayedMass = 0f; + private float displayedEnergy = 0f; + + public TelemetryPanelWidget(int x, int y, int width, int height, + Supplier machineSupplier, + Supplier stageSupplier) { + super(x, y, width, height); + this.machineSupplier = machineSupplier; + this.stageSupplier = stageSupplier; + initLogLines(); + } + + private void initLogLines() { + logLines.add("[SYS] Stellar Iris v3.7.2 initialized"); + logLines.add("[SYS] Containment field generators online"); + logLines.add("[SYS] Plasma injectors standby"); + logLines.add("[SYS] Gravitational stabilizers active"); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + + Stage stage = stageSupplier.get(); + scrollOffset += 0.02f; + dataUpdateTimer += 1f; + glitchPhase += 0.1f; + + updateDisplayedValues(stage); + + if (dataUpdateTimer > 60f) { + dataUpdateTimer = 0f; + addLogLine(stage); + } + } + + private void updateDisplayedValues(Stage stage) { + float targetTemp = getTemperature(stage); + float targetPressure = getPressure(stage); + float targetMass = getMass(stage); + float targetEnergy = getEnergy(stage); + + float lerpSpeed = 0.05f; + displayedTemp = Mth.lerp(lerpSpeed, displayedTemp, targetTemp); + displayedPressure = Mth.lerp(lerpSpeed, displayedPressure, targetPressure); + displayedMass = Mth.lerp(lerpSpeed, displayedMass, targetMass); + displayedEnergy = Mth.lerp(lerpSpeed, displayedEnergy, targetEnergy); + + if (stage == Stage.DEATH) { + displayedTemp += (float) (Math.random() - 0.5) * 1000; + displayedPressure += (float) (Math.random() - 0.5) * 50; + } + } + + private float getTemperature(Stage stage) { + return switch (stage) { + case EMPTY -> 2.7f; + case GROWING -> 5_000_000f; + case STAR -> 15_000_000f; + case SUPERSTAR -> 100_000_000f; + case BLACK_HOLE -> Float.POSITIVE_INFINITY; + case DEATH -> 500_000_000f; + case DEATH_GRACEFUL -> 1_000_000f; + }; + } + + private float getPressure(Stage stage) { + return switch (stage) { + case EMPTY -> 0f; + case GROWING -> 150f; + case STAR -> 250f; + case SUPERSTAR -> 450f; + case BLACK_HOLE -> 999f; + case DEATH -> 800f; + case DEATH_GRACEFUL -> 50f; + }; + } + + private float getMass(Stage stage) { + return switch (stage) { + case EMPTY -> 0f; + case GROWING -> 0.3f; + case STAR -> 1f; + case SUPERSTAR -> 8f; + case BLACK_HOLE -> 25f; + case DEATH -> 12f; + case DEATH_GRACEFUL -> 0.1f; + }; + } + + private float getEnergy(Stage stage) { + return switch (stage) { + case EMPTY -> 0f; + case GROWING -> 1_000f; + case STAR -> 50_000f; + case SUPERSTAR -> 500_000f; + case BLACK_HOLE -> 10_000_000f; + case DEATH -> 100_000_000f; + case DEATH_GRACEFUL -> 500f; + }; + } + + private void addLogLine(Stage stage) { + String newLine = generateLogLine(stage); + logLines.add(newLine); + if (logLines.size() > 50) { + logLines.remove(0); + } + } + + private String generateLogLine(Stage stage) { + long tick = System.currentTimeMillis() / 50; + String timestamp = String.format("[%04d]", tick % 10000); + + return switch (stage) { + case EMPTY -> timestamp + " [IDLE] Awaiting ignition sequence"; + case GROWING -> { + String[] msgs = { + " [CORE] Fusion rate increasing", + " [FUEL] Hydrogen consumption nominal", + " [TEMP] Core temperature rising", + " [STAB] Plasma containment stable" + }; + yield timestamp + msgs[(int) (tick % msgs.length)]; + } + case STAR -> { + String[] msgs = { + " [CORE] Main sequence fusion active", + " [OUT] Energy output: " + (int) displayedEnergy + " TW", + " [FUEL] Helium ash accumulating", + " [STAB] All systems nominal" + }; + yield timestamp + msgs[(int) (tick % msgs.length)]; + } + case SUPERSTAR -> { + String[] msgs = { + " [WARN] Core pressure critical", + " [WARN] Mass exceeding safe limits", + " [ALERT] Collapse threshold approaching", + " [CORE] Heavy element fusion detected" + }; + yield timestamp + msgs[(int) (tick % msgs.length)]; + } + case BLACK_HOLE -> { + String[] msgs = { + " [SING] Event horizon stable", + " [GRAV] Hawking radiation detected", + " [CONT] Exotic matter containment active", + " [DATA] Spacetime curvature nominal" + }; + yield timestamp + msgs[(int) (tick % msgs.length)]; + } + case DEATH -> { + String[] msgs = { + " [CRIT] CONTAINMENT FAILURE", + " [CRIT] EMERGENCY PROTOCOLS ACTIVE", + " [CRIT] EVACUATE IMMEDIATELY", + " [CRIT] SYSTEM FAILURE IMMINENT" + }; + yield timestamp + msgs[(int) (tick % msgs.length)]; + } + case DEATH_GRACEFUL -> timestamp + " [SHUT] Controlled shutdown in progress"; + }; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + Stage stage = stageSupplier.get(); + int accentColor = getStageColor(stage); + + DrawerHelper.drawSolidRect(graphics, x, y, w, h, 0xDD080810); + DrawerHelper.drawBorder(graphics, x, y, w, h, (0x80 << 24) | accentColor, 1); + + graphics.fill(x + 1, y + 1, x + w - 1, y + 3, (0x60 << 24) | accentColor); + + int dataHeight = 50; + drawDataReadouts(graphics, x + 5, y + 8, w - 10, dataHeight, stage, accentColor); + + int logY = y + 8 + dataHeight + 5; + int logH = h - dataHeight - 18; + drawLogPanel(graphics, x + 5, logY, w - 10, logH, stage, accentColor); + } + + private void drawDataReadouts(GuiGraphics graphics, int x, int y, int w, int h, Stage stage, int accentColor) { + var font = Minecraft.getInstance().font; + + graphics.fill(x, y, x + w, y + h, 0x40000000); + DrawerHelper.drawBorder(graphics, x, y, w, h, (0x40 << 24) | accentColor, 1); + + int col1 = x + 5; + int col2 = x + w / 2 + 5; + int row1 = y + 5; + int row2 = y + 15; + int row3 = y + 25; + int row4 = y + 35; + + int labelColor = 0xFF606080; + int valueColor = 0xFFCCCCCC; + + graphics.drawString(font, "CORE TEMP:", col1, row1, labelColor, false); + graphics.drawString(font, formatTemperature(displayedTemp), col1 + 60, row1, getTemperatureColor(displayedTemp), + false); + + graphics.drawString(font, "PRESSURE:", col1, row2, labelColor, false); + graphics.drawString(font, String.format("%.1f GPa", displayedPressure), col1 + 60, row2, valueColor, false); + + graphics.drawString(font, "MASS:", col2, row1, labelColor, false); + graphics.drawString(font, String.format("%.2f M\u2609", displayedMass), col2 + 35, row1, valueColor, false); + + graphics.drawString(font, "OUTPUT:", col2, row2, labelColor, false); + graphics.drawString(font, formatEnergy(displayedEnergy), col2 + 45, row2, valueColor, false); + + String status = getStatusString(stage); + int statusColor = getStatusColor(stage); + graphics.drawString(font, "STATUS:", col1, row3, labelColor, false); + + if (stage == Stage.DEATH && ((int) (glitchPhase * 2) % 3 == 0)) { + int glitchOffset = (int) ((Math.random() - 0.5) * 4); + graphics.drawString(font, status, col1 + 45 + glitchOffset, row3, statusColor, false); + } else { + graphics.drawString(font, status, col1 + 45, row3, statusColor, false); + } + + String stageLabel = "PHASE: " + stage.name().replace("_", " "); + graphics.drawString(font, stageLabel, col1, row4, (0xC0 << 24) | accentColor, false); + } + + private void drawLogPanel(GuiGraphics graphics, int x, int y, int w, int h, Stage stage, int accentColor) { + var font = Minecraft.getInstance().font; + + graphics.fill(x, y, x + w, y + h, 0x60000000); + DrawerHelper.drawBorder(graphics, x, y, w, h, (0x30 << 24) | accentColor, 1); + + graphics.drawString(font, "SYSTEM LOG", x + 3, y + 2, (0x80 << 24) | accentColor, false); + + int logStartY = y + 12; + int logHeight = h - 14; + int lineHeight = 9; + int visibleLines = logHeight / lineHeight; + + graphics.enableScissor(x + 2, logStartY, x + w - 2, y + h - 2); + + int startIndex = Math.max(0, logLines.size() - visibleLines); + for (int i = startIndex; i < logLines.size(); i++) { + int lineY = logStartY + (i - startIndex) * lineHeight; + String line = logLines.get(i); + + int lineColor; + if (line.contains("[CRIT]")) { + lineColor = 0xFFFF4444; + } else if (line.contains("[WARN]") || line.contains("[ALERT]")) { + lineColor = 0xFFFFAA44; + } else if (line.contains("[SYS]")) { + lineColor = 0xFF44AAFF; + } else { + lineColor = 0xFF888888; + } + + if (stage == Stage.DEATH && line.contains("[CRIT]")) { + if ((int) (glitchPhase * 3) % 2 == 0) { + int glitchX = (int) ((Math.random() - 0.5) * 3); + graphics.drawString(font, line, x + 3 + glitchX, lineY, lineColor, false); + } + } else { + graphics.drawString(font, line, x + 3, lineY, lineColor, false); + } + } + + graphics.disableScissor(); + } + + private String formatTemperature(float temp) { + if (Float.isInfinite(temp)) { + return "\u221E K"; + } else if (temp >= 1_000_000_000) { + return String.format("%.1f GK", temp / 1_000_000_000); + } else if (temp >= 1_000_000) { + return String.format("%.1f MK", temp / 1_000_000); + } else if (temp >= 1000) { + return String.format("%.1f kK", temp / 1000); + } else { + return String.format("%.1f K", temp); + } + } + + private String formatEnergy(float energy) { + if (energy >= 1_000_000) { + return String.format("%.1f PW", energy / 1_000_000); + } else if (energy >= 1000) { + return String.format("%.1f TW", energy / 1000); + } else { + return String.format("%.0f GW", energy); + } + } + + private int getTemperatureColor(float temp) { + if (temp >= 100_000_000) return 0xFFFF4444; + if (temp >= 10_000_000) return 0xFFFFAA44; + if (temp >= 1_000_000) return 0xFFFFFF44; + return 0xFFCCCCCC; + } + + private String getStatusString(Stage stage) { + return switch (stage) { + case EMPTY -> "DORMANT"; + case GROWING -> "IGNITING"; + case STAR -> "STABLE"; + case SUPERSTAR -> "CRITICAL"; + case BLACK_HOLE -> "CONTAINED"; + case DEATH -> "FAILURE"; + case DEATH_GRACEFUL -> "SHUTDOWN"; + }; + } + + private int getStatusColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0xFF606060; + case GROWING -> 0xFF66AAFF; + case STAR -> 0xFF66FF66; + case SUPERSTAR -> 0xFFFFAA44; + case BLACK_HOLE -> 0xFFAA66FF; + case DEATH -> 0xFFFF4444; + case DEATH_GRACEFUL -> 0xFF886666; + }; + } + + private int getStageColor(Stage stage) { + return switch (stage) { + case EMPTY -> 0x405060; + case GROWING -> 0x6090FF; + case STAR -> 0xFFCC44; + case SUPERSTAR -> 0xFF7722; + case BLACK_HOLE -> 0xAA55FF; + case DEATH -> 0xFF3030; + case DEATH_GRACEFUL -> 0x664040; + }; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/WarningOverlayWidget.java b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/WarningOverlayWidget.java new file mode 100644 index 000000000..a3be92574 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/gui/widget/stellar/WarningOverlayWidget.java @@ -0,0 +1,253 @@ +package com.ghostipedia.cosmiccore.client.gui.widget.stellar; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine.Stage; + +import com.lowdragmc.lowdraglib.gui.widget.Widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +public class WarningOverlayWidget extends Widget { + + private final Supplier stageSupplier; + + private float warningPhase = 0f; + private float alertFlash = 0f; + private float textGlitch = 0f; + + public WarningOverlayWidget(int x, int y, int width, int height, Supplier stageSupplier) { + super(x, y, width, height); + this.stageSupplier = stageSupplier; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void updateScreen() { + super.updateScreen(); + + Stage stage = stageSupplier.get(); + warningPhase += 0.1f; + + if (stage == Stage.DEATH) { + alertFlash += 0.3f; + textGlitch = (float) Math.random() * 0.5f; + } else if (stage == Stage.SUPERSTAR || stage == Stage.BLACK_HOLE) { + alertFlash += 0.15f; + textGlitch *= 0.9f; + } else { + alertFlash *= 0.9f; + textGlitch *= 0.8f; + } + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(@Nonnull GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + + Stage stage = stageSupplier.get(); + + if (stage == Stage.EMPTY || stage == Stage.GROWING || stage == Stage.STAR) { + return; + } + + int x = getPosition().x; + int y = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + switch (stage) { + case SUPERSTAR -> drawSuperstarWarning(graphics, x, y, w, h); + case BLACK_HOLE -> drawBlackHoleWarning(graphics, x, y, w, h); + case DEATH -> drawCriticalAlert(graphics, x, y, w, h); + case DEATH_GRACEFUL -> drawShutdownNotice(graphics, x, y, w, h); + } + } + + private void drawSuperstarWarning(GuiGraphics graphics, int x, int y, int w, int h) { + float pulse = Mth.sin(warningPhase) * 0.5f + 0.5f; + + int bannerAlpha = (int) (0x40 * pulse); + int bannerColor = (bannerAlpha << 24) | 0xFF8800; + + int bannerY = y + 5; + int bannerH = 16; + graphics.fill(x, bannerY, x + w, bannerY + bannerH, bannerColor); + + int borderColor = (0x80 << 24) | 0xFF6600; + graphics.fill(x, bannerY, x + w, bannerY + 1, borderColor); + graphics.fill(x, bannerY + bannerH - 1, x + w, bannerY + bannerH, borderColor); + + var font = Minecraft.getInstance().font; + String warning = "!! CRITICAL MASS APPROACHING !!"; + int textWidth = font.width(warning); + + float scroll = (warningPhase * 2f) % (textWidth + w); + int textX = x + w - (int) scroll; + + graphics.enableScissor(x, bannerY, x + w, bannerY + bannerH); + graphics.drawString(font, warning, textX, bannerY + 4, 0xFFFFCC00, false); + graphics.drawString(font, warning, textX + textWidth + 50, bannerY + 4, 0xFFFFCC00, false); + graphics.disableScissor(); + + drawHazardStripes(graphics, x, bannerY + bannerH + 2, w, 3, 0xFFFF8800, 0xFF442200); + } + + private void drawBlackHoleWarning(GuiGraphics graphics, int x, int y, int w, int h) { + float pulse = Mth.sin(warningPhase * 0.7f) * 0.5f + 0.5f; + + int topBannerY = y + 5; + int bottomBannerY = y + h - 21; + int bannerH = 16; + + int bannerAlpha = (int) (0x50 * pulse); + int bannerColor = (bannerAlpha << 24) | 0x8040FF; + + graphics.fill(x, topBannerY, x + w, topBannerY + bannerH, bannerColor); + graphics.fill(x, bottomBannerY, x + w, bottomBannerY + bannerH, bannerColor); + + int borderColor = (0xA0 << 24) | 0x6020DD; + graphics.fill(x, topBannerY, x + w, topBannerY + 1, borderColor); + graphics.fill(x, topBannerY + bannerH - 1, x + w, topBannerY + bannerH, borderColor); + graphics.fill(x, bottomBannerY, x + w, bottomBannerY + 1, borderColor); + graphics.fill(x, bottomBannerY + bannerH - 1, x + w, bottomBannerY + bannerH, borderColor); + + var font = Minecraft.getInstance().font; + + String topText = ">> SINGULARITY CONTAINMENT ACTIVE <<"; + int topTextW = font.width(topText); + graphics.drawString(font, topText, x + (w - topTextW) / 2, topBannerY + 4, 0xFFCC99FF, false); + + String bottomText = "GRAVITATIONAL ANOMALY DETECTED"; + int bottomTextW = font.width(bottomText); + int glitchOffset = (int) (textGlitch * 3); + graphics.drawString(font, bottomText, x + (w - bottomTextW) / 2 + glitchOffset, bottomBannerY + 4, 0xFFAA77FF, + false); + + drawCornerBrackets(graphics, x + 10, topBannerY - 5, w - 20, bannerH + 10, 0xAA8040FF); + } + + private void drawCriticalAlert(GuiGraphics graphics, int x, int y, int w, int h) { + float flash = Mth.sin(alertFlash) * 0.5f + 0.5f; + + int screenFlashAlpha = (int) (0x20 * flash); + graphics.fill(x, y, x + w, y + h, (screenFlashAlpha << 24) | 0xFF0000); + + int topY = y + 5; + int bottomY = y + h - 25; + int bannerH = 20; + + int bannerAlpha = (int) (0x60 + 0x40 * flash); + int bannerColor = (bannerAlpha << 24) | 0xCC0000; + + graphics.fill(x, topY, x + w, topY + bannerH, bannerColor); + graphics.fill(x, bottomY, x + w, bottomY + bannerH, bannerColor); + + drawHazardStripes(graphics, x, topY - 4, w, 4, 0xFFFF0000, 0xFF440000); + drawHazardStripes(graphics, x, topY + bannerH, w, 4, 0xFFFF0000, 0xFF440000); + drawHazardStripes(graphics, x, bottomY - 4, w, 4, 0xFFFF0000, 0xFF440000); + drawHazardStripes(graphics, x, bottomY + bannerH, w, 4, 0xFFFF0000, 0xFF440000); + + var font = Minecraft.getInstance().font; + + String criticalText = "!!! CRITICAL FAILURE !!!"; + int textW = font.width(criticalText); + int glitchX = (int) ((Math.random() - 0.5) * textGlitch * 10); + int glitchY = (int) ((Math.random() - 0.5) * textGlitch * 4); + + int textColor = flash > 0.5f ? 0xFFFFFFFF : 0xFFFF4444; + graphics.drawString(font, criticalText, x + (w - textW) / 2 + glitchX, topY + 6 + glitchY, textColor, false); + + if (textGlitch > 0.2f) { + int ghostAlpha = (int) (0x40 * textGlitch); + int ghostColor = (ghostAlpha << 24) | 0x00FFFF; + graphics.drawString(font, criticalText, x + (w - textW) / 2 + glitchX + 2, topY + 6 + glitchY, ghostColor, + false); + } + + String evacuateText = "EVACUATE IMMEDIATELY"; + int evacW = font.width(evacuateText); + graphics.drawString(font, evacuateText, x + (w - evacW) / 2, bottomY + 6, 0xFFFFAAAA, false); + + drawWarningTriangles(graphics, x + 15, topY + 3, 14); + drawWarningTriangles(graphics, x + w - 29, topY + 3, 14); + } + + private void drawShutdownNotice(GuiGraphics graphics, int x, int y, int w, int h) { + float fade = Mth.sin(warningPhase * 0.3f) * 0.3f + 0.7f; + + int bannerY = y + h / 2 - 12; + int bannerH = 24; + + int bannerAlpha = (int) (0x50 * fade); + int bannerColor = (bannerAlpha << 24) | 0x604040; + graphics.fill(x + 20, bannerY, x + w - 20, bannerY + bannerH, bannerColor); + + int borderColor = (0x60 << 24) | 0x804040; + graphics.fill(x + 20, bannerY, x + w - 20, bannerY + 1, borderColor); + graphics.fill(x + 20, bannerY + bannerH - 1, x + w - 20, bannerY + bannerH, borderColor); + graphics.fill(x + 20, bannerY, x + 21, bannerY + bannerH, borderColor); + graphics.fill(x + w - 21, bannerY, x + w - 20, bannerY + bannerH, borderColor); + + var font = Minecraft.getInstance().font; + String text = "CONTROLLED SHUTDOWN IN PROGRESS"; + int textW = font.width(text); + int textColor = (int) (0xFF * fade) << 24 | 0x999999; + graphics.drawString(font, text, x + (w - textW) / 2, bannerY + 8, textColor, false); + + int dotsVisible = ((int) (warningPhase * 2)) % 4; + StringBuilder dots = new StringBuilder(); + for (int i = 0; i < dotsVisible; i++) dots.append("."); + graphics.drawString(font, dots.toString(), x + (w + textW) / 2 + 2, bannerY + 8, textColor, false); + } + + private void drawHazardStripes(GuiGraphics graphics, int x, int y, int w, int h, int color1, int color2) { + int stripeWidth = 8; + float offset = (warningPhase * 20) % (stripeWidth * 2); + + graphics.enableScissor(x, y, x + w, y + h); + for (int sx = x - stripeWidth * 2 + (int) offset; sx < x + w + stripeWidth; sx += stripeWidth * 2) { + graphics.fill(sx, y, sx + stripeWidth, y + h, color1); + graphics.fill(sx + stripeWidth, y, sx + stripeWidth * 2, y + h, color2); + } + graphics.disableScissor(); + } + + private void drawCornerBrackets(GuiGraphics graphics, int x, int y, int w, int h, int color) { + int len = 8; + int thickness = 2; + + graphics.fill(x, y, x + len, y + thickness, color); + graphics.fill(x, y, x + thickness, y + len, color); + + graphics.fill(x + w - len, y, x + w, y + thickness, color); + graphics.fill(x + w - thickness, y, x + w, y + len, color); + + graphics.fill(x, y + h - thickness, x + len, y + h, color); + graphics.fill(x, y + h - len, x + thickness, y + h, color); + + graphics.fill(x + w - len, y + h - thickness, x + w, y + h, color); + graphics.fill(x + w - thickness, y + h - len, x + w, y + h, color); + } + + private void drawWarningTriangles(GuiGraphics graphics, int x, int y, int size) { + int color = 0xFFFFCC00; + + for (int row = 0; row < size; row++) { + int halfWidth = row * size / (size * 2); + int cx = x + size / 2; + int drawY = y + row; + graphics.fill(cx - halfWidth, drawY, cx + halfWidth + 1, drawY + 1, color); + } + + var font = Minecraft.getInstance().font; + graphics.drawString(font, "!", x + size / 2 - 2, y + size / 3, 0xFF000000, false); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/renderer/BackgroundRenderer.java b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/BackgroundRenderer.java new file mode 100644 index 000000000..fe3990d72 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/BackgroundRenderer.java @@ -0,0 +1,105 @@ +package com.ghostipedia.cosmiccore.client.renderer; + +import com.ghostipedia.cosmiccore.client.CosmicCoreClient; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.ShaderInstance; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import org.joml.Matrix4f; + +@OnlyIn(Dist.CLIENT) +public class BackgroundRenderer { + + public enum BackgroundType { + VOID, // Mystical ethereal void + GALAXY // Deep space galaxy with nebulae + } + + public static void render(PoseStack poseStack, BackgroundType type, float fadeAlpha, + int screenWidth, int screenHeight) { + Matrix4f matrix = poseStack.last().pose(); + + // Black overlay first so world fades to black during transitions + int blackAlpha = (int) (fadeAlpha * 255); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + + BufferBuilder blackBuffer = Tesselator.getInstance().getBuilder(); + blackBuffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + blackBuffer.vertex(matrix, 0, screenHeight, 0).color(0, 0, 0, blackAlpha).endVertex(); + blackBuffer.vertex(matrix, screenWidth, screenHeight, 0).color(0, 0, 0, blackAlpha).endVertex(); + blackBuffer.vertex(matrix, screenWidth, 0, 0).color(0, 0, 0, blackAlpha).endVertex(); + blackBuffer.vertex(matrix, 0, 0, 0).color(0, 0, 0, blackAlpha).endVertex(); + BufferUploader.drawWithShader(blackBuffer.end()); + + ShaderInstance shader = type == BackgroundType.VOID ? CosmicCoreClient.getVoidBgShader() : + CosmicCoreClient.getGalaxyBgShader(); + + if (shader == null) { + RenderSystem.disableBlend(); + return; + } + + if (fadeAlpha < 0.01f) { + RenderSystem.disableBlend(); + return; + } + + RenderSystem.setShader(() -> shader); + + if (shader.GAME_TIME != null) { + shader.GAME_TIME.set(RenderSystem.getShaderGameTime()); + } + if (shader.SCREEN_SIZE != null) { + shader.SCREEN_SIZE.set((float) screenWidth, (float) screenHeight); + } + + setUniformSafe(shader, "Intensity", fadeAlpha); + + BufferBuilder buffer = Tesselator.getInstance().getBuilder(); + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + buffer.vertex(matrix, 0, screenHeight, 0).uv(0, 1).endVertex(); + buffer.vertex(matrix, screenWidth, screenHeight, 0).uv(1, 1).endVertex(); + buffer.vertex(matrix, screenWidth, 0, 0).uv(1, 0).endVertex(); + buffer.vertex(matrix, 0, 0, 0).uv(0, 0).endVertex(); + BufferUploader.drawWithShader(buffer.end()); + + RenderSystem.disableBlend(); + RenderSystem.setShader(GameRenderer::getPositionTexShader); + } + + private static void renderFallback(PoseStack poseStack, int screenWidth, int screenHeight) { + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + + Matrix4f matrix = poseStack.last().pose(); + + BufferBuilder buffer = Tesselator.getInstance().getBuilder(); + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + buffer.vertex(matrix, 0, screenHeight, 0).color(0, 0, 0, 255).endVertex(); + buffer.vertex(matrix, screenWidth, screenHeight, 0).color(0, 0, 0, 255).endVertex(); + buffer.vertex(matrix, screenWidth, 0, 0).color(0, 0, 0, 255).endVertex(); + buffer.vertex(matrix, 0, 0, 0).color(0, 0, 0, 255).endVertex(); + BufferUploader.drawWithShader(buffer.end()); + + RenderSystem.disableBlend(); + } + + private static void setUniformSafe(ShaderInstance shader, String name, float value) { + var uniform = shader.getUniform(name); + if (uniform != null) { + uniform.set(value); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/renderer/CosmicCoreRenderTypes.java b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/CosmicCoreRenderTypes.java index 95401554a..6b5037b53 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/client/renderer/CosmicCoreRenderTypes.java +++ b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/CosmicCoreRenderTypes.java @@ -13,6 +13,8 @@ public class CosmicCoreRenderTypes extends RenderType { protected static final ShaderStateShard NEBULAE_SHADER = new ShaderStateShard(CosmicCoreClient::getNebulaeShader); + protected static final ShaderStateShard SOUL_AURA_SHADER = new ShaderStateShard( + CosmicCoreClient::getSoulAuraShader); private static final RenderType NEBULAE = RenderType.create("nebulae", DefaultVertexFormat.POSITION, VertexFormat.Mode.QUADS, 256, false, false, @@ -20,6 +22,14 @@ public class CosmicCoreRenderTypes extends RenderType { .setShaderState(NEBULAE_SHADER) .createCompositeState(false)); + private static final RenderType SOUL_AURA = RenderType.create("soul_aura", + DefaultVertexFormat.POSITION_TEX, VertexFormat.Mode.QUADS, 256, false, false, + RenderType.CompositeState.builder() + .setShaderState(SOUL_AURA_SHADER) + .setTransparencyState(TRANSLUCENT_TRANSPARENCY) + .setWriteMaskState(COLOR_WRITE) + .createCompositeState(false)); + private CosmicCoreRenderTypes(String name, VertexFormat format, VertexFormat.Mode mode, int bufferSize, boolean affectsCrumbling, boolean sortOnUpload, Runnable setupState, Runnable clearState) { @@ -29,4 +39,8 @@ private CosmicCoreRenderTypes(String name, VertexFormat format, VertexFormat.Mod public static RenderType nebulae() { return NEBULAE; } + + public static RenderType soulAura() { + return SOUL_AURA; + } } diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/renderer/SoulAuraRenderer.java b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/SoulAuraRenderer.java new file mode 100644 index 000000000..65326a0ed --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/SoulAuraRenderer.java @@ -0,0 +1,110 @@ +package com.ghostipedia.cosmiccore.client.renderer; + +import com.ghostipedia.cosmiccore.client.CosmicCoreClient; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionConstants; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.ShaderInstance; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import org.joml.Matrix4f; + +@OnlyIn(Dist.CLIENT) +public class SoulAuraRenderer { + + public static void render(PoseStack poseStack, int centerX, int centerY, int radius, + int erosion, float intensity, int screenWidth, int screenHeight) { + ShaderInstance shader = CosmicCoreClient.getSoulAuraShader(); + if (shader == null) return; + + float[] color = getAuraColor(erosion); + + float normalizedCenterX = (float) centerX / screenWidth; + float normalizedCenterY = (float) centerY / screenHeight; + float normalizedRadius = (float) radius / Math.min(screenWidth, screenHeight); + + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.setShader(() -> shader); + + if (shader.GAME_TIME != null) { + shader.GAME_TIME.set(RenderSystem.getShaderGameTime()); + } + if (shader.SCREEN_SIZE != null) { + shader.SCREEN_SIZE.set((float) screenWidth, (float) screenHeight); + } + + setUniformSafe(shader, "Center", normalizedCenterX, normalizedCenterY); + setUniformSafe(shader, "BaseColor", color[0], color[1], color[2]); + setUniformSafe(shader, "Intensity", intensity); + setUniformSafe(shader, "Radius", normalizedRadius); + + int auraSize = (int) (radius * 3.0f); + int x1 = centerX - auraSize; + int y1 = centerY - auraSize; + int x2 = centerX + auraSize; + int y2 = centerY + auraSize; + + float u1 = (float) x1 / screenWidth; + float v1 = (float) y1 / screenHeight; + float u2 = (float) x2 / screenWidth; + float v2 = (float) y2 / screenHeight; + + Matrix4f matrix = poseStack.last().pose(); + + BufferBuilder buffer = Tesselator.getInstance().getBuilder(); + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + buffer.vertex(matrix, x1, y2, 0).uv(u1, v2).endVertex(); + buffer.vertex(matrix, x2, y2, 0).uv(u2, v2).endVertex(); + buffer.vertex(matrix, x2, y1, 0).uv(u2, v1).endVertex(); + buffer.vertex(matrix, x1, y1, 0).uv(u1, v1).endVertex(); + BufferUploader.drawWithShader(buffer.end()); + + RenderSystem.disableBlend(); + RenderSystem.setShader(GameRenderer::getPositionTexShader); + } + + // Aura colors are complementary to soul orb colors + private static float[] getAuraColor(int erosion) { + int tier = ReflectionConstants.getSoulColorTier(erosion); + + return switch (tier) { + case 0 -> new float[] { 1.0f, 0.85f, 0.45f }; + case 1 -> new float[] { 1.0f, 0.60f, 0.30f }; + case 2 -> new float[] { 0.70f, 0.90f, 0.30f }; + case 3 -> new float[] { 0.30f, 0.85f, 0.50f }; + case 4 -> new float[] { 0.25f, 0.75f, 0.75f }; + case 5 -> new float[] { 0.15f, 0.45f, 0.45f }; + default -> new float[] { 0.10f, 0.25f, 0.25f }; + }; + } + + private static void setUniformSafe(ShaderInstance shader, String name, float value) { + var uniform = shader.getUniform(name); + if (uniform != null) { + uniform.set(value); + } + } + + private static void setUniformSafe(ShaderInstance shader, String name, float x, float y) { + var uniform = shader.getUniform(name); + if (uniform != null) { + uniform.set(x, y); + } + } + + private static void setUniformSafe(ShaderInstance shader, String name, float x, float y, float z) { + var uniform = shader.getUniform(name); + if (uniform != null) { + uniform.set(x, y, z); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/renderer/machine/CosmicDynamicRenderHelpers.java b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/machine/CosmicDynamicRenderHelpers.java index 88b01a9f3..ca52ceef3 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/client/renderer/machine/CosmicDynamicRenderHelpers.java +++ b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/machine/CosmicDynamicRenderHelpers.java @@ -53,4 +53,8 @@ public class CosmicDynamicRenderHelpers { public static DynamicRender getBioVatRenderer() { return BioVatRender.INSTANCE; } + + public static DynamicRender getStarLadderRender() { + return StarLadderRender.INSTANCE; + } } diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/renderer/machine/StarLadderRender.java b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/machine/StarLadderRender.java new file mode 100644 index 000000000..6318ab5be --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/machine/StarLadderRender.java @@ -0,0 +1,538 @@ +package com.ghostipedia.cosmiccore.client.renderer.machine; + +import com.ghostipedia.cosmiccore.CosmicCore; + +import com.gregtechceu.gtceu.api.machine.multiblock.WorkableElectricMultiblockMachine; +import com.gregtechceu.gtceu.api.pattern.util.RelativeDirection; +import com.gregtechceu.gtceu.client.renderer.GTRenderTypes; +import com.gregtechceu.gtceu.client.renderer.machine.DynamicRender; +import com.gregtechceu.gtceu.client.renderer.machine.DynamicRenderType; +import com.gregtechceu.gtceu.client.util.ModelUtils; +import com.gregtechceu.gtceu.client.util.RenderBufferHelper; + +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.Sheets; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.AABB; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.serialization.Codec; +import org.jetbrains.annotations.NotNull; +import org.joml.Matrix4f; +import org.joml.Quaternionf; + +import java.util.HashSet; +import java.util.function.BiFunction; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class StarLadderRender extends + DynamicRender { + + public static final StarLadderRender INSTANCE = new StarLadderRender(); + public static final Codec CODEC = Codec.unit(INSTANCE); + public static final DynamicRenderType TYPE = new DynamicRenderType<>( + StarLadderRender.CODEC); + + private static final BiFunction renderBoundCache = Util.memoize((front, upwards) -> { + Direction up = RelativeDirection.UP.getRelative(front, upwards, false); + Direction back = RelativeDirection.BACK.getRelative(front, upwards, false); + Direction left = RelativeDirection.LEFT.getRelative(front, upwards, false); + + // offset from the controller to the inner cube (scaled up by 1 in all directions) + // values are from the multi pattern + BlockPos.MutableBlockPos minPos = new BlockPos.MutableBlockPos() + .move(left, 3).move(up, 1).move(back, 2); + BlockPos.MutableBlockPos maxPos = new BlockPos.MutableBlockPos() + .move(left, -3).move(up, 7).move(back, 8); + + return new AABB(minPos, maxPos); + }); + + public static final ResourceLocation BLOOD_CUBE_TEXTURE = CosmicCore.id("block/iris/blood_cube"); + + private static TextureAtlasSprite bloodCubeSprite = null; + private static boolean isEventListenerRegistered = false; + + @SuppressWarnings("deprecation") + private StarLadderRender() { + if (!isEventListenerRegistered) { + ModelUtils.registerAtlasStitchedEventListener(true, TextureAtlas.LOCATION_BLOCKS, event -> { + bloodCubeSprite = event.getAtlas().getSprite(BLOOD_CUBE_TEXTURE); + }); + isEventListenerRegistered = true; + } + } + + @Override + public DynamicRenderType getType() { + return TYPE; + } + + @Override + public int getViewDistance() { + return 512; + } + + @Override + public AABB getRenderBoundingBox(WorkableElectricMultiblockMachine multi) { + return new AABB(multi.getPos()).inflate(getViewDistance(), 256, getViewDistance()); + } + + @Override + public boolean shouldRenderOffScreen(WorkableElectricMultiblockMachine machine) { + return true; + } + + @Override + public void render(WorkableElectricMultiblockMachine machine, float partialTick, PoseStack poseStack, + MultiBufferSource buffer, int packedLight, int packedOverlay) { + if (!machine.isFormed()) { + return; + } + float totalTick = (Minecraft.getInstance().player.tickCount + partialTick); + + poseStack.pushPose(); + + // Position the pillar 11 blocks in front of the controller + Direction front = machine.getFrontFacing(); + Direction upwards = machine.getUpwardsFacing(); + boolean flipped = machine.isFlipped(); + Direction frontDir = RelativeDirection.FRONT.getRelative(front, upwards, flipped); + Direction.Axis leftAxis = RelativeDirection.LEFT.getRelative(front, upwards, flipped).getAxis(); + + float x0ffset = 0, y0ffset = 0.5f, z0ffset = 0; + + // Calculate offset 11 blocks in front of controller + Vec3i frontNormal = frontDir.getNormal(); + for (Direction.Axis axis : Direction.Axis.VALUES) { + int frontOffset = frontNormal.get(axis); + float offset = frontOffset * 10.5f; + switch (axis) { + case X -> x0ffset = offset; + case Y -> y0ffset = 0.5f; + case Z -> z0ffset = offset; + } + } + + poseStack.translate( + x0ffset + (leftAxis == Direction.Axis.X ? 0.5f : 0.0f), + y0ffset, + z0ffset + (leftAxis == Direction.Axis.Z ? 0.5f : 0.0f)); + + // Render the massive woven cable pillar + renderWovenCablePillar(poseStack, buffer, totalTick, packedLight, packedOverlay); + + poseStack.popPose(); + } + + private static void renderWireDodecahedron(PoseStack poseStack, MultiBufferSource buffer, + float scale, int sidesColorRGBA, float alpha) { + float r = ((sidesColorRGBA >> 16) & 0xFF) / 255f; + float g = ((sidesColorRGBA >> 8) & 0xFF) / 255f; + float b = ((sidesColorRGBA) & 0xFF) / 255f; + + final float phi = (1f + (float) Math.sqrt(5.0)) * 0.5f; + final float inv = 1f / phi; + + // build vertex list! Wahoo! + final float[][] V = new float[20][3]; + int idx = 0; + for (int sx = -1; sx <= 1; sx += 2) + for (int sy = -1; sy <= 1; sy += 2) + for (int sz = -1; sz <= 1; sz += 2) + V[idx++] = new float[] { sx, sy, sz }; + + for (int s1 = -1; s1 <= 1; s1 += 2) + for (int s2 = -1; s2 <= 1; s2 += 2) { + V[idx++] = new float[] { 0f, s1 * inv, s2 * phi }; + V[idx++] = new float[] { s1 * inv, s2 * phi, 0f }; + V[idx++] = new float[] { s1 * phi, 0f, s2 * inv }; + } + + for (int i = 0; i < 20; i++) { + V[i][0] *= scale; + V[i][1] *= scale; + V[i][2] *= scale; + } + + float edge2 = Float.POSITIVE_INFINITY; + for (int i = 0; i < 20; i++) + for (int j = i + 1; j < 20; j++) { + float dx = V[i][0] - V[j][0], dy = V[i][1] - V[j][1], dz = V[i][2] - V[j][2]; + float d2 = dx * dx + dy * dy + dz * dz; + if (d2 > 1e-6f && d2 < edge2) edge2 = d2; + } + final float eps = edge2 * 1.0015f; + + var mat = poseStack.last().pose(); + var nrm = poseStack.last().normal(); + VertexConsumer vc = buffer.getBuffer(net.minecraft.client.renderer.RenderType.lines()); + + for (int i = 0; i < 20; i++) + for (int j = i + 1; j < 20; j++) { + float dx = V[i][0] - V[j][0], dy = V[i][1] - V[j][1], dz = V[i][2] - V[j][2]; + float d2 = dx * dx + dy * dy + dz * dz; + if (d2 <= eps) { + float len = (float) Math.sqrt(d2); + float nx = dx / len, ny = dy / len, nz = dz / len; + + vc.vertex(mat, V[i][0], V[i][1], V[i][2]) + .color(r, g, b, alpha).normal(nrm, nx, ny, nz).endVertex(); + vc.vertex(mat, V[j][0], V[j][1], V[j][2]) + .color(r, g, b, alpha).normal(nrm, nx, ny, nz).endVertex(); + } + } + } + + private static void renderWireDodecahedronThick(PoseStack poseStack, MultiBufferSource buffer, + float scale, float thickness, + float r, float g, float b, float a) { + final float phi = (1f + (float) Math.sqrt(5.0)) * 0.5f; + final float inv = 1f / phi; + + final float[][] V = new float[20][3]; + int idx = 0; + for (int sx = -1; sx <= 1; sx += 2) + for (int sy = -1; sy <= 1; sy += 2) + for (int sz = -1; sz <= 1; sz += 2) + V[idx++] = new float[] { sx, sy, sz }; + + for (int s1 = -1; s1 <= 1; s1 += 2) + for (int s2 = -1; s2 <= 1; s2 += 2) { + V[idx++] = new float[] { 0f, s1 * inv, s2 * phi }; + V[idx++] = new float[] { s1 * inv, s2 * phi, 0f }; + V[idx++] = new float[] { s1 * phi, 0f, s2 * inv }; + } + + for (int i = 0; i < 20; i++) { + V[i][0] *= scale; + V[i][1] *= scale; + V[i][2] *= scale; + } + + HashSet edges = new java.util.HashSet<>(64); + for (int i = 0; i < 20; i++) { + int[] ns = getInts(V, i); + for (int j : ns) { + int low = Math.min(i, j); + int high = Math.max(i, j); + long key = ((long) low << 32) | (high & 0xFFFFFFFFL); + edges.add(key); + } + } + VertexConsumer vertexConsumer = buffer.getBuffer(GTRenderTypes.getLightRing()); + final Matrix4f mat = poseStack.last().pose(); + + var cam = Minecraft.getInstance().gameRenderer.getMainCamera(); + var v3 = cam.getLookVector(); + float vx = (float) v3.x, vy = (float) v3.y, vz = (float) v3.z; + float vlen = (float) Math.sqrt(vx * vx + vy * vy + vz * vz); + if (vlen > 0f) { + vx /= vlen; + vy /= vlen; + vz /= vlen; + } + + final float EPS = 1e-6f; + for (long key : edges) { + vertexConsumer = buffer.getBuffer(GTRenderTypes.getLightRing()); + int i0 = (int) (key >>> 32); + int i1 = (int) (key & 0xFFFFFFFFL); + + float x0 = V[i0][0], y0 = V[i0][1], z0 = V[i0][2]; + float x1 = V[i1][0], y1 = V[i1][1], z1 = V[i1][2]; + + float dx = x1 - x0, dy = y1 - y0, dz = z1 - z0; + float L2 = dx * dx + dy * dy + dz * dz; + if (L2 < EPS) continue; + float L = (float) Math.sqrt(L2); + float ex = dx / L, ey = dy / L, ez = dz / L; + float nx = ey * vz - ez * vy; + float ny = ez * vx - ex * vz; + float nz = ex * vy - ey * vx; + float n2 = nx * nx + ny * ny + nz * nz; + if (n2 < EPS) { + float upx = 0f, upy = 1f, upz = 0f; + nx = ey * upz - ez * upy; + ny = ez * upx - ex * upz; + nz = ex * upy - ey * upx; + n2 = nx * nx + ny * ny + nz * nz; + if (n2 < EPS) { + nx = 1f; + ny = 0f; + nz = 0f; + n2 = 1f; + } + } + float s = (0.5f * thickness) / (float) Math.sqrt(n2); + float ox = nx * s, oy = ny * s, oz = nz * s; + float ax = x0 - ox, ay = y0 - oy, az = z0 - oz; + float bx = x0 + ox, by = y0 + oy, bz = z0 + oz; + float cx = x1 + ox, cy = y1 + oy, cz = z1 + oz; + float dxq = x1 - ox, dyq = y1 - oy, dzq = z1 - oz; + + // Tri 1: A,B,C + vertexConsumer.vertex(mat, ax, ay, az).color(r, g, b, a).endVertex(); + vertexConsumer.vertex(mat, bx, by, bz).color(r, g, b, a).endVertex(); + vertexConsumer.vertex(mat, cx, cy, cz).color(r, g, b, a).endVertex(); + + // Tri 2: A,C,D + vertexConsumer.vertex(mat, ax, ay, az).color(r, g, b, a).endVertex(); + vertexConsumer.vertex(mat, cx, cy, cz).color(r, g, b, a).endVertex(); + vertexConsumer.vertex(mat, dxq, dyq, dzq).color(r, g, b, a).endVertex(); + } + } + + private static int @NotNull [] getInts(float[][] V, int i) { + int n1 = -1, n2 = -1, n3 = -1; + float d1 = Float.POSITIVE_INFINITY, d2 = Float.POSITIVE_INFINITY, d3 = Float.POSITIVE_INFINITY; + + float xi = V[i][0], yi = V[i][1], zi = V[i][2]; + for (int j = 0; j < 20; j++) if (j != i) { + float dx = xi - V[j][0], dy = yi - V[j][1], dz = zi - V[j][2]; + float d = dx * dx + dy * dy + dz * dz; + if (d < d1) { + d3 = d2; + n3 = n2; + d2 = d1; + n2 = n1; + d1 = d; + n1 = j; + } else if (d < d2) { + d3 = d2; + n3 = n2; + d2 = d; + n2 = j; + } else if (d < d3) { + d3 = d; + n3 = j; + } + } + int[] ns = { n1, n2, n3 }; + return ns; + } + + public static void renderSolidSphere(PoseStack poseStack, MultiBufferSource buffer, + float cx, float cy, float cz, + float radius, int slices, int stacks, + float r, float g, float b, float a) { + Matrix4f mat = poseStack.last().pose(); + VertexConsumer vc = buffer.getBuffer(GTRenderTypes.getLightRing()); + + float dPhi = (float) (Mth.TWO_PI / Math.max(3, slices)); + float dTheta = (float) (Math.PI / Math.max(2, stacks)); + + for (int i = 0; i < stacks; i++) { + float th0 = i * dTheta; + float th1 = (i + 1) * dTheta; + float sin0 = Mth.sin(th0), cos0 = Mth.cos(th0); + float sin1 = Mth.sin(th1), cos1 = Mth.cos(th1); + + // one triangle strip per latitude band; <= closes seam + for (int j = 0; j <= slices; j++) { + float ph = j * dPhi; + float cosp = Mth.cos(ph), sinp = Mth.sin(ph); + + // band top (th0) + float x0 = cx + radius * sin0 * cosp; + float y0 = cy + radius * cos0; + float z0 = cz + radius * sin0 * sinp; + + // band bottom (th1) + float x1 = cx + radius * sin1 * cosp; + float y1 = cy + radius * cos1; + float z1 = cz + radius * sin1 * sinp; + + // order chosen for typical backface cull; swap if it looks inside-out + vc.vertex(mat, x0, y0, z0).color(r, g, b, a).endVertex(); + vc.vertex(mat, x1, y1, z1).color(r, g, b, a).endVertex(); + } + } + } + + @OnlyIn(Dist.CLIENT) + public void renderBloodCube(PoseStack poseStack, MultiBufferSource bufferSource, float totalTick) { + poseStack.pushPose(); + // rotate around center + Quaternionf rot = new Quaternionf() + .rotateXYZ(Mth.sin(totalTick / 20), + Mth.sin(totalTick / 30), + Mth.cos(Mth.HALF_PI + totalTick / 60)) + .rotateXYZ(55f * Mth.DEG_TO_RAD, 30f * Mth.DEG_TO_RAD, 0); + poseStack.mulPose(rot); + + // draw cube quads + var consumer = bufferSource.getBuffer(Sheets.translucentCullBlockSheet()); + RenderBufferHelper.renderCube(consumer, poseStack.last(), 0xffffffff, + LightTexture.FULL_BRIGHT, bloodCubeSprite, + -1, -1, -1, 1, 1, 1); + + poseStack.popPose(); + } + + @OnlyIn(Dist.CLIENT) + private void renderWovenCablePillar(PoseStack poseStack, MultiBufferSource buffer, + float totalTick, int packedLight, int packedOverlay) { + int numStrands = 4; + float pillarHeight = 2048f; + float coreRadius = 3.0f; + float strandRadius = 0.85f; + float helixRadius = 3.8f; + float windingSpeed = 0.03f; + + float animTime = -totalTick * 0.10f; + + // Render central core column first (back to front for translucency) + VertexConsumer coreConsumer = buffer.getBuffer(GTRenderTypes.getLightRing()); + renderCoreColumn(poseStack, coreConsumer, pillarHeight, coreRadius, totalTick, packedLight, packedOverlay); + + // Render counter-rotating helix layers for structural stability look + // First layer - clockwise spiral + for (int strand = 0; strand < numStrands; strand++) { + VertexConsumer strandConsumer = buffer.getBuffer(GTRenderTypes.getLightRing()); + float strandAngleOffset = (strand / (float) numStrands) * Mth.TWO_PI; + renderBraidedStrand(poseStack, strandConsumer, pillarHeight, helixRadius, strandRadius, + strandAngleOffset, animTime, windingSpeed, packedLight, packedOverlay, true); + } + + // Second layer - counter-clockwise spiral (creates woven/braided effect) + for (int strand = 0; strand < numStrands; strand++) { + VertexConsumer strandConsumer = buffer.getBuffer(GTRenderTypes.getLightRing()); + float strandAngleOffset = (strand / (float) numStrands) * Mth.TWO_PI + (Mth.PI / numStrands); + renderBraidedStrand(poseStack, strandConsumer, pillarHeight, helixRadius * 0.95f, strandRadius * 0.8f, + strandAngleOffset, animTime, -windingSpeed, packedLight, packedOverlay, false); + } + } + + @OnlyIn(Dist.CLIENT) + private void renderBraidedStrand(PoseStack poseStack, VertexConsumer consumer, float height, + float helixRadius, float strandRadius, float angleOffset, + float animTime, float windingSpeed, int packedLight, int packedOverlay, + boolean isClockwise) { + int segments = 256; + float segmentHeight = height / segments; + + Matrix4f mat = poseStack.last().pose(); + + // Different colors for each layer to show the braiding + float r = isClockwise ? 0.1f : 0.15f; + float g = isClockwise ? 0.1f : 0.15f; + float b = isClockwise ? 0.1f : 0.15f; + + for (int i = 0; i < segments; i++) { + float y1 = i * segmentHeight; + float y2 = (i + 1) * segmentHeight; + + float angle1 = (y1 * windingSpeed + angleOffset + animTime) % Mth.TWO_PI; + float angle2 = (y2 * windingSpeed + angleOffset + animTime) % Mth.TWO_PI; + + float x1 = helixRadius * Mth.cos(angle1); + float z1 = helixRadius * Mth.sin(angle1); + float x2 = helixRadius * Mth.cos(angle2); + float z2 = helixRadius * Mth.sin(angle2); + + // Draw cylindrical tube segment + drawTubeSegment(mat, consumer, x1, y1, z1, x2, y2, z2, strandRadius, r, g, b, 1f); + } + } + + @OnlyIn(Dist.CLIENT) + private void renderCoreColumn(PoseStack poseStack, VertexConsumer consumer, float height, + float radius, float totalTick, int packedLight, int packedOverlay) { + int segments = 256; + float segmentHeight = height / segments; + + Matrix4f mat = poseStack.last().pose(); + + for (int i = 0; i < segments; i++) { + float y1 = i * segmentHeight; + float y2 = (i + 1) * segmentHeight; + + float glow = 0.7f + 0.3f * Mth.sin(totalTick * 0.05f + y1 * 0.1f); + + // Draw central glowing column + drawTubeSegment(mat, consumer, 0, y1, 0, 0, y2, 0, radius, + 0.3f * glow, 0.45f * glow, 0.6f * glow, 1f); + } + } + + @OnlyIn(Dist.CLIENT) + private void drawTubeSegment(Matrix4f mat, VertexConsumer consumer, + float x1, float y1, float z1, float x2, float y2, float z2, + float radius, float r, float g, float b, float a) { + int sides = 4; + float angleStep = Mth.TWO_PI / sides; + + for (int i = 0; i < sides; i++) { + float angle1 = i * angleStep; + float angle2 = (i + 1) * angleStep; + + float cos1 = Mth.cos(angle1); + float sin1 = Mth.sin(angle1); + float cos2 = Mth.cos(angle2); + float sin2 = Mth.sin(angle2); + + // First triangle of quad + consumer.vertex(mat, x1 + radius * cos1, y1, z1 + radius * sin1).color(r, g, b, a).endVertex(); + consumer.vertex(mat, x1 + radius * cos2, y1, z1 + radius * sin2).color(r, g, b, a).endVertex(); + consumer.vertex(mat, x2 + radius * cos2, y2, z2 + radius * sin2).color(r, g, b, a).endVertex(); + + // Second triangle of quad + consumer.vertex(mat, x1 + radius * cos1, y1, z1 + radius * sin1).color(r, g, b, a).endVertex(); + consumer.vertex(mat, x2 + radius * cos2, y2, z2 + radius * sin2).color(r, g, b, a).endVertex(); + consumer.vertex(mat, x2 + radius * cos1, y2, z2 + radius * sin1).color(r, g, b, a).endVertex(); + } + } + + @OnlyIn(Dist.CLIENT) + private void renderRings(Direction.Axis upAxis, float totalTick, PoseStack poseStack, MultiBufferSource buffer) { + VertexConsumer consumer = buffer.getBuffer(GTRenderTypes.getLightRing()); + + float xRot = totalTick / 20; + float zRot = Mth.HALF_PI + totalTick / 60; + float yRot = totalTick / 30; + float sinX = Mth.sin(xRot), cosX = Mth.cos(xRot); + float sinY = Mth.sin(yRot), cosY = Mth.cos(yRot); + float sinZ = Mth.sin(zRot), cosZ = Mth.cos(zRot); + + poseStack.pushPose(); + poseStack.mulPose(new Quaternionf().rotateXYZ(sinX, cosY, sinZ)); + RenderBufferHelper.renderRing(poseStack, consumer, + 0, 0, 0, + 2f, 0.1F, 10, 36, + 0.5F, 0, 0, 1, upAxis); + poseStack.popPose(); + + poseStack.pushPose(); + poseStack.mulPose(new Quaternionf().rotateXYZ(cosX, sinY, sinZ)); + RenderBufferHelper.renderRing(poseStack, consumer, + 0, 0, 0, + 1.8f, 0.1F, 10, 36, + 0.4F, 0f, 0, 1, upAxis); + poseStack.popPose(); + + poseStack.pushPose(); + poseStack.mulPose(new Quaternionf().rotateZ(cosZ)); + RenderBufferHelper.renderRing(poseStack, consumer, + 0, 0, 0, + 1.6f, 0.1F, 10, 36, + 0.6F, 0, 0, 1, upAxis); + poseStack.popPose(); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/client/renderer/machine/StellarIrisRender.java b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/machine/StellarIrisRender.java index a922e929e..133de3bdc 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/client/renderer/machine/StellarIrisRender.java +++ b/src/main/java/com/ghostipedia/cosmiccore/client/renderer/machine/StellarIrisRender.java @@ -40,8 +40,12 @@ public class StellarIrisRender extends DynamicRender { public static final StellarIrisRender INSTANCE = new StellarIrisRender(); - public String hexColor = "#a262e3"; + // Default star color - will be overridden by machine's customStarColor if set + public static final String DEFAULT_STAR_COLOR = "#FFCC44"; // Golden yellow public static final Codec CODEC = Codec.unit(StellarIrisRender.INSTANCE); + + // Current hex color being used for rendering (set per-render from machine or default) + private String hexColor = DEFAULT_STAR_COLOR; public static final DynamicRenderType TYPE = new DynamicRenderType<>( StellarIrisRender.CODEC); @@ -65,7 +69,6 @@ private StellarIrisRender() { irisRingModel = event.getModels().get(IRIS_MODEL_RING); irisSmallRingModel = event.getModels().get(IRIS_MODEL_RING_WHITE); - // Todo : Figure out why these don't render the ball. starCoreModel = event.getModels().get(STAR_MODEL_CORE); outerStarSphereModel = event.getModels().get(STAR_MODEL_OUTER); innerStarSphereModel = event.getModels().get(STAR_MODEL_INNER); @@ -77,43 +80,39 @@ public DynamicRenderType getType() { return TYPE; } + /** + * Gets the star color from the machine, converting from int to hex string. + * Returns DEFAULT_STAR_COLOR if machine has no custom color set (-1). + */ + private String getStarColorFromMachine(IrisMultiblockMachine machine) { + int customColor = machine.getCustomStarColor(); + if (customColor == -1) { + return DEFAULT_STAR_COLOR; + } + // Convert int color (0xRRGGBB) to hex string (#RRGGBB) + return String.format("#%06X", customColor & 0xFFFFFF); + } + @OnlyIn(Dist.CLIENT) @Override public void render(IrisMultiblockMachine machine, float partialTick, PoseStack poseStack, MultiBufferSource buffer, int packedLight, int packedOverlay) { - if (!machine.isFormed()) return; + // if (!machine.isFormed()) return; + + // Set the star color from the machine's custom color (or default) + this.hexColor = getStarColorFromMachine(machine); float totalTick = (Minecraft.getInstance().player.tickCount + partialTick); VertexConsumer consumer = buffer.getBuffer(Sheets.translucentCullBlockSheet()); poseStack.pushPose(); - Direction front = machine.getFrontFacing(); - Direction upwards = machine.getUpwardsFacing(); - - float x0ffset = 0, y0ffset = -2.5f, z0ffset = 0; - - if (front.getAxis() == Direction.Axis.X) { - if (front.getAxisDirection() == Direction.AxisDirection.POSITIVE) { - x0ffset = -45.5f; - z0ffset = 0.5f; - } else { - x0ffset = 46.5f; - z0ffset = 0.5f; - } - } - - if (front.getAxis() == Direction.Axis.Z) { - if (front.getAxisDirection() == Direction.AxisDirection.POSITIVE) { - z0ffset = -45.55f; - x0ffset = 0.5f; - } else { - z0ffset = 46.5f; - x0ffset = 0.5f; - } - } + // Controller is now on top spire - star renders 23 blocks below controller + // Center offset: controller is at top, star center is 23 blocks down + float x0ffset = 0.5f; + float y0ffset = -23f; + float z0ffset = 0.5f; poseStack.translate(x0ffset, y0ffset, z0ffset); - // poseStack.mulPose(new Quaternionf().rotateAxis(totalTick * Mth.TWO_PI / 80, 0, 1, 0)); poseStack.scale(7.0f, 7, 7); if (machine.getStage() == IrisMultiblockMachine.Stage.STAR) { @@ -124,18 +123,21 @@ public void render(IrisMultiblockMachine machine, float partialTick, PoseStack p poseStack.popPose(); } else if (machine.getStage() == IrisMultiblockMachine.Stage.GROWING) { + poseStack.pushPose(); poseStack.mulPose(new Quaternionf().rotateAxis(-totalTick * Mth.TWO_PI / 80f, 0, 1, 0)); renderStar(poseStack, consumer, totalTick, packedLight, packedOverlay); renderStarInsides(poseStack, consumer, totalTick, packedLight, packedOverlay); renderStarShell(poseStack, consumer, totalTick, packedLight, packedOverlay); + poseStack.popPose(); + renderMultiStarSystemRandomized( machine, poseStack, buffer, totalTick, packedLight, packedOverlay, - /* count */ 5, - /* meanRadius */ 4.5f, - /* radiusJitter */ 0.75f, - /* periodSec */ 2000f, - /* starMin */ 0.05f, /* starMax */ 0.6f, - /* spinSelf */ true); + 10, + 4.5f, + 0.75f, + 5f, + 0.12f, 0.45f, + true); poseStack.popPose(); } else if (machine.getStage() == IrisMultiblockMachine.Stage.SUPERSTAR) { poseStack.scale(2, 2, 2); @@ -146,7 +148,7 @@ public void render(IrisMultiblockMachine machine, float partialTick, PoseStack p } else if (machine.getStage() == IrisMultiblockMachine.Stage.BLACK_HOLE) { renderIris(poseStack, consumer, packedLight, packedOverlay); - renderRing(poseStack, consumer, packedLight, packedOverlay); + renderAccretionDisk(poseStack, consumer, totalTick, packedLight, packedOverlay); poseStack.popPose(); renderRingSmall(machine, poseStack, consumer, totalTick, packedLight, packedOverlay); @@ -163,7 +165,6 @@ public void render(IrisMultiblockMachine machine, float partialTick, PoseStack p BlockPos pos = machine.getPos(); if (!irisFadeStartSec.containsKey(pos)) { - // First frame: draw fully visible and start fade timer PoseStack.Pose pose = poseStack.last(); java.util.List quads = irisCoreModel.getQuads(null, null, random, ModelData.EMPTY, null); for (BakedQuad quad : quads) { @@ -176,10 +177,7 @@ public void render(IrisMultiblockMachine machine, float partialTick, PoseStack p poseStack, consumer, packedLight, packedOverlay, pos, partialTick); if (vanished) { - machine.setStage(IrisMultiblockMachine.Stage.STAR); // TODO: This does NOT set the stage of the - // multi, it sets the render stage to star which - // desyncs it figure out how to set the MACHINES - // STATE and set BOTH to EMPTY! ! ! + machine.setStage(IrisMultiblockMachine.Stage.STAR); } } poseStack.popPose(); @@ -212,11 +210,9 @@ public void renderIris(PoseStack poseStack, VertexConsumer consumer, int packedL } } - // Start time per machine position, in seconds private final java.util.Map irisFadeStartSec = new java.util.HashMap<>(); private static final float IRIS_FADE_DURATION_SEC = 10f; - // Call this once to begin the fading shrink private void startIrisFade(BlockPos pos, float partialTick) { float tSec = (Minecraft.getInstance().player.tickCount + partialTick) / 20.0f; irisFadeStartSec.put(pos, tSec); @@ -258,7 +254,7 @@ private boolean renderIrisFading(PoseStack poseStack, VertexConsumer consumer, float alpha = (1f - k) * (1f - k); poseStack.pushPose(); - poseStack.scale(scale, scale, scale); // shrink while fading + poseStack.scale(scale, scale, scale); PoseStack.Pose pose = poseStack.last(); java.util.List quads = irisCoreModel.getQuads(null, null, random, ModelData.EMPTY, null); for (BakedQuad quad : quads) { @@ -282,34 +278,41 @@ public void renderRing(PoseStack poseStack, VertexConsumer consumer, int packedL poseStack.popPose(); } + /** + * Renders the main accretion disk with rotation around the black hole. + */ @OnlyIn(Dist.CLIENT) - public void renderRingSmall(IrisMultiblockMachine machine, PoseStack poseStack, VertexConsumer consumer, - float totalTick, int packedLight, int packedOverlay) { + public void renderAccretionDisk(PoseStack poseStack, VertexConsumer consumer, + float totalTick, int packedLight, int packedOverlay) { poseStack.pushPose(); - Direction front = machine.getFrontFacing(); - Direction upwards = machine.getUpwardsFacing(); - float x0ffset = 0, y0ffset = -2.3f, z0ffset = 0; + // Offset slightly down to avoid z-fighting with the fast spinning ring + poseStack.translate(0, -0.05f, 0); - if (front.getAxis() == Direction.Axis.X) { - if (front.getAxisDirection() == Direction.AxisDirection.POSITIVE) { - x0ffset = -46.5f; - z0ffset = -0.5f; - } else { - x0ffset = 46.5f; - z0ffset = 0.5f; - } - } + // Slow rotation around Y axis (orbital motion) + float rotationSpeed = totalTick * Mth.TWO_PI / 200f; // Full rotation every 10 seconds + poseStack.mulPose(new Quaternionf().rotateY(rotationSpeed)); - if (front.getAxis() == Direction.Axis.Z) { - if (front.getAxisDirection() == Direction.AxisDirection.POSITIVE) { - z0ffset = -46.5f; - x0ffset = -0.5f; - } else { - z0ffset = 46.5f; - x0ffset = 0.5f; - } + poseStack.scale(2.0f, 2.0f, 2.0f); + + List quads = irisRingModel.getQuads(null, null, random, ModelData.EMPTY, null); + for (BakedQuad quad : quads) { + consumer.putBulkData(poseStack.last(), quad, 1f, 1f, 1f, packedLight, packedOverlay); } + + poseStack.popPose(); + } + + @OnlyIn(Dist.CLIENT) + public void renderRingSmall(IrisMultiblockMachine machine, PoseStack poseStack, VertexConsumer consumer, + float totalTick, int packedLight, int packedOverlay) { + poseStack.pushPose(); + + // Controller is now on top spire - ring renders at star center (23 blocks below controller) + float x0ffset = 0.5f; + float y0ffset = -23f; + float z0ffset = 0.5f; + poseStack.translate(x0ffset, y0ffset, z0ffset); poseStack.mulPose(new Quaternionf().rotateAxis(totalTick * Mth.TWO_PI / 20, 0, 1, 0)); poseStack.scale(13.0f, 13.0f, 13.0f); @@ -364,130 +367,78 @@ private void renderStarAt(PoseStack poseStack, VertexConsumer consumer, private void renderMultiStarSystemRandomized(IrisMultiblockMachine machine, PoseStack poseStack, MultiBufferSource buffer, float totalTick, int packedLight, int packedOverlay, - int count, // 1..6 - float meanRadius, // base ring radius (in your local space) - float radiusJitter, // 0..1 fraction (e.g. 0.35f) - float periodSec, // orbit period in seconds - float starMin, float starMax, // visual size range (e.g. 0.10..0.40) + int count, + float meanRadius, + float radiusJitter, + float periodSec, + float starMin, float starMax, boolean spinSelf) { if (count < 1) return; - if (count > 6) count = 6; + if (count > 5) count = 5; - // --- params controlling look/feel --- - final float holeFrac = 0.15f; // keep at least this fraction of meanRadius clear in the center - final float eccMax = 0.22f; // max eccentricity per-star (subtle ellipse) - final float tiltMax = 0.20f; // max tilt per-star (radians) ~ 11.5° - // ------------------------------------ - - // Deterministic RNG per machine + count long seed = hashPos(machine.getPos()) ^ (count * 0x9E3779B97F4A7C15L); long[] S = new long[] { seed }; - // Masses ~ 0.6..2.0, then normalize → used for size and COM - float[] m = new float[count]; - float mSum = 0f; - for (int i = 0; i < count; i++) { - m[i] = 0.6f + 1.4f * rand01(S); - mSum += m[i]; - } - for (int i = 0; i < count; i++) m[i] /= mSum; + String[] palette = new String[] { "#ffd28a", "#9ad0ff", "#ff9fb0", "#fff6a4", "#b4ffea", "#d2a0ff" }; + VertexConsumer consumer = buffer.getBuffer(Sheets.translucentCullBlockSheet()); + + float innerRadius = 2.5f; + float outerRadius = meanRadius * 0.40f; - // Per-star random base angle (0..2π), speed jitter, radius with jitter, eccentricity, and orbit orientation - float[] baseAng = new float[count]; - float[] wMul = new float[count]; - float[] rad = new float[count]; - float[] ecc = new float[count]; - float[] yaw = new float[count]; // in-plane orientation - float[] tiltX = new float[count]; // small 3D tilt - float[] tiltZ = new float[count]; + float maxStarSize = starMax; + float minSpacing = maxStarSize * 1.8f; - final float minR = Math.max(0.05f, meanRadius * holeFrac); // enforce central hole + float totalSpace = outerRadius - innerRadius; + float neededSpace = minSpacing * (count - 1); + float radiusStep = Math.max(minSpacing, totalSpace / Math.max(1, count - 1)); + + float tSec = totalTick / 20.0f; for (int i = 0; i < count; i++) { - baseAng[i] = rand01(S) * Mth.TWO_PI; // random starting phase - wMul[i] = 0.92f + 0.16f * rand01(S); // ~0.92..1.08 angular speed jitter + float radius = innerRadius + (i * radiusStep); - float rJ = (rand01(S) * 2f - 1f) * radiusJitter; // [-jitter, +jitter] - rad[i] = Math.max(minR, meanRadius * (1f + rJ)); + float orbitTilt = (rand01(S) - 0.5f) * Mth.PI * 0.8f; + float orbitRotation = rand01(S) * Mth.TWO_PI; - ecc[i] = eccMax * rand01(S); // 0..eccMax (mild ellipse) - yaw[i] = rand01(S) * Mth.TWO_PI; // random ellipse orientation in-plane - tiltX[i] = (rand01(S) - 0.5f) * 2f * tiltMax; // small 3D tilt - tiltZ[i] = (rand01(S) - 0.5f) * 2f * tiltMax; - } + float direction = rand01(S) > 0.5f ? 1f : -1f; - // Time → base phase - float tSec = totalTick / 20.0f; - float baseTheta = (tSec / periodSec) * Mth.TWO_PI; + float speedMult = (0.3f + rand01(S) * 2.2f) * direction; - // Compute 3D positions - float[] px = new float[count]; - float[] py = new float[count]; - float[] pz = new float[count]; + float startAngle = rand01(S) * Mth.TWO_PI; - for (int i = 0; i < count; i++) { - // per-star phase - float theta = baseTheta * wMul[i] + baseAng[i]; - - // ellipse axes - float a = Math.max(minR, rad[i] * (1f + 0.5f * ecc[i])); // major - float b = Math.max(minR, rad[i] * (1f - 0.5f * ecc[i])); // minor - - // base ellipse vector in local (x,z) - float ct = Mth.cos(theta), st = Mth.sin(theta); - float ex = a * ct; - float ez = b * st; - - // rotate ellipse within XZ by yaw - float cy = Mth.cos(yaw[i]), sy = Mth.sin(yaw[i]); - float rx = ex * cy - ez * sy; - float rz = ex * sy + ez * cy; - float ry = 0f; - - // apply small tilts to give 3D orbits (X then Z) - float cx = Mth.cos(tiltX[i]), sx = Mth.sin(tiltX[i]); - float cz = Mth.cos(tiltZ[i]), sz = Mth.sin(tiltZ[i]); - - // rotate around X - float ry1 = ry * cx - rz * sx; - float rz1 = ry * sx + rz * cx; - - // rotate around Z - float rx2 = rx * cz - ry1 * sz; - float ry2 = rx * sz + ry1 * cz; - - px[i] = rx2; - py[i] = ry2; - pz[i] = rz1; - } + float orbitalPeriod = periodSec * (1.0f / Math.abs(speedMult)); + float angle = startAngle + (tSec / orbitalPeriod) * Mth.TWO_PI * Math.signum(speedMult); - // Center-of-mass correction in 3D: keep barycenter pinned at origin - float comX = 0f, comY = 0f, comZ = 0f; - for (int i = 0; i < count; i++) { - comX += m[i] * px[i]; - comY += m[i] * py[i]; - comZ += m[i] * pz[i]; - } - for (int i = 0; i < count; i++) { - px[i] -= comX; - py[i] -= comY; - pz[i] -= comZ; - } + float orbitX = radius * Mth.cos(angle); + float orbitY = 0f; + float orbitZ = radius * Mth.sin(angle); - // Colors (fallback palette) - String[] palette = new String[] { "#ffd28a", "#9ad0ff", "#ff9fb0", "#fff6a4", "#b4ffea", "#d2a0ff" }; - VertexConsumer consumer = buffer.getBuffer(Sheets.translucentCullBlockSheet()); + float cosTilt = Mth.cos(orbitTilt); + float sinTilt = Mth.sin(orbitTilt); + float rotX1 = orbitX; + float rotY1 = orbitY * cosTilt - orbitZ * sinTilt; + float rotZ1 = orbitY * sinTilt + orbitZ * cosTilt; + + float cosRot = Mth.cos(orbitRotation); + float sinRot = Mth.sin(orbitRotation); + float finalX = rotX1 * cosRot + rotZ1 * sinRot; + float finalY = rotY1; + float finalZ = -rotX1 * sinRot + rotZ1 * cosRot; + + float sizeRand = rand01(S); + float starSize = Mth.lerp(sizeRand, starMin, starMax); + starSize = Mth.clamp(starSize, starMin, starMax); - // Render stars with size ~ m^(1/3), mapped into [starMin, starMax] - for (int i = 0; i < count; i++) { - float size01 = (float) Math.pow(m[i], 1f / 3f); - float sizeMul = Mth.lerp(size01, starMin, starMax); String color = palette[i % palette.length]; + float spinSpeed = 0.5f + rand01(S) * 2.0f; + float spinOffset = rand01(S) * 1000f; + float uniqueSpinTick = spinSelf ? (totalTick * spinSpeed + spinOffset) : 0f; + poseStack.pushPose(); - poseStack.translate(px[i], py[i], pz[i]); - renderStarAt(poseStack, consumer, totalTick, packedLight, packedOverlay, - sizeMul, color, /* spinSelf= */spinSelf); + poseStack.translate(finalX, finalY, finalZ); + renderStarAt(poseStack, consumer, uniqueSpinTick, packedLight, packedOverlay, + starSize, color, spinSelf); poseStack.popPose(); } } @@ -536,7 +487,6 @@ public void renderStarShell(PoseStack poseStack, VertexConsumer consumer, private long pulseSeed = 0L; private boolean seedInit = false; - // Deterministic hash (like xorshift) to make a seed from block pos private static long hashPos(BlockPos p) { long x = p.getX(), y = p.getY(), z = p.getZ(); long h = x * 0x9E3779B97F4A7C15L ^ (y + 0xC2B2AE3D27D4EB4FL) ^ (z * 0x94D049BB133111EBL); @@ -548,7 +498,6 @@ private static long hashPos(BlockPos p) { return h; } - // Simple LCG for reproducible floats [0,1) private static final long A = 6364136223846793005L, C = 1442695040888963407L; private static long lcg(long s) { @@ -560,7 +509,6 @@ private static float rand01(long[] s) { return ((s[0] >>> 8) & 0xFFFFFF) / (float) (1 << 24); } - // Exponential RNG with mean = 1/lambda (Poisson inter-arrival); clamp U to avoid log(0) private static float expSample(long[] s, float lambda) { float u = Math.max(1e-6f, rand01(s)); return (float) (-Math.log(u) / lambda); @@ -569,33 +517,28 @@ private static float expSample(long[] s, float lambda) { @OnlyIn(Dist.CLIENT) private float erraticPulseEffect(float min, float max, float partial, float intensity, IrisMultiblockMachine machine) { - // absolute time in seconds float tSec = (Minecraft.getInstance().player.tickCount + partial) / 20.0f; - // seed once per machine so multiple instances don't sync - realistically you never have more than one if (!seedInit) { pulseSeed = hashPos(machine.getPos()); seedInit = true; - nextSpikeT = tSec + 0.2f; // first spike soon-ish + nextSpikeT = tSec + 0.2f; } - // real dt (handles multi-pass calls where time didn't advance) float dt; if (Float.isNaN(prevTSec)) dt = 0f; else { dt = tSec - prevTSec; if (dt < 0f) dt = 0f; - if (dt > 0.25f) dt = 0.25f; // clamp long stalls + if (dt > 0.25f) dt = 0.25f; } prevTSec = tSec; - // Map intensity -> spike rate and strength intensity = Mth.clamp(intensity, 0f, 1f); float rateHz = Mth.lerp(intensity, 0.3f, 3.0f); float gain = Mth.lerp(intensity, 0.25f, 0.9f); float decayTau = Mth.lerp(intensity, 0.60f, 0.20f); - // Drive Poisson spike train if (dt > 0f) { long[] s = new long[] { pulseSeed }; while (tSec >= nextSpikeT) { @@ -606,20 +549,17 @@ private float erraticPulseEffect(float min, float max, float partial, float inte pulseSeed = s[0]; } - // exponential decay of envelope float decay = (float) Math.exp(-dt / decayTau); spikeEnv *= decay; } float w = tSec; float jitter = 0.04f * (float) Math.sin(7.23 * w + 0.3) + 0.03f * (float) Math.sin(11.1 * w + 1.7) + - 0.02f * (float) Math.sin(4.7 * w * w + 0.5); // slight chaos + 0.02f * (float) Math.sin(4.7 * w * w + 0.5); jitter = Mth.clamp(jitter, -0.15f, 0.15f); - // Combine: base floor + spikes + jitter, then clamp 0..1 float pulse01 = Mth.clamp(0.12f + spikeEnv + jitter, 0f, 1f); - // Map to [min,max] return Mth.lerp(pulse01, min, max); } @@ -646,7 +586,6 @@ private void renderRings(Direction.Axis upAxis, float totalTick, PoseStack poseS 0, 0, 0, 6.3f, 0.3F, 10, 36, 0F, 0, 0F, 1, upAxis); - poseStack.scale(3, 3, 3); poseStack.popPose(); poseStack.pushPose(); @@ -689,13 +628,13 @@ private void renderRingsSecondary(Direction.Axis upAxis, float totalTick, PoseSt consumer = buffer.getBuffer(GTRenderTypes.getLightRing()); RenderBufferHelper.renderRing(poseStack, consumer, 0, 0, 0, - 5.10f + 0.25f * Mth.sin(totalTick * 0.9f + 0.3f), // radius breath - 0.26F + 0.06F * Mth.sin(totalTick * 0.291f + 1.7f), // thickness breath + 5.10f + 0.25f * Mth.sin(totalTick * 0.9f + 0.3f), + 0.26F + 0.06F * Mth.sin(totalTick * 0.291f + 1.7f), 12, 40, 0F, 0, 0F, - 1, // alpha breath + 1, upAxis); - poseStack.scale(scale * 2.5f, scale * 3.2f, scale * 2.7f); // tie scale to osc (ring 1) + poseStack.scale(scale * 2.5f, scale * 3.2f, scale * 2.7f); poseStack.popPose(); poseStack.pushPose(); @@ -703,7 +642,7 @@ private void renderRingsSecondary(Direction.Axis upAxis, float totalTick, PoseSt consumer = buffer.getBuffer(GTRenderTypes.getLightRing()); RenderBufferHelper.renderRing(poseStack, consumer, 0, 0, 0, - 4.55f + 0.30f * Mth.sin(totalTick * 0.377f + 0.9f), // different rhythm + 4.55f + 0.30f * Mth.sin(totalTick * 0.377f + 0.9f), 0.34F + 0.05F * Mth.sin(totalTick * 0.291f + 1.6f), 11, 30, 0F, 0, 0F, @@ -716,7 +655,7 @@ private void renderRingsSecondary(Direction.Axis upAxis, float totalTick, PoseSt consumer = buffer.getBuffer(GTRenderTypes.getLightRing()); RenderBufferHelper.renderRing(poseStack, consumer, 0, 0, 0, - 5.05f + 0.22f * Mth.sin(totalTick * 0.065f + 1.9f), // third rhythm + 5.05f + 0.22f * Mth.sin(totalTick * 0.065f + 1.9f), 0.28F + 0.05F * Mth.sin(totalTick * 0.15f + 0.2f), 13, 32, 0F, 0, 0F, diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/airControl/IOxygen.java b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/IOxygen.java new file mode 100644 index 000000000..a4c72fe69 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/IOxygen.java @@ -0,0 +1,19 @@ +package com.ghostipedia.cosmiccore.common.airControl; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; + +public interface IOxygen { + + long getOxygenTicks(ResourceKey dimension); + + void setOxygenTicks(ResourceKey dimension, long ticks); + + boolean isConsuming(ResourceKey dimension); + + void setConsuming(ResourceKey dimension, boolean consuming); + + double getRegenBuffer(ResourceKey dimension); + + void setRegenBuffer(ResourceKey dimension, double buffer); +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/airControl/IOxygenProvider.java b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/IOxygenProvider.java new file mode 100644 index 000000000..de3713313 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/IOxygenProvider.java @@ -0,0 +1,46 @@ +package com.ghostipedia.cosmiccore.common.airControl; + +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +/** + * Interface for items that can provide oxygen to the CosmicCore oxygen system. + * Implemented by space suit chestplates, oxygen tanks, etc. + */ +public interface IOxygenProvider { + + /** + * Check if this item can currently provide oxygen. + * + * @param stack The item stack + * @param player The player wearing/holding the item + * @return true if oxygen is available + */ + boolean hasOxygen(ItemStack stack, Player player); + + /** + * Consume oxygen from this provider. + * + * @param stack The item stack + * @param player The player + * @param amount Amount to consume (in millibuckets for fluid tanks, or ticks for other systems) + * @return Amount actually consumed + */ + long consumeOxygen(ItemStack stack, Player player, long amount); + + /** + * Get current oxygen amount. + * + * @param stack The item stack + * @return Current oxygen in millibuckets (or equivalent units) + */ + long getOxygenAmount(ItemStack stack); + + /** + * Get maximum oxygen capacity. + * + * @param stack The item stack + * @return Maximum capacity in millibuckets (or equivalent units) + */ + long getMaxOxygenCapacity(ItemStack stack); +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/airControl/IOxygenSupplyItem.java b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/IOxygenSupplyItem.java new file mode 100644 index 000000000..89ae160a1 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/IOxygenSupplyItem.java @@ -0,0 +1,13 @@ +package com.ghostipedia.cosmiccore.common.airControl; + +import net.minecraft.world.item.ItemStack; + +public interface IOxygenSupplyItem { + + /** + * Try to provide up to requestTicks of oxygen from this stack. + * + * @return ticks actually provided (0..requestTicks) + */ + int drainOxygenTicks(ItemStack stack, int requestTicks); +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenBudget.java b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenBudget.java new file mode 100644 index 000000000..6573d19d3 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenBudget.java @@ -0,0 +1,82 @@ +package com.ghostipedia.cosmiccore.common.airControl; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import java.util.HashMap; +import java.util.Map; + +public class OxygenBudget implements IOxygen { + + private final Map oxygenTicksByDimension = new HashMap<>(); + private final Map consumingByDimension = new HashMap<>(); + private final Map regenBufferByDimension = new HashMap<>(); + + @Override + public long getOxygenTicks(ResourceKey dimension) { + return oxygenTicksByDimension.getOrDefault(dimension.location(), -1L); + } + + @Override + public void setOxygenTicks(ResourceKey dimension, long ticks) { + oxygenTicksByDimension.put(dimension.location(), ticks); + } + + @Override + public boolean isConsuming(ResourceKey dimension) { + return consumingByDimension.getOrDefault(dimension.location(), false); + } + + @Override + public void setConsuming(ResourceKey dimension, boolean consuming) { + consumingByDimension.put(dimension.location(), consuming); + } + + @Override + public double getRegenBuffer(ResourceKey dimension) { + return regenBufferByDimension.getOrDefault(dimension.location(), 0.0); + } + + @Override + public void setRegenBuffer(ResourceKey dimension, double buffer) { + regenBufferByDimension.put(dimension.location(), buffer); + } + + // ---- NBT persistence ---- + + public CompoundTag saveTag() { + CompoundTag root = new CompoundTag(); + ListTag list = new ListTag(); + + for (var entry : oxygenTicksByDimension.entrySet()) { + ResourceLocation dim = entry.getKey(); + CompoundTag tag = new CompoundTag(); + tag.putString("dimension", dim.toString()); + tag.putLong("oxygenTicks", entry.getValue()); + tag.putBoolean("consuming", consumingByDimension.getOrDefault(dim, false)); + tag.putDouble("regenBuffer", regenBufferByDimension.getOrDefault(dim, 0.0)); + list.add(tag); + } + root.put("entries", list); + return root; + } + + public void loadTag(CompoundTag root) { + oxygenTicksByDimension.clear(); + consumingByDimension.clear(); + regenBufferByDimension.clear(); + + ListTag list = root.getList("entries", Tag.TAG_COMPOUND); + for (Tag element : list) { + CompoundTag tag = (CompoundTag) element; + ResourceLocation dim = new ResourceLocation(tag.getString("dimension")); + oxygenTicksByDimension.put(dim, tag.getLong("oxygenTicks")); + consumingByDimension.put(dim, tag.getBoolean("consuming")); + regenBufferByDimension.put(dim, tag.getDouble("regenBuffer")); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenBudgetCap.java b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenBudgetCap.java new file mode 100644 index 000000000..bd9ae1fa6 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenBudgetCap.java @@ -0,0 +1,66 @@ +package com.ghostipedia.cosmiccore.common.airControl; + +import com.ghostipedia.cosmiccore.CosmicCore; + +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.Entity; +import net.minecraftforge.common.capabilities.*; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import javax.annotation.Nullable; + +@Mod.EventBusSubscriber(modid = CosmicCore.MOD_ID) +public class OxygenBudgetCap { + + private OxygenBudgetCap() {} + + public static final Capability CAP = CapabilityManager.get(new CapabilityToken<>() {}); + + public static class Provider implements ICapabilityProvider, ICapabilitySerializable { + + private final OxygenBudget impl = new OxygenBudget(); + private final LazyOptional opt = LazyOptional.of(() -> impl); + + @Override + public LazyOptional getCapability(Capability cap, @Nullable Direction side) { + return cap == CAP ? opt.cast() : LazyOptional.empty(); + } + + @Override + public CompoundTag serializeNBT() { + return impl.saveTag(); + } + + @Override + public void deserializeNBT(CompoundTag nbt) { + impl.loadTag(nbt); + } + } + + @SubscribeEvent + public static void registerCaps(RegisterCapabilitiesEvent event) { + event.register(IOxygen.class); + } + + @SubscribeEvent + public static void attach(AttachCapabilitiesEvent event) { + if (event.getObject() instanceof net.minecraft.world.entity.player.Player) + event.addCapability(CosmicCore.id("oxygen"), new Provider()); + } + + @SubscribeEvent + public static void clone(PlayerEvent.Clone event) { + event.getOriginal().reviveCaps(); + event.getOriginal().getCapability(CAP).ifPresent(old -> event.getEntity().getCapability(CAP).ifPresent(now -> { + if (now instanceof OxygenBudget newCap && old instanceof OxygenBudget oldCap) { + newCap.loadTag(oldCap.saveTag()); + } + })); + event.getOriginal().invalidateCaps(); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenConfig.java b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenConfig.java new file mode 100644 index 000000000..9215ac39f --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenConfig.java @@ -0,0 +1,66 @@ +package com.ghostipedia.cosmiccore.common.airControl; + +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.DepthsBargain; + +import net.minecraft.world.entity.player.Player; + +/** + * Configuration constants for the oxygen system. + */ +public final class OxygenConfig { + + private OxygenConfig() {} + + // ------------------------------------------------------------------------- + // Oxygen Budget + // ------------------------------------------------------------------------- + + /** Base maximum oxygen capacity in ticks (90 seconds) */ + public static final long MAX_OXYGEN_TICKS = 20L * 90; + + /** + * Get the effective max oxygen capacity for a player. + * This accounts for bargains that modify capacity (e.g., Depths bargain). + */ + public static long getMaxOxygenTicks(Player player) { + float multiplier = DepthsBargain.getCapacityMultiplier(player); + return (long) (MAX_OXYGEN_TICKS * multiplier); + } + + /** Seconds remaining at which to show warnings */ + public static final int[] WARNING_SECONDS = { 60, 30, 15, 10, 5 }; + + // ------------------------------------------------------------------------- + // Tank Behavior + // ------------------------------------------------------------------------- + + /** Extra ticks tanks can top-up per game tick when protecting player */ + public static final int TANK_TOPUP_TICKS_PER_TICK = 2; + + /** How many oxygen ticks per mB consumed from space suits (higher = suits last longer) */ + public static final int SPACE_SUIT_TICKS_PER_MB = 5; + + // ------------------------------------------------------------------------- + // HUD Sync + // ------------------------------------------------------------------------- + + /** How often to sync oxygen HUD to client (in ticks) */ + public static final int HUD_SYNC_INTERVAL = 10; + + // ------------------------------------------------------------------------- + // Damage + // ------------------------------------------------------------------------- + + /** Interval between suffocation damage ticks */ + public static final int SUFFOCATION_DAMAGE_INTERVAL = 20; + + // ------------------------------------------------------------------------- + // Rebreather Behavior + // ------------------------------------------------------------------------- + + /** Drain reduction multiplier for simple rebreather in THIN air (0.5 = half drain) */ + public static final double SIMPLE_REBREATHER_DRAIN_MULT = 0.5; + + /** Drain reduction multiplier for pressurized rebreather (0.25 = quarter drain) */ + public static final double PRESSURIZED_REBREATHER_DRAIN_MULT = 0.25; +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenItemCap.java b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenItemCap.java new file mode 100644 index 000000000..6f2dc0a4f --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenItemCap.java @@ -0,0 +1,19 @@ +package com.ghostipedia.cosmiccore.common.airControl; + +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.CapabilityManager; +import net.minecraftforge.common.capabilities.CapabilityToken; +import net.minecraftforge.common.capabilities.RegisterCapabilitiesEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +public class OxygenItemCap { + + private OxygenItemCap() {} + + public static final Capability OXYGEN_SUPPLY = CapabilityManager.get(new CapabilityToken<>() {}); + + @SubscribeEvent + public static void onRegisterCaps(RegisterCapabilitiesEvent event) { + event.register(IOxygenSupplyItem.class); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenLogic.java b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenLogic.java new file mode 100644 index 000000000..c25551279 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenLogic.java @@ -0,0 +1,356 @@ +package com.ghostipedia.cosmiccore.common.airControl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.api.item.armor.SpaceArmorComponentItem; +import com.ghostipedia.cosmiccore.common.airControl.RebreatherHelper.RebreatherType; +import com.ghostipedia.cosmiccore.common.network.CCoreNetwork; +import com.ghostipedia.cosmiccore.common.network.packet.OxygenWarnPacket; +import com.ghostipedia.cosmiccore.common.network.packet.SyncOxygenBarPacket; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.DepthsBargain; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import earth.terrarium.adastra.common.items.armor.SpaceSuitItem; +import top.theillusivec4.curios.api.CuriosApi; +import top.theillusivec4.curios.api.type.inventory.IDynamicStackHandler; + +import static com.ghostipedia.cosmiccore.common.airControl.OxygenConfig.*; +import static com.ghostipedia.cosmiccore.common.airControl.OxygenItemCap.OXYGEN_SUPPLY; + +@Mod.EventBusSubscriber(modid = CosmicCore.MOD_ID) +public final class OxygenLogic { + + private OxygenLogic() {} + + // Track the oxygen value at last HUD sync to calculate accurate rate (per-player) + private static final java.util.Map lastSyncOxygenValue = new java.util.concurrent.ConcurrentHashMap<>(); + private static final java.util.Map lastSyncGameTime = new java.util.concurrent.ConcurrentHashMap<>(); + + @SubscribeEvent + public static void onPlayerTick(TickEvent.PlayerTickEvent event) { + if (event.phase != TickEvent.Phase.END || event.player.level().isClientSide) return; + + ServerPlayer player = (ServerPlayer) event.player; + + // Skip oxygen logic for creative/spectator players + if (player.isCreative() || player.isSpectator()) { + // Send hide packet if needed + if ((player.serverLevel().getGameTime() % HUD_SYNC_INTERVAL) == 0) { + long playerMaxOxygen = getMaxOxygenTicks(player); + CCoreNetwork.sendToPlayer(player, + new SyncOxygenBarPacket(playerMaxOxygen, playerMaxOxygen, false, 0.0)); + } + return; + } + + ServerLevel level = player.serverLevel(); + + player.getCapability(OxygenBudgetCap.CAP).ifPresent(cap -> { + // Get player-specific max capacity (may be modified by bargains) + long playerMaxOxygen = getMaxOxygenTicks(player); + + // Initialize if needed + if (cap.getOxygenTicks(level.dimension()) < 0) { + cap.setOxygenTicks(level.dimension(), playerMaxOxygen); + cap.setRegenBuffer(level.dimension(), 0.0); + } + + // Get player's Y and determine air quality + int yValue = player.blockPosition().getY(); + OxygenRules.AirRanges range = OxygenRules.getRanges(level.dimension(), yValue); + + OxygenRules.AirQuality quality; + OxygenRules.Rates rates; + + if (range == null) { + quality = OxygenRules.AirQuality.SAFE; + rates = OxygenRules.QUALITY_RATES.get(quality).copy(); + } else { + quality = range.quality; + rates = range.airRangeRates(); + } + + // Check if player is in a fluid (eyes submerged) + BlockPos eyePos = BlockPos.containing(player.getX(), player.getEyeY(), player.getZ()); + boolean eyesInFluid = !level.getFluidState(eyePos).isEmpty(); + if (eyesInFluid) { + OxygenRules.Rates thinAir = OxygenRules.QUALITY_RATES.get(OxygenRules.AirQuality.THIN).copy(); + rates.oxygenDrainPerTick = Math.max(rates.oxygenDrainPerTick, thinAir.oxygenDrainPerTick); + rates.oxygenRecoveryPerTick = 0.0; // No passive regen while submerged + rates.suffocationDamage = Math.max(rates.suffocationDamage, 2.0f); + quality = OxygenRules.AirQuality.THIN; + } + + long current = cap.getOxygenTicks(level.dimension()); + cap.setConsuming(level.dimension(), rates.oxygenDrainPerTick > 0); + + if (rates.oxygenDrainPerTick > 0) { + // Clear regen buffer when draining - prevents accumulated regen from triggering later + cap.setRegenBuffer(level.dimension(), 0.0); + + // Check for rebreather equipment and apply drain modifiers + RebreatherType rebreather = RebreatherHelper.getEquippedRebreather(player); + double drainMult = 1.0; + + // Apply rebreather effects based on air quality + if (quality == OxygenRules.AirQuality.THIN) { + // Both rebreathers work in THIN air + if (rebreather == RebreatherType.PRESSURIZED) { + drainMult = PRESSURIZED_REBREATHER_DRAIN_MULT; + } else if (rebreather == RebreatherType.SIMPLE) { + drainMult = SIMPLE_REBREATHER_DRAIN_MULT; + } + } else if (quality == OxygenRules.AirQuality.NO_AIR) { + // Only pressurized rebreather works in NO_AIR + if (rebreather == RebreatherType.PRESSURIZED) { + drainMult = PRESSURIZED_REBREATHER_DRAIN_MULT; + } + } + // TOXIC and ABYSS are not affected by rebreathers + + // Apply multiplier to drain rate + int baseDrain = (int) Math.min(Integer.MAX_VALUE, Math.max(0L, rates.oxygenDrainPerTick)); + int drain = (int) Math.ceil(baseDrain * drainMult); + + // Tanks can only be used with pressurized rebreather + int providedByTanks = 0; + if (rebreather == RebreatherType.PRESSURIZED) { + providedByTanks = drainFromCarriedTanks(player, drain); + } + int cover = Math.min(providedByTanks, drain); + int remainingDrain = Math.max(0, drain - cover); + + long next = current - remainingDrain; + next = Math.max(0, Math.min(playerMaxOxygen, next)); + + cap.setOxygenTicks(level.dimension(), next); + + // Warnings & damage + if (next % 20 == 0) { + int sec = (int) (next / 20); + for (int w : WARNING_SECONDS) { + if (sec == w) { + CCoreNetwork.sendToPlayer(player, new OxygenWarnPacket("cosmiccore.oxygen.warn", w)); + break; + } + } + } + if (next <= 0 && rates.suffocationDamage > 0f && + (level.getGameTime() % SUFFOCATION_DAMAGE_INTERVAL) == 0) { + // Check for Depths bargain - instant death instead of gradual damage + if (DepthsBargain.shouldInstantKillOnSuffocation(player)) { + DepthsBargain.executeInstantSuffocation(player); + } else { + player.hurt(player.damageSources().drown(), rates.suffocationDamage); + } + } + + } else if (rates.oxygenRecoveryPerTick > 0 && current < playerMaxOxygen) { + // Passive recovery in safe air + double buffer = cap.getRegenBuffer(level.dimension()) + (rates.oxygenRecoveryPerTick / 20.0); + long gain = (long) (buffer * 20.0); + double rem = buffer - (gain / 20.0); + + if (gain > 0) { + long next = Math.min(playerMaxOxygen, current + gain); + cap.setOxygenTicks(level.dimension(), next); + } + cap.setRegenBuffer(level.dimension(), rem); + } + + // HUD sync - calculate rate based on change since last sync + if ((level.getGameTime() % HUD_SYNC_INTERVAL) == 0) { + long remaining = cap.getOxygenTicks(level.dimension()); + boolean show = (quality != OxygenRules.AirQuality.SAFE) || remaining < playerMaxOxygen; + + // Calculate rate based on actual change over the sync interval + java.util.UUID playerId = player.getUUID(); + double ratePerSecond = 0.0; + long currentGameTime = level.getGameTime(); + Long prevOxygen = lastSyncOxygenValue.get(playerId); + Long prevTime = lastSyncGameTime.get(playerId); + if (prevOxygen != null && prevTime != null) { + long ticksElapsed = currentGameTime - prevTime; + if (ticksElapsed > 0) { + long oxygenChange = remaining - prevOxygen; + // Convert to per-second rate: (change / ticks) * 20 ticks/second + ratePerSecond = (oxygenChange * 20.0) / ticksElapsed; + } + } + + // Update tracking for next sync + lastSyncOxygenValue.put(playerId, remaining); + lastSyncGameTime.put(playerId, currentGameTime); + + CCoreNetwork.sendToPlayer(player, + new SyncOxygenBarPacket(remaining, playerMaxOxygen, show, ratePerSecond)); + } + }); + } + + @SubscribeEvent + public static void onLogin(PlayerEvent.PlayerLoggedInEvent event) { + if (event.getEntity().level().isClientSide) return; + + ServerPlayer player = (ServerPlayer) event.getEntity(); + ServerLevel level = player.serverLevel(); + + // Reset rate tracking for fresh rate calculation + java.util.UUID playerId = player.getUUID(); + lastSyncOxygenValue.remove(playerId); + lastSyncGameTime.remove(playerId); + + player.getCapability(OxygenBudgetCap.CAP).ifPresent(cap -> { + long playerMaxOxygen = getMaxOxygenTicks(player); + if (cap.getOxygenTicks(level.dimension()) < 0) { + cap.setOxygenTicks(level.dimension(), playerMaxOxygen); + cap.setRegenBuffer(level.dimension(), 0.0); + } + long remaining = cap.getOxygenTicks(level.dimension()); + boolean show = remaining < playerMaxOxygen; + CCoreNetwork.sendToPlayer(player, new SyncOxygenBarPacket(remaining, playerMaxOxygen, show, 0.0)); + }); + } + + @SubscribeEvent + public static void onLogout(PlayerEvent.PlayerLoggedOutEvent event) { + // Clean up tracking maps to prevent memory leaks + java.util.UUID playerId = event.getEntity().getUUID(); + lastSyncOxygenValue.remove(playerId); + lastSyncGameTime.remove(playerId); + } + + @SubscribeEvent + public static void onRespawn(PlayerEvent.PlayerRespawnEvent event) { + if (event.getEntity().level().isClientSide) return; + + ServerPlayer player = (ServerPlayer) event.getEntity(); + ServerLevel level = player.serverLevel(); + + player.getCapability(OxygenBudgetCap.CAP).ifPresent(cap -> { + long playerMaxOxygen = getMaxOxygenTicks(player); + cap.setOxygenTicks(level.dimension(), playerMaxOxygen); + cap.setRegenBuffer(level.dimension(), 0.0); + cap.setConsuming(level.dimension(), false); + CCoreNetwork.sendToPlayer(player, new SyncOxygenBarPacket(playerMaxOxygen, playerMaxOxygen, false, 0.0)); + }); + } + + // --- Oxygen provider draining --- + + /** + * Drain oxygen from all available sources. + * Priority: Space suit chestplate > Ad Astra suit > Curios back slot + * Note: Tanks in inventory do NOT work - must be equipped in Curios back slot + */ + private static int drainFromCarriedTanks(ServerPlayer player, int requestTicks) { + if (requestTicks <= 0) return 0; + + int remaining = requestTicks; + + // 1. Check CosmicCore space suit chestplate first (highest priority) + remaining = drainFromSpaceSuit(player, remaining); + if (remaining <= 0) return requestTicks; + + // 2. Check vanilla Ad Astra space suit + remaining = drainFromAdAstraSuit(player, remaining); + if (remaining <= 0) return requestTicks; + + // 3. Check Curios back slot (oxygen tanks worn on back) + // Tanks MUST be equipped in Curios back slot to work - inventory tanks are ignored + remaining = drainFromCuriosBackSlot(player, remaining); + + return requestTicks - remaining; + } + + /** + * Drain from oxygen tanks in Curios back slot. + */ + private static int drainFromCuriosBackSlot(ServerPlayer player, int requestTicks) { + if (requestTicks <= 0) return 0; + + int remaining = requestTicks; + + var curiosCap = CuriosApi.getCuriosInventory(player); + if (curiosCap.isPresent()) { + var curiosHandler = curiosCap.resolve().get(); + var backHandler = curiosHandler.getStacksHandler("back"); + if (backHandler.isPresent()) { + IDynamicStackHandler stacks = backHandler.get().getStacks(); + for (int i = 0; i < stacks.getSlots() && remaining > 0; i++) { + ItemStack stack = stacks.getStackInSlot(i); + remaining = drainFromStack(stack, remaining); + } + } + } + + return remaining; + } + + /** + * Drain from CosmicCore SpaceArmorComponentItem (nano/quantum/sanguine suits). + * Consumes 1 mB every SPACE_SUIT_TICKS_PER_MB game ticks to slow drain rate. + */ + private static int drainFromSpaceSuit(ServerPlayer player, int requestTicks) { + if (requestTicks <= 0) return 0; + + ItemStack chestStack = player.getItemBySlot(EquipmentSlot.CHEST); + if (chestStack.isEmpty()) return requestTicks; + if (!(chestStack.getItem() instanceof SpaceArmorComponentItem suit)) return requestTicks; + + if (!suit.hasOxygen(player)) return requestTicks; + + // Only drain 1 mB every SPACE_SUIT_TICKS_PER_MB game ticks + // This makes suits last much longer than basic tanks + if ((player.serverLevel().getGameTime() % SPACE_SUIT_TICKS_PER_MB) == 0) { + suit.consumeOxygen(chestStack, 1); + } + + // Return 0 remaining if we have oxygen (suit provides full coverage) + return suit.hasOxygen(player) ? 0 : requestTicks; + } + + /** + * Drain from vanilla Ad Astra SpaceSuitItem. + * Consumes 1 mB every SPACE_SUIT_TICKS_PER_MB game ticks to slow drain rate. + */ + private static int drainFromAdAstraSuit(ServerPlayer player, int requestTicks) { + if (requestTicks <= 0) return 0; + + ItemStack chestStack = player.getItemBySlot(EquipmentSlot.CHEST); + if (chestStack.isEmpty()) return requestTicks; + if (!(chestStack.getItem() instanceof SpaceSuitItem suit)) return requestTicks; + + if (!SpaceSuitItem.hasOxygen(player)) return requestTicks; + + // Only drain 1 mB every SPACE_SUIT_TICKS_PER_MB game ticks + if ((player.serverLevel().getGameTime() % SPACE_SUIT_TICKS_PER_MB) == 0) { + suit.consumeOxygen(chestStack, 1); + } + + // Return 0 remaining if suit still has oxygen + return SpaceSuitItem.hasOxygen(player) ? 0 : requestTicks; + } + + /** + * Drain from GTCEu-style oxygen supply tanks via capability. + */ + private static int drainFromStack(ItemStack stack, int requestTicks) { + if (stack.isEmpty() || requestTicks <= 0) return requestTicks; + + return stack.getCapability(OXYGEN_SUPPLY) + .map(provider -> { + int got = Math.max(0, provider.drainOxygenTicks(stack, requestTicks)); + return Math.max(0, requestTicks - got); + }) + .orElse(requestTicks); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenRules.java b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenRules.java new file mode 100644 index 000000000..29572fc0f --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/OxygenRules.java @@ -0,0 +1,180 @@ +package com.ghostipedia.cosmiccore.common.airControl; + +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.Level; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public final class OxygenRules { + + private OxygenRules() {} + + public enum AirQuality { + SAFE, + THIN, + TOXIC, + ABYSS, + NO_AIR + } + + public static final class Rates { + + public int oxygenDrainPerTick; + public double oxygenRecoveryPerTick; + public float suffocationDamage; + + public Rates copy() { + Rates rates = new Rates(); + rates.oxygenDrainPerTick = oxygenDrainPerTick; + rates.oxygenRecoveryPerTick = oxygenRecoveryPerTick; + rates.suffocationDamage = suffocationDamage; + return rates; + } + } + + private static Rates rates(int drain, double regen, float dmg) { + Rates result = new Rates(); + result.oxygenDrainPerTick = drain; + result.oxygenRecoveryPerTick = regen; + result.suffocationDamage = dmg; + return result; + } + + public static final Map QUALITY_RATES = Map.of( + AirQuality.SAFE, rates(0, 2.0, 5f), + AirQuality.THIN, rates(1, 0.0, 5f), + AirQuality.TOXIC, rates(1, 0.5, 5f), + AirQuality.ABYSS, rates(8, 0.0, 1000f), + AirQuality.NO_AIR, rates(2, 0.0, 5f)); + + // Air Ranges + + public static final class AirRanges { + + public final int minY; + public final int maxY; + public final AirQuality quality; + + public final Integer drainPerTickOverride; + public final Double regenOverride; + public final Float damageOverride; + + public AirRanges(int minY, int maxY, AirQuality quality) { + this(minY, maxY, quality, null, null, null); + } + + public AirRanges(int minY, int maxY, AirQuality quality, + Integer drainPerTickOverride, + Double regenOverride, + Float damageOverride) { + this.minY = minY; + this.maxY = maxY; + this.quality = quality; + this.drainPerTickOverride = drainPerTickOverride; + this.regenOverride = regenOverride; + this.damageOverride = damageOverride; + } + + public boolean presentAtY(int yValue) { + return yValue >= minY && yValue <= maxY; + } + + public Rates airRangeRates() { + Rates base = QUALITY_RATES.get(quality).copy(); + if (drainPerTickOverride != null) base.oxygenDrainPerTick = drainPerTickOverride; + if (regenOverride != null) base.oxygenRecoveryPerTick = regenOverride; + if (damageOverride != null) base.suffocationDamage = damageOverride; + return base; + } + } + + private static final Map, List> RANGES = new ConcurrentHashMap<>(); + + public static void addRanges(ResourceKey dimension, AirRanges... ranges) { + RANGES.computeIfAbsent(dimension, d -> new ArrayList<>()).addAll(Arrays.asList(ranges)); + RANGES.get(dimension).sort(Comparator.comparingInt(b -> b.minY)); + } + + public static AirRanges getRanges(ResourceKey dimension, int y) { + List airRangesList = RANGES.get(dimension); + if (airRangesList == null || airRangesList.isEmpty()) return null; + for (AirRanges range : airRangesList) { + if (range.presentAtY(y)) return range; + } + return null; + } + + // --- Ad Astra dimension keys --- + private static ResourceKey adAstraDim(String name) { + return ResourceKey.create(Registries.DIMENSION, new ResourceLocation("ad_astra", name)); + } + + public static final ResourceKey MOON = adAstraDim("moon"); + public static final ResourceKey MOON_ORBIT = adAstraDim("moon_orbit"); + public static final ResourceKey MARS = adAstraDim("mars"); + public static final ResourceKey MARS_ORBIT = adAstraDim("mars_orbit"); + public static final ResourceKey VENUS = adAstraDim("venus"); + public static final ResourceKey VENUS_ORBIT = adAstraDim("venus_orbit"); + public static final ResourceKey MERCURY = adAstraDim("mercury"); + public static final ResourceKey MERCURY_ORBIT = adAstraDim("mercury_orbit"); + public static final ResourceKey GLACIO = adAstraDim("glacio"); + public static final ResourceKey GLACIO_ORBIT = adAstraDim("glacio_orbit"); + public static final ResourceKey EARTH_ORBIT = adAstraDim("earth_orbit"); + + // Default range registration + public static void registerAirRanges() { + // --- Overworld --- + addRanges(Level.OVERWORLD, + // y ≤ 0 : THIN air underground + new AirRanges(Integer.MIN_VALUE, 0, AirQuality.THIN, 1, 0.0, 2.0f), + // 1 to 199 : SAFE (faster regen) + new AirRanges(1, 199, AirQuality.SAFE, null, 3.0, null), + // 200+ : THIN at high altitude + new AirRanges(200, Integer.MAX_VALUE, AirQuality.THIN)); + + // --- Space (no atmosphere) --- + // All orbit dimensions have no air at all Y levels + for (ResourceKey orbit : List.of(EARTH_ORBIT, MOON_ORBIT, MARS_ORBIT, VENUS_ORBIT, MERCURY_ORBIT, + GLACIO_ORBIT)) { + addRanges(orbit, new AirRanges(Integer.MIN_VALUE, Integer.MAX_VALUE, AirQuality.NO_AIR)); + } + + // --- Planetary surfaces (no atmosphere) --- + // Moon, Mars, Mercury - no atmosphere + for (ResourceKey airless : List.of(MOON, MARS, MERCURY)) { + addRanges(airless, new AirRanges(Integer.MIN_VALUE, Integer.MAX_VALUE, AirQuality.NO_AIR)); + } + + // Venus - toxic atmosphere + addRanges(VENUS, new AirRanges(Integer.MIN_VALUE, Integer.MAX_VALUE, AirQuality.TOXIC, 2, 0.0, 3.0f)); + + // Glacio - thin but breathable at surface (ice world with some atmosphere) + addRanges(GLACIO, + new AirRanges(Integer.MIN_VALUE, 0, AirQuality.THIN, 1, 0.0, 2.0f), + new AirRanges(1, 127, AirQuality.SAFE, null, 1.5, null), + new AirRanges(128, Integer.MAX_VALUE, AirQuality.THIN)); + } + + public static final class ResolvedAirRange { + + public final AirQuality airQuality; + public final Rates rates; + + public ResolvedAirRange(AirQuality quality, Rates rates) { + this.airQuality = quality; + this.rates = rates; + } + } + + public static ResolvedAirRange resolve(ResourceKey dimension, int yVal) { + AirRanges range = getRanges(dimension, yVal); + if (range == null) { + Rates safe = QUALITY_RATES.get(AirQuality.SAFE).copy(); + return new ResolvedAirRange(AirQuality.SAFE, safe); + } + return new ResolvedAirRange(range.quality, range.airRangeRates()); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/airControl/RebreatherHelper.java b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/RebreatherHelper.java new file mode 100644 index 000000000..d5f5adf52 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/airControl/RebreatherHelper.java @@ -0,0 +1,95 @@ +package com.ghostipedia.cosmiccore.common.airControl; + +import com.ghostipedia.cosmiccore.common.data.CosmicItems; + +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.common.util.LazyOptional; + +import top.theillusivec4.curios.api.CuriosApi; +import top.theillusivec4.curios.api.type.capability.ICuriosItemHandler; +import top.theillusivec4.curios.api.type.inventory.ICurioStacksHandler; +import top.theillusivec4.curios.api.type.inventory.IDynamicStackHandler; + +import java.util.Optional; + +/** + * Helper class for detecting and interacting with rebreather equipment. + */ +public final class RebreatherHelper { + + private RebreatherHelper() {} + + /** + * Rebreather types in order of capability. + */ + public enum RebreatherType { + /** No rebreather equipped */ + NONE, + /** Simple rebreather - slows oxygen drain in THIN air only */ + SIMPLE, + /** Pressurized rebreather - works in NO_AIR, allows tank usage */ + PRESSURIZED + } + + /** + * Get the best rebreather type the player has equipped. + * Checks head curio slot for rebreather items. + * + * @param player The player to check + * @return The best rebreather type found + */ + public static RebreatherType getEquippedRebreather(Player player) { + if (player == null) return RebreatherType.NONE; + + // Check for pressurized first (better) + if (hasCurioItem(player, "head", CosmicItems.PRESSURIZED_REBREATHER.asItem())) { + return RebreatherType.PRESSURIZED; + } + + // Check for simple + if (hasCurioItem(player, "head", CosmicItems.SIMPLE_REBREATHER.asItem())) { + return RebreatherType.SIMPLE; + } + + return RebreatherType.NONE; + } + + /** + * Check if player has a simple rebreather or better equipped. + */ + public static boolean hasSimpleRebreatherOrBetter(Player player) { + RebreatherType type = getEquippedRebreather(player); + return type == RebreatherType.SIMPLE || type == RebreatherType.PRESSURIZED; + } + + /** + * Check if player has a pressurized rebreather equipped. + */ + public static boolean hasPressurizedRebreather(Player player) { + return getEquippedRebreather(player) == RebreatherType.PRESSURIZED; + } + + /** + * Check if a specific item is in a curio slot. + */ + private static boolean hasCurioItem(LivingEntity entity, String slotId, Item item) { + LazyOptional cap = CuriosApi.getCuriosInventory(entity); + if (cap.isPresent()) { + ICuriosItemHandler curioHandler = cap.resolve().get(); + Optional handler = curioHandler.getStacksHandler(slotId); + if (handler.isPresent()) { + IDynamicStackHandler stackHandler = handler.get().getStacks(); + for (int i = 0; i < stackHandler.getSlots(); i++) { + ItemStack stack = stackHandler.getStackInSlot(i); + if (stack.is(item)) { + return true; + } + } + } + } + return false; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/breath/OxygenHelper.java b/src/main/java/com/ghostipedia/cosmiccore/common/breath/OxygenHelper.java index 3c4e22de0..ec6741412 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/common/breath/OxygenHelper.java +++ b/src/main/java/com/ghostipedia/cosmiccore/common/breath/OxygenHelper.java @@ -1,23 +1,37 @@ package com.ghostipedia.cosmiccore.common.breath; -import net.minecraft.world.entity.LivingEntity; +import com.ghostipedia.cosmiccore.common.airControl.OxygenRules; -import fuzs.thinair.api.v1.AirQualityLevel; -import fuzs.thinair.helper.AirQualityHelperImpl; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +/** + * Helper for checking air quality using CosmicCore's oxygen system. + * Used by Create diving helmet integration. + */ public class OxygenHelper { + /** + * Check if the air quality at the entity's location should activate breathing equipment. + * Returns true if air is not SAFE (i.e., THIN, TOXIC, ABYSS, or NO_AIR). + */ public static boolean airQualityActivatesHelmet(LivingEntity entity) { - final var air = AirQualityHelperImpl.INSTANCE.getAirQualityAtLocation(entity.level(), entity.getEyePosition()); - return air == AirQualityLevel.RED || air == AirQualityLevel.YELLOW; - } + Level level = entity.level(); + BlockPos pos = entity.blockPosition(); - // - // @SubscribeEvent - // public static void aircheck(LivingEvent.LivingTickEvent event){ - // if(event.getEntity() instanceof Player player){ - // - // } - // - // } + // Check if eyes are in fluid - always needs helmet + BlockPos eyePos = BlockPos.containing(entity.getX(), entity.getEyeY(), entity.getZ()); + if (!level.getFluidState(eyePos).isEmpty()) { + return true; + } + + // Check our air quality system + OxygenRules.AirRanges range = OxygenRules.getRanges(level.dimension(), pos.getY()); + if (range == null) { + return false; // No range defined = SAFE + } + + return range.quality != OxygenRules.AirQuality.SAFE; + } } diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/data/CosmicItems.java b/src/main/java/com/ghostipedia/cosmiccore/common/data/CosmicItems.java index 992dc5035..4c129b8fe 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/common/data/CosmicItems.java +++ b/src/main/java/com/ghostipedia/cosmiccore/common/data/CosmicItems.java @@ -10,13 +10,16 @@ import com.ghostipedia.cosmiccore.common.item.AsteroidItem; import com.ghostipedia.cosmiccore.common.item.AsteroidTargetingChipItem; import com.ghostipedia.cosmiccore.common.item.CosmicScytheItem; +import com.ghostipedia.cosmiccore.common.item.OxygenTankItem; import com.ghostipedia.cosmiccore.common.item.armor.ChestSanguineWarptechSuite; import com.ghostipedia.cosmiccore.common.item.armor.HelmetSanguineWarptechSuite; import com.ghostipedia.cosmiccore.common.item.armor.SanguineWarptechSuite; import com.ghostipedia.cosmiccore.common.item.behavior.EffectApplicationBehavior; import com.ghostipedia.cosmiccore.common.item.behavior.InfiniteSprayCanBehavior; +import com.ghostipedia.cosmiccore.common.item.behavior.OxygenSupplyTankBehavior; import com.ghostipedia.cosmiccore.common.item.behavior.StructureWriteBehavior; import com.ghostipedia.cosmiccore.common.item.behavior.WirelessPDABehavior; +import com.ghostipedia.cosmiccore.common.reflection.item.MirrorItem; import com.ghostipedia.cosmiccore.utils.StringUtil; import com.gregtechceu.gtceu.GTCEu; @@ -1065,31 +1068,37 @@ public class CosmicItems { .item("shard_of_perpetuity", ComponentItem::create) .lang("Shard of Perpetuity") .properties(p -> p.stacksTo(64)) - .onRegister(attach(new TooltipBehavior(tooltips -> { - tooltips.add(Component.translatable("cosmiccore.lore.shard_small.0")); - tooltips.add(Component.translatable("cosmiccore.lore.shard_small.1")); - }))) + .onRegister(attach( + new TooltipBehavior(tooltips -> { + tooltips.add(Component.translatable("cosmiccore.lore.shard_small.0")); + tooltips.add(Component.translatable("cosmiccore.lore.shard_small.1")); + }), + new com.ghostipedia.cosmiccore.common.reflection.item.ShardConsumeBehavior(1))) .defaultModel() .register(); public static final ItemEntry PERPETUITY_SHARD_LARGE = REGISTRATE .item("large_shard_of_perpetuity", ComponentItem::create) .lang("Large Shard of Perpetuity") .properties(p -> p.stacksTo(64)) - .onRegister(attach(new TooltipBehavior(tooltips -> { - tooltips.add(Component.translatable("cosmiccore.lore.shard_large.0")); - tooltips.add(Component.translatable("cosmiccore.lore.shard_large.1")); - }))) + .onRegister(attach( + new TooltipBehavior(tooltips -> { + tooltips.add(Component.translatable("cosmiccore.lore.shard_large.0")); + tooltips.add(Component.translatable("cosmiccore.lore.shard_large.1")); + }), + new com.ghostipedia.cosmiccore.common.reflection.item.ShardConsumeBehavior(8))) .defaultModel() .register(); public static final ItemEntry PERPETUITY_SHARD_MASSIVE = REGISTRATE .item("cluster_of_perpetuity", ComponentItem::create) .lang("Cluster of Perpetuity") .properties(p -> p.stacksTo(60)) - .onRegister(attach(new TooltipBehavior(tooltips -> { - tooltips.add(Component.translatable("cosmiccore.lore.shard_huge.0")); - tooltips.add(Component.translatable("cosmiccore.lore.shard_huge.1")); - tooltips.add(Component.translatable("cosmiccore.lore.shard_huge.2")); - }))) + .onRegister(attach( + new TooltipBehavior(tooltips -> { + tooltips.add(Component.translatable("cosmiccore.lore.shard_huge.0")); + tooltips.add(Component.translatable("cosmiccore.lore.shard_huge.1")); + tooltips.add(Component.translatable("cosmiccore.lore.shard_huge.2")); + }), + new com.ghostipedia.cosmiccore.common.reflection.item.ShardConsumeBehavior(64))) .defaultModel() .register(); public static final ItemEntry WIRELESS_PDA = REGISTRATE @@ -1199,6 +1208,25 @@ public boolean isFoil(ItemStack stack) { }))) .register(); + public static ItemEntry SIMPLE_REBREATHER = REGISTRATE + .item("simple_rebreather", ComponentItem::create) + .lang("Simple Rebreather") + .properties(p -> p.stacksTo(1).fireResistant()) + .onRegister(attach(new TooltipBehavior(list -> { + list.add(Component.translatable("item.cosmiccore.simple_rebreather.tooltip")); + }))) + .register(); + + public static ItemEntry PRESSURIZED_REBREATHER = REGISTRATE + .item("pressurized_rebreather", ComponentItem::create) + .lang("Pressurized Rebreather") + .properties(p -> p.stacksTo(1).fireResistant()) + .onRegister(attach(new TooltipBehavior(list -> { + list.add(Component.translatable("item.cosmiccore.simple_rebreather.tooltip")); + list.add(Component.translatable("item.cosmiccore.pressurized_rebreather.tooltip")); + }))) + .register(); + public static final ItemEntry WAXED_LEATHER = REGISTRATE.item("waxed_leather", ComponentItem::create) .lang("Waxed Leather") .properties(p -> p.stacksTo(64)) @@ -2562,6 +2590,26 @@ public boolean isFoil(ItemStack stack) { .defaultModel() .register(); + // ------------------------------------------------------------------------- + // Oxygen Supply Tanks + // ------------------------------------------------------------------------- + + public static final ItemEntry OXYGEN_SUPPLY_TANK_BRONZE = REGISTRATE + .item("bronze_supply_tank", OxygenTankItem::new) + .lang("Bronze Supply Tank") + .properties(p -> p.stacksTo(1)) + .onRegister(attach(new OxygenSupplyTankBehavior(1000, 5, 10))) + .defaultModel() + .register(); + + public static final ItemEntry OXYGEN_SUPPLY_TANK_STEEL = REGISTRATE + .item("steel_supply_tank", OxygenTankItem::new) + .lang("Steel Supply Tank") + .properties(p -> p.stacksTo(1)) + .onRegister(attach(new OxygenSupplyTankBehavior(2500, 5, 15))) + .defaultModel() + .register(); + public static ICustomDescriptionId cellName() { return new ICustomDescriptionId() { @@ -2578,6 +2626,17 @@ public static NonNullConsumer attach(IItemComponent return item -> item.attachComponents(components); } + // ------------------------------------------------------------------------- + // Reflection System + // ------------------------------------------------------------------------- + + public static final ItemEntry REFLECTION_MIRROR = REGISTRATE + .item("reflection_mirror", MirrorItem::new) + .lang("Mirror of Erosion") + .properties(p -> p.stacksTo(1)) + .defaultModel() + .register(); + public static NonNullConsumer modelPredicate(ResourceLocation predicate, Function property) { return item -> { diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/data/CosmicMachines.java b/src/main/java/com/ghostipedia/cosmiccore/common/data/CosmicMachines.java index 9fd012dcc..13b0cba22 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/common/data/CosmicMachines.java +++ b/src/main/java/com/ghostipedia/cosmiccore/common/data/CosmicMachines.java @@ -928,7 +928,11 @@ private static MachineDefinition[] registerTieredMachines(String name, GTCEu.id("block/multiblock/generator/large_steam_turbine"), false); + // Dreamer's Basin is now registered in DreamersBasin.java + public static void init() { + // Initialize DreamersBasin + com.ghostipedia.cosmiccore.common.machine.multiblock.multi.DreamersBasin.init(); GTMultiMachines.LARGE_COMBUSTION_ENGINE.setRecipeTypes(new GTRecipeType[] { DUMMY_RECIPES }); GTMultiMachines.LARGE_COMBUSTION_ENGINE.setRenderXEIPreview(false); GTMultiMachines.LARGE_COMBUSTION_ENGINE.setRenderWorldPreview(false); diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/data/lang/CosmicLangHandler.java b/src/main/java/com/ghostipedia/cosmiccore/common/data/lang/CosmicLangHandler.java index 25d0ef37d..8e08a306f 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/common/data/lang/CosmicLangHandler.java +++ b/src/main/java/com/ghostipedia/cosmiccore/common/data/lang/CosmicLangHandler.java @@ -284,6 +284,10 @@ public static void init(RegistrateLangProvider provider) { // generic machine tooltips provider.add("item.cosmiccore.space_radio.tooltip", "§6Lets you hear sounds in space!"); + provider.add("item.cosmiccore.simple_rebreather.tooltip", + "§7Reduces oxygen drain in §bThin Air§7 environments."); + provider.add("item.cosmiccore.pressurized_rebreather.tooltip", + "§6Enables oxygen tank usage. Works in §cNo Air§6 environments."); provider.add("cosmiccore.universal.tooltip.lube_info.0", "§aProviding Better Lubricants increases the total EU created"); provider.add("cosmiccore.universal.tooltip.lube_info.1", "§eLubricant§f: §c1x §fEU total @ 1000mb/hr"); @@ -390,6 +394,7 @@ public static void init(RegistrateLangProvider provider) { provider.add("config.jade.plugin_cosmiccore.drone_maintenance_interface", "[CC] Drone Maintenance Interface"); provider.add("config.jade.plugin_cosmiccore.parallel_info_cc", "[CC] Parallel Info"); + provider.add("config.jade.plugin_cosmiccore.stellar_module", "[CC] Stellar Module"); provider.add("debug.owner.uuid", "§aOwner UUID:§a %s"); provider.add("debug.team.uuid", "§aTeam UUID:§a %s"); @@ -515,5 +520,859 @@ public static void init(RegistrateLangProvider provider) { provider.add("cosmiccore.calorific.tooltip.prefix", "§5Calorific:§r %s"); provider.add("cosmiccore.lubricant.tooltip.prefix", "§6Lubricant:§r Tier %s"); provider.add("cosmiccore.booster.tooltip.prefix", "§bBooster:§r Tier %s"); + + // Dreamer's Basin - Multithreaded Machine + multiLang(provider, "cosmiccore.machine.dreamers_basin.tooltip", + "§bRuns multiple unique recipes simultaneously", + "§fEach thread requires a uniquely §6colored§f input bus/hatch", + "§fMax threads = Energy Hatch amperage (4A=4, 16A=16)", + "§aAll threads share output buses/hatches"); + + // Multithreaded Machine Display (base) + provider.add("cosmiccore.machine.multithreaded.thread_status", "§b=== Thread Status ==="); + provider.add("cosmiccore.machine.multithreaded.max_threads", "§7Max Threads: §f%s"); + provider.add("cosmiccore.machine.multithreaded.active_threads", "§7Active: §a%s§7/§f%s"); + + // Dreamer's Basin Custom UI + provider.add("cosmiccore.machine.dreamers_basin.thread_header", "Thread Status"); + provider.add("cosmiccore.machine.dreamers_basin.threads_summary", "%s running / %s active / %s max"); + provider.add("cosmiccore.machine.dreamers_basin.eu_budget_header", "Energy Budget"); + provider.add("cosmiccore.machine.dreamers_basin.eu_per_thread", "%s EU/t per thread (%s)"); + provider.add("cosmiccore.machine.dreamers_basin.time_remaining", "Time: %s remaining"); + provider.add("cosmiccore.machine.dreamers_basin.status_idle", "Idle - No recipe"); + provider.add("cosmiccore.machine.dreamers_basin.status_waiting", "Waiting for inputs"); + provider.add("cosmiccore.machine.dreamers_basin.status_suspended", "Suspended"); + provider.add("cosmiccore.machine.dreamers_basin.status_unknown", "Unknown"); + + // Dreamer's Basin Hover Tooltips + provider.add("cosmiccore.machine.dreamers_basin.tooltip.crafting", "Crafting:"); + provider.add("cosmiccore.machine.dreamers_basin.tooltip.no_recipe", "No recipe data"); + provider.add("cosmiccore.machine.dreamers_basin.tooltip.processing", " Processing..."); + provider.add("cosmiccore.machine.dreamers_basin.tooltip.duration", "Recipe duration: %s"); + + // Stellar Iris Widget UI + provider.add("cosmiccore.stellar.prestige.title", "STELLAR CONVERGENCE"); + provider.add("cosmiccore.stellar.prestige.points_earned", "POINTS EARNED"); + provider.add("cosmiccore.stellar.prestige.total_points", "Total: %s points"); + provider.add("cosmiccore.stellar.prestige.current_tier", "CURRENT TIER"); + provider.add("cosmiccore.stellar.prestige.next_tier", "%s pts for %s"); + provider.add("cosmiccore.stellar.prestige.max_tier", "MAXIMUM TIER REACHED"); + provider.add("cosmiccore.stellar.prestige.tier_up", "TIER UP!"); + provider.add("cosmiccore.stellar.prestige.continue", "[Click anywhere to continue]"); + + provider.add("cosmiccore.stellar.prestige.tier.novice", "NOVICE"); + provider.add("cosmiccore.stellar.prestige.tier.apprentice", "APPRENTICE"); + provider.add("cosmiccore.stellar.prestige.tier.journeyman", "JOURNEYMAN"); + provider.add("cosmiccore.stellar.prestige.tier.expert", "EXPERT"); + provider.add("cosmiccore.stellar.prestige.tier.master", "MASTER"); + provider.add("cosmiccore.stellar.prestige.tier.grandmaster", "GRANDMASTER"); + provider.add("cosmiccore.stellar.prestige.tier.unknown", "UNKNOWN"); + + provider.add("cosmiccore.stellar.ignition.requires_star", "REQUIRES ACTIVE STAR"); + provider.add("cosmiccore.stellar.ignition.breaking", "!!! BREAKING !!!"); + provider.add("cosmiccore.stellar.ignition.ignite", "IGNITE"); + + provider.add("cosmiccore.stellar.module.status", "Status"); + provider.add("cosmiccore.stellar.module.status.processing", "PROCESSING"); + provider.add("cosmiccore.stellar.module.status.idle", "IDLE"); + provider.add("cosmiccore.stellar.module.status.offline", "OFFLINE"); + provider.add("cosmiccore.stellar.module.status.ready", "READY"); + provider.add("cosmiccore.stellar.module.status.iris_inactive", "IRIS INACTIVE"); + provider.add("cosmiccore.stellar.module.status.disconnected", "DISCONNECTED"); + provider.add("cosmiccore.stellar.module.status.power_fail", "POWER FAIL"); + provider.add("cosmiccore.stellar.module.status.no_wireless", "NO WIRELESS"); + + provider.add("cosmiccore.stellar.module.max_eut", "Max EU/t"); + provider.add("cosmiccore.stellar.module.parallel", "Parallel"); + provider.add("cosmiccore.stellar.module.parallel_max", "%sx (max %s)"); + provider.add("cosmiccore.stellar.module.current", "Current"); + provider.add("cosmiccore.stellar.module.speed_bonus", "Speed Bonus"); + provider.add("cosmiccore.stellar.module.iris_limit", "Iris Limit"); + provider.add("cosmiccore.stellar.module.stage", "Stage"); + provider.add("cosmiccore.stellar.module.waiting_iris", "Waiting for Iris"); + provider.add("cosmiccore.stellar.module.not_linked", "Not linked to Stellar Iris"); + provider.add("cosmiccore.stellar.module.config", "Module Config"); + + provider.add("cosmiccore.stellar.power.title", "Power Control Panel"); + provider.add("cosmiccore.stellar.power.max_parallel", "Maximum Parallel"); + provider.add("cosmiccore.stellar.power.voltage_per_parallel", "Voltage Per Parallel"); + + provider.add("cosmiccore.stellar.stage.initialization", "INITIALIZATION"); + provider.add("cosmiccore.stellar.stage.stellar_ignition", "STELLAR IGNITION"); + provider.add("cosmiccore.stellar.stage.stellar_operations", "STELLAR OPERATIONS"); + provider.add("cosmiccore.stellar.stage.critical_mass", "CRITICAL MASS"); + provider.add("cosmiccore.stellar.stage.singularity_control", "SINGULARITY CONTROL"); + provider.add("cosmiccore.stellar.stage.emergency_protocols", "EMERGENCY PROTOCOLS"); + provider.add("cosmiccore.stellar.stage.controlled_shutdown", "CONTROLLED SHUTDOWN"); + + provider.add("cosmiccore.stellar.context.empty_line1", "Insert star seed and"); + provider.add("cosmiccore.stellar.context.empty_line2", "provide stellar gases"); + provider.add("cosmiccore.stellar.context.empty_line3", "to begin ignition."); + provider.add("cosmiccore.stellar.context.growing_line1", "Stellar fusion"); + provider.add("cosmiccore.stellar.context.growing_line2", "initiating..."); + provider.add("cosmiccore.stellar.context.star_line1", "Stable fusion active"); + provider.add("cosmiccore.stellar.context.star_line2", "Processing available"); + provider.add("cosmiccore.stellar.context.superstar_line1", "WARNING: Critical mass"); + provider.add("cosmiccore.stellar.context.superstar_line2", "Collapse imminent"); + provider.add("cosmiccore.stellar.context.blackhole_line1", "Singularity contained"); + provider.add("cosmiccore.stellar.context.blackhole_line2", "Exotic processing"); + provider.add("cosmiccore.stellar.context.death_line1", "CRITICAL FAILURE"); + provider.add("cosmiccore.stellar.context.death_line2", "SOUL FUSE ENGAGED"); + provider.add("cosmiccore.stellar.context.death_graceful_line1", "Controlled shutdown"); + provider.add("cosmiccore.stellar.context.death_graceful_line2", "in progress..."); + + provider.add("cosmiccore.stellar.slot.star_seed", "Star Seed"); + + // ========================================================================= + // REFLECTION SYSTEM + // ========================================================================= + initReflectionLang(provider); + } + + private static void initReflectionLang(RegistrateLangProvider provider) { + // UI Elements - Basic + provider.add("reflection.cosmiccore.ui.void_title", "The Void Between"); + provider.add("reflection.cosmiccore.ui.constellation_title", "The Constellation of Bargains"); + provider.add("reflection.cosmiccore.ui.available_bargains", "Available Bargains"); + provider.add("reflection.cosmiccore.ui.your_bargains", "Your Bargains"); + provider.add("reflection.cosmiccore.ui.defiance", "Defiance"); + provider.add("reflection.cosmiccore.ui.continue", "[Continue]"); + provider.add("reflection.cosmiccore.ui.acknowledge", "[I understand]"); + provider.add("reflection.cosmiccore.ui.back", "[Back]"); + provider.add("reflection.cosmiccore.ui.exit", "[Leave]"); + provider.add("reflection.cosmiccore.ui.leave", "[Leave this place]"); + provider.add("reflection.cosmiccore.ui.view_bargains", "[View Available Bargains]"); + provider.add("reflection.cosmiccore.ui.view_active", "[View Your Bargains]"); + provider.add("reflection.cosmiccore.ui.enter_defiance", "[Enter Defiance Mode]"); + provider.add("reflection.cosmiccore.ui.defy_bargain", "[Defy This Bargain]"); + provider.add("reflection.cosmiccore.ui.confirm_defiance", "[Confirm Defiance]"); + provider.add("reflection.cosmiccore.ui.cancel", "[Cancel]"); + provider.add("reflection.cosmiccore.ui.select", "[Select]"); + provider.add("reflection.cosmiccore.ui.no_bargains", "No bargains accepted yet."); + provider.add("reflection.cosmiccore.ui.defiance_warning", + "Defying a bargain will cost you power but restore some of your soul."); + provider.add("reflection.cosmiccore.ui.powers", "Powers:"); + provider.add("reflection.cosmiccore.ui.drawbacks", "Drawbacks:"); + provider.add("reflection.cosmiccore.ui.soul_erosion", "Soul Erosion: %d%%"); + provider.add("reflection.cosmiccore.ui.soul_erosion_display", "Soul Erosion: %s%%"); + provider.add("reflection.cosmiccore.ui.soul_label", "Soul"); + provider.add("reflection.cosmiccore.ui.dialogue_continue", "Click to continue..."); + provider.add("reflection.cosmiccore.ui.no_available_bargains", "The void has nothing to offer you... for now."); + provider.add("reflection.cosmiccore.ui.select_to_view", "Select a bargain to view details"); + provider.add("reflection.cosmiccore.ui.cost", "Cost: %d erosion"); + provider.add("reflection.cosmiccore.ui.erosion", "erosion"); + provider.add("reflection.cosmiccore.ui.of", "of"); + provider.add("reflection.cosmiccore.ui.defy", "Defy"); + provider.add("reflection.cosmiccore.ui.tooltip.no_details", "No additional details"); + + // Scroll indicators + provider.add("reflection.cosmiccore.ui.scroll_up", "\u25B2 Scroll up"); + provider.add("reflection.cosmiccore.ui.scroll_down", "\u25BC Scroll down"); + + // Hub menu options + provider.add("reflection.cosmiccore.ui.review_bargains", "[Review your %s bargains]"); + provider.add("reflection.cosmiccore.ui.browse_bargains", "[Browse %s available bargains]"); + provider.add("reflection.cosmiccore.ui.gaze_constellation", "[Gaze upon the constellation]"); + provider.add("reflection.cosmiccore.ui.just_look", "[Just... look at yourself]"); + provider.add("reflection.cosmiccore.ui.unlock_cost", "Cost: %d soul erosion"); + provider.add("reflection.cosmiccore.ui.defiance_cost", "Defiance will cost %d erosion"); + + // Hub option details + provider.add("reflection.cosmiccore.ui.hub.review.power", "See what you've given away"); + provider.add("reflection.cosmiccore.ui.hub.review.drawback", "Consider defying a bargain"); + provider.add("reflection.cosmiccore.ui.hub.browse.power", "See what the void offers"); + provider.add("reflection.cosmiccore.ui.hub.browse.response", "So many choices... so little soul."); + provider.add("reflection.cosmiccore.ui.hub.browse.response_empty", "Nothing for you. Yet."); + provider.add("reflection.cosmiccore.ui.hub.reflect.power", "Contemplate your existence"); + provider.add("reflection.cosmiccore.ui.hub.review_response", "Let's see what you've become."); + provider.add("reflection.cosmiccore.ui.hub.reflect_response", "Gazing into the abyss, are we?"); + provider.add("reflection.cosmiccore.ui.hub.leave_response", "Running away? How predictable."); + + // Hub greetings + provider.add("reflection.cosmiccore.ui.hub.greeting.many_bargains_high.0", "Look at you. So much given away."); + provider.add("reflection.cosmiccore.ui.hub.greeting.many_bargains_high.1", + "Do you even remember what you were?"); + provider.add("reflection.cosmiccore.ui.hub.greeting.many_bargains.0", "Back again. Of course you are."); + provider.add("reflection.cosmiccore.ui.hub.greeting.many_bargains.1", "Your soul grows thinner each time."); + provider.add("reflection.cosmiccore.ui.hub.greeting.has_bargains.0", "Ah, you return. Hungry for more?"); + provider.add("reflection.cosmiccore.ui.hub.greeting.has_bargains.1", "I have plenty left to offer."); + provider.add("reflection.cosmiccore.ui.hub.greeting.has_scars.0", "I see the scars of defiance."); + provider.add("reflection.cosmiccore.ui.hub.greeting.has_scars.1", "You took power, then threw it away."); + provider.add("reflection.cosmiccore.ui.hub.greeting.has_scars.2", "Was the cost of keeping it too high?"); + provider.add("reflection.cosmiccore.ui.hub.greeting.erosion_no_bargains.0", + "Erosion without bargains? Curious."); + provider.add("reflection.cosmiccore.ui.hub.greeting.erosion_no_bargains.1", + "Something else has been taking from you."); + provider.add("reflection.cosmiccore.ui.hub.greeting.erosion_no_bargains.2", + "Perhaps you should let me help instead."); + provider.add("reflection.cosmiccore.ui.hub.greeting.fresh.0", "A pristine soul. How rare."); + provider.add("reflection.cosmiccore.ui.hub.greeting.fresh.1", "Don't worry. That won't last."); + provider.add("reflection.cosmiccore.ui.hub.greeting.question", "What brings you to my domain?"); + + // Reflection dialogues (erosion-based) + provider.add("reflection.cosmiccore.ui.reflection.no_erosion.0", "Untouched. Pure. How boring."); + provider.add("reflection.cosmiccore.ui.reflection.no_erosion.1", "You come here with nothing to show?"); + provider.add("reflection.cosmiccore.ui.reflection.no_erosion.2", "That will change. They always change."); + provider.add("reflection.cosmiccore.ui.reflection.low_erosion.0", "Just a taste. That's how it starts."); + provider.add("reflection.cosmiccore.ui.reflection.low_erosion.1", "You'll be back for more."); + provider.add("reflection.cosmiccore.ui.reflection.mid_erosion.0", "Getting comfortable with the darkness?"); + provider.add("reflection.cosmiccore.ui.reflection.mid_erosion.1", "I can see it settling into you."); + provider.add("reflection.cosmiccore.ui.reflection.high_erosion.0", "So much of you is gone now."); + provider.add("reflection.cosmiccore.ui.reflection.high_erosion.1", "Do you remember what you were?"); + provider.add("reflection.cosmiccore.ui.reflection.extreme_erosion.0", "Almost nothing left. Almost."); + provider.add("reflection.cosmiccore.ui.reflection.extreme_erosion.1", + "One more push and you're mine completely."); + provider.add("reflection.cosmiccore.ui.reflection.has_bargains.0", "I see you've made some... arrangements."); + provider.add("reflection.cosmiccore.ui.reflection.has_bargains.1", "Each one a piece of you given away."); + + // Browsing bargains + provider.add("reflection.cosmiccore.ui.browse.interesting_choice", "An interesting choice. Let me show you."); + + // Defiance UI + provider.add("reflection.cosmiccore.ui.defiance.question", "You wish to break this bargain?"); + provider.add("reflection.cosmiccore.ui.defiance.lose_power", "You will lose all powers from this bargain"); + provider.add("reflection.cosmiccore.ui.defiance.scar_remains", "A scar will remain on your soul forever"); + provider.add("reflection.cosmiccore.ui.defiance.confirm", "[Yes, I defy this bargain]"); + provider.add("reflection.cosmiccore.ui.defiance.cancel", "[No, I've changed my mind]"); + provider.add("reflection.cosmiccore.ui.defiance.so_be_it", "So be it. Feel the pain of reclamation."); + provider.add("reflection.cosmiccore.ui.defiance.wise", "Wise. The power is worth more than your principles."); + provider.add("reflection.cosmiccore.ui.defiance.will_lose", "You will lose: %s"); + provider.add("reflection.cosmiccore.ui.defiance.cost_amount", "This will cost you %d erosion"); + provider.add("reflection.cosmiccore.ui.defiance.cannot_undo", "This cannot be undone"); + provider.add("reflection.cosmiccore.ui.defiance.warning1", "You would break the bargain of %s?"); + provider.add("reflection.cosmiccore.ui.defiance.warning2", "The cost of defiance is %d erosion."); + provider.add("reflection.cosmiccore.ui.defiance.warning3", "The power will leave you. The scar will not."); + provider.add("reflection.cosmiccore.ui.defiance.warning4", "Are you certain?"); + + // Constellation UI + provider.add("reflection.cosmiccore.ui.forever_scarred", "Forever Scarred"); + provider.add("reflection.cosmiccore.ui.click_to_bargain", "Click to bargain"); + provider.add("reflection.cosmiccore.ui.click_to_defy", "Click to defy (%d erosion)"); + provider.add("reflection.cosmiccore.ui.power", "Power"); + provider.add("reflection.cosmiccore.ui.drawback", "Drawback"); + + // ========================================================================= + // BARGAINS + // ========================================================================= + + // --- Quake Movement Bargain (quake_movement) --- + // Answers: yes, refuse | Dialogues: 4 + provider.add("reflection.cosmiccore.bargain.quake_movement.name", "Velocity"); + provider.add("reflection.cosmiccore.bargain.quake_movement.description", + "Master momentum itself through unnatural locomotion"); + provider.add("reflection.cosmiccore.bargain.quake_movement.dialogue.0", + "You move like prey. Hesitant. Fearful."); + provider.add("reflection.cosmiccore.bargain.quake_movement.dialogue.1", + "I remember when things moved differently. Before physics became so... rigid."); + provider.add("reflection.cosmiccore.bargain.quake_movement.dialogue.2", + "I can teach your legs to remember that older way."); + provider.add("reflection.cosmiccore.bargain.quake_movement.dialogue.3", + "But once they learn, they will never be content with stillness again."); + provider.add("reflection.cosmiccore.bargain.quake_movement.question", + "Will you embrace the velocity that waits within you?"); + provider.add("reflection.cosmiccore.bargain.quake_movement.answer.yes.text", "Teach me to move like the wind."); + provider.add("reflection.cosmiccore.bargain.quake_movement.answer.yes.response", + "Feel it now - your muscles rewiring, learning trajectories they were never meant to know."); + provider.add("reflection.cosmiccore.bargain.quake_movement.answer.yes.power.0", + "Bunny hopping preserves and builds momentum"); + provider.add("reflection.cosmiccore.bargain.quake_movement.answer.yes.power.1", + "Air strafing for mid-air direction control"); + provider.add("reflection.cosmiccore.bargain.quake_movement.answer.yes.drawback.0", + "Movement feels unnatural to observers"); + provider.add("reflection.cosmiccore.bargain.quake_movement.answer.refuse.text", + "My feet know their own rhythm."); + provider.add("reflection.cosmiccore.bargain.quake_movement.answer.refuse.response", + "Such faith in the pedestrian. We shall see how long that serves you."); + provider.add("reflection.cosmiccore.bargain.quake_movement.on_accept", + "Your joints crack and reform. Movement becomes instinct."); + provider.add("reflection.cosmiccore.bargain.quake_movement.on_defy", + "Your legs remember what it was to walk normally. The speed fades like a dream."); + + // --- Depths Bargain (depths) --- + // Answers: embrace, refuse | Dialogues: 6 + provider.add("reflection.cosmiccore.bargain.depths.name", "The Depths"); + provider.add("reflection.cosmiccore.bargain.depths.description", "Enhanced breath with fatal consequences"); + provider.add("reflection.cosmiccore.bargain.depths.dialogue.0", + "You've felt the water closing over your head."); + provider.add("reflection.cosmiccore.bargain.depths.dialogue.1", + "That desperate burn in your lungs. The panic."); + provider.add("reflection.cosmiccore.bargain.depths.dialogue.2", + "I can remake those fragile organs. Give them capacity beyond mortal limits."); + provider.add("reflection.cosmiccore.bargain.depths.dialogue.3", + "Your breath will stretch to fill the void between heartbeats."); + provider.add("reflection.cosmiccore.bargain.depths.dialogue.4", + "But understand - when they finally empty, there will be no warning."); + provider.add("reflection.cosmiccore.bargain.depths.dialogue.5", + "No desperate gasps. No gradual fading. Just... silence."); + provider.add("reflection.cosmiccore.bargain.depths.question", "Will you let me reshape your breath?"); + provider.add("reflection.cosmiccore.bargain.depths.answer.embrace.text", "Remake me for the depths."); + provider.add("reflection.cosmiccore.bargain.depths.answer.embrace.response", + "Your chest feels hollow now. That's normal. The new capacity needs room."); + provider.add("reflection.cosmiccore.bargain.depths.answer.embrace.power.0", "5x oxygen capacity underwater"); + provider.add("reflection.cosmiccore.bargain.depths.answer.embrace.power.1", + "Extended breath in toxic atmospheres"); + provider.add("reflection.cosmiccore.bargain.depths.answer.embrace.drawback.0", + "Instant death when oxygen fully depletes"); + provider.add("reflection.cosmiccore.bargain.depths.answer.embrace.drawback.1", + "No drowning damage warning - just death"); + provider.add("reflection.cosmiccore.bargain.depths.answer.refuse.text", "I'll keep my mortal breath."); + provider.add("reflection.cosmiccore.bargain.depths.answer.refuse.response", + "The depths wait patiently. They always have."); + provider.add("reflection.cosmiccore.bargain.depths.on_accept", + "Something shifts in your chest. The air tastes different now."); + provider.add("reflection.cosmiccore.bargain.depths.on_defy", + "You gasp - your lungs remember panic, remember struggle. You are mortal again."); + + // --- Swiftness Bargain (swiftness) --- + // Answers: accept, refuse | Dialogues: 3 + provider.add("reflection.cosmiccore.bargain.swiftness.name", "Swiftness"); + provider.add("reflection.cosmiccore.bargain.swiftness.description", + "Supernatural speed courses through your veins"); + provider.add("reflection.cosmiccore.bargain.swiftness.dialogue.0", + "The world moves so slowly around you, doesn't it?"); + provider.add("reflection.cosmiccore.bargain.swiftness.dialogue.1", + "Everyone else trudging through molasses while you ache to run."); + provider.add("reflection.cosmiccore.bargain.swiftness.dialogue.2", + "I can accelerate you. Make the world blur past like a fading dream."); + provider.add("reflection.cosmiccore.bargain.swiftness.question", "Do you wish to leave the slow world behind?"); + provider.add("reflection.cosmiccore.bargain.swiftness.answer.accept.text", "Make me swift beyond measure."); + provider.add("reflection.cosmiccore.bargain.swiftness.answer.accept.response", + "Your blood sings now. Feel it racing faster than any heart should allow."); + provider.add("reflection.cosmiccore.bargain.swiftness.answer.accept.power.0", "+40% movement speed"); + provider.add("reflection.cosmiccore.bargain.swiftness.answer.accept.power.1", "Sprint without hunger drain"); + provider.add("reflection.cosmiccore.bargain.swiftness.answer.accept.drawback.0", + "Increased hunger when standing still"); + provider.add("reflection.cosmiccore.bargain.swiftness.answer.refuse.text", "I am content with my pace."); + provider.add("reflection.cosmiccore.bargain.swiftness.answer.refuse.response", + "Content. Such a mortal sentiment. It will fade."); + provider.add("reflection.cosmiccore.bargain.swiftness.on_accept", + "Lightning arcs through your muscles. You twitch with restless energy."); + provider.add("reflection.cosmiccore.bargain.swiftness.on_defy", + "The world speeds up around you. You are merely human once more."); + + // --- Stride Bargain (stride) --- + // Answers: accept, suspicious, refuse | Dialogues: 4 + provider.add("reflection.cosmiccore.bargain.stride.name", "Stride"); + provider.add("reflection.cosmiccore.bargain.stride.description", "Walk over obstacles as if they were nothing"); + provider.add("reflection.cosmiccore.bargain.stride.dialogue.0", + "Every ledge. Every block. Every small obstacle."); + provider.add("reflection.cosmiccore.bargain.stride.dialogue.1", "They mock you with their need to be climbed."); + provider.add("reflection.cosmiccore.bargain.stride.dialogue.2", + "What if the world simply... accommodated your stride?"); + provider.add("reflection.cosmiccore.bargain.stride.dialogue.3", + "Your feet need never leave the ground. The ground will rise to meet them."); + provider.add("reflection.cosmiccore.bargain.stride.question", "Shall obstacles bow before your passage?"); + provider.add("reflection.cosmiccore.bargain.stride.answer.accept.text", "Let the world flatten before me."); + provider.add("reflection.cosmiccore.bargain.stride.answer.accept.response", + "Walk now. Feel how terrain reshapes itself for your convenience."); + provider.add("reflection.cosmiccore.bargain.stride.answer.accept.power.0", "Auto step-up to 1 block height"); + provider.add("reflection.cosmiccore.bargain.stride.answer.accept.power.1", "Smooth terrain traversal"); + provider.add("reflection.cosmiccore.bargain.stride.answer.accept.drawback.0", "Cannot crouch-walk off edges"); + provider.add("reflection.cosmiccore.bargain.stride.answer.refuse.text", "I'll climb my own way."); + provider.add("reflection.cosmiccore.bargain.stride.answer.refuse.response", + "Such determination. It will erode. They always do."); + provider.add("reflection.cosmiccore.bargain.stride.on_accept", + "The earth seems to shift slightly, eager to smooth your path."); + provider.add("reflection.cosmiccore.bargain.stride.on_defy", + "Gravity reasserts itself. Every ledge is a challenge again."); + + // --- Darksight Bargain (darksight) --- + // Answers: yes, refuse | Dialogues: 5 + provider.add("reflection.cosmiccore.bargain.darksight.name", "Darksight"); + provider.add("reflection.cosmiccore.bargain.darksight.description", "See through the deepest darkness"); + provider.add("reflection.cosmiccore.bargain.darksight.dialogue.0", + "You fear the dark. Every creature does, at first."); + provider.add("reflection.cosmiccore.bargain.darksight.dialogue.1", + "But darkness is merely the absence of something. Not presence."); + provider.add("reflection.cosmiccore.bargain.darksight.dialogue.2", + "I can teach your eyes to drink the shadow. To see what lurks unseen."); + provider.add("reflection.cosmiccore.bargain.darksight.dialogue.3", + "Every hidden corner will surrender its secrets to you."); + provider.add("reflection.cosmiccore.bargain.darksight.dialogue.4", + "But be warned - the light will begin to burn."); + provider.add("reflection.cosmiccore.bargain.darksight.question", + "Will you trade the sun for the gift of shadow-sight?"); + provider.add("reflection.cosmiccore.bargain.darksight.answer.yes.text", "Let me see in the darkness."); + provider.add("reflection.cosmiccore.bargain.darksight.answer.yes.response", + "Your pupils dilate... and keep dilating. The dark becomes your domain."); + provider.add("reflection.cosmiccore.bargain.darksight.answer.yes.power.0", "Permanent Night Vision effect"); + provider.add("reflection.cosmiccore.bargain.darksight.answer.yes.power.1", "See in complete darkness"); + provider.add("reflection.cosmiccore.bargain.darksight.answer.yes.drawback.0", + "Blindness effect in bright sunlight"); + provider.add("reflection.cosmiccore.bargain.darksight.answer.yes.drawback.1", + "Must wear helmet or stay underground during day"); + provider.add("reflection.cosmiccore.bargain.darksight.answer.refuse.text", "The light serves me well enough."); + provider.add("reflection.cosmiccore.bargain.darksight.answer.refuse.response", + "Cling to your torch then. See how long it lasts in the deep places."); + provider.add("reflection.cosmiccore.bargain.darksight.on_accept", + "The shadows retreat from your vision. You see everything now."); + provider.add("reflection.cosmiccore.bargain.darksight.on_defy", + "Light floods back. The darkness closes its secrets to you once more."); + + // --- Carapace Bargain (carapace) --- + // Answers: survive, feel | Dialogues: 5 + provider.add("reflection.cosmiccore.bargain.carapace.name", "Carapace"); + provider.add("reflection.cosmiccore.bargain.carapace.description", "Your flesh hardens into living armor"); + provider.add("reflection.cosmiccore.bargain.carapace.dialogue.0", "Your skin is so soft. So vulnerable."); + provider.add("reflection.cosmiccore.bargain.carapace.dialogue.1", + "Every blade, every claw, every falling stone - they all threaten you."); + provider.add("reflection.cosmiccore.bargain.carapace.dialogue.2", + "I can harden your flesh. Make it something more... enduring."); + provider.add("reflection.cosmiccore.bargain.carapace.dialogue.3", + "Blows will glance off. Damage will diminish."); + provider.add("reflection.cosmiccore.bargain.carapace.dialogue.4", + "But you will feel less. Touch will become... distant."); + provider.add("reflection.cosmiccore.bargain.carapace.question", "Will you sacrifice sensation for survival?"); + provider.add("reflection.cosmiccore.bargain.carapace.answer.survive.text", "Harden me. I choose survival."); + provider.add("reflection.cosmiccore.bargain.carapace.answer.survive.response", + "Your skin tightens. Hardens. You are becoming something more durable."); + provider.add("reflection.cosmiccore.bargain.carapace.answer.survive.power.0", + "+8 armor points (4 full armor icons)"); + provider.add("reflection.cosmiccore.bargain.carapace.answer.survive.power.1", "Stacks with worn armor"); + provider.add("reflection.cosmiccore.bargain.carapace.answer.survive.drawback.0", + "-20% healing from all sources"); + provider.add("reflection.cosmiccore.bargain.carapace.answer.survive.drawback.1", + "Reduced potion effectiveness"); + provider.add("reflection.cosmiccore.bargain.carapace.answer.refuse.text", + "I would rather feel than merely endure."); + provider.add("reflection.cosmiccore.bargain.carapace.answer.refuse.response", + "Feeling. How fragile. How mortal. How temporary."); + provider.add("reflection.cosmiccore.bargain.carapace.on_accept", + "Your flesh ripples and tightens. It doesn't hurt. That's the point."); + provider.add("reflection.cosmiccore.bargain.carapace.on_defy", + "Sensation floods back - every breeze, every texture. You are soft again."); + + // --- Soft Landing Bargain (soft_landing) --- + // Answers: yes, careful | Dialogues: 5 + provider.add("reflection.cosmiccore.bargain.soft_landing.name", "Soft Landing"); + provider.add("reflection.cosmiccore.bargain.soft_landing.description", + "The ground welcomes you gently from any height"); + provider.add("reflection.cosmiccore.bargain.soft_landing.dialogue.0", + "Heights terrify you. The primal fear of falling."); + provider.add("reflection.cosmiccore.bargain.soft_landing.dialogue.1", + "That sickening moment when gravity claims you."); + provider.add("reflection.cosmiccore.bargain.soft_landing.dialogue.2", "But what if the ground... forgave you?"); + provider.add("reflection.cosmiccore.bargain.soft_landing.dialogue.3", + "What if every fall ended softly, no matter the height?"); + provider.add("reflection.cosmiccore.bargain.soft_landing.dialogue.4", "You need never fear the drop again."); + provider.add("reflection.cosmiccore.bargain.soft_landing.question", "Will you let the earth catch you?"); + provider.add("reflection.cosmiccore.bargain.soft_landing.answer.yes.text", "Take away my fear of falling."); + provider.add("reflection.cosmiccore.bargain.soft_landing.answer.yes.response", + "Jump. Go ahead. The ground will embrace you like a mother."); + provider.add("reflection.cosmiccore.bargain.soft_landing.answer.yes.power.0", "Complete fall damage immunity"); + provider.add("reflection.cosmiccore.bargain.soft_landing.answer.yes.power.1", "Safe drops from any height"); + provider.add("reflection.cosmiccore.bargain.soft_landing.answer.yes.drawback.0", + "+15% damage taken from all sources"); + provider.add("reflection.cosmiccore.bargain.soft_landing.answer.yes.drawback.1", + "Reduced knockback resistance"); + provider.add("reflection.cosmiccore.bargain.soft_landing.answer.refuse.text", + "Fear keeps me cautious. I'll keep it."); + provider.add("reflection.cosmiccore.bargain.soft_landing.answer.refuse.response", + "Caution. A slow path to nowhere. But walk it if you must."); + provider.add("reflection.cosmiccore.bargain.soft_landing.on_accept", + "Your relationship with gravity shifts. It still pulls, but gently now."); + provider.add("reflection.cosmiccore.bargain.soft_landing.on_defy", + "Weight crashes back into your bones. Every fall matters again."); + + // --- Cinder Bargain (cinder) --- + // Answers: burn, refuse | Dialogues: 5 + provider.add("reflection.cosmiccore.bargain.cinder.name", "Cinder"); + provider.add("reflection.cosmiccore.bargain.cinder.description", "Fire cannot harm what has already burned"); + provider.add("reflection.cosmiccore.bargain.cinder.dialogue.0", + "Fire consumes. It is in its nature to destroy."); + provider.add("reflection.cosmiccore.bargain.cinder.dialogue.1", + "Every flame that touches you leaves its mark."); + provider.add("reflection.cosmiccore.bargain.cinder.dialogue.2", + "But what if you had already burned? Completely. Utterly."); + provider.add("reflection.cosmiccore.bargain.cinder.dialogue.3", + "Fire cannot consume what is already ash and ember."); + provider.add("reflection.cosmiccore.bargain.cinder.dialogue.4", + "Let me burn away your vulnerability. You will walk through infernos unscathed."); + provider.add("reflection.cosmiccore.bargain.cinder.question", + "Will you let the flame claim you, so it can never hurt you again?"); + provider.add("reflection.cosmiccore.bargain.cinder.answer.burn.text", "Burn me completely. Make me immune."); + provider.add("reflection.cosmiccore.bargain.cinder.answer.burn.response", + "It hurts. Just for a moment. Then the pain becomes a memory, and fire becomes a friend."); + provider.add("reflection.cosmiccore.bargain.cinder.answer.burn.power.0", "Complete fire and lava immunity"); + provider.add("reflection.cosmiccore.bargain.cinder.answer.burn.power.1", "Can swim in lava safely"); + provider.add("reflection.cosmiccore.bargain.cinder.answer.burn.drawback.0", + "2x damage from freezing and cold sources"); + provider.add("reflection.cosmiccore.bargain.cinder.answer.burn.drawback.1", + "Water extinguishes slower, feels unpleasant"); + provider.add("reflection.cosmiccore.bargain.cinder.answer.refuse.text", + "Fire should be respected, not befriended."); + provider.add("reflection.cosmiccore.bargain.cinder.answer.refuse.response", + "Respect. For something that would consume you without thought. How noble."); + provider.add("reflection.cosmiccore.bargain.cinder.on_accept", + "Heat floods through you, then recedes. Fire will never frighten you again."); + provider.add("reflection.cosmiccore.bargain.cinder.on_defy", + "The warmth drains away. Flames flicker hungrily when they see you now."); + + // --- Vitality Bargain (vitality) --- + // Answers: accept, refuse | Dialogues: 5 + provider.add("reflection.cosmiccore.bargain.vitality.name", "Vitality"); + provider.add("reflection.cosmiccore.bargain.vitality.description", "Life force beyond mortal limits"); + provider.add("reflection.cosmiccore.bargain.vitality.dialogue.0", + "Your body has limits. A fixed amount of life."); + provider.add("reflection.cosmiccore.bargain.vitality.dialogue.1", + "When it empties, you die. Simple. Brutal. Final."); + provider.add("reflection.cosmiccore.bargain.vitality.dialogue.2", + "I can give you more. Stretch your life force beyond its natural bounds."); + provider.add("reflection.cosmiccore.bargain.vitality.dialogue.3", + "More blood. More breath. More heartbeats before the end."); + provider.add("reflection.cosmiccore.bargain.vitality.dialogue.4", + "But maintaining excess takes effort. You will heal... slower."); + provider.add("reflection.cosmiccore.bargain.vitality.question", "Will you trade recovery for resilience?"); + provider.add("reflection.cosmiccore.bargain.vitality.answer.accept.text", "Give me more life to spend."); + provider.add("reflection.cosmiccore.bargain.vitality.answer.accept.response", + "Your heart swells. Literally. It has more to pump now."); + provider.add("reflection.cosmiccore.bargain.vitality.answer.accept.power.0", "+10 max health (5 extra hearts)"); + provider.add("reflection.cosmiccore.bargain.vitality.answer.accept.power.1", + "Increased damage absorption buffer"); + provider.add("reflection.cosmiccore.bargain.vitality.answer.accept.drawback.0", + "-50% natural regeneration rate"); + provider.add("reflection.cosmiccore.bargain.vitality.answer.accept.drawback.1", + "Healing potions 30% less effective"); + provider.add("reflection.cosmiccore.bargain.vitality.answer.refuse.text", "I'll work with what I was given."); + provider.add("reflection.cosmiccore.bargain.vitality.answer.refuse.response", + "Given. As if anyone gave you anything. You simply are. For now."); + provider.add("reflection.cosmiccore.bargain.vitality.on_accept", + "Your veins surge with new vigor. Everything feels more... present."); + provider.add("reflection.cosmiccore.bargain.vitality.on_defy", + "The excess drains away. You are mortal-sized once more."); + + // --- Satiated Bargain (satiated) --- + // Answers: empty, food | Dialogues: 5 + provider.add("reflection.cosmiccore.bargain.satiated.name", "Satiated"); + provider.add("reflection.cosmiccore.bargain.satiated.description", "Hunger becomes a distant memory"); + provider.add("reflection.cosmiccore.bargain.satiated.dialogue.0", + "The constant gnawing. The endless need to consume."); + provider.add("reflection.cosmiccore.bargain.satiated.dialogue.1", "Your stomach rules you like a tyrant."); + provider.add("reflection.cosmiccore.bargain.satiated.dialogue.2", + "What if I could silence it? Make fullness your natural state?"); + provider.add("reflection.cosmiccore.bargain.satiated.dialogue.3", + "You would eat for taste, for ritual - never for need."); + provider.add("reflection.cosmiccore.bargain.satiated.dialogue.4", + "But taste itself would fade. Food becomes... fuel. Nothing more."); + provider.add("reflection.cosmiccore.bargain.satiated.question", + "Will you trade the pleasure of eating for freedom from hunger?"); + provider.add("reflection.cosmiccore.bargain.satiated.answer.empty.text", "Free me from this hunger."); + provider.add("reflection.cosmiccore.bargain.satiated.answer.empty.response", + "The emptiness fades. You will never truly need to eat again."); + provider.add("reflection.cosmiccore.bargain.satiated.answer.empty.power.0", "Hunger depletes 80% slower"); + provider.add("reflection.cosmiccore.bargain.satiated.answer.empty.power.1", "Food provides 3x saturation"); + provider.add("reflection.cosmiccore.bargain.satiated.answer.empty.drawback.0", + "Food restores 50% less hunger bars"); + provider.add("reflection.cosmiccore.bargain.satiated.answer.empty.drawback.1", + "Cannot benefit from food-based buffs"); + provider.add("reflection.cosmiccore.bargain.satiated.answer.refuse.text", + "I enjoy my meals. I'll keep the hunger."); + provider.add("reflection.cosmiccore.bargain.satiated.answer.refuse.response", + "Enjoy. Such a mortal word. The hunger will remind you of your place."); + provider.add("reflection.cosmiccore.bargain.satiated.on_accept", + "The gnawing stops. Silence in your belly. Freedom."); + provider.add("reflection.cosmiccore.bargain.satiated.on_defy", + "Hunger returns with a vengeance. You remember what need feels like."); + + // --- Back Bargain (back) --- + // Answers: accept, refuse | Dialogues: 3 + provider.add("reflection.cosmiccore.bargain.back.name", "The Way Back"); + provider.add("reflection.cosmiccore.bargain.back.description", "Return to where you fell"); + provider.add("reflection.cosmiccore.bargain.back.dialogue.0", + "Death scatters you. Sends you back to beds, to spawns, to arbitrary points."); + provider.add("reflection.cosmiccore.bargain.back.dialogue.1", + "But what if you could return to where you fell?"); + provider.add("reflection.cosmiccore.bargain.back.dialogue.2", + "I can teach you to remember. To reach back through death itself."); + provider.add("reflection.cosmiccore.bargain.back.question", "Will you learn to retrace death's path?"); + provider.add("reflection.cosmiccore.bargain.back.answer.accept.text", "Teach me to find my way back."); + provider.add("reflection.cosmiccore.bargain.back.answer.accept.response", + "Death becomes a waypoint now. Not an ending - a detour."); + provider.add("reflection.cosmiccore.bargain.back.answer.accept.power.0", + "Teleport to death location (once per death)"); + provider.add("reflection.cosmiccore.bargain.back.answer.accept.power.1", "Death marker visible through walls"); + provider.add("reflection.cosmiccore.bargain.back.answer.accept.drawback.0", "5 erosion cost per teleport use"); + provider.add("reflection.cosmiccore.bargain.back.answer.accept.drawback.1", "Marker fades after 10 minutes"); + provider.add("reflection.cosmiccore.bargain.back.answer.refuse.text", "Death should have consequences."); + provider.add("reflection.cosmiccore.bargain.back.answer.refuse.response", + "Consequences. You'll have plenty of those regardless."); + provider.add("reflection.cosmiccore.bargain.back.on_accept", + "A thread connects you to your last breath. You can follow it back."); + provider.add("reflection.cosmiccore.bargain.back.on_defy", "The thread snaps. Death becomes final once more."); + + // --- Home Bargain (home) --- + // Answers: accept, refuse | Dialogues: 3 + provider.add("reflection.cosmiccore.bargain.home.name", "Homeward"); + provider.add("reflection.cosmiccore.bargain.home.description", "The way home is always open"); + provider.add("reflection.cosmiccore.bargain.home.dialogue.0", "Home. Such a powerful concept for mortals."); + provider.add("reflection.cosmiccore.bargain.home.dialogue.1", + "The place you return to. The anchor that grounds you."); + provider.add("reflection.cosmiccore.bargain.home.dialogue.2", + "I can make that connection stronger. Instant. Unbreakable."); + provider.add("reflection.cosmiccore.bargain.home.question", + "Will you bind yourself to your home with chains of void?"); + provider.add("reflection.cosmiccore.bargain.home.answer.accept.text", "Bind me to my home."); + provider.add("reflection.cosmiccore.bargain.home.answer.accept.response", + "Feel the pull now? Home is never more than a thought away."); + provider.add("reflection.cosmiccore.bargain.home.answer.accept.power.0", "Instant teleport to spawn/bed point"); + provider.add("reflection.cosmiccore.bargain.home.answer.accept.power.1", "5 minute cooldown between uses"); + provider.add("reflection.cosmiccore.bargain.home.answer.accept.drawback.0", "10 erosion cost per teleport"); + provider.add("reflection.cosmiccore.bargain.home.answer.accept.drawback.1", "-10% XP gain while far from home"); + provider.add("reflection.cosmiccore.bargain.home.answer.refuse.text", "Home should be earned, not summoned."); + provider.add("reflection.cosmiccore.bargain.home.answer.refuse.response", + "Earned. Through miles of walking. How charmingly primitive."); + provider.add("reflection.cosmiccore.bargain.home.on_accept", + "A cord of void stretches between you and home. Pull it anytime."); + provider.add("reflection.cosmiccore.bargain.home.on_defy", + "The cord dissolves. Home is a journey again, not a destination."); + + // --- Ascension Bargain (ascension) --- + // Answers: ready, refuse | Dialogues: 6 + provider.add("reflection.cosmiccore.bargain.ascension.name", "Ascension"); + provider.add("reflection.cosmiccore.bargain.ascension.description", "Rise above the crawling earth"); + provider.add("reflection.cosmiccore.bargain.ascension.dialogue.0", + "You are bound to the ground. Chained by gravity's petty tyranny."); + provider.add("reflection.cosmiccore.bargain.ascension.dialogue.1", "You dream of flight. All mortals do."); + provider.add("reflection.cosmiccore.bargain.ascension.dialogue.2", "I can sever those chains. Let you rise."); + provider.add("reflection.cosmiccore.bargain.ascension.dialogue.3", + "Not gliding. Not falling with style. True flight."); + provider.add("reflection.cosmiccore.bargain.ascension.dialogue.4", "The sky will open to you like a door."); + provider.add("reflection.cosmiccore.bargain.ascension.dialogue.5", + "But the ground... the ground will become alien. Uncomfortable. Wrong."); + provider.add("reflection.cosmiccore.bargain.ascension.question", "Will you abandon the earth for the sky?"); + provider.add("reflection.cosmiccore.bargain.ascension.answer.ready.text", "I am ready to fly."); + provider.add("reflection.cosmiccore.bargain.ascension.answer.ready.response", + "Then rise. The ground has no claim on you anymore."); + provider.add("reflection.cosmiccore.bargain.ascension.answer.ready.power.0", + "Creative-style flight (toggle with jump while airborne)"); + provider.add("reflection.cosmiccore.bargain.ascension.answer.ready.power.1", + "Fly indefinitely without hunger or stamina cost"); + provider.add("reflection.cosmiccore.bargain.ascension.answer.ready.power.2", + "Full 3D movement control while flying"); + provider.add("reflection.cosmiccore.bargain.ascension.answer.ready.power.3", + "No fall damage while flight is active"); + provider.add("reflection.cosmiccore.bargain.ascension.answer.ready.drawback.0", + "-30% movement speed when not flying"); + provider.add("reflection.cosmiccore.bargain.ascension.answer.ready.drawback.1", + "Vulnerable in no-fly zones or enclosed spaces"); + provider.add("reflection.cosmiccore.bargain.ascension.answer.refuse.text", "The ground has served me well."); + provider.add("reflection.cosmiccore.bargain.ascension.answer.refuse.response", + "Served. Like a servant. How long until you realize you were the servant all along?"); + provider.add("reflection.cosmiccore.bargain.ascension.on_accept", + "Weight leaves you. The sky opens. You are no longer earth-bound."); + provider.add("reflection.cosmiccore.bargain.ascension.on_defy", + "Gravity reclaims you. The ground welcomes you back, possessively."); + + // --- Violence Bargain (violence) --- + // Answers: accept, refuse | Dialogues: 5 + provider.add("reflection.cosmiccore.bargain.violence.name", "Violence"); + provider.add("reflection.cosmiccore.bargain.violence.description", + "Strike with the force of something terrible"); + provider.add("reflection.cosmiccore.bargain.violence.dialogue.0", "Your blows are so... restrained. Hesitant."); + provider.add("reflection.cosmiccore.bargain.violence.dialogue.1", + "You hold back. Every swing. Some part of you fears the damage."); + provider.add("reflection.cosmiccore.bargain.violence.dialogue.2", + "I can remove that restraint. Let your violence flow freely."); + provider.add("reflection.cosmiccore.bargain.violence.dialogue.3", "Your enemies will shatter before you."); + provider.add("reflection.cosmiccore.bargain.violence.dialogue.4", + "But violence is a river that flows both ways."); + provider.add("reflection.cosmiccore.bargain.violence.question", + "Will you embrace true, unrestrained violence?"); + provider.add("reflection.cosmiccore.bargain.violence.answer.accept.text", "Remove my restraints."); + provider.add("reflection.cosmiccore.bargain.violence.answer.accept.response", + "Feel it now? The urge to destroy? Don't fight it. It's yours."); + provider.add("reflection.cosmiccore.bargain.violence.answer.accept.power.0", "+30% melee damage dealt"); + provider.add("reflection.cosmiccore.bargain.violence.answer.accept.power.1", "+15% attack speed"); + provider.add("reflection.cosmiccore.bargain.violence.answer.accept.drawback.0", + "+20% damage taken from all sources"); + provider.add("reflection.cosmiccore.bargain.violence.answer.accept.drawback.1", "Cannot use shields"); + provider.add("reflection.cosmiccore.bargain.violence.answer.refuse.text", "Restraint is its own strength."); + provider.add("reflection.cosmiccore.bargain.violence.answer.refuse.response", + "Restraint. A leash you put on yourself. How adorable."); + provider.add("reflection.cosmiccore.bargain.violence.on_accept", + "Power surges through your arms. Everything looks so... breakable now."); + provider.add("reflection.cosmiccore.bargain.violence.on_defy", + "The rage drains away. Your blows return to mortal weight."); + + // --- Reach Bargain (reach) --- + // Answers: further, practical, refuse | Dialogues: 4 + provider.add("reflection.cosmiccore.bargain.reach.name", "Reach"); + provider.add("reflection.cosmiccore.bargain.reach.description", "Your grasp extends beyond natural limits"); + provider.add("reflection.cosmiccore.bargain.reach.dialogue.0", + "So close, yet so far. The eternal frustration of short arms."); + provider.add("reflection.cosmiccore.bargain.reach.dialogue.1", + "Everything just slightly out of reach. Mocking you."); + provider.add("reflection.cosmiccore.bargain.reach.dialogue.2", + "I can extend you. Stretch your grasp beyond mortal limits."); + provider.add("reflection.cosmiccore.bargain.reach.dialogue.3", + "Your arms will find what they seek. But others may find them... unsettling."); + provider.add("reflection.cosmiccore.bargain.reach.question", "Will you extend your reach into the unnatural?"); + provider.add("reflection.cosmiccore.bargain.reach.answer.further.text", + "Stretch me further. I want to grasp everything."); + provider.add("reflection.cosmiccore.bargain.reach.answer.further.response", + "There. Don't look at your hands too closely. It's easier that way."); + provider.add("reflection.cosmiccore.bargain.reach.answer.further.power.0", + "+3 block reach (build from further)"); + provider.add("reflection.cosmiccore.bargain.reach.answer.further.power.1", "+2 attack reach"); + provider.add("reflection.cosmiccore.bargain.reach.answer.further.drawback.0", "-15% mining speed"); + provider.add("reflection.cosmiccore.bargain.reach.answer.further.drawback.1", "Item pickup range reduced"); + provider.add("reflection.cosmiccore.bargain.reach.answer.refuse.text", "My reach is sufficient."); + provider.add("reflection.cosmiccore.bargain.reach.answer.refuse.response", + "For now. But you'll want more. They always do."); + provider.add("reflection.cosmiccore.bargain.reach.on_accept", + "Something shifts in your shoulders. Your arms remember being longer."); + provider.add("reflection.cosmiccore.bargain.reach.on_defy", + "Your arms contract back to normal. The world feels close and small again."); + + // --- Void Anchor Bargain (void_anchor) --- + // Answers: anchor, refuse | Dialogues: 6 + provider.add("reflection.cosmiccore.bargain.void_anchor.name", "Void Anchor"); + provider.add("reflection.cosmiccore.bargain.void_anchor.description", + "The void cannot claim what is already its own"); + provider.add("reflection.cosmiccore.bargain.void_anchor.dialogue.0", + "You've felt it. The pull of the void beneath the world."); + provider.add("reflection.cosmiccore.bargain.void_anchor.dialogue.1", + "That endless fall. That final darkness that swallows everything."); + provider.add("reflection.cosmiccore.bargain.void_anchor.dialogue.2", + "I dwell there. In that space between existence and nothing."); + provider.add("reflection.cosmiccore.bargain.void_anchor.dialogue.3", + "I can mark you. Make you mine. And what is mine, the void cannot destroy."); + provider.add("reflection.cosmiccore.bargain.void_anchor.dialogue.4", + "Fall as far as you like. The darkness will recognize you. Welcome you."); + provider.add("reflection.cosmiccore.bargain.void_anchor.dialogue.5", + "But being marked by the void... it changes how existence sees you."); + provider.add("reflection.cosmiccore.bargain.void_anchor.question", "Will you become an anchor in the nothing?"); + provider.add("reflection.cosmiccore.bargain.void_anchor.answer.anchor.text", "Mark me. Make me yours."); + provider.add("reflection.cosmiccore.bargain.void_anchor.answer.anchor.response", + "Done. The void knows your name now. It will not harm what belongs to it."); + provider.add("reflection.cosmiccore.bargain.void_anchor.answer.anchor.power.0", "Void damage immunity (Y < 0)"); + provider.add("reflection.cosmiccore.bargain.void_anchor.answer.anchor.power.1", + "Teleport to surface when entering void"); + provider.add("reflection.cosmiccore.bargain.void_anchor.answer.anchor.drawback.0", + "-25% damage in lit areas (sky access)"); + provider.add("reflection.cosmiccore.bargain.void_anchor.answer.anchor.drawback.1", + "Takes damage from direct sunlight exposure"); + provider.add("reflection.cosmiccore.bargain.void_anchor.answer.refuse.text", + "I'll stay in the light, thank you."); + provider.add("reflection.cosmiccore.bargain.void_anchor.answer.refuse.response", + "The light. Yes. Such a thin shield against the endless dark. Good luck."); + provider.add("reflection.cosmiccore.bargain.void_anchor.on_accept", + "Something cold touches your soul. Marks it. The void knows you now."); + provider.add("reflection.cosmiccore.bargain.void_anchor.on_defy", + "The mark burns away. The void forgets you. It will not be merciful next time."); + + // ========================================================================= + // THRESHOLD ENCOUNTERS + // ========================================================================= + + // --- Threshold 0 (10% erosion) --- + provider.add("reflection.cosmiccore.threshold.0.dialogue.0", "You took something that wasn't freely given."); + provider.add("reflection.cosmiccore.threshold.0.dialogue.1", "Did it feel good? The power flooding in?"); + provider.add("reflection.cosmiccore.threshold.0.dialogue.2", "Of course it did. That's the point."); + provider.add("reflection.cosmiccore.threshold.0.dialogue.3", + "I'll be watching now. We have business together."); + provider.add("reflection.cosmiccore.threshold.0.question", "Do you understand what you've started?"); + provider.add("reflection.cosmiccore.threshold.0.response", "Good. Or not. It doesn't matter now."); + + // --- Threshold 1 (20% erosion) --- + provider.add("reflection.cosmiccore.threshold.1.dialogue.0", "Already back for more. I'm not surprised."); + provider.add("reflection.cosmiccore.threshold.1.dialogue.1", "Your soul stretches thinner. Can you feel it?"); + provider.add("reflection.cosmiccore.threshold.1.dialogue.2", "Like taffy. Like mist. Like a memory fading."); + provider.add("reflection.cosmiccore.threshold.1.dialogue.3", "Don't worry. You have plenty left. For now."); + provider.add("reflection.cosmiccore.threshold.1.question", "Still comfortable?"); + provider.add("reflection.cosmiccore.threshold.1.response", "Comfort is overrated anyway."); + + // --- Threshold 2 (30% erosion) --- + provider.add("reflection.cosmiccore.threshold.2.dialogue.0", "A third of you belongs to me now."); + provider.add("reflection.cosmiccore.threshold.2.dialogue.1", "That's not metaphor. I can see it. Taste it."); + provider.add("reflection.cosmiccore.threshold.2.dialogue.2", "Your edges blur. Your definition softens."); + provider.add("reflection.cosmiccore.threshold.2.dialogue.3", "Others might start to notice soon."); + provider.add("reflection.cosmiccore.threshold.2.question", "Having second thoughts?"); + provider.add("reflection.cosmiccore.threshold.2.response", + "Second thoughts require a first. You never had one."); + + // --- Threshold 3 (40% erosion) --- + provider.add("reflection.cosmiccore.threshold.3.dialogue.0", "The dreams are starting, aren't they?"); + provider.add("reflection.cosmiccore.threshold.3.dialogue.1", "The ones where you fall and never land."); + provider.add("reflection.cosmiccore.threshold.3.dialogue.2", + "Where you look in a mirror and something else looks back."); + provider.add("reflection.cosmiccore.threshold.3.dialogue.3", "That's not a dream. That's prophecy."); + provider.add("reflection.cosmiccore.threshold.3.question", "Do you still know who you are?"); + provider.add("reflection.cosmiccore.threshold.3.response", "Keep telling yourself that name means something."); + + // --- Threshold 4 (50% erosion) --- + provider.add("reflection.cosmiccore.threshold.4.dialogue.0", "Halfway. The point of no return approaches."); + provider.add("reflection.cosmiccore.threshold.4.dialogue.1", "Half of you, gone. Given away for trinkets."); + provider.add("reflection.cosmiccore.threshold.4.dialogue.2", "Was it worth it? Don't answer. I don't care."); + provider.add("reflection.cosmiccore.threshold.4.dialogue.3", "What matters is what comes next."); + provider.add("reflection.cosmiccore.threshold.4.question", "Ready to see what's on the other side?"); + provider.add("reflection.cosmiccore.threshold.4.response", "No one ever is. But they cross anyway."); + + // --- Threshold 5 (60% erosion) --- + provider.add("reflection.cosmiccore.threshold.5.dialogue.0", "More of you is mine than yours now."); + provider.add("reflection.cosmiccore.threshold.5.dialogue.1", "Does that frighten you? It should."); + provider.add("reflection.cosmiccore.threshold.5.dialogue.2", "I know thoughts you haven't had yet."); + provider.add("reflection.cosmiccore.threshold.5.dialogue.3", "I feel feelings you've forgotten."); + provider.add("reflection.cosmiccore.threshold.5.question", "Who's really in control?"); + provider.add("reflection.cosmiccore.threshold.5.response", "Keep pretending you still have choice."); + + // --- Threshold 6 (70% erosion) --- + provider.add("reflection.cosmiccore.threshold.6.dialogue.0", "Your reflection doesn't quite match anymore."); + provider.add("reflection.cosmiccore.threshold.6.dialogue.1", "The delay is slight. Others might not notice."); + provider.add("reflection.cosmiccore.threshold.6.dialogue.2", "But you know. You feel it."); + provider.add("reflection.cosmiccore.threshold.6.question", "What stares back at you in mirrors?"); + provider.add("reflection.cosmiccore.threshold.6.response", "Me. Always me now."); + + // --- Threshold 7 (80% erosion) --- + provider.add("reflection.cosmiccore.threshold.7.dialogue.0", "So little left of what you were."); + provider.add("reflection.cosmiccore.threshold.7.dialogue.1", "Fragments. Echoes. Shadows of intention."); + provider.add("reflection.cosmiccore.threshold.7.dialogue.2", "The body walks. The mind calculates."); + provider.add("reflection.cosmiccore.threshold.7.dialogue.3", "But the soul? The soul is almost spent."); + provider.add("reflection.cosmiccore.threshold.7.question", "Can you remember your mother's face?"); + provider.add("reflection.cosmiccore.threshold.7.response", "No. You can't. I took that already."); + + // --- Threshold 8 (90% erosion) --- + provider.add("reflection.cosmiccore.threshold.8.dialogue.0", "One step from the edge now."); + provider.add("reflection.cosmiccore.threshold.8.dialogue.1", "Ten percent. A sliver. A thread."); + provider.add("reflection.cosmiccore.threshold.8.dialogue.2", "That's all that separates you from... me."); + provider.add("reflection.cosmiccore.threshold.8.dialogue.3", "One more bargain. Just one more."); + provider.add("reflection.cosmiccore.threshold.8.question", "Will you take that final step?"); + provider.add("reflection.cosmiccore.threshold.8.response", "We both know you will. The question is when."); + + // --- Threshold 9 (100% erosion) --- + provider.add("reflection.cosmiccore.threshold.9.dialogue.0", "Finally."); + provider.add("reflection.cosmiccore.threshold.9.dialogue.1", "You gave everything. Every last piece."); + provider.add("reflection.cosmiccore.threshold.9.dialogue.2", "There's nothing left of what walked in here."); + provider.add("reflection.cosmiccore.threshold.9.dialogue.3", "Only power. Only hunger. Only me."); + provider.add("reflection.cosmiccore.threshold.9.question", "What do you see when you look at yourself?"); + provider.add("reflection.cosmiccore.threshold.9.response", "Nothing. Because there's nothing left to see."); + + // Stellar Iris Module System + provider.add("cosmiccore.multiblock.stellar_module.not_connected", "§cNot Connected to Stellar Iris"); + provider.add("cosmiccore.multiblock.stellar_module.iris_not_formed", "§cStellar Iris Not Formed"); + provider.add("cosmiccore.multiblock.stellar_module.iris_not_ready", "§eStellar Iris Not Ready"); + provider.add("cosmiccore.multiblock.stellar_module.connected", "§aConnected to Stellar Iris"); + provider.add("cosmiccore.multiblock.stellar_module.stage", "§7Iris Stage: §e%s"); + provider.add("cosmiccore.multiblock.stellar_module.speed_bonus", "§7Speed Bonus: §a%s"); + provider.add("cosmiccore.multiblock.stellar_module.parallel", "§7Parallel Limit: §b%s"); + provider.add("cosmiccore.multiblock.stellar_module.no_wireless", "§cNo Wireless Energy Network"); + provider.add("cosmiccore.multiblock.stellar_module.energy_usage", "§eWireless EU/t: §f%s"); + provider.add("cosmiccore.multiblock.stellar_module.loading", "§7Loading..."); + provider.add("cosmiccore.multiblock.stellar_module.power_failure", "§c§lPOWER FAILURE - Insufficient Energy!"); + provider.add("cosmiccore.multiblock.stellar_module.power_config", "§7Config: §b%s §7@ §a%dx §7Parallel"); + provider.add("cosmiccore.multiblock.pattern.stellar_module_slot", "§7Module Slot (Air or Formed Module)"); + + // JADE Stellar Module Provider + provider.add("cosmiccore.jade.stellar_module.not_connected", "Iris: Not Connected"); + provider.add("cosmiccore.jade.stellar_module.iris_not_ready", "Iris: Not Ready"); + provider.add("cosmiccore.jade.stellar_module.connected", "Iris: Connected"); + provider.add("cosmiccore.jade.stellar_module.stage", "Stage: %s"); + provider.add("cosmiccore.jade.stellar_module.speed_bonus", "Speed: %s"); + provider.add("cosmiccore.jade.stellar_module.no_wireless", "No Wireless Network"); + provider.add("cosmiccore.jade.stellar_module.energy_usage", "Usage: %s"); + + // Stellar Iris GUI - Module Toggle + provider.add("cosmiccore.gui.stellar.show_star", "Show Star View"); + provider.add("cosmiccore.gui.stellar.show_modules", "Show Module Control"); } } diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/data/materials/CosmicMaterials.java b/src/main/java/com/ghostipedia/cosmiccore/common/data/materials/CosmicMaterials.java index 0e0757eb3..dedf7014f 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/common/data/materials/CosmicMaterials.java +++ b/src/main/java/com/ghostipedia/cosmiccore/common/data/materials/CosmicMaterials.java @@ -70,6 +70,9 @@ public class CosmicMaterials { public static Material Lumium; public static Material Signalum; public static Material Enderium; + public static Material Tenbrium; + public static Material Halizine; + public static Material Rosmotosin; public static Material VibrantAlloy; public static Material EnergeticAlloy; @@ -299,6 +302,33 @@ public static void register() { .blastTemp(4500, BlastProperty.GasTier.HIGH, GTValues.VA[GTValues.EV], 1200) .buildAndRegister(); + Tenbrium = new Material.Builder(CosmicCore.id("tenbrium")) + .ingot() + .liquid(new FluidBuilder().temperature(1340)) + .color(0x8a8a8a).secondaryColor(0x404040).iconSet(MaterialIconSet.SHINY) + .flags(GENERATE_BOLT_SCREW, GENERATE_ROUND, GENERATE_GEAR, GENERATE_SMALL_GEAR, GENERATE_RING, + GENERATE_FRAME, GENERATE_SPRING, GENERATE_SPRING_SMALL, GENERATE_FINE_WIRE, GENERATE_DENSE) + .blastTemp(4500, BlastProperty.GasTier.HIGH, GTValues.VA[GTValues.EV], 1200) + .buildAndRegister(); + + Halizine = new Material.Builder(CosmicCore.id("halizine")) + .ingot() + .liquid(new FluidBuilder().temperature(1340)) + .color(0x8b1a7d).secondaryColor(0x3d0835).iconSet(MaterialIconSet.SHINY) + .flags(GENERATE_BOLT_SCREW, GENERATE_ROUND, GENERATE_GEAR, GENERATE_SMALL_GEAR, GENERATE_RING, + GENERATE_FRAME, GENERATE_SPRING, GENERATE_SPRING_SMALL, GENERATE_FINE_WIRE, GENERATE_DENSE) + .blastTemp(4500, BlastProperty.GasTier.HIGH, GTValues.VA[GTValues.EV], 1200) + .buildAndRegister(); + + Rosmotosin = new Material.Builder(CosmicCore.id("rosmotosin")) + .ingot() + .liquid(new FluidBuilder().temperature(1340)) + .color(0xdc143c).secondaryColor(0x8b0000).iconSet(CosmicMaterialSet.CRYSTAL) + .flags(GENERATE_BOLT_SCREW, GENERATE_ROUND, GENERATE_GEAR, GENERATE_SMALL_GEAR, GENERATE_RING, + GENERATE_FRAME, GENERATE_SPRING, GENERATE_SPRING_SMALL, GENERATE_FINE_WIRE, GENERATE_DENSE) + .blastTemp(4500, BlastProperty.GasTier.HIGH, GTValues.VA[GTValues.EV], 1200) + .buildAndRegister(); + VoidSpark = new Material.Builder(CosmicCore.id("voidspark")) .ingot() .liquid(new FluidBuilder().temperature(933)) diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/data/recipe/CosmicRecipeModifiers.java b/src/main/java/com/ghostipedia/cosmiccore/common/data/recipe/CosmicRecipeModifiers.java index 0ffb6c940..e1c56978c 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/common/data/recipe/CosmicRecipeModifiers.java +++ b/src/main/java/com/ghostipedia/cosmiccore/common/data/recipe/CosmicRecipeModifiers.java @@ -1,5 +1,6 @@ package com.ghostipedia.cosmiccore.common.data.recipe; +import com.ghostipedia.cosmiccore.api.machine.multiblock.StellarBaseModule; import com.ghostipedia.cosmiccore.common.data.CosmicItems; import com.ghostipedia.cosmiccore.common.machine.multiblock.electric.MagneticFieldMachine; import com.ghostipedia.cosmiccore.common.machine.multiblock.multi.logic.LarvaMachine; @@ -13,6 +14,7 @@ import com.gregtechceu.gtceu.api.machine.multiblock.MultiblockControllerMachine; import com.gregtechceu.gtceu.api.machine.multiblock.WorkableMultiblockMachine; import com.gregtechceu.gtceu.api.recipe.GTRecipe; +import com.gregtechceu.gtceu.api.recipe.OverclockingLogic; import com.gregtechceu.gtceu.api.recipe.RecipeHelper; import com.gregtechceu.gtceu.api.recipe.content.ContentModifier; import com.gregtechceu.gtceu.api.recipe.modifier.ModifierFunction; @@ -35,6 +37,61 @@ public class CosmicRecipeModifiers { public static final RecipeModifier COSMIC_MODULES = CosmicRecipeModifiers::moduleParallel; + /** + * Recipe modifier for Stellar Modules. + * Uses the module's configured voltage and parallel settings. + * - Applies overclocking based on configured voltage per parallel + * - Applies parallelization up to the effective parallel limit (min of configured and Iris limit) + */ + public static final RecipeModifier STELLAR_MODULE_OVERCLOCK = CosmicRecipeModifiers::stellarModuleOverclock; + + /** + * Stellar module overclock logic. + * Uses configuredVoltagePerParallel for OC tier and configuredMaxParallel for parallels. + */ + public static @NotNull ModifierFunction stellarModuleOverclock(MetaMachine machine, GTRecipe recipe) { + if (!(machine instanceof StellarBaseModule module)) { + return ModifierFunction.NULL; + } + + // Check if recipe tier is within our configured tier + int recipeTier = RecipeHelper.getRecipeEUtTier(recipe); + int moduleTier = module.getOverclockTier(); + if (recipeTier > moduleTier) { + return ModifierFunction.NULL; // Recipe requires higher tier than configured + } + + // Get the effective parallel limit (min of user config and Iris limit) + int maxParallels = module.getEffectiveParallelLimit(); + + // Calculate actual parallels based on available resources + int actualParallels = ParallelLogic.getParallelAmount(machine, recipe, maxParallels); + if (actualParallels == 0) { + return ModifierFunction.NULL; + } + + // Calculate maximum voltage for overclocking + // Total voltage = voltage per parallel * number of parallels + long maxVoltage = module.getConfiguredVoltagePerParallel() * actualParallels; + + // Apply overclock using non-perfect subtick logic + // This uses the configured voltage as the maximum + var ocModifier = OverclockingLogic.NON_PERFECT_OVERCLOCK_SUBTICK.getModifier( + machine, recipe, maxVoltage, false); // Don't let OC logic add more parallels + + // Apply parallel modifier + if (actualParallels > 1) { + var parallelModifier = ModifierFunction.builder() + .modifyAllContents(ContentModifier.multiplier(actualParallels)) + .eutMultiplier(actualParallels) + .parallels(actualParallels) + .build(); + return ocModifier.andThen(parallelModifier); + } + + return ocModifier; + } + public static ModifierFunction asteroidYieldModifier(MetaMachine machine, GTRecipe recipe) { if (!(machine instanceof IRecipeLogicMachine recipeLogicMachine)) { return ModifierFunction.NULL; diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/item/OxygenTankItem.java b/src/main/java/com/ghostipedia/cosmiccore/common/item/OxygenTankItem.java new file mode 100644 index 000000000..fdda47b1a --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/item/OxygenTankItem.java @@ -0,0 +1,106 @@ +package com.ghostipedia.cosmiccore.common.item; + +import com.gregtechceu.gtceu.api.item.ComponentItem; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.fluids.capability.IFluidHandlerItem; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class OxygenTankItem extends ComponentItem { + + public OxygenTankItem(Properties props) { + super(props); + } + + @Override + public boolean isBarVisible(@NotNull ItemStack stack) { + return true; + } + + @Override + public int getBarWidth(@NotNull ItemStack stack) { + IFluidHandlerItem h = stack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).orElse(null); + if (h == null) return 0; + int amount = h.getFluidInTank(0).getAmount(); + int cap = Math.max(1, h.getTankCapacity(0)); + return Math.round(13.0f * amount / cap); + } + + @Override + public int getBarColor(@NotNull ItemStack stack) { + return 0x55D8FF; // Light blue for oxygen + } + + @Override + public void appendHoverText(@NotNull ItemStack stack, @Nullable Level level, + @NotNull List tooltip, @NotNull TooltipFlag flag) { + IFluidHandlerItem h = stack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).orElse(null); + if (h == null) return; + + int amt = h.getFluidInTank(0).getAmount(); + int cap = h.getTankCapacity(0); + tooltip.add(line("Oxygen", amt + " / " + cap + " mB", ChatFormatting.AQUA)); + + // Read tuning written by the behavior into NBT + var tag = stack.getOrCreateTag().getCompound("CosmicCoreO2"); + int ticksPerMb = tag.getInt("TicksPerMb"); + int transferPerTick = tag.getInt("TransferPerTick"); + + // Fallbacks if capability hasn't been touched yet + if (ticksPerMb <= 0 || transferPerTick < 0) { + stack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).orElse(null); + tag = stack.getOrCreateTag().getCompound("CosmicCoreO2"); + ticksPerMb = Math.max(1, tag.getInt("TicksPerMb")); + transferPerTick = Math.max(0, tag.getInt("TransferPerTick")); + } + + int ticksPerSec = transferPerTick * 20; + + // Fluid use at max output + double mbPerTickAtMax = Math.min(1.0, transferPerTick / (double) ticksPerMb); + double mbPerSecAtMax = mbPerTickAtMax * 20.0; + + tooltip.add(line("Max Output", transferPerTick + " O\u2082/t (" + ticksPerSec + "/s)", ChatFormatting.GRAY)); + tooltip.add(line("Conversion", ticksPerMb + " O\u2082-ticks per mB", ChatFormatting.GRAY)); + tooltip.add(line("Use @ Max", fmt(mbPerTickAtMax) + " mB/t (" + fmt(mbPerSecAtMax) + " mB/s)", + ChatFormatting.DARK_GRAY)); + + if (cap > 0 && transferPerTick > 0) { + long totalOTicks = (long) amt * (long) ticksPerMb; + long runGTicks = (long) Math.floor(totalOTicks / (double) transferPerTick); + tooltip.add(line("Est. Runtime @ Max", formatDurationSeconds(runGTicks / 20.0), ChatFormatting.DARK_GREEN)); + } + + // Requirement warning + tooltip.add(Component.empty()); + tooltip.add(Component.literal("Requires Pressurized Rebreather").withStyle(ChatFormatting.RED)); + } + + private static Component line(String label, String value, ChatFormatting color) { + return Component.literal(label + ": ").withStyle(ChatFormatting.WHITE) + .append(Component.literal(value).withStyle(color)); + } + + private static String fmt(double d) { + if (d >= 10) return String.format("%.0f", d); + if (d >= 1) return String.format("%.2f", d); + return String.format("%.3f", d); + } + + private static String formatDurationSeconds(double seconds) { + if (seconds < 60) return String.format("%.1fs", seconds); + int s = (int) Math.floor(seconds); + int m = s / 60; + int r = s % 60; + return String.format("%dm %02ds", m, r); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/item/behavior/OxygenSupplyTankBehavior.java b/src/main/java/com/ghostipedia/cosmiccore/common/item/behavior/OxygenSupplyTankBehavior.java new file mode 100644 index 000000000..2ada0306a --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/item/behavior/OxygenSupplyTankBehavior.java @@ -0,0 +1,120 @@ +package com.ghostipedia.cosmiccore.common.item.behavior; + +import com.ghostipedia.cosmiccore.common.airControl.IOxygenSupplyItem; +import com.ghostipedia.cosmiccore.common.airControl.OxygenItemCap; + +import com.gregtechceu.gtceu.api.item.component.IItemComponent; +import com.gregtechceu.gtceu.api.item.component.forge.IComponentCapability; +import com.gregtechceu.gtceu.common.data.GTMaterials; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandlerItem; +import net.minecraftforge.fluids.capability.templates.FluidHandlerItemStack; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +public class OxygenSupplyTankBehavior implements IItemComponent, IComponentCapability { + + private static final String TAG_ROOT = "CosmicCoreO2"; + private static final String TAG_BUF = "TickBuffer"; + private static final String TAG_CAP = "CapacityMb"; + private static final String TAG_TPT = "TransferPerTick"; + private static final String TAG_TPM = "TicksPerMb"; + + @Getter + private final int capacityMb; + @Getter + private final int transferPerTick; + @Getter + private final int ticksPerMb; + + public OxygenSupplyTankBehavior(int capacityMb, int transferPerTick, int ticksPerMb) { + this.capacityMb = capacityMb; + this.transferPerTick = Math.max(0, transferPerTick); + this.ticksPerMb = Math.max(1, ticksPerMb); + } + + /** + * Drains oxygen ticks from the tank's internal buffer, refilling from fluid as needed. + */ + public int drainTicks(ItemStack stack, int requestTicks) { + if (requestTicks <= 0) return 0; + + int outLimit = Math.min(requestTicks, transferPerTick); + + IFluidHandlerItem fluidHandler = getFluidHandler(stack); + if (fluidHandler == null) return 0; + + int buffer = getTickBuffer(stack); + + // If buffer can't satisfy the output limit, top-up by draining 1 mB + if (buffer < outLimit) { + FluidStack drained = fluidHandler.drain( + new FluidStack(GTMaterials.Oxygen.getFluid(), 1), + IFluidHandlerItem.FluidAction.EXECUTE); + if (!drained.isEmpty()) { + buffer += drained.getAmount() * ticksPerMb; + } + } + + int provided = Math.min(outLimit, buffer); + if (provided > 0) { + setTickBuffer(stack, buffer - provided); + } + return provided; + } + + private IFluidHandlerItem getFluidHandler(ItemStack stack) { + return stack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).orElse(null); + } + + private int getTickBuffer(ItemStack stack) { + CompoundTag compoundTag = stack.getOrCreateTag().getCompound(TAG_ROOT); + return compoundTag.getInt(TAG_BUF); + } + + private void setTickBuffer(ItemStack stack, int value) { + CompoundTag tag = stack.getOrCreateTag(); + CompoundTag compoundTag = tag.getCompound(TAG_ROOT); + compoundTag.putInt(TAG_BUF, Math.max(0, value)); + tag.put(TAG_ROOT, compoundTag); + } + + private void ensureConfigWritten(ItemStack stack) { + CompoundTag tag = stack.getOrCreateTag(); + CompoundTag compoundTag = tag.getCompound(TAG_ROOT); + compoundTag.putInt(TAG_CAP, capacityMb); + compoundTag.putInt(TAG_TPT, transferPerTick); + compoundTag.putInt(TAG_TPM, ticksPerMb); + tag.put(TAG_ROOT, compoundTag); + } + + @Override + public @NotNull LazyOptional getCapability(ItemStack stack, @NotNull Capability cap) { + ensureConfigWritten(stack); + + if (cap == ForgeCapabilities.FLUID_HANDLER_ITEM) { + return ForgeCapabilities.FLUID_HANDLER_ITEM.orEmpty(cap, + LazyOptional.of(() -> new FluidHandlerItemStack(stack, capacityMb) { + + @Override + public boolean isFluidValid(int tank, FluidStack fluidStack) { + return fluidStack.getFluid() == GTMaterials.Oxygen.getFluid(); + } + })); + } + + if (cap == OxygenItemCap.OXYGEN_SUPPLY) { + IOxygenSupplyItem provider = (stk, req) -> drainTicks(stk, req); + return OxygenItemCap.OXYGEN_SUPPLY.orEmpty(cap, LazyOptional.of(() -> provider)); + } + + return LazyOptional.empty(); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/DreamersBasin.java b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/DreamersBasin.java new file mode 100644 index 000000000..3386cbbfa --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/DreamersBasin.java @@ -0,0 +1,105 @@ +package com.ghostipedia.cosmiccore.common.machine.multiblock.multi; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.DreamersBasinMachine; +import com.ghostipedia.cosmiccore.common.data.CosmicBlocks; +import com.ghostipedia.cosmiccore.gtbridge.CosmicRecipeTypes; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.data.RotationState; +import com.gregtechceu.gtceu.api.machine.MultiblockMachineDefinition; +import com.gregtechceu.gtceu.api.machine.multiblock.PartAbility; +import com.gregtechceu.gtceu.api.pattern.FactoryBlockPattern; +import com.gregtechceu.gtceu.api.pattern.Predicates; + +import net.minecraft.network.chat.Component; + +import static com.ghostipedia.cosmiccore.api.registries.CosmicRegistration.REGISTRATE; +import static com.gregtechceu.gtceu.api.pattern.Predicates.*; +import static com.gregtechceu.gtceu.common.data.GTBlocks.CASING_STEEL_SOLID; + +/** + * The Dreamer's Basin - A multithreaded processing machine. + *

+ * This machine can run multiple unique recipes simultaneously using color-coded input buses. + * Each thread requires a uniquely colored input bus/hatch pair. + * Maximum threads is determined by energy hatch amperage (4A = 4 threads, 16A = 16 threads). + * All threads share output buses/hatches. + *

+ * Energy is split evenly among threads - each thread gets 1A worth of the input voltage. + * Recipes can overclock within each thread's energy budget. + */ +public class DreamersBasin { + + // ===== Machine Definition ===== + + public static final MultiblockMachineDefinition DREAMERS_BASIN = REGISTRATE + .multiblock("dreamers_basin", DreamersBasinMachine::new) + .langValue("Dreamer's Basin") + .rotationState(RotationState.NON_Y_AXIS) + .recipeType(CosmicRecipeTypes.MULTITHREADED_PROCESSOR) + // CRITICAL: Disable default overclock modifier - we handle overclocking per-thread + .noRecipeModifier() + .appearanceBlock(CASING_STEEL_SOLID) + .tooltips( + Component.translatable("cosmiccore.machine.dreamers_basin.tooltip.0"), + Component.translatable("cosmiccore.machine.dreamers_basin.tooltip.1"), + Component.translatable("cosmiccore.machine.dreamers_basin.tooltip.2"), + Component.translatable("cosmiccore.machine.dreamers_basin.tooltip.3")) + .pattern(definition -> FactoryBlockPattern.start() + .aisle(" AAA AAA ", " A A ", " A A ", " B ", " B ", + " BBB ", " BBBBBBB ", " BBBBB ", " BBB ", " ", + " ") + .aisle(" ACCA ACCA ", " ", " A B A ", " AB BA ", " B B ", + " BBBBBBB ", " BCCBBBCCB ", " CC CC ", " CC CC ", " C C ", + " ") + .aisle("ACCA ACCA", " C B C ", " C B B C ", " A B A ", " A B A ", + " B A C A B ", " BCBBCCCBBCB ", " CC CC ", " C C ", " C C ", + " C C ") + .aisle("ACA B ACA", " B B ", " B ", " C C ", " C CCC C ", + " B CCC CCC B ", "BCBC CBCB", " CC CC ", " C C ", " C C ", + " ") + .aisle("AA B AA", "A B B A", "AA B AA", " AA AA ", " A A ", + " BAC CAB ", "BCB BCB", "BC CB", " C C ", " ", + " ") + .aisle(" BBB ", " BBBBBBB ", " B C B ", " B B ", " B C C B ", + "BB C C BB", "BBC CBB", "B B", "B B", " ", + " ") + .aisle(" BBBBBBB ", " B BBB B ", " B BBCCCBB B ", "B B B B", "B BC CB B", + "BBC CBB", "BBC CBB", "B B", "B B", " ", + " ") + .aisle(" BBB ", " BBBBBBB ", " B C B ", " B B ", " B C C B ", + "BB C C BB", "BBC CBB", "B B", "B B", " ", + " ") + .aisle("AA B AA", "A B B A", "AA B AA", " AA AA ", " A A ", + " BAC CAB ", "BCB BCB", "BC CB", " C C ", " ", + " ") + .aisle("ACA B ACA", " B B ", " B ", " C C ", " C CCC C ", + " B CCC CCC B ", "BCBC CBCB", " CC CC ", " C C ", " C C ", + " ") + .aisle("ACCA ACCA", " C B C ", " C B B C ", " A B A ", " A B A ", + " B A C A B ", " BCBBCCCBBCB ", " CC CC ", " C C ", " C C ", + " C C ") + .aisle(" ACCA ACCA ", " ", " A B A ", " AB BA ", " B B ", + " BBBBBBB ", " BCCBBBCCB ", " CC CC ", " CC CC ", " C C ", + " ") + .aisle(" AAA AAA ", " A A ", " A A ", " B ", " B ", + " BBB ", " BBBBBBB ", " BBDBB ", " BBB ", " ", + " ") + + .where('D', controller(blocks(definition.getBlock()))) + .where(' ', any()) + .where('A', blocks(CosmicBlocks.SOUL_MUTED_CASING.get())) + .where('B', blocks(CosmicBlocks.SUPERHEAVY_STEEL_CASING.get()).setMinGlobalLimited(200) + .or(autoAbilities(CosmicRecipeTypes.MULTITHREADED_PROCESSOR)) + .or(Predicates.abilities(PartAbility.INPUT_ENERGY).setMinGlobalLimited(1) + .setMaxGlobalLimited(2)) + .or(Predicates.abilities(PartAbility.MAINTENANCE).setExactLimit(1))) + .where('C', blocks(CosmicBlocks.SOMARUST_CASING.get())) + .build()) + .workableCasingModel( + GTCEu.id("block/casings/solid/machine_casing_solid_steel"), + GTCEu.id("block/multiblock/implosion_compressor")) + .register(); + + public static void init() {} +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/StarLadder.java b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/StarLadder.java index 1770abad2..af661526e 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/StarLadder.java +++ b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/StarLadder.java @@ -1,6 +1,7 @@ package com.ghostipedia.cosmiccore.common.machine.multiblock.multi; import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.client.renderer.machine.CosmicDynamicRenderHelpers; import com.ghostipedia.cosmiccore.common.data.CosmicBlocks; import com.ghostipedia.cosmiccore.gtbridge.CosmicRecipeTypes; @@ -21,6 +22,7 @@ import static com.ghostipedia.cosmiccore.api.registries.CosmicRegistration.REGISTRATE; import static com.ghostipedia.cosmiccore.common.data.CosmicBlocks.*; import static com.gregtechceu.gtceu.api.pattern.Predicates.*; +import static com.gregtechceu.gtceu.common.data.models.GTMachineModels.createWorkableCasingMachineModel; public class StarLadder { @@ -172,8 +174,11 @@ public class StarLadder { .where('E', frames(GTMaterials.TungstenSteel)) .where('A', blocks(SUPERHEAVY_STEEL_CASING.get())) .build()) - .workableCasingModel(CosmicCore.id("block/casings/solid/rigid_high_speed_steel_casing"), + .model(createWorkableCasingMachineModel( + CosmicCore.id("block/casings/solid/rigid_high_speed_steel_casing"), GTCEu.id("block/multiblock/generator/large_gas_turbine")) + .andThen(model -> model.addDynamicRenderer(CosmicDynamicRenderHelpers::getStarLadderRender))) + .hasBER(true) .tooltips(Component.translatable("cosmiccore.multiblock.star_ladder.tooltip.0"), Component.translatable("cosmiccore.multiblock.star_ladder.tooltip.1"), diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/StellarIris.java b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/StellarIris.java index 3b5414f80..eec2e585e 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/StellarIris.java +++ b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/StellarIris.java @@ -2,6 +2,7 @@ import com.ghostipedia.cosmiccore.CosmicCore; import com.ghostipedia.cosmiccore.api.machine.multiblock.IrisMultiblockMachine; +import com.ghostipedia.cosmiccore.api.pattern.CosmicPredicates; import com.ghostipedia.cosmiccore.client.renderer.machine.CosmicDynamicRenderHelpers; import com.ghostipedia.cosmiccore.common.data.CosmicBlocks; import com.ghostipedia.cosmiccore.gtbridge.CosmicRecipeTypes; @@ -9,9 +10,7 @@ import com.gregtechceu.gtceu.GTCEu; import com.gregtechceu.gtceu.api.data.RotationState; import com.gregtechceu.gtceu.api.machine.MultiblockMachineDefinition; -import com.gregtechceu.gtceu.api.machine.multiblock.PartAbility; import com.gregtechceu.gtceu.api.pattern.FactoryBlockPattern; -import com.gregtechceu.gtceu.api.pattern.Predicates; import com.gregtechceu.gtceu.api.recipe.OverclockingLogic; import com.gregtechceu.gtceu.common.data.GTRecipeModifiers; @@ -32,4717 +31,144 @@ public class StellarIris { .recipeModifier(GTRecipeModifiers.ELECTRIC_OVERCLOCK.apply(OverclockingLogic.NON_PERFECT_OVERCLOCK_SUBTICK)) .appearanceBlock(CosmicBlocks.CYCLOZINE_CHEMICALLY_REPELLING_CASING) .pattern(definition -> FactoryBlockPattern.start() - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAAAAAAAAAAAA ", - " ", - " ", - " ", - " ", - " ", - " AAAAAAAAAAAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAAAAAAAAAAAAAAAAAAAAAAAA ", - " BBBBBBBBBBBBBBB ", - " BBBBBB BBBBBB ", - " BBBBBB BBBBBB ", - " BBBBBB BBBBBB ", - " BBBBBBBBBBBBBBB ", - " AAAAAAAAAAAAAAAAAAAAAAAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAAAAAAACCCCCCCCCCCCCCCAAAAAAAAAA ", - " BBBBBBBBBBBBBBBBBBBBBBBBBBB ", - " BBBBBBBBBBBB BBBBBBBBBBBB ", - " BBBBBBBBBBBB BBBBBBBBBBBB ", - " BBBBBBBBBBBB BBBBBBBBBBBB ", - " BBBBBBBBBBBBBBBBBBBBBBBBBBB ", - " AAAAAAAAAACCCCCCCCCCCCCCCAAAAAAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAAAACCCCCCAAAAAAAAAAAAAAACCCCCCAAAAAAA ", - " BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB ", - " BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB ", - " BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB ", - " BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB ", - " BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB ", - " AAAAAAACCCCCCAAAAAAAAAAAAAAACCCCCCAAAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAACCCCAAAAAAAAAAAAAAAAAAAAAAAAAAACCCCAAAAA ", - " BBBBBBBBBBBBB BBBBBBBBBBBBB ", - " BBBBBBBBBBBBB BBBBBBBBBBBBB ", - " BBBBBBBBBBBBB BBBBBBBBBBBBB ", - " BBBBBBBBBBBBB BBBBBBBBBBBBB ", - " BBBBBBBBBBBBB BBBBBBBBBBBBB ", - " AAAAACCCCAAAAAAAAAAAAAAAAAAAAAAAAAAACCCCAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAACCCAAAAAAAAAA AAAAAAAAAACCCAAAAA ", - " BBBBBBBBB BBBBBBBBB ", - " BBBBBBBBB BBBBBBBBB ", - " BBBBBBBBB BBBBBBBBB ", - " BBBBBBBBB BBBBBBBBB ", - " BBBBBBBBB BBBBBBBBB ", - " AAAAACCCAAAAAAAAAA AAAAAAAAAACCCAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAACCAAAAAAA AAAAAAACCAAAAA ", - " BBBBBBBB BBBBBBBB ", - " BBBBBBBB BBBBBBBB ", - " BBBBBBBB BBBBBBBB ", - " BBBBBBBB BBBBBBBB ", - " BBBBBBBB BBBBBBBB ", - " AAAAACCAAAAAAA AAAAAAACCAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAACCCAAAAA AAAAACCCAAAA ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " AAAACCCAAAAA AAAAACCCAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAACCAAAAA AAAAACCAAA ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " AAACCAAAAA AAAAACCAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAACCAAAAA AAAAACCAAA ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " AAACCAAAAA AAAAACCAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAACAAAA AAAACAAAA ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " AAAACAAAA AAAACAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAACCAAA AAACCAAA ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " AAACCAAA AAACCAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACCAAA AAACCAA ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " AACCAAA AAACCAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAACAAAA AAAACAAA ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " AAACAAAA AAAACAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CAACAAA AAACAAC ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " CAACAAA AAACAAC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCAA AAAAAAAAAAAAA AACCCC ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " CCCCAA AAAAAAAAAAAAA AACCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCAAA AAAAAAAAAAAAAAAAAAAAAAA AAACCC ", - " BBBB BBBBBBBBBBBBB BBBB ", - " BBBB BBBBBBBBBBBBB BBBB ", - " BBBB BBBBBBBBBBBBB BBBB ", - " BBBB BBBBBBBBBBBBB BBBB ", - " BBBB BBBBBBBBBBBBB BBBB ", - " CCCAAA AAAAAAAAAAAAAAAAAAAAAAA AAACCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCAA AAAAAAAACCCCCCCCCCCCCAAAAAAAA AACCCC ", - " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", - " CCCCAA AAAAAAAACCCCCCCCCCCCCAAAAAAAA AACCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AAAAAACCCCCAAAAAAAAAAAAACCCCCAAAAAA CCCCC ", - " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", - " CCCCC AAAAAACCCCCAAAAAAAAAAAAACCCCCAAAAAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AAAAACCCAAAAAAAAAAAAAAAAAAAAAAACCCAAAAA CCCCC ", - " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", - " CCCCC AAAAACCCAAAAAAAAAAAAAAAAAAAAAAACCCAAAAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AAAACCCAAAAAAAA AAAAAAAACCCAAAA CCCCC ", - " BBB BBBBBBBB BBBBBBBB BBB ", - " BBB BBBBBBBB BBBBBBBB BBB ", - " BBB BBBBBBBB BBBBBBBB BBB ", - " BBB BBBBBBBB BBBBBBBB BBB ", - " BBB BBBBBBBB BBBBBBBB BBB ", - " CCCCC AAAACCCAAAAAAAA AAAAAAAACCCAAAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AAAACCAAAAAA AAAAAACCAAAA CCCCC ", - " BBB BBBBBBB BBBBBBB BBB ", - " BBB BBBBBBB BBBBBBB BBB ", - " BBB BBBBBBB BBBBBBB BBB ", - " BBB BBBBBBB BBBBBBB BBB ", - " BBB BBBBBBB BBBBBBB BBB ", - " CCCCC AAAACCAAAAAA AAAAAACCAAAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AAAACCAAAAA AAAAACCAAAA CCCCC ", - " BBB BBBBBB BBBBBB BBB ", - " BBB BBBBBB BBBBBB BBB ", - " BBB BBBBBB BBBBBB BBB ", - " BBB BBBBBB BBBBBB BBB ", - " BBB BBBBBB BBBBBB BBB ", - " CCCCC AAAACCAAAAA AAAAACCAAAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC DAACCAAAA AAAACCAAD CCCCC ", - " BBB DBBBBBB BBBBBBD BBB ", - " BBB DBBBBBB BBBBBBD BBB ", - " BBB DBBBBBB BBBBBBD BBB ", - " BBB DBBBBBB BBBBBBD BBB ", - " BBB DBBBBBB BBBBBBD BBB ", - " CCCCC DAACCAAAA AAAACCAAD CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AADDCAAAA AAAACDDAA CCCCC ", - " BBB BBBBB BBBBB BBB ", - " BBB BBBBB BBBBB BBB ", - " BBB BBBBB BBBBB BBB ", - " BBB BBBBB BBBBB BBB ", - " BBB BBBBB BBBBB BBB ", - " CCCCC AADDCAAAA AAAACDDAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " AACAA AADDDDAA AADDDDAA AACAA ", - " BBB BBBBD DBBBB BBB ", - " BBB BBBBD DBBBB BBB ", - " BBB BBBBD DBBBB BBB ", - " BBB BBBBD DBBBB BBB ", - " BBB BBBBD DBBBB BBB ", - " AACAA AADDDDAA AADDDDAA AACAA ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA AADDDAA AADDDAA AACAA ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " AACAA AADDDAA AADDDAA AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA AADDDAA AADDDAA AACAA ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " AACAA AADDDAA AADDDAA AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " AACAA ADDDAA AADDDA AACAA ", - " BBB DBBBBE EBBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBE EBBBBD BBB ", - " AACAA ADDDAA AADDDA AACAA ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA DDDDDAA AADDDDD AACAA ", - " BBB DBBBBE EBBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBE EBBBBD BBB ", - " AACAA DDDDDAA AADDDDD AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " D E E D ", - " D E E D ", - " DDE EDD ", - " DDE EDD ", - " DDD DDD ", - " DDD DDD ", - " AACAA DDDDDAA AADDDDD AACAA ", - " BBB DBBBDE EDBBBD BBB ", - " BBB DBBBDE EDBBBD BBB ", - " BBB DBBBDE EDBBBD BBB ", - " BBB DBBBDE EDBBBD BBB ", - " BBB DBBBDE EDBBBD BBB ", - " AACAA DDDDDAA AADDDDD AACAA ", - " DDD DDD ", - " DDD DDD ", - " DDE EDD ", - " DDE EDD ", - " D E E D ", - " D E E D ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " ", - " ", - " ", - " ") - .aisle(" A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " D D ", - " D D ", - " DD DD ", - " DDE EDD ", - " DDE EDD ", - " DDE EDD ", - " AACAA DDDDB BDDDD AACAA ", - " BBB DBBBDB BDBBBD BBB ", - " BBB DBBBDB BDBBBD BBB ", - " BBB DBBBDB BDBBBD BBB ", - " BBB DBBBDB BDBBBD BBB ", - " BBB DBBBDB BDBBBD BBB ", - " AACAA DDDDB BDDDD AACAA ", - " DDE EDD ", - " DDE EDD ", - " DDE EDD ", - " DD DD ", - " D D ", - " D D ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ") - .aisle(" ", - " ", - " ", - " ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " EE EE ", - " DE ED ", - " DEA AED ", - " AACAA AADDDA ADDDAA AACAA ", - " BBB BBBDA ADBBB BBB ", - " BBB BBBDA ADBBB BBB ", - " BBB BBBDA ADBBB BBB ", - " BBB BBBDA ADBBB BBB ", - " BBB BBBDA ADBBB BBB ", - " AACAA AADDDA ADDDAA AACAA ", - " DEA AED ", - " DE ED ", - " EE EE ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA AADDDB BDDDAA AACAA ", - " BBB BBBDB BDBBB BBB ", - " BBB BBBDB BDBBB BBB ", - " BBB BBBDB BDBBB BBB ", - " BBB BBBDB BDBBB BBB ", - " BBB BBBDB BDBBB BBB ", - " AACAA AADDDB BDDDAA AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " AACAA AADDDA ADDDAA AACAA ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " AACAA AADDDA ADDDAA AACAA ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA AADDDAA AADDDAA AACAA ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " AACAA AADDDAA AADDDAA AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA ADDDAA AADDDA AACAA ", - " BBB BBBE EBBB BBB ", - " BBB BBBA ABBB BBB ", - " BBB BBBA ABBB BBB ", - " BBB BBBA ABBB BBB ", - " BBB BBBE EBBB BBB ", - " AACAA ADDDAA AADDDA AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " AACAA DDDDAA AADDDD AACAA ", - " BBB DBBBE EBBBD BBB ", - " BBB DBBBA ABBBD BBB ", - " BBB DBBBA ABBBD BBB ", - " BBB DBBBA ABBBD BBB ", - " BBB DBBBE EBBBD BBB ", - " AACAA DDDDAA AADDDD AACAA ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AADDAA AADDAA AACAA ", - " BBB BBBE EBBB BBB ", - " BBB BBBE EBBB BBB ", - " BBB BBBE EBBB BBB ", - " BBB BBBE EBBB BBB ", - " BBB BBBE EBBB BBB ", - " AACAA AADDAA AADDAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACDA ADCAA AACAA ", - " BBB BBD DBB BBB ", - " BBB BBD DBB BBB ", - " BBB BBD DBB BBB ", - " BBB BBD DBB BBB ", - " BBB BBD DBB BBB ", - " AACAA AACDA ADCAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB CCCCCCC BBB BBB ", - " BBB BBB FFC CFF BBB BBB ", - " BBB BBB CCCCCCC BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB CCC CCC BBB BBB ", - " BBB BBB FFF FFF BBB BBB ", - " BBB BBB CCC CCC BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB CC CC BBB BBB ", - " BBB BBB FF FF BBB BBB ", - " BBB BBB CC CC BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB C C BBB ", - " BBB C C BBB ", - " BBB C C BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB C C BBB ", - " BBB A BBB ", - " BBB C C BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB C C BBB ", - " BBB C C BBB ", - " BBB C C BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "AACAA AACAA AACAA AACAA", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - "AACAA AACAA AACAA AACAA", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB F F BBB BBB ", - " BBB BBB C C BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB CC CC BBB BBB ", - " BBB BBB FF FF BBB BBB ", - " BBB BBB CC CC BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB CCC CCC BBB BBB ", - " BBB BBB FFF FFF BBB BBB ", - " BBB BBB CCC CCC BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB CCCCCCC BBB BBB ", - " BBB BBB FFC CFF BBB BBB ", - " BBB BBB CCCCCCC BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACAA AACAA AACAA ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " BBB BBB BBB BBB ", - " AACAA AACAA AACAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AACDA ADCAA AACAA ", - " BBB BBD DBB BBB ", - " BBB BBD DBB BBB ", - " BBB BBD DBB BBB ", - " BBB BBD DBB BBB ", - " BBB BBD DBB BBB ", - " AACAA AACDA ADCAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACAA AADDAA AADDAA AACAA ", - " BBB BBBE EBBB BBB ", - " BBB BBBE EBBB BBB ", - " BBB BBBE EBBB BBB ", - " BBB BBBE EBBB BBB ", - " BBB BBBE EBBB BBB ", - " AACAA AADDAA AADDAA AACAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " AACAA DDDDAA AADDDD AACAA ", - " BBB DBBBE EBBBD BBB ", - " BBB DBBBA ABBBD BBB ", - " BBB DBBBA ABBBD BBB ", - " BBB DBBBA ABBBD BBB ", - " BBB DBBBE EBBBD BBB ", - " AACAA DDDDAA AADDDD AACAA ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA ADDDAA AADDDA AACAA ", - " BBB BBBE EBBB BBB ", - " BBB BBBA ABBB BBB ", - " BBB BBBA ABBB BBB ", - " BBB BBBA ABBB BBB ", - " BBB BBBE EBBB BBB ", - " AACAA ADDDAA AADDDA AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA AADDDAA AADDDAA AACAA ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " AACAA AADDDAA AADDDAA AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " AACAA AADDDA ADDDAA AACAA ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " AACAA AADDDA ADDDAA AACAA ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA AADDDB BDDDAA AACAA ", - " BBB BBBDB BDBBB BBB ", - " BBB BBBDB BDBBB BBB ", - " BBB BBBDB BDBBB BBB ", - " BBB BBBDB BDBBB BBB ", - " BBB BBBDB BDBBB BBB ", - " AACAA AADDDB BDDDAA AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " EE EE ", - " DE ED ", - " DEA AED ", - " AACAA AADDDA ADDDAA AACAA ", - " BBB BBBDA ADBBB BBB ", - " BBB BBBDA ADBBB BBB ", - " BBB BBBDA ADBBB BBB ", - " BBB BBBDA ADBBB BBB ", - " BBB BBBDA ADBBB BBB ", - " AACAA AADDDA ADDDAA AACAA ", - " DEA AED ", - " DE ED ", - " EE EE ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " ", - " ", - " ", - " ") - .aisle(" A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " D D ", - " D D ", - " DD DD ", - " DDE EDD ", - " DDE EDD ", - " DDE EDD ", - " AACAA DDDDB BDDDD AACAA ", - " BBB DBBBDB BDBBBD BBB ", - " BBB DBBBDB BDBBBD BBB ", - " BBB DBBBDB BDBBBD BBB ", - " BBB DBBBDB BDBBBD BBB ", - " BBB DBBBDB BDBBBD BBB ", - " AACAA DDDDB BDDDD AACAA ", - " DDE EDD ", - " DDE EDD ", - " DDE EDD ", - " DD DD ", - " D D ", - " D D ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ", - " A A ") - .aisle(" ", - " ", - " ", - " ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " D E E D ", - " D E E D ", - " DDE EDD ", - " DDE EDD ", - " DDD DDD ", - " DDD DDD ", - " AACAA DDDDDAA AADDDDD AACAA ", - " BBB DBBBDE EDBBBD BBB ", - " BBB DBBBDE EDBBBD BBB ", - " BBB DBBBDE EDBBBD BBB ", - " BBB DBBBDE EDBBBD BBB ", - " BBB DBBBDE EDBBBD BBB ", - " AACAA DDDDDAA AADDDDD AACAA ", - " DDD DDD ", - " DDD DDD ", - " DDE EDD ", - " DDE EDD ", - " D E E D ", - " D E E D ", - " E E ", - " E E ", - " E E ", - " E E ", - " E E ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA DDDDDAA AADDDDD AACAA ", - " BBB DBBBBE EBBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBE EBBBBD BBB ", - " AACAA DDDDDAA AADDDDD AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " AACAA ADDDAA AADDDA AACAA ", - " BBB DBBBBE EBBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBA ABBBBD BBB ", - " BBB DBBBBE EBBBBD BBB ", - " AACAA ADDDAA AADDDA AACAA ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA AADDDAA AADDDAA AACAA ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBA ABBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " AACAA AADDDAA AADDDAA AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " D D ", - " D D ", - " D D ", - " AACAA AADDDAA AADDDAA AACAA ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " BBB BBBBE EBBBB BBB ", - " AACAA AADDDAA AADDDAA AACAA ", - " D D ", - " D D ", - " D D ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " D D ", - " AACAA AADDDDAA AADDDDAA AACAA ", - " BBB BBBBD DBBBB BBB ", - " BBB BBBBD DBBBB BBB ", - " BBB BBBBD DBBBB BBB ", - " BBB BBBBD DBBBB BBB ", - " BBB BBBBD DBBBB BBB ", - " AACAA AADDDDAA AADDDDAA AACAA ", - " D D ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AADDCAAAA AAAACDDAA CCCCC ", - " BBB BBBBB BBBBB BBB ", - " BBB BBBBB BBBBB BBB ", - " BBB BBBBB BBBBB BBB ", - " BBB BBBBB BBBBB BBB ", - " BBB BBBBB BBBBB BBB ", - " CCCCC AADDCAAAA AAAACDDAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC DAACCAAAA AAAACCAAD CCCCC ", - " BBB DBBBBBB BBBBBBD BBB ", - " BBB DBBBBBB BBBBBBD BBB ", - " BBB DBBBBBB BBBBBBD BBB ", - " BBB DBBBBBB BBBBBBD BBB ", - " BBB DBBBBBB BBBBBBD BBB ", - " CCCCC DAACCAAAA AAAACCAAD CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AAAACCAAAAA AAAAACCAAAA CCCCC ", - " BBB BBBBBB BBBBBB BBB ", - " BBB BBBBBB BBBBBB BBB ", - " BBB BBBBBB BBBBBB BBB ", - " BBB BBBBBB BBBBBB BBB ", - " BBB BBBBBB BBBBBB BBB ", - " CCCCC AAAACCAAAAA AAAAACCAAAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AAAACCAAAAAA AAAAAACCAAAA CCCCC ", - " BBB BBBBBBB BBBBBBB BBB ", - " BBB BBBBBBB BBBBBBB BBB ", - " BBB BBBBBBB BBBBBBB BBB ", - " BBB BBBBBBB BBBBBBB BBB ", - " BBB BBBBBBB BBBBBBB BBB ", - " CCCCC AAAACCAAAAAA AAAAAACCAAAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AAAACCCAAAAAAAA AAAAAAAACCCAAAA CCCCC ", - " BBB BBBBBBBB BBBBBBBB BBB ", - " BBB BBBBBBBB BBBBBBBB BBB ", - " BBB BBBBBBBB BBBBBBBB BBB ", - " BBB BBBBBBBB BBBBBBBB BBB ", - " BBB BBBBBBBB BBBBBBBB BBB ", - " CCCCC AAAACCCAAAAAAAA AAAAAAAACCCAAAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AAAAACCCAAAAAAAAAAAAAAAAAAAAAAACCCAAAAA CCCCC ", - " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", - " CCCCC AAAAACCCAAAAAAAAAAAAAAAAAAAAAAACCCAAAAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCC AAAAAACCCCCAAAAAAAAAAAAACCCCCAAAAAA CCCCC ", - " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", - " CCCCC AAAAAACCCCCAAAAAAAAAAAAACCCCCAAAAAA CCCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCAA AAAAAAAACCCCCCCCCCCCCAAAAAAAA AACCCC ", - " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", - " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", - " CCCCAA AAAAAAAACCCCCCGCCCCCCAAAAAAAA AACCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCAAA AAAAAAAAAAAAAAAAAAAAAAA AAACCC ", - " BBBB BBBBBBBBBBBBB BBBB ", - " BBBB BBBBBBBBBBBBB BBBB ", - " BBBB BBBBBBBBBBBBB BBBB ", - " BBBB BBBBBBBBBBBBB BBBB ", - " BBBB BBBBBBBBBBBBB BBBB ", - " CCCAAA AAAAAAAAAAAAAAAAAAAAAAA AAACCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CCCCAA AAAAAAAAAAAAA AACCCC ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " CCCCAA AAAAAAAAAAAAA AACCCC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " CAACAAA AAACAAC ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " CAACAAA AAACAAC ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAACAAAA AAAACAAA ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " BBBB BBBB ", - " AAACAAAA AAAACAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AACCAAA AAACCAA ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " AACCAAA AAACCAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAACCAAA AAACCAAA ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " AAACCAAA AAACCAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAACAAAA AAAACAAAA ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " AAAACAAAA AAAACAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAACCAAAAA AAAAACCAAA ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " BBBBB BBBBB ", - " AAACCAAAAA AAAAACCAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAACCAAAAA AAAAACCAAA ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " AAACCAAAAA AAAAACCAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAACCCAAAAA AAAAACCCAAAA ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " BBBBBBB BBBBBBB ", - " AAAACCCAAAAA AAAAACCCAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAACCAAAAAAA AAAAAAACCAAAAA ", - " BBBBBBBB BBBBBBBB ", - " BBBBBBBB BBBBBBBB ", - " BBBBBBBB BBBBBBBB ", - " BBBBBBBB BBBBBBBB ", - " BBBBBBBB BBBBBBBB ", - " AAAAACCAAAAAAA AAAAAAACCAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAACCCAAAAAAAAAA AAAAAAAAAACCCAAAAA ", - " BBBBBBBBB BBBBBBBBB ", - " BBBBBBBBB BBBBBBBBB ", - " BBBBBBBBB BBBBBBBBB ", - " BBBBBBBBB BBBBBBBBB ", - " BBBBBBBBB BBBBBBBBB ", - " AAAAACCCAAAAAAAAAA AAAAAAAAAACCCAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAACCCCAAAAAAAAAAAAAAAAAAAAAAAAAAACCCCAAAAA ", - " BBBBBBBBBBBBB BBBBBBBBBBBBB ", - " BBBBBBBBBBBBB BBBBBBBBBBBBB ", - " BBBBBBBBBBBBB BBBBBBBBBBBBB ", - " BBBBBBBBBBBBB BBBBBBBBBBBBB ", - " BBBBBBBBBBBBB BBBBBBBBBBBBB ", - " AAAAACCCCAAAAAAAAAAAAAAAAAAAAAAAAAAACCCCAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAAAACCCCCCAAAAAAAAAAAAAAACCCCCCAAAAAAA ", - " BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB ", - " BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB ", - " BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB ", - " BBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBB ", - " BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB ", - " AAAAAAACCCCCCAAAAAAAAAAAAAAACCCCCCAAAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAAAAAAACCCCCCCCCCCCCCCAAAAAAAAAA ", - " BBBBBBBBBBBBBBBBBBBBBBBBBBB ", - " BBBBBBBBBBBB BBBBBBBBBBBB ", - " BBBBBBBBBBBB BBBBBBBBBBBB ", - " BBBBBBBBBBBB BBBBBBBBBBBB ", - " BBBBBBBBBBBBBBBBBBBBBBBBBBB ", - " AAAAAAAAAACCCCCCCCCCCCCCCAAAAAAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAAAAAAAAAAAAAAAAAAAAAAAA ", - " BBBBBBBBBBBBBBB ", - " BBBBBB BBBBBB ", - " BBBBBB BBBBBB ", - " BBBBBB BBBBBB ", - " BBBBBBBBBBBBBBB ", - " AAAAAAAAAAAAAAAAAAAAAAAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") - .aisle(" ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " AAAAAAAAAAAAAAA ", - " ", - " ", - " ", - " ", - " ", - " AAAAAAAAAAAAAAA ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ") + // spotless:off + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAAAAAAAAAAAA ", " ", " ", " ", " ", " ", " AAAAAAAAAAAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAAAAAAAAAAAAAAAAAAAAAAAA ", " BBBBBBBBBBBBBBB ", " BBBBBBCCCBBBBBB ", " BBBBBBCCCBBBBBB ", " BBBBBBCCCBBBBBB ", " BBBBBBBBBBBBBBB ", " AAAAAAAAAAAAAAAAAAAAAAAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAAAAAAABBBBBBBBBBBBBBBAAAAAAAAAA ", " BBBBBBBBBBBBBBBBBBBBBBBBBBB ", " BBBBBBBBBBBBCCCBBBBBBBBBBBB ", " BBBBBBBBBBBBCCCBBBBBBBBBBBB ", " BBBBBBBBBBBBCCCBBBBBBBBBBBB ", " BBBBBBBBBBBBBBBBBBBBBBBBBBB ", " AAAAAAAAAABBBBBBBBBBBBBBBAAAAAAAAAA ", " D ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAAAABBBBBBAAAAAAAAAAAAAAABBBBBBAAAAAAA ", " BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB ", " BBBBBBBBBBBBBBBBCCCBBBBBBBBBBBBBBBB ", " BBBBBBBBBBBBBBBBCCCBBBBBBBBBBBBBBBB ", " BBBBBBBBBBBBBBBBCCCBBBBBBBBBBBBBBBB ", " BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB ", " AAAAAAABBBBBBAAAAAAAAAAAAAAABBBBBBAAAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAABBBBAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBAAAAA ", " BBBBBBBBBBBBB BBBBBBBBBBBBB ", " BBBBBBBBBBBBB BBBBBBBBBBBBB ", " BBBBBBBBBBBBB BBBBBBBBBBBBB ", " BBBBBBBBBBBBB BBBBBBBBBBBBB ", " BBBBBBBBBBBBB BBBBBBBBBBBBB ", " AAAAABBBBAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAABBBAAAAAAAAAA AAAAAAAAAABBBAAAAA ", " BBBBBBBBB BBBBBBBBB ", " BBBBBBBBB BBBBBBBBB ", " BBBBBBBBB BBBBBBBBB ", " BBBBBBBBB BBBBBBBBB ", " BBBBBBBBB BBBBBBBBB ", " AAAAABBBAAAAAAAAAA AAAAAAAAAABBBAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAABBAAAAAAA AAAAAAABBAAAAA ", " BBBBBBBB BBBBBBBB ", " BBBBBBBB BBBBBBBB ", " BBBBBBBB BBBBBBBB ", " BBBBBBBB BBBBBBBB ", " BBBBBBBB BBBBBBBB ", " AAAAABBAAAAAAA AAAAAAABBAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAABBBAAAAA AAAAABBBAAAA ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " AAAABBBAAAAA AAAAABBBAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAABBAAAAA AAAAABBAAA ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " AAABBAAAAA AAAAABBAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAABBAAAAA AAAAABBAAA ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " AAABBAAAAA AAAAABBAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAABAAAA AAAABAAAA ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " AAAABAAAA AAAABAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAABBAAA AAABBAAA ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " AAABBAAA AAABBAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABBAAA AAABBAA ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " AABBAAA AAABBAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAABAAAA AAAABAAA ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " AAABAAAA AAAABAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BAABAAA AAABAAB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BAABAAA AAABAAB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBAA AAAAAAAAAAAAA AABBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBBAA AAAAAAAAAAAAA AABBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBAAA AAAAAAAAAAAAAAAAAAAAAAA AAABBB ", " BBBB BBBBBBBBBBBBB BBBB ", " BBBB BBBBBCCCBBBBB BBBB ", " BBBB BBBBBCCCBBBBB BBBB ", " BBBB BBBBBCCCBBBBB BBBB ", " BBBB BBBBBBBBBBBBB BBBB ", " BBBAAA AAAAAAAAAAAAAAAAAAAAAAA AAABBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBAA AAAAAAAABBBBBBBBBBBBBAAAAAAAA AABBBB ", " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", " BBB BBBBBBBBBBCCCBBBBBBBBBB BBB ", " BBB BBBBBBBBBBCCCBBBBBBBBBB BBB ", " BBB BBBBBBBBBBCCCBBBBBBBBBB BBB ", " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", " BBBBAA AAAAAAAABBBBBBBBBBBBBAAAAAAAA AABBBB ", " D ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAAAAABBBBBAAAAAAAAAAAAABBBBBAAAAAA BBBBB ", " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", " BBB BBBBBBBBBBBBBCCCBBBBBBBBBBBBB BBB ", " BBB BBBBBBBBBBBBBCCCBBBBBBBBBBBBB BBB ", " BBB BBBBBBBBBBBBBCCCBBBBBBBBBBBBB BBB ", " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", " BBBBB AAAAAABBBBBAAAAAAAAAAAAABBBBBAAAAAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAAAABBBAAAAAAAAAAAAAAAAAAAAAAABBBAAAAA BBBBB ", " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", " BBBBB AAAAABBBAAAAAAAAAAAAAAAAAAAAAAABBBAAAAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAAABBBAAAAAAAA AAAAAAAABBBAAAA BBBBB ", " BBB BBBBBBBB BBBBBBBB BBB ", " BBB BBBBBBBB BBBBBBBB BBB ", " BBB BBBBBBBB BBBBBBBB BBB ", " BBB BBBBBBBB BBBBBBBB BBB ", " BBB BBBBBBBB BBBBBBBB BBB ", " BBBBB AAAABBBAAAAAAAA AAAAAAAABBBAAAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAAABBAAAAAA AAAAAABBAAAA BBBBB ", " BBB BBBBBBB BBBBBBB BBB ", " BBB BBBBBBB BBBBBBB BBB ", " BBB BBBBBBB BBBBBBB BBB ", " BBB BBBBBBB BBBBBBB BBB ", " BBB BBBBBBB BBBBBBB BBB ", " BBBBB AAAABBAAAAAA AAAAAABBAAAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAAABBAAAAA AAAAABBAAAA BBBBB ", " BBB BBBBBB BBBBBB BBB ", " BBB BBBBBB BBBBBB BBB ", " BBB BBBBBB BBBBBB BBB ", " BBB BBBBBB BBBBBB BBB ", " BBB BBBBBB BBBBBB BBB ", " BBBBB AAAABBAAAAA AAAAABBAAAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB EAABBAAAA AAAABBAAE BBBBB ", " BBB EBBBBBB BBBBBBE BBB ", " BBB EBBBBBB BBBBBBE BBB ", " BBB EBBBBBB BBBBBBE BBB ", " BBB EBBBBBB BBBBBBE BBB ", " BBB EBBBBBB BBBBBBE BBB ", " BBBBB EAABBAAAA AAAABBAAE BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAEEBAAAA AAAABEEAA BBBBB ", " BBB BBBBB BBBBB BBB ", " BBB BBBBB BBBBB BBB ", " BBB BBBBB BBBBB BBB ", " BBB BBBBB BBBBB BBB ", " BBB BBBBB BBBBB BBB ", " BBBBB AAEEBAAAA AAAABEEAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " AABAA AAEEEEAA AAEEEEAA AABAA ", " BBB BBBBE EBBBB BBB ", " BBB BBBBE EBBBB BBB ", " BBB BBBBE EBBBB BBB ", " BBB BBBBE EBBBB BBB ", " BBB BBBBE EBBBB BBB ", " AABAA AAEEEEAA AAEEEEAA AABAA ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " AABAA AAEEEAA AAEEEAA AABAA ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " AABAA AAEEEAA AAEEEAA AABAA ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " AABAA AAEEEAA AAEEEAA AABAA ", " BBB BBBBF FBBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBF FBBBB BBB ", " AABAA AAEEEAA AAEEEAA AABAA ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " AABAA AEEEAA AAEEEA AABAA ", " BBB EBBBBF FBBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBF FBBBBE BBB ", " AABAA AEEEAA AAEEEA AABAA ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " AABAA EEEEEAA AAEEEEE AABAA ", " BBB EBBBBF FBBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBF FBBBBE BBB ", " AABAA EEEEEAA AAEEEEE AABAA ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " F F ", " F F ", " F F ", " F F ", " F F ", " E F F E ", " E F F E ", " EEF FEE ", " EEF FEE ", " EEE EEE ", " EEE EEE ", " AABAA EEEEEAA AAEEEEE AABAA ", " BBB EBBBEF FEBBBE BBB ", " BBB EBBBEF FEBBBE BBB ", " BBB EBBBEF FEBBBE BBB ", " BBB EBBBEF FEBBBE BBB ", " BBB EBBBEF FEBBBE BBB ", " AABAA EEEEEAA AAEEEEE AABAA ", " EEE EEE ", " EEE EEE ", " EEF FEE ", " EEF FEE ", " E F F E ", " E F F E ", " F F ", " F F ", " F F ", " F F ", " F F ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " E E ", " E E ", " EE EE ", " EEF FEE ", " EEF FEE ", " EEF FEE ", " AABAA EEEEB BEEEE AABAA ", " BBB EBBBEB BEBBBE BBB ", " BBB EBBBEB BEBBBE BBB ", " BBB EBBBEB BEBBBE BBB ", " BBB EBBBEB BEBBBE BBB ", " BBB EBBBEB BEBBBE BBB ", " AABAA EEEEB BEEEE AABAA ", " EEF FEE ", " EEF FEE ", " EEF FEE ", " EE EE ", " E E ", " E E ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " FF FF ", " EF FE ", " EFA AFE ", " AABAA AAEEEA AEEEAA AABAA ", " BBB BBBEA AEBBB BBB ", " BBB BBBEA AEBBB BBB ", " BBB BBBEA AEBBB BBB ", " BBB BBBEA AEBBB BBB ", " BBB BBBEA AEBBB BBB ", " AABAA AAEEEA AEEEAA AABAA ", " EFA AFE ", " EF FE ", " FF FF ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " AABAA AAEEEB BEEEAA AABAA ", " BBB BBBEB BEBBB BBB ", " BBB BBBEB BEBBB BBB ", " BBB BBBEB BEBBB BBB ", " BBB BBBEB BEBBB BBB ", " BBB BBBEB BEBBB BBB ", " AABAA AAEEEB BEEEAA AABAA ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " AABAA AAEEEA AEEEAA AABAA ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " AABAA AAEEEA AEEEAA AABAA ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " AABAA AAEEEAA AAEEEAA AABAA ", " BBB BBBBF FBBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBF FBBBB BBB ", " AABAA AAEEEAA AAEEEAA AABAA ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " AABAA AEEEAA AAEEEA AABAA ", " BBB BBBF FBBB BBB ", " BBB BBBA ABBB BBB ", " BBB BBBA ABBB BBB ", " BBB BBBA ABBB BBB ", " BBB BBBF FBBB BBB ", " AABAA AEEEAA AAEEEA AABAA ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " AABAA EEEEAA AAEEEE AABAA ", " BBB EBBBF FBBBE BBB ", " BBB EBBBA ABBBE BBB ", " BBB EBBBA ABBBE BBB ", " BBB EBBBA ABBBE BBB ", " BBB EBBBF FBBBE BBB ", " AABAA EEEEAA AAEEEE AABAA ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AAEEAA AAEEAA AABAA ", " BBB BBBF FBBB BBB ", " BBB BBBF FBBB BBB ", " BBB BBBF FBBB BBB ", " BBB BBBF FBBB BBB ", " BBB BBBF FBBB BBB ", " AABAA AAEEAA AAEEAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABEA AEBAA AABAA ", " BBB BBE EBB BBB ", " BBB BBE EBB BBB ", " BBB BBE EBB BBB ", " BBB BBE EBB BBB ", " BBB BBE EBB BBB ", " AABAA AABEA AEBAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " GGGGG ", " ", " GGGGG ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " GGGGG ", " ", " GGGGG ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" H H ", " H H ", " H H ", " H H ", " H H ", " HH HH ", " HH HH ", " GGGHH HHGGG ", " HH HH ", " GGGHH HHGGG ", " HH HH ", " H H ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " H H ", " HH HH ", " GGGHH HHGGG ", " HH HH ", " GGGHH HHGGG ", " HH HH ", " HH HH ", " H H ", " H H ", " H H ", " H H ", " H H ") + .aisle(" ", " ", " ", " ", " ", " AACCCCCAA ", " AA HHH AA ", " G AA HHH AA G ", " AA HHH AA ", " G AA HHH AA G ", " AA HHH AA ", " AACHHHCAA ", " HHH ", " HHH ", " HHH ", " H ", " H ", " H ", " H ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " H ", " H ", " H ", " H ", " HHH ", " HHH ", " HHH ", " AACHHHCAA ", " AA HHH AA ", " G AA HHH AA G ", " AA HHH AA ", " G AA HHH AA G ", " AA HHH AA ", " AACCCCCAA ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " G G ", " AACCCCCCCCCAA ", " AA AA ", " GGAA G G AAGG ", " AA AA ", " GGAA AAGG ", " AA AA ", " AACCCCCCCCCAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AACCCCCCCCCAA ", " AA AA ", " GGAA AAGG ", " AA AA ", " GGAA G G AAGG ", " AA AA ", " AACCCCCCCCCAA ", " G G ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " G G ", " ACCCC HHH CCCCA ", " A A ", " GA G G AG ", " A A ", " GA AG ", " A A ", " ACCCC CCCCA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ACCCC CCCCA ", " A A ", " GA AG ", " A A ", " GA G G AG ", " A A ", " ACCCC HHH CCCCA ", " G G ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " G G ", " ACCC HHH CCCA ", " A A ", " GA G G AG ", " A A ", " GA AG ", " A A ", " ACCC CCCA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ACCC CCCA ", " A A ", " GA AG ", " A A ", " GA G G AG ", " A A ", " ACCC HHH CCCA ", " G G ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " G G ", " ACC HHH CCA ", " A A ", " G A G G A G ", " A A ", " G A A G ", " A A ", " ACC CCA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ACC CCA ", " A A ", " G A A G ", " A A ", " G A G G A G ", " A A ", " ACC HHH CCA ", " G G ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " G G ", " ACC HHH CCA ", " A A ", " GA G G AG ", " A A ", " GA AG ", " A A ", " ACC CCA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ACC CCA ", " A A ", " GA AG ", " A A ", " GA G G AG ", " A A ", " ACC HHH CCA ", " G G ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " GGGGG ", " ACC HHH CCA ", " A A ", " GA G G AG ", " A A ", " GA AG ", " A A ", " ACC CCA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ACC CCA ", " A A ", " GA AG ", " A A ", " GA G G AG ", " A A ", " ACC HHH CCA ", " GGGGG ", " ", " ", " ", " ") + .aisle(" H H ", " H H ", " H H ", " H H ", " H GGGGGGGCCCGGGGGGG H ", " HCC HHH CCH ", " H H ", " GH G G HG ", " H H ", " GH HG ", " H H ", " CC CC ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " CC CC ", " H H ", " GH HG ", " H H ", " GH G G HG ", " H H ", " HCC HHH CCH ", " H GGGGGGGCCCGGGGGGG H ", " H H ", " H H ", " H H ", " H H ") + .aisle(" ", " ", " ", " ", " GCCCCCG ", " HCCHHHHHHHHHHHHHHHCCH ", " HH HH ", " GHHGGGGGGGGGGGGGGGGGHHG ", " HH HH ", " GHH HHG ", " HH HH ", " HHC CHH ", " H H ", " H H ", " H H ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " CCC CCC CCC CCC ", " CCC CCC CCC CCC ", " CCC CCC CCC CCC ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " H H ", " H H ", " H H ", " HHC CHH ", " HH HH ", " GHH HHG ", " HH HH ", " GHHGGGGGGGGGGGGGGGGGHHG ", " HH HH ", " HCCHHHHHHHHHHHHHHHCCH ", " GCCCCCG ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " GCC CCG ", " CCHHHHHHHHHHHHHHHCC ", " H H ", " G H G G H G ", " H H ", " G H H G ", " H H ", " HC CH ", " H H ", " H H ", " H H ", " H H ", " H H ", " H H ", " H H ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " CCC CCC CCC CCC ", " CCC CCC CCC CCC ", " CCC CCC CCC CCC ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " D D D D ", " ", " ", " ", " ", " ", " H H ", " H H ", " H H ", " H H ", " H H ", " H H ", " H H ", " HC CH ", " H H ", " G H H G ", " H H ", " G H G G H G ", " H H ", " CCHHHHHHHHHHHHHHHCC ", " GCCQCCG ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " GCCCCCG ", " HCCHHHHHHHHHHHHHHHCCH ", " HH HH ", " GHHGGGGGGGGGGGGGGGGGHHG ", " HH HH ", " GHH HHG ", " HH HH ", " HHC CHH ", " H H ", " H H ", " H H ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " CCC CCC CCC CCC ", " CCC CCC CCC CCC ", " CCC CCC CCC CCC ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " H H ", " H H ", " H H ", " HHC CHH ", " HH HH ", " GHH HHG ", " HH HH ", " GHHGGGGGGGGGGGGGGGGGHHG ", " HH HH ", " HCCHHHHHHHHHHHHHHHCCH ", " GCCCCCG ", " ", " ", " ", " ") + .aisle(" H H ", " H H ", " H H ", " H H ", " H GGGGGGGCCCGGGGGGG H ", " HCC HHH CCH ", " H H ", " GH G G HG ", " H H ", " GH HG ", " H H ", " CC CC ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " CC CC ", " H H ", " GH HG ", " H H ", " GH G G HG ", " H H ", " HCC HHH CCH ", " H GGGGGGGCCCGGGGGGG H ", " H H ", " H H ", " H H ", " H H ") + .aisle(" ", " ", " ", " ", " GGGGG ", " ACC HHH CCA ", " A A ", " GA G G AG ", " A A ", " GA AG ", " A A ", " ACC CCA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ACC CCA ", " A A ", " GA AG ", " A A ", " GA G G AG ", " A A ", " ACC HHH CCA ", " GGGGG ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " G G ", " ACC HHH CCA ", " A A ", " GA G G AG ", " A A ", " GA AG ", " A A ", " ACC CCA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ACC CCA ", " A A ", " GA AG ", " A A ", " GA G G AG ", " A A ", " ACC HHH CCA ", " G G ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " G G ", " ACC HHH CCA ", " A A ", " G A G G A G ", " A A ", " G A A G ", " A A ", " ACC CCA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ACC CCA ", " A A ", " G A A G ", " A A ", " G A G G A G ", " A A ", " ACC HHH CCA ", " G G ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " G G ", " ACCC HHH CCCA ", " A A ", " GA G G AG ", " A A ", " GA AG ", " A A ", " ACCC CCCA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ACCC CCCA ", " A A ", " GA AG ", " A A ", " GA G G AG ", " A A ", " ACCC HHH CCCA ", " G G ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " G G ", " ACCCC HHH CCCCA ", " A A ", " GA G G AG ", " A A ", " GA AG ", " A A ", " ACCCC CCCCA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "AABAA AABAA AABAA AABAA", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", "AABAA AABAA AABAA AABAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ACCCC CCCCA ", " A A ", " GA AG ", " A A ", " GA G G AG ", " A A ", " ACCCC HHH CCCCA ", " G G ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " G G ", " AACCCCCCCCCAA ", " AA AA ", " GGAA G G AAGG ", " AA AA ", " GGAA AAGG ", " AA AA ", " AACCCCCCCCCAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AACCCCCCCCCAA ", " AA AA ", " GGAA AAGG ", " AA AA ", " GGAA G G AAGG ", " AA AA ", " AACCCCCCCCCAA ", " G G ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " AACCCCCAA ", " AA HHH AA ", " G AA HHH AA G ", " AA HHH AA ", " G AA HHH AA G ", " AA HHH AA ", " AACHHHCAA ", " HHH ", " HHH ", " HHH ", " H ", " H ", " H ", " H ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " H ", " H ", " H ", " H ", " HHH ", " HHH ", " HHH ", " AACHHHCAA ", " AA HHH AA ", " G AA HHH AA G ", " AA HHH AA ", " G AA HHH AA G ", " AA HHH AA ", " AACCCCCAA ", " ", " ", " ", " ", " ") + .aisle(" H H ", " H H ", " H H ", " H H ", " H H ", " HH HH ", " HH HH ", " GGGHH HHGGG ", " HH HH ", " GGGHH HHGGG ", " HH HH ", " H H ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " H H ", " HH HH ", " GGGHH HHGGG ", " HH HH ", " GGGHH HHGGG ", " HH HH ", " HH HH ", " H H ", " H H ", " H H ", " H H ", " H H ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " GGGGG ", " ", " GGGGG ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " GGGGG ", " ", " GGGGG ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABAA AABAA AABAA ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " BBB BBB BBB BBB ", " AABAA AABAA AABAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AABEA AEBAA AABAA ", " BBB BBE EBB BBB ", " BBB BBE EBB BBB ", " BBB BBE EBB BBB ", " BBB BBE EBB BBB ", " BBB BBE EBB BBB ", " AABAA AABEA AEBAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABAA AAEEAA AAEEAA AABAA ", " BBB BBBF FBBB BBB ", " BBB BBBF FBBB BBB ", " BBB BBBF FBBB BBB ", " BBB BBBF FBBB BBB ", " BBB BBBF FBBB BBB ", " AABAA AAEEAA AAEEAA AABAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " AABAA EEEEAA AAEEEE AABAA ", " BBB EBBBF FBBBE BBB ", " BBB EBBBA ABBBE BBB ", " BBB EBBBA ABBBE BBB ", " BBB EBBBA ABBBE BBB ", " BBB EBBBF FBBBE BBB ", " AABAA EEEEAA AAEEEE AABAA ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " AABAA AEEEAA AAEEEA AABAA ", " BBB BBBF FBBB BBB ", " BBB BBBA ABBB BBB ", " BBB BBBA ABBB BBB ", " BBB BBBA ABBB BBB ", " BBB BBBF FBBB BBB ", " AABAA AEEEAA AAEEEA AABAA ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " AABAA AAEEEAA AAEEEAA AABAA ", " BBB BBBBF FBBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBF FBBBB BBB ", " AABAA AAEEEAA AAEEEAA AABAA ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " AABAA AAEEEA AEEEAA AABAA ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " AABAA AAEEEA AEEEAA AABAA ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " AABAA AAEEEB BEEEAA AABAA ", " BBB BBBEB BEBBB BBB ", " BBB BBBEB BEBBB BBB ", " BBB BBBEB BEBBB BBB ", " BBB BBBEB BEBBB BBB ", " BBB BBBEB BEBBB BBB ", " AABAA AAEEEB BEEEAA AABAA ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " FF FF ", " EF FE ", " EFA AFE ", " AABAA AAEEEA AEEEAA AABAA ", " BBB BBBEA AEBBB BBB ", " BBB BBBEA AEBBB BBB ", " BBB BBBEA AEBBB BBB ", " BBB BBBEA AEBBB BBB ", " BBB BBBEA AEBBB BBB ", " AABAA AAEEEA AEEEAA AABAA ", " EFA AFE ", " EF FE ", " FF FF ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " F F ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " E E ", " E E ", " EE EE ", " EEF FEE ", " EEF FEE ", " EEF FEE ", " AABAA EEEEB BEEEE AABAA ", " BBB EBBBEB BEBBBE BBB ", " BBB EBBBEB BEBBBE BBB ", " BBB EBBBEB BEBBBE BBB ", " BBB EBBBEB BEBBBE BBB ", " BBB EBBBEB BEBBBE BBB ", " AABAA EEEEB BEEEE AABAA ", " EEF FEE ", " EEF FEE ", " EEF FEE ", " EE EE ", " E E ", " E E ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " A A ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " F F ", " F F ", " F F ", " F F ", " F F ", " E F F E ", " E F F E ", " EEF FEE ", " EEF FEE ", " EEE EEE ", " EEE EEE ", " AABAA EEEEEAA AAEEEEE AABAA ", " BBB EBBBEF FEBBBE BBB ", " BBB EBBBEF FEBBBE BBB ", " BBB EBBBEF FEBBBE BBB ", " BBB EBBBEF FEBBBE BBB ", " BBB EBBBEF FEBBBE BBB ", " AABAA EEEEEAA AAEEEEE AABAA ", " EEE EEE ", " EEE EEE ", " EEF FEE ", " EEF FEE ", " E F F E ", " E F F E ", " F F ", " F F ", " F F ", " F F ", " F F ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " AABAA EEEEEAA AAEEEEE AABAA ", " BBB EBBBBF FBBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBF FBBBBE BBB ", " AABAA EEEEEAA AAEEEEE AABAA ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " AABAA AEEEAA AAEEEA AABAA ", " BBB EBBBBF FBBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBA ABBBBE BBB ", " BBB EBBBBF FBBBBE BBB ", " AABAA AEEEAA AAEEEA AABAA ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " AABAA AAEEEAA AAEEEAA AABAA ", " BBB BBBBF FBBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBA ABBBB BBB ", " BBB BBBBF FBBBB BBB ", " AABAA AAEEEAA AAEEEAA AABAA ", " E E ", " E E ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " E E ", " E E ", " E E ", " AABAA AAEEEAA AAEEEAA AABAA ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " BBB BBBBF FBBBB BBB ", " AABAA AAEEEAA AAEEEAA AABAA ", " E E ", " E E ", " E E ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " E E ", " AABAA AAEEEEAA AAEEEEAA AABAA ", " BBB BBBBE EBBBB BBB ", " BBB BBBBE EBBBB BBB ", " BBB BBBBE EBBBB BBB ", " BBB BBBBE EBBBB BBB ", " BBB BBBBE EBBBB BBB ", " AABAA AAEEEEAA AAEEEEAA AABAA ", " E E ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAEEBAAAA AAAABEEAA BBBBB ", " BBB BBBBB BBBBB BBB ", " BBB BBBBB BBBBB BBB ", " BBB BBBBB BBBBB BBB ", " BBB BBBBB BBBBB BBB ", " BBB BBBBB BBBBB BBB ", " BBBBB AAEEBAAAA AAAABEEAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB EAABBAAAA AAAABBAAE BBBBB ", " BBB EBBBBBB BBBBBBE BBB ", " BBB EBBBBBB BBBBBBE BBB ", " BBB EBBBBBB BBBBBBE BBB ", " BBB EBBBBBB BBBBBBE BBB ", " BBB EBBBBBB BBBBBBE BBB ", " BBBBB EAABBAAAA AAAABBAAE BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAAABBAAAAA AAAAABBAAAA BBBBB ", " BBB BBBBBB BBBBBB BBB ", " BBB BBBBBB BBBBBB BBB ", " BBB BBBBBB BBBBBB BBB ", " BBB BBBBBB BBBBBB BBB ", " BBB BBBBBB BBBBBB BBB ", " BBBBB AAAABBAAAAA AAAAABBAAAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAAABBAAAAAA AAAAAABBAAAA BBBBB ", " BBB BBBBBBB BBBBBBB BBB ", " BBB BBBBBBB BBBBBBB BBB ", " BBB BBBBBBB BBBBBBB BBB ", " BBB BBBBBBB BBBBBBB BBB ", " BBB BBBBBBB BBBBBBB BBB ", " BBBBB AAAABBAAAAAA AAAAAABBAAAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAAABBBAAAAAAAA AAAAAAAABBBAAAA BBBBB ", " BBB BBBBBBBB BBBBBBBB BBB ", " BBB BBBBBBBB BBBBBBBB BBB ", " BBB BBBBBBBB BBBBBBBB BBB ", " BBB BBBBBBBB BBBBBBBB BBB ", " BBB BBBBBBBB BBBBBBBB BBB ", " BBBBB AAAABBBAAAAAAAA AAAAAAAABBBAAAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAAAABBBAAAAAAAAAAAAAAAAAAAAAAABBBAAAAA BBBBB ", " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", " BBB BBBBBBBBBBB BBBBBBBBBBB BBB ", " BBBBB AAAAABBBAAAAAAAAAAAAAAAAAAAAAAABBBAAAAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBB AAAAAABBBBBAAAAAAAAAAAAABBBBBAAAAAA BBBBB ", " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", " BBB BBBBBBBBBBBBBCCCBBBBBBBBBBBBB BBB ", " BBB BBBBBBBBBBBBBCCCBBBBBBBBBBBBB BBB ", " BBB BBBBBBBBBBBBBCCCBBBBBBBBBBBBB BBB ", " BBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBB ", " BBBBB AAAAAABBBBBAAAAAAAAAAAAABBBBBAAAAAA BBBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBAA AAAAAAAABBBBBBBBBBBBBAAAAAAAA AABBBB ", " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", " BBB BBBBBBBBBBCCCBBBBBBBBBB BBB ", " BBB BBBBBBBBBBCCCBBBBBBBBBB BBB ", " BBB BBBBBBBBBBCCCBBBBBBBBBB BBB ", " BBB BBBBBBBBBBBBBBBBBBBBBBB BBB ", " BBBBAA AAAAAAAABBBBBBBBBBBBBAAAAAAAA AABBBB ", " D ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBAAA AAAAAAAAAAAAAAAAAAAAAAA AAABBB ", " BBBB BBBBBBBBBBBBB BBBB ", " BBBB BBBBBCCCBBBBB BBBB ", " BBBB BBBBBCCCBBBBB BBBB ", " BBBB BBBBBCCCBBBBB BBBB ", " BBBB BBBBBBBBBBBBB BBBB ", " BBBAAA AAAAAAAAAAAAAAAAAAAAAAA AAABBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BBBBAA AAAAAAAAAAAAA AABBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBBAA AAAAAAAAAAAAA AABBBB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " BAABAAA AAABAAB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BAABAAA AAABAAB ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAABAAAA AAAABAAA ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " BBBB BBBB ", " AAABAAAA AAAABAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AABBAAA AAABBAA ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " AABBAAA AAABBAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAABBAAA AAABBAAA ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " AAABBAAA AAABBAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAABAAAA AAAABAAAA ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " AAAABAAAA AAAABAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAABBAAAAA AAAAABBAAA ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " BBBBB BBBBB ", " AAABBAAAAA AAAAABBAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAABBAAAAA AAAAABBAAA ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " AAABBAAAAA AAAAABBAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAABBBAAAAA AAAAABBBAAAA ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " BBBBBBB BBBBBBB ", " AAAABBBAAAAA AAAAABBBAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAABBAAAAAAA AAAAAAABBAAAAA ", " BBBBBBBB BBBBBBBB ", " BBBBBBBB BBBBBBBB ", " BBBBBBBB BBBBBBBB ", " BBBBBBBB BBBBBBBB ", " BBBBBBBB BBBBBBBB ", " AAAAABBAAAAAAA AAAAAAABBAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAABBBAAAAAAAAAA AAAAAAAAAABBBAAAAA ", " BBBBBBBBB BBBBBBBBB ", " BBBBBBBBB BBBBBBBBB ", " BBBBBBBBB BBBBBBBBB ", " BBBBBBBBB BBBBBBBBB ", " BBBBBBBBB BBBBBBBBB ", " AAAAABBBAAAAAAAAAA AAAAAAAAAABBBAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAABBBBAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBAAAAA ", " BBBBBBBBBBBBB BBBBBBBBBBBBB ", " BBBBBBBBBBBBB BBBBBBBBBBBBB ", " BBBBBBBBBBBBB BBBBBBBBBBBBB ", " BBBBBBBBBBBBB BBBBBBBBBBBBB ", " BBBBBBBBBBBBB BBBBBBBBBBBBB ", " AAAAABBBBAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAAAABBBBBBAAAAAAAAAAAAAAABBBBBBAAAAAAA ", " BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB ", " BBBBBBBBBBBBBBBBCCCBBBBBBBBBBBBBBBB ", " BBBBBBBBBBBBBBBBCCCBBBBBBBBBBBBBBBB ", " BBBBBBBBBBBBBBBBCCCBBBBBBBBBBBBBBBB ", " BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB ", " AAAAAAABBBBBBAAAAAAAAAAAAAAABBBBBBAAAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAAAAAAABBBBBBBBBBBBBBBAAAAAAAAAA ", " BBBBBBBBBBBBBBBBBBBBBBBBBBB ", " BBBBBBBBBBBBCCCBBBBBBBBBBBB ", " BBBBBBBBBBBBCCCBBBBBBBBBBBB ", " BBBBBBBBBBBBCCCBBBBBBBBBBBB ", " BBBBBBBBBBBBBBBBBBBBBBBBBBB ", " AAAAAAAAAABBBBBBBBBBBBBBBAAAAAAAAAA ", " D ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAAAAAAAAAAAAAAAAAAAAAAAA ", " BBBBBBBBBBBBBBB ", " BBBBBBCCCBBBBBB ", " BBBBBBCCCBBBBBB ", " BBBBBBCCCBBBBBB ", " BBBBBBBBBBBBBBB ", " AAAAAAAAAAAAAAAAAAAAAAAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + .aisle(" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " AAAAAAAAAAAAAAA ", " ", " ", " ", " ", " ", " AAAAAAAAAAAAAAA ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ") + // spotless:on + .where(' ', any()) - .where("G", controller(blocks(definition.getBlock()))) - .where('F', blocks(CYCLOZINE_CHEMICALLY_REPELLING_CASING.get()) - .or(Predicates.abilities(PartAbility.EXPORT_ITEMS).setMaxGlobalLimited(16)) - .or(Predicates.abilities(PartAbility.EXPORT_FLUIDS).setMaxGlobalLimited(16)) - .or(Predicates.abilities(PartAbility.INPUT_ENERGY).setMaxGlobalLimited(2)) - .or(Predicates.abilities(PartAbility.INPUT_LASER).setMaxGlobalLimited(1)) - .or(Predicates.abilities(PartAbility.OUTPUT_LASER).setMaxGlobalLimited(1)) - .or(Predicates.abilities(PartAbility.IMPORT_FLUIDS).setMaxGlobalLimited(16)) - .or(Predicates.abilities(PartAbility.IMPORT_ITEMS).setMaxGlobalLimited(16))) - .where('C', blocks(MULTIPURPOSE_INTERSTELLAR_GRADE_CASING.get())) - .where('D', blocks(CASING_ATOMIC.get())) + .where("Q", controller(blocks(definition.getBlock()))) + .where('H', blocks(CYCLOZINE_CHEMICALLY_REPELLING_CASING.get())) + .where('G', blocks(CYCLOZINE_CHEMICALLY_REPELLING_CASING.get())) + .where('F', blocks(CYCLOZINE_CHEMICALLY_REPELLING_CASING.get())) + .where('C', blocks(ROYAL_ICHORIUM_CASING.get())) + .where('D', CosmicPredicates.stellarModuleSlot()) // Module controller anchor points - accepts air + // or formed stellar modules .where('B', blocks(MULTIPURPOSE_INTERSTELLAR_GRADE_CASING.get())) .where('E', blocks(ULTRA_POWERED_CASING.get())) .where('A', blocks(CASING_HIGH_TEMPERATURE_SMELTING.get())) diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/StellarSmeltingModule.java b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/StellarSmeltingModule.java new file mode 100644 index 000000000..b5ecd853c --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/StellarSmeltingModule.java @@ -0,0 +1,59 @@ +package com.ghostipedia.cosmiccore.common.machine.multiblock.multi; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.StellarBaseModule; +import com.ghostipedia.cosmiccore.common.data.recipe.CosmicRecipeModifiers; +import com.ghostipedia.cosmiccore.gtbridge.CosmicRecipeTypes; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.data.RotationState; +import com.gregtechceu.gtceu.api.machine.MultiblockMachineDefinition; +import com.gregtechceu.gtceu.api.pattern.FactoryBlockPattern; + +import static com.ghostipedia.cosmiccore.api.registries.CosmicRegistration.REGISTRATE; +import static com.ghostipedia.cosmiccore.common.data.CosmicBlocks.*; +import static com.gregtechceu.gtceu.api.pattern.Predicates.*; +import static com.gregtechceu.gtceu.api.pattern.util.RelativeDirection.*; +import static com.gregtechceu.gtceu.common.data.GCYMBlocks.*; + +public class StellarSmeltingModule { + + public static final MultiblockMachineDefinition STELLAR_SMELTING_MODULE = REGISTRATE + .multiblock("stellar_smelting_module", StellarBaseModule::new) + .langValue("Godsbane Hyper-tensor Platform") + .rotationState(RotationState.NON_Y_AXIS) + .recipeType(CosmicRecipeTypes.STELLAR_SMELTING) + .appearanceBlock(CASING_HIGH_TEMPERATURE_SMELTING) + .recipeModifiers(CosmicRecipeModifiers.STELLAR_MODULE_OVERCLOCK) + // spotless:off + .pattern(definition -> FactoryBlockPattern.start(RIGHT, BACK, UP) + // The module has a compact structure that extends from the Iris ring + // 'A' = CASING_HIGH_TEMPERATURE_SMELTING (shared with Iris ring structure) + // 'B' = MULTIPURPOSE_INTERSTELLAR_GRADE_CASING + // 'C' = Controller + .aisle(" AAAAA ", " ACCCCCA ", "AFCFFFCFA", "AFCFFFCFA", "AFCFFFCFA", " ACCCCCA ", " AAAAA ") + .aisle(" B B ", "DDDDDDDDD", "DDDDDDDDD", "GGGGGGGGG", "DDDDDDDDD", "DDDDDDDDD", " B B ") + .aisle(" B B ", "BCCCCCCCB", "GGGGGGGGG", "GGGGGGGGG", "GGGGGGGGG", "BCC CCB", " B B ") + .aisle(" B B ", "BCC CCB", "GGGBBBGGG", "GGGBBBGGG", "GGGBBBGGG", "BCC CCB", " B B ") + .aisle(" BBBBBBB ", "BCC B CCB", "GGGBBBGGG", "GGGBBBGGG", "GGGBBBGGG", "BCC B CCB", " BBBBBBB ") + .aisle(" B B ", "BCC CCB", "GGGBBBGGG", "GGGBBBGGG", "GGGBBBGGG", "BCC CCB", " B B ") + .aisle(" B B ", "BCCCCCCCB", "GGGGGGGGG", "GGGGGGGGG", "GGGGGGGGG", "BCC CCB", " B B ") + .aisle(" B B ", "DDDDDDDDD", "DDDDDDDDD", "GGGGGGGGG", "DDDDDDDDD", "DDDDDDDDD", " B B ") + .aisle(" AAAAA ", " AEEEEEA ", "AFEFFFEFA", "AFEFQFEFA", "AFEFFFEFA", " AEEEEEA ", " AAAAA ") + + .where(' ', any()) + .where('Q', controller(blocks(definition.getBlock()))) + .where('B', blocks(ROYAL_ICHORIUM_CASING.get())) // Shared ring blocks + .where('C', blocks(CYCLOZINE_CHEMICALLY_REPELLING_CASING.get()))// Shared ring blocks + .where('D', blocks(CASING_HIGH_TEMPERATURE_SMELTING.get())) // Shared ring blocks + .where('E', blocks(MULTIPURPOSE_INTERSTELLAR_GRADE_CASING.get())) + .where('F', blocks(ULTRA_POWERED_CASING.get())) + .where('G', blocks(MULTIPURPOSE_INTERSTELLAR_GRADE_CASING.get())) + .where('A', blocks(SOMARUST_CASING.get())) + .build()) + // spotless:on + .workableCasingModel(GTCEu.id("block/casings/gcym/high_temperature_smelting_casing"), + GTCEu.id("block/overlay/machine/alloy_blast_smelter")) + .register(); + + public static void init() {} +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/logic/MultithreadedMachine.java b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/logic/MultithreadedMachine.java new file mode 100644 index 000000000..1567f2411 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/logic/MultithreadedMachine.java @@ -0,0 +1,500 @@ +package com.ghostipedia.cosmiccore.common.machine.multiblock.multi.logic; + +import com.ghostipedia.cosmiccore.api.machine.multiblock.IMultithreadedMachine; + +import com.gregtechceu.gtceu.api.capability.recipe.EURecipeCapability; +import com.gregtechceu.gtceu.api.capability.recipe.FluidRecipeCapability; +import com.gregtechceu.gtceu.api.capability.recipe.IO; +import com.gregtechceu.gtceu.api.capability.recipe.IRecipeHandler; +import com.gregtechceu.gtceu.api.capability.recipe.ItemRecipeCapability; +import com.gregtechceu.gtceu.api.capability.recipe.RecipeCapability; +import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; +import com.gregtechceu.gtceu.api.machine.TickableSubscription; +import com.gregtechceu.gtceu.api.machine.feature.multiblock.IMultiPart; +import com.gregtechceu.gtceu.api.machine.multiblock.MultiblockDisplayText; +import com.gregtechceu.gtceu.api.machine.multiblock.WorkableElectricMultiblockMachine; +import com.gregtechceu.gtceu.api.machine.trait.RecipeHandlerList; +import com.gregtechceu.gtceu.api.machine.trait.RecipeLogic; +import com.gregtechceu.gtceu.api.recipe.GTRecipe; +import com.gregtechceu.gtceu.common.machine.multiblock.part.EnergyHatchPartMachine; +import com.gregtechceu.gtceu.utils.FormattingUtil; + +import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; +import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; +import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.DyeColor; + +import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * A multiblock machine that can run multiple independent recipes simultaneously. + * Each "thread" is assigned a color-coded set of input buses/hatches. + * The maximum number of threads is determined by the energy hatch amperage. + *

+ * Design: + * - 4A energy hatch = 4 max threads + * - 16A energy hatch = 16 max threads + * - Each thread needs a uniquely colored input bus/hatch pair + * - All threads share output buses/hatches + * - Energy is split evenly among active threads + */ +public class MultithreadedMachine extends WorkableElectricMultiblockMachine implements IMultithreadedMachine { + + protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( + MultithreadedMachine.class, WorkableElectricMultiblockMachine.MANAGED_FIELD_HOLDER); + + /** + * Maximum possible threads (limited by largest energy hatch amperage) + */ + public static final int MAX_THREADS = 16; + + /** + * Map of thread color -> RecipeLogic for that thread + */ + @Getter + private final Int2ObjectMap threadLogics = new Int2ObjectLinkedOpenHashMap<>(); + + /** + * Map of thread color -> input handler list for that thread + */ + private final Int2ObjectMap> threadInputHandlers = new Int2ObjectLinkedOpenHashMap<>(); + + /** + * Shared output handlers for all threads + */ + private List sharedOutputHandlers = new ArrayList<>(); + + /** + * Maximum number of threads allowed by the energy hatch + */ + @Persisted + @DescSynced + @Getter + private int maxThreads = 0; + + /** + * Currently active thread count + */ + @Persisted + @DescSynced + @Getter + private int activeThreadCount = 0; + + /** + * Total amperage available from energy hatch(es) + */ + @Getter + private int totalAmperage = 0; + + @Nullable + private TickableSubscription threadTickSubscription; + + public MultithreadedMachine(IMachineBlockEntity holder) { + super(holder); + } + + @Override + @NotNull + public ManagedFieldHolder getFieldHolder() { + return MANAGED_FIELD_HOLDER; + } + + @Override + protected RecipeLogic createRecipeLogic(Object... args) { + // We don't use the default recipe logic - we manage multiple thread logics instead + // Return a dummy that does nothing, actual work is done by thread logics + return new RecipeLogic(this) { + + @Override + public void serverTick() { + // Do nothing - threading is handled separately + } + }; + } + + @Override + public void onStructureFormed() { + super.onStructureFormed(); + + // Clear previous state + threadLogics.clear(); + threadInputHandlers.clear(); + sharedOutputHandlers.clear(); + maxThreads = 0; + totalAmperage = 0; + + // Detect energy hatch amperage to determine max threads + detectEnergyHatchAmperage(); + + // Partition input handlers by color + partitionHandlersByColor(); + + // Collect shared output handlers + collectOutputHandlers(); + + // Create thread logics for each color group + createThreadLogics(); + + // Start the thread tick subscription + updateThreadSubscription(); + } + + @Override + public void onStructureInvalid() { + super.onStructureInvalid(); + + // Deactivate all threads + for (MultithreadedRecipeLogic logic : threadLogics.values()) { + logic.deactivateThread(); + } + + threadLogics.clear(); + threadInputHandlers.clear(); + sharedOutputHandlers.clear(); + maxThreads = 0; + activeThreadCount = 0; + totalAmperage = 0; + + if (threadTickSubscription != null) { + threadTickSubscription.unsubscribe(); + threadTickSubscription = null; + } + } + + /** + * Detect the amperage of energy hatches to determine max thread count. + */ + private void detectEnergyHatchAmperage() { + Map ioMap = getMultiblockState().getMatchContext() + .getOrCreate("ioMap", Long2ObjectMaps::emptyMap); + + for (IMultiPart part : getParts()) { + if (part instanceof EnergyHatchPartMachine energyHatch) { + IO io = ioMap.getOrDefault(part.self().getPos().asLong(), IO.IN); + if (io == IO.IN || io == IO.BOTH) { + totalAmperage += energyHatch.getAmperage(); + } + } + } + + // Max threads = total amperage, capped at MAX_THREADS + maxThreads = Math.min(totalAmperage, MAX_THREADS); + } + + /** + * Partition input handlers by their paint color. + * Each unique color becomes a potential thread. + */ + private void partitionHandlersByColor() { + for (IMultiPart part : getParts()) { + var handlerLists = part.getRecipeHandlers(); + for (RecipeHandlerList handlerList : handlerLists) { + if (handlerList.getHandlerIO() == IO.IN || handlerList.getHandlerIO() == IO.BOTH) { + // Check if this handler has item or fluid capability (not just energy) + boolean hasItemOrFluid = handlerList.hasCapability(ItemRecipeCapability.CAP) || + handlerList.hasCapability(FluidRecipeCapability.CAP); + + if (hasItemOrFluid) { + int color = handlerList.getColor(); + threadInputHandlers.computeIfAbsent(color, k -> new ArrayList<>()).add(handlerList); + } + } + } + } + } + + /** + * Collect output handlers that will be shared by all threads. + */ + private void collectOutputHandlers() { + for (IMultiPart part : getParts()) { + var handlerLists = part.getRecipeHandlers(); + for (RecipeHandlerList handlerList : handlerLists) { + if (handlerList.getHandlerIO() == IO.OUT || handlerList.getHandlerIO() == IO.BOTH) { + // Check if this handler has item or fluid capability + boolean hasItemOrFluid = handlerList.hasCapability(ItemRecipeCapability.CAP) || + handlerList.hasCapability(FluidRecipeCapability.CAP); + + if (hasItemOrFluid) { + sharedOutputHandlers.add(handlerList); + } + } + } + } + } + + /** + * Create a MultithreadedRecipeLogic for each color group, up to maxThreads. + */ + private void createThreadLogics() { + int threadIndex = 0; + + // Calculate EU/t budget per thread + // Each thread gets 1A of voltage from the total amperage pool + // With 16A UV hatch and 16 threads, each gets 1A UV = 524,288 EU/t + // IMPORTANT: EnergyContainerList.getInputVoltage() returns TOTAL EU/t (voltage*amperage compacted) + // We need to use getHighestInputVoltage() which returns the actual per-amp voltage + long euPerThread = energyContainer != null ? energyContainer.getHighestInputVoltage() : 0; + + for (Int2ObjectMap.Entry> entry : threadInputHandlers.int2ObjectEntrySet()) { + if (threadIndex >= maxThreads) break; + + int color = entry.getIntKey(); + List inputHandlers = entry.getValue(); + + MultithreadedRecipeLogic logic = new MultithreadedRecipeLogic(this, threadIndex, color); + + // Set the EU/t budget for this thread + logic.setMaxEUtPerThread(euPerThread); + + // Build capability maps for this thread + Map> threadProxy = new EnumMap<>(IO.class); + Map, List>>> threadFlat = new EnumMap<>(IO.class); + + // Add input handlers (thread-specific, color-coded) + threadProxy.put(IO.IN, new ArrayList<>(inputHandlers)); + + // Add output handlers (shared) + threadProxy.put(IO.OUT, new ArrayList<>(sharedOutputHandlers)); + + // Build flattened map from proxy + for (Map.Entry> proxyEntry : threadProxy.entrySet()) { + IO io = proxyEntry.getKey(); + Map, List>> capMap = new HashMap<>(); + + for (RecipeHandlerList handlerList : proxyEntry.getValue()) { + for (var capEntry : handlerList.getHandlerMap().entrySet()) { + RecipeCapability cap = capEntry.getKey(); + List> handlers = capEntry.getValue(); + capMap.computeIfAbsent(cap, k -> new ArrayList<>()).addAll(handlers); + } + } + + threadFlat.put(io, capMap); + } + + // Also add energy handlers from machine for recipe EU consumption + addEnergyHandlersToThread(threadProxy, threadFlat); + + logic.setThreadCapabilitiesProxy(threadProxy); + logic.setThreadCapabilitiesFlat(threadFlat); + logic.activateThread(); + + threadLogics.put(color, logic); + threadIndex++; + } + + activeThreadCount = threadLogics.size(); + } + + /** + * Add energy handlers to a thread's capability maps so recipes can consume EU. + * Must add to both proxy and flat maps for full compatibility. + */ + private void addEnergyHandlersToThread( + Map> threadProxy, + Map, List>>> threadFlat) { + // Get energy handlers from the machine's global capabilities (proxy) + var machineProxy = getCapabilitiesProxy(); + if (machineProxy != null && machineProxy.containsKey(IO.IN)) { + for (RecipeHandlerList handlerList : machineProxy.get(IO.IN)) { + if (handlerList.hasCapability(EURecipeCapability.CAP)) { + threadProxy.computeIfAbsent(IO.IN, k -> new ArrayList<>()).add(handlerList); + } + } + } + + // Also add to flat map + var machineFlat = getCapabilitiesFlat(); + if (machineFlat != null && machineFlat.containsKey(IO.IN)) { + var inCaps = machineFlat.get(IO.IN); + if (inCaps != null && inCaps.containsKey(EURecipeCapability.CAP)) { + var energyHandlers = inCaps.get(EURecipeCapability.CAP); + if (energyHandlers != null && !energyHandlers.isEmpty()) { + threadFlat.computeIfAbsent(IO.IN, k -> new HashMap<>()) + .put(EURecipeCapability.CAP, new ArrayList<>(energyHandlers)); + } + } + } + } + + /** + * Update the tick subscription for thread processing. + */ + private void updateThreadSubscription() { + if (isFormed() && !threadLogics.isEmpty()) { + threadTickSubscription = subscribeServerTick(threadTickSubscription, this::tickThreads); + } else if (threadTickSubscription != null) { + threadTickSubscription.unsubscribe(); + threadTickSubscription = null; + } + } + + /** + * Called every server tick to process all thread logics. + */ + private void tickThreads() { + if (!isFormed() || !isWorkingEnabled()) return; + + // Calculate energy per thread + long availableEnergy = getEnergyPerThread(); + + // Tick each active thread + int runningThreads = 0; + for (MultithreadedRecipeLogic logic : threadLogics.values()) { + if (logic.isThreadActive()) { + // Each thread gets its share of energy + logic.serverTick(); + if (logic.isWorking()) { + runningThreads++; + } + } + } + + activeThreadCount = runningThreads; + } + + /** + * Calculate energy available per thread. + * Energy is split evenly among all active threads. + */ + private long getEnergyPerThread() { + if (energyContainer == null || activeThreadCount == 0) return 0; + return energyContainer.getInputVoltage() * totalAmperage / Math.max(1, getRunningThreadCount()); + } + + /** + * Get the number of threads currently running recipes. + */ + public int getRunningThreadCount() { + int count = 0; + for (MultithreadedRecipeLogic logic : threadLogics.values()) { + if (logic.isWorking()) count++; + } + return count; + } + + /** + * Get the input handlers for a specific thread color. + */ + @Nullable + public List getThreadInputHandlers(int color) { + return threadInputHandlers.get(color); + } + + /** + * Get the shared output handlers. + */ + public List getSharedOutputHandlers() { + return sharedOutputHandlers; + } + + /** + * Get a color name for display purposes. + */ + public static String getColorName(int color) { + if (color == -1) return "Unpainted"; + for (DyeColor dye : DyeColor.values()) { + if (dye.getFireworkColor() == color || dye.getTextColor() == color) { + return dye.getName(); + } + } + return "Color #" + Integer.toHexString(color); + } + + @Override + public void addDisplayText(List textList) { + // Basic multiblock status + var builder = MultiblockDisplayText.builder(textList, isFormed()) + .setWorkingStatus(isWorkingEnabled(), getRunningThreadCount() > 0); + + if (isFormed()) { + // Thread status header + builder.addCustom(tl -> { + tl.add(Component.translatable("cosmiccore.machine.multithreaded.thread_status") + .withStyle(ChatFormatting.AQUA)); + tl.add(Component.translatable("cosmiccore.machine.multithreaded.max_threads", + FormattingUtil.formatNumbers(maxThreads)) + .withStyle(ChatFormatting.GRAY)); + tl.add(Component.translatable("cosmiccore.machine.multithreaded.active_threads", + FormattingUtil.formatNumbers(getRunningThreadCount()), + FormattingUtil.formatNumbers(threadLogics.size())) + .withStyle(ChatFormatting.GRAY)); + }); + + // Per-thread status + builder.addCustom(tl -> { + for (MultithreadedRecipeLogic logic : threadLogics.values()) { + String colorName = getColorName(logic.getThreadColor()); + ChatFormatting statusColor = logic.isWorking() ? ChatFormatting.GREEN : ChatFormatting.YELLOW; + + String status; + if (logic.isWorking()) { + int percent = (int) (logic.getProgressPercent() * 100); + status = percent + "%"; + } else if (logic.isIdle()) { + status = "Idle"; + } else if (logic.isWaiting()) { + status = "Waiting"; + } else { + status = "Suspended"; + } + + tl.add(Component.literal(" [" + colorName + "] " + status) + .withStyle(statusColor)); + } + }); + + // Energy info + builder.addEnergyUsageLine(energyContainer); + builder.addEnergyTierLine(tier); + } + + getDefinition().getAdditionalDisplay().accept(this, textList); + } + + // === IMultithreadedMachine interface implementation === + + @Override + public Int2ObjectMap getThreadLogicsMap() { + return threadLogics; + } + + @Override + public int getMaxThreadCount() { + return maxThreads; + } + + @Override + public int getCurrentThreadCount() { + return threadLogics.size(); + } + + @Override + public boolean beforeWorking(@Nullable GTRecipe recipe) { + // Called by individual thread logics + return super.beforeWorking(recipe); + } + + @Override + public boolean onWorking() { + // Called by individual thread logics + return super.onWorking(); + } + + @Override + public void afterWorking() { + // Called by individual thread logics + super.afterWorking(); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/logic/MultithreadedRecipeLogic.java b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/logic/MultithreadedRecipeLogic.java new file mode 100644 index 000000000..99fc1f7aa --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/logic/MultithreadedRecipeLogic.java @@ -0,0 +1,435 @@ +package com.ghostipedia.cosmiccore.common.machine.multiblock.multi.logic; + +import com.gregtechceu.gtceu.api.capability.recipe.IO; +import com.gregtechceu.gtceu.api.capability.recipe.IRecipeCapabilityHolder; +import com.gregtechceu.gtceu.api.capability.recipe.IRecipeHandler; +import com.gregtechceu.gtceu.api.capability.recipe.RecipeCapability; +import com.gregtechceu.gtceu.api.machine.feature.IRecipeLogicMachine; +import com.gregtechceu.gtceu.api.machine.trait.RecipeHandlerList; +import com.gregtechceu.gtceu.api.machine.trait.RecipeLogic; +import com.gregtechceu.gtceu.api.recipe.ActionResult; +import com.gregtechceu.gtceu.api.recipe.GTRecipe; +import com.gregtechceu.gtceu.api.recipe.RecipeHelper; +import com.gregtechceu.gtceu.api.recipe.modifier.ModifierFunction; + +import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; +import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; +import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; + +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.Iterator; + +/** + * A RecipeLogic instance that represents a single "thread" in a MultithreadedMachine. + * Each thread can process one recipe independently of other threads. + *

+ * This class implements IRecipeCapabilityHolder to provide a filtered view of handlers + * that only includes this thread's color-coded inputs and shared outputs. + */ +public class MultithreadedRecipeLogic extends RecipeLogic implements IRecipeCapabilityHolder { + + public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( + MultithreadedRecipeLogic.class, RecipeLogic.MANAGED_FIELD_HOLDER); + + @Getter + private final int threadIndex; + + @Getter + private final int threadColor; + + @Persisted + @DescSynced + @Getter + private boolean threadActive = false; + + /** + * Maximum EU/t this thread can use. + * Set by the parent MultithreadedMachine based on energy hatch amperage / thread count. + */ + @Setter + @Getter + private long maxEUtPerThread = 0; + + /** + * This thread's capability proxy - filtered to only include its handlers. + * Set by the parent MultithreadedMachine. + */ + @Setter + private Map> threadCapabilitiesProxy = new EnumMap<>(IO.class); + + /** + * Flattened capability map for this thread. + */ + @Setter + private Map, List>>> threadCapabilitiesFlat = new EnumMap<>(IO.class); + + public MultithreadedRecipeLogic(IRecipeLogicMachine machine, int threadIndex, int threadColor) { + super(machine); + this.threadIndex = threadIndex; + this.threadColor = threadColor; + } + + @Override + public ManagedFieldHolder getFieldHolder() { + return MANAGED_FIELD_HOLDER; + } + + // === IRecipeCapabilityHolder implementation === + // These methods provide a filtered view of handlers for this thread only + + @Override + public Map> getCapabilitiesProxy() { + return threadCapabilitiesProxy; + } + + @Override + public Map, List>>> getCapabilitiesFlat() { + return threadCapabilitiesFlat; + } + + /** + * Called by the parent machine to activate this thread. + */ + public void activateThread() { + this.threadActive = true; + } + + /** + * Called by the parent machine to deactivate this thread. + */ + public void deactivateThread() { + this.threadActive = false; + if (isWorking()) { + setStatus(Status.IDLE); + this.progress = 0; + this.duration = 0; + this.lastRecipe = null; + } + } + + /** + * Check if this thread can process recipes. + */ + public boolean canWork() { + return threadActive && machine.isRecipeLogicAvailable(); + } + + @Override + public void serverTick() { + if (!canWork()) { + if (isWorking()) { + // Thread was deactivated mid-recipe, pause it + setStatus(Status.SUSPEND); + } + return; + } + super.serverTick(); + } + + /** + * Get the parent MultithreadedMachine. + */ + @Nullable + private MultithreadedMachine getParentMachine() { + if (machine instanceof MultithreadedMachine mtm) { + return mtm; + } + return null; + } + + /** + * Standard overclock voltage multiplier (4x per OC level). + */ + private static final double OC_VOLTAGE_FACTOR = 4.0; + + /** + * Standard overclock duration multiplier (0.5x per OC level - halves duration). + */ + private static final double OC_DURATION_FACTOR = 0.5; + + /** + * Calculate the maximum number of overclock levels that fit within the thread's EU/t budget. + * Each overclock level multiplies EU/t by 4 and halves duration. + * + * @param baseEUt The base recipe EU/t + * @return Number of overclock levels possible (0 = no overclocking) + */ + protected int calculateMaxOverclockLevels(long baseEUt) { + if (maxEUtPerThread <= 0 || baseEUt <= 0) { + return 0; + } + + int levels = 0; + long currentEUt = baseEUt; + + // Each OC level multiplies EU/t by 4 + while (currentEUt * OC_VOLTAGE_FACTOR <= maxEUtPerThread) { + currentEUt = (long) (currentEUt * OC_VOLTAGE_FACTOR); + levels++; + } + + return levels; + } + + /** + * Apply overclocking to a recipe within this thread's energy budget. + * Overclocking multiplies EU/t by 4 and halves duration per level. + * + * @param recipe The base recipe + * @return The overclocked recipe copy, or a copy of the original if no overclocking is possible + */ + @Nullable + protected GTRecipe applyThreadOverclock(GTRecipe recipe) { + long baseEUt = recipe.getInputEUt().getTotalEU(); + + // If base recipe exceeds budget, can't run at all + if (baseEUt > maxEUtPerThread && maxEUtPerThread > 0) { + return null; + } + + int ocLevels = calculateMaxOverclockLevels(baseEUt); + if (ocLevels <= 0) { + // No overclocking possible, return a copy of the recipe + return recipe.copy(); + } + + // Calculate overclocked values + double eutMultiplier = Math.pow(OC_VOLTAGE_FACTOR, ocLevels); + double durationMultiplier = Math.pow(OC_DURATION_FACTOR, ocLevels); + + // Build modifier to apply overclock + ModifierFunction modifier = ModifierFunction.builder() + .eutMultiplier(eutMultiplier) + .durationMultiplier(durationMultiplier) + .build(); + + // Apply to a COPY of the recipe to avoid modifying the original + return modifier.apply(recipe.copy()); + } + + /** + * Override to apply thread-specific recipe modification and check availability. + * Applies overclocking within the thread's energy budget. + * + * IMPORTANT: The 'match' parameter is the RAW recipe from the recipe type, + * NOT yet modified by the machine. We must NOT call machine.fullModifyRecipe() + * as that would apply overclock based on full machine power. + */ + @Override + public boolean checkMatchedRecipeAvailable(GTRecipe match) { + // Get the BASE recipe EU/t before any modification + long baseEUt = match.getInputEUt().getTotalEU(); + + // Check if base recipe fits within thread's budget + if (baseEUt > maxEUtPerThread && maxEUtPerThread > 0) { + // Base recipe too expensive for this thread + return false; + } + + // Apply our custom overclock within thread's energy budget + GTRecipe modified = applyThreadOverclock(match); + if (modified == null) { + return false; + } + + // Trim outputs to fit in output slots + GTRecipe trimmed = RecipeHelper.trimRecipeOutputs(modified, machine.getOutputLimits()); + if (trimmed == null) { + return false; + } + + // Check if the modified recipe matches our thread's inputs + ActionResult result = checkRecipe(trimmed); + if (result.isSuccess()) { + // Store the modified recipe for execution + setupRecipe(trimmed); + + // IMPORTANT: Store the original (unmodified) recipe for later re-application + // This is used by onRecipeFinish to re-overclock when repeating the recipe + if (lastRecipe != null && getStatus() == Status.WORKING) { + lastOriginRecipe = match; + lastFailedMatches = null; + return true; + } + } + return false; + } + + /** + * Override searchRecipe to use THIS THREAD as the capability holder, + * not the machine. This is critical for proper recipe filtering. + */ + @Override + public @NotNull Iterator searchRecipe() { + // Use THIS thread as the capability holder for recipe searching + return machine.getRecipeType().searchRecipe(this, r -> matchRecipe(r).isSuccess()); + } + + /** + * Override findAndHandleRecipe to ensure we ALWAYS go through our custom + * checkMatchedRecipeAvailable, even when re-running a cached recipe. + * The base implementation has a shortcut path that bypasses our EU budget checks. + */ + @Override + public void findAndHandleRecipe() { + lastFailedMatches = null; + + // If we have a cached origin recipe, try to re-apply our thread-specific overclock + if (!recipeDirty && lastOriginRecipe != null) { + // Re-apply OUR thread overclock to the origin recipe + GTRecipe modified = applyThreadOverclock(lastOriginRecipe); + if (modified != null) { + GTRecipe trimmed = RecipeHelper.trimRecipeOutputs(modified, machine.getOutputLimits()); + if (trimmed != null && checkRecipe(trimmed).isSuccess()) { + setupRecipe(trimmed); + recipeDirty = false; + return; + } + } + } + + // No valid cached recipe, search for a new one + lastRecipe = null; + lastOriginRecipe = null; + handleSearchingRecipes(searchRecipe()); + recipeDirty = false; + } + + /** + * Override checkRecipe to use this thread's capability holder. + * Also checks recipe conditions. + */ + @Override + public ActionResult checkRecipe(GTRecipe recipe) { + // First check recipe conditions + var conditionResult = RecipeHelper.checkConditions(recipe, this); + if (!conditionResult.isSuccess()) return conditionResult; + + // Use this thread as the capability holder for matching + return matchRecipe(recipe); + } + + /** + * Override to use this thread's capability holder for recipe matching. + * This ensures recipe searching uses this thread's handlers. + */ + @Override + protected ActionResult matchRecipe(GTRecipe recipe) { + // Use this thread as the capability holder instead of the machine + return RecipeHelper.matchContents(this, recipe); + } + + /** + * Override to use this thread's handlers instead of the machine's global handlers. + */ + @Override + protected ActionResult handleRecipeIO(GTRecipe recipe, IO io) { + // Use this thread as the capability holder instead of the machine + return RecipeHelper.handleRecipeIO(this, recipe, io, chanceCaches); + } + + /** + * Override to use this thread's handlers for tick-based IO. + */ + @Override + protected ActionResult handleTickRecipeIO(GTRecipe recipe, IO io) { + // Use this thread as the capability holder instead of the machine + return RecipeHelper.handleTickRecipeIO(this, recipe, io, chanceCaches); + } + + /** + * Override to use this thread's capability holder for tick recipe matching. + * This is critical for EU consumption - the base implementation uses the machine, + * but we need to use this thread's handlers. + */ + @Override + public ActionResult handleTickRecipe(GTRecipe recipe) { + if (recipe.hasTick()) { + // Use this thread as the capability holder for matching + var result = RecipeHelper.matchTickRecipe(this, recipe); + if (!result.isSuccess()) { + return result; + } + result = handleTickRecipeIO(recipe, IO.IN); + if (!result.isSuccess()) { + return result; + } + return handleTickRecipeIO(recipe, IO.OUT); + } + return ActionResult.SUCCESS; + } + + /** + * Override onRecipeFinish to prevent the base class from applying machine-level overclock. + * The base implementation calls machine.fullModifyRecipe(lastOriginRecipe) which would + * apply overclock based on full machine power, ignoring our thread budget. + */ + @Override + public void onRecipeFinish() { + machine.afterWorking(); + if (lastRecipe != null) { + // Reset run attempt tracking + // Note: runAttempt and runDelay are package-private in base class + // but we can still access them since we're in the same hierarchy + + consecutiveRecipes++; + handleRecipeIO(lastRecipe, IO.OUT); + + // CRITICAL: Do NOT call machine.fullModifyRecipe here! + // Instead, re-apply OUR thread-specific overclock to the origin recipe + if (lastOriginRecipe != null) { + GTRecipe modified = applyThreadOverclock(lastOriginRecipe); + if (modified == null) { + markLastRecipeDirty(); + } else { + // Trim outputs + GTRecipe trimmed = RecipeHelper.trimRecipeOutputs(modified, machine.getOutputLimits()); + if (trimmed == null) { + markLastRecipeDirty(); + } else { + lastRecipe = trimmed; + } + } + } else { + markLastRecipeDirty(); + } + + // Try to run the recipe again + var recipeCheck = checkRecipe(lastRecipe); + if (!recipeDirty && !isSuspendAfterFinish() && recipeCheck.isSuccess()) { + setupRecipe(lastRecipe); + } else { + if (isSuspendAfterFinish()) { + setStatus(Status.SUSPEND); + } else { + setStatus(Status.IDLE); + } + consecutiveRecipes = 0; + progress = 0; + duration = 0; + isActive = false; + } + } + } + + /** + * Get the recipe currently being processed by this thread. + */ + @Nullable + public GTRecipe getCurrentRecipe() { + return lastRecipe; + } + + /** + * Get progress as a percentage (0.0 to 1.0) + */ + public double getProgressPercent() { + if (duration == 0) return 0; + return (double) progress / duration; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/modular/MultiblockInit.java b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/modular/MultiblockInit.java index f634d8435..105dcc8b8 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/modular/MultiblockInit.java +++ b/src/main/java/com/ghostipedia/cosmiccore/common/machine/multiblock/multi/modular/MultiblockInit.java @@ -12,6 +12,7 @@ public static void init() { // MegaStructures PrismaticOreFoundry.init(); StellarIris.init(); + StellarSmeltingModule.init(); // StellarStarBallast.init(); HemophagicTransfuser.init(); PlasmiteDistillery.init(); diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/network/CCoreNetwork.java b/src/main/java/com/ghostipedia/cosmiccore/common/network/CCoreNetwork.java index 4942bfbca..86decb527 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/common/network/CCoreNetwork.java +++ b/src/main/java/com/ghostipedia/cosmiccore/common/network/CCoreNetwork.java @@ -2,7 +2,11 @@ import com.ghostipedia.cosmiccore.CosmicCore; import com.ghostipedia.cosmiccore.common.network.packet.AbyssTimeWarnPacket; +import com.ghostipedia.cosmiccore.common.network.packet.OxygenWarnPacket; +import com.ghostipedia.cosmiccore.common.network.packet.SyncOxygenBarPacket; import com.ghostipedia.cosmiccore.common.network.packet.SyncTimeBarPacket; +import com.ghostipedia.cosmiccore.common.reflection.network.SyncQuakeMovementPacket; +import com.ghostipedia.cosmiccore.common.reflection.ui.VoidUIPackets; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceKey; @@ -78,5 +82,11 @@ public static void init() { INITIALIZED = true; register(SyncTimeBarPacket.class, SyncTimeBarPacket::new, NetworkDirection.PLAY_TO_CLIENT); register(AbyssTimeWarnPacket.class, AbyssTimeWarnPacket::new, NetworkDirection.PLAY_TO_CLIENT); + register(SyncOxygenBarPacket.class, SyncOxygenBarPacket::new, NetworkDirection.PLAY_TO_CLIENT); + register(OxygenWarnPacket.class, OxygenWarnPacket::new, NetworkDirection.PLAY_TO_CLIENT); + register(SyncQuakeMovementPacket.class, SyncQuakeMovementPacket::new, NetworkDirection.PLAY_TO_CLIENT); + + // Void UI packets + VoidUIPackets.register(); } } diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/network/packet/OxygenWarnPacket.java b/src/main/java/com/ghostipedia/cosmiccore/common/network/packet/OxygenWarnPacket.java new file mode 100644 index 000000000..09b0eb498 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/network/packet/OxygenWarnPacket.java @@ -0,0 +1,38 @@ +package com.ghostipedia.cosmiccore.common.network.packet; + +import com.ghostipedia.cosmiccore.common.network.CCoreNetwork; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.network.NetworkEvent; + +public class OxygenWarnPacket implements CCoreNetwork.INetPacket { + + private final String message; + private final int seconds; + + public OxygenWarnPacket(String message, int seconds) { + this.message = message; + this.seconds = seconds; + } + + public OxygenWarnPacket(FriendlyByteBuf buf) { + this.message = buf.readUtf(); + this.seconds = buf.readVarInt(); + } + + @Override + public void encode(FriendlyByteBuf buffer) { + buffer.writeUtf(message); + buffer.writeVarInt(seconds); + } + + @Override + public void execute(NetworkEvent.Context context) { + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> Minecraft.getInstance().execute( + () -> Minecraft.getInstance().gui.setOverlayMessage(Component.translatable(message, seconds), false))); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/network/packet/SyncOxygenBarPacket.java b/src/main/java/com/ghostipedia/cosmiccore/common/network/packet/SyncOxygenBarPacket.java new file mode 100644 index 000000000..554863932 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/network/packet/SyncOxygenBarPacket.java @@ -0,0 +1,45 @@ +package com.ghostipedia.cosmiccore.common.network.packet; + +import com.ghostipedia.cosmiccore.client.CosmicHudGuiOverlay; +import com.ghostipedia.cosmiccore.common.network.CCoreNetwork; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.network.NetworkEvent; + +public class SyncOxygenBarPacket implements CCoreNetwork.INetPacket { + + private final long left; + private final long max; + private final boolean show; + private final double ratePerSecond; // signed ticks/sec; negative = draining + + public SyncOxygenBarPacket(long left, long max, boolean show, double ratePerSecond) { + this.left = left; + this.max = max; + this.show = show; + this.ratePerSecond = ratePerSecond; + } + + public SyncOxygenBarPacket(FriendlyByteBuf buf) { + this.left = buf.readVarLong(); + this.max = buf.readVarLong(); + this.show = buf.readBoolean(); + this.ratePerSecond = buf.readDouble(); + } + + @Override + public void encode(FriendlyByteBuf buf) { + buf.writeVarLong(left); + buf.writeVarLong(max); + buf.writeBoolean(show); + buf.writeDouble(ratePerSecond); + } + + @Override + public void execute(NetworkEvent.Context ctx) { + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, + () -> () -> CosmicHudGuiOverlay.setOxygenBar(left, max, show, ratePerSecond)); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/IReflection.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/IReflection.java new file mode 100644 index 000000000..39a7cebf7 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/IReflection.java @@ -0,0 +1,219 @@ +package com.ghostipedia.cosmiccore.common.reflection; + +import net.minecraft.resources.ResourceLocation; + +import java.util.Map; +import java.util.Set; + +/** + * Interface for the Reflection capability - tracks player soul erosion and bargains. + * The Reflection is YOU - a fragment of self that remembers what you were before immortality. + */ +public interface IReflection { + + // ---- Shards (Currency) ---- + + /** + * @return current shard balance (Shards of Perpetuity consumed via right-click) + */ + int getShardBalance(); + + /** + * Sets shard balance. + */ + void setShardBalance(int shards); + + /** + * Adds shards to balance. + */ + void addShards(int amount); + + /** + * Attempts to spend shards. Returns true if successful. + */ + boolean spendShards(int amount); + + // ---- Soul Capacity ---- + + /** + * @return base soul capacity (default 100, can be expanded) + */ + int getBaseCapacity(); + + /** + * Sets base capacity (for permanent expansions). + */ + void setBaseCapacity(int capacity); + + /** + * @return bonus capacity from gear/temporary effects + */ + int getBonusCapacity(); + + /** + * Sets bonus capacity (from curios, armor, etc.). + */ + void setBonusCapacity(int bonus); + + /** + * @return total capacity (base + bonus) + */ + default int getTotalCapacity() { + return getBaseCapacity() + getBonusCapacity(); + } + + /** + * @return current weight used by active bargains + */ + int getUsedCapacity(); + + /** + * @return remaining capacity available + */ + default int getRemainingCapacity() { + return getTotalCapacity() - getUsedCapacity(); + } + + /** + * Check if a bargain can fit within current capacity. + */ + default boolean canFitBargain(int weight) { + return getRemainingCapacity() >= weight; + } + + // ---- Erosion ---- + + /** + * @return total erosion accumulated (1 death = 1 erosion, bargains add more) + */ + int getErosion(); + + /** + * Sets total erosion value. + */ + void setErosion(int erosion); + + /** + * Adds erosion. Use for deaths, bargains, power usage. + */ + void addErosion(int amount); + + /** + * @return total number of deaths tracked + */ + int getDeathCount(); + + /** + * Increment death counter and add 1 erosion. + */ + void recordDeath(); + + // ---- Bargains ---- + + /** + * @return set of bargain IDs the player has accepted + */ + Set getActiveBargains(); + + /** + * @return true if player has this bargain active + */ + boolean hasBargain(ResourceLocation bargainId); + + /** + * Accept a bargain. Does NOT add erosion - caller should handle cost. + */ + void acceptBargain(ResourceLocation bargainId); + + /** + * Defy (remove) a bargain. The scar remains tracked separately. + */ + void defy(ResourceLocation bargainId); + + /** + * @return set of bargain IDs that have been defied (scars) + */ + Set getDefianceScars(); + + /** + * @return true if this bargain was defied (scarred) + */ + boolean hasDefianceScar(ResourceLocation bargainId); + + // ---- Threshold Tracking ---- + + /** + * @return the highest threshold index the player has seen (0-9) + */ + int getHighestThresholdSeen(); + + /** + * Mark a threshold as seen. + */ + void setHighestThresholdSeen(int threshold); + + // ---- First Encounter ---- + + /** + * @return true if the reflection has awakened (first deaths occurred) + */ + boolean hasAwakened(); + + /** + * Mark the reflection as awakened. + */ + void setAwakened(boolean awakened); + + /** + * @return true if the awakening sequence (first bargain offer) has been shown + */ + boolean hasCompletedAwakeningSequence(); + + /** + * Mark the awakening sequence as completed. + */ + void setAwakeningSequenceCompleted(boolean completed); + + // ---- Command Usage Tracking ---- + + /** + * Get recent usage count for a command (for cost escalation). + * + * @param commandId the command identifier (e.g., "home", "back") + * @return number of uses in the current escalation window + */ + int getCommandUsageCount(String commandId); + + /** + * Record a command use. + */ + void recordCommandUse(String commandId); + + /** + * Reset command usage (called when cooldown expires). + */ + void resetCommandUsage(String commandId); + + /** + * @return timestamp of last command use for cooldown tracking + */ + long getLastCommandUseTime(String commandId); + + // ---- Memory / Context ---- + + /** + * Store arbitrary data for whisper/dialogue context. + * Examples: death causes, dimensions visited, etc. + */ + Map getMemory(); + + /** + * Increment a memory counter. + */ + void rememberEvent(String eventKey); + + /** + * Get count for a specific memory. + */ + int getMemoryCount(String eventKey); +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionCapability.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionCapability.java new file mode 100644 index 000000000..a073ad797 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionCapability.java @@ -0,0 +1,89 @@ +package com.ghostipedia.cosmiccore.common.reflection; + +import com.ghostipedia.cosmiccore.CosmicCore; + +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.common.capabilities.*; +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.event.AttachCapabilitiesEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.Optional; + +import javax.annotation.Nullable; + +/** + * Capability provider for the Reflection system. + * Handles attachment to players and persistence across death/respawn. + */ +@Mod.EventBusSubscriber(modid = CosmicCore.MOD_ID) +public class ReflectionCapability { + + private ReflectionCapability() {} + + public static final Capability CAP = CapabilityManager.get(new CapabilityToken<>() {}); + + /** + * Get the reflection data for a player. + */ + public static Optional get(Player player) { + return player.getCapability(CAP).resolve(); + } + + /** + * Get the reflection data, or throw if not present. + */ + public static IReflection getOrThrow(Player player) { + return get(player).orElseThrow(() -> new IllegalStateException("Player missing Reflection capability")); + } + + public static class Provider implements ICapabilityProvider, ICapabilitySerializable { + + private final ReflectionData impl = new ReflectionData(); + private final LazyOptional opt = LazyOptional.of(() -> impl); + + @Override + public LazyOptional getCapability(Capability cap, @Nullable Direction side) { + return cap == CAP ? opt.cast() : LazyOptional.empty(); + } + + @Override + public CompoundTag serializeNBT() { + return impl.saveTag(); + } + + @Override + public void deserializeNBT(CompoundTag nbt) { + impl.loadTag(nbt); + } + } + + @SubscribeEvent + public static void registerCaps(RegisterCapabilitiesEvent event) { + event.register(IReflection.class); + } + + @SubscribeEvent + public static void attach(AttachCapabilitiesEvent event) { + if (event.getObject() instanceof Player) { + event.addCapability(CosmicCore.id("reflection"), new Provider()); + } + } + + @SubscribeEvent + public static void clone(PlayerEvent.Clone event) { + // Preserve reflection data across death/respawn and dimension changes + event.getOriginal().reviveCaps(); + event.getOriginal().getCapability(CAP).ifPresent(old -> event.getEntity().getCapability(CAP).ifPresent(now -> { + if (now instanceof ReflectionData newCap && old instanceof ReflectionData oldCap) { + newCap.loadTag(oldCap.saveTag()); + } + })); + event.getOriginal().invalidateCaps(); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionCommand.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionCommand.java new file mode 100644 index 000000000..c257a4781 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionCommand.java @@ -0,0 +1,311 @@ +package com.ghostipedia.cosmiccore.common.reflection; + +import com.ghostipedia.cosmiccore.common.network.CCoreNetwork; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.BargainRegistry; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.QuakeMovementBargain; +import com.ghostipedia.cosmiccore.common.reflection.network.SyncQuakeMovementPacket; +import com.ghostipedia.cosmiccore.common.reflection.ui.VoidUIPackets; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; + +/** + * Debug/test commands for the Reflection system. + * /reflection status - Show current erosion, deaths, bargains + * /reflection add_erosion - Add erosion + * /reflection accept - Accept a bargain + * /reflection defy - Defy a bargain + * /reflection awaken - Force awakening + * /reflection reset - Reset all reflection data + */ +public class ReflectionCommand { + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register( + Commands.literal("reflection") + .requires(source -> source.hasPermission(2)) // Op level 2 + .then(Commands.literal("status") + .executes(ctx -> showStatus(ctx.getSource()))) + .then(Commands.literal("add_erosion") + .then(Commands.argument("amount", IntegerArgumentType.integer(1, 1000)) + .executes(ctx -> addErosion(ctx.getSource(), + IntegerArgumentType.getInteger(ctx, "amount"))))) + .then(Commands.literal("accept") + .then(Commands.argument("bargain", StringArgumentType.string()) + .executes(ctx -> acceptBargain(ctx.getSource(), + StringArgumentType.getString(ctx, "bargain"))))) + .then(Commands.literal("defy") + .then(Commands.argument("bargain", StringArgumentType.string()) + .executes(ctx -> defyBargain(ctx.getSource(), + StringArgumentType.getString(ctx, "bargain"))))) + .then(Commands.literal("awaken") + .executes(ctx -> forceAwaken(ctx.getSource()))) + .then(Commands.literal("reset") + .executes(ctx -> resetData(ctx.getSource()))) + .then(Commands.literal("list_bargains") + .executes(ctx -> listBargains(ctx.getSource()))) + .then(Commands.literal("mirror") + .executes(ctx -> openMirror(ctx.getSource())) + .then(Commands.argument("bargain", StringArgumentType.string()) + .executes(ctx -> openMirrorWithBargain(ctx.getSource(), + StringArgumentType.getString(ctx, "bargain")))))); + } + + private static int showStatus(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.literal("Must be run by a player")); + return 0; + } + + ReflectionCapability.get(player).ifPresentOrElse(reflection -> { + source.sendSuccess(() -> Component.literal("=== Reflection Status ==="), false); + source.sendSuccess(() -> Component.literal("Erosion: " + reflection.getErosion()), false); + source.sendSuccess(() -> Component.literal("Deaths: " + reflection.getDeathCount()), false); + source.sendSuccess(() -> Component.literal("Awakened: " + reflection.hasAwakened()), false); + source.sendSuccess(() -> Component.literal("Threshold: " + reflection.getHighestThresholdSeen() + + " (current: " + ReflectionConstants.getThresholdIndex(reflection.getErosion()) + ")"), false); + source.sendSuccess( + () -> Component + .literal("Color Tier: " + ReflectionConstants.getSoulColorTier(reflection.getErosion())), + false); + + source.sendSuccess(() -> Component.literal("Active Bargains:"), false); + if (reflection.getActiveBargains().isEmpty()) { + source.sendSuccess(() -> Component.literal(" (none)"), false); + } else { + for (ResourceLocation id : reflection.getActiveBargains()) { + source.sendSuccess(() -> Component.literal(" - " + id), false); + } + } + + if (!reflection.getDefianceScars().isEmpty()) { + source.sendSuccess(() -> Component.literal("Defiance Scars:"), false); + for (ResourceLocation id : reflection.getDefianceScars()) { + source.sendSuccess(() -> Component.literal(" - " + id), false); + } + } + + // Command usage info + source.sendSuccess(() -> Component.literal("Command Costs:"), false); + int homeCost = ReflectionConstants.getCommandCost(reflection, "home"); + int backCost = ReflectionConstants.getCommandCost(reflection, "back"); + int homeUsage = reflection.getCommandUsageCount("home"); + int backUsage = reflection.getCommandUsageCount("back"); + source.sendSuccess(() -> Component.literal(" /home: " + homeCost + " erosion (used " + homeUsage + "x)"), + false); + source.sendSuccess(() -> Component.literal(" /back: " + backCost + " erosion (used " + backUsage + "x)"), + false); + }, () -> source.sendFailure(Component.literal("No reflection data found"))); + + return 1; + } + + private static int addErosion(CommandSourceStack source, int amount) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.literal("Must be run by a player")); + return 0; + } + + ReflectionCapability.get(player).ifPresentOrElse(reflection -> { + int oldErosion = reflection.getErosion(); + reflection.addErosion(amount); + int newErosion = reflection.getErosion(); + + source.sendSuccess(() -> Component.literal("Added " + amount + " erosion. Total: " + newErosion), false); + + if (ReflectionConstants.crossedNewThreshold(oldErosion, newErosion)) { + int threshold = ReflectionConstants.getThresholdIndex(newErosion); + source.sendSuccess(() -> Component.literal("Crossed threshold " + threshold + "!"), false); + } + }, () -> source.sendFailure(Component.literal("No reflection data found"))); + + return 1; + } + + private static int acceptBargain(CommandSourceStack source, String bargainId) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.literal("Must be run by a player")); + return 0; + } + + ResourceLocation id = new ResourceLocation("cosmiccore", bargainId); + + return BargainRegistry.get(id).map(bargain -> { + ReflectionCapability.get(player).ifPresentOrElse(reflection -> { + if (reflection.hasBargain(id)) { + source.sendFailure(Component.literal("Already have this bargain")); + return; + } + + // Calculate and apply cost + int cost = BargainRegistry.calculateCost(player, bargain); + reflection.addErosion(cost); + reflection.acceptBargain(id); + + // Call the bargain's accept handler + bargain.onAccept(player, bargain.getAnswers().get(0)); // Use first answer for testing + + // Sync to client if this is the quake movement bargain + if (id.equals(QuakeMovementBargain.INSTANCE.getId())) { + CCoreNetwork.sendToPlayer(player, new SyncQuakeMovementPacket(true)); + } + + source.sendSuccess(() -> Component.literal("Accepted bargain: " + bargain.getName().getString() + + " (cost: " + cost + " erosion)"), false); + }, () -> source.sendFailure(Component.literal("No reflection data found"))); + return 1; + }).orElseGet(() -> { + source.sendFailure(Component.literal("Unknown bargain: " + id)); + return 0; + }); + } + + private static int defyBargain(CommandSourceStack source, String bargainId) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.literal("Must be run by a player")); + return 0; + } + + ResourceLocation id = new ResourceLocation("cosmiccore", bargainId); + + return BargainRegistry.get(id).map(bargain -> { + ReflectionCapability.get(player).ifPresentOrElse(reflection -> { + if (!reflection.hasBargain(id)) { + source.sendFailure(Component.literal("Don't have this bargain")); + return; + } + + // Calculate and apply defiance cost + int cost = BargainRegistry.calculateDefianceCost(player, bargain); + reflection.addErosion(cost); + reflection.defy(id); + + // Call the bargain's defy handler + bargain.onDefy(player); + + // Sync to client if this is the quake movement bargain + if (id.equals(QuakeMovementBargain.INSTANCE.getId())) { + CCoreNetwork.sendToPlayer(player, new SyncQuakeMovementPacket(false)); + } + + source.sendSuccess(() -> Component.literal("Defied bargain: " + bargain.getName().getString() + + " (cost: " + cost + " erosion, debuff remains)"), false); + }, () -> source.sendFailure(Component.literal("No reflection data found"))); + return 1; + }).orElseGet(() -> { + source.sendFailure(Component.literal("Unknown bargain: " + id)); + return 0; + }); + } + + private static int forceAwaken(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.literal("Must be run by a player")); + return 0; + } + + ReflectionCapability.get(player).ifPresentOrElse(reflection -> { + reflection.setAwakened(true); + source.sendSuccess(() -> Component.literal("Reflection awakened."), false); + }, () -> source.sendFailure(Component.literal("No reflection data found"))); + + return 1; + } + + private static int resetData(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.literal("Must be run by a player")); + return 0; + } + + ReflectionCapability.get(player).ifPresentOrElse(reflection -> { + // Reset by loading empty data + if (reflection instanceof ReflectionData data) { + data.loadTag(new net.minecraft.nbt.CompoundTag()); + } + + // Sync all bargain states to client (all should now be false) + CCoreNetwork.sendToPlayer(player, new SyncQuakeMovementPacket(false)); + // Add more bargain syncs here as they're implemented + + source.sendSuccess(() -> Component.literal("Reflection data reset."), false); + }, () -> source.sendFailure(Component.literal("No reflection data found"))); + + return 1; + } + + private static int listBargains(CommandSourceStack source) { + source.sendSuccess(() -> Component.literal("=== Available Bargains ==="), false); + + var allBargains = BargainRegistry.getAll(); + source.sendSuccess(() -> Component.literal("Total registered: " + allBargains.size()), false); + + for (Bargain bargain : allBargains) { + // Capture in final variable for lambda + final Bargain b = bargain; + source.sendSuccess(() -> Component.literal("- " + b.getId().getPath() + + " (" + b.getName().getString() + ") [" + b.getTier().name() + "]"), false); + } + + return 1; + } + + private static int openMirror(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.literal("Must be run by a player")); + return 0; + } + + // Force awaken if not already + ReflectionCapability.get(player).ifPresent(reflection -> { + if (!reflection.hasAwakened()) { + reflection.setAwakened(true); + } + }); + + VoidUIPackets.sendOpenVoidScreen(player); + return 1; + } + + private static int openMirrorWithBargain(CommandSourceStack source, String bargainId) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.literal("Must be run by a player")); + return 0; + } + + ResourceLocation id = new ResourceLocation("cosmiccore", bargainId); + + return BargainRegistry.get(id).map(bargain -> { + // Force awaken if not already + ReflectionCapability.get(player).ifPresent(reflection -> { + if (!reflection.hasAwakened()) { + reflection.setAwakened(true); + } + }); + + VoidUIPackets.sendOpenVoidScreen(player, id); + source.sendSuccess(() -> Component.literal("Opened mirror with bargain: " + bargain.getName().getString()), + false); + return 1; + }).orElseGet(() -> { + source.sendFailure(Component.literal("Unknown bargain: " + id)); + return 0; + }); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionCommands.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionCommands.java new file mode 100644 index 000000000..9360362a9 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionCommands.java @@ -0,0 +1,63 @@ +package com.ghostipedia.cosmiccore.common.reflection; + +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.BackBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.HomeBargain; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +import com.mojang.brigadier.CommandDispatcher; + +/** + * Registers the /home and /back commands that are unlocked via Reflection bargains. + */ +public class ReflectionCommands { + + public static void register(CommandDispatcher dispatcher) { + // /home command - teleport to spawn/bed + dispatcher.register( + Commands.literal("home") + .executes(ctx -> { + ServerPlayer player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + // Check if they have the bargain + boolean hasBargain = ReflectionCapability.get(player) + .map(r -> r.hasBargain(HomeBargain.INSTANCE.getId())) + .orElse(false); + + if (!hasBargain) { + player.displayClientMessage( + Component.literal("§7§o*You haven't bargained for this power.*"), + false); + return 0; + } + + return HomeBargain.executeHome(player) ? 1 : 0; + })); + + // /back command - return to last death location + dispatcher.register( + Commands.literal("back") + .executes(ctx -> { + ServerPlayer player = ctx.getSource().getPlayer(); + if (player == null) return 0; + + // Check if they have the bargain + boolean hasBargain = ReflectionCapability.get(player) + .map(r -> r.hasBargain(BackBargain.INSTANCE.getId())) + .orElse(false); + + if (!hasBargain) { + player.displayClientMessage( + Component.literal("§7§o*You haven't bargained for this power.*"), + false); + return 0; + } + + return BackBargain.executeBack(player) ? 1 : 0; + })); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionConstants.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionConstants.java new file mode 100644 index 000000000..72769cb4f --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionConstants.java @@ -0,0 +1,146 @@ +package com.ghostipedia.cosmiccore.common.reflection; + +/** + * Constants for the Reflection system - erosion thresholds, costs, etc. + */ +public final class ReflectionConstants { + + private ReflectionConstants() {} + + // ---- Erosion Thresholds ---- + // These trigger mandatory reflection encounters + + public static final int[] THRESHOLDS = { + 25, // Threshold 0: Curious - "You're starting to show wear." + 50, // Threshold 1: Observational - "We've died enough to notice now." + 100, // Threshold 2: Familiar - "This is becoming routine, isn't it?" + 200, // Threshold 3: Ambiguous - "You're changing faster than I expected." + 350, // Threshold 4: Philosophical - "Do you remember what we looked like before?" + 500, // Threshold 5: Heavy - "Halfway to... something." + 666, // Threshold 6: Ominous - Silence. Just stares. + 800, // Threshold 7: Unsettling - "I'm having trouble telling us apart." + 900, // Threshold 8: Quiet - "Almost there. Thank you for looking." + 1000 // Threshold 9: Unknown - "We're just... this now." + }; + + /** + * Get the threshold index for a given erosion value. + * Returns -1 if below first threshold. + */ + public static int getThresholdIndex(int erosion) { + for (int i = THRESHOLDS.length - 1; i >= 0; i--) { + if (erosion >= THRESHOLDS[i]) { + return i; + } + } + return -1; + } + + /** + * Check if a new threshold was crossed. + */ + public static boolean crossedNewThreshold(int oldErosion, int newErosion) { + return getThresholdIndex(newErosion) > getThresholdIndex(oldErosion); + } + + // ---- Bargain Cost Scaling ---- + // Bargains cost more at higher corruption + + public static final int[][] BARGAIN_COST_RANGES = { + { 0, 100, 25, 50 }, // 0-100 erosion: costs 25-50 + { 101, 300, 75, 150 }, // 101-300 erosion: costs 75-150 + { 301, 500, 150, 250 }, // 301-500 erosion: costs 150-250 + { 501, 750, 250, 400 }, // 501-750 erosion: costs 250-400 + { 751, Integer.MAX_VALUE, 400, 600 } // 751+: costs 400-600 + }; + + /** + * Get the base cost range for a bargain at the given erosion level. + * Returns [minCost, maxCost] + */ + public static int[] getBargainCostRange(int currentErosion) { + for (int[] range : BARGAIN_COST_RANGES) { + if (currentErosion >= range[0] && currentErosion <= range[1]) { + return new int[] { range[2], range[3] }; + } + } + // Fallback to highest tier + return new int[] { 400, 600 }; + } + + // ---- Command Cost Escalation ---- + // /home and /back costs double with repeated use + + public static final int HOME_BASE_COST = 1; + public static final int BACK_BASE_COST = 2; + public static final int HOME_COST_CEILING = 16; + public static final int BACK_COST_CEILING = 32; + public static final int HOME_UNLOCK_COST = 12; + public static final int BACK_UNLOCK_COST = 12; + + /** Cooldown before command usage count resets (15 minutes in milliseconds) */ + public static final long COMMAND_USAGE_RESET_TIME = 15 * 60 * 1000L; + + /** + * Calculate command cost based on usage count. + * Cost doubles each use until ceiling. + */ + public static int calculateCommandCost(int baseCost, int ceiling, int usageCount) { + if (usageCount <= 0) return baseCost; + int cost = baseCost * (1 << usageCount); // baseCost * 2^usageCount + return Math.min(cost, ceiling); + } + + /** + * Get the current cost for a command bargain, accounting for usage escalation. + */ + public static int getCommandCost(IReflection reflection, String command) { + int usageCount = reflection.getCommandUsageCount(command); + + return switch (command) { + case "home" -> calculateCommandCost(HOME_BASE_COST, HOME_COST_CEILING, usageCount); + case "back" -> calculateCommandCost(BACK_BASE_COST, BACK_COST_CEILING, usageCount); + default -> 1; + }; + } + + // ---- Soul Color Ranges ---- + // For visualization + + public static final int[][] SOUL_COLOR_RANGES = { + { 0, 50 }, // Pale white/silver + { 51, 150 }, // Faint blue tint + { 151, 300 }, // Deep blue/purple + { 301, 500 }, // Violet/crimson threads + { 501, 750 }, // Dark red/black veins + { 751, 1000 }, // Mostly black, faint glow + { 1001, Integer.MAX_VALUE } // Void-like, inverted + }; + + /** + * Get the soul color tier (0-6) for visualization. + */ + public static int getSoulColorTier(int erosion) { + for (int i = 0; i < SOUL_COLOR_RANGES.length; i++) { + if (erosion >= SOUL_COLOR_RANGES[i][0] && erosion <= SOUL_COLOR_RANGES[i][1]) { + return i; + } + } + return SOUL_COLOR_RANGES.length - 1; + } + + // ---- Defiance Costs ---- + + /** Multiplier for defiance erosion spike (applied to original bargain cost) */ + public static final float DEFIANCE_COST_MULTIPLIER = 2.5f; + + // ---- Awakening ---- + + /** Number of deaths before the reflection awakens */ + public static final int DEATHS_TO_AWAKEN = 3; + + // ---- Blink Power ---- + + /** Erosion cost per blink use */ + public static final float BLINK_EROSION_PER_USE = 0.5f; +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionData.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionData.java new file mode 100644 index 000000000..015619a8a --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionData.java @@ -0,0 +1,335 @@ +package com.ghostipedia.cosmiccore.common.reflection; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Implementation of IReflection - stores all soul/erosion/bargain data for a player. + */ +public class ReflectionData implements IReflection { + + // Currency and capacity + private int shardBalance = 0; + private int baseCapacity = 100; + private int bonusCapacity = 0; + + // Erosion and death tracking + private int erosion = 0; + private int deathCount = 0; + private int highestThresholdSeen = -1; + private boolean awakened = false; + private boolean awakeningSequenceCompleted = false; + + private final Set activeBargains = new HashSet<>(); + private final Set defianceScars = new HashSet<>(); + private final Map commandUsageCount = new HashMap<>(); + private final Map commandLastUseTime = new HashMap<>(); + private final Map memory = new HashMap<>(); + + // ---- Shards (Currency) ---- + + @Override + public int getShardBalance() { + return shardBalance; + } + + @Override + public void setShardBalance(int shards) { + this.shardBalance = Math.max(0, shards); + } + + @Override + public void addShards(int amount) { + this.shardBalance = Math.max(0, this.shardBalance + amount); + } + + @Override + public boolean spendShards(int amount) { + if (shardBalance >= amount) { + shardBalance -= amount; + return true; + } + return false; + } + + // ---- Soul Capacity ---- + + @Override + public int getBaseCapacity() { + return baseCapacity; + } + + @Override + public void setBaseCapacity(int capacity) { + this.baseCapacity = Math.max(0, capacity); + } + + @Override + public int getBonusCapacity() { + return bonusCapacity; + } + + @Override + public void setBonusCapacity(int bonus) { + this.bonusCapacity = Math.max(0, bonus); + } + + @Override + public int getUsedCapacity() { + // Calculate weight from all active bargains + int totalWeight = 0; + for (ResourceLocation bargainId : activeBargains) { + var bargainOpt = com.ghostipedia.cosmiccore.common.reflection.bargain.BargainRegistry.get(bargainId); + if (bargainOpt.isPresent()) { + totalWeight += bargainOpt.get().getWeight(); + } + } + return totalWeight; + } + + // ---- Erosion ---- + + @Override + public int getErosion() { + return erosion; + } + + @Override + public void setErosion(int erosion) { + this.erosion = Math.max(0, erosion); + } + + @Override + public void addErosion(int amount) { + this.erosion = Math.max(0, this.erosion + amount); + } + + @Override + public int getDeathCount() { + return deathCount; + } + + @Override + public void recordDeath() { + deathCount++; + addErosion(1); + } + + // ---- Bargains ---- + + @Override + public Set getActiveBargains() { + return new HashSet<>(activeBargains); + } + + @Override + public boolean hasBargain(ResourceLocation bargainId) { + return activeBargains.contains(bargainId); + } + + @Override + public void acceptBargain(ResourceLocation bargainId) { + activeBargains.add(bargainId); + } + + @Override + public void defy(ResourceLocation bargainId) { + if (activeBargains.remove(bargainId)) { + defianceScars.add(bargainId); + } + } + + @Override + public Set getDefianceScars() { + return new HashSet<>(defianceScars); + } + + @Override + public boolean hasDefianceScar(ResourceLocation bargainId) { + return defianceScars.contains(bargainId); + } + + // ---- Threshold Tracking ---- + + @Override + public int getHighestThresholdSeen() { + return highestThresholdSeen; + } + + @Override + public void setHighestThresholdSeen(int threshold) { + this.highestThresholdSeen = Math.max(this.highestThresholdSeen, threshold); + } + + // ---- First Encounter ---- + + @Override + public boolean hasAwakened() { + return awakened; + } + + @Override + public void setAwakened(boolean awakened) { + this.awakened = awakened; + } + + @Override + public boolean hasCompletedAwakeningSequence() { + return awakeningSequenceCompleted; + } + + @Override + public void setAwakeningSequenceCompleted(boolean completed) { + this.awakeningSequenceCompleted = completed; + } + + // ---- Command Usage Tracking ---- + + @Override + public int getCommandUsageCount(String commandId) { + return commandUsageCount.getOrDefault(commandId, 0); + } + + @Override + public void recordCommandUse(String commandId) { + commandUsageCount.merge(commandId, 1, Integer::sum); + commandLastUseTime.put(commandId, System.currentTimeMillis()); + } + + @Override + public void resetCommandUsage(String commandId) { + commandUsageCount.remove(commandId); + } + + @Override + public long getLastCommandUseTime(String commandId) { + return commandLastUseTime.getOrDefault(commandId, 0L); + } + + // ---- Memory / Context ---- + + @Override + public Map getMemory() { + return new HashMap<>(memory); + } + + @Override + public void rememberEvent(String eventKey) { + memory.merge(eventKey, 1, Integer::sum); + } + + @Override + public int getMemoryCount(String eventKey) { + return memory.getOrDefault(eventKey, 0); + } + + // ---- NBT Persistence ---- + + public CompoundTag saveTag() { + CompoundTag root = new CompoundTag(); + + // Currency and capacity + root.putInt("shardBalance", shardBalance); + root.putInt("baseCapacity", baseCapacity); + root.putInt("bonusCapacity", bonusCapacity); + + // Core stats + root.putInt("erosion", erosion); + root.putInt("deathCount", deathCount); + root.putInt("highestThresholdSeen", highestThresholdSeen); + root.putBoolean("awakened", awakened); + root.putBoolean("awakeningSequenceCompleted", awakeningSequenceCompleted); + + // Active bargains + ListTag bargainList = new ListTag(); + for (ResourceLocation bargain : activeBargains) { + bargainList.add(StringTag.valueOf(bargain.toString())); + } + root.put("activeBargains", bargainList); + + // Defiance scars + ListTag scarList = new ListTag(); + for (ResourceLocation scar : defianceScars) { + scarList.add(StringTag.valueOf(scar.toString())); + } + root.put("defianceScars", scarList); + + // Command usage + CompoundTag cmdUsage = new CompoundTag(); + for (Map.Entry entry : commandUsageCount.entrySet()) { + cmdUsage.putInt(entry.getKey(), entry.getValue()); + } + root.put("commandUsageCount", cmdUsage); + + CompoundTag cmdTime = new CompoundTag(); + for (Map.Entry entry : commandLastUseTime.entrySet()) { + cmdTime.putLong(entry.getKey(), entry.getValue()); + } + root.put("commandLastUseTime", cmdTime); + + // Memory + CompoundTag memoryTag = new CompoundTag(); + for (Map.Entry entry : memory.entrySet()) { + memoryTag.putInt(entry.getKey(), entry.getValue()); + } + root.put("memory", memoryTag); + + return root; + } + + public void loadTag(CompoundTag root) { + // Currency and capacity (with defaults for backwards compatibility) + shardBalance = root.getInt("shardBalance"); + baseCapacity = root.contains("baseCapacity") ? root.getInt("baseCapacity") : 100; + bonusCapacity = root.getInt("bonusCapacity"); + + // Core stats + erosion = root.getInt("erosion"); + deathCount = root.getInt("deathCount"); + highestThresholdSeen = root.getInt("highestThresholdSeen"); + awakened = root.getBoolean("awakened"); + awakeningSequenceCompleted = root.getBoolean("awakeningSequenceCompleted"); + + // Active bargains + activeBargains.clear(); + ListTag bargainList = root.getList("activeBargains", Tag.TAG_STRING); + for (Tag tag : bargainList) { + activeBargains.add(new ResourceLocation(tag.getAsString())); + } + + // Defiance scars + defianceScars.clear(); + ListTag scarList = root.getList("defianceScars", Tag.TAG_STRING); + for (Tag tag : scarList) { + defianceScars.add(new ResourceLocation(tag.getAsString())); + } + + // Command usage + commandUsageCount.clear(); + CompoundTag cmdUsage = root.getCompound("commandUsageCount"); + for (String key : cmdUsage.getAllKeys()) { + commandUsageCount.put(key, cmdUsage.getInt(key)); + } + + commandLastUseTime.clear(); + CompoundTag cmdTime = root.getCompound("commandLastUseTime"); + for (String key : cmdTime.getAllKeys()) { + commandLastUseTime.put(key, cmdTime.getLong(key)); + } + + // Memory + memory.clear(); + CompoundTag memoryTag = root.getCompound("memory"); + for (String key : memoryTag.getAllKeys()) { + memory.put(key, memoryTag.getInt(key)); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionEventHandler.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionEventHandler.java new file mode 100644 index 000000000..7b21df7f9 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionEventHandler.java @@ -0,0 +1,282 @@ +package com.ghostipedia.cosmiccore.common.reflection; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.network.CCoreNetwork; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.BargainRegistry; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.BackBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.QuakeMovementBargain; +import com.ghostipedia.cosmiccore.common.reflection.network.SyncQuakeMovementPacket; +import com.ghostipedia.cosmiccore.common.reflection.ui.VoidUIPackets; +import com.ghostipedia.cosmiccore.common.reflection.whisper.WhisperSystem; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.entity.living.LivingDeathEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Handles Reflection system events - deaths, ticks, thresholds, awakening. + */ +@Mod.EventBusSubscriber(modid = CosmicCore.MOD_ID) +public class ReflectionEventHandler { + + // Track players who just died so we can whisper on respawn + private static final Map pendingDeathWhispers = new HashMap<>(); + + // Track pending respawn events (what UI to open after respawn) + private static final Map pendingRespawnEvents = new HashMap<>(); + + /** + * Types of respawn events that trigger UI + */ + private enum RespawnEventType { + AWAKENING, // First 3rd death - offer quake movement + THRESHOLD, // Crossed erosion milestone + CONTEXTUAL_BARGAIN // Offer relevant bargain (e.g., /back after death) + } + + private record RespawnEvent(RespawnEventType type, int data) { + + static RespawnEvent awakening() { + return new RespawnEvent(RespawnEventType.AWAKENING, 0); + } + + static RespawnEvent threshold(int index) { + return new RespawnEvent(RespawnEventType.THRESHOLD, index); + } + + static RespawnEvent contextualBargain(int bargainType) { + // 0 = back bargain, 1 = home bargain (can expand later) + return new RespawnEvent(RespawnEventType.CONTEXTUAL_BARGAIN, bargainType); + } + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public static void onPlayerDeath(LivingDeathEvent event) { + if (!(event.getEntity() instanceof ServerPlayer player)) return; + + // Record death location for /back bargain + BackBargain.recordDeath(player); + + ReflectionCapability.get(player).ifPresent(reflection -> { + int oldErosion = reflection.getErosion(); + int oldDeathCount = reflection.getDeathCount(); + + // Record death - adds 1 erosion + reflection.recordDeath(); + + // Remember the death cause for whispers/context + String deathCause = categorizeDeathCause(event.getSource().getMsgId()); + reflection.rememberEvent("death." + deathCause); + reflection.rememberEvent("death.total"); + + // Determine what event to queue for respawn + // Priority: Awakening > Threshold > Contextual Bargain + + // Check for awakening (first time reaching 3 deaths) + boolean justAwakened = false; + if (!reflection.hasAwakened() && reflection.getDeathCount() >= ReflectionConstants.DEATHS_TO_AWAKEN) { + reflection.setAwakened(true); + justAwakened = true; + CosmicCore.LOGGER.info("Reflection awakened for player {}", player.getName().getString()); + } + + // Queue respawn event + if (justAwakened && !reflection.hasCompletedAwakeningSequence()) { + // Priority 1: Awakening sequence (first time player reaches 3 deaths) + pendingRespawnEvents.put(player.getUUID(), RespawnEvent.awakening()); + CosmicCore.LOGGER.info("Queued awakening sequence for {}", player.getName().getString()); + } else { + // Check for threshold crossing + int newErosion = reflection.getErosion(); + if (ReflectionConstants.crossedNewThreshold(oldErosion, newErosion)) { + int newThreshold = ReflectionConstants.getThresholdIndex(newErosion); + if (newThreshold > reflection.getHighestThresholdSeen()) { + // Priority 2: Threshold encounter + pendingRespawnEvents.put(player.getUUID(), RespawnEvent.threshold(newThreshold)); + CosmicCore.LOGGER.info("Queued threshold {} encounter for {}", + newThreshold, player.getName().getString()); + } + } else if (reflection.hasAwakened() && !reflection.hasBargain(BackBargain.INSTANCE.getId())) { + // Priority 3: Contextual bargain offer (offer /back if they don't have it) + pendingRespawnEvents.put(player.getUUID(), RespawnEvent.contextualBargain(0)); + CosmicCore.LOGGER.info("Queued contextual /back bargain offer for {}", + player.getName().getString()); + } + } + + // Queue whisper for after respawn (if awakened and no UI event pending) + if (reflection.hasAwakened() && !pendingRespawnEvents.containsKey(player.getUUID())) { + pendingDeathWhispers.put(player.getUUID(), deathCause); + } + + CosmicCore.LOGGER.debug("Player {} died. Deaths: {}, Erosion: {}", + player.getName().getString(), reflection.getDeathCount(), reflection.getErosion()); + }); + } + + @SubscribeEvent + public static void onPlayerRespawn(PlayerEvent.PlayerRespawnEvent event) { + if (!(event.getEntity() instanceof ServerPlayer player)) return; + + ReflectionCapability.get(player).ifPresent(reflection -> { + // Sync bargain states to client after respawn + syncBargainStates(player, reflection); + + // Check for pending respawn event + RespawnEvent pendingEvent = pendingRespawnEvents.remove(player.getUUID()); + if (pendingEvent != null) { + // Delay the UI slightly to ensure player is fully loaded + player.getServer().execute(() -> { + processRespawnEvent(player, reflection, pendingEvent); + }); + // Clear any whisper since we're showing UI + pendingDeathWhispers.remove(player.getUUID()); + } else { + // Send death whisper after respawn (if no UI event) + String deathCause = pendingDeathWhispers.remove(player.getUUID()); + if (deathCause != null && reflection.hasAwakened()) { + // Schedule whisper for next tick + player.getServer().execute(() -> { + WhisperSystem.triggerEvent(player, WhisperSystem.WhisperEvent.DEATH); + }); + } + } + }); + } + + /** + * Process a pending respawn event - opens the appropriate UI. + */ + private static void processRespawnEvent(ServerPlayer player, IReflection reflection, RespawnEvent event) { + switch (event.type()) { + case AWAKENING -> { + // Awakening sequence: offer Quake Movement bargain + CosmicCore.LOGGER.info("Triggering awakening sequence for {}", player.getName().getString()); + VoidUIPackets.sendOpenVoidScreen(player, QuakeMovementBargain.INSTANCE.getId()); + reflection.setAwakeningSequenceCompleted(true); + } + case THRESHOLD -> { + // Threshold encounter: show milestone dialogue + int thresholdIndex = event.data(); + CosmicCore.LOGGER.info("Triggering threshold {} encounter for {}", + thresholdIndex, player.getName().getString()); + VoidUIPackets.sendThresholdEncounter(player, thresholdIndex); + reflection.setHighestThresholdSeen(thresholdIndex); + } + case CONTEXTUAL_BARGAIN -> { + // Contextual bargain offer + int bargainType = event.data(); + if (bargainType == 0 && !reflection.hasBargain(BackBargain.INSTANCE.getId())) { + // Offer /back bargain + CosmicCore.LOGGER.info("Triggering contextual /back bargain offer for {}", + player.getName().getString()); + VoidUIPackets.sendOpenVoidScreen(player, BackBargain.INSTANCE.getId()); + } + // Can add more contextual bargains here (e.g., bargainType == 1 for /home) + } + } + } + + @SubscribeEvent + public static void onPlayerTick(TickEvent.PlayerTickEvent event) { + if (event.phase != TickEvent.Phase.END) return; + if (!(event.player instanceof ServerPlayer player)) return; + + // Tick active bargains + for (Bargain bargain : BargainRegistry.getActive(player)) { + bargain.tick(player); + } + + // Command usage cooldown check + ReflectionCapability.get(player).ifPresent(reflection -> { + long now = System.currentTimeMillis(); + + // Reset command usage if cooldown expired + for (String cmd : new String[] { "home", "back" }) { + long lastUse = reflection.getLastCommandUseTime(cmd); + if (lastUse > 0 && (now - lastUse) > ReflectionConstants.COMMAND_USAGE_RESET_TIME) { + reflection.resetCommandUsage(cmd); + } + } + }); + + // Whisper system tick (runs periodically, not every tick) + if (player.tickCount % 100 == 0) { // Every 5 seconds + WhisperSystem.tick(player); + } + } + + @SubscribeEvent + public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { + if (!(event.getEntity() instanceof ServerPlayer player)) return; + + ReflectionCapability.get(player).ifPresent(reflection -> { + CosmicCore.LOGGER.debug("Player {} logged in. Erosion: {}, Deaths: {}, Awakened: {}", + player.getName().getString(), + reflection.getErosion(), + reflection.getDeathCount(), + reflection.hasAwakened()); + + // Sync bargain states to client + syncBargainStates(player, reflection); + }); + } + + /** + * Sync all client-side bargain states to a player. + * Called on login and respawn to ensure client knows about active bargains. + */ + private static void syncBargainStates(ServerPlayer player, IReflection reflection) { + // Sync Quake Movement bargain + boolean hasQuake = reflection.hasBargain(QuakeMovementBargain.INSTANCE.getId()); + CCoreNetwork.sendToPlayer(player, new SyncQuakeMovementPacket(hasQuake)); + + // Add more bargain syncs here as they're implemented + } + + /** + * Categorize death cause for memory tracking. + */ + private static String categorizeDeathCause(String msgId) { + if (msgId == null) return "unknown"; + + // Fall damage + if (msgId.contains("fall") || msgId.contains("stalagmite")) return "fall"; + + // Fire/lava + if (msgId.contains("fire") || msgId.contains("lava") || msgId.contains("burn") || msgId.contains("inFire") || + msgId.contains("onFire")) + return "fire"; + + // Drowning/suffocation + if (msgId.contains("drown") || msgId.contains("suffocate") || msgId.contains("inWall")) return "suffocation"; + + // Void + if (msgId.contains("void") || msgId.contains("outOfWorld")) return "void"; + + // Freezing + if (msgId.contains("freeze") || msgId.contains("cold")) return "freeze"; + + // Starving + if (msgId.contains("starve")) return "starve"; + + // Combat + if (msgId.contains("mob") || msgId.contains("player") || msgId.contains("arrow") || msgId.contains("thrown") || + msgId.contains("explosion")) + return "combat"; + + // Magic + if (msgId.contains("magic") || msgId.contains("wither") || msgId.contains("indirectMagic")) return "magic"; + + return "other"; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionLang.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionLang.java new file mode 100644 index 000000000..45b243a23 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ReflectionLang.java @@ -0,0 +1,304 @@ +package com.ghostipedia.cosmiccore.common.reflection; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +/** + * Translation key helper for the Reflection system. + * All translatable strings should go through this class. + * + * Key structure: + * - reflection.cosmiccore.bargain.[id].name - Bargain display name + * - reflection.cosmiccore.bargain.[id].description - Bargain description + * - reflection.cosmiccore.bargain.[id].dialogue.[n] - Offer dialogue lines + * - reflection.cosmiccore.bargain.[id].question - The philosophical question + * - reflection.cosmiccore.bargain.[id].answer.[answerId].text - Answer button text + * - reflection.cosmiccore.bargain.[id].answer.[answerId].response - Reflection's response + * - reflection.cosmiccore.bargain.[id].answer.[answerId].power.[n] - Power description lines + * - reflection.cosmiccore.bargain.[id].answer.[answerId].drawback.[n] - Drawback description lines + * - reflection.cosmiccore.bargain.[id].accept.[n] - Post-accept dialogue + * - reflection.cosmiccore.bargain.[id].refuse.[n] - Refuse dialogue + * - reflection.cosmiccore.bargain.[id].defy - Message when defying + * - reflection.cosmiccore.bargain.[id].visual - Soul visual description + * + * - reflection.cosmiccore.threshold.[n].dialogue.[m] - Threshold dialogue lines + * - reflection.cosmiccore.threshold.[n].question - Threshold question + * - reflection.cosmiccore.threshold.[n].response - Acknowledge response + * + * - reflection.cosmiccore.ui.[key] - UI strings + */ +public class ReflectionLang { + + private static final String PREFIX = "reflection.cosmiccore."; + + // ========================================================================= + // Bargain Keys + // ========================================================================= + + public static MutableComponent bargainName(String bargainId) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".name"); + } + + public static MutableComponent bargainDescription(String bargainId) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".description"); + } + + public static MutableComponent bargainDialogue(String bargainId, int index) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".dialogue." + index); + } + + public static MutableComponent bargainQuestion(String bargainId) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".question"); + } + + public static MutableComponent answerText(String bargainId, String answerId) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".answer." + answerId + ".text"); + } + + public static MutableComponent answerResponse(String bargainId, String answerId) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".answer." + answerId + ".response"); + } + + public static MutableComponent answerPower(String bargainId, String answerId, int index) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".answer." + answerId + ".power." + index); + } + + public static MutableComponent answerDrawback(String bargainId, String answerId, int index) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".answer." + answerId + ".drawback." + index); + } + + public static MutableComponent bargainAccept(String bargainId, int index) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".accept." + index); + } + + public static MutableComponent bargainRefuse(String bargainId, int index) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".refuse." + index); + } + + public static MutableComponent bargainOnAccept(String bargainId) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".on_accept"); + } + + public static MutableComponent bargainOnDefy(String bargainId) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".on_defy"); + } + + public static MutableComponent bargainSuffocation(String bargainId) { + return Component.translatable(PREFIX + "bargain." + bargainId + ".suffocation"); + } + + // ========================================================================= + // Threshold Encounter Keys + // ========================================================================= + + public static MutableComponent thresholdDialogue(int thresholdIndex, int lineIndex) { + return Component.translatable(PREFIX + "threshold." + thresholdIndex + ".dialogue." + lineIndex); + } + + public static MutableComponent thresholdQuestion(int thresholdIndex) { + return Component.translatable(PREFIX + "threshold." + thresholdIndex + ".question"); + } + + public static MutableComponent thresholdResponse(int thresholdIndex) { + return Component.translatable(PREFIX + "threshold." + thresholdIndex + ".response"); + } + + // ========================================================================= + // UI Keys + // ========================================================================= + + public static MutableComponent ui(String key) { + return Component.translatable(PREFIX + "ui." + key); + } + + // Common UI strings + public static MutableComponent uiYourBargains() { + return ui("your_bargains"); + } + + public static MutableComponent uiAvailableBargains() { + return ui("available_bargains"); + } + + public static MutableComponent uiDefiance() { + return ui("defiance"); + } + + public static MutableComponent uiScrollUp() { + return ui("scroll_up"); + } + + public static MutableComponent uiScrollDown() { + return ui("scroll_down"); + } + + public static MutableComponent uiSoulErosion() { + return ui("soul_erosion"); + } + + public static MutableComponent uiUnlockCost(int cost) { + return Component.translatable(PREFIX + "ui.unlock_cost", cost); + } + + public static MutableComponent uiDefianceCost(int cost) { + return Component.translatable(PREFIX + "ui.defiance_cost", cost); + } + + public static MutableComponent uiBack() { + return ui("back"); + } + + public static MutableComponent uiContinue() { + return ui("continue"); + } + + public static MutableComponent uiAcknowledge() { + return ui("acknowledge"); + } + + public static MutableComponent uiReviewBargains(int count) { + return Component.translatable(PREFIX + "ui.review_bargains", count); + } + + public static MutableComponent uiBrowseBargains(int count) { + return Component.translatable(PREFIX + "ui.browse_bargains", count); + } + + public static MutableComponent uiGazeConstellation() { + return ui("gaze_constellation"); + } + + public static MutableComponent uiJustLook() { + return ui("just_look"); + } + + public static MutableComponent uiLeave() { + return ui("leave"); + } + + public static MutableComponent uiForeverScarred() { + return ui("forever_scarred"); + } + + public static MutableComponent uiClickToBargain() { + return ui("click_to_bargain"); + } + + public static MutableComponent uiClickToDefy(int cost) { + return Component.translatable(PREFIX + "ui.click_to_defy", cost); + } + + public static MutableComponent uiPower() { + return ui("power"); + } + + public static MutableComponent uiDrawback() { + return ui("drawback"); + } + + public static MutableComponent uiCost() { + return ui("cost"); + } + + // Hub dialogue responses + public static MutableComponent hubReviewResponse() { + return ui("hub.review_response"); + } + + public static MutableComponent hubBrowseResponse() { + return ui("hub.browse_response"); + } + + public static MutableComponent hubReflectResponse() { + return ui("hub.reflect_response"); + } + + public static MutableComponent hubLeaveResponse() { + return ui("hub.leave_response"); + } + + // Defiance confirmation + public static MutableComponent defianceConfirm() { + return ui("defiance.confirm"); + } + + public static MutableComponent defianceCancel() { + return ui("defiance.cancel"); + } + + public static MutableComponent defianceWarning1(String bargainName) { + return Component.translatable(PREFIX + "ui.defiance.warning1", bargainName); + } + + public static MutableComponent defianceWarning2(int cost) { + return Component.translatable(PREFIX + "ui.defiance.warning2", cost); + } + + public static MutableComponent defianceWarning3() { + return ui("defiance.warning3"); + } + + public static MutableComponent defianceWarning4() { + return ui("defiance.warning4"); + } + + // ========================================================================= + // Reflection Dialogue (Hub mode contextual greetings) + // ========================================================================= + + public static MutableComponent hubGreeting(String key) { + return Component.translatable(PREFIX + "hub.greeting." + key); + } + + // ========================================================================= + // Key generators for lang file generation + // ========================================================================= + + public static String keyBargainName(String bargainId) { + return PREFIX + "bargain." + bargainId + ".name"; + } + + public static String keyBargainDescription(String bargainId) { + return PREFIX + "bargain." + bargainId + ".description"; + } + + public static String keyBargainDialogue(String bargainId, int index) { + return PREFIX + "bargain." + bargainId + ".dialogue." + index; + } + + public static String keyBargainQuestion(String bargainId) { + return PREFIX + "bargain." + bargainId + ".question"; + } + + public static String keyAnswerText(String bargainId, String answerId) { + return PREFIX + "bargain." + bargainId + ".answer." + answerId + ".text"; + } + + public static String keyAnswerResponse(String bargainId, String answerId) { + return PREFIX + "bargain." + bargainId + ".answer." + answerId + ".response"; + } + + public static String keyAnswerPower(String bargainId, String answerId, int index) { + return PREFIX + "bargain." + bargainId + ".answer." + answerId + ".power." + index; + } + + public static String keyAnswerDrawback(String bargainId, String answerId, int index) { + return PREFIX + "bargain." + bargainId + ".answer." + answerId + ".drawback." + index; + } + + public static String keyThresholdDialogue(int thresholdIndex, int lineIndex) { + return PREFIX + "threshold." + thresholdIndex + ".dialogue." + lineIndex; + } + + public static String keyThresholdQuestion(int thresholdIndex) { + return PREFIX + "threshold." + thresholdIndex + ".question"; + } + + public static String keyThresholdResponse(int thresholdIndex) { + return PREFIX + "threshold." + thresholdIndex + ".response"; + } + + public static String keyUi(String key) { + return PREFIX + "ui." + key; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ThresholdEncounter.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ThresholdEncounter.java new file mode 100644 index 000000000..659775a5e --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ThresholdEncounter.java @@ -0,0 +1,49 @@ +package com.ghostipedia.cosmiccore.common.reflection; + +import net.minecraft.network.chat.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * Threshold encounters - mandatory reflection dialogues at erosion milestones. + * No bargain is offered - just the Reflection observing the player's degradation. + */ +public class ThresholdEncounter { + + // Number of dialogue lines per threshold (for building the list) + private static final int[] DIALOGUE_COUNTS = { 4, 4, 4, 4, 4, 4, 3, 4, 4, 4 }; + + /** + * Get the dialogue for a specific erosion threshold. + * + * @param thresholdIndex 0-9 matching ReflectionConstants.THRESHOLDS + */ + public static List getDialogue(int thresholdIndex) { + if (thresholdIndex < 0 || thresholdIndex >= DIALOGUE_COUNTS.length) { + return List.of(ReflectionLang.thresholdDialogue(0, 0)); // Fallback + } + + List dialogue = new ArrayList<>(); + int count = DIALOGUE_COUNTS[thresholdIndex]; + for (int i = 0; i < count; i++) { + dialogue.add(ReflectionLang.thresholdDialogue(thresholdIndex, i)); + } + return dialogue; + } + + /** + * Get the question/prompt for a specific threshold. + * These are rhetorical - player can only acknowledge. + */ + public static Component getQuestion(int thresholdIndex) { + return ReflectionLang.thresholdQuestion(thresholdIndex); + } + + /** + * Get the response to the player's acknowledgment. + */ + public static Component getAcknowledgeResponse(int thresholdIndex) { + return ReflectionLang.thresholdResponse(thresholdIndex); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/Bargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/Bargain.java new file mode 100644 index 000000000..391e81f1e --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/Bargain.java @@ -0,0 +1,326 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; + +import java.util.List; +import java.util.Optional; + +/** + * Base class for all bargains in the Reflection system. + * A bargain represents a deal with your reflection - power for erosion. + * + * Economy: + * - Shards of Perpetuity: Currency to accept bargains (quest-gated) + * - Weight: How much soul capacity this bargain consumes (0-100 base capacity) + * - Erosion: Consequence of accepting (accumulates, drives visuals/whispers) + */ +public abstract class Bargain { + + private final ResourceLocation id; + private final BargainTier tier; + private final int shardCost; + private final int weight; + private final int erosionCost; + + protected Bargain(ResourceLocation id, BargainTier tier, int shardCost, int weight, int erosionCost) { + this.id = id; + this.tier = tier; + this.shardCost = shardCost; + this.weight = weight; + this.erosionCost = erosionCost; + } + + /** + * Legacy constructor for backwards compatibility during migration. + * + * @deprecated Use the new constructor with shardCost, weight, and erosionCost + */ + @Deprecated + protected Bargain(ResourceLocation id, BargainTier tier, int baseCost) { + this(id, tier, 0, 0, baseCost); + } + + /** + * @return unique identifier for this bargain + */ + public ResourceLocation getId() { + return id; + } + + /** + * @return what corruption range this bargain is available in + */ + public BargainTier getTier() { + return tier; + } + + /** + * @return shard cost to accept this bargain (Shards of Perpetuity currency) + */ + public int getShardCost() { + return shardCost; + } + + /** + * @return weight against soul capacity (base 100 capacity) + */ + public int getWeight() { + return weight; + } + + /** + * @return erosion gained when accepting this bargain + */ + public int getErosionCost() { + return erosionCost; + } + + /** + * @return base erosion cost (modified by current corruption level) + * @deprecated Use getErosionCost() instead + */ + @Deprecated + public int getBaseCost() { + return erosionCost; + } + + /** + * @return the display name of this bargain + */ + public abstract Component getName(); + + /** + * @return description shown in the mirror interface + */ + public abstract Component getDescription(); + + /** + * @return the reflection's dialogue when offering this bargain + */ + public abstract List getOfferDialogue(Player player); + + /** + * @return the philosophical question posed by the reflection + */ + public abstract Component getQuestion(); + + /** + * @return possible answers to the question + */ + public abstract List getAnswers(); + + /** + * Check if this bargain can be offered to the player. + * Override for custom conditions beyond tier requirements. + */ + public boolean canOffer(Player player, int currentErosion) { + return tier.isAvailableAt(currentErosion); + } + + /** + * Check if context makes this bargain relevant (for curated offers). + * Examples: hunger bargain offered when player is starving, + * fall damage bargain offered after dying to fall damage. + */ + public boolean isContextuallyRelevant(Player player, BargainContext context) { + return true; // Override in subclasses + } + + /** + * Called when the player accepts this bargain with a specific answer. + * Apply the power/effects here. + */ + public abstract void onAccept(Player player, BargainAnswer answer); + + /** + * Called when the player defies (removes) this bargain. + * Remove the power but NOT the debuff (scar). + */ + public abstract void onDefy(Player player); + + /** + * Called every tick while this bargain is active. + * Override for passive effects, erosion-per-use, etc. + */ + public void tick(Player player) { + // Default: no tick behavior + } + + /** + * @return the soul visual transformation for this bargain + */ + public abstract BargainVisual getSoulVisual(); + + /** + * @return display name for UI purposes + */ + public Component getDisplayName() { + return getName(); + } + + /** + * @return power description lines for tooltips/hub + */ + public List getPowerDescriptions() { + // Default: derive from description + return List.of(Component.literal("\u00A7a" + getDescription().getString())); + } + + /** + * @return drawback description lines for tooltips/hub + */ + public List getDrawbackDescriptions() { + // Default: generic drawback showing all costs + List drawbacks = new java.util.ArrayList<>(); + if (shardCost > 0) { + drawbacks.add(Component.literal("\u00A7b" + shardCost + " shards")); + } + if (weight > 0) { + drawbacks.add(Component.literal("\u00A7d" + weight + " weight")); + } + if (erosionCost > 0) { + drawbacks.add(Component.literal("\u00A7c+" + erosionCost + " erosion")); + } + return drawbacks.isEmpty() ? List.of(Component.literal("\u00A7aFree")) : drawbacks; + } + + /** + * @return dialogue the reflection says after accepting + */ + public List getAcceptDialogue(Player player, BargainAnswer answer) { + return List.of(Component.literal("How does it feel?")); + } + + /** + * @return dialogue the reflection says if the player refuses + */ + public List getRefuseDialogue(Player player) { + return List.of(Component.literal("...Maybe next time.")); + } + + /** + * Tiers determine when bargains become available. + */ + public enum BargainTier { + + /** Available at low corruption (0-100). Early game traps. */ + EARLY(0, 100), + /** Available at low-mid corruption (0-300). */ + EARLY_MID(0, 300), + /** Only available at mid corruption (100-500). */ + MID(100, 500), + /** Only available at high corruption (300-750). */ + LATE(300, 750), + /** Only available at very high corruption (500+). The dangerous stuff. */ + EXTREME(500, Integer.MAX_VALUE), + /** Always available, cost scales. */ + ANY(0, Integer.MAX_VALUE); + + private final int minErosion; + private final int maxErosion; + + BargainTier(int minErosion, int maxErosion) { + this.minErosion = minErosion; + this.maxErosion = maxErosion; + } + + public boolean isAvailableAt(int erosion) { + return erosion >= minErosion && erosion <= maxErosion; + } + + public int getMinErosion() { + return minErosion; + } + + public int getMaxErosion() { + return maxErosion; + } + } + + /** + * Represents an answer choice in a bargain dialogue. + */ + public record BargainAnswer( + String id, + Component text, + Optional reflectionResponse, + boolean grantsFullPower, + float costModifier, + List powerDescription, + List drawbacks) { + + public BargainAnswer(String id, Component text) { + this(id, text, Optional.empty(), true, 1.0f, List.of(), List.of()); + } + + public BargainAnswer(String id, Component text, Component response) { + this(id, text, Optional.of(response), true, 1.0f, List.of(), List.of()); + } + + public BargainAnswer withCostModifier(float modifier) { + return new BargainAnswer(id, text, reflectionResponse, grantsFullPower, modifier, powerDescription, + drawbacks); + } + + public BargainAnswer withReducedPower() { + return new BargainAnswer(id, text, reflectionResponse, false, costModifier, powerDescription, drawbacks); + } + + /** + * Add power description lines that show on hover. + */ + public BargainAnswer withPower(Component... powers) { + return new BargainAnswer(id, text, reflectionResponse, grantsFullPower, costModifier, List.of(powers), + drawbacks); + } + + /** + * Add drawback/curse description lines that show on hover. + */ + public BargainAnswer withDrawbacks(Component... curses) { + return new BargainAnswer(id, text, reflectionResponse, grantsFullPower, costModifier, powerDescription, + List.of(curses)); + } + + /** + * Add both power and drawback descriptions. + */ + public BargainAnswer withDetails(List powers, List curses) { + return new BargainAnswer(id, text, reflectionResponse, grantsFullPower, costModifier, powers, curses); + } + } + + /** + * Visual transformation data for the soul portrait. + */ + public record BargainVisual( + String visualType, + String description) { + + public static BargainVisual of(String type, String desc) { + return new BargainVisual(type, desc); + } + } + + /** + * Context for determining if a bargain is relevant to offer. + */ + public record BargainContext( + Optional lastDeathCause, + Optional currentDimension, + boolean isLowHealth, + boolean isHungry, + boolean isSuffocating, + boolean isBurning, + boolean isFreezing) { + + public static BargainContext empty() { + return new BargainContext( + Optional.empty(), + Optional.empty(), + false, false, false, false, false); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/BargainRegistry.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/BargainRegistry.java new file mode 100644 index 000000000..f721cba29 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/BargainRegistry.java @@ -0,0 +1,179 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Registry for all bargains in the Reflection system. + * Bargains are registered here and can be looked up by ID or filtered by context. + */ +public final class BargainRegistry { + + private BargainRegistry() {} + + private static final Map BARGAINS = new LinkedHashMap<>(); + + /** + * Register a bargain. Called during mod initialization. + */ + public static void register(Bargain bargain) { + if (BARGAINS.containsKey(bargain.getId())) { + CosmicCore.LOGGER.warn("Duplicate bargain registration: {}", bargain.getId()); + } + BARGAINS.put(bargain.getId(), bargain); + CosmicCore.LOGGER.debug("Registered bargain: {}", bargain.getId()); + } + + /** + * Get a bargain by ID. + */ + public static Optional get(ResourceLocation id) { + return Optional.ofNullable(BARGAINS.get(id)); + } + + /** + * Get all registered bargains. + */ + public static Collection getAll() { + return Collections.unmodifiableCollection(BARGAINS.values()); + } + + /** + * Get all bargains available to a player at their current erosion level. + */ + public static List getAvailable(Player player) { + return ReflectionCapability.get(player).map(reflection -> { + int erosion = reflection.getErosion(); + return BARGAINS.values().stream() + .filter(b -> b.canOffer(player, erosion)) + .filter(b -> !reflection.hasBargain(b.getId())) + .collect(Collectors.toList()); + }).orElse(Collections.emptyList()); + } + + /** + * Get bargains that are contextually relevant to offer. + * Used for curated offers in the mirror interface. + */ + public static List getContextualOffers(Player player, Bargain.BargainContext context, int maxOffers) { + return ReflectionCapability.get(player).map(reflection -> { + int erosion = reflection.getErosion(); + + // Get all available bargains + List available = BARGAINS.values().stream() + .filter(b -> b.canOffer(player, erosion)) + .filter(b -> !reflection.hasBargain(b.getId())) + .collect(Collectors.toList()); + + // Prioritize contextually relevant ones + List relevant = available.stream() + .filter(b -> b.isContextuallyRelevant(player, context)) + .limit(maxOffers) + .collect(Collectors.toList()); + + // If we don't have enough relevant ones, fill with others + if (relevant.size() < maxOffers) { + available.stream() + .filter(b -> !relevant.contains(b)) + .limit(maxOffers - relevant.size()) + .forEach(relevant::add); + } + + return relevant; + }).orElse(Collections.emptyList()); + } + + /** + * Get all active bargains for a player. + */ + public static List getActive(Player player) { + return ReflectionCapability.get(player).map(reflection -> reflection.getActiveBargains().stream() + .map(BargainRegistry::get) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList())).orElse(Collections.emptyList()); + } + + /** + * Get all bargains the player has defied (for showing scars). + */ + public static List getDefied(Player player) { + return ReflectionCapability.get(player).map(reflection -> reflection.getDefianceScars().stream() + .map(BargainRegistry::get) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList())).orElse(Collections.emptyList()); + } + + /** + * Check if the player can defy a specific bargain. + */ + public static boolean canDefy(Player player, ResourceLocation bargainId) { + return ReflectionCapability.get(player) + .map(reflection -> reflection.hasBargain(bargainId)) + .orElse(false); + } + + /** + * Calculate the cost of a bargain for the player. + * Takes into account current erosion level and bargain base cost. + */ + public static int calculateCost(Player player, Bargain bargain) { + return ReflectionCapability.get(player).map(reflection -> { + int erosion = reflection.getErosion(); + int baseCost = bargain.getBaseCost(); + + // Scale cost based on current erosion + float multiplier = 1.0f; + if (erosion > 750) multiplier = 2.0f; + else if (erosion > 500) multiplier = 1.75f; + else if (erosion > 300) multiplier = 1.5f; + else if (erosion > 100) multiplier = 1.25f; + + return Math.round(baseCost * multiplier); + }).orElse(bargain.getBaseCost()); + } + + /** + * Calculate the defiance cost for removing a bargain. + */ + public static int calculateDefianceCost(Player player, Bargain bargain) { + int originalCost = calculateCost(player, bargain); + return Math.round(originalCost * 2.5f); // Defiance costs 2.5x the original + } + + /** + * Calculate the defiance cost without player context (client-side). + * Uses base cost * 2.5 + */ + public static int calculateDefianceCost(Bargain bargain) { + return Math.round(bargain.getBaseCost() * 2.5f); + } + + /** + * Get available bargains based on pre-synced active and scar sets (client-side). + */ + public static List getAvailable(Set activeBargains, Set scars) { + return BARGAINS.values().stream() + .filter(b -> !activeBargains.contains(b.getId())) + .filter(b -> !scars.contains(b.getId())) + .collect(Collectors.toList()); + } + + /** + * Get active bargains based on pre-synced set (client-side). + */ + public static List getActive(Set activeBargains) { + return activeBargains.stream() + .map(BargainRegistry::get) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/CosmicBargains.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/CosmicBargains.java new file mode 100644 index 000000000..0fad9c4df --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/CosmicBargains.java @@ -0,0 +1,68 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.ArmorBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.BackBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.DepthsBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.FallImmunityBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.FireImmunityBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.FlightBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.HealthBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.HomeBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.HungerBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.NightVisionBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.QuakeMovementBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.ReachBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.StepAssistBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.StrengthBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.SwiftnessBargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.VoidResistanceBargain; + +/** + * Registers all bargains for the Reflection system. + * Called during mod initialization. + */ +public final class CosmicBargains { + + private CosmicBargains() {} + + public static void init() { + CosmicCore.LOGGER.info("Registering Reflection bargains..."); + + // EARLY TIER - Gateway bargains (0-100 erosion) + safeRegister("QuakeMovementBargain", () -> BargainRegistry.register(QuakeMovementBargain.INSTANCE)); + safeRegister("StepAssistBargain", () -> BargainRegistry.register(StepAssistBargain.INSTANCE)); + safeRegister("NightVisionBargain", () -> BargainRegistry.register(NightVisionBargain.INSTANCE)); + safeRegister("SwiftnessBargain", () -> BargainRegistry.register(SwiftnessBargain.INSTANCE)); + + // EARLY_MID TIER - Building addiction (0-300) + safeRegister("HomeBargain", () -> BargainRegistry.register(HomeBargain.INSTANCE)); + safeRegister("BackBargain", () -> BargainRegistry.register(BackBargain.INSTANCE)); + safeRegister("HealthBargain", () -> BargainRegistry.register(HealthBargain.INSTANCE)); + safeRegister("StrengthBargain", () -> BargainRegistry.register(StrengthBargain.INSTANCE)); + safeRegister("DepthsBargain", () -> BargainRegistry.register(DepthsBargain.INSTANCE)); + + // MID TIER - Significant commitment (100-500) + safeRegister("ReachBargain", () -> BargainRegistry.register(ReachBargain.INSTANCE)); + safeRegister("FallImmunityBargain", () -> BargainRegistry.register(FallImmunityBargain.INSTANCE)); + safeRegister("HungerBargain", () -> BargainRegistry.register(HungerBargain.INSTANCE)); + safeRegister("ArmorBargain", () -> BargainRegistry.register(ArmorBargain.INSTANCE)); + safeRegister("FireImmunityBargain", () -> BargainRegistry.register(FireImmunityBargain.INSTANCE)); + + // LATE TIER - Deep corruption (300-750) + safeRegister("VoidResistanceBargain", () -> BargainRegistry.register(VoidResistanceBargain.INSTANCE)); + + // EXTREME TIER - Point of no return (500+) + safeRegister("FlightBargain", () -> BargainRegistry.register(FlightBargain.INSTANCE)); + + CosmicCore.LOGGER.info("Registered {} bargains", BargainRegistry.getAll().size()); + } + + private static void safeRegister(String name, Runnable registrar) { + try { + registrar.run(); + } catch (Throwable t) { + CosmicCore.LOGGER.error("Failed to register bargain: {}", name, t); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/ArmorBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/ArmorBargain.java new file mode 100644 index 000000000..38fc5e31c --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/ArmorBargain.java @@ -0,0 +1,158 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.player.Player; + +import java.util.List; +import java.util.UUID; + +/** + * Armor Bargain: More armor, but slower movement. + * + * POWER: +8 armor (equivalent to full iron armor) + * DRAWBACK: 15% slower movement speed + * + * Thematically: Your flesh has calcified, hardened against blows. But that + * hardness comes with weight. Every step is heavier. You're safer... but + * slower. A walking fortress that can't flee. + * + * This creates tank gameplay - you can stand and fight but can't escape. + * Good for holding ground, bad for kiting or hit-and-run. + */ +public class ArmorBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("carapace"); + public static final ArmorBargain INSTANCE = new ArmorBargain(); + private static final String BARGAIN_ID = "carapace"; + private static final UUID ARMOR_MODIFIER_UUID = UUID.fromString("e2a9b0c8-5678-8901-cdef-012345678901"); + private static final UUID SPEED_MODIFIER_UUID = UUID.fromString("e2a9b0c8-5678-8901-cdef-012345678902"); + + /** Movement speed penalty (0.15 = 15% slower) */ + public static final float SPEED_PENALTY = -0.15f; + + private ArmorBargain() { + super( + ID, + BargainTier.MID, + 64, // shardCost + 25, // weight + 100 // erosion + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3), + ReflectionLang.bargainDialogue(BARGAIN_ID, 4)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("survive", ReflectionLang.answerText(BARGAIN_ID, "survive"), + ReflectionLang.answerResponse(BARGAIN_ID, "survive")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "survive", 0), + ReflectionLang.answerPower(BARGAIN_ID, "survive", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "survive", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "survive", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (answer.id().equals("survive")) { + applyArmorBoost(player); + applySpeedPenalty(player); + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + removeArmorBoost(player); + removeSpeedPenalty(player); + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("shell", "The soul's surface appears calcified, plated, moving slowly"); + } + + public static void applyArmorBoost(Player player) { + var attribute = player.getAttribute(Attributes.ARMOR); + if (attribute != null) { + attribute.removeModifier(ARMOR_MODIFIER_UUID); + attribute.addPermanentModifier(new AttributeModifier( + ARMOR_MODIFIER_UUID, "Reflection Carapace", 8.0, AttributeModifier.Operation.ADDITION)); + } + } + + public static void removeArmorBoost(Player player) { + var attribute = player.getAttribute(Attributes.ARMOR); + if (attribute != null) { + attribute.removeModifier(ARMOR_MODIFIER_UUID); + } + } + + public static void applySpeedPenalty(Player player) { + var attribute = player.getAttribute(Attributes.MOVEMENT_SPEED); + if (attribute != null) { + attribute.removeModifier(SPEED_MODIFIER_UUID); + attribute.addPermanentModifier(new AttributeModifier( + SPEED_MODIFIER_UUID, "Carapace Weight", SPEED_PENALTY, AttributeModifier.Operation.MULTIPLY_TOTAL)); + } + } + + public static void removeSpeedPenalty(Player player) { + var attribute = player.getAttribute(Attributes.MOVEMENT_SPEED); + if (attribute != null) { + attribute.removeModifier(SPEED_MODIFIER_UUID); + } + } + + // ========================================================================= + // Static helper methods + // ========================================================================= + + /** + * Check if a player has the Calcified Flesh bargain active. + */ + public static boolean hasBargain(Player player) { + return ReflectionCapability.get(player) + .map(reflection -> reflection.hasBargain(ID)) + .orElse(false); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/BackBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/BackBargain.java new file mode 100644 index 000000000..fcd3860f4 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/BackBargain.java @@ -0,0 +1,227 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionConstants; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * The /back bargain - return to your last death location. + * + * Even more of a trap than /home. You WILL die. You WILL want your stuff back. + * And every time you use it, the cost grows. + * + * Base cost: 2 erosion + * Quick use penalty: Doubles each time used within 15 minutes (2 -> 4 -> 8 -> 16 -> 32) + * Ceiling: 32 erosion per use + * Reset: After 15 minutes of no use, resets to base cost + */ +public class BackBargain extends Bargain { + + private static final ResourceLocation ID = CosmicCore.id("back"); + public static final BackBargain INSTANCE = new BackBargain(); + private static final String BARGAIN_ID = "back"; + + // Track last death positions per player + private static final Map lastDeathLocations = new HashMap<>(); + + private BackBargain() { + super( + ID, + BargainTier.EARLY, + 0, // shardCost - FREE (poisoned apple - per-use costs add up) + 0, // weight - FREE (doesn't consume soul capacity) + 100 // erosion - significant upfront cost + per-use escalation + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("accept", ReflectionLang.answerText(BARGAIN_ID, "accept"), + ReflectionLang.answerResponse(BARGAIN_ID, "accept")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "accept", 0), + ReflectionLang.answerPower(BARGAIN_ID, "accept", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "accept", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "accept", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (answer.id().equals("accept")) { + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("echo", "A faint afterimage follows you, showing where death last found you"); + } + + @Override + public void tick(Player player) { + // No passive effects + } + + /** + * Record a death location for a player. + * Called from death event handler. + */ + public static void recordDeath(ServerPlayer player) { + lastDeathLocations.put(player.getUUID(), new DeathLocation( + player.level().dimension(), + player.position())); + } + + /** + * Execute the /back teleport. + * Called when player uses the command (if they have the bargain). + * + * @return true if teleport succeeded, false otherwise + */ + public static boolean executeBack(ServerPlayer player) { + return ReflectionCapability.get(player).map(reflection -> { + if (!reflection.hasBargain(ID)) { + player.displayClientMessage( + Component.literal("\u00A7cYou haven't made this bargain."), + false); + return false; + } + + // Check for death location + DeathLocation deathLoc = lastDeathLocations.get(player.getUUID()); + if (deathLoc == null) { + player.displayClientMessage( + Component.literal("\u00A7cYou have no death to return to."), + false); + return false; + } + + // Get the target dimension + ServerLevel targetLevel = player.server.getLevel(deathLoc.dimension); + if (targetLevel == null) { + player.displayClientMessage( + Component.literal("\u00A7cThat place no longer exists."), + false); + return false; + } + + // Calculate current cost based on usage + int cost = ReflectionConstants.getCommandCost(reflection, "back"); + + // Apply erosion cost + reflection.addErosion(cost); + reflection.recordCommandUse("back"); + + // Teleport (handling cross-dimension) + Vec3 pos = deathLoc.position; + if (player.level().dimension() != deathLoc.dimension) { + player.teleportTo(targetLevel, pos.x, pos.y, pos.z, player.getYRot(), player.getXRot()); + } else { + player.teleportTo(pos.x, pos.y, pos.z); + } + + // Effects + player.level().playSound(null, player.blockPosition(), + SoundEvents.ENDERMAN_TELEPORT, SoundSource.PLAYERS, 1.0f, 0.5f); + + // Feedback based on cost - gets increasingly ominous + if (cost <= 4) { + player.displayClientMessage( + Component.literal("\u00A77\u00A7o*You return to where you fell.*"), + true); + } else if (cost <= 12) { + player.displayClientMessage( + Component.literal("\u00A77\u00A7o*Again you return. The ground remembers your blood.*"), + true); + } else if (cost <= 24) { + player.displayClientMessage( + Component.literal("\u00A77\u00A7o*How many times have you died here? Does it matter anymore?*"), + true); + } else { + player.displayClientMessage( + Component.literal("\u00A77\u00A7o*Death. Return. Death. Return. You've made this a ritual.*"), + true); + } + + // Show cost in chat + player.displayClientMessage( + Component.literal("\u00A78[Erosion +" + cost + "]"), + false); + + // Clear the death location after use (can't spam back to same spot) + lastDeathLocations.remove(player.getUUID()); + + return true; + }).orElse(false); + } + + /** + * Check if a player has a death location recorded. + */ + public static boolean hasDeathLocation(UUID playerId) { + return lastDeathLocations.containsKey(playerId); + } + + /** + * Clear death location (on logout, etc.) + */ + public static void clearDeathLocation(UUID playerId) { + lastDeathLocations.remove(playerId); + } + + private record DeathLocation( + ResourceKey dimension, + Vec3 position) {} +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/DepthsBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/DepthsBargain.java new file mode 100644 index 000000000..09af29ee6 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/DepthsBargain.java @@ -0,0 +1,145 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; + +import java.util.List; + +/** + * Depths Bargain: Enhanced lung capacity, but instant death on suffocation. + * + * POWER: +50% oxygen capacity (90 seconds -> 135 seconds base air) + * DRAWBACK: When oxygen reaches 0, you instantly die instead of taking gradual damage. + * + * Thematically: Your lungs have been remade by the depths. They can hold more, + * but they've forgotten how to struggle - when they fail, they fail completely. + * + * This replaces the old "infinite water breathing" bargain with something more + * interesting and balanced - a meaningful power boost with a meaningful risk. + */ +public class DepthsBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("depths"); + public static final DepthsBargain INSTANCE = new DepthsBargain(); + private static final String BARGAIN_ID = "depths"; + + /** Multiplier for max oxygen capacity (1.5 = 50% more) */ + public static final float OXYGEN_CAPACITY_MULTIPLIER = 1.5f; + + private DepthsBargain() { + super( + ID, + BargainTier.EARLY_MID, + 64, // shardCost + 15, // weight + 75 // erosion + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3), + ReflectionLang.bargainDialogue(BARGAIN_ID, 4), + ReflectionLang.bargainDialogue(BARGAIN_ID, 5)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("embrace", + ReflectionLang.answerText(BARGAIN_ID, "embrace"), + ReflectionLang.answerResponse(BARGAIN_ID, "embrace")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "embrace", 0), + ReflectionLang.answerPower(BARGAIN_ID, "embrace", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "embrace", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "embrace", 1))), + new BargainAnswer("refuse", + ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (!answer.id().equals("refuse")) { + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("gills", "depths_visual"); + } + + // ========================================================================= + // Static helper methods for OxygenLogic integration + // ========================================================================= + + /** + * Check if a player has the Depths bargain active. + */ + public static boolean hasBargain(Player player) { + return ReflectionCapability.get(player) + .map(reflection -> reflection.hasBargain(ID)) + .orElse(false); + } + + /** + * Get the oxygen capacity multiplier for a player. + * Returns 1.0 if they don't have the bargain. + */ + public static float getCapacityMultiplier(Player player) { + return hasBargain(player) ? OXYGEN_CAPACITY_MULTIPLIER : 1.0f; + } + + /** + * Check if this player should die instantly when oxygen reaches 0. + * Returns true if they have the bargain (the drawback). + */ + public static boolean shouldInstantKillOnSuffocation(Player player) { + return hasBargain(player); + } + + /** + * Execute instant death for suffocation (for use in OxygenLogic). + */ + public static void executeInstantSuffocation(ServerPlayer player) { + player.displayClientMessage(ReflectionLang.bargainSuffocation(BARGAIN_ID), false); + // Deal massive damage to ensure death - using drown damage type + player.hurt(player.damageSources().drown(), Float.MAX_VALUE); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/FallImmunityBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/FallImmunityBargain.java new file mode 100644 index 000000000..40bf40a5a --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/FallImmunityBargain.java @@ -0,0 +1,166 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageTypes; +import net.minecraft.world.entity.player.Player; + +import java.util.List; + +/** + * Fall Damage Bargain: Reduced fall damage with fly-swatter vulnerability. + * + * POWER: 80% fall damage reduction - falls barely hurt you + * DRAWBACK: 50% increased damage from player attacks and explosions + * + * Thematically: Your body has learned to absorb impact, becoming soft and + * yielding on landing. But that softness makes you more vulnerable to + * intentional force - a sword, an arrow, an explosion. The ground forgives + * you, but other players won't. + * + * This creates interesting PvP dynamics - the bargain holder is great at + * exploration but vulnerable in combat. + */ +public class FallImmunityBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("soft_landing"); + public static final FallImmunityBargain INSTANCE = new FallImmunityBargain(); + private static final String BARGAIN_ID = "soft_landing"; + + /** Fall damage multiplier (0.2 = 80% reduction) */ + public static final float FALL_DAMAGE_MULTIPLIER = 0.2f; + + /** Combat damage multiplier (1.5 = 50% more damage from players/explosions) */ + public static final float COMBAT_DAMAGE_MULTIPLIER = 1.5f; + + private FallImmunityBargain() { + super( + ID, + BargainTier.MID, + 64, // shardCost + 25, // weight + 100 // erosion + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3), + ReflectionLang.bargainDialogue(BARGAIN_ID, 4)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("yes", ReflectionLang.answerText(BARGAIN_ID, "yes"), + ReflectionLang.answerResponse(BARGAIN_ID, "yes")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "yes", 0), + ReflectionLang.answerPower(BARGAIN_ID, "yes", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "yes", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "yes", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public boolean isContextuallyRelevant(Player player, BargainContext context) { + return context.lastDeathCause().map(cause -> cause.contains("fall")).orElse(false); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (answer.id().equals("yes")) { + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("floating", "The soul hovers slightly, its form diffuse and yielding"); + } + + // ========================================================================= + // Static helper methods for damage integration + // ========================================================================= + + /** + * Check if a player has the Phantom Weight bargain active. + */ + public static boolean hasBargain(Player player) { + return ReflectionCapability.get(player) + .map(reflection -> reflection.hasBargain(ID)) + .orElse(false); + } + + /** + * Modify fall damage for a player with this bargain. + */ + public static float modifyFallDamage(Player player, float originalDamage) { + if (hasBargain(player)) { + return originalDamage * FALL_DAMAGE_MULTIPLIER; + } + return originalDamage; + } + + /** + * Modify combat damage (player attacks, explosions) for a player with this bargain. + */ + public static float modifyCombatDamage(Player player, float originalDamage) { + if (hasBargain(player)) { + return originalDamage * COMBAT_DAMAGE_MULTIPLIER; + } + return originalDamage; + } + + /** + * Check if a damage source is fall-related. + */ + public static boolean isFallDamage(DamageSource source) { + return source.is(DamageTypes.FALL) || + source.is(DamageTypes.FLY_INTO_WALL); + } + + /** + * Check if a damage source is combat-related (player attacks, explosions). + */ + public static boolean isCombatDamage(DamageSource source) { + return source.is(DamageTypes.PLAYER_ATTACK) || + source.is(DamageTypes.EXPLOSION) || + source.is(DamageTypes.PLAYER_EXPLOSION) || + source.getEntity() instanceof Player; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/FireImmunityBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/FireImmunityBargain.java new file mode 100644 index 000000000..968063b00 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/FireImmunityBargain.java @@ -0,0 +1,171 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.damagesource.DamageTypes; +import net.minecraft.world.entity.player.Player; + +import java.util.List; + +/** + * Cinder Bargain: Fire resistance with cold vulnerability. + * + * POWER: 75% fire/lava damage reduction (not immunity!) + * DRAWBACK: 2x damage from freezing/cold sources + * + * Thematically: Your soul burns with borrowed heat. Fire recognizes you as kin + * and holds back its fury - but cold now bites twice as deep. You've traded + * one element's wrath for another's. + */ +public class FireImmunityBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("cinder"); + public static final FireImmunityBargain INSTANCE = new FireImmunityBargain(); + private static final String BARGAIN_ID = "cinder"; + + /** Fire damage multiplier (0.25 = 75% reduction) */ + public static final float FIRE_DAMAGE_MULTIPLIER = 0.25f; + + /** Cold damage multiplier (2.0 = double damage) */ + public static final float COLD_DAMAGE_MULTIPLIER = 2.0f; + + private FireImmunityBargain() { + super( + ID, + BargainTier.MID, + 64, // shardCost + 25, // weight + 100 // erosion + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3), + ReflectionLang.bargainDialogue(BARGAIN_ID, 4)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("burn", ReflectionLang.answerText(BARGAIN_ID, "burn"), + ReflectionLang.answerResponse(BARGAIN_ID, "burn")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "burn", 0), + ReflectionLang.answerPower(BARGAIN_ID, "burn", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "burn", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "burn", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public boolean isContextuallyRelevant(Player player, BargainContext context) { + return context.isBurning(); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (!answer.id().equals("refuse")) { + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public void tick(Player player) { + // Extinguish fire visual faster (still take reduced damage, but less annoying burning animation) + if (player.getRemainingFireTicks() > 20) { + player.setRemainingFireTicks(20); + } + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("ember", "Faint embers drift from the soul's form, it radiates warmth but shivers"); + } + + // ========================================================================= + // Static helper methods for damage integration + // ========================================================================= + + /** + * Check if a player has the Cinder bargain active. + */ + public static boolean hasBargain(Player player) { + return ReflectionCapability.get(player) + .map(reflection -> reflection.hasBargain(ID)) + .orElse(false); + } + + /** + * Modify fire damage for a player with this bargain. + * Returns the modified damage amount. + */ + public static float modifyFireDamage(Player player, float originalDamage) { + if (hasBargain(player)) { + return originalDamage * FIRE_DAMAGE_MULTIPLIER; + } + return originalDamage; + } + + /** + * Modify cold/freeze damage for a player with this bargain. + * Returns the modified damage amount. + */ + public static float modifyColdDamage(Player player, float originalDamage) { + if (hasBargain(player)) { + return originalDamage * COLD_DAMAGE_MULTIPLIER; + } + return originalDamage; + } + + /** + * Check if a damage source is fire-related. + */ + public static boolean isFireDamage(DamageSource source) { + return source.is(DamageTypes.IN_FIRE) || + source.is(DamageTypes.ON_FIRE) || + source.is(DamageTypes.LAVA) || + source.is(DamageTypes.HOT_FLOOR); + } + + /** + * Check if a damage source is cold-related. + */ + public static boolean isColdDamage(DamageSource source) { + return source.is(DamageTypes.FREEZE); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/FlightBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/FlightBargain.java new file mode 100644 index 000000000..78d9b9b67 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/FlightBargain.java @@ -0,0 +1,178 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.player.Player; + +import java.util.List; +import java.util.UUID; + +/** + * Flight Bargain: Creative-style flight, but slower when grounded. + * + * POWER: True flight like creative mode + * DRAWBACK: 30% slower movement speed when not flying + * + * Thematically: You've abandoned your connection to the ground. Gravity no longer + * binds you... but walking has become foreign. Your legs have forgotten their + * purpose. The ground feels wrong beneath feet that were made to soar. + * + * This creates interesting gameplay - you're incredibly mobile in the air, + * but if you're grounded (out of stamina, in a no-fly zone, etc.) you're + * sluggish and vulnerable. + */ +public class FlightBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("ascension"); + public static final FlightBargain INSTANCE = new FlightBargain(); + private static final String BARGAIN_ID = "ascension"; + private static final UUID GROUND_SLOW_UUID = UUID.fromString("f0a1b2c3-4567-89ab-cdef-012345678901"); + + /** Movement speed reduction when grounded (0.3 = 30% slower) */ + public static final float GROUND_SPEED_PENALTY = -0.3f; + + private FlightBargain() { + super( + ID, + BargainTier.EXTREME, + 1000, // shardCost - VERY expensive, late-game + 50, // weight - takes up half your soul capacity + 1000 // erosion - massive transformation + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3), + ReflectionLang.bargainDialogue(BARGAIN_ID, 4), + ReflectionLang.bargainDialogue(BARGAIN_ID, 5)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("ready", ReflectionLang.answerText(BARGAIN_ID, "ready"), + ReflectionLang.answerResponse(BARGAIN_ID, "ready")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "ready", 0), + ReflectionLang.answerPower(BARGAIN_ID, "ready", 1), + ReflectionLang.answerPower(BARGAIN_ID, "ready", 2), + ReflectionLang.answerPower(BARGAIN_ID, "ready", 3)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "ready", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "ready", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (!answer.id().equals("refuse")) { + player.getAbilities().mayfly = true; + player.onUpdateAbilities(); + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + player.getAbilities().mayfly = false; + player.getAbilities().flying = false; + player.onUpdateAbilities(); + removeGroundPenalty(player); + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public void tick(Player player) { + // Ensure flight stays enabled + if (!player.getAbilities().mayfly) { + player.getAbilities().mayfly = true; + player.onUpdateAbilities(); + } + + // Apply/remove ground penalty based on flying state + if (player.getAbilities().flying) { + // Flying - remove ground penalty + removeGroundPenalty(player); + } else { + // Grounded - apply speed penalty + applyGroundPenalty(player); + } + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("weightless", "The soul floats high, tethers to earth severed, legs atrophied"); + } + + // ========================================================================= + // Ground penalty methods + // ========================================================================= + + private static void applyGroundPenalty(Player player) { + var attribute = player.getAttribute(Attributes.MOVEMENT_SPEED); + if (attribute != null && attribute.getModifier(GROUND_SLOW_UUID) == null) { + attribute.addTransientModifier(new AttributeModifier( + GROUND_SLOW_UUID, "Ascension Ground Penalty", GROUND_SPEED_PENALTY, + AttributeModifier.Operation.MULTIPLY_TOTAL)); + } + } + + private static void removeGroundPenalty(Player player) { + var attribute = player.getAttribute(Attributes.MOVEMENT_SPEED); + if (attribute != null) { + attribute.removeModifier(GROUND_SLOW_UUID); + } + } + + // ========================================================================= + // Static helper methods + // ========================================================================= + + /** + * Check if a player has the Ascension bargain active. + */ + public static boolean hasBargain(Player player) { + return ReflectionCapability.get(player) + .map(reflection -> reflection.hasBargain(ID)) + .orElse(false); + } + + /** + * Check if a player is currently suffering the ground penalty. + */ + public static boolean hasGroundPenalty(Player player) { + if (!hasBargain(player)) return false; + var attribute = player.getAttribute(Attributes.MOVEMENT_SPEED); + return attribute != null && attribute.getModifier(GROUND_SLOW_UUID) != null; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/HealthBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/HealthBargain.java new file mode 100644 index 000000000..f9e31622a --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/HealthBargain.java @@ -0,0 +1,150 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.player.Player; + +import java.util.List; +import java.util.UUID; + +/** + * Health Bargain: More max health, but healing is less effective. + * + * POWER: +10 max health (5 hearts) + * DRAWBACK: Healing from potions and instant health is 50% less effective + * + * Thematically: Your body has expanded beyond its natural limits. There's more + * of you now... but that borrowed flesh resists healing. Potions can't fully + * reach it. Regeneration struggles against what shouldn't exist. + * + * This creates interesting gameplay - you can tank more hits, but recovering + * from damage is harder. Good for exploration, risky for extended combat. + */ +public class HealthBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("vitality"); + public static final HealthBargain INSTANCE = new HealthBargain(); + private static final String BARGAIN_ID = "vitality"; + private static final UUID MODIFIER_UUID = UUID.fromString("a8c5b6d4-1234-4567-89ab-cdef01234567"); + + /** Healing reduction multiplier (0.5 = 50% less healing from potions) */ + public static final float HEALING_REDUCTION = 0.5f; + + private HealthBargain() { + super( + ID, + BargainTier.EARLY_MID, + 64, // shardCost + 25, // weight + 100 // erosion + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3), + ReflectionLang.bargainDialogue(BARGAIN_ID, 4)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("accept", ReflectionLang.answerText(BARGAIN_ID, "accept"), + ReflectionLang.answerResponse(BARGAIN_ID, "accept")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "accept", 0), + ReflectionLang.answerPower(BARGAIN_ID, "accept", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "accept", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "accept", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (answer.id().equals("accept")) { + applyHealthBoost(player); + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + removeHealthBoost(player); + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("swollen", "The soul appears bloated, stretched, rejecting attempts to mend it"); + } + + public static void applyHealthBoost(Player player) { + var attribute = player.getAttribute(Attributes.MAX_HEALTH); + if (attribute != null) { + attribute.removeModifier(MODIFIER_UUID); + attribute.addPermanentModifier(new AttributeModifier( + MODIFIER_UUID, "Reflection Vitality", 10.0, AttributeModifier.Operation.ADDITION)); + } + } + + public static void removeHealthBoost(Player player) { + var attribute = player.getAttribute(Attributes.MAX_HEALTH); + if (attribute != null) { + attribute.removeModifier(MODIFIER_UUID); + } + } + + // ========================================================================= + // Static helper methods for healing integration + // ========================================================================= + + /** + * Check if a player has the Borrowed Vitality bargain active. + */ + public static boolean hasBargain(Player player) { + return ReflectionCapability.get(player) + .map(reflection -> reflection.hasBargain(ID)) + .orElse(false); + } + + /** + * Modify healing amount for a player with this bargain. + * Returns the modified healing amount. + */ + public static float modifyHealing(Player player, float originalHealing) { + if (hasBargain(player)) { + return originalHealing * HEALING_REDUCTION; + } + return originalHealing; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/HomeBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/HomeBargain.java new file mode 100644 index 000000000..f68e63489 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/HomeBargain.java @@ -0,0 +1,201 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionConstants; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +import java.util.List; +import java.util.Optional; + +/** + * The /home bargain - teleport to spawn/bed. + * + * This is the "poisoned apple" - offered early, seems cheap, but costs escalate + * with repeated quick use. The Reflection knows you'll become dependent on it. + * + * Base cost: 1 erosion (seems cheap!) + * Quick use penalty: Doubles each time used within 15 minutes (1 -> 2 -> 4 -> 8 -> 16) + * Ceiling: 16 erosion per use + * Reset: After 15 minutes of no use, resets to base cost + */ +public class HomeBargain extends Bargain { + + private static final ResourceLocation ID = CosmicCore.id("home"); + public static final HomeBargain INSTANCE = new HomeBargain(); + private static final String BARGAIN_ID = "home"; + + private HomeBargain() { + super( + ID, + BargainTier.EARLY, + 0, // shardCost - FREE (poisoned apple - per-use costs add up) + 0, // weight - FREE (doesn't consume soul capacity) + 100 // erosion - significant upfront cost + per-use escalation + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("accept", ReflectionLang.answerText(BARGAIN_ID, "accept"), + ReflectionLang.answerResponse(BARGAIN_ID, "accept")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "accept", 0), + ReflectionLang.answerPower(BARGAIN_ID, "accept", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "accept", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "accept", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (answer.id().equals("accept")) { + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("threads", "Faint silver threads extending outward, always pointing home"); + } + + @Override + public void tick(Player player) { + // No passive effects - this is an on-demand ability + } + + /** + * Execute the /home teleport. + * Called when player uses the command (if they have the bargain). + * + * @return true if teleport succeeded, false otherwise + */ + public static boolean executeHome(ServerPlayer player) { + return ReflectionCapability.get(player).map(reflection -> { + if (!reflection.hasBargain(ID)) { + player.displayClientMessage( + Component.literal("\u00A7cYou haven't made this bargain."), + false); + return false; + } + + // Calculate current cost based on usage + int cost = ReflectionConstants.getCommandCost(reflection, "home"); + + // Find home location (bed or world spawn) + Optional homePos = findHomePosition(player); + if (homePos.isEmpty()) { + player.displayClientMessage( + Component.literal("\u00A7cYou have no home to return to."), + false); + return false; + } + + Vec3 home = homePos.get(); + + // Apply erosion cost + reflection.addErosion(cost); + reflection.recordCommandUse("home"); + + // Teleport + player.teleportTo(home.x, home.y, home.z); + + // Effects + player.level().playSound(null, player.blockPosition(), + SoundEvents.ENDERMAN_TELEPORT, SoundSource.PLAYERS, 1.0f, 0.8f); + + // Feedback based on cost + if (cost <= 2) { + player.displayClientMessage( + Component.literal("\u00A77\u00A7o*You feel the familiar pull of home.*"), + true); + } else if (cost <= 8) { + player.displayClientMessage( + Component.literal("\u00A77\u00A7o*The path feels... worn. Familiar. Too familiar.*"), + true); + } else { + player.displayClientMessage( + Component.literal( + "\u00A77\u00A7o*Home. Again. Always home. Do you even remember the world outside?*"), + true); + } + + // Show cost in chat + player.displayClientMessage( + Component.literal("\u00A78[Erosion +" + cost + "]"), + false); + + return true; + }).orElse(false); + } + + private static Optional findHomePosition(ServerPlayer player) { + // Check for bed spawn first + BlockPos bedPos = player.getRespawnPosition(); + if (bedPos != null) { + ServerLevel respawnLevel = player.server.getLevel(player.getRespawnDimension()); + if (respawnLevel != null) { + // Find safe spawn near bed + Optional bedSpawn = Player.findRespawnPositionAndUseSpawnBlock( + respawnLevel, bedPos, player.getRespawnAngle(), true, false); + if (bedSpawn.isPresent()) { + return bedSpawn; + } + } + } + + // Fall back to world spawn + ServerLevel overworld = player.server.getLevel(Level.OVERWORLD); + if (overworld != null) { + BlockPos spawnPos = overworld.getSharedSpawnPos(); + return Optional.of(new Vec3(spawnPos.getX() + 0.5, spawnPos.getY(), spawnPos.getZ() + 0.5)); + } + + return Optional.empty(); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/HungerBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/HungerBargain.java new file mode 100644 index 000000000..082eb7112 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/HungerBargain.java @@ -0,0 +1,149 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.food.FoodData; + +import java.util.List; + +/** + * Hunger Bargain: Reduced hunger drain, but food doesn't restore health. + * + * POWER: Hunger depletes 75% slower - you rarely need to eat + * DRAWBACK: Food no longer triggers natural regeneration - you can't heal by eating + * + * Thematically: Your body has forgotten hunger... but also forgotten how to draw + * life from food. You can eat, but it only fills the stomach - it doesn't nourish. + * You'll need potions, golden apples, or other means to heal. + * + * This keeps hunger as a minor concern while creating an interesting healing + * limitation that changes combat strategy. + */ +public class HungerBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("satiated"); + public static final HungerBargain INSTANCE = new HungerBargain(); + private static final String BARGAIN_ID = "satiated"; + + /** How often to restore hunger (every N ticks, prevents hunger drain) */ + public static final int HUNGER_RESTORE_INTERVAL = 80; // Every 4 seconds + + private HungerBargain() { + super( + ID, + BargainTier.MID, + 256, // shardCost - premium + 50, // weight - expensive commitment + 250 // erosion + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3), + ReflectionLang.bargainDialogue(BARGAIN_ID, 4)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("empty", ReflectionLang.answerText(BARGAIN_ID, "empty"), + ReflectionLang.answerResponse(BARGAIN_ID, "empty")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "empty", 0), + ReflectionLang.answerPower(BARGAIN_ID, "empty", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "empty", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "empty", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public boolean isContextuallyRelevant(Player player, BargainContext context) { + return context.isHungry(); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (answer.id().equals("empty")) { + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public void tick(Player player) { + // Periodically restore some hunger to simulate 75% slower drain + // Instead of keeping it full, we restore 1 hunger every 4 seconds + // This means hunger still exists but drains very slowly + if (player.level().getGameTime() % HUNGER_RESTORE_INTERVAL == 0) { + FoodData food = player.getFoodData(); + if (food.getFoodLevel() < 20) { + food.setFoodLevel(Math.min(20, food.getFoodLevel() + 1)); + } + // Keep saturation moderate (but not max - allows some natural drain) + if (food.getSaturationLevel() < 2.0f) { + food.setSaturation(2.0f); + } + } + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("hollow", + "The soul's midsection is translucent, food passes through without nourishing"); + } + + // ========================================================================= + // Static helper methods for healing integration + // ========================================================================= + + /** + * Check if a player has the Hollow Satiation bargain active. + */ + public static boolean hasBargain(Player player) { + return ReflectionCapability.get(player) + .map(reflection -> reflection.hasBargain(ID)) + .orElse(false); + } + + /** + * Check if natural regeneration should be blocked for this player. + * Returns true if player has bargain and should NOT heal from food. + */ + public static boolean shouldBlockNaturalRegen(Player player) { + return hasBargain(player); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/NightVisionBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/NightVisionBargain.java new file mode 100644 index 000000000..64b349e04 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/NightVisionBargain.java @@ -0,0 +1,183 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.player.Player; + +import java.util.List; + +/** + * Night Vision Bargain: See in darkness, but bright light hurts. + * + * POWER: Permanent night vision effect + * DRAWBACK: Bright light (sky light level 15) causes blindness and minor damage + * + * Thematically: Your eyes have been remade for the dark. They see perfectly + * in shadow... but the sun is now your enemy. You've traded one limitation + * for another - now you must hide from daylight or suffer. + * + * This creates interesting gameplay - underground exploration is trivial, + * but surface travel during day requires planning or infrastructure. + */ +public class NightVisionBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("darksight"); + public static final NightVisionBargain INSTANCE = new NightVisionBargain(); + private static final String BARGAIN_ID = "darksight"; + + /** Light level at which blindness starts (bright sunlight) */ + public static final int BRIGHT_LIGHT_THRESHOLD = 14; + + /** Damage dealt per tick when in bright light (0.5 = 1 heart per second at 20 ticks/sec) */ + public static final float BRIGHT_LIGHT_DAMAGE = 0.5f; + + /** How often to apply light damage (every N ticks) */ + public static final int LIGHT_DAMAGE_INTERVAL = 20; + + private NightVisionBargain() { + super( + ID, + BargainTier.EARLY, + 256, // shardCost - premium + 25, // weight + 150 // erosion + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3), + ReflectionLang.bargainDialogue(BARGAIN_ID, 4)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("yes", ReflectionLang.answerText(BARGAIN_ID, "yes"), + ReflectionLang.answerResponse(BARGAIN_ID, "yes")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "yes", 0), + ReflectionLang.answerPower(BARGAIN_ID, "yes", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "yes", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "yes", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (!answer.id().equals("refuse")) { + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + player.removeEffect(MobEffects.NIGHT_VISION); + player.removeEffect(MobEffects.BLINDNESS); + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public void tick(Player player) { + // Always apply night vision + if (!player.hasEffect(MobEffects.NIGHT_VISION) || + player.getEffect(MobEffects.NIGHT_VISION).getDuration() < 400) { + player.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, 600, 0, true, false, false)); + } + + // Check light level - apply blindness and damage in bright light + BlockPos pos = player.blockPosition(); + int skyLight = player.level().getBrightness(net.minecraft.world.level.LightLayer.SKY, pos); + int blockLight = player.level().getBrightness(net.minecraft.world.level.LightLayer.BLOCK, pos); + int effectiveLight = Math.max(skyLight, blockLight); + + // Only sky light at max during day causes problems (not torches) + boolean inBrightSunlight = skyLight >= BRIGHT_LIGHT_THRESHOLD && + player.level().isDay() && + player.level().canSeeSky(pos); + + if (inBrightSunlight) { + // Apply blindness + if (!player.hasEffect(MobEffects.BLINDNESS) || + player.getEffect(MobEffects.BLINDNESS).getDuration() < 40) { + player.addEffect(new MobEffectInstance(MobEffects.BLINDNESS, 60, 0, true, false, true)); + } + + // Apply periodic damage + if (player.level().getGameTime() % LIGHT_DAMAGE_INTERVAL == 0) { + player.hurt(player.damageSources().magic(), BRIGHT_LIGHT_DAMAGE); + + // Occasional warning + if (player.level().getGameTime() % 100 == 0) { + player.displayClientMessage( + Component.literal("\u00A7c\u00A7o*The light burns! Seek shadow!*"), + true); + } + } + } + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("voidEyes", + "The soul's eyes are empty voids that somehow still see, flinching from light"); + } + + // ========================================================================= + // Static helper methods + // ========================================================================= + + /** + * Check if a player has the Void Sight bargain active. + */ + public static boolean hasBargain(Player player) { + return ReflectionCapability.get(player) + .map(reflection -> reflection.hasBargain(ID)) + .orElse(false); + } + + /** + * Check if a player is currently being hurt by light. + */ + public static boolean isBeingHurtByLight(Player player) { + if (!hasBargain(player)) return false; + + BlockPos pos = player.blockPosition(); + int skyLight = player.level().getBrightness(net.minecraft.world.level.LightLayer.SKY, pos); + + return skyLight >= BRIGHT_LIGHT_THRESHOLD && + player.level().isDay() && + player.level().canSeeSky(pos); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/QuakeMovementBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/QuakeMovementBargain.java new file mode 100644 index 000000000..0819bcffa --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/QuakeMovementBargain.java @@ -0,0 +1,120 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Player; + +import java.util.List; + +/** + * The First Bargain: Quake Movement + * + * Offered after the player dies a few times and realizes they're immortal. + * They're running from the realization of what they are. + * + * Power: Bunny hopping, air strafing, momentum preservation. + * Cost: Chose denial - some self-understanding paths locked. + */ +public class QuakeMovementBargain extends Bargain { + + public static final QuakeMovementBargain INSTANCE = new QuakeMovementBargain(); + private static final String BARGAIN_ID = "quake_movement"; + + private QuakeMovementBargain() { + super( + CosmicCore.id("quake_movement"), + BargainTier.EARLY, + 0, // shardCost - FREE (the hook to get players into the system) + 0, // weight - FREE (doesn't consume soul capacity) + 0 // erosion - FREE (truly no cost) + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer( + "yes", + ReflectionLang.answerText(BARGAIN_ID, "yes"), + ReflectionLang.answerResponse(BARGAIN_ID, "yes")).withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "yes", 0), + ReflectionLang.answerPower(BARGAIN_ID, "yes", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "yes", 0))), + new BargainAnswer( + "refuse", + ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")).withReducedPower()); + } + + @Override + public boolean isContextuallyRelevant(Player player, BargainContext context) { + // This bargain is always relevant for the first encounter + return true; + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + // The quake movement effect is applied via the movement handler + // This just marks the bargain as accepted + CosmicCore.LOGGER.info("Player {} accepted Quake Movement bargain with answer: {}", + player.getName().getString(), answer.id()); + } + + @Override + public void onDefy(Player player) { + CosmicCore.LOGGER.info("Player {} defied Quake Movement bargain", player.getName().getString()); + // Movement returns to normal, but they still remember running + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of( + "quake_legs", + "Legs elongated, joints bent wrong, always slightly blurred"); + } + + @Override + public List getAcceptDialogue(Player player, BargainAnswer answer) { + return List.of( + ReflectionLang.bargainAccept(BARGAIN_ID, 0), + ReflectionLang.bargainAccept(BARGAIN_ID, 1), + ReflectionLang.bargainAccept(BARGAIN_ID, 2)); + } + + @Override + public List getRefuseDialogue(Player player) { + return List.of( + ReflectionLang.bargainRefuse(BARGAIN_ID, 0), + ReflectionLang.bargainRefuse(BARGAIN_ID, 1), + ReflectionLang.bargainRefuse(BARGAIN_ID, 2)); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/QuakeMovementHandler.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/QuakeMovementHandler.java new file mode 100644 index 000000000..053d8b9de --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/QuakeMovementHandler.java @@ -0,0 +1,377 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Handles Quake-style movement for players with the Quake Movement bargain. + * + * This implements proper Quake/Source-engine style air strafing: + * - Air acceleration allows gaining speed by looking perpendicular to movement and strafing + * - Bunny hopping preserves and builds momentum + * - Ground friction is reduced to maintain speed + * + * The key insight of Quake movement is that air acceleration is applied in the + * WISH direction (where player wants to go) but only adds speed if current + * velocity in that direction is below a threshold. This creates the characteristic + * "strafe jumping" behavior. + */ +@Mod.EventBusSubscriber(modid = CosmicCore.MOD_ID) +public class QuakeMovementHandler { + + // Track if player was on ground last tick (for bhop detection) + private static final Map wasOnGround = new HashMap<>(); + private static final Map airTime = new HashMap<>(); + + // Client-side state - synced from server + private static boolean clientHasQuakeMovement = false; + + // === TUNING PARAMETERS === + // Cranked up for testing + + /** Air acceleration rate - higher = faster speed gain while strafing */ + private static final double AIR_ACCELERATE = 200.0; + + /** Max speed you can accelerate to in air (per-axis wish speed) */ + private static final double AIR_WISH_SPEED = 0.8; + + /** Speed boost when bunny hopping (multiplier) */ + private static final double BHOP_BOOST = 1.25; + + /** Maximum horizontal speed cap (blocks/tick). ~4x sprint speed */ + private static final double MAX_SPEED = 1.2; + + /** Minimum speed to trigger bhop mechanics */ + private static final double MIN_BHOP_SPEED = 0.08; + + /** How much to counteract ground friction (1.0 = no friction, 0.0 = normal friction) */ + private static final double FRICTION_COUNTER = 0.85; + + // === DEBUG MODE === + private static final boolean DEBUG_MODE = false; + + /** Vanilla sprint speed for comparison (blocks/tick) */ + private static final double VANILLA_SPRINT_SPEED = 0.28; + private static final double VANILLA_SPRINT_JUMP_SPEED = 0.36; + + // Debug state tracking + private static int lastBhopTick = 0; + private static int lastStrafeTick = 0; + private static double sessionMaxSpeed = 0; + + /** + * Called from network packet to set client-side state. + */ + @OnlyIn(Dist.CLIENT) + public static void setClientHasQuakeMovement(boolean has) { + clientHasQuakeMovement = has; + CosmicCore.LOGGER.info("Client quake movement set to: {}", has); + } + + @SubscribeEvent + public static void onPlayerTick(TickEvent.PlayerTickEvent event) { + // Run at START of tick so our velocity changes happen before Minecraft processes movement + if (event.phase != TickEvent.Phase.START) return; + + Player player = event.player; + + // Only run on client side - server movement causes rubber-banding + if (!player.level().isClientSide()) return; + + // Check client-side state + if (!clientHasQuakeMovement) return; + + // Only apply to the local player + if (player != Minecraft.getInstance().player) return; + + // Don't apply in fluids (water, lava) - prevents mach 11 swimming + if (player.isInWater() || player.isInLava() || player.isInFluidType((fluidType, height) -> height > 0.0)) + return; + + UUID uuid = player.getUUID(); + boolean onGround = player.onGround(); + boolean wasGrounded = wasOnGround.getOrDefault(uuid, true); + + Vec3 motion = player.getDeltaMovement(); + double horizontalSpeed = getHorizontalSpeed(motion); + + // Track air time + if (!onGround) { + airTime.merge(uuid, 1, Integer::sum); + } else { + airTime.put(uuid, 0); + } + + // === BUNNY HOP: Just landed while moving fast === + boolean didBhop = false; + if (onGround && !wasGrounded && horizontalSpeed > MIN_BHOP_SPEED) { + double oldSpeed = horizontalSpeed; + motion = applyBunnyHop(player, motion, horizontalSpeed); + player.setDeltaMovement(motion); + double newSpeed = getHorizontalSpeed(motion); + horizontalSpeed = newSpeed; + didBhop = newSpeed > oldSpeed; + + if (DEBUG_MODE && didBhop) { + lastBhopTick = player.tickCount; + } + } + + // === AIR STRAFING: In the air with movement input === + boolean didStrafe = false; + if (!onGround) { + double oldSpeed = getHorizontalSpeed(motion); + motion = applyAirAcceleration(player, motion); + player.setDeltaMovement(motion); + double newSpeed = getHorizontalSpeed(motion); + didStrafe = newSpeed > oldSpeed + 0.005; + + if (DEBUG_MODE && didStrafe) { + lastStrafeTick = player.tickCount; + } + } + + // === GROUND MOVEMENT: Reduce friction to preserve speed === + if (onGround && horizontalSpeed > MIN_BHOP_SPEED) { + motion = reduceGroundFriction(player, motion, horizontalSpeed); + player.setDeltaMovement(motion); + } + + // === DEBUG: Unified display === + if (DEBUG_MODE) { + double speed = getHorizontalSpeed(player.getDeltaMovement()); + if (speed > sessionMaxSpeed) { + sessionMaxSpeed = speed; + } + + // Build status line + StringBuilder status = new StringBuilder(); + + // Show speed with color + ChatFormatting speedColor = getSpeedColor(speed); + String speedMultiple = String.format("%.1fx", speed / VANILLA_SPRINT_SPEED); + + // Recent events (within last 20 ticks) + boolean recentBhop = (player.tickCount - lastBhopTick) < 20; + boolean recentStrafe = (player.tickCount - lastStrafeTick) < 10; + + if (recentBhop) { + status.append("§a[BHOP!] "); + } + if (recentStrafe && !onGround) { + status.append("§b[STRAFE] "); + } + + status.append(speedColor.toString()); + status.append(String.format("%.2f b/t (%s)", speed, speedMultiple)); + + // Show max speed achieved + if (sessionMaxSpeed > VANILLA_SPRINT_JUMP_SPEED) { + status.append(String.format(" §7| Max: §d%.2f", sessionMaxSpeed)); + } + + // Ground state indicator + status.append(onGround ? " §7[G]" : " §e[AIR]"); + + // Input indicator + float fwd = player.zza; + float strafe = player.xxa; + if (Math.abs(fwd) > 0.01 || Math.abs(strafe) > 0.01) { + status.append(String.format(" §8[%.1f/%.1f]", fwd, strafe)); + } + + player.displayClientMessage(Component.literal(status.toString()), true); + } + + // Update ground state for next tick + wasOnGround.put(uuid, onGround); + } + + /** + * Check if the player has the Quake Movement bargain active (server-side check). + */ + public static boolean hasQuakeMovement(Player player) { + return ReflectionCapability.get(player) + .map(reflection -> reflection.hasBargain(QuakeMovementBargain.INSTANCE.getId())) + .orElse(false); + } + + /** + * Apply bunny hop boost when landing. + * This is the key to maintaining speed - boost slightly on each landing. + */ + private static Vec3 applyBunnyHop(Player player, Vec3 motion, double currentSpeed) { + // Boost horizontal velocity + double boostedSpeed = Math.min(currentSpeed * BHOP_BOOST, MAX_SPEED); + + if (boostedSpeed > currentSpeed && currentSpeed > 0) { + double scale = boostedSpeed / currentSpeed; + return new Vec3(motion.x * scale, motion.y, motion.z * scale); + } + + return motion; + } + + /** + * Apply Quake-style air acceleration. + * This is where the magic happens - allows gaining speed by strafing. + */ + private static Vec3 applyAirAcceleration(Player player, Vec3 motion) { + // Get player's input direction + float forward = player.zza; // Forward/backward input (-1 to 1) + float strafe = player.xxa; // Left/right input (-1 to 1) + + // No input = no acceleration + if (Math.abs(forward) < 0.01 && Math.abs(strafe) < 0.01) { + return motion; + } + + // Get player's facing direction (horizontal only) + float yaw = player.getYRot() * ((float) Math.PI / 180f); + + // Calculate wish direction based on input + // Forward: -sin(yaw), cos(yaw) + // Right (strafe positive = right): cos(yaw), sin(yaw) + double wishX = -Math.sin(yaw) * forward + Math.cos(yaw) * strafe; + double wishZ = Math.cos(yaw) * forward + Math.sin(yaw) * strafe; + + // Normalize wish direction + double wishLength = Math.sqrt(wishX * wishX + wishZ * wishZ); + if (wishLength > 0.01) { + wishX /= wishLength; + wishZ /= wishLength; + } else { + return motion; + } + + // Current velocity in wish direction (dot product) + double currentWishSpeed = motion.x * wishX + motion.z * wishZ; + + // Calculate how much we can add + double addSpeed = AIR_WISH_SPEED - currentWishSpeed; + + if (addSpeed <= 0) { + return motion; // Already going fast enough in wish direction + } + + // Calculate acceleration (scaled by tick time ~0.05) + double accelSpeed = AIR_ACCELERATE * 0.05 * AIR_WISH_SPEED; + + // Cap the acceleration + if (accelSpeed > addSpeed) { + accelSpeed = addSpeed; + } + + // Apply acceleration in wish direction + double newX = motion.x + accelSpeed * wishX; + double newZ = motion.z + accelSpeed * wishZ; + + // Cap max speed + double newSpeed = Math.sqrt(newX * newX + newZ * newZ); + if (newSpeed > MAX_SPEED) { + double scale = MAX_SPEED / newSpeed; + newX *= scale; + newZ *= scale; + } + + return new Vec3(newX, motion.y, newZ); + } + + /** + * Reduce ground friction to help maintain speed. + * Minecraft's default friction is very high - we counteract it. + */ + private static Vec3 reduceGroundFriction(Player player, Vec3 motion, double currentSpeed) { + // Minecraft applies friction as: velocity *= 0.91 * slipperiness (roughly 0.6 on grass) + // So velocity gets multiplied by ~0.546 each tick on ground + // We want to counteract most of that + + // Calculate boost to counteract friction + // Normal friction: v *= 0.546 + // We want: v *= 0.546 * boost = ~0.9 or higher + double boost = 1.0 + FRICTION_COUNTER; + + double newX = motion.x * boost; + double newZ = motion.z * boost; + + double newSpeed = Math.sqrt(newX * newX + newZ * newZ); + + // Cap at max speed + if (newSpeed > MAX_SPEED) { + double scale = MAX_SPEED / newSpeed; + newX *= scale; + newZ *= scale; + } + + // Don't go faster than we were (unless bhop just happened) + if (newSpeed > currentSpeed * 1.01) { + double scale = currentSpeed / newSpeed; + newX *= scale; + newZ *= scale; + } + + return new Vec3(newX, motion.y, newZ); + } + + private static double getHorizontalSpeed(Vec3 motion) { + return Math.sqrt(motion.x * motion.x + motion.z * motion.z); + } + + /** + * Clean up player data when they log out. + */ + public static void removePlayer(UUID uuid) { + wasOnGround.remove(uuid); + airTime.remove(uuid); + } + + // === DEBUG HELPERS === + + @OnlyIn(Dist.CLIENT) + private static void sendDebugMessage(Player player, ChatFormatting color, String format, Object... args) { + if (!DEBUG_MODE) return; + String msg = String.format(format, args); + player.displayClientMessage( + Component.literal("[QUAKE] ").withStyle(ChatFormatting.GOLD) + .append(Component.literal(msg).withStyle(color)), + true // Action bar + ); + } + + private static ChatFormatting getSpeedColor(double speed) { + if (speed >= 1.0) return ChatFormatting.LIGHT_PURPLE; // Insane + if (speed >= 0.6) return ChatFormatting.RED; // Very fast + if (speed >= VANILLA_SPRINT_JUMP_SPEED) return ChatFormatting.YELLOW; // Above sprint-jump + if (speed >= VANILLA_SPRINT_SPEED) return ChatFormatting.GREEN; // Above sprint + return ChatFormatting.GRAY; // Normal + } + + private static String getSpeedComparison(double speed) { + double sprintMultiple = speed / VANILLA_SPRINT_SPEED; + if (speed < 0.05) { + return "(standing)"; + } else if (speed < VANILLA_SPRINT_SPEED) { + return String.format("(%.0f%% of sprint)", sprintMultiple * 100); + } else if (speed < VANILLA_SPRINT_JUMP_SPEED) { + return String.format("(%.1fx sprint)", sprintMultiple); + } else { + return String.format("(%.1fx sprint, %.1fx sprint-jump)", + sprintMultiple, speed / VANILLA_SPRINT_JUMP_SPEED); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/ReachBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/ReachBargain.java new file mode 100644 index 000000000..e0d64f6e0 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/ReachBargain.java @@ -0,0 +1,125 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.common.ForgeMod; + +import java.util.List; +import java.util.UUID; + +/** + * Reach Bargain: +2 block reach for mining/attacking + * + * Your arms stretch. Just a little. Just enough. + * Very useful QoL for building and combat. + */ +public class ReachBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("reach"); + public static final ReachBargain INSTANCE = new ReachBargain(); + private static final String BARGAIN_ID = "reach"; + private static final UUID REACH_MODIFIER_UUID = UUID.fromString("d1f8e907-4567-7890-bcde-f01234567890"); + private static final UUID ATTACK_MODIFIER_UUID = UUID.fromString("d1f8e907-4567-7890-bcde-f01234567891"); + + private ReachBargain() { + super( + ID, + BargainTier.MID, + 64, // shardCost + 25, // weight + 100 // erosion + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("further", ReflectionLang.answerText(BARGAIN_ID, "further"), + ReflectionLang.answerResponse(BARGAIN_ID, "further")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "further", 0), + ReflectionLang.answerPower(BARGAIN_ID, "further", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "further", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "further", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (!answer.id().equals("refuse")) { + applyReachBoost(player); + } + } + + @Override + public void onDefy(Player player) { + removeReachBoost(player); + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("longArms", "The soul's arms extend slightly too far, joints in wrong places"); + } + + public static void applyReachBoost(Player player) { + var reachAttr = player.getAttribute(ForgeMod.BLOCK_REACH.get()); + if (reachAttr != null) { + reachAttr.removeModifier(REACH_MODIFIER_UUID); + reachAttr.addPermanentModifier(new AttributeModifier( + REACH_MODIFIER_UUID, "Reflection Reach", 2.0, AttributeModifier.Operation.ADDITION)); + } + + var attackAttr = player.getAttribute(ForgeMod.ENTITY_REACH.get()); + if (attackAttr != null) { + attackAttr.removeModifier(ATTACK_MODIFIER_UUID); + attackAttr.addPermanentModifier(new AttributeModifier( + ATTACK_MODIFIER_UUID, "Reflection Attack Reach", 2.0, AttributeModifier.Operation.ADDITION)); + } + } + + public static void removeReachBoost(Player player) { + var reachAttr = player.getAttribute(ForgeMod.BLOCK_REACH.get()); + if (reachAttr != null) { + reachAttr.removeModifier(REACH_MODIFIER_UUID); + } + + var attackAttr = player.getAttribute(ForgeMod.ENTITY_REACH.get()); + if (attackAttr != null) { + attackAttr.removeModifier(ATTACK_MODIFIER_UUID); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/StepAssistBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/StepAssistBargain.java new file mode 100644 index 000000000..809b7bb47 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/StepAssistBargain.java @@ -0,0 +1,111 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.common.ForgeMod; + +import java.util.List; +import java.util.UUID; + +/** + * Step Assist Bargain: Auto-step up 1.5 blocks + * + * Small obstacles no longer impede you. + * A minor convenience with a minor cost. + */ +public class StepAssistBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("stride"); + public static final StepAssistBargain INSTANCE = new StepAssistBargain(); + private static final String BARGAIN_ID = "stride"; + private static final UUID MODIFIER_UUID = UUID.fromString("f3b0c1d9-6789-9012-def0-123456789012"); + + private StepAssistBargain() { + super( + ID, + BargainTier.EARLY, + 16, // shardCost - cheap starter + 10, // weight + 50 // erosion + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("accept", ReflectionLang.answerText(BARGAIN_ID, "accept"), + ReflectionLang.answerResponse(BARGAIN_ID, "accept")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "accept", 0), + ReflectionLang.answerPower(BARGAIN_ID, "accept", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "accept", 0))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (!answer.id().equals("refuse")) { + applyStepAssist(player); + } + } + + @Override + public void onDefy(Player player) { + removeStepAssist(player); + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("tallLegs", "The soul's legs appear slightly elongated"); + } + + public static void applyStepAssist(Player player) { + var attribute = player.getAttribute(ForgeMod.STEP_HEIGHT_ADDITION.get()); + if (attribute != null) { + attribute.removeModifier(MODIFIER_UUID); + attribute.addPermanentModifier(new AttributeModifier( + MODIFIER_UUID, "Reflection Stride", 1.0, AttributeModifier.Operation.ADDITION)); + } + } + + public static void removeStepAssist(Player player) { + var attribute = player.getAttribute(ForgeMod.STEP_HEIGHT_ADDITION.get()); + if (attribute != null) { + attribute.removeModifier(MODIFIER_UUID); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/StrengthBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/StrengthBargain.java new file mode 100644 index 000000000..bacdea037 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/StrengthBargain.java @@ -0,0 +1,159 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; + +import java.util.List; +import java.util.UUID; + +/** + * Strength Bargain: More damage dealt, but more damage taken from mobs. + * + * POWER: +4 attack damage + * DRAWBACK: Take 25% more damage from hostile mobs + * + * Thematically: Violence begets violence. Your strikes carry borrowed fury... + * but that fury makes you a magnet for aggression. Mobs sense the violence + * in you and strike harder, as if responding to a challenge. + * + * This creates glass-cannon gameplay - you hit harder but can't afford to + * get hit. Rewards skilled combat but punishes mistakes. + */ +public class StrengthBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("violence"); + public static final StrengthBargain INSTANCE = new StrengthBargain(); + private static final String BARGAIN_ID = "violence"; + private static final UUID MODIFIER_UUID = UUID.fromString("b9d6c7e5-2345-5678-9abc-def012345678"); + + /** Damage increase from mobs (1.25 = 25% more damage taken) */ + public static final float MOB_DAMAGE_MULTIPLIER = 1.25f; + + private StrengthBargain() { + super( + ID, + BargainTier.EARLY_MID, + 64, // shardCost + 25, // weight + 100 // erosion + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3), + ReflectionLang.bargainDialogue(BARGAIN_ID, 4)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("accept", ReflectionLang.answerText(BARGAIN_ID, "accept"), + ReflectionLang.answerResponse(BARGAIN_ID, "accept")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "accept", 0), + ReflectionLang.answerPower(BARGAIN_ID, "accept", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "accept", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "accept", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (!answer.id().equals("refuse")) { + applyStrengthBoost(player); + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + removeStrengthBoost(player); + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("claws", "The soul's hands end in dark, sharp points, surrounded by hostile energy"); + } + + public static void applyStrengthBoost(Player player) { + var attribute = player.getAttribute(Attributes.ATTACK_DAMAGE); + if (attribute != null) { + attribute.removeModifier(MODIFIER_UUID); + attribute.addPermanentModifier(new AttributeModifier( + MODIFIER_UUID, "Reflection Violence", 4.0, AttributeModifier.Operation.ADDITION)); + } + } + + public static void removeStrengthBoost(Player player) { + var attribute = player.getAttribute(Attributes.ATTACK_DAMAGE); + if (attribute != null) { + attribute.removeModifier(MODIFIER_UUID); + } + } + + // ========================================================================= + // Static helper methods for damage integration + // ========================================================================= + + /** + * Check if a player has the Inherited Violence bargain active. + */ + public static boolean hasBargain(Player player) { + return ReflectionCapability.get(player) + .map(reflection -> reflection.hasBargain(ID)) + .orElse(false); + } + + /** + * Modify damage from mobs for a player with this bargain. + * Returns the modified damage amount. + */ + public static float modifyMobDamage(Player player, float originalDamage, DamageSource source) { + if (hasBargain(player) && isMobDamage(source)) { + return originalDamage * MOB_DAMAGE_MULTIPLIER; + } + return originalDamage; + } + + /** + * Check if a damage source is from a hostile mob. + */ + public static boolean isMobDamage(DamageSource source) { + return source.getEntity() instanceof Monster; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/SwiftnessBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/SwiftnessBargain.java new file mode 100644 index 000000000..029c779c8 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/SwiftnessBargain.java @@ -0,0 +1,110 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.player.Player; + +import java.util.List; +import java.util.UUID; + +/** + * Swiftness Bargain: +20% movement speed + * + * A minor stat boost for those who want to move faster. + * Less dramatic than Quake Movement, but always active. + */ +public class SwiftnessBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("swiftness"); + public static final SwiftnessBargain INSTANCE = new SwiftnessBargain(); + private static final String BARGAIN_ID = "swiftness"; + private static final UUID MODIFIER_UUID = UUID.fromString("c0e7d8f6-3456-6789-abcd-ef0123456789"); + + private SwiftnessBargain() { + super( + ID, + BargainTier.EARLY, + 16, // shardCost - cheap starter + 10, // weight + 50 // erosion + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("accept", ReflectionLang.answerText(BARGAIN_ID, "accept"), + ReflectionLang.answerResponse(BARGAIN_ID, "accept")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "accept", 0), + ReflectionLang.answerPower(BARGAIN_ID, "accept", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "accept", 0))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (!answer.id().equals("refuse")) { + applySpeedBoost(player); + } + } + + @Override + public void onDefy(Player player) { + removeSpeedBoost(player); + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("blur", "The soul's legs appear slightly blurred, always in motion"); + } + + public static void applySpeedBoost(Player player) { + var attribute = player.getAttribute(Attributes.MOVEMENT_SPEED); + if (attribute != null) { + attribute.removeModifier(MODIFIER_UUID); + attribute.addPermanentModifier(new AttributeModifier( + MODIFIER_UUID, "Reflection Swiftness", 0.2, AttributeModifier.Operation.MULTIPLY_TOTAL)); + } + } + + public static void removeSpeedBoost(Player player) { + var attribute = player.getAttribute(Attributes.MOVEMENT_SPEED); + if (attribute != null) { + attribute.removeModifier(MODIFIER_UUID); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/VoidResistanceBargain.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/VoidResistanceBargain.java new file mode 100644 index 000000000..87092a2b6 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/bargain/impl/VoidResistanceBargain.java @@ -0,0 +1,192 @@ +package com.ghostipedia.cosmiccore.common.reflection.bargain.impl; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Void Resistance Bargain: Survive falling into the void, but each save costs more. + * + * POWER: Teleport back to surface when falling into the void + * DRAWBACK: Each void save costs escalating erosion (10 -> 20 -> 40 -> 80...) + * + * Thematically: The void remembers each time you escape. Each rescue costs more + * of your soul. Eventually, the price becomes unbearable - and you must choose + * whether to pay it or accept oblivion. + * + * This creates tension around void exploration - you're safe, but not free. + * The escalating cost means reckless void-diving has consequences. + */ +public class VoidResistanceBargain extends Bargain { + + public static final ResourceLocation ID = CosmicCore.id("void_anchor"); + public static final VoidResistanceBargain INSTANCE = new VoidResistanceBargain(); + private static final String BARGAIN_ID = "void_anchor"; + + /** Base erosion cost for first void save */ + public static final int BASE_VOID_SAVE_COST = 10; + + /** Maximum cost cap (prevents infinite escalation) */ + public static final int MAX_VOID_SAVE_COST = 160; + + /** Track void save count per player for escalating costs */ + private static final Map voidSaveCount = new ConcurrentHashMap<>(); + + private VoidResistanceBargain() { + super( + ID, + BargainTier.LATE, + 0, // shardCost - FREE in shards, but massive commitment + 75, // weight - takes up most of your soul capacity + 500 // erosion - huge transformation + ); + } + + @Override + public Component getName() { + return ReflectionLang.bargainName(BARGAIN_ID); + } + + @Override + public Component getDescription() { + return ReflectionLang.bargainDescription(BARGAIN_ID); + } + + @Override + public List getOfferDialogue(Player player) { + return List.of( + ReflectionLang.bargainDialogue(BARGAIN_ID, 0), + ReflectionLang.bargainDialogue(BARGAIN_ID, 1), + ReflectionLang.bargainDialogue(BARGAIN_ID, 2), + ReflectionLang.bargainDialogue(BARGAIN_ID, 3), + ReflectionLang.bargainDialogue(BARGAIN_ID, 4), + ReflectionLang.bargainDialogue(BARGAIN_ID, 5)); + } + + @Override + public Component getQuestion() { + return ReflectionLang.bargainQuestion(BARGAIN_ID); + } + + @Override + public List getAnswers() { + return List.of( + new BargainAnswer("anchor", ReflectionLang.answerText(BARGAIN_ID, "anchor"), + ReflectionLang.answerResponse(BARGAIN_ID, "anchor")) + .withDetails( + List.of( + ReflectionLang.answerPower(BARGAIN_ID, "anchor", 0), + ReflectionLang.answerPower(BARGAIN_ID, "anchor", 1)), + List.of( + ReflectionLang.answerDrawback(BARGAIN_ID, "anchor", 0), + ReflectionLang.answerDrawback(BARGAIN_ID, "anchor", 1))), + new BargainAnswer("refuse", ReflectionLang.answerText(BARGAIN_ID, "refuse"), + ReflectionLang.answerResponse(BARGAIN_ID, "refuse")) + .withReducedPower()); + } + + @Override + public void onAccept(Player player, BargainAnswer answer) { + if (!answer.id().equals("refuse")) { + // Reset save count on fresh bargain acceptance + voidSaveCount.remove(player.getUUID()); + player.displayClientMessage(ReflectionLang.bargainOnAccept(BARGAIN_ID), false); + } + } + + @Override + public void onDefy(Player player) { + voidSaveCount.remove(player.getUUID()); + player.displayClientMessage(ReflectionLang.bargainOnDefy(BARGAIN_ID), false); + } + + @Override + public void tick(Player player) { + // Check if player is in the void + if (player.getY() < -128 && !player.isCreative() && !player.isSpectator()) { + if (!(player instanceof ServerPlayer serverPlayer)) return; + + // Calculate current cost + int saveCount = voidSaveCount.getOrDefault(player.getUUID(), 0); + int cost = Math.min(MAX_VOID_SAVE_COST, BASE_VOID_SAVE_COST * (1 << saveCount)); + + // Teleport back to surface + double x = player.getX(); + double z = player.getZ(); + double y = player.level().getHeight(net.minecraft.world.level.levelgen.Heightmap.Types.MOTION_BLOCKING, + (int) x, (int) z) + 1; + + player.teleportTo(x, y, z); + + // Apply erosion cost + ReflectionCapability.get(player).ifPresent(reflection -> { + reflection.addErosion(cost); + }); + + // Increment save count for next time + voidSaveCount.put(player.getUUID(), saveCount + 1); + + // Feedback - gets more ominous as cost increases + if (cost <= 20) { + player.displayClientMessage( + Component.literal( + "\u00A75\u00A7o*The void rejects you. Not yet, it whispers. [Erosion +" + cost + "]*"), + true); + } else if (cost <= 80) { + player.displayClientMessage( + Component.literal("\u00A75\u00A7o*The anchor strains. The void's grip tightens. [Erosion +" + + cost + "]*"), + true); + } else { + player.displayClientMessage( + Component.literal( + "\u00A74\u00A7o*The price is almost too high. The void is patient. It will have you eventually. [Erosion +" + + cost + "]*"), + true); + } + } + } + + @Override + public BargainVisual getSoulVisual() { + return BargainVisual.of("anchored", + "A dark chain extends from the soul downward, links multiplying with each save"); + } + + // ========================================================================= + // Static helper methods + // ========================================================================= + + /** + * Get the current void save cost for a player. + */ + public static int getCurrentCost(Player player) { + int saveCount = voidSaveCount.getOrDefault(player.getUUID(), 0); + return Math.min(MAX_VOID_SAVE_COST, BASE_VOID_SAVE_COST * (1 << saveCount)); + } + + /** + * Get the number of times a player has been saved from the void. + */ + public static int getSaveCount(Player player) { + return voidSaveCount.getOrDefault(player.getUUID(), 0); + } + + /** + * Clear save count for a player (on logout, etc.). + */ + public static void clearSaveCount(UUID playerId) { + voidSaveCount.remove(playerId); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/item/MirrorItem.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/item/MirrorItem.java new file mode 100644 index 000000000..dad9f074f --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/item/MirrorItem.java @@ -0,0 +1,83 @@ +package com.ghostipedia.cosmiccore.common.reflection.item; + +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ui.VoidUIPackets; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Rarity; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; + +import java.util.List; + +import javax.annotation.Nullable; + +/** + * The Mirror - a handheld item that lets you face your Reflection. + * + * Right-click to gaze into the void and commune with your soul. + * The Reflection will offer bargains, show your erosion state, + * and speak to you about your journey. + * + * "It's just a mirror. Why does it feel like it's looking back?" + */ +public class MirrorItem extends Item { + + public MirrorItem(Properties properties) { + super(properties.stacksTo(1).rarity(Rarity.UNCOMMON)); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) { + // Check if the reflection has awakened + boolean awakened = ReflectionCapability.get(serverPlayer) + .map(r -> r.hasAwakened()) + .orElse(false); + + if (!awakened) { + // The mirror shows nothing yet + serverPlayer.displayClientMessage( + Component.literal("§7§oYou see only yourself. Nothing stirs within."), + true); + // Play subtle sound + level.playSound(null, player.blockPosition(), + SoundEvents.GLASS_HIT, SoundSource.PLAYERS, 0.5f, 0.8f); + } else { + // Open the mirror hub UI + VoidUIPackets.sendOpenHub(serverPlayer); + + // Play ominous sound + level.playSound(null, player.blockPosition(), + SoundEvents.AMBIENT_CAVE.value(), SoundSource.PLAYERS, 0.3f, 0.5f); + } + } + + // Cooldown to prevent spam + player.getCooldowns().addCooldown(this, 20); // 1 second + + return InteractionResultHolder.sidedSuccess(stack, level.isClientSide()); + } + + @Override + public void appendHoverText(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + tooltip.add(Component.literal("§7A polished surface that reflects more than light.")); + tooltip.add(Component.literal("§8§oRight-click to gaze into the void.")); + } + + @Override + public boolean isFoil(ItemStack stack) { + // Enchanted glint when holding - looks mysterious + return true; + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/item/ShardConsumeBehavior.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/item/ShardConsumeBehavior.java new file mode 100644 index 000000000..bb79afa44 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/item/ShardConsumeBehavior.java @@ -0,0 +1,95 @@ +package com.ghostipedia.cosmiccore.common.reflection.item; + +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; + +import com.gregtechceu.gtceu.api.item.component.IAddInformation; +import com.gregtechceu.gtceu.api.item.component.IInteractionItem; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Behavior for Shards of Perpetuity - right-click to consume and add to soul shard balance. + * Used as currency for bargains in the Reflection system. + */ +public class ShardConsumeBehavior implements IInteractionItem, IAddInformation { + + private final int shardValue; + + /** + * @param shardValue How many shards this item is worth when consumed + */ + public ShardConsumeBehavior(int shardValue) { + this.shardValue = shardValue; + } + + @Override + public InteractionResultHolder use(Item item, Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + + if (level.isClientSide()) { + return InteractionResultHolder.success(stack); + } + + // Check if player has awakened (has the reflection capability active) + return ReflectionCapability.get(player).map(reflection -> { + if (!reflection.hasAwakened()) { + player.displayClientMessage( + Component + .literal( + "The shard feels... dormant. Perhaps after you've seen yourself in the mirror.") + .withStyle(ChatFormatting.GRAY, ChatFormatting.ITALIC), + true); + return InteractionResultHolder.fail(stack); + } + + // Calculate total shards to add (stack size * value) + int count = player.isCrouching() ? stack.getCount() : 1; + int totalShards = count * shardValue; + + // Add shards to balance + reflection.addShards(totalShards); + + // Consume items + stack.shrink(count); + + // Feedback + player.displayClientMessage( + Component.literal("+" + totalShards + " shards absorbed") + .withStyle(ChatFormatting.AQUA), + true); + + // Sound effect + level.playSound(null, player.blockPosition(), + SoundEvents.EXPERIENCE_ORB_PICKUP, SoundSource.PLAYERS, 0.5f, + 1.2f + (level.random.nextFloat() * 0.2f)); + + // Particle effect could be added here via packet + + return InteractionResultHolder.consume(stack); + }).orElse(InteractionResultHolder.fail(stack)); + } + + @Override + public void appendHoverText(ItemStack stack, @Nullable Level level, List tooltip, TooltipFlag flag) { + tooltip.add(Component.literal("Right-click to absorb into your soul") + .withStyle(ChatFormatting.GRAY)); + tooltip.add(Component.literal("Shift+Right-click to absorb entire stack") + .withStyle(ChatFormatting.DARK_GRAY)); + tooltip.add(Component.literal("Value: " + shardValue + " shard" + (shardValue > 1 ? "s" : "") + " each") + .withStyle(ChatFormatting.AQUA)); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/network/SyncQuakeMovementPacket.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/network/SyncQuakeMovementPacket.java new file mode 100644 index 000000000..a7f796f17 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/network/SyncQuakeMovementPacket.java @@ -0,0 +1,34 @@ +package com.ghostipedia.cosmiccore.common.reflection.network; + +import com.ghostipedia.cosmiccore.common.network.CCoreNetwork; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.QuakeMovementHandler; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +/** + * Syncs Quake movement bargain state from server to client. + */ +public class SyncQuakeMovementPacket implements CCoreNetwork.INetPacket { + + private final boolean hasQuakeMovement; + + public SyncQuakeMovementPacket(boolean hasQuakeMovement) { + this.hasQuakeMovement = hasQuakeMovement; + } + + public SyncQuakeMovementPacket(FriendlyByteBuf buffer) { + this.hasQuakeMovement = buffer.readBoolean(); + } + + @Override + public void encode(FriendlyByteBuf buffer) { + buffer.writeBoolean(hasQuakeMovement); + } + + @Override + public void execute(NetworkEvent.Context context) { + // This runs on the client + QuakeMovementHandler.setClientHasQuakeMovement(hasQuakeMovement); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ui/BargainConstellationScreen.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ui/BargainConstellationScreen.java new file mode 100644 index 000000000..8d2d5a2e6 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ui/BargainConstellationScreen.java @@ -0,0 +1,1063 @@ +package com.ghostipedia.cosmiccore.common.reflection.ui; + +import com.ghostipedia.cosmiccore.client.renderer.BackgroundRenderer; +import com.ghostipedia.cosmiccore.client.renderer.SoulAuraRenderer; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionConstants; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.BargainRegistry; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import javax.annotation.Nullable; + +/** + * A constellation-style visual browser for bargains. + * + * Bargains appear as glowing nodes orbiting the player's soul. + * - Active bargains: Bright, pulsing with their signature color + * - Available bargains: Dim, waiting to be claimed + * - Scarred bargains: Cracked, dark, forever marked + * + * Click a node to see its details in a Thaumonomicon-style panel. + * From there, accept or (if active) defy the bargain. + */ +@OnlyIn(Dist.CLIENT) +public class BargainConstellationScreen extends Screen { + + // Player state + private final int erosion; + private final Set activeBargains; + private final Set defianceScars; + + // Economy state + private int shardBalance = 0; + private int usedCapacity = 0; + private int totalCapacity = 100; + + // All bargains organized into nodes + private final List nodes = new ArrayList<>(); + + // Visual state + private float fadeAlpha = 0f; + private int totalTicks = 0; + private float soulPulse = 0f; + private float soulBreath = 0f; + + // Interaction state + @Nullable + private BargainNode hoveredNode = null; + @Nullable + private BargainNode selectedNode = null; + private float selectionAnimation = 0f; + + // Detail panel animation - track previous for interpolation + private float panelSlide = 0f; // 0 = hidden, 1 = fully visible + private float panelSlidePrev = 0f; + private float selectionAnimationPrev = 0f; + + // Action button bounds for click detection + private int actionButtonX, actionButtonY, actionButtonWidth, actionButtonHeight; + private boolean actionButtonVisible = false; + + // Particles for atmosphere + private final List stars = new ArrayList<>(); + private final Random random = new Random(); + + // Pan and zoom state + private float viewOffsetX = 0f; + private float viewOffsetY = 0f; + private float zoom = 1.0f; + private boolean isDragging = false; + private double lastDragX, lastDragY; + + // Constants + private static final int FADE_TICKS = 30; + private static final int STAR_COUNT = 50; // Restored for better atmosphere + private static final float ORBIT_BASE_RADIUS = 120f; + private static final float ORBIT_RING_SPACING = 45f; + private static final float MIN_ZOOM = 0.5f; + private static final float MAX_ZOOM = 2.0f; + + // If true, skip fade-in (already coming from dark screen) + private boolean skipFadeIn = false; + + public BargainConstellationScreen(int erosion, Set activeBargains, + Set defianceScars) { + super(ReflectionLang.ui("constellation_title")); + this.erosion = erosion; + this.activeBargains = activeBargains; + this.defianceScars = defianceScars; + } + + public static void open(int erosion, Set activeBargains, Set defianceScars, + int shardBalance, int usedCapacity, int totalCapacity) { + openFromVoid(erosion, activeBargains, defianceScars, shardBalance, usedCapacity, totalCapacity); + } + + public static void openFromVoid(int erosion, Set activeBargains, + Set defianceScars, + int shardBalance, int usedCapacity, int totalCapacity) { + BargainConstellationScreen screen = new BargainConstellationScreen(erosion, activeBargains, defianceScars); + screen.skipFadeIn = true; + screen.fadeAlpha = 1f; + screen.shardBalance = shardBalance; + screen.usedCapacity = usedCapacity; + screen.totalCapacity = totalCapacity; + Minecraft.getInstance().setScreen(screen); + } + + public void setEconomyData(int shardBalance, int usedCapacity, int totalCapacity) { + this.shardBalance = shardBalance; + this.usedCapacity = usedCapacity; + this.totalCapacity = totalCapacity; + } + + @Override + protected void init() { + super.init(); + + // Initialize star particles + stars.clear(); + for (int i = 0; i < STAR_COUNT; i++) { + stars.add(new StarParticle(width, height, random)); + } + + // Build bargain nodes + buildConstellationNodes(); + } + + private void buildConstellationNodes() { + nodes.clear(); + + List allBargains = new ArrayList<>(BargainRegistry.getAll()); + + // Organize by tier into orbital rings + // Ring 0 (closest): EARLY, ANY + // Ring 1: EARLY_MID + // Ring 2: MID + // Ring 3: LATE + // Ring 4 (farthest): EXTREME + + int centerX = width / 2; + int centerY = height / 2; + + // Count bargains per ring for spacing + int[] ringCounts = new int[5]; + int[] ringIndices = new int[5]; + + for (Bargain bargain : allBargains) { + int ring = getTierRing(bargain.getTier()); + ringCounts[ring]++; + } + + // Place each bargain + for (Bargain bargain : allBargains) { + int ring = getTierRing(bargain.getTier()); + float radius = ORBIT_BASE_RADIUS + (ring * ORBIT_RING_SPACING); + + // Distribute evenly around the ring + int count = ringCounts[ring]; + int index = ringIndices[ring]++; + + // Offset each ring slightly so they don't all align + float ringOffset = ring * 0.3f; + float angle = (float) (2 * Math.PI * index / count) + ringOffset; + + // Determine node state + BargainNode.NodeState state; + if (activeBargains.contains(bargain.getId())) { + state = BargainNode.NodeState.ACTIVE; + } else if (defianceScars.contains(bargain.getId())) { + state = BargainNode.NodeState.SCARRED; + } else { + state = BargainNode.NodeState.AVAILABLE; + } + + nodes.add(new BargainNode(bargain, centerX, centerY, radius, angle, state)); + } + } + + private int getTierRing(Bargain.BargainTier tier) { + return switch (tier) { + case EARLY, ANY -> 0; + case EARLY_MID -> 1; + case MID -> 2; + case LATE -> 3; + case EXTREME -> 4; + }; + } + + @Override + public void tick() { + super.tick(); + totalTicks++; + + // Fade in + if (fadeAlpha < 1f) { + fadeAlpha = Math.min(1f, fadeAlpha + (1f / FADE_TICKS)); + } + + // Soul animations + soulPulse += 0.08f; + soulBreath += 0.03f; + + // Note: No constellation rotation - keeps nodes stationary for better UX + + // Update stars + for (StarParticle star : stars) { + star.tick(); + if (star.isDead()) { + star.reset(width, height, random); + } + } + + // Update node animations (pulse effects only) + for (BargainNode node : nodes) { + node.tick(); + } + + // Panel slide animation - save previous for interpolation + panelSlidePrev = panelSlide; + selectionAnimationPrev = selectionAnimation; + + if (selectedNode != null) { + panelSlide = Math.min(1f, panelSlide + 0.1f); + selectionAnimation += 0.15f; + } else { + panelSlide = Math.max(0f, panelSlide - 0.15f); + } + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + // Deep space galaxy background shader + BackgroundRenderer.render(graphics.pose(), BackgroundRenderer.BackgroundType.GALAXY, fadeAlpha, width, height); + + if (fadeAlpha < 0.1f) return; + + // Render additional star particles on top of shader background + for (StarParticle star : stars) { + star.render(graphics, fadeAlpha, partialTick); + } + + // Render vignette + renderVignette(graphics); + + // Render orbital rings (faint guide lines) + renderOrbitalRings(graphics); + + // Render connecting lines between nodes + renderConnections(graphics); + + // Update and render all nodes + // Update screen positions every frame for smooth dragging + int centerX = width / 2; + int centerY = height / 2; + hoveredNode = null; + for (BargainNode node : nodes) { + node.updateScreenPosition(centerX, centerY, viewOffsetX, viewOffsetY, zoom); + boolean hovered = node.isMouseOver(mouseX, mouseY); + if (hovered) { + hoveredNode = node; + } + node.render(graphics, font, fadeAlpha, hovered, node == selectedNode, totalTicks, zoom, partialTick); + } + + // Render central soul + renderSoulOrb(graphics, partialTick); + + // Render detail panel if node selected - interpolate for smooth animation + float smoothPanelSlide = panelSlidePrev + (panelSlide - panelSlidePrev) * partialTick; + if (smoothPanelSlide > 0.01f) { + renderDetailPanel(graphics, mouseX, mouseY, smoothPanelSlide); + } + + // Render hover tooltip for non-selected nodes + if (hoveredNode != null && hoveredNode != selectedNode) { + renderNodeTooltip(graphics, hoveredNode, mouseX, mouseY); + } + + // Render erosion indicator + renderErosionIndicator(graphics); + + // Render back hint + renderBackHint(graphics); + } + + private void renderVignette(GuiGraphics graphics) { + int vignetteStrength = (int) (fadeAlpha * 150); + int bandSize = 2; // 2px bands for smooth gradients + + for (int i = 0; i < 50; i += bandSize) { + int alpha = (int) (vignetteStrength * (1f - (float) i / 50f)); + int color = (alpha << 24); + graphics.fill(0, i, width, i + bandSize, color); + graphics.fill(0, height - i - bandSize, width, height - i, color); + } + + for (int i = 0; i < 70; i += bandSize) { + int alpha = (int) (vignetteStrength * (1f - (float) i / 70f) * 0.6f); + int color = (alpha << 24); + graphics.fill(i, 0, i + bandSize, height, color); + graphics.fill(width - i - bandSize, 0, width - i, height, color); + } + } + + private void renderOrbitalRings(GuiGraphics graphics) { + int centerX = width / 2 + (int) viewOffsetX; + int centerY = height / 2 + (int) viewOffsetY; + int alpha = (int) (fadeAlpha * 25); + + for (int ring = 0; ring < 5; ring++) { + float radius = (ORBIT_BASE_RADIUS + (ring * ORBIT_RING_SPACING)) * zoom; + int color = (alpha << 24) | 0x404060; + + // Draw ring as series of dashed segments (fewer draws) + int segments = 24; // Fixed low segment count for performance + for (int i = 0; i < segments; i += 2) { // Skip every other for dashed effect + float angle = (float) (2 * Math.PI * i / segments); + int x = centerX + (int) (Math.cos(angle) * radius); + int y = centerY + (int) (Math.sin(angle) * radius); + graphics.fill(x - 1, y - 1, x + 2, y + 2, color); + } + } + } + + private void renderConnections(GuiGraphics graphics) { + // Draw faint lines from soul to each bargain (brighter for active) + int centerX = width / 2 + (int) viewOffsetX; + int centerY = height / 2 + (int) viewOffsetY; + + for (BargainNode node : nodes) { + int[] color = node.getColor(); + int alpha; + + // Different visibility based on state + switch (node.state) { + case ACTIVE -> alpha = (int) (fadeAlpha * 60); // Brightest + case AVAILABLE -> alpha = (int) (fadeAlpha * 20); // Dim + case SCARRED -> alpha = (int) (fadeAlpha * 10); // Very faint + default -> alpha = (int) (fadeAlpha * 15); + } + + int lineColor = (alpha << 24) | (color[0] << 16) | (color[1] << 8) | color[2]; + + // Line from center to node + drawLine(graphics, centerX, centerY, (int) node.screenX, (int) node.screenY, lineColor); + } + } + + private void drawLine(GuiGraphics graphics, int x1, int y1, int x2, int y2, int color) { + int dx = Math.abs(x2 - x1); + int dy = Math.abs(y2 - y1); + int steps = Math.max(dx, dy); + if (steps == 0) return; + + // Draw fewer, larger dots for performance + int dotSpacing = Math.max(8, steps / 12); // Max ~12 dots per line + for (int i = 0; i <= steps; i += dotSpacing) { + int x = x1 + (x2 - x1) * i / steps; + int y = y1 + (y2 - y1) * i / steps; + graphics.fill(x, y, x + 2, y + 2, color); + } + } + + private void renderSoulOrb(GuiGraphics graphics, float partialTick) { + int centerX = width / 2 + (int) viewOffsetX; + int centerY = height / 2 + (int) viewOffsetY; + + int[] rgb = getSoulColor(); + + // Smooth animation with partialTick for 60fps fluidity + float smoothBreath = soulBreath + (0.03f * partialTick); + float smoothPulse = soulPulse + (0.08f * partialTick); + float breath = (float) Math.sin(smoothBreath) * 0.05f + 1f; + float pulse = (float) Math.sin(smoothPulse) * 0.08f + 1f; + int baseRadius = (int) (30 * zoom); + int radius = (int) (baseRadius * breath * pulse); + + int alpha = (int) (fadeAlpha * 255); + + // Render ethereal flame aura BEHIND the soul orb + int auraRadius = (int) (baseRadius * 1.8f * zoom); + SoulAuraRenderer.render( + graphics.pose(), + centerX, centerY, + auraRadius, + erosion, + fadeAlpha * 0.8f, + width, height); + + // Outer glow - use 4px steps + for (int r = radius + 30; r > radius; r -= 4) { + float glowProgress = (float) (r - radius) / 30f; + int glowAlpha = (int) ((1f - glowProgress) * 35 * fadeAlpha); + int color = (glowAlpha << 24) | (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; + drawCircleFast(graphics, centerX, centerY, r, color); + } + + // Core - use 3px steps + for (int r = radius; r > 0; r -= 3) { + float coreProgress = (float) r / radius; + int coreAlpha = (int) (alpha * (0.6f + 0.4f * coreProgress)); + int lr = Math.min(255, rgb[0] + (int) ((255 - rgb[0]) * (1f - coreProgress) * 0.3f)); + int lg = Math.min(255, rgb[1] + (int) ((255 - rgb[1]) * (1f - coreProgress) * 0.3f)); + int lb = Math.min(255, rgb[2] + (int) ((255 - rgb[2]) * (1f - coreProgress) * 0.3f)); + int color = (coreAlpha << 24) | (lr << 16) | (lg << 8) | lb; + drawCircleFast(graphics, centerX, centerY, r, color); + } + } + + private void drawCircle(GuiGraphics graphics, int cx, int cy, int radius, int color) { + for (int y = -radius; y <= radius; y++) { + int halfWidth = (int) Math.sqrt(radius * radius - y * y); + graphics.fill(cx - halfWidth, cy + y, cx + halfWidth + 1, cy + y + 1, color); + } + } + + private void drawCircleFast(GuiGraphics graphics, int cx, int cy, int radius, int color) { + if (radius <= 0) return; + + // For small radii, use precise 1px drawing + if (radius <= 6) { + drawCircle(graphics, cx, cy, radius, color); + return; + } + + // Use 2px bands for good quality + int bandSize = 2; + + for (int y = -radius; y <= radius; y += bandSize) { + int halfWidth = (int) Math.sqrt(radius * radius - y * y); + int bandEnd = Math.min(y + bandSize, radius + 1); + graphics.fill(cx - halfWidth, cy + y, cx + halfWidth + 1, cy + bandEnd, color); + } + } + + private int[] getSoulColor() { + int tier = ReflectionConstants.getSoulColorTier(erosion); + return switch (tier) { + case 0 -> new int[] { 220, 220, 235 }; + case 1 -> new int[] { 180, 200, 255 }; + case 2 -> new int[] { 140, 120, 220 }; + case 3 -> new int[] { 180, 80, 160 }; + case 4 -> new int[] { 160, 50, 50 }; + case 5 -> new int[] { 80, 30, 30 }; + default -> new int[] { 20, 10, 30 }; + }; + } + + private void renderDetailPanel(GuiGraphics graphics, int mouseX, int mouseY, float smoothPanelSlide) { + if (selectedNode == null) return; + + Bargain bargain = selectedNode.bargain; + + // Panel dimensions - wider to fit text better + int panelWidth = 240; + int maxTextWidth = panelWidth - 20; // 10px padding on each side + + // Calculate dynamic height based on content + int contentHeight = 58; // Header space (title + tier + cost + divider) + for (Component line : bargain.getPowerDescriptions()) { + contentHeight += wrapText(line.getString(), maxTextWidth).size() * 11; + } + contentHeight += 30; // Action hint space + + int panelHeight = Math.max(180, contentHeight); + + // Slide in from right - use interpolated value for smooth animation + int panelX = width - (int) (smoothPanelSlide * (panelWidth + 20)); + int panelY = (height - panelHeight) / 2; + + int bgAlpha = (int) (fadeAlpha * smoothPanelSlide * 230); + int borderAlpha = (int) (fadeAlpha * smoothPanelSlide * 255); + + // Background + int bgColor = (bgAlpha << 24) | 0x101018; + graphics.fill(panelX, panelY, panelX + panelWidth, panelY + panelHeight, bgColor); + + // Border + int borderColor = (borderAlpha << 24) | 0x404080; + graphics.fill(panelX, panelY, panelX + panelWidth, panelY + 1, borderColor); + graphics.fill(panelX, panelY + panelHeight - 1, panelX + panelWidth, panelY + panelHeight, borderColor); + graphics.fill(panelX, panelY, panelX + 1, panelY + panelHeight, borderColor); + graphics.fill(panelX + panelWidth - 1, panelY, panelX + panelWidth, panelY + panelHeight, borderColor); + + int textAlpha = (int) (fadeAlpha * smoothPanelSlide * 255); + + // Title + String title = bargain.getDisplayName().getString(); + int[] nodeColor = selectedNode.getColor(); + int titleColor = (textAlpha << 24) | (nodeColor[0] << 16) | (nodeColor[1] << 8) | nodeColor[2]; + graphics.drawString(font, title, panelX + 10, panelY + 10, titleColor, false); + + // Tier and cost + String tierStr = bargain.getTier().name(); + String costStr = "Cost: " + bargain.getBaseCost() + " erosion"; + int subtitleColor = (textAlpha << 24) | 0x888888; + graphics.drawString(font, tierStr, panelX + 10, panelY + 24, subtitleColor, false); + graphics.drawString(font, costStr, panelX + 10, panelY + 36, subtitleColor, false); + + // Divider + int dividerColor = ((textAlpha / 2) << 24) | 0x606080; + graphics.fill(panelX + 10, panelY + 50, panelX + panelWidth - 10, panelY + 51, dividerColor); + + // Description / power with word wrapping + int descY = panelY + 58; + int descColor = (textAlpha << 24) | 0xBBBBBB; + for (Component line : bargain.getPowerDescriptions()) { + for (String wrappedLine : wrapText(line.getString(), maxTextWidth)) { + graphics.drawString(font, wrappedLine, panelX + 10, descY, descColor, false); + descY += 11; + } + } + // Note: We don't render drawbacks here since cost is already shown at top + + // Action button based on state + String actionHint; + int hintColor; + boolean isClickable = false; + switch (selectedNode.state) { + case AVAILABLE -> { + actionHint = "[Click to make bargain]"; + hintColor = 0x80FF80; + isClickable = true; + } + case ACTIVE -> { + actionHint = "[Click to defy - costs " + BargainRegistry.calculateDefianceCost(bargain) + "]"; + hintColor = 0xFF8080; + isClickable = true; + } + case SCARRED -> { + actionHint = "Forever scarred"; + hintColor = 0x555555; + isClickable = false; + } + default -> { + actionHint = ""; + hintColor = 0; + isClickable = false; + } + } + + // Store action button bounds for click detection + actionButtonX = panelX + 10; + actionButtonY = panelY + panelHeight - 28; + actionButtonWidth = font.width(actionHint) + 16; + actionButtonHeight = 20; + actionButtonVisible = isClickable && smoothPanelSlide > 0.9f; + + if (isClickable) { + // Draw button background + int btnBgAlpha = (int) (fadeAlpha * smoothPanelSlide * 60); + int btnBgColor = (btnBgAlpha << 24) | (hintColor & 0xFFFFFF); + graphics.fill(actionButtonX - 4, actionButtonY, actionButtonX + actionButtonWidth, + actionButtonY + actionButtonHeight, btnBgColor); + + // Draw button border + int btnBorderAlpha = (int) (fadeAlpha * smoothPanelSlide * 150); + int btnBorderColor = (btnBorderAlpha << 24) | (hintColor & 0xFFFFFF); + graphics.fill(actionButtonX - 4, actionButtonY, actionButtonX + actionButtonWidth, actionButtonY + 1, + btnBorderColor); + graphics.fill(actionButtonX - 4, actionButtonY + actionButtonHeight - 1, actionButtonX + actionButtonWidth, + actionButtonY + actionButtonHeight, btnBorderColor); + graphics.fill(actionButtonX - 4, actionButtonY, actionButtonX - 3, actionButtonY + actionButtonHeight, + btnBorderColor); + graphics.fill(actionButtonX + actionButtonWidth - 1, actionButtonY, actionButtonX + actionButtonWidth, + actionButtonY + actionButtonHeight, btnBorderColor); + } + + int textColor = (textAlpha << 24) | (hintColor & 0xFFFFFF); + graphics.drawString(font, actionHint, actionButtonX, actionButtonY + 6, textColor, false); + } + + private List wrapText(String text, int maxWidth) { + List lines = new ArrayList<>(); + if (text == null || text.isEmpty()) { + return lines; + } + + // Handle color codes - preserve them across lines + String colorPrefix = ""; + if (text.startsWith("\u00A7") && text.length() >= 2) { + colorPrefix = text.substring(0, 2); + } + + String[] words = text.split(" "); + StringBuilder currentLine = new StringBuilder(); + + for (String word : words) { + String testLine = currentLine.length() == 0 ? word : currentLine + " " + word; + if (font.width(testLine) <= maxWidth) { + if (currentLine.length() > 0) currentLine.append(" "); + currentLine.append(word); + } else { + if (currentLine.length() > 0) { + lines.add(currentLine.toString()); + currentLine = new StringBuilder(colorPrefix + word); + } else { + // Single word too long, just add it + lines.add(word); + } + } + } + + if (currentLine.length() > 0) { + lines.add(currentLine.toString()); + } + + return lines; + } + + private void renderNodeTooltip(GuiGraphics graphics, BargainNode node, int mouseX, int mouseY) { + String name = node.bargain.getDisplayName().getString(); + int tooltipWidth = font.width(name) + 10; + int tooltipHeight = 16; + + int x = mouseX + 10; + int y = mouseY - tooltipHeight - 5; + + // Keep on screen + if (x + tooltipWidth > width - 10) x = width - tooltipWidth - 10; + if (y < 10) y = 10; + + int bgAlpha = (int) (fadeAlpha * 200); + int bgColor = (bgAlpha << 24) | 0x101018; + graphics.fill(x - 2, y - 2, x + tooltipWidth + 2, y + tooltipHeight + 2, bgColor); + + int[] nodeColor = node.getColor(); + int textColor = (255 << 24) | (nodeColor[0] << 16) | (nodeColor[1] << 8) | nodeColor[2]; + graphics.drawString(font, name, x + 3, y + 3, textColor, false); + } + + private void renderErosionIndicator(GuiGraphics graphics) { + int alpha = (int) (fadeAlpha * 100); + int color = (alpha << 24) | 0x555555; + + String text = "Soul Erosion: " + erosion; + graphics.drawString(font, text, 15, height - 25, color, false); + + // Render economy display in top right + renderEconomyDisplay(graphics); + } + + private void renderEconomyDisplay(GuiGraphics graphics) { + int alpha = (int) (fadeAlpha * 200); + if (alpha < 20) return; + + int rightMargin = width - 15; + int topY = 15; + + // Shard balance (aqua color) + int shardColor = (alpha << 24) | 0x55FFFF; + String shardText = "\u2726 " + shardBalance; + int shardWidth = font.width(shardText); + graphics.drawString(font, shardText, rightMargin - shardWidth, topY, shardColor, false); + + // Capacity display (purple color) + int capacityColor = (alpha << 24) | 0xAA55FF; + String capacityText = usedCapacity + "/" + totalCapacity + " soul"; + int capacityWidth = font.width(capacityText); + graphics.drawString(font, capacityText, rightMargin - capacityWidth, topY + 12, capacityColor, false); + + // Capacity bar + int barWidth = 60; + int barHeight = 4; + int barX = rightMargin - barWidth; + int barY = topY + 24; + + // Background + int bgColor = (alpha << 24) | 0x222222; + graphics.fill(barX, barY, barX + barWidth, barY + barHeight, bgColor); + + // Filled portion + float fillPercent = totalCapacity > 0 ? (float) usedCapacity / totalCapacity : 0f; + int fillWidth = (int) (barWidth * fillPercent); + int fillColor = fillPercent > 0.9f ? ((alpha << 24) | 0xFF5555) : + fillPercent > 0.7f ? ((alpha << 24) | 0xFFAA55) : + ((alpha << 24) | 0xAA55FF); + if (fillWidth > 0) { + graphics.fill(barX, barY, barX + fillWidth, barY + barHeight, fillColor); + } + } + + private void renderBackHint(GuiGraphics graphics) { + int alpha = (int) (fadeAlpha * 120); + int color = (alpha << 24) | 0x888888; + + String hint = "[ESC] Back"; + graphics.drawString(font, hint, 15, 15, color, false); + + // Zoom level indicator + String zoomHint = String.format("Zoom: %.0f%%", zoom * 100); + graphics.drawString(font, zoomHint, 15, 28, color, false); + + // Control hints - positioned below economy display (which takes ~40px) + String controlHint = "Scroll: Zoom | Right-click drag: Pan"; + int controlWidth = font.width(controlHint); + graphics.drawString(font, controlHint, width - controlWidth - 15, 55, color, false); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + int mx = (int) mouseX; + int my = (int) mouseY; + + // Left click - select nodes or action buttons + if (button == 0) { + // Check if clicking on the action button in the detail panel + if (actionButtonVisible && selectedNode != null) { + if (mx >= actionButtonX - 4 && mx <= actionButtonX + actionButtonWidth && + my >= actionButtonY && my <= actionButtonY + actionButtonHeight) { + performNodeAction(selectedNode); + return true; + } + } + + // Check if clicking on a node + for (BargainNode node : nodes) { + if (node.isMouseOver(mx, my)) { + if (selectedNode == node) { + // Already selected - perform action + performNodeAction(node); + } else { + // Select this node + selectedNode = node; + selectionAnimation = 0f; + } + return true; + } + } + + // Clicking elsewhere deselects + selectedNode = null; + return true; + } + + // Middle or right click - start dragging + if (button == 1 || button == 2) { + isDragging = true; + lastDragX = mouseX; + lastDragY = mouseY; + return true; + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button == 1 || button == 2) { + isDragging = false; + return true; + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (isDragging) { + viewOffsetX += (float) (mouseX - lastDragX); + viewOffsetY += (float) (mouseY - lastDragY); + lastDragX = mouseX; + lastDragY = mouseY; + return true; + } + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double delta) { + // Zoom in/out with scroll wheel + float oldZoom = zoom; + zoom += (float) delta * 0.1f; + zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom)); + + // Adjust offset to zoom toward mouse position + if (oldZoom != zoom) { + float zoomRatio = zoom / oldZoom; + float mx = (float) mouseX - width / 2f; + float my = (float) mouseY - height / 2f; + viewOffsetX = (viewOffsetX - mx) * zoomRatio + mx; + viewOffsetY = (viewOffsetY - my) * zoomRatio + my; + } + + return true; + } + + private void performNodeAction(BargainNode node) { + switch (node.state) { + case AVAILABLE -> { + // Open bargain offer dialogue in VoidScreen with economy data + VoidScreen.openWithBargain(node.bargain, erosion, activeBargains, + shardBalance, usedCapacity, totalCapacity); + } + case ACTIVE -> { + // TODO: Implement defiance from here + // For now, go back to hub which has defiance + onClose(); + } + case SCARRED -> { + // Can't do anything with scarred bargains + } + } + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == 256) { // ESC + if (selectedNode != null) { + selectedNode = null; + return true; + } + onClose(); + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + private static class BargainNode { + + final Bargain bargain; + final int centerX, centerY; + final float orbitRadius; + final float baseAngle; + final NodeState state; + + float screenX, screenY; + float pulsePhase; + + enum NodeState { + AVAILABLE, // Can be acquired + ACTIVE, // Currently owned + SCARRED // Defied - forever marked + } + + BargainNode(Bargain bargain, int centerX, int centerY, float orbitRadius, float baseAngle, NodeState state) { + this.bargain = bargain; + this.centerX = centerX; + this.centerY = centerY; + this.orbitRadius = orbitRadius; + this.baseAngle = baseAngle; + this.state = state; + this.pulsePhase = (float) (Math.random() * Math.PI * 2); + } + + void tick() { + // Only update animation phase - position is calculated in render for smooth dragging + pulsePhase += 0.1f; + } + + void updateScreenPosition(int viewCenterX, int viewCenterY, float offsetX, float offsetY, float zoom) { + float scaledRadius = orbitRadius * zoom; + screenX = viewCenterX + offsetX + (float) (Math.cos(baseAngle) * scaledRadius); + screenY = viewCenterY + offsetY + (float) (Math.sin(baseAngle) * scaledRadius); + } + + boolean isMouseOver(int mouseX, int mouseY) { + float dx = mouseX - screenX; + float dy = mouseY - screenY; + float radius = getRadius(); + return dx * dx + dy * dy <= radius * radius * 1.5f; // Slightly generous hitbox + } + + float getRadius(float zoom) { + float baseRadius = switch (state) { + case ACTIVE -> 10f; + case AVAILABLE -> 7f; + case SCARRED -> 6f; + }; + return baseRadius * zoom; + } + + float getRadius() { + // For mouse hit detection, use a generous radius + return switch (state) { + case ACTIVE -> 12f; + case AVAILABLE -> 10f; + case SCARRED -> 8f; + }; + } + + int[] getColor() { + if (state == NodeState.SCARRED) { + return new int[] { 60, 40, 50 }; // Dark, cracked + } + + // Unique color for each bargain type + String path = bargain.getId().getPath(); + return switch (path) { + // EARLY tier - cool/inviting colors + case "quake_movement" -> new int[] { 100, 200, 255 }; // Cyan - movement + case "stride" -> new int[] { 120, 220, 180 }; // Seafoam - step assist + case "darksight" -> new int[] { 160, 120, 255 }; // Violet - night vision + case "swiftness" -> new int[] { 255, 200, 100 }; // Amber - speed + + // EARLY_MID tier - warmer colors + case "home" -> new int[] { 255, 220, 100 }; // Gold - hearth + case "back" -> new int[] { 180, 100, 220 }; // Purple - death echo + case "vitality" -> new int[] { 255, 120, 120 }; // Coral - health + case "violence" -> new int[] { 220, 80, 80 }; // Crimson - strength + case "depths" -> new int[] { 80, 180, 220 }; // Ocean blue - water breathing + + // MID tier - more intense colors + case "reach" -> new int[] { 200, 160, 255 }; // Lavender - elongated grasp + case "soft_landing" -> new int[] { 180, 255, 180 }; // Mint - fall immunity + case "satiated" -> new int[] { 200, 180, 120 }; // Tan - no hunger + case "carapace" -> new int[] { 160, 160, 180 }; // Steel - armor + case "cinder" -> new int[] { 255, 140, 60 }; // Flame orange - fire immunity + + // LATE tier - darker/ominous + case "void_anchor" -> new int[] { 120, 60, 180 }; // Deep purple - void resistance + + // EXTREME tier - dangerous red/black + case "ascension" -> new int[] { 255, 255, 220 }; // Pale gold - flight (heavenly) + + default -> { + // Hash-based unique color as ultimate fallback + int hash = path.hashCode(); + int r = 100 + Math.abs(hash % 100); + int g = 100 + Math.abs((hash >> 8) % 100); + int b = 100 + Math.abs((hash >> 16) % 100); + yield new int[] { r, g, b }; + } + }; + } + + void render(GuiGraphics graphics, net.minecraft.client.gui.Font font, float fadeAlpha, + boolean hovered, boolean selected, int ticks, float zoom, float partialTick) { + int[] rgb = getColor(); + float radius = getRadius(zoom); + + // Pulse effect for active/hovered - smooth with partialTick + float pulse = 1f; + if (state == NodeState.ACTIVE || hovered || selected) { + float smoothPulse = pulsePhase + (0.1f * partialTick); + pulse = 1f + (float) Math.sin(smoothPulse) * 0.15f; + } + int renderRadius = (int) (radius * pulse); + + // Alpha based on state + float stateAlpha = switch (state) { + case ACTIVE -> 1f; + case AVAILABLE -> hovered ? 0.9f : 0.5f; + case SCARRED -> 0.3f; + }; + + int alpha = (int) (fadeAlpha * stateAlpha * 255); + + // Outer glow - only 3 bands instead of 8 for performance + if (state == NodeState.ACTIVE || hovered || selected) { + for (int r = renderRadius + 8; r > renderRadius; r -= 3) { + float glowProgress = (float) (r - renderRadius) / 8f; + int glowAlpha = (int) ((1f - glowProgress) * 60 * fadeAlpha * stateAlpha); + int color = (glowAlpha << 24) | (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; + drawNodeCircle(graphics, (int) screenX, (int) screenY, r, color); + } + } + + // Core + int coreColor = (alpha << 24) | (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; + drawNodeCircle(graphics, (int) screenX, (int) screenY, renderRadius, coreColor); + + // Selection ring - only 8 points instead of 12, smooth with partialTick + if (selected) { + int ringAlpha = (int) (fadeAlpha * 200); + int ringColor = (ringAlpha << 24) | 0xFFFFFF; + float smoothTicks = ticks + partialTick; + for (int i = 0; i < 8; i++) { + float angle = (float) (2 * Math.PI * i / 8) + smoothTicks * 0.05f; + int rx = (int) (screenX + Math.cos(angle) * (renderRadius + 5)); + int ry = (int) (screenY + Math.sin(angle) * (renderRadius + 5)); + graphics.fill(rx, ry, rx + 2, ry + 2, ringColor); + } + } + + // Scar cracks for defied bargains + if (state == NodeState.SCARRED) { + int crackAlpha = (int) (fadeAlpha * 150); + int crackColor = (crackAlpha << 24) | 0x200010; + Random crackRandom = new Random(bargain.getId().hashCode()); + for (int i = 0; i < 3; i++) { + float crackAngle = crackRandom.nextFloat() * (float) Math.PI * 2; + int cx = (int) (screenX + Math.cos(crackAngle) * radius * 0.5f); + int cy = (int) (screenY + Math.sin(crackAngle) * radius * 0.5f); + graphics.fill(cx - 1, cy - 1, cx + 2, cy + 2, crackColor); + } + } + } + + private void drawNodeCircle(GuiGraphics graphics, int cx, int cy, int radius, int color) { + for (int y = -radius; y <= radius; y++) { + int halfWidth = (int) Math.sqrt(radius * radius - y * y); + graphics.fill(cx - halfWidth, cy + y, cx + halfWidth + 1, cy + y + 1, color); + } + } + } + + private static class StarParticle { + + float x, y; + float twinklePhase; + float maxAlpha; + int lifetime; + int age; + + StarParticle(int screenWidth, int screenHeight, Random random) { + reset(screenWidth, screenHeight, random); + } + + void reset(int screenWidth, int screenHeight, Random random) { + x = random.nextFloat() * screenWidth; + y = random.nextFloat() * screenHeight; + twinklePhase = random.nextFloat() * (float) Math.PI * 2; + maxAlpha = 0.2f + random.nextFloat() * 0.4f; + lifetime = 200 + random.nextInt(300); + age = 0; + } + + void tick() { + age++; + twinklePhase += 0.08f; + } + + boolean isDead() { + return age >= lifetime; + } + + void render(GuiGraphics graphics, float screenAlpha, float partialTick) { + // Smooth twinkle with partialTick for 60fps animation + float smoothTwinkle = twinklePhase + (0.08f * partialTick); + float twinkle = (float) (Math.sin(smoothTwinkle) * 0.3f + 0.7f); + + // Fade in/out + float lifeFade = 1f; + float progress = (float) age / lifetime; + if (progress < 0.1f) lifeFade = progress / 0.1f; + else if (progress > 0.9f) lifeFade = (1f - progress) / 0.1f; + + int alpha = (int) (maxAlpha * twinkle * lifeFade * screenAlpha * 255); + if (alpha <= 0) return; + + int color = (alpha << 24) | 0xCCCCDD; + graphics.fill((int) x, (int) y, (int) x + 1, (int) y + 1, color); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ui/VoidScreen.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ui/VoidScreen.java new file mode 100644 index 000000000..8135199c2 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ui/VoidScreen.java @@ -0,0 +1,1719 @@ +package com.ghostipedia.cosmiccore.common.reflection.ui; + +import com.ghostipedia.cosmiccore.client.renderer.BackgroundRenderer; +import com.ghostipedia.cosmiccore.client.renderer.SoulAuraRenderer; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionConstants; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionLang; +import com.ghostipedia.cosmiccore.common.reflection.ThresholdEncounter; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain.BargainAnswer; +import com.ghostipedia.cosmiccore.common.reflection.bargain.BargainRegistry; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.Set; + +import javax.annotation.Nullable; + +/** + * The Void UI - where you face your Reflection. + * + * A full-screen dark environment with: + * - Animated void background with floating particles + * - Soul orb visualization (color based on erosion, marks from bargains) + * - Dialogue text with typewriter effect and word wrapping + * - Stylized choice buttons for bargains + * - Vignette effect for atmosphere + */ +@OnlyIn(Dist.CLIENT) +public class VoidScreen extends Screen { + + // State machine + private VoidState state = VoidState.FADE_IN; + private int ticksInState = 0; + private int totalTicks = 0; + + // Mode determines what the screen does after dialogue + private VoidMode mode = VoidMode.REFLECTION; + + // Dialogue system + private final List dialogueQueue = new ArrayList<>(); + private int currentDialogueIndex = 0; + private String displayedText = ""; + private int charIndex = 0; + private int textTickCounter = 0; + + // Current bargain being offered (if any) + @Nullable + private Bargain currentBargain; + private List currentAnswers; + + // Threshold encounter mode (no bargain, just dialogue with acknowledgment) + private int thresholdIndex = -1; + private boolean isThresholdEncounter = false; + + // Hub mode - browsing bargains + private List availableBargains = new ArrayList<>(); + private List playerActiveBargains = new ArrayList<>(); + private int selectedBargainIndex = 0; + private int bargainListScrollOffset = 0; + @Nullable + private Bargain viewingBargain = null; // Currently viewing details of this bargain + + // Defiance scars for display + private Set defianceScars = Set.of(); + + // Flag to open constellation after dialogue + private boolean pendingConstellationOpen = false; + + // Player's active bargains (for soul marks) + private Set activeBargains = Set.of(); + + // Visual state + private float fadeAlpha = 0f; + private float soulPulse = 0f; + private float soulBreath = 0f; + private int erosion = 0; + + // Economy state + private int shardBalance = 0; + private int usedCapacity = 0; + private int totalCapacity = 100; + + // Particles + private final List particles = new ArrayList<>(); + private final Random random = new Random(); + + // Answer button state (custom rendering) + private final List answerButtons = new ArrayList<>(); + private int hoveredButton = -1; + + // Constants + private static final int FADE_TICKS = 40; + private static final int CHARS_PER_TICK = 2; + private static final int TICKS_BETWEEN_CHARS = 1; + private static final int MAX_LINE_WIDTH = 350; + private static final int PARTICLE_COUNT = 30; // Restored for better atmosphere + + public VoidScreen(int erosion) { + super(ReflectionLang.ui("void_title")); + this.erosion = erosion; + } + + public VoidScreen(int erosion, Set activeBargains) { + this(erosion); + this.activeBargains = activeBargains; + } + + public static void openWithBargain(Bargain bargain, int erosion) { + VoidScreen screen = new VoidScreen(erosion); + screen.currentBargain = bargain; + screen.setupBargainDialogue(); + Minecraft.getInstance().setScreen(screen); + } + + public static void openWithBargain(Bargain bargain, int erosion, Set activeBargains) { + VoidScreen screen = new VoidScreen(erosion, activeBargains); + screen.currentBargain = bargain; + screen.setupBargainDialogue(); + Minecraft.getInstance().setScreen(screen); + } + + public static void openWithBargain(Bargain bargain, int erosion, Set activeBargains, + int shardBalance, int usedCapacity, int totalCapacity) { + VoidScreen screen = new VoidScreen(erosion, activeBargains); + screen.currentBargain = bargain; + screen.shardBalance = shardBalance; + screen.usedCapacity = usedCapacity; + screen.totalCapacity = totalCapacity; + screen.setupBargainDialogue(); + Minecraft.getInstance().setScreen(screen); + } + + public static void openForReflection(int erosion) { + VoidScreen screen = new VoidScreen(erosion); + screen.setupReflectionDialogue(); + Minecraft.getInstance().setScreen(screen); + } + + public static void openForReflection(int erosion, Set activeBargains) { + VoidScreen screen = new VoidScreen(erosion, activeBargains); + screen.setupReflectionDialogue(); + Minecraft.getInstance().setScreen(screen); + } + + public static void openForReflection(int erosion, Set activeBargains, + int shardBalance, int usedCapacity, int totalCapacity) { + VoidScreen screen = new VoidScreen(erosion, activeBargains); + screen.shardBalance = shardBalance; + screen.usedCapacity = usedCapacity; + screen.totalCapacity = totalCapacity; + screen.setupReflectionDialogue(); + Minecraft.getInstance().setScreen(screen); + } + + public static void openForThreshold(int thresholdIndex, int erosion, Set activeBargains) { + VoidScreen screen = new VoidScreen(erosion, activeBargains); + screen.thresholdIndex = thresholdIndex; + screen.isThresholdEncounter = true; + screen.setupThresholdDialogue(); + Minecraft.getInstance().setScreen(screen); + } + + public static void openForHub(int erosion, Set activeBargains, + Set defianceScars) { + VoidScreen screen = new VoidScreen(erosion, activeBargains); + screen.mode = VoidMode.HUB; + screen.defianceScars = defianceScars != null ? defianceScars : Set.of(); + screen.setupHubDialogue(); + Minecraft.getInstance().setScreen(screen); + } + + public static void openForHub(int erosion, Set activeBargains, + Set defianceScars, + int shardBalance, int usedCapacity, int totalCapacity) { + VoidScreen screen = new VoidScreen(erosion, activeBargains); + screen.mode = VoidMode.HUB; + screen.defianceScars = defianceScars != null ? defianceScars : Set.of(); + screen.shardBalance = shardBalance; + screen.usedCapacity = usedCapacity; + screen.totalCapacity = totalCapacity; + screen.setupHubDialogue(); + Minecraft.getInstance().setScreen(screen); + } + + public void setEconomyData(int shardBalance, int usedCapacity, int totalCapacity) { + this.shardBalance = shardBalance; + this.usedCapacity = usedCapacity; + this.totalCapacity = totalCapacity; + } + + @Override + protected void init() { + super.init(); + // Initialize particles + particles.clear(); + for (int i = 0; i < PARTICLE_COUNT; i++) { + particles.add(new VoidParticle(width, height, random)); + } + } + + private void setupBargainDialogue() { + if (currentBargain == null) return; + + dialogueQueue.clear(); + + // Add the bargain's offer dialogue + if (minecraft != null && minecraft.player != null) { + for (Component line : currentBargain.getOfferDialogue(minecraft.player)) { + dialogueQueue.add(line.getString()); + } + } + + // Add the question + dialogueQueue.add(currentBargain.getQuestion().getString()); + + // Store answers for later + currentAnswers = currentBargain.getAnswers(); + } + + private void setupReflectionDialogue() { + dialogueQueue.clear(); + + // Contextual greeting based on erosion + int colorTier = ReflectionConstants.getSoulColorTier(erosion); + + if (erosion == 0) { + dialogueQueue.add(ReflectionLang.ui("reflection.no_erosion.0").getString()); + dialogueQueue.add(ReflectionLang.ui("reflection.no_erosion.1").getString()); + dialogueQueue.add(ReflectionLang.ui("reflection.no_erosion.2").getString()); + } else if (colorTier <= 1) { + dialogueQueue.add(ReflectionLang.ui("reflection.low_erosion.0").getString()); + dialogueQueue.add(ReflectionLang.ui("reflection.low_erosion.1").getString()); + } else if (colorTier <= 3) { + dialogueQueue.add(ReflectionLang.ui("reflection.mid_erosion.0").getString()); + dialogueQueue.add(ReflectionLang.ui("reflection.mid_erosion.1").getString()); + } else if (colorTier <= 5) { + dialogueQueue.add(ReflectionLang.ui("reflection.high_erosion.0").getString()); + dialogueQueue.add(ReflectionLang.ui("reflection.high_erosion.1").getString()); + } else { + dialogueQueue.add(ReflectionLang.ui("reflection.extreme_erosion.0").getString()); + dialogueQueue.add(ReflectionLang.ui("reflection.extreme_erosion.1").getString()); + } + + // If they have bargains, comment on them + if (!activeBargains.isEmpty()) { + dialogueQueue.add(ReflectionLang.ui("reflection.has_bargains.0").getString()); + dialogueQueue.add(ReflectionLang.ui("reflection.has_bargains.1").getString()); + } + } + + private void setupThresholdDialogue() { + dialogueQueue.clear(); + + // Get the threshold-specific dialogue + for (Component line : ThresholdEncounter.getDialogue(thresholdIndex)) { + dialogueQueue.add(line.getString()); + } + + // Add the rhetorical question/prompt + String question = ThresholdEncounter.getQuestion(thresholdIndex).getString(); + if (!question.isEmpty()) { + dialogueQueue.add(question); + } + } + + private void setupHubDialogue() { + dialogueQueue.clear(); + + // Contextual greeting based on player's state + int colorTier = ReflectionConstants.getSoulColorTier(erosion); + + if (!activeBargains.isEmpty()) { + // Has bargains - comment on them + if (colorTier >= 4) { + dialogueQueue.add(ReflectionLang.ui("hub.greeting.many_bargains_high.0").getString()); + dialogueQueue.add(ReflectionLang.ui("hub.greeting.many_bargains_high.1").getString()); + } else if (activeBargains.size() >= 3) { + dialogueQueue.add(ReflectionLang.ui("hub.greeting.many_bargains.0").getString()); + dialogueQueue.add(ReflectionLang.ui("hub.greeting.many_bargains.1").getString()); + } else { + dialogueQueue.add(ReflectionLang.ui("hub.greeting.has_bargains.0").getString()); + dialogueQueue.add(ReflectionLang.ui("hub.greeting.has_bargains.1").getString()); + } + } else if (!defianceScars.isEmpty()) { + // Has defied before - acknowledge the scars + dialogueQueue.add(ReflectionLang.ui("hub.greeting.has_scars.0").getString()); + dialogueQueue.add(ReflectionLang.ui("hub.greeting.has_scars.1").getString()); + dialogueQueue.add(ReflectionLang.ui("hub.greeting.has_scars.2").getString()); + } else if (erosion > 0) { + // Has erosion but no bargains - curious + dialogueQueue.add(ReflectionLang.ui("hub.greeting.erosion_no_bargains.0").getString()); + dialogueQueue.add(ReflectionLang.ui("hub.greeting.erosion_no_bargains.1").getString()); + dialogueQueue.add(ReflectionLang.ui("hub.greeting.erosion_no_bargains.2").getString()); + } else { + // Fresh - rare to hit hub with no erosion + dialogueQueue.add(ReflectionLang.ui("hub.greeting.fresh.0").getString()); + dialogueQueue.add(ReflectionLang.ui("hub.greeting.fresh.1").getString()); + } + + dialogueQueue.add(ReflectionLang.ui("hub.greeting.question").getString()); + } + + private void setupHubMenu() { + answerButtons.clear(); + + int buttonWidth = 280; + int buttonHeight = 28; + int startY = height / 2 + 40; + int spacing = 35; + + // Populate bargain lists for later + availableBargains = BargainRegistry.getAvailable(activeBargains, defianceScars); + playerActiveBargains = BargainRegistry.getActive(activeBargains); + + // Menu options + List menuOptions = new ArrayList<>(); + + // Option 1: View active bargains (if any) + if (!playerActiveBargains.isEmpty()) { + menuOptions.add(new BargainAnswer( + "view_active", + ReflectionLang.uiReviewBargains(playerActiveBargains.size()))); + } + + // Option 2: Browse the constellation (always show - displays all bargain states) + Component browseText = availableBargains.isEmpty() ? ReflectionLang.uiGazeConstellation() : + ReflectionLang.uiBrowseBargains(availableBargains.size()); + menuOptions.add(new BargainAnswer( + "browse_bargains", + browseText)); + + // Option 3: Just reflect (always available) + menuOptions.add(new BargainAnswer( + "just_reflect", + ReflectionLang.uiJustLook())); + + // Option 4: Leave + menuOptions.add(new BargainAnswer( + "leave", + ReflectionLang.uiLeave())); + + for (int i = 0; i < menuOptions.size(); i++) { + BargainAnswer option = menuOptions.get(i); + int x = (width - buttonWidth) / 2; + int y = startY + (i * spacing); + answerButtons.add(new AnswerButton(x, y, buttonWidth, buttonHeight, option, i)); + } + } + + private void setupBrowseBargains() { + answerButtons.clear(); + + int buttonWidth = 280; + int buttonHeight = 28; + int startY = height / 2 + 20; + int spacing = 35; + + List options = new ArrayList<>(); + + // List available bargains + for (Bargain bargain : availableBargains) { + String tierStr = getTierString(bargain.getTier()); + int cost = bargain.getBaseCost(); + + options.add(new BargainAnswer( + "view_" + bargain.getId().getPath(), + Component.literal(tierStr + bargain.getDisplayName().getString() + " \u00A78[" + cost + " " + + ReflectionLang.ui("erosion").getString() + "]"), + Optional.of(ReflectionLang.ui("browse.interesting_choice")), + false, 0, + bargain.getPowerDescriptions(), + bargain.getDrawbackDescriptions())); + } + + // Back option + options.add(new BargainAnswer( + "back", + ReflectionLang.uiBack())); + + for (int i = 0; i < options.size(); i++) { + BargainAnswer option = options.get(i); + int x = (width - buttonWidth) / 2; + int y = startY + (i * spacing); + answerButtons.add(new AnswerButton(x, y, buttonWidth, buttonHeight, option, i)); + } + } + + private void setupViewActiveBargains() { + answerButtons.clear(); + + int buttonWidth = 280; + int buttonHeight = 28; + int startY = height / 2 + 60; // Below the soul orb and orbiting marks + int spacing = 35; + + // Calculate max visible items based on screen height + int maxVisibleItems = Math.max(3, (height - startY - 80) / spacing); + + List allOptions = new ArrayList<>(); + + // List active bargains with defiance option + for (Bargain bargain : playerActiveBargains) { + int defianceCost = BargainRegistry.calculateDefianceCost(bargain); + + allOptions.add(new BargainAnswer( + "defy_" + bargain.getId().getPath(), + Component.literal("\u00A7c\u2717 " + bargain.getDisplayName().getString() + " \u00A78[" + + ReflectionLang.ui("defy").getString() + ": " + defianceCost + "]"), + Optional.of(ReflectionLang.ui("defiance.question")), + false, 0, + bargain.getPowerDescriptions(), + List.of( + ReflectionLang.uiDefianceCost(defianceCost), + ReflectionLang.ui("defiance.lose_power"), + ReflectionLang.ui("defiance.scar_remains")))); + } + + // Back option (always at the end) + allOptions.add(new BargainAnswer( + "back", + ReflectionLang.uiBack())); + + // Store total options for scroll calculation + viewActiveAllOptions = allOptions; + viewActiveMaxVisible = maxVisibleItems; + + // Clamp scroll offset + int maxScroll = Math.max(0, allOptions.size() - maxVisibleItems); + bargainListScrollOffset = Math.max(0, Math.min(bargainListScrollOffset, maxScroll)); + + // Create visible buttons with scroll offset applied + int visibleCount = Math.min(maxVisibleItems, allOptions.size() - bargainListScrollOffset); + for (int i = 0; i < visibleCount; i++) { + int optionIndex = i + bargainListScrollOffset; + BargainAnswer option = allOptions.get(optionIndex); + int x = (width - buttonWidth) / 2; + int y = startY + (i * spacing); + answerButtons.add(new AnswerButton(x, y, buttonWidth, buttonHeight, option, optionIndex)); + } + } + + // Track all options for scrolling + private List viewActiveAllOptions = new ArrayList<>(); + private int viewActiveMaxVisible = 5; + + private void setupDefianceConfirm(Bargain bargain) { + answerButtons.clear(); + + int buttonWidth = 280; + int buttonHeight = 28; + int startY = height / 2 + 60; + int spacing = 35; + + int defianceCost = BargainRegistry.calculateDefianceCost(bargain); + + // Add warning dialogue + dialogueQueue.clear(); + dialogueQueue.add(ReflectionLang.defianceWarning1(bargain.getDisplayName().getString()).getString()); + dialogueQueue.add(ReflectionLang.defianceWarning2(defianceCost).getString()); + dialogueQueue.add(ReflectionLang.defianceWarning3().getString()); + dialogueQueue.add(ReflectionLang.defianceWarning4().getString()); + currentDialogueIndex = 0; + charIndex = 0; + displayedText = ""; + + List options = new ArrayList<>(); + + // Confirm defiance - simple button, dialogue already explains consequences + options.add(new BargainAnswer( + "confirm_defiance", + ReflectionLang.defianceConfirm())); + + // Cancel + options.add(new BargainAnswer( + "cancel", + ReflectionLang.defianceCancel())); + + for (int i = 0; i < options.size(); i++) { + BargainAnswer option = options.get(i); + int x = (width - buttonWidth) / 2; + int y = startY + (i * spacing); + answerButtons.add(new AnswerButton(x, y, buttonWidth, buttonHeight, option, i)); + } + } + + private String getTierString(Bargain.BargainTier tier) { + return switch (tier) { + case EARLY -> "\u00A77"; // Gray - early game + case EARLY_MID -> "\u00A7f"; // White + case MID -> "\u00A7b"; // Aqua + case LATE -> "\u00A75"; // Purple + case EXTREME -> "\u00A74"; // Dark red - dangerous + case ANY -> "\u00A7e"; // Yellow - always available + }; + } + + @Override + public void tick() { + super.tick(); + ticksInState++; + totalTicks++; + + // Soul animations + soulPulse += 0.08f; + soulBreath += 0.03f; + + // Update particles + for (VoidParticle particle : particles) { + particle.tick(); + if (particle.isDead()) { + particle.reset(width, height, random); + } + } + + switch (state) { + case FADE_IN -> { + fadeAlpha = Math.min(1f, (float) ticksInState / FADE_TICKS); + if (ticksInState >= FADE_TICKS) { + transitionTo(VoidState.DIALOGUE); + } + } + case DIALOGUE -> { + tickDialogue(); + } + case AWAITING_CHOICE, HUB_MENU, BROWSE_BARGAINS, VIEW_ACTIVE, DEFIANCE_CONFIRM -> { + // Just wait for button interaction + } + case FADE_OUT -> { + fadeAlpha = Math.max(0f, 1f - (float) ticksInState / FADE_TICKS); + if (ticksInState >= FADE_TICKS) { + onClose(); + } + } + } + } + + private void tickDialogue() { + if (currentDialogueIndex >= dialogueQueue.size()) { + // Done with dialogue - what comes next depends on mode + + // Check for pending state transition first + if (pendingNextState != null) { + VoidState nextState = pendingNextState; + Runnable setup = pendingSetup; + pendingNextState = null; + pendingSetup = null; + transitionTo(nextState); + if (setup != null) { + setup.run(); + } + return; + } + + // Check if we should open the constellation browser + if (pendingConstellationOpen) { + pendingConstellationOpen = false; + BargainConstellationScreen.openFromVoid(erosion, activeBargains, defianceScars, + shardBalance, usedCapacity, totalCapacity); + return; + } + + if (currentBargain != null && currentAnswers != null) { + transitionTo(VoidState.AWAITING_CHOICE); + setupAnswerButtons(); + } else if (isThresholdEncounter) { + // Threshold encounter - show acknowledge button + transitionTo(VoidState.AWAITING_CHOICE); + setupThresholdAcknowledge(); + } else if (mode == VoidMode.HUB) { + // Hub mode - show menu + transitionTo(VoidState.HUB_MENU); + setupHubMenu(); + } else { + transitionTo(VoidState.FADE_OUT); + } + return; + } + + String fullText = dialogueQueue.get(currentDialogueIndex); + + textTickCounter++; + if (textTickCounter >= TICKS_BETWEEN_CHARS) { + textTickCounter = 0; + charIndex = Math.min(charIndex + CHARS_PER_TICK, fullText.length()); + displayedText = fullText.substring(0, charIndex); + } + } + + private void setupThresholdAcknowledge() { + answerButtons.clear(); + + int buttonWidth = 200; + int buttonHeight = 28; + int x = (width - buttonWidth) / 2; + int y = height / 2 + 60; + + // Single "acknowledge" button for threshold encounters + BargainAnswer acknowledge = new BargainAnswer( + "acknowledge", + ReflectionLang.uiAcknowledge()); + + answerButtons.add(new AnswerButton(x, y, buttonWidth, buttonHeight, acknowledge, 0)); + } + + private void setupAnswerButtons() { + answerButtons.clear(); + + if (currentAnswers == null) return; + + int buttonWidth = 280; + int buttonHeight = 28; + int startY = height / 2 + 60; + int spacing = 35; + + for (int i = 0; i < currentAnswers.size(); i++) { + BargainAnswer answer = currentAnswers.get(i); + int x = (width - buttonWidth) / 2; + int y = startY + (i * spacing); + + answerButtons.add(new AnswerButton(x, y, buttonWidth, buttonHeight, answer, i)); + } + } + + private void onAnswerSelected(BargainAnswer answer) { + // Handle threshold acknowledgment + if (isThresholdEncounter && answer.id().equals("acknowledge")) { + dialogueQueue.clear(); + String response = ThresholdEncounter.getAcknowledgeResponse(thresholdIndex).getString(); + if (!response.isEmpty()) { + dialogueQueue.add(response); + } + currentDialogueIndex = 0; + charIndex = 0; + displayedText = ""; + answerButtons.clear(); + isThresholdEncounter = false; + + if (response.isEmpty()) { + // Silent response, just fade out + transitionTo(VoidState.FADE_OUT); + } else { + transitionTo(VoidState.DIALOGUE); + } + return; + } + + // Handle hub menu choices + if (state == VoidState.HUB_MENU) { + handleHubMenuChoice(answer); + return; + } + + // Handle browse bargains choices + if (state == VoidState.BROWSE_BARGAINS) { + handleBrowseBargainsChoice(answer); + return; + } + + // Handle view active choices + if (state == VoidState.VIEW_ACTIVE) { + handleViewActiveChoice(answer); + return; + } + + // Handle defiance confirmation + if (state == VoidState.DEFIANCE_CONFIRM) { + handleDefianceConfirmChoice(answer); + return; + } + + if (currentBargain == null) return; + + // Send packet to server to process the choice + VoidUIPackets.sendBargainChoice(currentBargain.getId(), answer.id()); + + // Show response dialogue + dialogueQueue.clear(); + String response = answer.reflectionResponse() + .map(Component::getString) + .orElse("..."); + dialogueQueue.add(response); + currentDialogueIndex = 0; + charIndex = 0; + displayedText = ""; + + // Clear buttons and transition back to dialogue, then fade out + answerButtons.clear(); + currentBargain = null; + currentAnswers = null; + + transitionTo(VoidState.DIALOGUE); + } + + private void handleHubMenuChoice(BargainAnswer answer) { + answerButtons.clear(); + + switch (answer.id()) { + case "view_active" -> { + // Transition directly to VIEW_ACTIVE + transitionTo(VoidState.VIEW_ACTIVE); + setupViewActiveBargains(); + } + case "browse_bargains" -> { + // Open the constellation browser with economy data + BargainConstellationScreen.openFromVoid(erosion, activeBargains, defianceScars, + shardBalance, usedCapacity, totalCapacity); + } + case "just_reflect" -> { + // Just fade out + transitionTo(VoidState.FADE_OUT); + } + case "leave" -> { + transitionTo(VoidState.FADE_OUT); + } + } + } + + private void handleBrowseBargainsChoice(BargainAnswer answer) { + answerButtons.clear(); + + if (answer.id().equals("back")) { + // Return to hub menu + viewingBargain = null; + transitionTo(VoidState.HUB_MENU); + setupHubMenu(); + return; + } + + if (answer.id().startsWith("view_")) { + // View a specific bargain's details + String bargainPath = answer.id().substring(5); // Remove "view_" + for (Bargain bargain : availableBargains) { + if (bargain.getId().getPath().equals(bargainPath)) { + viewingBargain = bargain; + currentBargain = bargain; + currentAnswers = bargain.getAnswers(); + // Show the bargain's offer dialogue + setupBargainDialogue(); + transitionTo(VoidState.DIALOGUE); + return; + } + } + } + } + + private void handleViewActiveChoice(BargainAnswer answer) { + answerButtons.clear(); + + if (answer.id().equals("back")) { + viewingBargain = null; + transitionTo(VoidState.HUB_MENU); + setupHubMenu(); + return; + } + + if (answer.id().startsWith("defy_")) { + // Initiate defiance for a bargain + String bargainPath = answer.id().substring(5); + for (Bargain bargain : playerActiveBargains) { + if (bargain.getId().getPath().equals(bargainPath)) { + viewingBargain = bargain; + transitionTo(VoidState.DEFIANCE_CONFIRM); + setupDefianceConfirm(bargain); + return; + } + } + } + } + + private void handleDefianceConfirmChoice(BargainAnswer answer) { + answerButtons.clear(); + + if (answer.id().equals("cancel")) { + viewingBargain = null; + transitionTo(VoidState.VIEW_ACTIVE); + setupViewActiveBargains(); + return; + } + + if (answer.id().equals("confirm_defiance") && viewingBargain != null) { + // Send defiance packet to server + VoidUIPackets.sendDefianceChoice(viewingBargain.getId()); + + // Show dramatic response + dialogueQueue.clear(); + dialogueQueue.add("So be it."); + dialogueQueue.add("The power leaves you..."); + dialogueQueue.add("But the scar remains."); + currentDialogueIndex = 0; + charIndex = 0; + displayedText = ""; + + // Update local state + activeBargains.remove(viewingBargain.getId()); + defianceScars = new java.util.HashSet<>(defianceScars); + ((java.util.HashSet) defianceScars).add(viewingBargain.getId()); + playerActiveBargains = BargainRegistry.getActive(activeBargains); + + viewingBargain = null; + mode = VoidMode.REFLECTION; // Return to simple reflection mode + transitionTo(VoidState.DIALOGUE); + } + } + + private void showResponseThenState(String response, VoidState nextState, Runnable setupNext) { + dialogueQueue.clear(); + dialogueQueue.add(response); + currentDialogueIndex = 0; + charIndex = 0; + displayedText = ""; + + // Store what to do after dialogue + this.pendingNextState = nextState; + this.pendingSetup = setupNext; + mode = VoidMode.REFLECTION; // Temporarily switch mode so dialogue finishes normally + transitionTo(VoidState.DIALOGUE); + } + + private void showResponseThenFadeOut(String response) { + dialogueQueue.clear(); + dialogueQueue.add(response); + currentDialogueIndex = 0; + charIndex = 0; + displayedText = ""; + mode = VoidMode.REFLECTION; + transitionTo(VoidState.DIALOGUE); + } + + // Pending state for after response dialogue + @Nullable + private VoidState pendingNextState = null; + @Nullable + private Runnable pendingSetup = null; + + private void transitionTo(VoidState newState) { + state = newState; + ticksInState = 0; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + // Mystical void background shader + BackgroundRenderer.render(graphics.pose(), BackgroundRenderer.BackgroundType.VOID, fadeAlpha, width, height); + + if (fadeAlpha < 0.1f) { + return; + } + + // Render void particles (on top of shader background) + renderParticles(graphics, partialTick); + + // Render vignette + renderVignette(graphics); + + // Render soul orb in center + renderSoulOrb(graphics, partialTick); + + // Render active bargain marks around soul + renderBargainMarks(graphics, partialTick); + + // Render dialogue text + if (state == VoidState.DIALOGUE || state == VoidState.AWAITING_CHOICE || + state == VoidState.HUB_MENU || state == VoidState.DEFIANCE_CONFIRM) { + renderDialogue(graphics); + } + + // Render state-specific headers + renderStateHeader(graphics); + + // Render click to continue hint + if (state == VoidState.DIALOGUE && currentDialogueIndex < dialogueQueue.size()) { + String fullText = dialogueQueue.get(currentDialogueIndex); + if (charIndex >= fullText.length()) { + renderContinueHint(graphics); + } + } + + // Render bargain cost preview (before buttons so tooltip can cover it) + if (state == VoidState.AWAITING_CHOICE && currentBargain != null) { + renderCostPreview(graphics); + } + + // Render answer buttons for all interactive states + if (state == VoidState.AWAITING_CHOICE || state == VoidState.HUB_MENU || + state == VoidState.BROWSE_BARGAINS || state == VoidState.VIEW_ACTIVE || + state == VoidState.DEFIANCE_CONFIRM) { + renderAnswerButtons(graphics, mouseX, mouseY); + } + + // Render erosion indicator (subtle) + renderErosionIndicator(graphics); + } + + private void renderParticles(GuiGraphics graphics, float partialTick) { + for (VoidParticle particle : particles) { + particle.render(graphics, fadeAlpha); + } + } + + private void renderVignette(GuiGraphics graphics) { + // Create a vignette effect using gradient bands (2px bands for smooth gradients) + int vignetteStrength = (int) (fadeAlpha * 180); + int bandSize = 2; + + // Top and bottom gradients + for (int i = 0; i < 60; i += bandSize) { + int alpha = (int) (vignetteStrength * (1f - (float) i / 60f)); + int color = (alpha << 24); + graphics.fill(0, i, width, i + bandSize, color); + graphics.fill(0, height - i - bandSize, width, height - i, color); + } + + // Left and right gradients + for (int i = 0; i < 80; i += bandSize) { + int alpha = (int) (vignetteStrength * (1f - (float) i / 80f) * 0.7f); + int color = (alpha << 24); + graphics.fill(i, 0, i + bandSize, height, color); + graphics.fill(width - i - bandSize, 0, width - i, height, color); + } + } + + private void renderSoulOrb(GuiGraphics graphics, float partialTick) { + int centerX = width / 2; + int centerY = height / 2 - 40; + + // Calculate orb color based on erosion + int[] rgb = getSoulColor(); + + // Breathing + pulsing animation - interpolate with partialTick for smooth 60fps animation + float smoothBreath = soulBreath + (0.03f * partialTick); + float smoothPulse = soulPulse + (0.08f * partialTick); + float breath = (float) Math.sin(smoothBreath) * 0.05f + 1f; + float pulse = (float) Math.sin(smoothPulse) * 0.08f + 1f; + int baseRadius = 35; + int radius = (int) (baseRadius * breath * pulse); + + int alpha = (int) (fadeAlpha * 255); + + // Render ethereal flame aura BEHIND the soul orb + int auraRadius = (int) (baseRadius * 1.8f); // Aura is larger than the orb + SoulAuraRenderer.render( + graphics.pose(), + centerX, centerY, + auraRadius, + erosion, + fadeAlpha * 0.8f, // Slightly reduced intensity + width, height); + + // Outer glow - use 4px steps for smoother appearance + for (int r = radius + 35; r > radius; r -= 4) { + float glowProgress = (float) (r - radius) / 35f; + int glowAlpha = (int) ((1f - glowProgress) * 40 * fadeAlpha); + int color = (glowAlpha << 24) | (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; + drawCircleFast(graphics, centerX, centerY, r, color); + } + + // Core - use 3px steps for smoother appearance + for (int r = radius; r > 0; r -= 3) { + float coreProgress = (float) r / radius; + int coreAlpha = (int) (alpha * (0.6f + 0.4f * coreProgress)); + int lr = Math.min(255, rgb[0] + (int) ((255 - rgb[0]) * (1f - coreProgress) * 0.3f)); + int lg = Math.min(255, rgb[1] + (int) ((255 - rgb[1]) * (1f - coreProgress) * 0.3f)); + int lb = Math.min(255, rgb[2] + (int) ((255 - rgb[2]) * (1f - coreProgress) * 0.3f)); + int color = (coreAlpha << 24) | (lr << 16) | (lg << 8) | lb; + drawCircleFast(graphics, centerX, centerY, r, color); + } + + // Inner bright highlight + int highlightRadius = radius / 4; + int hx = centerX - radius / 3; + int hy = centerY - radius / 3; + int hAlpha = (int) (alpha * 0.4f); + int hColor = (hAlpha << 24) | 0xFFFFFF; + drawCircleFast(graphics, hx, hy, highlightRadius, hColor); + + // Erosion cracks at high levels + if (erosion >= 100) { + renderCracks(graphics, centerX, centerY, radius); + } + } + + private void renderCracks(GuiGraphics graphics, int cx, int cy, int radius) { + int crackAlpha = (int) (fadeAlpha * Math.min(200, erosion / 5)); + int crackColor = (crackAlpha << 24) | 0x200010; + + // Draw some jagged crack lines - reduced frequency + Random crackRandom = new Random(42); // Consistent cracks + int numCracks = Math.min(5, erosion / 200 + 1); // Fewer cracks + + for (int i = 0; i < numCracks; i++) { + double angle = crackRandom.nextDouble() * Math.PI * 2; + int length = radius / 2 + crackRandom.nextInt(radius / 2); + + int x1 = cx; + int y1 = cy; + + // Larger step for fewer draw calls + for (int j = 0; j < length; j += 5) { + angle += (crackRandom.nextDouble() - 0.5) * 0.5; + int x2 = x1 + (int) (Math.cos(angle) * 5); + int y2 = y1 + (int) (Math.sin(angle) * 5); + + graphics.fill(x1, y1, x2 + 2, y2 + 2, crackColor); + x1 = x2; + y1 = y2; + } + } + } + + private void renderBargainMarks(GuiGraphics graphics, float partialTick) { + if (activeBargains.isEmpty()) return; + + int centerX = width / 2; + int centerY = height / 2 - 40; + int orbitRadius = 55; + + int markIndex = 0; + for (ResourceLocation bargainId : activeBargains) { + // Position marks in orbit around the soul - smooth with partialTick + double smoothTicks = totalTicks + partialTick; + double angle = (smoothTicks * 0.02) + (markIndex * Math.PI * 2 / activeBargains.size()); + int mx = centerX + (int) (Math.cos(angle) * orbitRadius); + int my = centerY + (int) (Math.sin(angle) * orbitRadius); + + // Each bargain type has a different mark color + int[] markColor = getBargainMarkColor(bargainId); + int alpha = (int) (fadeAlpha * 200); + int color = (alpha << 24) | (markColor[0] << 16) | (markColor[1] << 8) | markColor[2]; + + // Draw small orbiting mark as a circle + drawCircle(graphics, mx, my, 4, color); + + // Trail - 2 fading points for smooth effect + for (int t = 1; t <= 2; t++) { + double trailAngle = angle - (t * 0.18); + int tx = centerX + (int) (Math.cos(trailAngle) * orbitRadius); + int ty = centerY + (int) (Math.sin(trailAngle) * orbitRadius); + int trailAlpha = (int) (alpha * (1f - t * 0.35f) * 0.5f); + int trailColor = (trailAlpha << 24) | (markColor[0] << 16) | (markColor[1] << 8) | markColor[2]; + drawCircle(graphics, tx, ty, 3 - t, trailColor); + } + + markIndex++; + } + } + + private int[] getBargainMarkColor(ResourceLocation id) { + String path = id.getPath(); + return switch (path) { + // EARLY tier - cool/inviting colors + case "quake_movement" -> new int[] { 100, 200, 255 }; // Cyan - movement + case "stride" -> new int[] { 120, 220, 180 }; // Seafoam - step assist + case "darksight" -> new int[] { 160, 120, 255 }; // Violet - night vision + case "swiftness" -> new int[] { 255, 200, 100 }; // Amber - speed + + // EARLY_MID tier - warmer colors + case "home" -> new int[] { 255, 220, 100 }; // Gold - hearth + case "back" -> new int[] { 180, 100, 220 }; // Purple - death echo + case "vitality" -> new int[] { 255, 120, 120 }; // Coral - health + case "violence" -> new int[] { 220, 80, 80 }; // Crimson - strength + case "depths" -> new int[] { 80, 180, 220 }; // Ocean blue - water breathing + + // MID tier - more intense colors + case "reach" -> new int[] { 200, 160, 255 }; // Lavender - elongated grasp + case "soft_landing" -> new int[] { 180, 255, 180 }; // Mint - fall immunity + case "satiated" -> new int[] { 200, 180, 120 }; // Tan - no hunger + case "carapace" -> new int[] { 160, 160, 180 }; // Steel - armor + case "cinder" -> new int[] { 255, 140, 60 }; // Flame orange - fire immunity + + // LATE tier - darker/ominous + case "void_anchor" -> new int[] { 120, 60, 180 }; // Deep purple - void resistance + + // EXTREME tier - dangerous red/black + case "ascension" -> new int[] { 255, 255, 220 }; // Pale gold - flight (heavenly) + + default -> { + // Hash-based unique color as fallback + int hash = path.hashCode(); + int r = 100 + Math.abs(hash % 100); + int g = 100 + Math.abs((hash >> 8) % 100); + int b = 100 + Math.abs((hash >> 16) % 100); + yield new int[] { r, g, b }; + } + }; + } + + private void drawCircle(GuiGraphics graphics, int cx, int cy, int radius, int color) { + for (int y = -radius; y <= radius; y++) { + int halfWidth = (int) Math.sqrt(radius * radius - y * y); + graphics.fill(cx - halfWidth, cy + y, cx + halfWidth + 1, cy + y + 1, color); + } + } + + private void drawCircleFast(GuiGraphics graphics, int cx, int cy, int radius, int color) { + if (radius <= 0) return; + + // For small radii, use precise 1px drawing + if (radius <= 6) { + drawCircle(graphics, cx, cy, radius, color); + return; + } + + // Use 2px bands for good quality + int bandSize = 2; + + for (int y = -radius; y <= radius; y += bandSize) { + int halfWidth = (int) Math.sqrt(radius * radius - y * y); + int bandEnd = Math.min(y + bandSize, radius + 1); + graphics.fill(cx - halfWidth, cy + y, cx + halfWidth + 1, cy + bandEnd, color); + } + } + + private int[] getSoulColor() { + int tier = ReflectionConstants.getSoulColorTier(erosion); + + return switch (tier) { + case 0 -> new int[] { 220, 220, 235 }; // Pale white/silver + case 1 -> new int[] { 180, 200, 255 }; // Faint blue + case 2 -> new int[] { 140, 120, 220 }; // Deep blue/purple + case 3 -> new int[] { 180, 80, 160 }; // Violet/crimson + case 4 -> new int[] { 160, 50, 50 }; // Dark red + case 5 -> new int[] { 80, 30, 30 }; // Almost black, faint red + default -> new int[] { 20, 10, 30 }; // Void-like + }; + } + + private void renderDialogue(GuiGraphics graphics) { + if (displayedText.isEmpty() && state != VoidState.AWAITING_CHOICE) return; + + int textY = height / 2 + 30; + int alpha = (int) (fadeAlpha * 255); + int textColor = (alpha << 24) | 0xBBBBBB; + + // Word wrap the text + List lines = wrapText(displayedText, MAX_LINE_WIDTH); + + // Render each line centered + int lineHeight = font.lineHeight + 2; + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + int lineWidth = font.width(line); + int textX = (width - lineWidth) / 2; + int y = textY + (i * lineHeight); + + // Italic style for reflection's voice + graphics.drawString(font, "\u00A7o" + line, textX, y, textColor, false); + } + } + + private List wrapText(String text, int maxWidth) { + List lines = new ArrayList<>(); + if (text.isEmpty()) return lines; + + String[] words = text.split(" "); + StringBuilder currentLine = new StringBuilder(); + + for (String word : words) { + String testLine = currentLine.isEmpty() ? word : currentLine + " " + word; + if (font.width(testLine) <= maxWidth) { + if (!currentLine.isEmpty()) currentLine.append(" "); + currentLine.append(word); + } else { + if (!currentLine.isEmpty()) { + lines.add(currentLine.toString()); + currentLine = new StringBuilder(); + } + // Handle words longer than max width + if (font.width(word) > maxWidth) { + lines.add(word); + } else { + currentLine.append(word); + } + } + } + + if (!currentLine.isEmpty()) { + lines.add(currentLine.toString()); + } + + return lines; + } + + private void renderContinueHint(GuiGraphics graphics) { + // Pulsing "click to continue" hint + float pulse = (float) Math.sin(totalTicks * 0.15) * 0.3f + 0.7f; + int alpha = (int) (fadeAlpha * pulse * 150); + int color = (alpha << 24) | 0x888888; + + String hint = "[ Click or press Space to continue ]"; + int hintWidth = font.width(hint); + int x = (width - hintWidth) / 2; + int y = height / 2 + 80; + + graphics.drawString(font, hint, x, y, color, false); + } + + private void renderAnswerButtons(GuiGraphics graphics, int mouseX, int mouseY) { + hoveredButton = -1; + AnswerButton hoveredBtn = null; + + for (int i = 0; i < answerButtons.size(); i++) { + AnswerButton button = answerButtons.get(i); + boolean hovered = button.isMouseOver(mouseX, mouseY); + if (hovered) { + hoveredButton = i; + hoveredBtn = button; + } + button.render(graphics, font, fadeAlpha, hovered, totalTicks); + } + + // Render tooltip for hovered button (rendered last so it's on top) + if (hoveredBtn != null) { + renderAnswerTooltip(graphics, hoveredBtn, mouseX, mouseY); + } + } + + private void renderAnswerTooltip(GuiGraphics graphics, AnswerButton button, int mouseX, int mouseY) { + BargainAnswer answer = button.answer; + + // Skip tooltips for simple menu items with no details + if (answer.powerDescription().isEmpty() && answer.drawbacks().isEmpty()) { + return; + } + + // Collect tooltip lines + List tooltipLines = new ArrayList<>(); + + // Context-aware headers based on current state + boolean isDefianceContext = (state == VoidState.VIEW_ACTIVE); + + // Add power description with header + if (!answer.powerDescription().isEmpty()) { + if (isDefianceContext) { + tooltipLines.add(Component.literal("\u00A7a\u00A7lCURRENT POWER:\u00A7r")); + } else { + tooltipLines.add(Component.literal("\u00A7a\u00A7lPOWERS:\u00A7r")); + } + for (Component power : answer.powerDescription()) { + tooltipLines.add(Component.literal("\u00A7a+ \u00A7f" + power.getString())); + } + } + + // Add spacing between power and drawbacks + if (!answer.powerDescription().isEmpty() && !answer.drawbacks().isEmpty()) { + tooltipLines.add(Component.literal("")); + } + + // Add drawbacks with header + if (!answer.drawbacks().isEmpty()) { + if (isDefianceContext) { + tooltipLines.add(Component.literal("\u00A7c\u00A7lDEFIANCE COST:\u00A7r")); + } else { + tooltipLines.add(Component.literal("\u00A7c\u00A7lDRAWBACKS:\u00A7r")); + } + for (Component drawback : answer.drawbacks()) { + tooltipLines.add(Component.literal("\u00A7c- \u00A77" + drawback.getString())); + } + } + + // Calculate tooltip dimensions + int tooltipWidth = 0; + for (Component line : tooltipLines) { + tooltipWidth = Math.max(tooltipWidth, font.width(line)); + } + tooltipWidth += 16; // Padding + + int lineHeight = font.lineHeight + 2; + int tooltipHeight = tooltipLines.size() * lineHeight + 8; + + // Find the topmost button to position tooltip above ALL buttons + int topmostButtonY = button.y; + for (AnswerButton btn : answerButtons) { + if (btn.y < topmostButtonY) { + topmostButtonY = btn.y; + } + } + + // Position tooltip above ALL buttons with some margin + int tooltipX = (width - tooltipWidth) / 2; // Center horizontally on screen + int tooltipY = topmostButtonY - tooltipHeight - 15; // Above the topmost button + + // Keep tooltip on screen + if (tooltipX < 10) tooltipX = 10; + if (tooltipX + tooltipWidth > width - 10) tooltipX = width - tooltipWidth - 10; + if (tooltipY < 10) tooltipY = 10; + + // Render tooltip background (solid, no transparency for readability) + int bgColor = (0xFF << 24) | 0x101018; + int borderColor = (0xFF << 24) | 0x505080; + int innerBorderColor = (0xFF << 24) | 0x303050; + + // Outer border + graphics.fill(tooltipX - 6, tooltipY - 6, tooltipX + tooltipWidth + 6, tooltipY + tooltipHeight + 6, + borderColor); + // Inner border + graphics.fill(tooltipX - 5, tooltipY - 5, tooltipX + tooltipWidth + 5, tooltipY + tooltipHeight + 5, + innerBorderColor); + // Background + graphics.fill(tooltipX - 4, tooltipY - 4, tooltipX + tooltipWidth + 4, tooltipY + tooltipHeight + 4, bgColor); + + // Render text lines + int lineY = tooltipY; + for (Component line : tooltipLines) { + String text = line.getString(); + if (text.isEmpty()) { + lineY += lineHeight / 2; // Half spacing for empty lines + } else { + graphics.drawString(font, line, tooltipX, lineY, 0xFFFFFFFF, false); + lineY += lineHeight; + } + } + } + + private void renderCostPreview(GuiGraphics graphics) { + if (currentBargain == null) return; + + int alpha = (int) (fadeAlpha * 180); + + // Build cost display lines first to know how much space we need + List costLines = new ArrayList<>(); + List costColors = new ArrayList<>(); + + int shardCost = currentBargain.getShardCost(); + int weight = currentBargain.getWeight(); + int erosionCost = currentBargain.getErosionCost(); + + if (shardCost > 0) { + boolean canAfford = shardBalance >= shardCost; + costLines.add("\u2726 " + shardCost + " shards"); + costColors.add((alpha << 24) | (canAfford ? 0x55FFFF : 0xFF5555)); + } + + if (weight > 0) { + int remaining = totalCapacity - usedCapacity; + boolean canFit = remaining >= weight; + costLines.add("\u25C6 " + weight + " weight"); + costColors.add((alpha << 24) | (canFit ? 0xAA55FF : 0xFF5555)); + } + + if (erosionCost > 0) { + costLines.add("+" + erosionCost + " erosion"); + costColors.add((alpha << 24) | 0xAA6666); + } + + if (costLines.isEmpty()) { + costLines.add("Free"); + costColors.add((alpha << 24) | 0x55FF55); + } + + // Calculate total height needed for cost lines + int lineHeight = 12; + int totalCostHeight = costLines.size() * lineHeight; + + // Position: prefer below buttons, but clamp to stay on screen + int buttonsEndY = height / 2 + 60 + (answerButtons.size() * 35) + 10; + int idealY = buttonsEndY + 10; + + // Ensure we stay at least 10px from bottom of screen + int maxY = height - totalCostHeight - 10; + int y = Math.min(idealY, maxY); + + int centerX = width / 2; + + // Render all cost lines + for (int i = 0; i < costLines.size(); i++) { + String line = costLines.get(i); + int lineWidth = font.width(line); + graphics.drawString(font, line, centerX - lineWidth / 2, y + (i * lineHeight), costColors.get(i), false); + } + } + + private void renderErosionIndicator(GuiGraphics graphics) { + // Subtle erosion display in bottom left + int alpha = (int) (fadeAlpha * 100); + int color = (alpha << 24) | 0x555555; + + Component text = Component.translatable("reflection.cosmiccore.ui.soul_erosion_display", erosion); + graphics.drawString(font, text, 15, height - 25, color, false); + + // Small colored indicator + int[] soulColor = getSoulColor(); + int indicatorColor = (alpha << 24) | (soulColor[0] << 16) | (soulColor[1] << 8) | soulColor[2]; + graphics.fill(15, height - 35, 25, height - 28, indicatorColor); + + // Render shards and capacity in top right corner + renderEconomyDisplay(graphics); + } + + private void renderEconomyDisplay(GuiGraphics graphics) { + int alpha = (int) (fadeAlpha * 200); + if (alpha < 20) return; + + int rightMargin = width - 15; + int topY = 15; + + // Shard balance (aqua color) + int shardColor = (alpha << 24) | 0x55FFFF; + String shardText = "\u2726 " + shardBalance; // Unicode diamond + int shardWidth = font.width(shardText); + graphics.drawString(font, shardText, rightMargin - shardWidth, topY, shardColor, false); + + // Capacity display (purple color) below shards + int capacityColor = (alpha << 24) | 0xAA55FF; + String capacityText = usedCapacity + "/" + totalCapacity + " soul"; + int capacityWidth = font.width(capacityText); + graphics.drawString(font, capacityText, rightMargin - capacityWidth, topY + 12, capacityColor, false); + + // Capacity bar + int barWidth = 60; + int barHeight = 4; + int barX = rightMargin - barWidth; + int barY = topY + 24; + + // Background + int bgColor = (alpha << 24) | 0x222222; + graphics.fill(barX, barY, barX + barWidth, barY + barHeight, bgColor); + + // Filled portion + float fillPercent = totalCapacity > 0 ? (float) usedCapacity / totalCapacity : 0f; + int fillWidth = (int) (barWidth * fillPercent); + int fillColor = fillPercent > 0.9f ? ((alpha << 24) | 0xFF5555) : // Red when almost full + fillPercent > 0.7f ? ((alpha << 24) | 0xFFAA55) : // Orange when high + ((alpha << 24) | 0xAA55FF); // Purple normally + if (fillWidth > 0) { + graphics.fill(barX, barY, barX + fillWidth, barY + barHeight, fillColor); + } + } + + private void renderStateHeader(GuiGraphics graphics) { + Component header = switch (state) { + case BROWSE_BARGAINS -> ReflectionLang.uiAvailableBargains(); + case VIEW_ACTIVE -> ReflectionLang.uiYourBargains(); + case DEFIANCE_CONFIRM -> ReflectionLang.uiDefiance(); + default -> null; + }; + + if (header == null) return; + + int alpha = (int) (fadeAlpha * 200); + int color = (alpha << 24) | 0xAAAAAA; + + int headerWidth = font.width(header); + int x = (width - headerWidth) / 2; + int y = height / 2 + 40; // Just above the buttons area + + graphics.drawString(font, header, x, y, color, false); + + // Render scroll indicators for VIEW_ACTIVE state + if (state == VoidState.VIEW_ACTIVE && !viewActiveAllOptions.isEmpty()) { + int maxScroll = Math.max(0, viewActiveAllOptions.size() - viewActiveMaxVisible); + int scrollAlpha = (int) (fadeAlpha * 150); + + // Show "scroll up" indicator if not at top + if (bargainListScrollOffset > 0) { + Component upHint = ReflectionLang.uiScrollUp(); + int upWidth = font.width(upHint); + int upColor = (scrollAlpha << 24) | 0x888888; + graphics.drawString(font, upHint, (width - upWidth) / 2, height / 2 + 52, upColor, false); + } + + // Show "scroll down" indicator if not at bottom + if (bargainListScrollOffset < maxScroll) { + Component downHint = ReflectionLang.uiScrollDown(); + int downWidth = font.width(downHint); + int downColor = (scrollAlpha << 24) | 0x888888; + // Position below the last visible button + int lastButtonY = height / 2 + 60 + (viewActiveMaxVisible * 35); + graphics.drawString(font, downHint, (width - downWidth) / 2, lastButtonY + 5, downColor, false); + } + + // Show scroll position indicator + String posHint = (bargainListScrollOffset + 1) + "-" + + Math.min(bargainListScrollOffset + viewActiveMaxVisible, viewActiveAllOptions.size()) + + " " + ReflectionLang.ui("of").getString() + " " + viewActiveAllOptions.size(); + int posWidth = font.width(posHint); + int posColor = ((scrollAlpha / 2) << 24) | 0x666666; + graphics.drawString(font, posHint, (width - posWidth) / 2, y + 12, posColor, false); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button != 0) return super.mouseClicked(mouseX, mouseY, button); + + // Check answer buttons for all interactive states + if (state == VoidState.AWAITING_CHOICE || state == VoidState.HUB_MENU || + state == VoidState.BROWSE_BARGAINS || state == VoidState.VIEW_ACTIVE || + state == VoidState.DEFIANCE_CONFIRM) { + for (AnswerButton answerButton : answerButtons) { + if (answerButton.isMouseOver((int) mouseX, (int) mouseY)) { + onAnswerSelected(answerButton.answer); + return true; + } + } + } + + // Click to advance dialogue + if (state == VoidState.DIALOGUE) { + if (currentDialogueIndex < dialogueQueue.size()) { + String fullText = dialogueQueue.get(currentDialogueIndex); + + if (charIndex < fullText.length()) { + // Skip to end of current line + charIndex = fullText.length(); + displayedText = fullText; + } else { + // Advance to next line + currentDialogueIndex++; + charIndex = 0; + displayedText = ""; + } + return true; + } + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + // ESC to close (with fade) + if (keyCode == 256) { // ESCAPE + if (state != VoidState.FADE_OUT) { + transitionTo(VoidState.FADE_OUT); + } + return true; + } + + // Space/Enter to advance dialogue + if ((keyCode == 32 || keyCode == 257) && state == VoidState.DIALOGUE) { + mouseClicked(0, 0, 0); + return true; + } + + // Number keys for quick answer selection + if (state == VoidState.AWAITING_CHOICE && keyCode >= 49 && keyCode <= 57) { + int index = keyCode - 49; // 1 = 0, 2 = 1, etc. + if (index < answerButtons.size()) { + onAnswerSelected(answerButtons.get(index).answer); + return true; + } + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double delta) { + // Scroll bargain list in VIEW_ACTIVE state + if (state == VoidState.VIEW_ACTIVE && !viewActiveAllOptions.isEmpty()) { + int maxScroll = Math.max(0, viewActiveAllOptions.size() - viewActiveMaxVisible); + if (delta > 0) { + // Scroll up + bargainListScrollOffset = Math.max(0, bargainListScrollOffset - 1); + } else if (delta < 0) { + // Scroll down + bargainListScrollOffset = Math.min(maxScroll, bargainListScrollOffset + 1); + } + // Rebuild buttons with new scroll position + setupViewActiveBargains(); + return true; + } + return super.mouseScrolled(mouseX, mouseY, delta); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + private enum VoidState { + FADE_IN, + DIALOGUE, + AWAITING_CHOICE, + HUB_MENU, // Hub menu with options + BROWSE_BARGAINS, // Browsing available bargains + VIEW_ACTIVE, // Viewing player's active bargains + DEFIANCE_CONFIRM,// Confirming defiance of a bargain + FADE_OUT + } + + private enum VoidMode { + REFLECTION, // General reflection (no bargain) + BARGAIN_OFFER, // Offering a specific bargain + THRESHOLD, // Erosion threshold encounter + HUB // Mirror hub - browse/manage bargains + } + + private static class VoidParticle { + + float x, y; + float vx, vy; + float size; + float alpha; + float maxAlpha; + int lifetime; + int age; + + VoidParticle(int screenWidth, int screenHeight, Random random) { + reset(screenWidth, screenHeight, random); + } + + void reset(int screenWidth, int screenHeight, Random random) { + x = random.nextFloat() * screenWidth; + y = random.nextFloat() * screenHeight; + vx = (random.nextFloat() - 0.5f) * 0.5f; + vy = (random.nextFloat() - 0.5f) * 0.3f - 0.2f; // Slight upward drift + size = 1 + random.nextFloat() * 2; + maxAlpha = 0.2f + random.nextFloat() * 0.3f; + alpha = 0; + lifetime = 100 + random.nextInt(200); + age = 0; + } + + void tick() { + x += vx; + y += vy; + age++; + + // Fade in and out + float progress = (float) age / lifetime; + if (progress < 0.2f) { + alpha = maxAlpha * (progress / 0.2f); + } else if (progress > 0.8f) { + alpha = maxAlpha * (1f - (progress - 0.8f) / 0.2f); + } else { + alpha = maxAlpha; + } + } + + boolean isDead() { + return age >= lifetime; + } + + void render(GuiGraphics graphics, float screenAlpha) { + int a = (int) (alpha * screenAlpha * 255); + if (a <= 0) return; + + int color = (a << 24) | 0x404050; + int s = (int) size; + graphics.fill((int) x, (int) y, (int) x + s, (int) y + s, color); + } + } + + private class AnswerButton { + + final int x, y, width, height; + final BargainAnswer answer; + final int index; + + AnswerButton(int x, int y, int width, int height, BargainAnswer answer, int index) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.answer = answer; + this.index = index; + } + + boolean isMouseOver(int mouseX, int mouseY) { + return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + height; + } + + void render(GuiGraphics graphics, net.minecraft.client.gui.Font font, float fadeAlpha, boolean hovered, + int ticks) { + int baseAlpha = (int) (fadeAlpha * (hovered ? 200 : 140)); + + // Background + int bgColor = hovered ? (baseAlpha << 24) | 0x303040 : (baseAlpha << 24) | 0x1a1a24; + graphics.fill(x, y, x + width, y + height, bgColor); + + // Border + int borderAlpha = (int) (fadeAlpha * (hovered ? 255 : 150)); + int borderColor = hovered ? (borderAlpha << 24) | 0x6080AA : (borderAlpha << 24) | 0x404060; + + // Top and bottom borders + graphics.fill(x, y, x + width, y + 1, borderColor); + graphics.fill(x, y + height - 1, x + width, y + height, borderColor); + // Left and right borders + graphics.fill(x, y, x + 1, y + height, borderColor); + graphics.fill(x + width - 1, y, x + width, y + height, borderColor); + + // Text + String text = answer.text().getString(); + int textAlpha = (int) (fadeAlpha * 255); + int textColor = hovered ? (textAlpha << 24) | 0xDDDDEE : (textAlpha << 24) | 0x999999; + + int textWidth = font.width(text); + int textX = x + (width - textWidth) / 2; + int textY = y + (height - font.lineHeight) / 2; + + graphics.drawString(font, text, textX, textY, textColor, false); + + // Keyboard hint + String hint = "[" + (index + 1) + "]"; + int hintAlpha = (int) (fadeAlpha * 100); + int hintColor = (hintAlpha << 24) | 0x666666; + graphics.drawString(font, hint, x + 8, textY, hintColor, false); + + // Hover glow effect + if (hovered) { + float glowPulse = (float) Math.sin(ticks * 0.2) * 0.3f + 0.7f; + int glowAlpha = (int) (fadeAlpha * glowPulse * 30); + int glowColor = (glowAlpha << 24) | 0x6080AA; + graphics.fill(x + 2, y + 2, x + width - 2, y + height - 2, glowColor); + } + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ui/VoidUIPackets.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ui/VoidUIPackets.java new file mode 100644 index 000000000..a68ee4906 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/ui/VoidUIPackets.java @@ -0,0 +1,406 @@ +package com.ghostipedia.cosmiccore.common.reflection.ui; + +import com.ghostipedia.cosmiccore.common.network.CCoreNetwork; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain; +import com.ghostipedia.cosmiccore.common.reflection.bargain.Bargain.BargainAnswer; +import com.ghostipedia.cosmiccore.common.reflection.bargain.BargainRegistry; +import com.ghostipedia.cosmiccore.common.reflection.bargain.impl.QuakeMovementBargain; +import com.ghostipedia.cosmiccore.common.reflection.network.SyncQuakeMovementPacket; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.network.NetworkDirection; +import net.minecraftforge.network.NetworkEvent; + +import java.util.HashSet; +import java.util.Set; + +public class VoidUIPackets { + + public static void register() { + CCoreNetwork.register(OpenVoidScreenPacket.class, OpenVoidScreenPacket::new, NetworkDirection.PLAY_TO_CLIENT); + CCoreNetwork.register(BargainChoicePacket.class, BargainChoicePacket::new, NetworkDirection.PLAY_TO_SERVER); + CCoreNetwork.register(ThresholdEncounterPacket.class, ThresholdEncounterPacket::new, + NetworkDirection.PLAY_TO_CLIENT); + CCoreNetwork.register(OpenHubPacket.class, OpenHubPacket::new, NetworkDirection.PLAY_TO_CLIENT); + CCoreNetwork.register(DefianceChoicePacket.class, DefianceChoicePacket::new, NetworkDirection.PLAY_TO_SERVER); + } + + public static void sendOpenVoidScreen(ServerPlayer player, ResourceLocation bargainId) { + ReflectionCapability.get(player).ifPresent(reflection -> { + int erosion = reflection.getErosion(); + Set activeBargains = reflection.getActiveBargains(); + int shardBalance = reflection.getShardBalance(); + int usedCapacity = reflection.getUsedCapacity(); + int totalCapacity = reflection.getTotalCapacity(); + CCoreNetwork.sendToPlayer(player, new OpenVoidScreenPacket(bargainId, erosion, activeBargains, + shardBalance, usedCapacity, totalCapacity)); + }); + } + + public static void sendOpenVoidScreen(ServerPlayer player) { + ReflectionCapability.get(player).ifPresent(reflection -> { + int erosion = reflection.getErosion(); + Set activeBargains = reflection.getActiveBargains(); + int shardBalance = reflection.getShardBalance(); + int usedCapacity = reflection.getUsedCapacity(); + int totalCapacity = reflection.getTotalCapacity(); + CCoreNetwork.sendToPlayer(player, new OpenVoidScreenPacket(null, erosion, activeBargains, + shardBalance, usedCapacity, totalCapacity)); + }); + } + + public static void sendBargainChoice(ResourceLocation bargainId, String answerId) { + CCoreNetwork.sendToServer(new BargainChoicePacket(bargainId, answerId)); + } + + public static void sendThresholdEncounter(ServerPlayer player, int thresholdIndex) { + ReflectionCapability.get(player).ifPresent(reflection -> { + int erosion = reflection.getErosion(); + Set activeBargains = reflection.getActiveBargains(); + CCoreNetwork.sendToPlayer(player, new ThresholdEncounterPacket(thresholdIndex, erosion, activeBargains)); + }); + } + + public static void sendOpenHub(ServerPlayer player) { + ReflectionCapability.get(player).ifPresent(reflection -> { + int erosion = reflection.getErosion(); + Set activeBargains = reflection.getActiveBargains(); + Set defianceScars = reflection.getDefianceScars(); + int shardBalance = reflection.getShardBalance(); + int usedCapacity = reflection.getUsedCapacity(); + int totalCapacity = reflection.getTotalCapacity(); + CCoreNetwork.sendToPlayer(player, new OpenHubPacket(erosion, activeBargains, defianceScars, + shardBalance, usedCapacity, totalCapacity)); + }); + } + + public static void sendDefianceChoice(ResourceLocation bargainId) { + CCoreNetwork.sendToServer(new DefianceChoicePacket(bargainId)); + } + + public static class OpenVoidScreenPacket implements CCoreNetwork.INetPacket { + + private final ResourceLocation bargainId; + private final int erosion; + private final Set activeBargains; + private final int shardBalance; + private final int usedCapacity; + private final int totalCapacity; + + public OpenVoidScreenPacket(ResourceLocation bargainId, int erosion, Set activeBargains, + int shardBalance, int usedCapacity, int totalCapacity) { + this.bargainId = bargainId; + this.erosion = erosion; + this.activeBargains = activeBargains != null ? activeBargains : Set.of(); + this.shardBalance = shardBalance; + this.usedCapacity = usedCapacity; + this.totalCapacity = totalCapacity; + } + + public OpenVoidScreenPacket(FriendlyByteBuf buf) { + if (buf.readBoolean()) { + this.bargainId = buf.readResourceLocation(); + } else { + this.bargainId = null; + } + this.erosion = buf.readVarInt(); + + int count = buf.readVarInt(); + Set bargains = new HashSet<>(); + for (int i = 0; i < count; i++) { + bargains.add(buf.readResourceLocation()); + } + this.activeBargains = bargains; + + this.shardBalance = buf.readVarInt(); + this.usedCapacity = buf.readVarInt(); + this.totalCapacity = buf.readVarInt(); + } + + @Override + public void encode(FriendlyByteBuf buf) { + buf.writeBoolean(bargainId != null); + if (bargainId != null) { + buf.writeResourceLocation(bargainId); + } + buf.writeVarInt(erosion); + buf.writeVarInt(activeBargains.size()); + for (ResourceLocation id : activeBargains) { + buf.writeResourceLocation(id); + } + + buf.writeVarInt(shardBalance); + buf.writeVarInt(usedCapacity); + buf.writeVarInt(totalCapacity); + } + + @Override + public void execute(NetworkEvent.Context ctx) { + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> { + if (bargainId != null) { + BargainRegistry.get(bargainId) + .ifPresent(bargain -> VoidScreen.openWithBargain(bargain, erosion, activeBargains, + shardBalance, usedCapacity, totalCapacity)); + } else { + VoidScreen.openForReflection(erosion, activeBargains, + shardBalance, usedCapacity, totalCapacity); + } + }); + } + } + + public static class BargainChoicePacket implements CCoreNetwork.INetPacket { + + private final ResourceLocation bargainId; + private final String answerId; + + public BargainChoicePacket(ResourceLocation bargainId, String answerId) { + this.bargainId = bargainId; + this.answerId = answerId; + } + + public BargainChoicePacket(FriendlyByteBuf buf) { + this.bargainId = buf.readResourceLocation(); + this.answerId = buf.readUtf(); + } + + @Override + public void encode(FriendlyByteBuf buf) { + buf.writeResourceLocation(bargainId); + buf.writeUtf(answerId); + } + + @Override + public void execute(NetworkEvent.Context ctx) { + ServerPlayer player = ctx.getSender(); + if (player == null) return; + + BargainRegistry.get(bargainId).ifPresent(bargain -> { + processBargainChoice(player, bargain, answerId); + }); + } + + private void processBargainChoice(ServerPlayer player, Bargain bargain, String answerId) { + // Find the answer + BargainAnswer foundAnswer = null; + for (BargainAnswer answer : bargain.getAnswers()) { + if (answer.id().equals(answerId)) { + foundAnswer = answer; + break; + } + } + + if (foundAnswer == null) return; + + // Make final for lambda + final BargainAnswer selectedAnswer = foundAnswer; + + ReflectionCapability.get(player).ifPresent(reflection -> { + boolean isAccept = answerId.equals("accept") || + (!answerId.equals("refuse") && selectedAnswer.grantsFullPower()); + + if (isAccept) { + if (!reflection.hasBargain(bargainId)) { + int shardCost = bargain.getShardCost(); + int weight = bargain.getWeight(); + + if (shardCost > 0 && reflection.getShardBalance() < shardCost) { + player.displayClientMessage( + net.minecraft.network.chat.Component + .literal("\u00A7cInsufficient shards. You need " + shardCost + " shards."), + false); + return; + } + + if (weight > 0 && !reflection.canFitBargain(weight)) { + player.displayClientMessage( + net.minecraft.network.chat.Component.literal( + "\u00A7cInsufficient soul capacity. Need " + weight + " weight, have " + + reflection.getRemainingCapacity() + " remaining."), + false); + return; + } + + if (shardCost > 0) { + reflection.spendShards(shardCost); + } + + int erosionCost = bargain.getErosionCost(); + if (erosionCost > 0) { + reflection.addErosion(erosionCost); + } + + reflection.acceptBargain(bargainId); + bargain.onAccept(player, selectedAnswer); + syncBargainState(player, bargain, true); + } + } + // Note: Refusing a bargain offer does NOT call onDefy. + // onDefy is only for breaking an existing bargain you've already accepted. + // Refusing simply declines the offer with no mechanical effect. + }); + } + + private void syncBargainState(ServerPlayer player, Bargain bargain, boolean active) { + if (bargain.getId().equals(QuakeMovementBargain.INSTANCE.getId())) { + CCoreNetwork.sendToPlayer(player, new SyncQuakeMovementPacket(active)); + } + } + } + + public static class ThresholdEncounterPacket implements CCoreNetwork.INetPacket { + + private final int thresholdIndex; + private final int erosion; + private final Set activeBargains; + + public ThresholdEncounterPacket(int thresholdIndex, int erosion, Set activeBargains) { + this.thresholdIndex = thresholdIndex; + this.erosion = erosion; + this.activeBargains = activeBargains != null ? activeBargains : Set.of(); + } + + public ThresholdEncounterPacket(FriendlyByteBuf buf) { + this.thresholdIndex = buf.readVarInt(); + this.erosion = buf.readVarInt(); + + int count = buf.readVarInt(); + Set bargains = new HashSet<>(); + for (int i = 0; i < count; i++) { + bargains.add(buf.readResourceLocation()); + } + this.activeBargains = bargains; + } + + @Override + public void encode(FriendlyByteBuf buf) { + buf.writeVarInt(thresholdIndex); + buf.writeVarInt(erosion); + + buf.writeVarInt(activeBargains.size()); + for (ResourceLocation id : activeBargains) { + buf.writeResourceLocation(id); + } + } + + @Override + public void execute(NetworkEvent.Context ctx) { + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> { + VoidScreen.openForThreshold(thresholdIndex, erosion, activeBargains); + }); + } + } + + public static class OpenHubPacket implements CCoreNetwork.INetPacket { + + private final int erosion; + private final Set activeBargains; + private final Set defianceScars; + private final int shardBalance; + private final int usedCapacity; + private final int totalCapacity; + + public OpenHubPacket(int erosion, Set activeBargains, Set defianceScars, + int shardBalance, int usedCapacity, int totalCapacity) { + this.erosion = erosion; + this.activeBargains = activeBargains != null ? activeBargains : Set.of(); + this.defianceScars = defianceScars != null ? defianceScars : Set.of(); + this.shardBalance = shardBalance; + this.usedCapacity = usedCapacity; + this.totalCapacity = totalCapacity; + } + + public OpenHubPacket(FriendlyByteBuf buf) { + this.erosion = buf.readVarInt(); + + int activeCount = buf.readVarInt(); + Set active = new HashSet<>(); + for (int i = 0; i < activeCount; i++) { + active.add(buf.readResourceLocation()); + } + this.activeBargains = active; + + int scarCount = buf.readVarInt(); + Set scars = new HashSet<>(); + for (int i = 0; i < scarCount; i++) { + scars.add(buf.readResourceLocation()); + } + this.defianceScars = scars; + + this.shardBalance = buf.readVarInt(); + this.usedCapacity = buf.readVarInt(); + this.totalCapacity = buf.readVarInt(); + } + + @Override + public void encode(FriendlyByteBuf buf) { + buf.writeVarInt(erosion); + + buf.writeVarInt(activeBargains.size()); + for (ResourceLocation id : activeBargains) { + buf.writeResourceLocation(id); + } + + buf.writeVarInt(defianceScars.size()); + for (ResourceLocation id : defianceScars) { + buf.writeResourceLocation(id); + } + + buf.writeVarInt(shardBalance); + buf.writeVarInt(usedCapacity); + buf.writeVarInt(totalCapacity); + } + + @Override + public void execute(NetworkEvent.Context ctx) { + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> { + VoidScreen.openForHub(erosion, activeBargains, defianceScars, + shardBalance, usedCapacity, totalCapacity); + }); + } + } + + public static class DefianceChoicePacket implements CCoreNetwork.INetPacket { + + private final ResourceLocation bargainId; + + public DefianceChoicePacket(ResourceLocation bargainId) { + this.bargainId = bargainId; + } + + public DefianceChoicePacket(FriendlyByteBuf buf) { + this.bargainId = buf.readResourceLocation(); + } + + @Override + public void encode(FriendlyByteBuf buf) { + buf.writeResourceLocation(bargainId); + } + + @Override + public void execute(NetworkEvent.Context ctx) { + ServerPlayer player = ctx.getSender(); + if (player == null) return; + + BargainRegistry.get(bargainId).ifPresent(bargain -> { + ReflectionCapability.get(player).ifPresent(reflection -> { + if (!reflection.hasBargain(bargainId)) return; + + int cost = BargainRegistry.calculateDefianceCost(player, bargain); + reflection.addErosion(cost); + reflection.defy(bargainId); + bargain.onDefy(player); + + if (bargainId.equals(QuakeMovementBargain.INSTANCE.getId())) { + CCoreNetwork.sendToPlayer(player, new SyncQuakeMovementPacket(false)); + } + }); + }); + } + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/common/reflection/whisper/WhisperSystem.java b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/whisper/WhisperSystem.java new file mode 100644 index 000000000..64df85ee2 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/common/reflection/whisper/WhisperSystem.java @@ -0,0 +1,300 @@ +package com.ghostipedia.cosmiccore.common.reflection.whisper; + +import com.ghostipedia.cosmiccore.common.reflection.IReflection; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCapability; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionConstants; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; + +import java.util.*; + +/** + * The Whisper System - ambient comments from the reflection. + * Event-based, contextual, scales with corruption. + */ +public final class WhisperSystem { + + private WhisperSystem() {} + + private static final Random RANDOM = new Random(); + + // Cooldown tracking to prevent spam + private static final Map lastWhisperTime = new HashMap<>(); + private static final long WHISPER_COOLDOWN = 30000L; // 30 seconds minimum between whispers + + // Style for whispers + private static final Style WHISPER_STYLE = Style.EMPTY.withItalic(true).withColor(0x9966CC); + + /** + * Called periodically to check for ambient whispers. + */ + public static void tick(ServerPlayer player) { + ReflectionCapability.get(player).ifPresent(reflection -> { + if (!reflection.hasAwakened()) return; + + // Check various conditions for contextual whispers + checkHealthWhisper(player, reflection); + checkIdleWhisper(player, reflection); + checkDimensionWhisper(player, reflection); + }); + } + + /** + * Trigger a whisper for a specific event. + */ + public static void triggerEvent(ServerPlayer player, WhisperEvent event) { + ReflectionCapability.get(player).ifPresent(reflection -> { + if (!reflection.hasAwakened()) return; + if (isOnCooldown(player)) return; + + List lines = getEventLines(event, reflection); + if (lines.isEmpty()) return; + + String line = lines.get(RANDOM.nextInt(lines.size())); + sendWhisper(player, line); + }); + } + + /** + * Send a whisper to the player. + */ + public static void sendWhisper(ServerPlayer player, String text) { + if (isOnCooldown(player)) return; + + Component message = Component.literal("* " + text + " *").withStyle(WHISPER_STYLE); + player.sendSystemMessage(message); + lastWhisperTime.put(player.getUUID(), System.currentTimeMillis()); + } + + /** + * Send a whisper with custom formatting. + */ + public static void sendWhisper(ServerPlayer player, Component text) { + if (isOnCooldown(player)) return; + + player.sendSystemMessage(text); + lastWhisperTime.put(player.getUUID(), System.currentTimeMillis()); + } + + private static boolean isOnCooldown(Player player) { + Long lastTime = lastWhisperTime.get(player.getUUID()); + if (lastTime == null) return false; + return (System.currentTimeMillis() - lastTime) < WHISPER_COOLDOWN; + } + + // ---- Condition Checks ---- + + private static void checkHealthWhisper(ServerPlayer player, IReflection reflection) { + if (player.getHealth() < player.getMaxHealth() * 0.25f) { + if (RANDOM.nextFloat() < 0.05f) { // 5% chance per tick check + triggerEvent(player, WhisperEvent.LOW_HEALTH); + } + } + } + + private static void checkIdleWhisper(ServerPlayer player, IReflection reflection) { + // Check if player has been standing still + if (player.getDeltaMovement().lengthSqr() < 0.001) { + if (RANDOM.nextFloat() < 0.01f) { // 1% chance per tick check + triggerEvent(player, WhisperEvent.IDLE); + } + } + } + + private static void checkDimensionWhisper(ServerPlayer player, IReflection reflection) { + // Random ambient whispers based on dimension + if (RANDOM.nextFloat() < 0.005f) { // 0.5% chance per tick check + triggerEvent(player, WhisperEvent.AMBIENT); + } + } + + // ---- Line Pools ---- + + private static List getEventLines(WhisperEvent event, IReflection reflection) { + int erosion = reflection.getErosion(); + int colorTier = ReflectionConstants.getSoulColorTier(erosion); + + return switch (event) { + case DEATH -> getDeathLines(reflection, colorTier); + case LOW_HEALTH -> getLowHealthLines(colorTier); + case LOW_OXYGEN -> getLowOxygenLines(colorTier); + case IDLE -> getIdleLines(colorTier); + case ENTERED_DIMENSION -> getDimensionLines(colorTier); + case POST_BARGAIN -> getPostBargainLines(colorTier); + case AMBIENT -> getAmbientLines(colorTier); + case COMBAT_KILL -> getCombatKillLines(colorTier); + }; + } + + private static List getDeathLines(IReflection reflection, int tier) { + int deathCount = reflection.getDeathCount(); + + List lines = new ArrayList<>(); + + // Low corruption + if (tier <= 1) { + lines.add("Welcome back."); + lines.add("That one was faster than usual."); + lines.add("I felt it too. I always do."); + lines.add("Does it still hurt? I can never tell."); + } + + // Mid corruption + if (tier >= 2 && tier <= 4) { + lines.add("Again. And again."); + lines.add("We're getting used to this, aren't we?"); + lines.add("That's " + deathCount + " now. I've been counting."); + lines.add("The dying is easy. It's the coming back that wears on us."); + } + + // High corruption + if (tier >= 5) { + lines.add("Another one."); + lines.add("Do you even notice anymore?"); + lines.add("We've done this " + deathCount + " times. It means nothing now."); + lines.add("Death is just... punctuation."); + } + + return lines; + } + + private static List getLowHealthLines(int tier) { + List lines = new ArrayList<>(); + + if (tier <= 2) { + lines.add("Careful. That looks like it hurts."); + lines.add("You're bleeding. Well, we're bleeding."); + lines.add("Should I be worried? Should we?"); + } + + if (tier >= 3) { + lines.add("Pain is just information."); + lines.add("We've felt worse."); + lines.add("This body is temporary anyway."); + } + + return lines; + } + + private static List getLowOxygenLines(int tier) { + List lines = new ArrayList<>(); + + if (tier <= 2) { + lines.add("Breathe. Oh wait."); + lines.add("The air is thin here. Or is it us?"); + lines.add("Mortals panic when this happens. What do we do?"); + } + + if (tier >= 3) { + lines.add("Still clinging to that breathing habit."); + lines.add("We don't need air. We just think we do."); + lines.add("Let go. It won't hurt for long."); + } + + return lines; + } + + private static List getIdleLines(int tier) { + List lines = new ArrayList<>(); + + if (tier <= 2) { + lines.add("Thinking? Or avoiding?"); + lines.add("I'm still here, Whenever you're ready."); + lines.add("Take your time, We have plenty."); + } + + if (tier >= 3) { + lines.add("You can't run from me by standing still."); + lines.add("I'm right here, I'm always right here."); + lines.add("The silence between us speaks volumes."); + } + + if (tier >= 5) { + lines.add("Are you listening? Or am I talking to myself?"); + lines.add("Sometimes I forget which one of us is which."); + lines.add("..."); + } + + return lines; + } + + private static List getDimensionLines(int tier) { + List lines = new ArrayList<>(); + + lines.add("Somewhere new. Somewhere dangerous. Good."); + lines.add("What do you think we'll find here?"); + lines.add("This place remembers things. Be careful what you show it."); + lines.add("The rules are different here. Can you feel it?"); + + return lines; + } + + private static List getPostBargainLines(int tier) { + List lines = new ArrayList<>(); + + lines.add("How does it feel?"); + lines.add("We're changing. Can you tell?"); + lines.add("No going back now. Isn't that freeing?"); + lines.add("Look at us. Look at what we're becoming."); + + return lines; + } + + private static List getAmbientLines(int tier) { + List lines = new ArrayList<>(); + + if (tier <= 1) { + lines.add("..."); + lines.add("I'm watching."); + lines.add("Interesting choice."); + } + + if (tier >= 2 && tier <= 4) { + lines.add("We're doing well. Aren't we?"); + lines.add("Keep going. I want to see what happens."); + lines.add("You're stronger than you think. We both are."); + } + + if (tier >= 5) { + lines.add("Beautiful, isn't it? What we've become?"); + lines.add("They wouldn't understand. Only we do."); + lines.add("Almost there. Almost..."); + } + + return lines; + } + + private static List getCombatKillLines(int tier) { + List lines = new ArrayList<>(); + + if (tier <= 2) { + lines.add("That was necessary. Wasn't it?"); + lines.add("They're gone. We're still here."); + } + + if (tier >= 3) { + lines.add("More."); + lines.add("Again."); + lines.add("Good."); + } + + return lines; + } + + /** + * Whisper event types. + */ + public enum WhisperEvent { + DEATH, + LOW_HEALTH, + LOW_OXYGEN, + IDLE, + ENTERED_DIMENSION, + POST_BARGAIN, + AMBIENT, + COMBAT_KILL + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/forge/ForgeCommonEventListener.java b/src/main/java/com/ghostipedia/cosmiccore/forge/ForgeCommonEventListener.java index d8c44a483..b1ade6ee0 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/forge/ForgeCommonEventListener.java +++ b/src/main/java/com/ghostipedia/cosmiccore/forge/ForgeCommonEventListener.java @@ -11,6 +11,8 @@ import com.ghostipedia.cosmiccore.common.machine.multiblock.multi.SteamCaster; import com.ghostipedia.cosmiccore.common.machine.multiblock.multi.SteamMixer; import com.ghostipedia.cosmiccore.common.machine.multiblock.part.SoulHatchPartMachine; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCommand; +import com.ghostipedia.cosmiccore.common.reflection.ReflectionCommands; import com.ghostipedia.cosmiccore.mixin.accessor.LivingEntityAccessor; import com.gregtechceu.gtceu.GTCEu; @@ -121,6 +123,8 @@ public static void onPlayerDeath(LivingDeathEvent event) { @SubscribeEvent public static void registerCommand(RegisterCommandsEvent event) { WirelessEnergyCommand.register(event.getDispatcher(), event.getBuildContext()); + ReflectionCommand.register(event.getDispatcher()); + ReflectionCommands.register(event.getDispatcher()); } @SubscribeEvent diff --git a/src/main/java/com/ghostipedia/cosmiccore/gtbridge/CosmicRecipeTypes.java b/src/main/java/com/ghostipedia/cosmiccore/gtbridge/CosmicRecipeTypes.java index 2dc71711f..7e771bcbf 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/gtbridge/CosmicRecipeTypes.java +++ b/src/main/java/com/ghostipedia/cosmiccore/gtbridge/CosmicRecipeTypes.java @@ -343,6 +343,13 @@ public class CosmicRecipeTypes { .setMaxIOSize(16, 16, 16, 16) // .setSound(CosmicSounds.BLACK_HOLE_CRY) .setProgressBar(GuiTextures.PROGRESS_BAR_ARROW, ProgressTexture.FillDirection.LEFT_TO_RIGHT); + + // Stellar Iris Module Recipe Types + public static final GTRecipeType STELLAR_SMELTING = GTRecipeTypes + .register("stellar_smelting", GTRecipeTypes.MULTIBLOCK) + .setMaxIOSize(9, 9, 3, 3) + .setProgressBar(GuiTextures.PROGRESS_BAR_ARROW, ProgressTexture.FillDirection.LEFT_TO_RIGHT); + public static final GTRecipeType CHROMATIC_DISTILLATION_PLANT = GTRecipeTypes .register("chormatic_distillation_plant", GTRecipeTypes.MULTIBLOCK) .setMaxIOSize(1, 1, 1, 16) @@ -444,6 +451,16 @@ public class CosmicRecipeTypes { .setHasResearchSlot(true) .setSound(GTSoundEntries.REPLICATOR) // TODO - Sounds .setProgressBar(GuiTextures.PROGRESS_BAR_ARROW_MULTIPLE, ProgressTexture.FillDirection.LEFT_TO_RIGHT); + + /** + * Recipe type for the Multithreaded Processor test machine. + * This machine can run multiple unique recipes simultaneously using color-coded input buses. + */ + public static final GTRecipeType MULTITHREADED_PROCESSOR = GTRecipeTypes + .register("dream_basin", GTRecipeTypes.MULTIBLOCK) + .setMaxIOSize(9, 9, 9, 9) + .setSound(GTSoundEntries.ASSEMBLER) + .setProgressBar(GuiTextures.PROGRESS_BAR_ARROW_MULTIPLE, ProgressTexture.FillDirection.LEFT_TO_RIGHT); /* * TODO - Allow This block to replace the Master Ritual stone, and then set the structure shape based on the ritual */ diff --git a/src/main/java/com/ghostipedia/cosmiccore/integration/jade/CCJadePlugin.java b/src/main/java/com/ghostipedia/cosmiccore/integration/jade/CCJadePlugin.java index c1b4f2465..ec10d8005 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/integration/jade/CCJadePlugin.java +++ b/src/main/java/com/ghostipedia/cosmiccore/integration/jade/CCJadePlugin.java @@ -3,6 +3,7 @@ import com.ghostipedia.cosmiccore.integration.jade.provider.DroneMaintenanceInterfaceProvider; import com.ghostipedia.cosmiccore.integration.jade.provider.DroneStationProvider; import com.ghostipedia.cosmiccore.integration.jade.provider.PCBParallelProvider; +import com.ghostipedia.cosmiccore.integration.jade.provider.StellarModuleProvider; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; @@ -20,6 +21,7 @@ public void register(IWailaCommonRegistration registration) { registration.registerBlockDataProvider(new DroneStationProvider(), BlockEntity.class); registration.registerBlockDataProvider(new DroneMaintenanceInterfaceProvider(), BlockEntity.class); registration.registerBlockDataProvider(new PCBParallelProvider(), BlockEntity.class); + registration.registerBlockDataProvider(new StellarModuleProvider(), BlockEntity.class); } @Override @@ -27,5 +29,6 @@ public void registerClient(IWailaClientRegistration registration) { registration.registerBlockComponent(new DroneStationProvider(), Block.class); registration.registerBlockComponent(new DroneMaintenanceInterfaceProvider(), Block.class); registration.registerBlockComponent(new PCBParallelProvider(), Block.class); + registration.registerBlockComponent(new StellarModuleProvider(), Block.class); } } diff --git a/src/main/java/com/ghostipedia/cosmiccore/integration/jade/provider/StellarModuleProvider.java b/src/main/java/com/ghostipedia/cosmiccore/integration/jade/provider/StellarModuleProvider.java new file mode 100644 index 000000000..baa1c4e88 --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/integration/jade/provider/StellarModuleProvider.java @@ -0,0 +1,122 @@ +package com.ghostipedia.cosmiccore.integration.jade.provider; + +import com.ghostipedia.cosmiccore.CosmicCore; +import com.ghostipedia.cosmiccore.api.machine.feature.IStellarIrisProvider; +import com.ghostipedia.cosmiccore.api.machine.multiblock.StellarBaseModule; + +import com.gregtechceu.gtceu.api.machine.MetaMachine; +import com.gregtechceu.gtceu.api.machine.feature.multiblock.IMultiController; +import com.gregtechceu.gtceu.integration.jade.provider.CapabilityBlockProvider; + +import net.minecraft.ChatFormatting; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +import org.jetbrains.annotations.Nullable; +import snownee.jade.api.BlockAccessor; +import snownee.jade.api.ITooltip; +import snownee.jade.api.config.IPluginConfig; + +public class StellarModuleProvider extends CapabilityBlockProvider { + + public StellarModuleProvider() { + super(CosmicCore.id("stellar_module")); + } + + @Nullable + @Override + protected StellarBaseModule getCapability(Level level, BlockPos blockPos, @Nullable Direction direction) { + if (MetaMachine.getMachine(level, blockPos) instanceof IMultiController controller) { + if (controller instanceof StellarBaseModule module) { + return module; + } + } + return null; + } + + @Override + protected void write(CompoundTag tag, StellarBaseModule module) { + IStellarIrisProvider iris = module.getStellarIris(); + boolean connected = iris != null && iris.isFormed(); + boolean canProcess = connected && iris.canProcess(); + + tag.putBoolean("connected", connected); + tag.putBoolean("canProcess", canProcess); + tag.putBoolean("wirelessAvailable", module.isWirelessEnergyAvailable()); + tag.putLong("energyPerTick", module.getEnergyConsumedPerTick()); + + if (iris != null) { + tag.putString("stage", iris.getStage().toString()); + if (canProcess) { + tag.putDouble("speedBonus", iris.getSpeedBonus()); + tag.putInt("parallel", iris.getParallelLimit()); + } + } + } + + @Override + protected void addTooltip(CompoundTag tag, ITooltip tooltip, Player player, BlockAccessor accessor, + BlockEntity blockEntity, IPluginConfig config) { + // Only show tooltip if we have data (i.e., this is actually a StellarBaseModule) + if (!tag.contains("connected")) { + return; + } + + boolean connected = tag.getBoolean("connected"); + boolean canProcess = tag.getBoolean("canProcess"); + boolean wirelessAvailable = tag.getBoolean("wirelessAvailable"); + + // Iris connection status + if (!connected) { + tooltip.add(Component.translatable("cosmiccore.jade.stellar_module.not_connected") + .setStyle(Style.EMPTY.withColor(ChatFormatting.RED))); + } else if (!canProcess) { + tooltip.add(Component.translatable("cosmiccore.jade.stellar_module.iris_not_ready") + .setStyle(Style.EMPTY.withColor(ChatFormatting.YELLOW))); + if (tag.contains("stage")) { + tooltip.add(Component.translatable("cosmiccore.jade.stellar_module.stage", + tag.getString("stage")) + .setStyle(Style.EMPTY.withColor(ChatFormatting.GRAY))); + } + } else { + tooltip.add(Component.translatable("cosmiccore.jade.stellar_module.connected") + .setStyle(Style.EMPTY.withColor(ChatFormatting.GREEN))); + if (tag.contains("stage")) { + tooltip.add(Component.translatable("cosmiccore.jade.stellar_module.stage", + tag.getString("stage")) + .setStyle(Style.EMPTY.withColor(ChatFormatting.AQUA))); + } + if (tag.contains("speedBonus")) { + tooltip.add(Component.translatable("cosmiccore.jade.stellar_module.speed_bonus", + String.format("%.1fx", tag.getDouble("speedBonus"))) + .setStyle(Style.EMPTY.withColor(ChatFormatting.GREEN))); + } + } + + // Wireless energy status + if (!wirelessAvailable) { + tooltip.add(Component.translatable("cosmiccore.jade.stellar_module.no_wireless") + .setStyle(Style.EMPTY.withColor(ChatFormatting.RED))); + } else { + long euPerTick = tag.getLong("energyPerTick"); + if (euPerTick > 0) { + tooltip.add(Component.translatable("cosmiccore.jade.stellar_module.energy_usage", + formatEnergy(euPerTick)) + .setStyle(Style.EMPTY.withColor(ChatFormatting.YELLOW))); + } + } + } + + private String formatEnergy(long eu) { + if (eu >= 1_000_000_000) return String.format("%.1fG EU/t", eu / 1_000_000_000.0); + if (eu >= 1_000_000) return String.format("%.1fM EU/t", eu / 1_000_000.0); + if (eu >= 1000) return String.format("%.1fk EU/t", eu / 1000.0); + return String.format("%d EU/t", eu); + } +} diff --git a/src/main/java/com/ghostipedia/cosmiccore/mixin/AdAstraSpaceSuitItemMixin.java b/src/main/java/com/ghostipedia/cosmiccore/mixin/AdAstraSpaceSuitItemMixin.java index 15915ba67..87485e638 100644 --- a/src/main/java/com/ghostipedia/cosmiccore/mixin/AdAstraSpaceSuitItemMixin.java +++ b/src/main/java/com/ghostipedia/cosmiccore/mixin/AdAstraSpaceSuitItemMixin.java @@ -5,11 +5,15 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; import earth.terrarium.adastra.common.items.armor.SpaceSuitItem; import org.spongepowered.asm.mixin.Debug; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Debug(export = true) @Mixin(value = SpaceSuitItem.class, remap = false) @@ -29,4 +33,14 @@ public static long getOxygenAmount(Entity entity) { return suit.getFluidContainer(stack).getFirstFluid().getFluidAmount(); } else return 0; } + + /** + * Prevent Ad Astra from consuming oxygen in inventoryTick. + * CosmicCore's OxygenLogic handles all oxygen consumption to avoid double-dipping. + */ + @Inject(method = "consumeOxygen", at = @At("HEAD"), cancellable = true) + private void cosmiccore$preventDoubleOxygenConsumption(ItemStack stack, long amount, CallbackInfo ci) { + // Cancel Ad Astra's oxygen consumption - OxygenLogic handles it + ci.cancel(); + } } diff --git a/src/main/java/com/ghostipedia/cosmiccore/mixin/frontiers/MinecraftRemoveOxygenMixin.java b/src/main/java/com/ghostipedia/cosmiccore/mixin/frontiers/MinecraftRemoveOxygenMixin.java new file mode 100644 index 000000000..1b0007d0b --- /dev/null +++ b/src/main/java/com/ghostipedia/cosmiccore/mixin/frontiers/MinecraftRemoveOxygenMixin.java @@ -0,0 +1,33 @@ +package com.ghostipedia.cosmiccore.mixin.frontiers; + +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; + +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; + +/** + * Disables vanilla Minecraft's air/drowning system for players. + * CosmicCore's oxygen system handles all breathing mechanics instead. + */ +@Mixin(LivingEntity.class) +public class MinecraftRemoveOxygenMixin { + + @Inject(method = "decreaseAirSupply", at = @At("HEAD"), cancellable = true) + private void cosmiccore$noPlayerAirDecrease(int currentAir, CallbackInfoReturnable cir) { + LivingEntity self = (LivingEntity) (Object) this; + if (self instanceof Player) { + cir.setReturnValue(self.getMaxAirSupply()); // stays full + } + } + + @Inject(method = "increaseAirSupply", at = @At("HEAD"), cancellable = true) + private void cosmiccore$noPlayerAirIncrease(int currentAir, CallbackInfoReturnable cir) { + LivingEntity self = (LivingEntity) (Object) this; + if (self instanceof Player) { + cir.setReturnValue(self.getMaxAirSupply()); // stays full + } + } +} diff --git a/src/main/resources/assets/cosmiccore/shaders/core/galaxy_bg.fsh b/src/main/resources/assets/cosmiccore/shaders/core/galaxy_bg.fsh new file mode 100644 index 000000000..b82c44659 --- /dev/null +++ b/src/main/resources/assets/cosmiccore/shaders/core/galaxy_bg.fsh @@ -0,0 +1,140 @@ +#version 150 + +in vec2 texCoord; +out vec4 fragColor; + +uniform float GameTime; +uniform vec2 ScreenSize; +uniform float Intensity; + +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} + +float hash21(vec2 p) { + p = fract(p * vec2(234.34, 435.345)); + p += dot(p, p + 34.23); + return fract(p.x * p.y); +} + +float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +float fbm(vec2 p) { + float value = 0.0; + float amplitude = 0.5; + for (int i = 0; i < 6; i++) { + value += amplitude * noise(p); + p *= 2.0; + amplitude *= 0.5; + } + return value; +} + +float stars(vec2 uv, float density, float seed) { + vec2 gv = fract(uv * density) - 0.5; + vec2 id = floor(uv * density); + + float star = 0.0; + float rnd = hash21(id + seed); + + if (rnd > 0.8) { + float size = (rnd - 0.8) * 5.0; + star = smoothstep(0.1 * size + 0.02, 0.0, length(gv)); + } + + return star; +} + +void main() { + float time = GameTime * 1200.0; + + vec2 uv = texCoord; + vec2 aspect = vec2(ScreenSize.x / ScreenSize.y, 1.0); + vec2 centered = (uv - 0.5) * aspect; + + vec3 color = vec3(0.01, 0.01, 0.02); + + // Nebula clouds + vec2 nebulaUV1 = centered * 2.0 + vec2(time * 0.005, time * 0.003); + vec2 nebulaUV2 = centered * 1.5 + vec2(-time * 0.004, time * 0.006); + vec2 nebulaUV3 = centered * 3.0 + vec2(time * 0.003, -time * 0.004); + + float nebula1 = fbm(nebulaUV1); + float nebula2 = fbm(nebulaUV2); + float nebula3 = fbm(nebulaUV3); + + nebula1 = pow(nebula1, 2.0) * smoothstep(0.3, 0.7, nebula1); + nebula2 = pow(nebula2, 2.5) * smoothstep(0.35, 0.75, nebula2); + nebula3 = pow(nebula3, 2.0) * smoothstep(0.25, 0.6, nebula3); + + vec3 nebulaColor1 = vec3(0.15, 0.05, 0.25); + vec3 nebulaColor2 = vec3(0.05, 0.10, 0.20); + vec3 nebulaColor3 = vec3(0.20, 0.08, 0.12); + + color += nebulaColor1 * nebula1 * 0.6; + color += nebulaColor2 * nebula2 * 0.5; + color += nebulaColor3 * nebula3 * 0.4; + + // Bright nebula cores + float brightCore1 = pow(nebula1, 4.0) * 2.0; + float brightCore2 = pow(nebula2, 4.0) * 1.5; + + color += vec3(0.4, 0.2, 0.5) * brightCore1 * 0.3; + color += vec3(0.2, 0.3, 0.5) * brightCore2 * 0.25; + + // Cosmic dust + vec2 dustUV = centered * 4.0 + vec2(time * 0.002, 0.0); + float dust = fbm(dustUV); + dust = smoothstep(0.4, 0.6, dust); + color *= 1.0 - dust * 0.3; + + // Star layers + float starLayer1 = stars(uv + vec2(0.0, time * 0.001), 80.0, 1.0); + color += vec3(0.6, 0.6, 0.7) * starLayer1 * 0.3; + + float starLayer2 = stars(uv + vec2(time * 0.002, 0.0), 40.0, 2.0); + color += vec3(0.8, 0.8, 0.9) * starLayer2 * 0.5; + + float starLayer3 = stars(uv, 20.0, 3.0); + float twinkle = sin(time * 0.5 + hash(floor(uv * 20.0)) * 6.28) * 0.3 + 0.7; + color += vec3(1.0, 0.95, 0.9) * starLayer3 * twinkle * 0.8; + + // Colored stars + float coloredStar = stars(uv + 0.5, 15.0, 4.0); + vec3 starColor = mix( + vec3(1.0, 0.7, 0.5), + vec3(0.7, 0.8, 1.0), + hash(floor(uv * 15.0 + 0.5)) + ); + color += starColor * coloredStar * 0.6; + + // Central glow + float coreGlow = exp(-length(centered) * 2.0) * 0.15; + color += vec3(0.3, 0.25, 0.35) * coreGlow; + + // Shimmer + float shimmer = sin(time * 0.1 + fbm(centered * 5.0) * 6.28) * 0.02 + 1.0; + color *= shimmer; + + // Vignette + float vignette = 1.0 - length(centered) * 0.4; + vignette = clamp(vignette, 0.0, 1.0); + vignette = pow(vignette, 1.2); + color *= 0.7 + vignette * 0.3; + + color *= Intensity; + color = clamp(color, 0.0, 1.0); + + fragColor = vec4(color, 1.0); +} diff --git a/src/main/resources/assets/cosmiccore/shaders/core/galaxy_bg.json b/src/main/resources/assets/cosmiccore/shaders/core/galaxy_bg.json new file mode 100644 index 000000000..af62135fa --- /dev/null +++ b/src/main/resources/assets/cosmiccore/shaders/core/galaxy_bg.json @@ -0,0 +1,21 @@ +{ + "blend": { + "func": "add", + "srcrgb": "one", + "dstrgb": "zero" + }, + "vertex": "cosmiccore:galaxy_bg", + "fragment": "cosmiccore:galaxy_bg", + "attributes": [ + "Position", + "UV0" + ], + "samplers": [], + "uniforms": [ + { "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": "GameTime", "type": "float", "count": 1, "values": [ 0.0 ] }, + { "name": "ScreenSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] }, + { "name": "Intensity", "type": "float", "count": 1, "values": [ 1.0 ] } + ] +} diff --git a/src/main/resources/assets/cosmiccore/shaders/core/galaxy_bg.vsh b/src/main/resources/assets/cosmiccore/shaders/core/galaxy_bg.vsh new file mode 100644 index 000000000..e58d02c9d --- /dev/null +++ b/src/main/resources/assets/cosmiccore/shaders/core/galaxy_bg.vsh @@ -0,0 +1,16 @@ +#version 150 + +#moj_import + +in vec3 Position; +in vec2 UV0; + +out vec2 texCoord; + +uniform mat4 ModelViewMat; +uniform mat4 ProjMat; + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + texCoord = UV0; +} diff --git a/src/main/resources/assets/cosmiccore/shaders/core/soul_aura.fsh b/src/main/resources/assets/cosmiccore/shaders/core/soul_aura.fsh new file mode 100644 index 000000000..dd54efe41 --- /dev/null +++ b/src/main/resources/assets/cosmiccore/shaders/core/soul_aura.fsh @@ -0,0 +1,132 @@ +#version 150 + +in vec2 texCoord; +out vec4 fragColor; + +uniform float GameTime; +uniform vec2 ScreenSize; +uniform vec2 Center; +uniform vec3 BaseColor; +uniform float Intensity; +uniform float Radius; + +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} + +void main() { + float time = GameTime * 1200.0; + + vec2 aspectRatio = vec2(ScreenSize.x / ScreenSize.y, 1.0); + vec2 uv = (texCoord - Center) * aspectRatio; + + float dist = length(uv); + + float alpha = 0.0; + vec3 color = BaseColor; + + // Slow water ripples + float ringSpeed = 0.12; + for (int i = 0; i < 3; i++) { + float phase = float(i) * 2.5 + time * ringSpeed; + float ringDist = mod(phase, Radius * 2.8); + float ringAlpha = 1.0 - ringDist / (Radius * 2.8); + ringAlpha = ringAlpha * ringAlpha * ringAlpha; + float ring = smoothstep(0.06, 0.0, abs(dist - ringDist)) * ringAlpha; + alpha += ring * 0.15; + } + + // Orbiting wisps + for (int i = 0; i < 4; i++) { + float fi = float(i); + float orbitSpeed = 0.3 + fi * 0.1; + float orbitRadius = Radius * (0.7 + fi * 0.15); + float orbitAngle = time * orbitSpeed + fi * 1.571; + + vec2 wispPos = vec2(cos(orbitAngle), sin(orbitAngle)) * orbitRadius; + float wispDist = length(uv - wispPos); + + float wispCore = smoothstep(Radius * 0.2, 0.0, wispDist); + wispCore = wispCore * wispCore; + + float trailAngle = orbitAngle - 0.4; + vec2 trailPos = vec2(cos(trailAngle), sin(trailAngle)) * orbitRadius; + float trailDist = length(uv - trailPos); + float trail = smoothstep(Radius * 0.25, 0.0, trailDist) * 0.3; + + alpha += (wispCore + trail) * 0.2; + } + + // Aurora flow (cartesian to avoid atan discontinuity) + float flow1 = sin(uv.x * 8.0 + uv.y * 6.0 + time * 0.6 + dist * 4.0) * 0.5 + 0.5; + flow1 = flow1 * flow1; + flow1 *= smoothstep(Radius * 1.6, Radius * 0.6, dist); + flow1 *= smoothstep(Radius * 0.3, Radius * 0.7, dist); + + float flow2 = sin(uv.x * 6.0 - uv.y * 8.0 - time * 0.4 + dist * 3.0 + 2.0) * 0.5 + 0.5; + flow2 = flow2 * flow2; + flow2 *= smoothstep(Radius * 1.4, Radius * 0.5, dist); + flow2 *= smoothstep(Radius * 0.25, Radius * 0.6, dist); + + alpha += (flow1 + flow2) * 0.1; + + // Floating embers + for (int i = 0; i < 6; i++) { + float fi = float(i); + float seed = fi * 127.1; + + float px = (hash(vec2(seed, 0.0)) - 0.5) * Radius * 1.6; + float baseY = hash(vec2(seed, 1.0)) * Radius * 2.0; + float py = mod(baseY - time * 0.15 - fi * 0.15, Radius * 2.5) - Radius * 0.3; + px += sin(time * 0.3 + fi * 2.0) * Radius * 0.08; + + vec2 emberPos = vec2(px, -py); + float emberDist = length(uv - emberPos); + + float ember = smoothstep(Radius * 0.1, 0.0, emberDist); + ember = ember * ember; + + float heightFade = smoothstep(Radius * 1.8, Radius * 0.3, -py); + + alpha += ember * heightFade * 0.25; + color = mix(color, vec3(1.0, 0.95, 0.85), ember * heightFade * 0.2); + } + + // Soft tendrils + for (int i = 0; i < 4; i++) { + float fi = float(i); + float tendrilAngle = fi * 1.571 + time * 0.1 + sin(time * 0.2 + fi) * 0.2; + + float alongTendril = dot(uv, vec2(cos(tendrilAngle), sin(tendrilAngle))); + float perpTendril = length(uv - vec2(cos(tendrilAngle), sin(tendrilAngle)) * max(alongTendril, 0.0)); + + float tendrilWidth = Radius * 0.1 * (1.0 - alongTendril / (Radius * 1.8)); + tendrilWidth = max(tendrilWidth, 0.02); + + float tendril = smoothstep(tendrilWidth, 0.0, perpTendril); + tendril *= smoothstep(0.0, Radius * 0.5, alongTendril); + tendril *= smoothstep(Radius * 1.6, Radius * 0.7, alongTendril); + tendril *= 0.6 + 0.4 * sin(alongTendril * 5.0 - time * 1.5); + + alpha += tendril * 0.1; + } + + // Core glow + float core = 1.0 - smoothstep(0.0, Radius * 0.6, dist); + core = pow(core, 1.8); + alpha += core * 0.25; + + // Outer glow + float outerGlow = 1.0 - smoothstep(Radius * 0.2, Radius * 1.5, dist); + outerGlow = outerGlow * outerGlow; + alpha += outerGlow * 0.15; + + alpha *= Intensity; + + float boundary = 1.0 - smoothstep(Radius * 1.3, Radius * 2.0, dist); + alpha *= boundary; + + alpha = clamp(alpha, 0.0, 1.0); + + fragColor = vec4(color, alpha); +} diff --git a/src/main/resources/assets/cosmiccore/shaders/core/soul_aura.json b/src/main/resources/assets/cosmiccore/shaders/core/soul_aura.json new file mode 100644 index 000000000..209d07c1f --- /dev/null +++ b/src/main/resources/assets/cosmiccore/shaders/core/soul_aura.json @@ -0,0 +1,24 @@ +{ + "blend": { + "func": "add", + "srcrgb": "srcalpha", + "dstrgb": "one" + }, + "vertex": "cosmiccore:soul_aura", + "fragment": "cosmiccore:soul_aura", + "attributes": [ + "Position", + "UV0" + ], + "samplers": [], + "uniforms": [ + { "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": "GameTime", "type": "float", "count": 1, "values": [ 0.0 ] }, + { "name": "ScreenSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] }, + { "name": "Center", "type": "float", "count": 2, "values": [ 0.5, 0.5 ] }, + { "name": "BaseColor", "type": "float", "count": 3, "values": [ 1.0, 0.85, 0.5 ] }, + { "name": "Intensity", "type": "float", "count": 1, "values": [ 1.0 ] }, + { "name": "Radius", "type": "float", "count": 1, "values": [ 0.15 ] } + ] +} diff --git a/src/main/resources/assets/cosmiccore/shaders/core/soul_aura.vsh b/src/main/resources/assets/cosmiccore/shaders/core/soul_aura.vsh new file mode 100644 index 000000000..e58d02c9d --- /dev/null +++ b/src/main/resources/assets/cosmiccore/shaders/core/soul_aura.vsh @@ -0,0 +1,16 @@ +#version 150 + +#moj_import + +in vec3 Position; +in vec2 UV0; + +out vec2 texCoord; + +uniform mat4 ModelViewMat; +uniform mat4 ProjMat; + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + texCoord = UV0; +} diff --git a/src/main/resources/assets/cosmiccore/shaders/core/void_bg.fsh b/src/main/resources/assets/cosmiccore/shaders/core/void_bg.fsh new file mode 100644 index 000000000..eb6360eec --- /dev/null +++ b/src/main/resources/assets/cosmiccore/shaders/core/void_bg.fsh @@ -0,0 +1,119 @@ +#version 150 + +in vec2 texCoord; +out vec4 fragColor; + +uniform float GameTime; +uniform vec2 ScreenSize; +uniform float Intensity; + +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} + +float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +float fbm(vec2 p) { + float value = 0.0; + float amplitude = 0.5; + for (int i = 0; i < 5; i++) { + value += amplitude * noise(p); + p *= 2.0; + amplitude *= 0.5; + } + return value; +} + +void main() { + float time = GameTime * 1200.0; + + vec2 uv = texCoord; + vec2 aspect = vec2(ScreenSize.x / ScreenSize.y, 1.0); + vec2 centered = (uv - 0.5) * aspect; + + vec3 color = vec3(0.0); + + // Void mist + vec2 mistUV1 = centered * 1.5 + vec2(time * 0.02, time * 0.01); + vec2 mistUV2 = centered * 2.0 + vec2(-time * 0.015, time * 0.025); + + float mist1 = fbm(mistUV1) * 0.5 + 0.5; + float mist2 = fbm(mistUV2) * 0.5 + 0.5; + float mist = mist1 * mist2; + + vec3 mistColor = vec3(0.08, 0.04, 0.12); + color += mistColor * mist * 0.4; + + // Drifting wisps + for (int i = 0; i < 4; i++) { + float fi = float(i); + float wispTime = time * 0.03 + fi * 1.57; + + vec2 wispCenter = vec2( + sin(wispTime * 0.7 + fi * 2.0) * 0.3, + cos(wispTime * 0.5 + fi * 1.5) * 0.25 + ); + + vec2 toWisp = centered - wispCenter; + float wispDist = length(toWisp); + float wisp = exp(-wispDist * 4.0) * 0.3; + + vec3 wispColor = mix( + vec3(0.15, 0.08, 0.20), + vec3(0.10, 0.12, 0.18), + sin(fi * 1.5) * 0.5 + 0.5 + ); + + color += wispColor * wisp; + } + + // Energy tendrils + float tendrilNoise = fbm(centered * 3.0 + vec2(time * 0.05, 0.0)); + tendrilNoise = pow(tendrilNoise, 3.0); + + vec3 tendrilColor = vec3(0.12, 0.06, 0.15); + color += tendrilColor * tendrilNoise * 0.2; + + // Floating dust + for (int i = 0; i < 8; i++) { + float fi = float(i); + float seed = fi * 127.1; + + float px = (hash(vec2(seed, 0.0)) - 0.5) * 1.5; + float py = mod(hash(vec2(seed, 1.0)) - time * 0.02 - fi * 0.05, 1.5) - 0.75; + px += sin(time * 0.1 + fi * 2.0) * 0.05; + + vec2 particlePos = vec2(px, py); + float particleDist = length(centered - particlePos); + + float particle = exp(-particleDist * 50.0) * 0.3; + float twinkle = sin(time * (0.5 + fi * 0.2) + fi * 3.0) * 0.3 + 0.7; + + color += vec3(0.3, 0.25, 0.35) * particle * twinkle; + } + + // Ambient pulse + float pulse = sin(time * 0.15) * 0.02 + 0.98; + color *= pulse; + + // Vignette + float vignette = 1.0 - length(centered) * 0.6; + vignette = clamp(vignette, 0.0, 1.0); + vignette = pow(vignette, 1.5); + color *= vignette; + + color *= Intensity; + + fragColor = vec4(color, 1.0); +} diff --git a/src/main/resources/assets/cosmiccore/shaders/core/void_bg.json b/src/main/resources/assets/cosmiccore/shaders/core/void_bg.json new file mode 100644 index 000000000..2425414e1 --- /dev/null +++ b/src/main/resources/assets/cosmiccore/shaders/core/void_bg.json @@ -0,0 +1,21 @@ +{ + "blend": { + "func": "add", + "srcrgb": "one", + "dstrgb": "zero" + }, + "vertex": "cosmiccore:void_bg", + "fragment": "cosmiccore:void_bg", + "attributes": [ + "Position", + "UV0" + ], + "samplers": [], + "uniforms": [ + { "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": "GameTime", "type": "float", "count": 1, "values": [ 0.0 ] }, + { "name": "ScreenSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] }, + { "name": "Intensity", "type": "float", "count": 1, "values": [ 1.0 ] } + ] +} diff --git a/src/main/resources/assets/cosmiccore/shaders/core/void_bg.vsh b/src/main/resources/assets/cosmiccore/shaders/core/void_bg.vsh new file mode 100644 index 000000000..e58d02c9d --- /dev/null +++ b/src/main/resources/assets/cosmiccore/shaders/core/void_bg.vsh @@ -0,0 +1,16 @@ +#version 150 + +#moj_import + +in vec3 Position; +in vec2 UV0; + +out vec2 texCoord; + +uniform mat4 ModelViewMat; +uniform mat4 ProjMat; + +void main() { + gl_Position = ProjMat * ModelViewMat * vec4(Position, 1.0); + texCoord = UV0; +} diff --git a/src/main/resources/assets/cosmiccore/textures/gui/oxygen_bg.png b/src/main/resources/assets/cosmiccore/textures/gui/oxygen_bg.png new file mode 100644 index 000000000..4d9c51484 Binary files /dev/null and b/src/main/resources/assets/cosmiccore/textures/gui/oxygen_bg.png differ diff --git a/src/main/resources/assets/cosmiccore/textures/gui/oxygen_fill.png b/src/main/resources/assets/cosmiccore/textures/gui/oxygen_fill.png new file mode 100644 index 000000000..eb386aa8f Binary files /dev/null and b/src/main/resources/assets/cosmiccore/textures/gui/oxygen_fill.png differ diff --git a/src/main/resources/assets/cosmiccore/textures/item/bronze_supply_tank.png b/src/main/resources/assets/cosmiccore/textures/item/bronze_supply_tank.png new file mode 100644 index 000000000..01e2673a9 Binary files /dev/null and b/src/main/resources/assets/cosmiccore/textures/item/bronze_supply_tank.png differ diff --git a/src/main/resources/assets/cosmiccore/textures/item/pressurized_rebreather.png b/src/main/resources/assets/cosmiccore/textures/item/pressurized_rebreather.png new file mode 100644 index 000000000..be1c7b687 Binary files /dev/null and b/src/main/resources/assets/cosmiccore/textures/item/pressurized_rebreather.png differ diff --git a/src/main/resources/assets/cosmiccore/textures/item/reflection_mirror.png b/src/main/resources/assets/cosmiccore/textures/item/reflection_mirror.png new file mode 100644 index 000000000..f66098920 Binary files /dev/null and b/src/main/resources/assets/cosmiccore/textures/item/reflection_mirror.png differ diff --git a/src/main/resources/assets/cosmiccore/textures/item/simple_rebreather.png b/src/main/resources/assets/cosmiccore/textures/item/simple_rebreather.png new file mode 100644 index 000000000..929f21035 Binary files /dev/null and b/src/main/resources/assets/cosmiccore/textures/item/simple_rebreather.png differ diff --git a/src/main/resources/assets/cosmiccore/textures/item/steel_supply_tank.png b/src/main/resources/assets/cosmiccore/textures/item/steel_supply_tank.png new file mode 100644 index 000000000..f3c8e7913 Binary files /dev/null and b/src/main/resources/assets/cosmiccore/textures/item/steel_supply_tank.png differ diff --git a/src/main/resources/cosmiccore.mixins.json b/src/main/resources/cosmiccore.mixins.json index 0d6ac907d..c7c1d7131 100644 --- a/src/main/resources/cosmiccore.mixins.json +++ b/src/main/resources/cosmiccore.mixins.json @@ -28,6 +28,7 @@ "MAE2GTIntegrationMixin", "PlayerBreathingMixin", "TagPrefixItemMixin", + "frontiers.MinecraftRemoveOxygenMixin", "accessor.GTMMEBufferAccessor", "accessor.LivingEntityAccessor", "accessor.LootTableAccessor", diff --git a/src/main/resources/data/cosmiccore/curios/entities/back.json b/src/main/resources/data/cosmiccore/curios/entities/back.json new file mode 100644 index 000000000..6cd82c21a --- /dev/null +++ b/src/main/resources/data/cosmiccore/curios/entities/back.json @@ -0,0 +1,4 @@ +{ + "entities": ["player"], + "slots": ["back"] +} diff --git a/src/main/resources/data/cosmiccore/curios/slots/back.json b/src/main/resources/data/cosmiccore/curios/slots/back.json new file mode 100644 index 000000000..66a689dc0 --- /dev/null +++ b/src/main/resources/data/cosmiccore/curios/slots/back.json @@ -0,0 +1,4 @@ +{ + "size": 2, + "icon": "curios:slot/empty_curio_slot" +} diff --git a/src/main/resources/data/curios/tags/items/back.json b/src/main/resources/data/curios/tags/items/back.json new file mode 100644 index 000000000..78896137f --- /dev/null +++ b/src/main/resources/data/curios/tags/items/back.json @@ -0,0 +1,6 @@ +{ + "values": [ + "cosmiccore:bronze_supply_tank", + "cosmiccore:steel_supply_tank" + ] +} diff --git a/src/main/resources/data/curios/tags/items/head.json b/src/main/resources/data/curios/tags/items/head.json index cab9d6d64..1df6d8775 100644 --- a/src/main/resources/data/curios/tags/items/head.json +++ b/src/main/resources/data/curios/tags/items/head.json @@ -1,5 +1,7 @@ { "values": [ - "cosmiccore:space_radio" + "cosmiccore:space_radio", + "cosmiccore:simple_rebreather", + "cosmiccore:pressurized_rebreather" ] }