Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a9263d273 | ||
|
|
4a74b6dfed | ||
|
|
06da8fbd2b | ||
|
|
bd747afbe0 | ||
|
|
7e459e3615 | ||
|
|
67220fa0c7 | ||
|
|
a345db5aeb | ||
|
|
0bd60d01cc | ||
|
|
94e6cf380d | ||
|
|
43962f4857 | ||
|
|
5af573deb4 | ||
|
|
e552bd68fd | ||
|
|
69dd9f919d | ||
|
|
2d212ae6a1 | ||
|
|
2b614ce5a8 | ||
|
|
99ac5fc3a7 | ||
|
|
ab82c3929e | ||
|
|
4820c736f4 | ||
|
|
a16347140a | ||
|
|
75fa88cfc5 | ||
|
|
f04aeb02b2 | ||
|
|
4ca42cb18a | ||
|
|
985e8b9192 | ||
|
|
1d8ab0b30a | ||
|
|
083c3449d5 | ||
|
|
51d630037d | ||
|
|
341f1eb8e5 | ||
|
|
10b606d01d | ||
|
|
b39c271c96 | ||
|
|
a16156b005 | ||
|
|
2518c20071 | ||
|
|
b7497c81d1 | ||
|
|
97638e0fa2 | ||
|
|
a071a0b746 | ||
|
|
6fcc56552c | ||
|
|
66944ec492 | ||
|
|
b513ab1262 | ||
|
|
b2222c5d33 | ||
|
|
2d0be21c5b | ||
|
|
a0b0788869 | ||
|
|
106d730dd8 | ||
|
|
21dd089486 | ||
|
|
e3b6c6ed01 | ||
|
|
7387096d98 | ||
|
|
457f91b92e | ||
|
|
f283c1705b | ||
|
|
6ee5cdc2fb | ||
|
|
87428c5f0d | ||
|
|
0599bf6885 | ||
|
|
624f441578 | ||
|
|
53fa32bbc5 |
@@ -6,6 +6,98 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
|
||||
- Depuis `26.2.0-alpha.6`, le pack et les modules ont des versions indépendantes. I Like To Move It et Only Fun deviennent les cinquième et sixième modules actifs dans l'alpha 46.
|
||||
- Les versions ci-dessous ont été reconstituées à partir des JAR archivés, des packs locaux et du code source actuel. Les dates n'étant pas enregistrées de manière fiable, elles ne sont pas inventées.
|
||||
|
||||
## Non publié
|
||||
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.135`** et **Red-Stoner `0.0.0-alpha.17`**.
|
||||
|
||||
- attribue les demandes de quêtes personnalisées terminées à leur preneur directement sur le panneau et dans les détails au clic, avec son pseudonyme et sa couleur conservés ;
|
||||
- répartit les lits de sortie rares sur chacun des cinq niveaux de la Backroom, avec exactement un lit par cellule de 10 × 10 chunks et par niveau dans les nouveaux chunks ;
|
||||
- conserve les IDs, les données Sanctuary v14 et le protocole réseau 24 ; les chunks Backroom déjà générés restent inchangés.
|
||||
- ajoute l’API publique Sanctuary de métriques et de classements persistants par UUID, avec import monotone des statistiques vanilla, podium borné et exclusion des actions créatives, spectateur ou simulées ;
|
||||
- ajoute `redstoner:trophy_panel`, panneau mural configurable par son propriétaire avec un item non consommé, pages publiques, rendu en monde et rafraîchissement serveur limité à une fois par seconde ;
|
||||
- passe Sanctuary aux données v15 et Red-Stoner aux données v4 sans nouveau payload ni changement de leurs protocoles réseau 24 et 7.
|
||||
|
||||
## `26.2.0-alpha.229`
|
||||
|
||||
Module modifié : **Red-Stoner `0.0.0-alpha.16`**.
|
||||
|
||||
- ajoute au terminal les tris alphabétique, par quantité croissante et par quantité décroissante, appliqués explicitement sans déplacer les objets sous le curseur lors des rafraîchissements périodiques ;
|
||||
|
||||
## `26.2.0-alpha.228`
|
||||
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.132`** et **Red-Stoner `0.0.0-alpha.15`**.
|
||||
|
||||
- étend `sanctuary:backroom` à toute la hauteur Y -64 → 383 et empile cinq niveaux thématiques de haut en bas : seuil modulaire, archives, ville, services et glitch ;
|
||||
- remplace les volumes ouverts par des pièces et couloirs raccordés, quatre angles de salle déclinés en cinq matériaux, des bibliothèques immenses, des tours plus serrées, des autoroutes continues et de rares parcs ;
|
||||
- ajoute au niveau inférieur une grille de prismes 3D et un labyrinthe déterministe connecté, avec portes physiquement ouvertes, salles fusionnées et trous volontaires vers le vide ;
|
||||
- fait correspondre exactement chaque ouverture entre le sol supérieur et le plafond inférieur, place une torche récupérable à chaque arrivée et raréfie fortement les lits de sortie ;
|
||||
- supprime le ciel, les astres, la lumière ambiante et tous les monstres, y compris ceux injectés par un autre système ;
|
||||
- conserve les IDs, les données Sanctuary v14 et le protocole réseau 24 ; seuls les nouveaux chunks Backroom utilisent la nouvelle architecture complète ;
|
||||
- rend le champ de recherche du terminal inactif à l’ouverture puis focalisable au clic, libère le focus au clic extérieur et laisse toujours la touche d’inventaire fermer le terminal ;
|
||||
- permet à la recherche du terminal de retrouver une boîte de Shulker par le nom ou l’identifiant d’un objet qu’elle contient, tout en conservant la boîte comme résultat afin que son aperçu vanilla reste directement consultable ;
|
||||
|
||||
## `26.2.0-alpha.227`
|
||||
|
||||
Modules modifiés : **Sanctuary `0.0.0-alpha.131`** et **Another World `0.0.0-alpha.57`**.
|
||||
|
||||
- ajoute la dimension persistante `sanctuary:backroom`, accessible en s’accroupissant devant un lit et quittée uniquement par un lit, avec arrivées variées dans une zone architecturale de 2 000 blocs ;
|
||||
- transfère atomiquement vers la Mailbox tout l’équipement d’entrée, y compris les rangées étendues, l’œuf compagnon et la cape, avec reprise idempotente après interruption ;
|
||||
- conserve les objets perdus au dépop, dans le vide et lors des morts en Backroom dans un registre borné et non duplicable ;
|
||||
- transforme tous les coffres de la dimension en butin renouvelable, pauvre la plupart du temps, qui privilégie les pertes personnelles dans les premiers coffres avant les pertes anciennes partagées ;
|
||||
- génère des villes, bureaux, plaines, agrégats flottants et pièces visuellement retournées à partir de palettes composées, en réservant les matériaux d’Another World aux anomalies et les blocs précieux à de rares cachettes ;
|
||||
- fait enregistrer la mousse turquoise par Another World via l’API publique de matériaux Backroom, sans dépendance circulaire ;
|
||||
- actualise Fabric API vers `0.159.0+26.2` et JEI vers `30.29.0.199`, avec empreintes Packwiz et Prism synchronisées ;
|
||||
- migre Sanctuary vers les données v14 sans changer le protocole réseau 24, sans réinitialiser les sauvegardes ni effacer les constructions.
|
||||
|
||||
## `26.2.0-alpha.226`
|
||||
|
||||
Module modifié : **Sanctuary `0.0.0-alpha.130`**. Les neuf autres modules conservent leur version issue de l'alpha.225.
|
||||
|
||||
- ajoute `sanctuary:white_cape`, une cape blanche craftable avec six laines blanches disposées comme une porte, en deux colonnes de trois ;
|
||||
- permet d'associer simplement la cape et n'importe quelle bannière dans l'établi afin d'en conserver la couleur de fond et jusqu'à six motifs ;
|
||||
- affiche les motifs dans l'inventaire et sur la cape équipée, avec la physique native des capes, y compris en multijoueur ;
|
||||
- conserve les sauvegardes Sanctuary en data v13 ; le payload d'apparence transporte désormais la couleur et les motifs bornés, donc le protocole Sanctuary passe de 23 à 24 et client comme serveur doivent être mis à jour, sans nouveau monde ni réinitialisation.
|
||||
|
||||
## `26.2.0-alpha.225`
|
||||
|
||||
Modules modifiés : **Another World `0.0.0-alpha.56`**, **It's Alive `0.0.0-alpha.38`**, **I Like To Move It `0.0.0-alpha.18`**, **Only Fun `0.0.0-alpha.27`** et **Sanctuary `0.0.0-alpha.129`**. Les cinq autres modules conservent leur version issue de l'alpha.224.
|
||||
|
||||
- exige que l’attaquant possède son propre coffre-fort actif avant de commencer un braquage, revalide cette condition pendant le forage et refuse défensivement tout transfert si le coffre disparaît entre-temps ;
|
||||
- empêche les familiers temporaires Sanctuary de produire du butin vanilla, de l’expérience ou une lootbox Only Fun, et les exclut des contrats de panneau comme des demandes communautaires ;
|
||||
- conserve la variante Canaplia des seize graines historiques pendant le placement, la croissance naturelle, la poudre d’os, la création de la partie haute, le rechargement et la récolte, ce qui réactive notamment l’hybridation Tendre + Olive vers Dorée ;
|
||||
- remplace la laine violette du Magic Carpet par la texture historique 16 × 16 du Totem de Notch, utilisée par l’objet canonique et son alias hérité sans modifier la plateforme 3 × 3 ;
|
||||
- conserve tous les IDs, sauvegardes et formats de données ; les protocoles réseau restent Another World 2, It's Alive 4, I Like To Move It 5, Only Fun 3 et Sanctuary 23, sans nouveau monde ni réinitialisation.
|
||||
|
||||
## `26.2.0-alpha.224`
|
||||
|
||||
Modules modifiés : **It's Alive `0.0.0-alpha.37`**, **Ouch `0.0.0-alpha.11`**, **I Like To Move It `0.0.0-alpha.17`**, **Only Fun `0.0.0-alpha.26`**, **Sanctuary `0.0.0-alpha.128`** et **MasterKey `0.0.0-alpha.9`**.
|
||||
|
||||
- étend le clic molette aux coffres, barils, boîtes de Shulker, entonnoirs et distributeurs, tout en excluant les fours, le Shop et le terminal Red-Stoner ;
|
||||
- trie l'inventaire du joueur lorsque le curseur survole l'une de ses cases dans un conteneur, sans toucher à la hotbar, et replace la hotbar en bas après connexion ou achat d'une rangée ;
|
||||
- ajoute Notch comme résident unique et persistant de la maison Indev, avec restauration automatique, déduplication et lecture des anciennes entités `portals:notch` ;
|
||||
- transforme Notch en Titan au premier coup volontaire, avec santé bornée selon les participants, poursuite, attaques télégraphiées, boules de feu renvoyables et réinitialisation si le combat est abandonné ;
|
||||
- limite la destruction de blocs à l'explosion initiale de transformation ; les attaques suivantes utilisent uniquement dégâts, poussée, particules et explosions non destructrices ;
|
||||
- enregistre la première victoire comme jalon mondial monotone, crédite les participants, inscrit un marqueur de boss dans leur Atlas déjà découvert et réserve une récompense physique unique ;
|
||||
- place le Magic Carpet de Notch dans un coffre déterministe signalé par feu d'artifice, avec réservation et livraison persistées séparément pour récupérer une interruption sans duplication ;
|
||||
- conserve `portals:notch_totem` comme alias caché et convertit inventaires, conteneurs et items au sol vers `iliketomoveit:magic_carpet` en gardant quantité, composants et nom personnalisé ;
|
||||
- maintient la plateforme temporaire 3 × 3 sous autorité serveur, sans écraser le terrain et avec un suivi par propriétaire qui protège les tapis superposés pendant les nettoyages ;
|
||||
- rend l'offre Magic Carpet visible et achetable uniquement après `sanctuary:notch_defeated`, au prix exceptionnel existant, avec paiement normal, livraison Mailbox, provenance et journal de remplacement ;
|
||||
- ajoute les diagnostics `/masterkey notch inspect` et `/masterkey notch restore`, les traductions FR/EN/RU et les contrats de test déterministes associés ;
|
||||
- migre les données It's Alive de v3 à v4, I Like To Move It de v3 à v4 et Sanctuary de v11 à v13 ; le payload de tri ayant gagné un booléen borné, le protocole Sanctuary passe de 22 à 23 et client comme serveur doivent être mis à jour, sans nouveau monde ni réinitialisation.
|
||||
|
||||
## `26.2.0-alpha.223`
|
||||
|
||||
Module modifié : **Sanctuary `0.0.0-alpha.125`**. Les neuf autres modules conservent leur version issue de l'alpha.222.
|
||||
|
||||
- ajoute un tri natif de l’inventaire par clic molette configurable, uniquement sur les rangées de stockage débloquées ;
|
||||
- fusionne et range les piles depuis le haut sans déplacer la hotbar, l’armure, la main secondaire, le craft ni les emplacements spéciaux ;
|
||||
- garde le tri sous autorité serveur, avec un délai de huit ticks, et l’ignore en créatif ou pendant la saisie dans un champ texte ;
|
||||
- ajoute un style configurable au suivi des quêtes (`N` par défaut), entre le texte transparent et le panneau original de Chris ;
|
||||
- place automatiquement le style texte sous la hauteur réelle de la colonne gauche du F3 permanent afin que les deux affichages ne se recouvrent pas ;
|
||||
- nomme et ordonne les contrats suivis par niveau de récompense, rubis puis saphir puis émeraude/autres, au lieu du libellé générique « Contrat du jour » ;
|
||||
- actualise JEI vers la bêta Fabric 26.2 `30.29.0.198` et synchronise ses empreintes entre Packwiz et Prism ;
|
||||
- conserve les données Sanctuary v11, le protocole réseau 22, les IDs et les sauvegardes existantes ; aucun nouveau monde ni réinitialisation n’est nécessaire.
|
||||
|
||||
## `26.2.0-alpha.222`
|
||||
|
||||
Modules modifiés : **Red-Stoner `0.0.0-alpha.13`** et **Sanctuary `0.0.0-alpha.124`**. Les huit autres modules conservent leur version issue de l'alpha.221.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Sanctuary 26.2
|
||||
|
||||
Workspace Fabric multi-projet de Sanctuary 2.0. Il construit dix JAR indépendants sur Minecraft 26.2, Fabric Loader 0.19.3, Fabric API 0.158.0+26.2, Loom 1.17, Gradle 9.5.1 et Java 25.
|
||||
Workspace Fabric multi-projet de Sanctuary 2.0. Il construit dix JAR indépendants sur Minecraft 26.2, Fabric Loader 0.19.3, Fabric API 0.159.0+26.2, Loom 1.17, Gradle 9.5.1 et Java 25.
|
||||
|
||||
Auteur : **KOKA99CAB**
|
||||
Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
|
||||
@@ -12,20 +12,36 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
|
||||
| `anotherworld` | Another World | choses matérielles | matière, photographie, instruments météo et 108 biomes natifs |
|
||||
| `itsalive` | It's Alive ! | vivant | Creepers, créatures, agriculture et gameplay serveur complet de Canaplia |
|
||||
| `ambiance` | Ambiance | perception | atmosphère, météo, lumières dynamiques, audio positionnel, calendrier et composition visuelle accessible |
|
||||
| `redstoner` | Red-Stoner | calcul et signaux | ordinateurs, identité serveur, terminal de stockage, Drawer et Particuleur |
|
||||
| `redstoner` | Red-Stoner | calcul et signaux | ordinateurs, identité serveur, terminal de stockage, panneau de trophées, Drawer et Particuleur |
|
||||
| `iliketomoveit` | I Like To Move It | déplacement | portails, véhicules, grappin, poulies et waystones locaux |
|
||||
| `ouch` | Ouch | combat et destruction | arsenal actif (6 armes) et TNT météo |
|
||||
| `onlyfun` | Only Fun | règles ludiques absurdes | lootboxes, Lucky Block, raretés et règles absurdes |
|
||||
| `sanctuary` | Sanctuary | progression et aventure | progression globale, Atlas et monde principal flottant |
|
||||
| `sanctuary` | Sanctuary | progression et aventure | progression globale, classements publics, Atlas et monde principal flottant |
|
||||
| `events` | Events | événements serveur | socle vide |
|
||||
| `masterkey` | MasterKey | administration et diagnostic | laboratoire minimal |
|
||||
|
||||
Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans les fichiers de langue. Les images fournies servent d'icônes Fabric de leur module.
|
||||
|
||||
Version courante du pack distribué : **`26.2.0-alpha.222`**. Cette release distribue Red-Stoner **`0.0.0-alpha.13`** et Sanctuary **`0.0.0-alpha.124`** ; les huit autres modules conservent leur version issue de l'alpha.221.
|
||||
Version courante du pack distribué : **`26.2.0-alpha.229`**. Cette release distribue Red-Stoner **`0.0.0-alpha.16`** ; les neuf autres modules conservent leur version issue de l'alpha.228.
|
||||
|
||||
Le prochain module Sanctuary **`0.0.0-alpha.134`** attribue les quêtes communautaires terminées à leur preneur et répartit les lits de sortie rares sur chacun des cinq niveaux de la Backroom. Chaque niveau garantit désormais un lit par cellule de 10 × 10 chunks dans les nouveaux chunks, sans modifier les sauvegardes, les données v14 ni le protocole réseau 24.
|
||||
|
||||
Sanctuary **`0.0.0-alpha.135`** et Red-Stoner **`0.0.0-alpha.17`** ajoutent les classements publics et le panneau mural `redstoner:trophy_panel`. Les statistiques vanilla des anciens joueurs sont importées une fois, les scores restent dans Sanctuary et le panneau ne reçoit que sa page visible ainsi qu’un podium de trois noms maximum. Les données passent respectivement à v15 et v4 sans changer les protocoles réseau 24 et 7.
|
||||
|
||||
L’alpha.229 ajoute au terminal les tris alphabétique, par quantité croissante et par quantité décroissante. Le tri ne change que sur demande explicite et les rafraîchissements périodiques conservent l’ordre ainsi que les objets sous le curseur. Les sauvegardes, les données Red-Stoner v3 et le protocole réseau 7 restent compatibles.
|
||||
|
||||
L’alpha.228 reconstruit la Backroom sur cinq niveaux empilés entre Y -64 et Y 383. Le monde devient entièrement noir et sans monstres ; l’arrivée est signalée par une torche récupérable. Les nouveaux chunks composent seuils modulaires, bibliothèques et bureaux, ville dense avec autoroutes et parcs, couloirs techniques, puis une grille 3D et un labyrinthe connecté dont certains trous mènent au vide. Red-Stoner corrige aussi le focus du terminal et permet de retrouver une boîte de Shulker par son contenu. Les chunks déjà générés et les sauvegardes existantes restent compatibles.
|
||||
|
||||
L’alpha.227 ajoute la dimension persistante `sanctuary:backroom`. On y entre accroupi devant un lit et on n’en ressort que par un lit. L’équipement d’entrée est transféré atomiquement vers la Mailbox, tandis que les objets perdus ou abandonnés dans la dimension alimentent des coffres renouvelables sans duplication. Les anciennes sauvegardes sont migrées vers Sanctuary data v14 sans réinitialisation ; le protocole réseau reste en version 24.
|
||||
|
||||
L’alpha.225 corrige quatre tickets : posséder un coffre-fort devient obligatoire pour en braquer un autre, les familiers temporaires ne donnent plus de butin, d’expérience, de lootbox ni de progression de quête, les seize variantes Canaplia survivent à tout leur cycle de culture, et le Magic Carpet retrouve la texture historique du Totem de Notch à la place de la laine violette. Les IDs, sauvegardes, formats de données et protocoles restent compatibles.
|
||||
|
||||
La migration [CANA-01 à CANA-09](CANAPLIA.md) porte Canaplia vers Minecraft 26.2. It's Alive **`0.0.0-alpha.35`** enregistre les seize familles, les découvertes, hybridations, résines, effets serveur et le Livre interdit personnel, partageable temporairement sur un pupitre. Ambiance **`0.0.0-alpha.14`** compose les profils visuels bornés avec ses réglages d'accessibilité, et Only Fun **`0.0.0-alpha.21`** exclut explicitement Canaplia et les raccourcis de progression du Shop.
|
||||
|
||||
L’alpha.224 ajoute Notch comme résident unique de la maison Indev et son combat de Titan. It's Alive possède l'entité persistante, Ouch les attaques et dégâts, Sanctuary le jalon mondial de victoire et l'Atlas, tandis que MasterKey fournit les commandes d'inspection et de restauration. Les anciennes entités `portals:notch` restent chargeables. La première victoire réserve maintenant un unique `iliketomoveit:magic_carpet` dans un coffre signalé par feu d'artifice. L'ancien `portals:notch_totem` est migré sans perte ; après Notch, Only Fun ouvre l'offre de remplacement au Shop avec paiement serveur et livraison par Mailbox. Le clic molette trie aussi les coffres, barils, boîtes de Shulker, entonnoirs et distributeurs, ou l'inventaire du joueur lorsqu'il est survolé, sans déplacer la hotbar. Le protocole Sanctuary passe à 23 : client et serveur doivent utiliser l'alpha.224.
|
||||
|
||||
L’alpha.223 ajoute le tri natif des rangées de stockage par clic molette sans déplacer la hotbar ni les emplacements spéciaux. Le suivi des quêtes propose désormais le texte transparent aligné sous la colonne gauche du F3 ou le panneau original de Chris, avec bascule configurable sur `N` et contrats ordonnés par récompense. JEI passe à la bêta Fabric 26.2 `30.29.0.198`.
|
||||
|
||||
L’alpha.222 améliore le terminal de stockage avec une recherche Unicode, des quantités compactes et des emplacements qui ne glissent plus lorsqu'une pile est vidée. Elle ajoute aussi le suivi textuel des quêtes actives en haut à gauche, masquable avec une touche configurable, ainsi que des suggestions d’IDs d’items et de mobs dans l’éditeur de demandes. JEI passe à la bêta Fabric 26.2 `30.29.0.196`.
|
||||
|
||||
L’alpha.221 corrige la cause réelle du disque blanc ralenti et trop grave : Ambiance décode désormais la source AAC en stéréo native à 48 kHz, puis moyenne explicitement chaque paire gauche/droite vers le flux mono positionnel de Minecraft. OpenAL reste à sa hauteur normale `1.0`, sans compensation artificielle ni changement de durée.
|
||||
@@ -34,7 +50,7 @@ L’alpha.220 transforme le terminal de stockage en véritable menu de conteneur
|
||||
|
||||
L’alpha.219 porte l'île finie Minecraft Indev dans `sanctuary:alpha` : 128 × 128 × 64 blocs déterministes, plages, cavernes, minerais, fluides, végétation et maison historique, le tout borné sans chunks parasites. Le cadre 4 × 5 en cobblestone moussue est maintenant actif et crée une paire persistante à une arrivée sûre hors de la maison. Ambiance peut enfin composer localement Golden Days Base puis Alpha uniquement dans cette dimension, sans redistribuer leurs textures et en restaurant exactement la pile précédente. Voir le [contrat Alpha](sanctuary/ALPHA.md), le [contrat des portails](iliketomoveit/PORTALS.md) et le [guide Golden Days](ambiance/GOLDEN_DAYS.md).
|
||||
|
||||
L’alpha.218 ajoute le terminal de stockage Red-Stoner : cette demi-dalle verticale se fixe sur un coffre, un coffre piégé ou un baril et donne accès, par pages défilantes de 54 types, à un réseau contigu borné à 128 conteneurs chargés. Les retraits et dépôts sont validés par le serveur, les composants d’items restent strictement séparés et le terminal endgame est exclu du Shop, du Black Market et des récompenses Only Fun.
|
||||
L’alpha.218 ajoute le terminal de stockage Red-Stoner : cette demi-dalle verticale se fixe sur un coffre, un coffre piégé ou un baril et donne accès, par pages défilantes de 54 types, à un réseau contigu borné à 128 conteneurs chargés. Les retraits et dépôts sont validés par le serveur, les composants d’items restent strictement séparés et le terminal endgame est exclu du Shop, du Black Market et des récompenses Only Fun. La recherche retrouve aussi une boîte de Shulker d’après son contenu direct ; la boîte reste affichée dans la grille afin que son aperçu vanilla soit consultable sans modifier les règles de retrait. Un bouton permet enfin de trier explicitement les résultats par nom, quantité croissante ou quantité décroissante.
|
||||
|
||||
L’alpha.217 tentait de compenser la hauteur trop grave du disque blanc par un facteur OpenAL de `4/3`. Cette compensation est retirée par l’alpha.221, qui corrige directement la conversion stéréo vers mono.
|
||||
|
||||
@@ -110,6 +126,8 @@ L’alpha.173 centralise l’audio du Miner : une seule boucle de frappe sur le
|
||||
|
||||
L’alpha.171 rassemble les **46 capes** dans un onglet créatif dédié « Capes ». Elles ne sont plus mélangées à l’onglet vanilla Tools & Utilities et ne sont donc affichées qu’une seule fois.
|
||||
|
||||
L’alpha.226 ajoute une 47e cape, `sanctuary:white_cape`, distincte de la collection Only Fun. Elle se fabrique avec six laines blanches en deux colonnes de trois, puis accepte une bannière dans une recette simple pour en conserver la couleur et jusqu’à six motifs ; les motifs s’affichent dans l’inventaire comme une fois la cape équipée.
|
||||
|
||||
L’alpha.170 ajoute les pictogrammes fournis aux emplacements vides de la cape et du familier. Ils disparaissent dès qu’une cape ou un œuf est équipé, sans modifier les objets, la persistance ni le protocole.
|
||||
|
||||
L’alpha.169 agrandit la collection de 13 à **46 capes équipables**. Les 33 nouvelles textures 64×32 fournies sont conservées telles quelles, avec une icône d’inventaire, un modèle et des traductions FR/EN/RU pour chaque cape. Elles utilisent le slot, la persistance et le rendu multijoueur déjà existants, restent sans recette et rejoignent toutes la lootbox de capes Only Fun. Les caméras Canon restent accessibles dans la lootbox normale et peuvent désormais aussi apparaître directement, très rarement, dans les coffres d’exploration et du donjon de cobblestone.
|
||||
@@ -305,7 +323,9 @@ Le guide détaillé est dans [`masterkey/tools/PANORAMA.md`](masterkey/tools/PAN
|
||||
|
||||
## Red-Stoner et photographie
|
||||
|
||||
Red-Stoner possède quatre blocs actifs sous son namespace : `eight_bit_computer`, `controller`, `personal_computer` et `drawer`. Son onglet créatif expose aussi les trois appareils Canon et les seize styles de pellicule.
|
||||
Red-Stoner possède sept blocs actifs sous son namespace : `eight_bit_computer`, `controller`, `personal_computer`, `drawer`, `particuleur`, `storage_terminal` et `trophy_panel`. Son onglet créatif expose aussi les trois appareils Canon et les seize styles de pellicule.
|
||||
|
||||
Le panneau mural `redstoner:trophy_panel` consomme l’API publique de classement de Sanctuary sans conserver les scores. Le joueur qui le pose peut ajouter des pages statistiques en cliquant avec un item, tandis que tous les visiteurs peuvent faire défiler les pages. Sanctuary importe une fois les statistiques vanilla existantes, garde les classements par UUID — champion déconnecté compris — et fournit seulement un podium borné à trois entrées. Les composants privés de l’item servant d’icône et la base complète des joueurs ne sont jamais synchronisés au client.
|
||||
|
||||
L'ordinateur 8 bits et le contrôleur exécutent la machine virtuelle historique : programme assembleur borné à 4 096 caractères, RAM, inventaire, six ports redstone directionnels, impulsions et sortie comparateur. Le contrôleur dispose de 1 024 cellules de RAM contre 256 pour l'ordinateur. L'instruction destinée à la matrice d'affichage renvoie volontairement une erreur claire tant que ce périphérique reste différé.
|
||||
|
||||
|
||||
@@ -1647,6 +1647,7 @@ tasks.register("generateAnotherWorldResources") {
|
||||
"message.anotherworld.safe_vault.empty_before_break": "Videz le coffre-fort et attendez la fin du forage avant de le démonter.",
|
||||
"message.anotherworld.safe_vault.drill_own": "Vous ne pouvez pas forer votre propre coffre-fort.",
|
||||
"message.anotherworld.safe_vault.inactive": "Ce coffre-fort n’est pas actif.",
|
||||
"message.anotherworld.safe_vault.raid_requires_vault": "Vous devez posséder un coffre-fort actif avant d’en braquer un autre.",
|
||||
"message.anotherworld.safe_vault.empty": "Ce coffre-fort est vide.",
|
||||
"message.anotherworld.safe_vault.owner_offline": "Le propriétaire doit être connecté pour que le coffre-fort soit attaquable.",
|
||||
"message.anotherworld.safe_vault.protected": "Ce coffre-fort reste protégé pendant environ %s minute(s).",
|
||||
@@ -1671,6 +1672,7 @@ tasks.register("generateAnotherWorldResources") {
|
||||
"message.anotherworld.safe_vault.empty_before_break": "Опустошите сейф и дождитесь окончания сверления перед разборкой.",
|
||||
"message.anotherworld.safe_vault.drill_own": "Нельзя сверлить собственный сейф.",
|
||||
"message.anotherworld.safe_vault.inactive": "Этот сейф не активен.",
|
||||
"message.anotherworld.safe_vault.raid_requires_vault": "Для ограбления чужого сейфа нужен собственный активный сейф.",
|
||||
"message.anotherworld.safe_vault.empty": "Этот сейф пуст.",
|
||||
"message.anotherworld.safe_vault.owner_offline": "Владелец должен быть в сети, чтобы атаковать сейф.",
|
||||
"message.anotherworld.safe_vault.protected": "Сейф защищён ещё примерно %s минут(ы).",
|
||||
@@ -1695,6 +1697,7 @@ tasks.register("generateAnotherWorldResources") {
|
||||
"message.anotherworld.safe_vault.empty_before_break": "Empty the safe and wait for drilling to end before dismantling it.",
|
||||
"message.anotherworld.safe_vault.drill_own": "You cannot drill your own safe.",
|
||||
"message.anotherworld.safe_vault.inactive": "This safe is not active.",
|
||||
"message.anotherworld.safe_vault.raid_requires_vault": "You need an active safe of your own before raiding another one.",
|
||||
"message.anotherworld.safe_vault.empty": "This safe is empty.",
|
||||
"message.anotherworld.safe_vault.owner_offline": "The owner must be online before the safe can be raided.",
|
||||
"message.anotherworld.safe_vault.protected": "This safe remains protected for about %s minute(s).",
|
||||
@@ -1953,7 +1956,7 @@ tasks.register("verifyAnotherWorldCatalog") {
|
||||
"src/main/resources/assets/redstoner/textures/item/weather_gauge.png",
|
||||
"src/main/resources/data/redstoner/recipe/weather_gauge.json"
|
||||
]
|
||||
if (project.version.toString() != "0.0.0-alpha.55"
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| sulfurMigration.target?.modules?.anotherworld != "0.0.0-alpha.44"
|
||||
|| sulfurMigration.sulfur_caves?.sanctuary?.dripstone_cell_period != 2
|
||||
|| !javaText.contains('Identifier.parse("sanctuary:sulfur_caves")')
|
||||
@@ -3270,7 +3273,7 @@ tasks.register("verifyClimateBiomeMixer") {
|
||||
"pack/migrations/26.2.0-alpha.188-beta-climate-provinces-to-alpha.189.json"))
|
||||
def mixer = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/anotherworld/mixin/MultiNoiseBiomeSourceParameterListPresetMixin.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.55"
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.188"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.189"
|
||||
|| migration.target?.modules?.anotherworld != "0.0.0-alpha.48"
|
||||
@@ -3297,3 +3300,33 @@ tasks.register("verifyClimateBiomeMixer") {
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyClimateBiomeMixer")
|
||||
}
|
||||
|
||||
tasks.register("verifyBackroomMaterialIntegration") {
|
||||
group = "verification"
|
||||
description = "Checks the one-way Another World material integration with Sanctuary Backroom."
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldBlocks.java"))
|
||||
inputs.file(rootProject.file(
|
||||
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java"))
|
||||
doLast {
|
||||
def initializer = file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/AnotherWorldMod.java").text
|
||||
def blocks = file("src/main/java/fr/koka99cab/sanctuary26/anotherworld/registry/AnotherWorldBlocks.java").text
|
||||
def api = rootProject.file(
|
||||
"sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/api/backroom/BackroomMaterialApi.java").text
|
||||
def rootBuild = rootProject.file("build.gradle").text
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.57"
|
||||
|| !initializer.contains("BackroomMaterialApi.registerAnomalyMaterial(AnotherWorldBlocks.TURQUOISE_MOSS)")
|
||||
|| !blocks.contains('"turquoise_moss"')
|
||||
|| !api.contains("public static void registerAnomalyMaterial")
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.131"
|
||||
|| !rootBuild.contains('project(":anotherworld")')
|
||||
|| !rootBuild.contains('implementation project(":sanctuary")')) {
|
||||
throw new GradleException("Another World alpha.57 Backroom material integration is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyBackroomMaterialIntegration")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import fr.koka99cab.canon.CanonMod;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.item.WeatherCompassItem;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.network.AnotherWorldNetworking;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldRegistries;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.registry.AnotherWorldBlocks;
|
||||
import fr.koka99cab.sanctuary26.anotherworld.vault.VaultService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.backroom.BackroomMaterialApi;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
@@ -26,6 +28,7 @@ public final class AnotherWorldMod implements ModInitializer {
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
AnotherWorldRegistries.initialize();
|
||||
BackroomMaterialApi.registerAnomalyMaterial(AnotherWorldBlocks.TURQUOISE_MOSS);
|
||||
VaultService.initialize();
|
||||
WeatherCompassItem.registerFrameInteraction();
|
||||
CanonMod.initialize();
|
||||
|
||||
+11
-1
@@ -113,6 +113,12 @@ public final class VaultData extends SavedData {
|
||||
&& address.pos() == center.asLong() && address.owner().equals(ownerId));
|
||||
}
|
||||
|
||||
public boolean hasRegisteredVault(UUID owner) {
|
||||
if (owner == null) return false;
|
||||
String ownerId = owner.toString();
|
||||
return vaults.stream().anyMatch(address -> address.owner().equals(ownerId));
|
||||
}
|
||||
|
||||
public void unregisterNear(ServerLevel level, BlockPos member) {
|
||||
String dimension = dimension(level);
|
||||
boolean changed = vaults.removeIf(address -> {
|
||||
@@ -163,7 +169,7 @@ public final class VaultData extends SavedData {
|
||||
|
||||
/** Transfers the exact 20% raid share into the attacker's unlimited balances without item spam. */
|
||||
public long transferRaid(UUID owner, UUID attacker) {
|
||||
if (owner == null || attacker == null || owner.equals(attacker)) return 0L;
|
||||
if (!canReceiveRaidLoot(owner, attacker, hasRegisteredVault(attacker))) return 0L;
|
||||
Account source = accounts.get(owner);
|
||||
if (source == null) return 0L;
|
||||
Account target = accounts.computeIfAbsent(attacker, ignored -> new Account());
|
||||
@@ -180,6 +186,10 @@ public final class VaultData extends SavedData {
|
||||
return total;
|
||||
}
|
||||
|
||||
static boolean canReceiveRaidLoot(UUID owner, UUID attacker, boolean attackerHasRegisteredVault) {
|
||||
return owner != null && attacker != null && !owner.equals(attacker) && attackerHasRegisteredVault;
|
||||
}
|
||||
|
||||
public long protectionRemaining(UUID owner, long gameTime) {
|
||||
Account account = accounts.get(owner);
|
||||
return account == null ? 0L : Math.max(0L, account.protectedUntil - gameTime);
|
||||
|
||||
+11
@@ -48,6 +48,10 @@ public final class VaultDrillingService {
|
||||
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.inactive"));
|
||||
return false;
|
||||
}
|
||||
if (!data.hasRegisteredVault(attacker.getUUID())) {
|
||||
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.raid_requires_vault"));
|
||||
return false;
|
||||
}
|
||||
if (!data.hasContents(formed.owner())) {
|
||||
attacker.sendSystemMessage(Component.translatable("message.anotherworld.safe_vault.empty"));
|
||||
return false;
|
||||
@@ -104,6 +108,13 @@ public final class VaultDrillingService {
|
||||
attacker.stopUsingItem();
|
||||
return;
|
||||
}
|
||||
if (!VaultData.get(level.getServer()).hasRegisteredVault(attacker.getUUID())) {
|
||||
cancel(attacker, false);
|
||||
attacker.sendOverlayMessage(Component.translatable(
|
||||
"message.anotherworld.safe_vault.raid_requires_vault"));
|
||||
attacker.stopUsingItem();
|
||||
return;
|
||||
}
|
||||
|
||||
int elapsed = session.elapsedTicks() + 1;
|
||||
Session progressed = session.withElapsed(elapsed);
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"ambiance": "*",
|
||||
"sanctuary": ">=0.0.0-alpha.117"
|
||||
"sanctuary": ">=0.0.0-alpha.131"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
|
||||
+10
@@ -1,5 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.anotherworld.vault;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class VaultRulesSmoke {
|
||||
private VaultRulesSmoke() {
|
||||
}
|
||||
@@ -55,6 +57,14 @@ public final class VaultRulesSmoke {
|
||||
require(VaultRules.raidLoot(Integer.MAX_VALUE) == 429_496_730L, "32-bit balance raid");
|
||||
require(VaultRules.raidLoot(Long.MAX_VALUE) == 1_844_674_407_370_955_162L,
|
||||
"64-bit balance raid without overflow");
|
||||
UUID owner = UUID.fromString("00000000-0000-0000-0000-000000000001");
|
||||
UUID attacker = UUID.fromString("00000000-0000-0000-0000-000000000002");
|
||||
require(!VaultData.canReceiveRaidLoot(owner, attacker, false),
|
||||
"an attacker without a registered vault cannot receive raid loot");
|
||||
require(VaultData.canReceiveRaidLoot(owner, attacker, true),
|
||||
"an attacker with a registered vault can receive raid loot");
|
||||
require(!VaultData.canReceiveRaidLoot(owner, owner, true),
|
||||
"a vault owner cannot receive loot from their own vault");
|
||||
require(VaultRules.saturatingAdd(Long.MAX_VALUE - 2L, 10L) == Long.MAX_VALUE,
|
||||
"balance saturation without overflow");
|
||||
for (long balance : new long[] {0L, Integer.MAX_VALUE, (long) Integer.MAX_VALUE + 1L, Long.MAX_VALUE}) {
|
||||
|
||||
+1487
-280
File diff suppressed because it is too large
Load Diff
+10
-10
@@ -6,7 +6,7 @@ org.gradle.configuration-cache=false
|
||||
minecraft_version=26.2
|
||||
loader_version=0.19.3
|
||||
loom_version=1.17-SNAPSHOT
|
||||
fabric_api_version=0.158.0+26.2
|
||||
fabric_api_version=0.159.0+26.2
|
||||
|
||||
# Sanctuary 26.2
|
||||
# Standby modules stay at zero until their migration really begins.
|
||||
@@ -14,21 +14,21 @@ mod_version=0.0.0-alpha.0
|
||||
module_lifecycle=standby
|
||||
ambiance_version=0.0.0-alpha.22
|
||||
ambiance_lifecycle=active
|
||||
redstoner_version=0.0.0-alpha.13
|
||||
redstoner_version=0.0.0-alpha.17
|
||||
redstoner_lifecycle=active
|
||||
anotherworld_version=0.0.0-alpha.55
|
||||
anotherworld_version=0.0.0-alpha.57
|
||||
anotherworld_lifecycle=active
|
||||
itsalive_version=0.0.0-alpha.36
|
||||
itsalive_version=0.0.0-alpha.38
|
||||
itsalive_lifecycle=active
|
||||
ouch_version=0.0.0-alpha.10
|
||||
ouch_version=0.0.0-alpha.11
|
||||
ouch_lifecycle=active
|
||||
masterkey_version=0.0.0-alpha.8
|
||||
masterkey_version=0.0.0-alpha.9
|
||||
masterkey_lifecycle=active
|
||||
iliketomoveit_version=0.0.0-alpha.16
|
||||
iliketomoveit_version=0.0.0-alpha.18
|
||||
iliketomoveit_lifecycle=active
|
||||
onlyfun_version=0.0.0-alpha.25
|
||||
onlyfun_version=0.0.0-alpha.27
|
||||
onlyfun_lifecycle=active
|
||||
sanctuary_version=0.0.0-alpha.124
|
||||
sanctuary_version=0.0.0-alpha.135
|
||||
sanctuary_lifecycle=active
|
||||
pack_version=26.2.0-alpha.222
|
||||
pack_version=26.2.0-alpha.229
|
||||
maven_group=fr.koka99cab.sanctuary26
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Magic Carpet et Totem de Notch
|
||||
|
||||
I Like To Move It possède l'objet, la plateforme temporaire, la remise physique
|
||||
de la récompense et la compatibilité de l'ancien Totem. Sanctuary reste
|
||||
propriétaire du jalon mondial et de la réservation unique ; Only Fun possède le
|
||||
catalogue et applique le jalon avant d'exposer l'offre.
|
||||
|
||||
## Identités et provenance
|
||||
|
||||
| Rôle | ID | Statut |
|
||||
|---|---|---|
|
||||
| objet canonique | `iliketomoveit:magic_carpet` | distribué après Notch puis achetable au Shop |
|
||||
| Totem historique | `portals:notch_totem` | alias caché, lisible et converti vers l'objet canonique |
|
||||
| jalon mondial | `sanctuary:notch_defeated` | autorité serveur Sanctuary |
|
||||
|
||||
La conversion utilise `ItemStack.transmuteCopy` : quantité, composants et nom
|
||||
personnalisé sont conservés. Les inventaires joueurs sont traités à la connexion,
|
||||
les conteneurs vanilla lors de leur chargement et les entités d'item dès leur
|
||||
tick serveur. Un Totem produit exactement un tapis et les nouveaux mondes ne
|
||||
distribuent jamais l'ancien ID. La donnée I Like To Move It passe en v4 ; une
|
||||
pile déjà convertie ne correspond plus à l'alias et ne peut donc pas être
|
||||
convertie une seconde fois.
|
||||
|
||||
Chaque tapis porte une provenance Sanctuary sans autorité de gameplay :
|
||||
`notch_reward`, `shop_purchase` ou `legacy_notch_totem`. Le serveur ne se fie
|
||||
jamais à cette étiquette pour autoriser un achat ; elle sert à l'historique, au
|
||||
diagnostic et à l'infobulle.
|
||||
|
||||
## Première récompense
|
||||
|
||||
Après la première victoire mondiale, Sanctuary réserve atomiquement la
|
||||
récompense puis I Like To Move It crée un coffre déterministe près de la maison
|
||||
de Notch. Le tapis unique est placé au centre et un feu d'artifice signale le
|
||||
coffre. Sanctuary ne marque la livraison terminée qu'après avoir retrouvé ou
|
||||
créé ce coffre.
|
||||
|
||||
Les états `initial_reward_claimed` et `initial_reward_delivered` sont distincts.
|
||||
Une reprise entre les deux relance la création de façon idempotente ; un coffre
|
||||
déjà créé et marqué est reconnu sans recevoir de second tapis. Les anciennes
|
||||
sauvegardes v1 dont la récompense était déjà déclarée remise sont migrées vers
|
||||
l'état livré afin de ne rien dupliquer.
|
||||
|
||||
## Plateforme temporaire
|
||||
|
||||
Tenir le tapis en main principale ou secondaire maintient neuf blocs de verre
|
||||
sous le joueur. Seul l'air est remplacé : terrain, liquides, plantes et blocs
|
||||
construits restent intacts. Le joueur conserve ses capacités normales ; seul le
|
||||
dégât de chute est réinitialisé tant que la plateforme est active.
|
||||
|
||||
Chaque position possède un ensemble implicite de propriétaires. Deux tapis
|
||||
peuvent se chevaucher et le verre ne disparaît qu'après le départ du dernier.
|
||||
Le nettoyage est effectué côté serveur au relâchement, à la déconnexion, au
|
||||
changement de dimension et pendant l'arrêt normal du serveur.
|
||||
|
||||
## Remplacement au Shop
|
||||
|
||||
Only Fun associe le Magic Carpet au resolver public
|
||||
`SanctuaryWorldMilestones`. Avant `sanctuary:notch_defeated`, l'offre est
|
||||
absente des snapshots et toute tentative réseau est rejetée par Sanctuary.
|
||||
Après la victoire, elle rejoint la rotation normale au prix configuré de
|
||||
1 536 rubis et 128 saphirs : les saphirs débloquent le catalogue, les rubis
|
||||
paient chaque remplacement et la Mailbox effectue la livraison différée.
|
||||
|
||||
Un achat porte la provenance `shop_purchase`, est journalisé comme remplacement
|
||||
et ne touche jamais aux états de réservation ou de coffre de la récompense
|
||||
initiale.
|
||||
+19
-10
@@ -32,6 +32,14 @@ tasks.register("alphaPortalContractSmoke", JavaExec) {
|
||||
dependsOn tasks.named("testClasses")
|
||||
}
|
||||
|
||||
tasks.register("magicCarpetLedgerSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks Magic Carpet overlap, dimension-change and shutdown cleanup ownership."
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetLedgerSmoke"
|
||||
dependsOn tasks.named("testClasses")
|
||||
}
|
||||
|
||||
tasks.named("test") {
|
||||
// Runtime contracts are executable smoke tasks, not JUnit engine tests.
|
||||
failOnNoDiscoveredTests = false
|
||||
@@ -100,11 +108,11 @@ tasks.register("verifyWaystones") {
|
||||
if (!missingResources.isEmpty()) throw new GradleException("Missing Waystone resources: ${missingResources}")
|
||||
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (manifest.custom?.sanctuary26?.data_version != 3
|
||||
if (manifest.custom?.sanctuary26?.data_version != 4
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 5
|
||||
|| manifest.depends?.anotherworld != ">=0.0.0-alpha.46"
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.21") {
|
||||
throw new GradleException("I Like To Move It must expose data v3, protocol 5 and its Another World alpha.46/Sanctuary alpha.21 dependencies")
|
||||
|| manifest.depends?.sanctuary != ">=0.0.0-alpha.127") {
|
||||
throw new GradleException("I Like To Move It must expose data v4, protocol 5 and its Another World alpha.46/Sanctuary alpha.127 dependencies")
|
||||
}
|
||||
["fr_fr", "en_us", "ru_ru"].each { language ->
|
||||
def translations = new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${language}.json"))
|
||||
@@ -203,9 +211,9 @@ tasks.register("verifyGrappling") {
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
if (!manifest.mixins?.any { it.config == "iliketomoveit.mixins.json" }
|
||||
|| !manifest.mixins?.any { it.config == "iliketomoveit.client.mixins.json" && it.environment == "client" }
|
||||
|| manifest.custom?.sanctuary26?.data_version != 3
|
||||
|| manifest.custom?.sanctuary26?.data_version != 4
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 5) {
|
||||
throw new GradleException("I Like To Move It must keep aggregate data v3/protocol 5 and register its common/client mixins")
|
||||
throw new GradleException("I Like To Move It must expose aggregate data v4/protocol 5 and register its common/client mixins")
|
||||
}
|
||||
["fr_fr", "en_us", "ru_ru"].each { language ->
|
||||
def translations = new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${language}.json"))
|
||||
@@ -313,7 +321,7 @@ tasks.register("verifyPortals") {
|
||||
}
|
||||
def atlasCavernsMigration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.149-atlas-caverns-to-alpha.150.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.18"
|
||||
|| atlasCavernsMigration.target?.pack_version != "26.2.0-alpha.150"
|
||||
|| atlasCavernsMigration.target?.modules?.iliketomoveit != "0.0.0-alpha.9"
|
||||
|| atlasCavernsMigration.portals?.active_routes != [
|
||||
@@ -349,7 +357,7 @@ tasks.register("verifySilverAircraft") {
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
def migration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.184-silver-engineering-to-alpha.185.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.18"
|
||||
|| recipe.pattern != ["PGP", "GCG", "PBP"]
|
||||
|| recipe.key != [B: "anotherworld:silver_block", C: "anotherworld:aircraft_cockpit",
|
||||
G: "anotherworld:silver_gear", P: "anotherworld:silver_plate"]
|
||||
@@ -378,7 +386,7 @@ tasks.register("verifyGrapplePrediction") {
|
||||
def payload = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/GrappleStatePayload.java").text
|
||||
def controller = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleController.java").text
|
||||
def prediction = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/grapple/ClientGrapplePrediction.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.18"
|
||||
|| migration.target?.modules?.iliketomoveit != "0.0.0-alpha.13"
|
||||
|| migration.grappling?.owner_prediction_payload != "iliketomoveit:s2c/grapple_state"
|
||||
|| migration.compatibility?.iliketomoveit_source_network_protocol != 4
|
||||
@@ -410,7 +418,7 @@ tasks.register("verifyWaystoneDescendingRelease") {
|
||||
"src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/waystone/WaystoneElevatorRules.java").text
|
||||
def teleportation = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/WaystoneTeleportation.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.18"
|
||||
|| migration.target?.modules?.iliketomoveit != "0.0.0-alpha.14"
|
||||
|| migration.waystone_elevator?.undiscovered_downward_destination_allowed != true
|
||||
|| migration.waystone_elevator?.destination_discovered_after_arrival != true
|
||||
@@ -444,7 +452,7 @@ tasks.register("verifyAlphaPortal") {
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collect {
|
||||
new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${it}.json"))
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.16"
|
||||
if (project.version.toString() != "0.0.0-alpha.18"
|
||||
|| route.schema_version != 2 || route.source_dimension != "minecraft:overworld"
|
||||
|| route.destination_dimension != "sanctuary:alpha"
|
||||
|| route.destination_resolver != "fixed"
|
||||
@@ -466,6 +474,7 @@ tasks.register("verifyAlphaPortal") {
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("grapplePhysicsSmoke")
|
||||
dependsOn tasks.named("magicCarpetLedgerSmoke")
|
||||
dependsOn tasks.named("waystoneElevatorRulesSmoke")
|
||||
dependsOn tasks.named("happyGhastCruiseSpeedSmoke")
|
||||
dependsOn tasks.named("verifyWaystones")
|
||||
|
||||
+5
-1
@@ -8,6 +8,8 @@ import fr.koka99cab.sanctuary26.iliketomoveit.pulley.PulleyHeadController;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.GrapplingLootTables;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItRegistries;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetController;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetMigrationService;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.NotchCarpetRewardService;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.waystone.WaystoneElevatorController;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
@@ -17,7 +19,7 @@ import org.slf4j.LoggerFactory;
|
||||
public final class ILikeToMoveItMod implements ModInitializer {
|
||||
public static final String MOD_ID = "iliketomoveit";
|
||||
public static final String DISPLAY_NAME = "I Like To Move It";
|
||||
public static final int CURRENT_DATA_VERSION = 3;
|
||||
public static final int CURRENT_DATA_VERSION = 4;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public ILikeToMoveItMod() {
|
||||
@@ -37,6 +39,8 @@ public final class ILikeToMoveItMod implements ModInitializer {
|
||||
GrappleController.register();
|
||||
PulleyHeadController.register();
|
||||
MagicCarpetController.register();
|
||||
MagicCarpetMigrationService.register();
|
||||
NotchCarpetRewardService.register();
|
||||
WaystoneElevatorController.initialize();
|
||||
LOGGER.info("[{}] Portals, vehicles, grappling, pulleys, Magic Carpet and dimension-local Waystones ready (data v{}, network v{}).",
|
||||
DISPLAY_NAME, CURRENT_DATA_VERSION, ILikeToMoveItNetworking.PROTOCOL_VERSION);
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.item.SanctuaryItemProvenance;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
|
||||
/** Epic carpet with explicit behaviour and provenance in its tooltip. */
|
||||
public final class MagicCarpetItem extends Item {
|
||||
public MagicCarpetItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.description")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
var origin = SanctuaryItemProvenance.origin(stack);
|
||||
if (SanctuaryItemProvenance.NOTCH_REWARD.equals(origin)) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.origin.notch")
|
||||
.withStyle(ChatFormatting.GOLD));
|
||||
} else if (SanctuaryItemProvenance.SHOP_PURCHASE.equals(origin)) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.origin.shop")
|
||||
.withStyle(ChatFormatting.AQUA));
|
||||
} else if (SanctuaryItemProvenance.LEGACY_NOTCH_TOTEM.equals(origin)) {
|
||||
tooltip.accept(Component.translatable("item.iliketomoveit.magic_carpet.origin.legacy")
|
||||
.withStyle(ChatFormatting.DARK_PURPLE));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoil(ItemStack stack) {
|
||||
return SanctuaryItemProvenance.hasOrigin(stack, SanctuaryItemProvenance.NOTCH_REWARD)
|
||||
|| super.isFoil(stack);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetMigration;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Converts historical Totems as loaded chests and other vanilla containers are decoded. */
|
||||
@Mixin(BaseContainerBlockEntity.class)
|
||||
abstract class MagicCarpetContainerMixin {
|
||||
@Inject(method = "loadAdditional", at = @At("TAIL"))
|
||||
private void sanctuary26$migrateNotchTotem(ValueInput input, CallbackInfo ci) {
|
||||
MagicCarpetMigration.migrateContainer((Container) (Object) this);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.mixin;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.transport.MagicCarpetMigration;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Converts historical Totems already present as loaded item entities. */
|
||||
@Mixin(ItemEntity.class)
|
||||
abstract class MagicCarpetItemEntityMixin {
|
||||
@Inject(method = "tick", at = @At("HEAD"))
|
||||
private void sanctuary26$migrateNotchTotem(CallbackInfo ci) {
|
||||
ItemEntity self = (ItemEntity) (Object) this;
|
||||
ItemStack current = self.getItem();
|
||||
ItemStack replacement = MagicCarpetMigration.migrateStack(current);
|
||||
if (replacement != current) self.setItem(replacement);
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.iliketomoveit.registry;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.item.AircraftItem;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.item.GrapplingRodItem;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.item.MagicCarpetItem;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -25,7 +26,11 @@ public final class ILikeToMoveItItems {
|
||||
key -> new BoatItem(ILikeToMoveItEntityTypes.MAGIC_CLOUD,
|
||||
new Item.Properties().stacksTo(1).rarity(Rarity.UNCOMMON).setId(key)));
|
||||
public static final Item MAGIC_CARPET = register("magic_carpet",
|
||||
key -> new Item(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC).setId(key)));
|
||||
key -> new MagicCarpetItem(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC).setId(key)));
|
||||
/** Hidden read-preserving alias for inventories saved by the historical MagicCarpet mod. */
|
||||
public static final Item LEGACY_NOTCH_TOTEM = register(
|
||||
Identifier.fromNamespaceAndPath("portals", "notch_totem"),
|
||||
key -> new MagicCarpetItem(new Item.Properties().stacksTo(1).rarity(Rarity.EPIC).setId(key)));
|
||||
public static final Item GRAPPLING_HAND = register("grappling_hand",
|
||||
key -> new Item(new Item.Properties().setId(key)));
|
||||
public static final Item GRAPPLING_ROD = register("grappling_rod",
|
||||
|
||||
+16
-25
@@ -1,10 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItItems;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
@@ -27,7 +24,7 @@ public final class MagicCarpetController {
|
||||
private static final int RADIUS = 1;
|
||||
private static final Block CARPET_BLOCK = Blocks.GLASS;
|
||||
private static final BlockState CARPET_STATE = CARPET_BLOCK.defaultBlockState();
|
||||
private static final Map<UUID, CarpetState> ACTIVE_CARPETS = new HashMap<>();
|
||||
private static final MagicCarpetLedger ACTIVE_CARPETS = new MagicCarpetLedger();
|
||||
|
||||
private MagicCarpetController() {
|
||||
}
|
||||
@@ -45,8 +42,8 @@ public final class MagicCarpetController {
|
||||
connected.add(id);
|
||||
boolean active = !player.isSpectator() && (isMagicCarpet(player.getMainHandItem()) || isMagicCarpet(player.getOffhandItem()));
|
||||
if (active) {
|
||||
if (!ACTIVE_CARPETS.containsKey(id)) {
|
||||
ACTIVE_CARPETS.put(id, CarpetState.empty(player.level().dimension()));
|
||||
if (!ACTIVE_CARPETS.contains(id)) {
|
||||
ACTIVE_CARPETS.track(id, player.level().dimension(), Set.of());
|
||||
player.sendOverlayMessage(Component.translatable("message.iliketomoveit.magic_carpet.enabled"));
|
||||
}
|
||||
updateCarpet(id, player);
|
||||
@@ -54,27 +51,26 @@ public final class MagicCarpetController {
|
||||
deactivate(server, id, true);
|
||||
}
|
||||
}
|
||||
Iterator<Map.Entry<UUID, CarpetState>> iterator = ACTIVE_CARPETS.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<UUID, CarpetState> entry = iterator.next();
|
||||
if (!connected.contains(entry.getKey())) {
|
||||
removePositions(server, entry.getKey(), entry.getValue(), entry.getValue().positions());
|
||||
iterator.remove();
|
||||
for (UUID owner : ACTIVE_CARPETS.owners()) {
|
||||
if (!connected.contains(owner)) {
|
||||
MagicCarpetLedger.State state = ACTIVE_CARPETS.remove(owner);
|
||||
if (state != null) removePositions(server, owner, state, state.positions());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMagicCarpet(ItemStack stack) {
|
||||
return !stack.isEmpty() && stack.is(ILikeToMoveItItems.MAGIC_CARPET);
|
||||
return MagicCarpetMigration.isAnyCarpet(stack);
|
||||
}
|
||||
|
||||
private static void updateCarpet(UUID owner, ServerPlayer player) {
|
||||
ServerLevel level = player.level();
|
||||
CarpetState previous = ACTIVE_CARPETS.getOrDefault(owner, CarpetState.empty(level.dimension()));
|
||||
MagicCarpetLedger.State previous = ACTIVE_CARPETS.state(owner, level.dimension());
|
||||
Set<BlockPos> target = carpetPositions(player.blockPosition().below(player.isCrouching() ? 2 : 1));
|
||||
if (!previous.dimension().equals(level.dimension())) {
|
||||
removePositions(level.getServer(), owner, previous, previous.positions());
|
||||
previous = CarpetState.empty(level.dimension());
|
||||
ACTIVE_CARPETS.remove(owner);
|
||||
previous = ACTIVE_CARPETS.state(owner, level.dimension());
|
||||
} else {
|
||||
Set<BlockPos> stale = new HashSet<>(previous.positions());
|
||||
stale.removeAll(target);
|
||||
@@ -91,7 +87,7 @@ public final class MagicCarpetController {
|
||||
placed.add(pos);
|
||||
}
|
||||
}
|
||||
ACTIVE_CARPETS.put(owner, new CarpetState(level.dimension(), Set.copyOf(placed)));
|
||||
ACTIVE_CARPETS.track(owner, level.dimension(), placed);
|
||||
player.resetFallDistance();
|
||||
}
|
||||
|
||||
@@ -104,14 +100,14 @@ public final class MagicCarpetController {
|
||||
}
|
||||
|
||||
private static void deactivate(MinecraftServer server, UUID owner, boolean notify) {
|
||||
CarpetState state = ACTIVE_CARPETS.remove(owner);
|
||||
MagicCarpetLedger.State state = ACTIVE_CARPETS.remove(owner);
|
||||
if (state == null) return;
|
||||
removePositions(server, owner, state, state.positions());
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(owner);
|
||||
if (notify && player != null) player.sendOverlayMessage(Component.translatable("message.iliketomoveit.magic_carpet.disabled"));
|
||||
}
|
||||
|
||||
private static void removePositions(MinecraftServer server, UUID owner, CarpetState state, Set<BlockPos> positions) {
|
||||
private static void removePositions(MinecraftServer server, UUID owner, MagicCarpetLedger.State state, Set<BlockPos> positions) {
|
||||
ServerLevel level = server.getLevel(state.dimension());
|
||||
if (level == null) return;
|
||||
for (BlockPos pos : positions) {
|
||||
@@ -122,18 +118,13 @@ public final class MagicCarpetController {
|
||||
}
|
||||
|
||||
private static boolean isTrackedByAnother(UUID owner, ResourceKey<Level> dimension, BlockPos pos) {
|
||||
return ACTIVE_CARPETS.entrySet().stream().anyMatch(entry -> !entry.getKey().equals(owner)
|
||||
&& entry.getValue().dimension().equals(dimension) && entry.getValue().positions().contains(pos));
|
||||
return ACTIVE_CARPETS.trackedByAnother(owner, dimension, pos);
|
||||
}
|
||||
|
||||
private static void reset(MinecraftServer server) {
|
||||
Map<UUID, CarpetState> copy = new HashMap<>(ACTIVE_CARPETS);
|
||||
ACTIVE_CARPETS.clear();
|
||||
Map<UUID, MagicCarpetLedger.State> copy = ACTIVE_CARPETS.drain();
|
||||
copy.forEach((owner, state) -> removePositions(server, owner, state, state.positions()));
|
||||
ILikeToMoveItMod.LOGGER.debug("Magic Carpet temporary platforms cleaned.");
|
||||
}
|
||||
|
||||
private record CarpetState(ResourceKey<Level> dimension, Set<BlockPos> positions) {
|
||||
private static CarpetState empty(ResourceKey<Level> dimension) { return new CarpetState(dimension, Set.of()); }
|
||||
}
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Pure ownership ledger preventing overlapping temporary platforms from deleting each other. */
|
||||
public final class MagicCarpetLedger {
|
||||
private final Map<UUID, State> carpets = new HashMap<>();
|
||||
|
||||
public boolean contains(UUID owner) {
|
||||
return carpets.containsKey(owner);
|
||||
}
|
||||
|
||||
public State state(UUID owner, ResourceKey<Level> dimension) {
|
||||
return carpets.getOrDefault(owner, State.empty(dimension));
|
||||
}
|
||||
|
||||
public void track(UUID owner, ResourceKey<Level> dimension, Set<BlockPos> positions) {
|
||||
carpets.put(owner, new State(dimension, Set.copyOf(positions)));
|
||||
}
|
||||
|
||||
public State remove(UUID owner) {
|
||||
return carpets.remove(owner);
|
||||
}
|
||||
|
||||
public Set<UUID> owners() {
|
||||
return Set.copyOf(carpets.keySet());
|
||||
}
|
||||
|
||||
public boolean trackedByAnother(UUID owner, ResourceKey<Level> dimension, BlockPos pos) {
|
||||
return carpets.entrySet().stream().anyMatch(entry -> !entry.getKey().equals(owner)
|
||||
&& entry.getValue().dimension().equals(dimension)
|
||||
&& entry.getValue().positions().contains(pos));
|
||||
}
|
||||
|
||||
public Map<UUID, State> drain() {
|
||||
Map<UUID, State> copy = Map.copyOf(carpets);
|
||||
carpets.clear();
|
||||
return copy;
|
||||
}
|
||||
|
||||
public record State(ResourceKey<Level> dimension, Set<BlockPos> positions) {
|
||||
public State {
|
||||
positions = Set.copyOf(positions);
|
||||
}
|
||||
|
||||
private static State empty(ResourceKey<Level> dimension) {
|
||||
return new State(dimension, Set.of());
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItItems;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.item.SanctuaryItemProvenance;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Read-preserving conversion from the historical Totem identity to the canonical carpet. */
|
||||
public final class MagicCarpetMigration {
|
||||
private MagicCarpetMigration() {
|
||||
}
|
||||
|
||||
public static boolean isAnyCarpet(ItemStack stack) {
|
||||
return stack != null && !stack.isEmpty()
|
||||
&& (stack.is(ILikeToMoveItItems.MAGIC_CARPET)
|
||||
|| stack.is(ILikeToMoveItItems.LEGACY_NOTCH_TOTEM));
|
||||
}
|
||||
|
||||
public static ItemStack migrateStack(ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty() || !stack.is(ILikeToMoveItItems.LEGACY_NOTCH_TOTEM)) return stack;
|
||||
ItemStack migrated = stack.transmuteCopy(ILikeToMoveItItems.MAGIC_CARPET, stack.getCount());
|
||||
SanctuaryItemProvenance.stampIfAbsent(migrated, SanctuaryItemProvenance.LEGACY_NOTCH_TOTEM);
|
||||
return migrated;
|
||||
}
|
||||
|
||||
public static int migrateContainer(Container container) {
|
||||
if (container == null) return 0;
|
||||
int migrated = 0;
|
||||
for (int slot = 0; slot < container.getContainerSize(); slot++) {
|
||||
ItemStack current = container.getItem(slot);
|
||||
ItemStack replacement = migrateStack(current);
|
||||
if (replacement == current) continue;
|
||||
container.setItem(slot, replacement);
|
||||
migrated++;
|
||||
}
|
||||
if (migrated > 0) container.setChanged();
|
||||
return migrated;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.data.ILikeToMoveItData;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Converts online inventories once their historical alias has been decoded. */
|
||||
public final class MagicCarpetMigrationService {
|
||||
private MagicCarpetMigrationService() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> migrate(handler.player));
|
||||
ServerTickEvents.END_SERVER_TICK.register(MagicCarpetMigrationService::tick);
|
||||
}
|
||||
|
||||
private static void tick(MinecraftServer server) {
|
||||
ILikeToMoveItData.get(server);
|
||||
if (server.overworld().getGameTime() % 20L != 0L) return;
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) migrate(player);
|
||||
}
|
||||
|
||||
private static void migrate(ServerPlayer player) {
|
||||
int migrated = MagicCarpetMigration.migrateContainer(player.getInventory());
|
||||
migrated += MagicCarpetMigration.migrateContainer(player.getEnderChestInventory());
|
||||
if (migrated > 0) {
|
||||
ILikeToMoveItMod.LOGGER.info("[Magic Carpet] Migrated {} historical Totem stack(s) for {}.",
|
||||
migrated, player.getGameProfile().name());
|
||||
}
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItItems;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.item.SanctuaryItemProvenance;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.world.SanctuaryAlphaWorld;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.world.SanctuaryDimensions;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.entity.projectile.FireworkRocketEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.component.FireworkExplosion;
|
||||
import net.minecraft.world.item.component.Fireworks;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
|
||||
/** Idempotently materializes the globally reserved Magic Carpet in Alpha. */
|
||||
public final class NotchCarpetRewardService {
|
||||
private static final int REWARD_SLOT = 13;
|
||||
|
||||
private NotchCarpetRewardService() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
ServerTickEvents.END_SERVER_TICK.register(NotchCarpetRewardService::tick);
|
||||
}
|
||||
|
||||
private static void tick(MinecraftServer server) {
|
||||
if (server.overworld().getGameTime() % 20L != 0L) return;
|
||||
var state = SanctuaryNotchMilestone.snapshot(server);
|
||||
if (!state.defeated() || state.initialRewardDelivered()) return;
|
||||
if (!state.initialRewardClaimed() && !SanctuaryNotchMilestone.claimInitialReward(server)) return;
|
||||
ServerLevel alpha = server.getLevel(SanctuaryDimensions.ALPHA);
|
||||
if (alpha == null) return;
|
||||
BlockPos home = SanctuaryAlphaWorld.residence(alpha.getSeed());
|
||||
if (!alpha.isLoaded(home)) return;
|
||||
RewardChest result = ensureRewardChest(alpha, home);
|
||||
if (result == null || !SanctuaryNotchMilestone.completeInitialReward(server)) return;
|
||||
if (result.created()) {
|
||||
launchFirework(alpha, result.pos());
|
||||
server.getPlayerList().broadcastSystemMessage(
|
||||
Component.translatable("message.iliketomoveit.magic_carpet.reward_created"), false);
|
||||
}
|
||||
ILikeToMoveItMod.LOGGER.info(
|
||||
"[Magic Carpet] Completed unique Notch reward at {} (origin=notch_reward, recovered={}).",
|
||||
result.pos().toShortString(), !result.created());
|
||||
}
|
||||
|
||||
private static RewardChest ensureRewardChest(ServerLevel level, BlockPos home) {
|
||||
List<BlockPos> candidates = candidates(home);
|
||||
for (BlockPos pos : candidates) {
|
||||
if (containsInitialReward(level, pos)) return new RewardChest(pos, false);
|
||||
}
|
||||
for (BlockPos pos : candidates) {
|
||||
if (!level.getBlockState(pos).isAir() || !level.getBlockState(pos.above()).isAir()
|
||||
|| level.getBlockState(pos.below()).isAir()) continue;
|
||||
if (!level.setBlock(pos, Blocks.CHEST.defaultBlockState(), Block.UPDATE_ALL)) continue;
|
||||
if (level.getBlockEntity(pos) instanceof ChestBlockEntity chest) {
|
||||
chest.setItem(REWARD_SLOT, rewardStack());
|
||||
chest.setChanged();
|
||||
level.sendBlockUpdated(pos, level.getBlockState(pos), level.getBlockState(pos), Block.UPDATE_ALL);
|
||||
return new RewardChest(pos, true);
|
||||
}
|
||||
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean containsInitialReward(ServerLevel level, BlockPos pos) {
|
||||
if (level.getBlockState(pos).getBlock() != Blocks.CHEST
|
||||
|| !(level.getBlockEntity(pos) instanceof Container container)) return false;
|
||||
for (ItemStack stack : container) {
|
||||
if (stack.is(ILikeToMoveItItems.MAGIC_CARPET)
|
||||
&& SanctuaryItemProvenance.hasOrigin(stack, SanctuaryItemProvenance.NOTCH_REWARD)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ItemStack rewardStack() {
|
||||
ItemStack reward = new ItemStack(ILikeToMoveItItems.MAGIC_CARPET);
|
||||
reward.set(DataComponents.CUSTOM_NAME,
|
||||
Component.translatable("item.iliketomoveit.magic_carpet.notch_reward_name"));
|
||||
SanctuaryItemProvenance.stamp(reward, SanctuaryItemProvenance.NOTCH_REWARD);
|
||||
return reward;
|
||||
}
|
||||
|
||||
private static List<BlockPos> candidates(BlockPos home) {
|
||||
ArrayList<BlockPos> positions = new ArrayList<>();
|
||||
for (int y : new int[] {0, 1, -1, 2}) {
|
||||
for (int radius = 2; radius <= 7; radius++) {
|
||||
for (int x = -radius; x <= radius; x++) for (int z = -radius; z <= radius; z++) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||
BlockPos candidate = home.offset(x, y, z).immutable();
|
||||
if (SanctuaryAlphaWorld.containsHorizontal(candidate)) positions.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(positions);
|
||||
}
|
||||
|
||||
private static void launchFirework(ServerLevel level, BlockPos pos) {
|
||||
ItemStack rocket = new ItemStack(Items.FIREWORK_ROCKET);
|
||||
FireworkExplosion explosion = new FireworkExplosion(FireworkExplosion.Shape.STAR,
|
||||
new IntArrayList(new int[] {0xE8B923, 0xF1F1F1}),
|
||||
new IntArrayList(new int[] {0x9B59B6}), true, true);
|
||||
rocket.set(DataComponents.FIREWORKS, new Fireworks(1, List.of(explosion)));
|
||||
level.addFreshEntity(new FireworkRocketEntity(level, pos.getX() + 0.5D,
|
||||
pos.getY() + 1.2D, pos.getZ() + 0.5D, rocket));
|
||||
}
|
||||
|
||||
private record RewardChest(BlockPos pos, boolean created) {
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,13 @@
|
||||
"itemGroup.iliketomoveit.transport": "I Like To Move It",
|
||||
"item.iliketomoveit.aircraft": "Aircraft",
|
||||
"item.iliketomoveit.magic_cloud": "Magic Cloud",
|
||||
"item.iliketomoveit.magic_carpet": "Magic Carpet (laboratory)",
|
||||
"item.iliketomoveit.magic_carpet": "Magic Carpet",
|
||||
"item.portals.notch_totem": "Notch Totem (legacy)",
|
||||
"item.iliketomoveit.magic_carpet.notch_reward_name": "Notch's Magic Carpet",
|
||||
"item.iliketomoveit.magic_carpet.description": "Hold it to maintain a temporary 3 × 3 platform beneath your feet.",
|
||||
"item.iliketomoveit.magic_carpet.origin.notch": "Unique world reward from Notch",
|
||||
"item.iliketomoveit.magic_carpet.origin.shop": "Replacement purchased from the Shop",
|
||||
"item.iliketomoveit.magic_carpet.origin.legacy": "Converted from the historical Notch Totem",
|
||||
"block.iliketomoveit.waystone": "Waystone",
|
||||
"block.waystone.waystone": "Waystone (legacy)",
|
||||
"entity.iliketomoveit.aircraft": "Aircraft",
|
||||
@@ -11,6 +17,7 @@
|
||||
"container.iliketomoveit.aircraft_engine": "Aircraft Engine",
|
||||
"message.iliketomoveit.magic_carpet.enabled": "Magic Carpet enabled",
|
||||
"message.iliketomoveit.magic_carpet.disabled": "Magic Carpet disabled",
|
||||
"message.iliketomoveit.magic_carpet.reward_created": "Notch's unique Magic Carpet appeared in a chest near his house.",
|
||||
"subtitles.iliketomoveit.magic_cloud.fly": "Magic Cloud flies",
|
||||
"screen.iliketomoveit.waystone.title": "Waystones",
|
||||
"screen.iliketomoveit.waystone.empty": "No other waystone in this dimension",
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
"itemGroup.iliketomoveit.transport": "I Like To Move It",
|
||||
"item.iliketomoveit.aircraft": "Avion",
|
||||
"item.iliketomoveit.magic_cloud": "Nuage magique",
|
||||
"item.iliketomoveit.magic_carpet": "Tapis magique (laboratoire)",
|
||||
"item.iliketomoveit.magic_carpet": "Tapis magique",
|
||||
"item.portals.notch_totem": "Totem de Notch (hérité)",
|
||||
"item.iliketomoveit.magic_carpet.notch_reward_name": "Tapis magique de Notch",
|
||||
"item.iliketomoveit.magic_carpet.description": "Tenu en main, il maintient une plateforme temporaire 3 × 3 sous vos pieds.",
|
||||
"item.iliketomoveit.magic_carpet.origin.notch": "Récompense mondiale unique de Notch",
|
||||
"item.iliketomoveit.magic_carpet.origin.shop": "Remplacement acheté au Shop",
|
||||
"item.iliketomoveit.magic_carpet.origin.legacy": "Converti depuis le Totem de Notch historique",
|
||||
"block.iliketomoveit.waystone": "Pierre de voyage",
|
||||
"block.waystone.waystone": "Pierre de voyage (héritée)",
|
||||
"entity.iliketomoveit.aircraft": "Avion",
|
||||
@@ -11,6 +17,7 @@
|
||||
"container.iliketomoveit.aircraft_engine": "Moteur de l'avion",
|
||||
"message.iliketomoveit.magic_carpet.enabled": "Tapis magique activé",
|
||||
"message.iliketomoveit.magic_carpet.disabled": "Tapis magique désactivé",
|
||||
"message.iliketomoveit.magic_carpet.reward_created": "Le Tapis magique unique de Notch est apparu dans un coffre près de sa maison.",
|
||||
"subtitles.iliketomoveit.magic_cloud.fly": "Le nuage magique vole",
|
||||
"screen.iliketomoveit.waystone.title": "Pierres de voyage",
|
||||
"screen.iliketomoveit.waystone.empty": "Aucune autre pierre dans cette dimension",
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
"itemGroup.iliketomoveit.transport": "I Like To Move It",
|
||||
"item.iliketomoveit.aircraft": "Самолёт",
|
||||
"item.iliketomoveit.magic_cloud": "Волшебное облако",
|
||||
"item.iliketomoveit.magic_carpet": "Ковёр-самолёт (лаборатория)",
|
||||
"item.iliketomoveit.magic_carpet": "Ковёр-самолёт",
|
||||
"item.portals.notch_totem": "Тотем Нотча (старый)",
|
||||
"item.iliketomoveit.magic_carpet.notch_reward_name": "Ковёр-самолёт Нотча",
|
||||
"item.iliketomoveit.magic_carpet.description": "В руке поддерживает временную платформу 3 × 3 под ногами.",
|
||||
"item.iliketomoveit.magic_carpet.origin.notch": "Уникальная мировая награда за Нотча",
|
||||
"item.iliketomoveit.magic_carpet.origin.shop": "Замена, купленная в Магазине",
|
||||
"item.iliketomoveit.magic_carpet.origin.legacy": "Преобразован из исторического Тотема Нотча",
|
||||
"block.iliketomoveit.waystone": "Камень пути",
|
||||
"block.waystone.waystone": "Камень пути (старый)",
|
||||
"entity.iliketomoveit.aircraft": "Самолёт",
|
||||
@@ -11,6 +17,7 @@
|
||||
"container.iliketomoveit.aircraft_engine": "Двигатель самолёта",
|
||||
"message.iliketomoveit.magic_carpet.enabled": "Ковёр-самолёт включён",
|
||||
"message.iliketomoveit.magic_carpet.disabled": "Ковёр-самолёт выключен",
|
||||
"message.iliketomoveit.magic_carpet.reward_created": "Уникальный ковёр-самолёт Нотча появился в сундуке возле его дома.",
|
||||
"subtitles.iliketomoveit.magic_cloud.fly": "Волшебное облако летит",
|
||||
"screen.iliketomoveit.waystone.title": "Камни пути",
|
||||
"screen.iliketomoveit.waystone.empty": "В этом измерении нет других камней",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": {
|
||||
"layer0": "minecraft:block/purple_wool"
|
||||
"layer0": "iliketomoveit:item/magic_carpet"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 366 B |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "iliketomoveit:item/magic_carpet"
|
||||
}
|
||||
}
|
||||
@@ -33,13 +33,13 @@
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"anotherworld": ">=0.0.0-alpha.46",
|
||||
"sanctuary": ">=0.0.0-alpha.21"
|
||||
"sanctuary": ">=0.0.0-alpha.127"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "iliketomoveit",
|
||||
"data_version": 3,
|
||||
"data_version": 4,
|
||||
"network_protocol": 5,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"package": "fr.koka99cab.sanctuary26.iliketomoveit.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"HappyGhastMixin"
|
||||
"HappyGhastMixin",
|
||||
"MagicCarpetContainerMixin",
|
||||
"MagicCarpetItemEntityMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package fr.koka99cab.sanctuary26.iliketomoveit.transport;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Regression checks for overlap, world changes and shutdown cleanup ownership. */
|
||||
public final class MagicCarpetLedgerSmoke {
|
||||
private MagicCarpetLedgerSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
MagicCarpetLedger ledger = new MagicCarpetLedger();
|
||||
UUID first = UUID.fromString("00000000-0000-0000-0000-000000000001");
|
||||
UUID second = UUID.fromString("00000000-0000-0000-0000-000000000002");
|
||||
ResourceKey<Level> firstWorld = ResourceKey.create(Registries.DIMENSION,
|
||||
Identifier.fromNamespaceAndPath("test", "first"));
|
||||
ResourceKey<Level> secondWorld = ResourceKey.create(Registries.DIMENSION,
|
||||
Identifier.fromNamespaceAndPath("test", "second"));
|
||||
BlockPos shared = new BlockPos(4, 70, 4);
|
||||
ledger.track(first, firstWorld, Set.of(shared));
|
||||
ledger.track(second, firstWorld, Set.of(shared));
|
||||
if (!ledger.trackedByAnother(first, firstWorld, shared)) {
|
||||
throw new AssertionError("Overlapping carpets lost their second owner");
|
||||
}
|
||||
ledger.remove(first);
|
||||
if (ledger.trackedByAnother(second, firstWorld, shared)) {
|
||||
throw new AssertionError("Removed owner still controls the shared glass");
|
||||
}
|
||||
ledger.track(second, secondWorld, Set.of(new BlockPos(0, 64, 0)));
|
||||
if (!ledger.state(second, secondWorld).dimension().equals(secondWorld)
|
||||
|| !ledger.state(second, firstWorld).dimension().equals(secondWorld)) {
|
||||
throw new AssertionError("Dimension change did not replace the tracked world");
|
||||
}
|
||||
Map<UUID, MagicCarpetLedger.State> shutdown = ledger.drain();
|
||||
if (shutdown.size() != 1 || !ledger.owners().isEmpty()) {
|
||||
throw new AssertionError("Server shutdown did not atomically drain temporary platforms");
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-1
@@ -2,7 +2,15 @@
|
||||
|
||||
It's Alive possède le vivant de Sanctuary. Il enrichit directement le Creeper vanilla et accueille désormais les papillons de Pipallon, les Mooblooms fleuries et les créatures de Real Ghost — baleine volante comprise — sous un référencement unifié `itsalive:*`.
|
||||
|
||||
## Version courante — `0.0.0-alpha.36`
|
||||
## Version courante — `0.0.0-alpha.37`
|
||||
|
||||
## Résident Notch
|
||||
|
||||
- `itsalive:notch` est l'unique résident persistant de la maison Indev dans `sanctuary:alpha` ; il n'apparaît jamais naturellement et ne peut pas être invoqué par les commandes génériques.
|
||||
- Son UUID canonique est sauvegardé, les doublons sont supprimés et le service le recrée au domicile s'il manque. L'alias caché `portals:notch` charge les anciennes entités du prototype sans dupliquer les règles.
|
||||
- Le premier coup volontaire d'un joueur transforme l'humain en Titan. Le nombre de participants est borné à huit, la santé va de 360 à 600 et un combat abandonné se réinitialise après quinze secondes sans participant actif.
|
||||
- It's Alive possède l'entité et son cycle de vie ; les attaques passent exclusivement par l'API publique `NotchCombatApi`, implémentée par Ouch.
|
||||
- Diagnostics : `/masterkey notch inspect` et `/masterkey notch restore`.
|
||||
|
||||
## Creepers
|
||||
|
||||
|
||||
+15
-6
@@ -35,6 +35,14 @@ tasks.register("culinaryPartialMatchingSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.itsalive.culinary.recipe.CulinaryPartialMatchingSmoke"
|
||||
}
|
||||
|
||||
tasks.register("notchFightRulesSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks bounded Notch Titan scaling, reset timing and reflected-fireball damage."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.itsalive.entity.notch.NotchFightRulesSmoke"
|
||||
}
|
||||
|
||||
tasks.named("test") {
|
||||
// Quantity contracts are executable smoke tasks, not JUnit engine tests.
|
||||
failOnNoDiscoveredTests = false
|
||||
@@ -43,6 +51,7 @@ tasks.named("test") {
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("canapliaFoundationSmoke")
|
||||
dependsOn tasks.named("canapliaGameplaySmoke")
|
||||
dependsOn tasks.named("notchFightRulesSmoke")
|
||||
}
|
||||
|
||||
def generatedResources = layout.buildDirectory.dir("generated/resources")
|
||||
@@ -556,7 +565,7 @@ tasks.register("verifyItsAlive") {
|
||||
def climateSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ClimateCropForaging.java").text
|
||||
def pineappleSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/PineappleItem.java").text
|
||||
def worldgenSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ItsAliveAgricultureWorldGeneration.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| climateMigration.target?.pack_version != "26.2.0-alpha.157"
|
||||
|| climateMigration.target?.modules?.itsalive != "0.0.0-alpha.24"
|
||||
|| !climateSource.contains("PlayerBlockBreakEvents.AFTER.register")
|
||||
@@ -1313,7 +1322,7 @@ tasks.register("verifyCulinaryReliability") {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.28"
|
||||
|| migration.culinary_fixes?.food_press_contents_destroyed_on_break != true
|
||||
|| migration.culinary_fixes?.comparator_contract?.blocks?.size() != 6
|
||||
@@ -1351,7 +1360,7 @@ tasks.register("verifyCauldronBatching") {
|
||||
def recipe = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java").text
|
||||
def cauldron = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java").text
|
||||
def timed = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractTimedCulinaryBlockEntity.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.29"
|
||||
|| migration.cauldron_batches?.logical_stack_limit != 64
|
||||
|| migration.cauldron_batches?.identity != "same_item_same_components"
|
||||
@@ -1389,7 +1398,7 @@ tasks.register("verifyFermentationBarrelAutomation") {
|
||||
def partialMatching = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatching.java").text
|
||||
def blockstate = new JsonSlurper().parse(
|
||||
file("src/main/resources/assets/itsalive/blockstates/fermentation_barrel.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| migration.source?.modules?.itsalive != "0.0.0-alpha.29"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.30"
|
||||
|| migration.fermentation_barrel?.maximum_units_per_input_slot != 1
|
||||
@@ -1437,7 +1446,7 @@ tasks.register("verifyCulinaryLecternRelease") {
|
||||
[(locale): new JsonSlurper().parse(file(
|
||||
"src/main/resources/assets/itsalive/lang/${locale}.json"))]
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.31"
|
||||
|| migration.culinary_lectern?.book_tag != "minecraft:lectern_books"
|
||||
|| migration.culinary_lectern?.owner_catalog_shared_while_placed != true
|
||||
@@ -1500,7 +1509,7 @@ tasks.register("verifyCanapliaForbiddenBook") {
|
||||
fr_fr: "Livre interdit",
|
||||
en_us: "Forbidden Book",
|
||||
ru_ru: "Запретная книга"]
|
||||
if (project.version.toString() != "0.0.0-alpha.36"
|
||||
if (project.version.toString() != "0.0.0-alpha.38"
|
||||
|| !pageItem.contains("CanapliaKnowledge.learnRandomUnknown(serverPlayer)")
|
||||
|| !knowledge.contains("unknownVariants(data.personal(player.getUUID()))")
|
||||
|| !knowledge.contains("data.personal(inscription.ownerId())")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaFoundation;
|
||||
import fr.koka99cab.sanctuary26.itsalive.entity.notch.NotchResidentService;
|
||||
import fr.koka99cab.sanctuary26.itsalive.network.ItsAliveNetworking;
|
||||
import fr.koka99cab.sanctuary26.itsalive.culinary.knowledge.CulinaryRecipeKnowledge;
|
||||
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveRegistries;
|
||||
@@ -13,7 +14,7 @@ import org.slf4j.LoggerFactory;
|
||||
public final class ItsAliveMod implements ModInitializer {
|
||||
public static final String MOD_ID = "itsalive";
|
||||
public static final String DISPLAY_NAME = "It's Alive !";
|
||||
public static final int CURRENT_DATA_VERSION = 3;
|
||||
public static final int CURRENT_DATA_VERSION = 4;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public ItsAliveMod() {
|
||||
@@ -27,6 +28,7 @@ public final class ItsAliveMod implements ModInitializer {
|
||||
public void onInitialize() {
|
||||
CanapliaFoundation.initialize();
|
||||
ItsAliveRegistries.initialize();
|
||||
NotchResidentService.initialize();
|
||||
ItsAliveNetworking.initialize();
|
||||
CulinaryRecipeKnowledge.initialize();
|
||||
ProceduralVillagerNames.initialize();
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.api.notch;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
|
||||
/** Public bridge allowing the combat-owning Ouch module to drive Notch attacks. */
|
||||
public final class NotchCombatApi {
|
||||
private static Engine engine = Engine.NOOP;
|
||||
private static boolean registered;
|
||||
|
||||
private NotchCombatApi() {
|
||||
}
|
||||
|
||||
public static synchronized void register(Engine implementation) {
|
||||
Objects.requireNonNull(implementation, "implementation");
|
||||
if (registered) throw new IllegalStateException("A Notch combat engine is already registered");
|
||||
engine = implementation;
|
||||
registered = true;
|
||||
}
|
||||
|
||||
public static void telegraph(ServerLevel level, LivingEntity boss, LivingEntity target, Attack attack) {
|
||||
engine.telegraph(level, boss, target, attack);
|
||||
}
|
||||
|
||||
public static void execute(ServerLevel level, LivingEntity boss, LivingEntity target, Attack attack) {
|
||||
engine.execute(level, boss, target, attack);
|
||||
}
|
||||
|
||||
public enum Attack {
|
||||
MELEE,
|
||||
FIREBALL,
|
||||
LIGHTNING
|
||||
}
|
||||
|
||||
public interface Engine {
|
||||
Engine NOOP = new Engine() {
|
||||
@Override public void telegraph(ServerLevel level, LivingEntity boss, LivingEntity target, Attack attack) { }
|
||||
@Override public void execute(ServerLevel level, LivingEntity boss, LivingEntity target, Attack attack) { }
|
||||
};
|
||||
|
||||
void telegraph(ServerLevel level, LivingEntity boss, LivingEntity target, Attack attack);
|
||||
|
||||
void execute(ServerLevel level, LivingEntity boss, LivingEntity target, Attack attack);
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -34,7 +34,8 @@ public final class CanapliaHarvestService {
|
||||
|
||||
public static void onMatureHarvest(ServerLevel level, ServerPlayer player, BlockPos pos, BlockState state) {
|
||||
if (!state.is(CanapliaContent.HEMP_CROP) || !CanapliaContent.HEMP_CROP.isMaxAge(state)) return;
|
||||
CanapliaVariantKey parent = state.getValue(CanapliaHempCropBlock.VARIANT);
|
||||
CanapliaVariantKey parent = CanapliaVariantLifecycle.harvestedFromPlant(
|
||||
state.getValue(CanapliaHempCropBlock.VARIANT));
|
||||
CanapliaKnowledge.discover(player, parent, "harvest");
|
||||
ClimateCropForaging.CropClimate climate = ClimateCropForaging.climate(level.getBiome(pos));
|
||||
AmbianceLunarHarvestApi.Conditions lunar = AmbianceLunarHarvestApi.evaluate(level, pos);
|
||||
@@ -65,7 +66,8 @@ public final class CanapliaHarvestService {
|
||||
if (pos.equals(center)) continue;
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (state.is(CanapliaContent.HEMP_CROP) && CanapliaContent.HEMP_CROP.isMaxAge(state)) {
|
||||
variants.add(state.getValue(CanapliaHempCropBlock.VARIANT));
|
||||
variants.add(CanapliaVariantLifecycle.harvestedFromPlant(
|
||||
state.getValue(CanapliaHempCropBlock.VARIANT)));
|
||||
}
|
||||
}
|
||||
return Set.copyOf(variants);
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia;
|
||||
|
||||
/** Stable variant hand-off across placement, growth, block-state persistence and harvest. */
|
||||
public final class CanapliaVariantLifecycle {
|
||||
private CanapliaVariantLifecycle() {
|
||||
}
|
||||
|
||||
public static CanapliaVariantKey plantedFromSeed(CanapliaVariantKey seedVariant) {
|
||||
return require(seedVariant);
|
||||
}
|
||||
|
||||
public static CanapliaVariantKey afterGrowth(CanapliaVariantKey currentVariant) {
|
||||
return require(currentVariant);
|
||||
}
|
||||
|
||||
public static CanapliaVariantKey upperFromLower(CanapliaVariantKey lowerVariant) {
|
||||
return require(lowerVariant);
|
||||
}
|
||||
|
||||
public static CanapliaVariantKey harvestedFromPlant(CanapliaVariantKey plantVariant) {
|
||||
return require(plantVariant);
|
||||
}
|
||||
|
||||
public static String persistedKey(CanapliaVariantKey variant) {
|
||||
return require(variant).key();
|
||||
}
|
||||
|
||||
public static CanapliaVariantKey fromPersistedKey(String key) {
|
||||
return CanapliaVariantKey.byKey(key).orElse(CanapliaVariantKey.COMMON);
|
||||
}
|
||||
|
||||
private static CanapliaVariantKey require(CanapliaVariantKey variant) {
|
||||
if (variant == null) throw new IllegalArgumentException("Canaplia variant cannot be null");
|
||||
return variant;
|
||||
}
|
||||
}
|
||||
+17
-6
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.itsalive.canaplia.block;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaContent;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaHarvestService;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantLifecycle;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantKey;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -43,19 +44,28 @@ public final class CanapliaHempCropBlock extends CropBlock {
|
||||
|
||||
@Override
|
||||
protected ItemStack getCloneItemStack(LevelReader level, BlockPos pos, BlockState state, boolean includeData) {
|
||||
return new ItemStack(CanapliaContent.family(state.getValue(VARIANT)).seeds());
|
||||
CanapliaVariantKey variant = CanapliaVariantLifecycle.harvestedFromPlant(state.getValue(VARIANT));
|
||||
return new ItemStack(CanapliaContent.family(variant).seeds());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
|
||||
super.randomTick(state, level, pos, random);
|
||||
synchronizeUpper(level, pos, level.getBlockState(pos));
|
||||
if (level.getRawBrightness(pos, 0) < 9) return;
|
||||
int age = getAge(state);
|
||||
if (age >= getMaxAge()) return;
|
||||
float speed = getGrowthSpeed(this, level, pos);
|
||||
if (random.nextInt((int) (25.0F / speed) + 1) != 0) return;
|
||||
BlockState grown = state.setValue(AGE, age + 1).setValue(VARIANT,
|
||||
CanapliaVariantLifecycle.afterGrowth(state.getValue(VARIANT)));
|
||||
level.setBlock(pos, grown, Block.UPDATE_CLIENTS);
|
||||
synchronizeUpper(level, pos, grown);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void growCrops(Level level, BlockPos pos, BlockState state) {
|
||||
int age = Math.min(getMaxAge(), getAge(state) + getBonemealAgeIncrease(level));
|
||||
BlockState grown = state.setValue(AGE, age);
|
||||
BlockState grown = state.setValue(AGE, age).setValue(VARIANT,
|
||||
CanapliaVariantLifecycle.afterGrowth(state.getValue(VARIANT)));
|
||||
level.setBlock(pos, grown, Block.UPDATE_CLIENTS);
|
||||
synchronizeUpper(level, pos, grown);
|
||||
}
|
||||
@@ -78,7 +88,7 @@ public final class CanapliaHempCropBlock extends CropBlock {
|
||||
if (!(level instanceof ServerLevel serverLevel) || !(player instanceof ServerPlayer serverPlayer)) {
|
||||
return InteractionResult.FAIL;
|
||||
}
|
||||
CanapliaVariantKey variant = state.getValue(VARIANT);
|
||||
CanapliaVariantKey variant = CanapliaVariantLifecycle.harvestedFromPlant(state.getValue(VARIANT));
|
||||
Block.popResource(serverLevel, pos,
|
||||
new ItemStack(CanapliaContent.family(variant).bud(), 1 + serverLevel.getRandom().nextInt(3)));
|
||||
CanapliaHarvestService.onMatureHarvest(serverLevel, serverPlayer, pos, state);
|
||||
@@ -97,7 +107,8 @@ public final class CanapliaHempCropBlock extends CropBlock {
|
||||
if (getAge(state) >= UPPER_START_AGE) {
|
||||
if (aboveState.isAir() || aboveState.is(CanapliaContent.HEMP_TOP)) {
|
||||
level.setBlock(above, CanapliaContent.HEMP_TOP.defaultBlockState()
|
||||
.setValue(AGE, getAge(state)).setValue(VARIANT, state.getValue(VARIANT)), Block.UPDATE_CLIENTS);
|
||||
.setValue(AGE, getAge(state)).setValue(VARIANT,
|
||||
CanapliaVariantLifecycle.upperFromLower(state.getValue(VARIANT))), Block.UPDATE_CLIENTS);
|
||||
}
|
||||
} else if (aboveState.is(CanapliaContent.HEMP_TOP)) {
|
||||
level.removeBlock(above, false);
|
||||
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia.block;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaContent;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantLifecycle;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantKey;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
@@ -26,7 +27,9 @@ public final class CanapliaHempTopBlock extends CropBlock {
|
||||
|
||||
@Override
|
||||
protected ItemStack getCloneItemStack(LevelReader level, BlockPos pos, BlockState state, boolean includeData) {
|
||||
return new ItemStack(CanapliaContent.family(state.getValue(CanapliaHempCropBlock.VARIANT)).seeds());
|
||||
CanapliaVariantKey variant = CanapliaVariantLifecycle.harvestedFromPlant(
|
||||
state.getValue(CanapliaHempCropBlock.VARIANT));
|
||||
return new ItemStack(CanapliaContent.family(variant).seeds());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+9
-3
@@ -1,5 +1,6 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantLifecycle;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaVariantKey;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.block.CanapliaHempCropBlock;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.block.CanapliaResinBlock;
|
||||
@@ -24,12 +25,17 @@ public final class CanapliaVariantBlockItem extends BlockItem {
|
||||
@Override
|
||||
protected BlockState getPlacementState(BlockPlaceContext context) {
|
||||
BlockState state = super.getPlacementState(context);
|
||||
if (state == null) return null;
|
||||
return applyVariant(state, variant);
|
||||
}
|
||||
|
||||
public static BlockState applyVariant(BlockState state, CanapliaVariantKey variant) {
|
||||
if (state == null || variant == null) return state;
|
||||
CanapliaVariantKey placed = CanapliaVariantLifecycle.plantedFromSeed(variant);
|
||||
if (state.hasProperty(CanapliaHempCropBlock.VARIANT)) {
|
||||
return state.setValue(CanapliaHempCropBlock.VARIANT, variant);
|
||||
return state.setValue(CanapliaHempCropBlock.VARIANT, placed);
|
||||
}
|
||||
if (state.hasProperty(CanapliaResinBlock.VARIANT)) {
|
||||
return state.setValue(CanapliaResinBlock.VARIANT, variant);
|
||||
return state.setValue(CanapliaResinBlock.VARIANT, placed);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import fr.koka99cab.sanctuary26.itsalive.client.model.ButterflyModel;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.model.GhostModel;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.model.FlyingWhaleModel;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.model.StandingCowModel;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.model.NotchModel;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.renderer.BrackenRenderer;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.renderer.ButterflyRenderer;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.renderer.GhostRenderer;
|
||||
@@ -12,6 +13,7 @@ import fr.koka99cab.sanctuary26.itsalive.client.renderer.GemChickenRenderer;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.renderer.FlyingWhaleRenderer;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.renderer.InfectedRenderer;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.renderer.MoobloomRenderer;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.renderer.NotchRenderer;
|
||||
import fr.koka99cab.sanctuary26.itsalive.culinary.client.renderer.FryingPanBlockEntityRenderer;
|
||||
import fr.koka99cab.sanctuary26.itsalive.culinary.client.renderer.FermentationBarrelBlockEntityRenderer;
|
||||
import fr.koka99cab.sanctuary26.itsalive.culinary.client.FermentationBarrelHud;
|
||||
@@ -62,6 +64,7 @@ public final class ItsAliveClient implements ClientModInitializer {
|
||||
ModelLayerRegistry.registerModelLayer(StandingCowModel.TEMPERATE_LAYER, StandingCowModel::createTemperateLayer);
|
||||
ModelLayerRegistry.registerModelLayer(StandingCowModel.COLD_LAYER, StandingCowModel::createColdLayer);
|
||||
ModelLayerRegistry.registerModelLayer(StandingCowModel.WARM_LAYER, StandingCowModel::createWarmLayer);
|
||||
ModelLayerRegistry.registerModelLayer(NotchModel.LAYER_LOCATION, NotchModel::createBodyLayer);
|
||||
EntityRendererRegistry.register(ItsAliveEntityTypes.BUTTERFLY, ButterflyRenderer::new);
|
||||
EntityRendererRegistry.register(ItsAliveEntityTypes.GHOST, GhostRenderer::new);
|
||||
EntityRendererRegistry.register(ItsAliveEntityTypes.BRACKEN, BrackenRenderer::new);
|
||||
@@ -70,6 +73,8 @@ public final class ItsAliveClient implements ClientModInitializer {
|
||||
EntityRendererRegistry.register(ItsAliveEntityTypes.MOOBLOOM, MoobloomRenderer::new);
|
||||
EntityRendererRegistry.register(ItsAliveEntityTypes.GEM_CHICKEN, GemChickenRenderer::new);
|
||||
EntityRendererRegistry.register(ItsAliveEntityTypes.ACID_GLOB, ThrownItemRenderer::new);
|
||||
EntityRendererRegistry.register(ItsAliveEntityTypes.NOTCH, NotchRenderer::new);
|
||||
EntityRendererRegistry.register(ItsAliveEntityTypes.LEGACY_NOTCH, NotchRenderer::new);
|
||||
BlockEntityRendererRegistry.register(
|
||||
ItsAliveFoodProcessing.FRYING_PAN_BLOCK_ENTITY,
|
||||
FryingPanBlockEntityRenderer::new
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.client.model;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.ItsAliveMod;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.renderer.state.NotchRenderState;
|
||||
import net.minecraft.client.model.HumanoidModel;
|
||||
import net.minecraft.client.model.geom.ModelLayerLocation;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.client.model.geom.builders.CubeDeformation;
|
||||
import net.minecraft.client.model.geom.builders.LayerDefinition;
|
||||
|
||||
public final class NotchModel extends HumanoidModel<NotchRenderState> {
|
||||
public static final ModelLayerLocation LAYER_LOCATION = new ModelLayerLocation(ItsAliveMod.id("notch"), "main");
|
||||
|
||||
public NotchModel(ModelPart root) {
|
||||
super(root);
|
||||
}
|
||||
|
||||
public static LayerDefinition createBodyLayer() {
|
||||
return LayerDefinition.create(HumanoidModel.createMesh(CubeDeformation.NONE, 0.0F), 64, 64);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.client.renderer;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import fr.koka99cab.sanctuary26.itsalive.ItsAliveMod;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.model.NotchModel;
|
||||
import fr.koka99cab.sanctuary26.itsalive.client.renderer.state.NotchRenderState;
|
||||
import fr.koka99cab.sanctuary26.itsalive.entity.notch.NotchEntity;
|
||||
import net.minecraft.client.renderer.entity.EntityRendererProvider;
|
||||
import net.minecraft.client.renderer.entity.HumanoidMobRenderer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
public final class NotchRenderer extends HumanoidMobRenderer<NotchEntity, NotchRenderState, NotchModel> {
|
||||
private static final Identifier TEXTURE = ItsAliveMod.id("textures/entity/notch.png");
|
||||
|
||||
public NotchRenderer(EntityRendererProvider.Context context) {
|
||||
super(context, new NotchModel(context.bakeLayer(NotchModel.LAYER_LOCATION)), 0.5F);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Identifier getTextureLocation(NotchRenderState state) {
|
||||
return TEXTURE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NotchRenderState createRenderState() {
|
||||
return new NotchRenderState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(NotchEntity notch, NotchRenderState state, float partialTick) {
|
||||
super.extractRenderState(notch, state, partialTick);
|
||||
state.titan = notch.isTitan();
|
||||
state.titanTransformationProgress = notch.titanTransformationProgress(partialTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scale(NotchRenderState state, PoseStack poseStack) {
|
||||
super.scale(state, poseStack);
|
||||
float scale = state.titan
|
||||
? Mth.lerp(state.titanTransformationProgress, 1.0F, NotchEntity.TITAN_RENDER_SCALE)
|
||||
: 1.0F;
|
||||
poseStack.scale(scale, scale, scale);
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.client.renderer.state;
|
||||
|
||||
import net.minecraft.client.renderer.entity.state.HumanoidRenderState;
|
||||
|
||||
public final class NotchRenderState extends HumanoidRenderState {
|
||||
public boolean titan;
|
||||
public float titanTransformationProgress;
|
||||
}
|
||||
+495
@@ -0,0 +1,495 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.entity.notch;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.ItsAliveMod;
|
||||
import fr.koka99cab.sanctuary26.itsalive.api.notch.NotchCombatApi;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.world.SanctuaryAlphaWorld;
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.syncher.EntityDataAccessor;
|
||||
import net.minecraft.network.syncher.EntityDataSerializers;
|
||||
import net.minecraft.network.syncher.SynchedEntityData;
|
||||
import net.minecraft.server.level.ServerBossEvent;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.BossEvent;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntityDimensions;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.entity.PathfinderMob;
|
||||
import net.minecraft.world.entity.Pose;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.entity.ai.goal.FloatGoal;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal;
|
||||
import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal;
|
||||
import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal;
|
||||
import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.entity.projectile.Projectile;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** The unique human resident of Alpha and his server-authoritative Titan phase. */
|
||||
public final class NotchEntity extends PathfinderMob {
|
||||
private static final EntityDataAccessor<Byte> PHASE = SynchedEntityData.defineId(
|
||||
NotchEntity.class, EntityDataSerializers.BYTE);
|
||||
private static final EntityDataAccessor<Integer> PHASE_STARTED_TICK = SynchedEntityData.defineId(
|
||||
NotchEntity.class, EntityDataSerializers.INT);
|
||||
public static final float TITAN_RENDER_SCALE = 8.0F;
|
||||
private static final float TITAN_HITBOX_SCALE = 5.0F;
|
||||
private static final double HUMAN_HOME_RADIUS = 9.0D;
|
||||
private static final double TITAN_HOME_RADIUS = 54.0D;
|
||||
private static final double PARTICIPANT_RADIUS = 64.0D;
|
||||
private static final String DATA_VERSION_KEY = "ItsAliveDataVersion";
|
||||
private static final String PARTICIPANTS_KEY = "NotchParticipants";
|
||||
private static final String LEGACY_REWARD_KEY = "NotchRewardGranted";
|
||||
private final ServerBossEvent bossEvent = new ServerBossEvent(UUID.randomUUID(),
|
||||
Component.translatable("boss.itsalive.notch_titan"), BossEvent.BossBarColor.RED,
|
||||
BossEvent.BossBarOverlay.NOTCHED_20);
|
||||
private final LinkedHashSet<UUID> participants = new LinkedHashSet<>();
|
||||
private BlockPos home;
|
||||
private int ticksWithoutParticipant;
|
||||
private float titanMaximumHealth = NotchFightRules.MIN_TITAN_HEALTH;
|
||||
private boolean legacyRewardGranted;
|
||||
|
||||
public NotchEntity(net.minecraft.world.entity.EntityType<? extends NotchEntity> type, Level level) {
|
||||
super(type, level);
|
||||
this.xpReward = 0;
|
||||
this.setCustomName(Component.translatable("entity.itsalive.notch"));
|
||||
this.setCustomNameVisible(true);
|
||||
this.setPersistenceRequired();
|
||||
this.bossEvent.setDarkenScreen(true);
|
||||
this.bossEvent.setCreateWorldFog(true);
|
||||
this.bossEvent.setVisible(false);
|
||||
this.applyPhaseAttributes(true);
|
||||
}
|
||||
|
||||
public static AttributeSupplier.Builder createAttributes() {
|
||||
return Mob.createMobAttributes()
|
||||
.add(Attributes.MAX_HEALTH, NotchFightRules.HUMAN_HEALTH)
|
||||
.add(Attributes.ATTACK_DAMAGE, 1.0D)
|
||||
.add(Attributes.FOLLOW_RANGE, 96.0D)
|
||||
.add(Attributes.MOVEMENT_SPEED, 0.16D)
|
||||
.add(Attributes.KNOCKBACK_RESISTANCE, 1.0D)
|
||||
.add(Attributes.ARMOR, 0.0D)
|
||||
.add(Attributes.ARMOR_TOUGHNESS, 0.0D)
|
||||
.add(Attributes.STEP_HEIGHT, 2.0D);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void defineSynchedData(SynchedEntityData.Builder builder) {
|
||||
super.defineSynchedData(builder);
|
||||
builder.define(PHASE, (byte)NotchFightRules.Phase.HUMAN.id());
|
||||
builder.define(PHASE_STARTED_TICK, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void registerGoals() {
|
||||
this.goalSelector.addGoal(0, new FloatGoal(this));
|
||||
this.goalSelector.addGoal(1, new TitanCombatGoal());
|
||||
this.goalSelector.addGoal(5, new BoundedStrollGoal());
|
||||
this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 24.0F));
|
||||
this.goalSelector.addGoal(7, new RandomLookAroundGoal(this));
|
||||
this.targetSelector.addGoal(1, new NearestAttackableTargetGoal<>(this, Player.class, 10,
|
||||
true, false, (target, level) -> this.isTitan() && target instanceof Player player
|
||||
&& this.canParticipate(player)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void customServerAiStep(ServerLevel level) {
|
||||
super.customServerAiStep(level);
|
||||
this.ensureHome(level);
|
||||
this.bossEvent.setVisible(this.isTitan());
|
||||
if (this.isTitan()) {
|
||||
this.collectParticipants(level);
|
||||
this.bossEvent.setProgress(Mth.clamp(this.getHealth() / this.getMaxHealth(), 0.0F, 1.0F));
|
||||
if (this.hasActiveParticipant(level)) {
|
||||
this.ticksWithoutParticipant = 0;
|
||||
} else if (NotchFightRules.shouldReset(++this.ticksWithoutParticipant)) {
|
||||
this.resetInterruptedFight(level);
|
||||
return;
|
||||
}
|
||||
this.returnInsideBoundary(TITAN_HOME_RADIUS);
|
||||
} else {
|
||||
this.bossEvent.setProgress(1.0F);
|
||||
this.ticksWithoutParticipant = 0;
|
||||
if (this.getTarget() != null) this.setTarget(null);
|
||||
this.returnInsideBoundary(HUMAN_HOME_RADIUS + 4.0D);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hurtServer(ServerLevel level, DamageSource source, float damage) {
|
||||
if (this.isDefeated()) return false;
|
||||
Entity attacker = source.getEntity();
|
||||
if (this.isHuman()) {
|
||||
if (attacker instanceof ServerPlayer player) {
|
||||
this.beginFight(level, player);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (attacker == this) return false;
|
||||
if (source.getDirectEntity() instanceof Projectile projectile && projectile.getOwner() == this) return false;
|
||||
if (attacker instanceof ServerPlayer player) this.addParticipant(player);
|
||||
boolean hurt = super.hurtServer(level, source, Math.max(0.0F, damage));
|
||||
if (hurt && attacker instanceof LivingEntity living && this.canAttack(living)) this.setTarget(living);
|
||||
return hurt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void die(DamageSource source) {
|
||||
if (this.isTitan() && this.level() instanceof ServerLevel level) {
|
||||
this.completeFight(level);
|
||||
return;
|
||||
}
|
||||
super.die(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canAttack(LivingEntity target) {
|
||||
return this.isTitan() && super.canAttack(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkFallDamage(double distance, boolean landed, BlockState state, BlockPos pos) {
|
||||
}
|
||||
|
||||
public void initializeAtHome(BlockPos home, boolean defeated) {
|
||||
this.home = home.immutable();
|
||||
this.snapTo(home.getX() + 0.5D, home.getY(), home.getZ() + 0.5D, 180.0F, 0.0F);
|
||||
this.setYHeadRot(180.0F);
|
||||
this.setPhase(defeated ? NotchFightRules.Phase.DEFEATED : NotchFightRules.Phase.HUMAN, true);
|
||||
}
|
||||
|
||||
public void restoreAtHome(BlockPos home, boolean defeated) {
|
||||
this.participants.clear();
|
||||
this.ticksWithoutParticipant = 0;
|
||||
this.setTarget(null);
|
||||
this.getNavigation().stop();
|
||||
this.setDeltaMovement(Vec3.ZERO);
|
||||
this.initializeAtHome(home, defeated);
|
||||
}
|
||||
|
||||
public NotchFightRules.Phase phase() {
|
||||
return NotchFightRules.Phase.byId(this.entityData.get(PHASE));
|
||||
}
|
||||
|
||||
public boolean isHuman() {
|
||||
return phase() == NotchFightRules.Phase.HUMAN;
|
||||
}
|
||||
|
||||
public boolean isTitan() {
|
||||
return phase() == NotchFightRules.Phase.TITAN;
|
||||
}
|
||||
|
||||
public boolean isDefeated() {
|
||||
return phase() == NotchFightRules.Phase.DEFEATED;
|
||||
}
|
||||
|
||||
public float titanTransformationProgress(float partialTick) {
|
||||
if (!isTitan()) return 0.0F;
|
||||
return Mth.clamp((this.tickCount + partialTick - this.entityData.get(PHASE_STARTED_TICK)) / 30.0F,
|
||||
0.0F, 1.0F);
|
||||
}
|
||||
|
||||
public boolean legacyRewardGranted() {
|
||||
return legacyRewardGranted;
|
||||
}
|
||||
|
||||
public Set<UUID> participantIds() {
|
||||
return Set.copyOf(participants);
|
||||
}
|
||||
|
||||
private void beginFight(ServerLevel level, ServerPlayer attacker) {
|
||||
this.participants.clear();
|
||||
this.addParticipant(attacker);
|
||||
level.players().stream()
|
||||
.filter(this::canParticipate)
|
||||
.sorted(java.util.Comparator.comparingDouble(this::distanceToSqr))
|
||||
.forEach(this::addParticipant);
|
||||
this.titanMaximumHealth = NotchFightRules.titanHealth(participants.size());
|
||||
this.setPhase(NotchFightRules.Phase.TITAN, true);
|
||||
this.setTarget(attacker);
|
||||
this.bossEvent.setVisible(true);
|
||||
this.spawnLightningRing(level, 4);
|
||||
level.explode(this, this.getX(), this.getY() + 1.0D, this.getZ(), 4.0F,
|
||||
Level.ExplosionInteraction.BLOCK);
|
||||
this.playSound(SoundEvents.WITHER_SPAWN, 1.4F, 0.72F);
|
||||
}
|
||||
|
||||
private void completeFight(ServerLevel level) {
|
||||
this.collectParticipants(level);
|
||||
this.setPhase(NotchFightRules.Phase.DEFEATED, true);
|
||||
this.setTarget(null);
|
||||
this.getNavigation().stop();
|
||||
this.setDeltaMovement(Vec3.ZERO);
|
||||
this.deathTime = 0;
|
||||
this.bossEvent.setVisible(false);
|
||||
this.spawnLightningRing(level, 3);
|
||||
level.sendParticles(ParticleTypes.EXPLOSION_EMITTER, this.getX(), this.getY() + 1.0D,
|
||||
this.getZ(), 1, 0.0D, 0.0D, 0.0D, 0.0D);
|
||||
this.playSound(SoundEvents.WITHER_DEATH, 1.25F, 1.15F);
|
||||
SanctuaryNotchMilestone.recordVictory(level, participants);
|
||||
}
|
||||
|
||||
private void resetInterruptedFight(ServerLevel level) {
|
||||
this.setPhase(NotchFightRules.Phase.HUMAN, true);
|
||||
this.participants.clear();
|
||||
this.setTarget(null);
|
||||
this.getNavigation().stop();
|
||||
this.setDeltaMovement(Vec3.ZERO);
|
||||
this.bossEvent.setVisible(false);
|
||||
if (home != null) this.snapTo(home.getX() + 0.5D, home.getY(), home.getZ() + 0.5D, 180.0F, 0.0F);
|
||||
level.sendParticles(ParticleTypes.SMOKE, this.getX(), this.getY() + 1.0D, this.getZ(),
|
||||
18, 0.8D, 1.0D, 0.8D, 0.04D);
|
||||
}
|
||||
|
||||
private void setPhase(NotchFightRules.Phase phase, boolean heal) {
|
||||
this.entityData.set(PHASE, (byte)phase.id());
|
||||
this.entityData.set(PHASE_STARTED_TICK, this.tickCount);
|
||||
this.applyPhaseAttributes(heal);
|
||||
this.refreshDimensions();
|
||||
}
|
||||
|
||||
private void applyPhaseAttributes(boolean heal) {
|
||||
boolean titan = isTitan();
|
||||
setBaseValue(Attributes.MAX_HEALTH, titan ? titanMaximumHealth : NotchFightRules.HUMAN_HEALTH);
|
||||
setBaseValue(Attributes.ATTACK_DAMAGE, titan ? 18.0D : 1.0D);
|
||||
setBaseValue(Attributes.MOVEMENT_SPEED, titan ? 0.235D : 0.16D);
|
||||
setBaseValue(Attributes.ARMOR, titan ? 10.0D : 0.0D);
|
||||
setBaseValue(Attributes.ARMOR_TOUGHNESS, titan ? 8.0D : 0.0D);
|
||||
if (heal) this.setHealth(this.getMaxHealth());
|
||||
}
|
||||
|
||||
private void setBaseValue(net.minecraft.core.Holder<net.minecraft.world.entity.ai.attributes.Attribute> attribute,
|
||||
double value) {
|
||||
AttributeInstance instance = this.getAttribute(attribute);
|
||||
if (instance != null) instance.setBaseValue(value);
|
||||
}
|
||||
|
||||
private void ensureHome(ServerLevel level) {
|
||||
if (home == null) home = SanctuaryAlphaWorld.residence(level.getSeed());
|
||||
}
|
||||
|
||||
private void returnInsideBoundary(double radius) {
|
||||
if (home == null || this.distanceToSqr(Vec3.atCenterOf(home)) <= radius * radius) return;
|
||||
this.getNavigation().stop();
|
||||
this.snapTo(home.getX() + 0.5D, home.getY(), home.getZ() + 0.5D, this.getYRot(), this.getXRot());
|
||||
}
|
||||
|
||||
private boolean canParticipate(Player player) {
|
||||
return player.isAlive() && !player.isSpectator() && player.level() == this.level()
|
||||
&& this.distanceToSqr(player) <= PARTICIPANT_RADIUS * PARTICIPANT_RADIUS;
|
||||
}
|
||||
|
||||
private void addParticipant(Player player) {
|
||||
if (participants.size() < NotchFightRules.MAX_PARTICIPANTS && canParticipate(player)) {
|
||||
participants.add(player.getUUID());
|
||||
}
|
||||
}
|
||||
|
||||
private void collectParticipants(ServerLevel level) {
|
||||
level.players().stream().filter(this::canParticipate)
|
||||
.sorted(java.util.Comparator.comparingDouble(this::distanceToSqr))
|
||||
.forEach(this::addParticipant);
|
||||
}
|
||||
|
||||
private boolean hasActiveParticipant(ServerLevel level) {
|
||||
return participants.stream().map(level.getServer().getPlayerList()::getPlayer)
|
||||
.anyMatch(player -> player != null && canParticipate(player));
|
||||
}
|
||||
|
||||
private void spawnLightningRing(ServerLevel level, int strikes) {
|
||||
for (int index = 0; index < strikes; index++) {
|
||||
double angle = Math.PI * 2.0D * index / Math.max(1, strikes);
|
||||
net.minecraft.world.entity.LightningBolt lightning = net.minecraft.world.entity.EntityTypes.LIGHTNING_BOLT
|
||||
.create(level, net.minecraft.world.entity.EntitySpawnReason.TRIGGERED);
|
||||
if (lightning == null) continue;
|
||||
lightning.setVisualOnly(true);
|
||||
lightning.setPos(this.getX() + Math.cos(angle) * 2.4D, this.getY(),
|
||||
this.getZ() + Math.sin(angle) * 2.4D);
|
||||
level.addFreshEntity(lightning);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EntityDimensions getDefaultDimensions(Pose pose) {
|
||||
EntityDimensions dimensions = super.getDefaultDimensions(pose);
|
||||
return isTitan() ? dimensions.scale(TITAN_HITBOX_SCALE) : dimensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSyncedDataUpdated(EntityDataAccessor<?> key) {
|
||||
super.onSyncedDataUpdated(key);
|
||||
if (PHASE.equals(key)) this.refreshDimensions();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAdditionalSaveData(ValueOutput output) {
|
||||
super.addAdditionalSaveData(output);
|
||||
output.putInt(DATA_VERSION_KEY, ItsAliveMod.CURRENT_DATA_VERSION);
|
||||
output.putByte("NotchPhase", this.entityData.get(PHASE));
|
||||
output.putInt("NotchPhaseStartedTick", this.entityData.get(PHASE_STARTED_TICK));
|
||||
output.putFloat("NotchStoredHealth", this.getHealth());
|
||||
output.putFloat("NotchTitanMaximumHealth", this.titanMaximumHealth);
|
||||
output.putString(PARTICIPANTS_KEY, participants.stream().map(UUID::toString).collect(Collectors.joining(",")));
|
||||
output.putBoolean(LEGACY_REWARD_KEY, legacyRewardGranted);
|
||||
if (home != null) {
|
||||
output.putInt("NotchHomeX", home.getX());
|
||||
output.putInt("NotchHomeY", home.getY());
|
||||
output.putInt("NotchHomeZ", home.getZ());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readAdditionalSaveData(ValueInput input) {
|
||||
super.readAdditionalSaveData(input);
|
||||
NotchFightRules.Phase phase = NotchFightRules.Phase.byId(input.getByteOr("NotchPhase", (byte)0));
|
||||
this.entityData.set(PHASE, (byte)phase.id());
|
||||
this.entityData.set(PHASE_STARTED_TICK, input.getIntOr("NotchPhaseStartedTick", 0));
|
||||
this.titanMaximumHealth = Mth.clamp(input.getFloatOr("NotchTitanMaximumHealth",
|
||||
NotchFightRules.MIN_TITAN_HEALTH), NotchFightRules.MIN_TITAN_HEALTH,
|
||||
NotchFightRules.MAX_TITAN_HEALTH);
|
||||
this.participants.clear();
|
||||
input.getString(PARTICIPANTS_KEY).ifPresent(value -> {
|
||||
for (String token : value.split(",")) {
|
||||
if (participants.size() >= NotchFightRules.MAX_PARTICIPANTS) break;
|
||||
try { participants.add(UUID.fromString(token)); } catch (IllegalArgumentException ignored) { }
|
||||
}
|
||||
});
|
||||
this.legacyRewardGranted = input.getBooleanOr(LEGACY_REWARD_KEY, false);
|
||||
if (input.getInt("NotchHomeX").isPresent() && input.getInt("NotchHomeY").isPresent()
|
||||
&& input.getInt("NotchHomeZ").isPresent()) {
|
||||
this.home = new BlockPos(input.getIntOr("NotchHomeX", 0), input.getIntOr("NotchHomeY", 0),
|
||||
input.getIntOr("NotchHomeZ", 0));
|
||||
}
|
||||
this.applyPhaseAttributes(false);
|
||||
this.setHealth(Mth.clamp(input.getFloatOr("NotchStoredHealth", this.getMaxHealth()),
|
||||
1.0F, this.getMaxHealth()));
|
||||
this.refreshDimensions();
|
||||
this.bossEvent.setVisible(isTitan());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startSeenByPlayer(ServerPlayer player) {
|
||||
super.startSeenByPlayer(player);
|
||||
this.bossEvent.addPlayer(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopSeenByPlayer(ServerPlayer player) {
|
||||
super.stopSeenByPlayer(player);
|
||||
this.bossEvent.removePlayer(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeWhenFarAway(double distance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxSpawnClusterSize() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private final class BoundedStrollGoal extends WaterAvoidingRandomStrollGoal {
|
||||
private BoundedStrollGoal() {
|
||||
super(NotchEntity.this, 0.72D, 0.008F);
|
||||
}
|
||||
|
||||
@Override public boolean canUse() { return !NotchEntity.this.isTitan() && super.canUse(); }
|
||||
@Override public boolean canContinueToUse() { return !NotchEntity.this.isTitan() && super.canContinueToUse(); }
|
||||
}
|
||||
|
||||
private final class TitanCombatGoal extends Goal {
|
||||
private static final double STRAFE_DISTANCE_SQUARED = 18.0D * 18.0D;
|
||||
private int rangedCooldown;
|
||||
private int meleeCooldown;
|
||||
private int attackIndex;
|
||||
private int windup;
|
||||
private NotchCombatApi.Attack pendingAttack;
|
||||
private boolean clockwise;
|
||||
private int strafeTicks;
|
||||
|
||||
private TitanCombatGoal() {
|
||||
this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override public boolean canUse() {
|
||||
LivingEntity target = NotchEntity.this.getTarget();
|
||||
return NotchEntity.this.isTitan() && target != null && target.isAlive();
|
||||
}
|
||||
|
||||
@Override public boolean canContinueToUse() { return canUse(); }
|
||||
|
||||
@Override public void start() {
|
||||
rangedCooldown = 45;
|
||||
meleeCooldown = 0;
|
||||
attackIndex = 0;
|
||||
windup = 0;
|
||||
pendingAttack = null;
|
||||
clockwise = NotchEntity.this.getRandom().nextBoolean();
|
||||
}
|
||||
|
||||
@Override public void stop() {
|
||||
NotchEntity.this.getNavigation().stop();
|
||||
pendingAttack = null;
|
||||
}
|
||||
|
||||
@Override public boolean requiresUpdateEveryTick() { return true; }
|
||||
|
||||
@Override public void tick() {
|
||||
LivingEntity target = NotchEntity.this.getTarget();
|
||||
if (target == null || !(NotchEntity.this.level() instanceof ServerLevel level)) return;
|
||||
double distanceSquared = NotchEntity.this.distanceToSqr(target);
|
||||
boolean canSee = NotchEntity.this.getSensing().hasLineOfSight(target);
|
||||
NotchEntity.this.getLookControl().setLookAt(target, 30.0F, 30.0F);
|
||||
if (!canSee || distanceSquared > STRAFE_DISTANCE_SQUARED) {
|
||||
NotchEntity.this.getNavigation().moveTo(target, 1.12D);
|
||||
} else {
|
||||
NotchEntity.this.getNavigation().stop();
|
||||
if (++strafeTicks >= 45) { strafeTicks = 0; clockwise = !clockwise; }
|
||||
NotchEntity.this.getMoveControl().strafe(distanceSquared < 100.0D ? -0.45F : 0.28F,
|
||||
clockwise ? 0.7F : -0.7F);
|
||||
}
|
||||
if (meleeCooldown > 0) meleeCooldown--;
|
||||
if (distanceSquared <= 64.0D && meleeCooldown == 0) {
|
||||
NotchCombatApi.execute(level, NotchEntity.this, target, NotchCombatApi.Attack.MELEE);
|
||||
meleeCooldown = 24;
|
||||
}
|
||||
if (pendingAttack != null) {
|
||||
if (--windup <= 0) {
|
||||
NotchCombatApi.execute(level, NotchEntity.this, target, pendingAttack);
|
||||
pendingAttack = null;
|
||||
rangedCooldown = 65 + NotchEntity.this.getRandom().nextInt(32);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (rangedCooldown > 0) rangedCooldown--;
|
||||
if (canSee && rangedCooldown == 0) {
|
||||
pendingAttack = attackIndex++ % 5 == 4
|
||||
? NotchCombatApi.Attack.LIGHTNING : NotchCombatApi.Attack.FIREBALL;
|
||||
windup = pendingAttack == NotchCombatApi.Attack.LIGHTNING ? 24 : 14;
|
||||
NotchCombatApi.telegraph(level, NotchEntity.this, target, pendingAttack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.entity.notch;
|
||||
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
/** Pure bounded rules shared by the live Notch entity and deterministic tests. */
|
||||
public final class NotchFightRules {
|
||||
public static final int MAX_PARTICIPANTS = 8;
|
||||
public static final int RESET_AFTER_EMPTY_TICKS = 20 * 15;
|
||||
public static final float HUMAN_HEALTH = 20.0F;
|
||||
public static final float MIN_TITAN_HEALTH = 360.0F;
|
||||
public static final float MAX_TITAN_HEALTH = 600.0F;
|
||||
public static final float RETURNED_FIREBALL_DAMAGE = 40.0F;
|
||||
|
||||
private NotchFightRules() {
|
||||
}
|
||||
|
||||
public static float titanHealth(int participantCount) {
|
||||
int bounded = Mth.clamp(participantCount, 1, MAX_PARTICIPANTS);
|
||||
return Math.min(MAX_TITAN_HEALTH, MIN_TITAN_HEALTH + (bounded - 1) * 40.0F);
|
||||
}
|
||||
|
||||
public static boolean shouldReset(int ticksWithoutParticipant) {
|
||||
return ticksWithoutParticipant >= RESET_AFTER_EMPTY_TICKS;
|
||||
}
|
||||
|
||||
public enum Phase {
|
||||
HUMAN(0),
|
||||
TITAN(1),
|
||||
DEFEATED(2);
|
||||
|
||||
private final int id;
|
||||
|
||||
Phase(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public static Phase byId(int id) {
|
||||
return switch (id) {
|
||||
case 1 -> TITAN;
|
||||
case 2 -> DEFEATED;
|
||||
default -> HUMAN;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.entity.notch;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka99cab.sanctuary26.itsalive.ItsAliveMod;
|
||||
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveEntityTypes;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.world.SanctuaryAlphaWorld;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.world.SanctuaryDimensions;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.datafix.DataFixTypes;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** Maintains exactly one persistent Notch resident in the finite Alpha world. */
|
||||
public final class NotchResidentService {
|
||||
private static final int CHECK_INTERVAL = 40;
|
||||
private static boolean initialized;
|
||||
|
||||
private NotchResidentService() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
ServerTickEvents.END_LEVEL_TICK.register(NotchResidentService::tick);
|
||||
ItsAliveMod.LOGGER.debug("[{}] Registered the unique Alpha resident service.",
|
||||
ItsAliveMod.DISPLAY_NAME);
|
||||
}
|
||||
|
||||
public static Inspection inspect(MinecraftServer server) {
|
||||
ServerLevel level = server.getLevel(SanctuaryDimensions.ALPHA);
|
||||
if (level == null) return Inspection.unavailable();
|
||||
SpawnState state = level.getDataStorage().computeIfAbsent(SpawnState.TYPE);
|
||||
List<NotchEntity> residents = loadedResidents(level);
|
||||
NotchEntity canonical = selectCanonical(residents, state.canonicalUuid()).orElse(null);
|
||||
return new Inspection(true, residents.size(), canonical == null ? null : canonical.getUUID(),
|
||||
canonical == null ? null : canonical.phase(), canonical == null ? null : canonical.blockPosition(),
|
||||
SanctuaryNotchMilestone.isDefeated(server));
|
||||
}
|
||||
|
||||
/** Force-loads the house chunk, removes duplicates and restores the canonical resident. */
|
||||
public static Inspection restore(MinecraftServer server) {
|
||||
ServerLevel level = server.getLevel(SanctuaryDimensions.ALPHA);
|
||||
if (level == null) return Inspection.unavailable();
|
||||
BlockPos home = SanctuaryAlphaWorld.residence(level.getSeed());
|
||||
level.getChunkAt(home);
|
||||
reconcile(level, true);
|
||||
return inspect(server);
|
||||
}
|
||||
|
||||
private static void tick(ServerLevel level) {
|
||||
if (!level.dimension().equals(SanctuaryDimensions.ALPHA)
|
||||
|| level.players().isEmpty()
|
||||
|| level.getGameTime() % CHECK_INTERVAL != 0) return;
|
||||
reconcile(level, false);
|
||||
}
|
||||
|
||||
private static void reconcile(ServerLevel level, boolean forceHome) {
|
||||
BlockPos home = SanctuaryAlphaWorld.residence(level.getSeed());
|
||||
if (!forceHome && !level.isLoaded(home)) return;
|
||||
SpawnState state = level.getDataStorage().computeIfAbsent(SpawnState.TYPE);
|
||||
List<NotchEntity> residents = loadedResidents(level);
|
||||
NotchEntity canonical = selectCanonical(residents, state.canonicalUuid()).orElse(null);
|
||||
if (canonical == null) {
|
||||
canonical = ItsAliveEntityTypes.NOTCH.create(level, EntitySpawnReason.EVENT);
|
||||
if (canonical == null) return;
|
||||
canonical.initializeAtHome(home, SanctuaryNotchMilestone.isDefeated(level.getServer()));
|
||||
if (!level.addFreshEntity(canonical)) return;
|
||||
residents = List.of(canonical);
|
||||
ItsAliveMod.LOGGER.info("[{}] Placed Notch in the Alpha residence at {}.",
|
||||
ItsAliveMod.DISPLAY_NAME, home);
|
||||
}
|
||||
for (NotchEntity resident : residents) {
|
||||
if (resident != canonical) resident.discard();
|
||||
}
|
||||
state.setCanonicalUuid(canonical.getUUID());
|
||||
if (canonical.isDefeated() && !SanctuaryNotchMilestone.isDefeated(level.getServer())) {
|
||||
SanctuaryNotchMilestone.migrateLegacyVictory(level, canonical.legacyRewardGranted());
|
||||
}
|
||||
boolean defeated = SanctuaryNotchMilestone.isDefeated(level.getServer());
|
||||
if (forceHome || defeated != canonical.isDefeated()) canonical.restoreAtHome(home, defeated);
|
||||
}
|
||||
|
||||
private static List<NotchEntity> loadedResidents(ServerLevel level) {
|
||||
List<NotchEntity> residents = new ArrayList<>();
|
||||
for (Entity entity : level.getAllEntities()) {
|
||||
if (entity instanceof NotchEntity notch && !notch.isRemoved()) residents.add(notch);
|
||||
}
|
||||
return residents;
|
||||
}
|
||||
|
||||
private static Optional<NotchEntity> selectCanonical(List<NotchEntity> residents, UUID storedUuid) {
|
||||
return residents.stream().min(Comparator
|
||||
.comparing((NotchEntity notch) -> !notch.getUUID().equals(storedUuid))
|
||||
.thenComparing(notch -> notch.getType() != ItsAliveEntityTypes.NOTCH)
|
||||
.thenComparing(notch -> notch.getUUID().toString()));
|
||||
}
|
||||
|
||||
public record Inspection(boolean alphaAvailable, int loadedResidents, UUID canonicalUuid,
|
||||
NotchFightRules.Phase phase, BlockPos position, boolean worldDefeated) {
|
||||
private static Inspection unavailable() {
|
||||
return new Inspection(false, 0, null, null, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SpawnState extends SavedData {
|
||||
private static final Codec<SpawnState> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.INT.optionalFieldOf("data_version", ItsAliveMod.CURRENT_DATA_VERSION)
|
||||
.forGetter(state -> state.dataVersion),
|
||||
Codec.STRING.optionalFieldOf("canonical_uuid", "").forGetter(state -> state.canonicalUuid)
|
||||
).apply(instance, SpawnState::new));
|
||||
private static final SavedDataType<SpawnState> TYPE = new SavedDataType<>(
|
||||
ItsAliveMod.id("alpha_notch_resident"), SpawnState::new, CODEC, DataFixTypes.LEVEL);
|
||||
private int dataVersion;
|
||||
private String canonicalUuid;
|
||||
|
||||
private SpawnState() {
|
||||
this(ItsAliveMod.CURRENT_DATA_VERSION, "");
|
||||
}
|
||||
|
||||
private SpawnState(int dataVersion, String canonicalUuid) {
|
||||
this.dataVersion = Math.max(1, dataVersion);
|
||||
this.canonicalUuid = canonicalUuid == null ? "" : canonicalUuid;
|
||||
if (this.dataVersion < ItsAliveMod.CURRENT_DATA_VERSION) {
|
||||
this.dataVersion = ItsAliveMod.CURRENT_DATA_VERSION;
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private UUID canonicalUuid() {
|
||||
try {
|
||||
return UUID.fromString(canonicalUuid);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return new UUID(0L, 0L);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCanonicalUuid(UUID uuid) {
|
||||
String value = uuid.toString();
|
||||
if (value.equals(canonicalUuid)) return;
|
||||
canonicalUuid = value;
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -9,6 +9,7 @@ import fr.koka99cab.sanctuary26.itsalive.entity.FlyingWhaleEntity;
|
||||
import fr.koka99cab.sanctuary26.itsalive.entity.GemChickenEntity;
|
||||
import fr.koka99cab.sanctuary26.itsalive.entity.InfectedEntity;
|
||||
import fr.koka99cab.sanctuary26.itsalive.entity.MoobloomEntity;
|
||||
import fr.koka99cab.sanctuary26.itsalive.entity.notch.NotchEntity;
|
||||
import java.util.function.Predicate;
|
||||
import net.fabricmc.fabric.api.biome.v1.BiomeModifications;
|
||||
import net.fabricmc.fabric.api.biome.v1.BiomeSelectionContext;
|
||||
@@ -44,6 +45,8 @@ public final class ItsAliveEntityTypes {
|
||||
public static final Identifier ACID_GLOB_ID = ItsAliveMod.id("acid_glob");
|
||||
public static final Identifier MOOBLOOM_ID = ItsAliveMod.id("moobloom");
|
||||
public static final Identifier GEM_CHICKEN_ID = ItsAliveMod.id("gem_chicken");
|
||||
public static final Identifier NOTCH_ID = ItsAliveMod.id("notch");
|
||||
public static final Identifier LEGACY_NOTCH_ID = Identifier.fromNamespaceAndPath("portals", "notch");
|
||||
|
||||
public static final EntityType<ButterflyEntity> BUTTERFLY = Registry.register(
|
||||
BuiltInRegistries.ENTITY_TYPE,
|
||||
@@ -167,6 +170,10 @@ public final class ItsAliveEntityTypes {
|
||||
.build(ResourceKey.create(Registries.ENTITY_TYPE, GEM_CHICKEN_ID))
|
||||
);
|
||||
|
||||
public static final EntityType<NotchEntity> NOTCH = registerNotch(NOTCH_ID);
|
||||
/** Hidden alias used only to deserialize Notch entities written by the historical Portals prototype. */
|
||||
public static final EntityType<NotchEntity> LEGACY_NOTCH = registerNotch(LEGACY_NOTCH_ID);
|
||||
|
||||
private ItsAliveEntityTypes() {
|
||||
}
|
||||
|
||||
@@ -192,7 +199,19 @@ public final class ItsAliveEntityTypes {
|
||||
entity.discard();
|
||||
}
|
||||
});
|
||||
ItsAliveMod.LOGGER.info("[{}] Registered papillon, 16-variant moobloom, three gem chickens, ghost, bracken, flying whale and five-role infected; vanilla phantoms disabled.",
|
||||
ItsAliveMod.LOGGER.info("[{}] Registered papillon, 16-variant moobloom, three gem chickens, ghost, bracken, flying whale, five-role infected and the unique Alpha resident; vanilla phantoms disabled.",
|
||||
ItsAliveMod.DISPLAY_NAME);
|
||||
}
|
||||
|
||||
private static EntityType<NotchEntity> registerNotch(Identifier id) {
|
||||
return Registry.register(BuiltInRegistries.ENTITY_TYPE, id,
|
||||
FabricEntityType.Builder.createMob(NotchEntity::new, MobCategory.CREATURE,
|
||||
mob -> mob.defaultAttributes(NotchEntity::createAttributes))
|
||||
.sized(0.6F, 1.8F)
|
||||
.clientTrackingRange(96)
|
||||
.updateInterval(2)
|
||||
.noSummon()
|
||||
.noLootTable()
|
||||
.build(ResourceKey.create(Registries.ENTITY_TYPE, id)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,5 +288,7 @@
|
||||
"hud.itsalive.cooking_cauldron.assembly": "Mixture incomplete: %s",
|
||||
"hud.itsalive.cooking_cauldron.cooking": "Cooking: %s",
|
||||
"hud.itsalive.cooking_cauldron.expired": "Mixture failed — collect the failed dish",
|
||||
"hud.itsalive.cooking_cauldron.ready": "Dish ready"
|
||||
"hud.itsalive.cooking_cauldron.ready": "Dish ready",
|
||||
"entity.itsalive.notch": "Notch",
|
||||
"boss.itsalive.notch_titan": "Notch Titan"
|
||||
}
|
||||
|
||||
@@ -288,5 +288,7 @@
|
||||
"hud.itsalive.cooking_cauldron.assembly": "Mélange incomplet : %s",
|
||||
"hud.itsalive.cooking_cauldron.cooking": "Cuisson : %s",
|
||||
"hud.itsalive.cooking_cauldron.expired": "Plat raté — récupère le résultat",
|
||||
"hud.itsalive.cooking_cauldron.ready": "Plat prêt"
|
||||
"hud.itsalive.cooking_cauldron.ready": "Plat prêt",
|
||||
"entity.itsalive.notch": "Notch",
|
||||
"boss.itsalive.notch_titan": "Notch Titan"
|
||||
}
|
||||
|
||||
@@ -288,5 +288,7 @@
|
||||
"hud.itsalive.cooking_cauldron.assembly": "Mixture incomplete: %s",
|
||||
"hud.itsalive.cooking_cauldron.cooking": "Cooking: %s",
|
||||
"hud.itsalive.cooking_cauldron.expired": "Mixture failed — collect the failed dish",
|
||||
"hud.itsalive.cooking_cauldron.ready": "Dish ready"
|
||||
"hud.itsalive.cooking_cauldron.ready": "Dish ready",
|
||||
"entity.itsalive.notch": "Нотч",
|
||||
"boss.itsalive.notch_titan": "Нотч-Титан"
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -38,13 +38,13 @@
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"anotherworld": "*",
|
||||
"ambiance": ">=0.0.0-alpha.14",
|
||||
"sanctuary": ">=0.0.0-alpha.117"
|
||||
"sanctuary": ">=0.0.0-alpha.126"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "itsalive",
|
||||
"data_version": 3,
|
||||
"data_version": 4,
|
||||
"network_protocol": 4,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
|
||||
+16
@@ -61,6 +61,22 @@ public final class CanapliaGameplaySmoke {
|
||||
Set.of(), Temperature.COLD, Humidity.BALANCED, 12, false, false, true, true, false)).isEmpty(),
|
||||
"lunar harvest requires a sky dimension");
|
||||
|
||||
for (CanapliaVariantKey variant : CanapliaVariantKey.values()) {
|
||||
CanapliaVariantKey placed = CanapliaVariantLifecycle.plantedFromSeed(variant);
|
||||
assertEquals(variant, placed,
|
||||
"historical seed placement variant " + variant.key());
|
||||
CanapliaVariantKey grown = CanapliaVariantLifecycle.afterGrowth(placed);
|
||||
assertEquals(variant, grown,
|
||||
"natural growth variant " + variant.key());
|
||||
CanapliaVariantKey persisted = CanapliaVariantLifecycle.fromPersistedKey(
|
||||
CanapliaVariantLifecycle.persistedKey(grown));
|
||||
assertEquals(variant, persisted, "persisted variant " + variant.key());
|
||||
assertEquals(variant, CanapliaVariantLifecycle.upperFromLower(persisted),
|
||||
"upper crop variant " + variant.key());
|
||||
assertEquals(variant, CanapliaVariantLifecycle.harvestedFromPlant(persisted),
|
||||
"harvest variant " + variant.key());
|
||||
}
|
||||
|
||||
CanapliaEffectRules.Plan minimum = CanapliaEffectRules.plan(0, 1, 0, 0);
|
||||
assertEquals(CanapliaEffectRules.MIN_PRIMARY_TICKS, minimum.primaryTicks(), "minimum primary duration");
|
||||
assertTrue(!minimum.badTrip(), "nonzero bad-trip roll");
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.entity.notch;
|
||||
|
||||
public final class NotchFightRulesSmoke {
|
||||
private NotchFightRulesSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
assertFloat(NotchFightRules.MIN_TITAN_HEALTH, NotchFightRules.titanHealth(-20),
|
||||
"health below one participant");
|
||||
assertFloat(400.0F, NotchFightRules.titanHealth(2), "two participant scaling");
|
||||
assertFloat(NotchFightRules.MAX_TITAN_HEALTH,
|
||||
NotchFightRules.titanHealth(NotchFightRules.MAX_PARTICIPANTS), "maximum scaling");
|
||||
assertFloat(NotchFightRules.MAX_TITAN_HEALTH, NotchFightRules.titanHealth(200),
|
||||
"participant bound");
|
||||
if (NotchFightRules.shouldReset(NotchFightRules.RESET_AFTER_EMPTY_TICKS - 1)) {
|
||||
throw new AssertionError("Fight reset one tick too early");
|
||||
}
|
||||
if (!NotchFightRules.shouldReset(NotchFightRules.RESET_AFTER_EMPTY_TICKS)) {
|
||||
throw new AssertionError("Fight did not reset at the documented threshold");
|
||||
}
|
||||
if (NotchFightRules.RETURNED_FIREBALL_DAMAGE != 40.0F
|
||||
|| NotchFightRules.Phase.byId(-1) != NotchFightRules.Phase.HUMAN
|
||||
|| NotchFightRules.Phase.byId(1) != NotchFightRules.Phase.TITAN
|
||||
|| NotchFightRules.Phase.byId(2) != NotchFightRules.Phase.DEFEATED) {
|
||||
throw new AssertionError("Notch phase or reflected-fireball contract changed");
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertFloat(float expected, float actual, String label) {
|
||||
if (Float.compare(expected, actual) != 0) {
|
||||
throw new AssertionError(label + ": expected " + expected + ", got " + actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -16,6 +16,7 @@ import fr.koka99cab.sanctuary26.itsalive.creeper.ItsAliveCreeper;
|
||||
import fr.koka99cab.sanctuary26.itsalive.creeper.ItsAliveCreepers;
|
||||
import fr.koka99cab.sanctuary26.itsalive.entity.InfectedEntity;
|
||||
import fr.koka99cab.sanctuary26.itsalive.entity.InfectedVariant;
|
||||
import fr.koka99cab.sanctuary26.itsalive.entity.notch.NotchResidentService;
|
||||
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveEntityTypes;
|
||||
import fr.koka99cab.sanctuary26.ambiance.environment.AmbianceDailyWeather;
|
||||
import fr.koka99cab.sanctuary26.ambiance.environment.AmbianceEnvironmentController;
|
||||
@@ -94,6 +95,7 @@ public final class MasterKeyCommands {
|
||||
.then(creeperCommand())
|
||||
.then(infectedCommand())
|
||||
.then(ambianceCommand())
|
||||
.then(notchCommand())
|
||||
.then(SanctuaryProgressionCommands.command())
|
||||
.then(panoramaCommand())
|
||||
.then(teleportCommand())
|
||||
@@ -113,6 +115,42 @@ public final class MasterKeyCommands {
|
||||
);
|
||||
}
|
||||
|
||||
private static com.mojang.brigadier.builder.ArgumentBuilder<CommandSourceStack, ?> notchCommand() {
|
||||
return Commands.literal("notch")
|
||||
.executes(context -> inspectNotch(context.getSource()))
|
||||
.then(Commands.literal("inspect")
|
||||
.executes(context -> inspectNotch(context.getSource())))
|
||||
.then(Commands.literal("restore")
|
||||
.executes(context -> restoreNotch(context.getSource())));
|
||||
}
|
||||
|
||||
private static int inspectNotch(CommandSourceStack source) {
|
||||
NotchResidentService.Inspection inspection = NotchResidentService.inspect(source.getServer());
|
||||
if (!inspection.alphaAvailable()) {
|
||||
source.sendFailure(Component.translatable("command.masterkey.notch.alpha_unavailable"));
|
||||
return 0;
|
||||
}
|
||||
source.sendSuccess(() -> Component.translatable("command.masterkey.notch.inspect",
|
||||
inspection.loadedResidents(),
|
||||
inspection.canonicalUuid() == null ? "-" : inspection.canonicalUuid().toString(),
|
||||
inspection.phase() == null ? "-" : inspection.phase().name().toLowerCase(java.util.Locale.ROOT),
|
||||
inspection.position() == null ? "-" : inspection.position().toShortString(),
|
||||
inspection.worldDefeated()), false);
|
||||
return Command.SINGLE_SUCCESS;
|
||||
}
|
||||
|
||||
private static int restoreNotch(CommandSourceStack source) {
|
||||
NotchResidentService.Inspection inspection = NotchResidentService.restore(source.getServer());
|
||||
if (!inspection.alphaAvailable() || inspection.canonicalUuid() == null) {
|
||||
source.sendFailure(Component.translatable("command.masterkey.notch.restore.failure"));
|
||||
return 0;
|
||||
}
|
||||
source.sendSuccess(() -> Component.translatable("command.masterkey.notch.restore.success",
|
||||
inspection.canonicalUuid().toString(), inspection.loadedResidents(),
|
||||
inspection.phase().name().toLowerCase(java.util.Locale.ROOT)), true);
|
||||
return Command.SINGLE_SUCCESS;
|
||||
}
|
||||
|
||||
private static com.mojang.brigadier.builder.ArgumentBuilder<CommandSourceStack, ?> ambianceCommand() {
|
||||
var root = Commands.literal("ambiance")
|
||||
.executes(context -> ambianceStatus(context.getSource()))
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"mod.masterkey.description": "Owns administration, diagnostics and developer tools.",
|
||||
"command.masterkey.help": "MasterKey lab: give, spawn, creeper, infected, ambiance, progression, panorama, teleport, dump, debug.",
|
||||
"command.masterkey.help": "MasterKey lab: give, spawn, creeper, infected, ambiance, notch, progression, panorama, teleport, dump, debug.",
|
||||
"command.masterkey.notch.alpha_unavailable": "The Alpha dimension is not loaded.",
|
||||
"command.masterkey.notch.inspect": "Notch: %1$s loaded resident(s), UUID %2$s, phase %3$s, position %4$s, world victory: %5$s.",
|
||||
"command.masterkey.notch.restore.success": "Notch restored: UUID %1$s, %2$s loaded resident, phase %3$s.",
|
||||
"command.masterkey.notch.restore.failure": "Notch could not be restored in Alpha.",
|
||||
"command.masterkey.ambiance.status": "Weather: %1$s; lunar night: %2$s (%3$s).",
|
||||
"command.masterkey.ambiance.mode.auto": "automatic",
|
||||
"command.masterkey.ambiance.mode.forced": "forced",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"mod.masterkey.description": "Possède l'administration, les diagnostics et les outils de développement.",
|
||||
"command.masterkey.help": "Laboratoire MasterKey : give, spawn, creeper, infected, ambiance, progression, panorama, teleport, dump, debug.",
|
||||
"command.masterkey.help": "Laboratoire MasterKey : give, spawn, creeper, infected, ambiance, notch, progression, panorama, teleport, dump, debug.",
|
||||
"command.masterkey.notch.alpha_unavailable": "La dimension Alpha n'est pas chargée.",
|
||||
"command.masterkey.notch.inspect": "Notch : %1$s résident(s) chargé(s), UUID %2$s, phase %3$s, position %4$s, victoire mondiale : %5$s.",
|
||||
"command.masterkey.notch.restore.success": "Notch restauré : UUID %1$s, %2$s résident chargé, phase %3$s.",
|
||||
"command.masterkey.notch.restore.failure": "Impossible de restaurer Notch dans Alpha.",
|
||||
"command.masterkey.ambiance.status": "Météo : %1$s ; nuit lunaire : %2$s (%3$s).",
|
||||
"command.masterkey.ambiance.mode.auto": "automatique",
|
||||
"command.masterkey.ambiance.mode.forced": "forcée",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"mod.masterkey.description": "Отвечает за администрирование, диагностику и инструменты разработчика.",
|
||||
"command.masterkey.help": "Лаборатория MasterKey: give, spawn, creeper, infected, ambiance, progression, panorama, teleport, dump, debug.",
|
||||
"command.masterkey.help": "Лаборатория MasterKey: give, spawn, creeper, infected, ambiance, notch, progression, panorama, teleport, dump, debug.",
|
||||
"command.masterkey.notch.alpha_unavailable": "Измерение Alpha не загружено.",
|
||||
"command.masterkey.notch.inspect": "Notch: загружено жителей — %1$s, UUID %2$s, фаза %3$s, позиция %4$s, мировая победа: %5$s.",
|
||||
"command.masterkey.notch.restore.success": "Notch восстановлен: UUID %1$s, загружено жителей — %2$s, фаза %3$s.",
|
||||
"command.masterkey.notch.restore.failure": "Не удалось восстановить Notch в Alpha.",
|
||||
"command.masterkey.ambiance.status": "Погода: %1$s; лунная ночь: %2$s (%3$s).",
|
||||
"command.masterkey.ambiance.mode.auto": "автоматически",
|
||||
"command.masterkey.ambiance.mode.forced": "принудительно",
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
"minecraft": "~26.2",
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"itsalive": ">=0.0.0-alpha.1",
|
||||
"sanctuary": ">=0.0.0-alpha.17",
|
||||
"itsalive": ">=0.0.0-alpha.37",
|
||||
"sanctuary": ">=0.0.0-alpha.126",
|
||||
"ambiance": "*"
|
||||
},
|
||||
"custom": {
|
||||
|
||||
@@ -48,7 +48,7 @@ public final class OnlyFunMod implements ModInitializer {
|
||||
OnlyFunCommands.register();
|
||||
AbjectionActions.initialize();
|
||||
AbjectionCommands.register();
|
||||
LOGGER.info("[{}] Loot generator, natural Lucky Blocks, mob lootboxes and chicken rules ready (data v{}, network v{}).",
|
||||
LOGGER.info("[{}] Loot generator, boss-gated Shop, natural Lucky Blocks, mob lootboxes and chicken rules ready (data v{}, network v{}).",
|
||||
DISPLAY_NAME, CURRENT_DATA_VERSION, OnlyFunNetworking.PROTOCOL_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.onlyfun.loot;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.registry.OnlyFunRegistries;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.entity.SanctuaryEntityPolicy;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
@@ -38,7 +39,8 @@ public final class OnlyFunLootboxDrops {
|
||||
}
|
||||
|
||||
private static void afterDeath(LivingEntity entity, DamageSource source) {
|
||||
if (!(entity instanceof Mob mob) || !(entity.level() instanceof ServerLevel level)) return;
|
||||
if (SanctuaryEntityPolicy.isTemporaryCompanion(entity)
|
||||
|| !(entity instanceof Mob mob) || !(entity.level() instanceof ServerLevel level)) return;
|
||||
ServerPlayer killer = playerKiller(entity, source);
|
||||
if (killer == null || killer.isSpectator()) return;
|
||||
|
||||
|
||||
+12
-17
@@ -1,29 +1,22 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.shop;
|
||||
|
||||
import java.util.Objects;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.progression.SanctuaryWorldMilestones;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
/** Keeps exceptional boss rewards out of the Shop until their future boss owner unlocks them. */
|
||||
/** Keeps exceptional boss rewards out of the Shop until their public world milestone is reached. */
|
||||
public final class OnlyFunBossShopGate {
|
||||
public static final Identifier MAGIC_CLOUD = Identifier.fromNamespaceAndPath("iliketomoveit", "magic_cloud");
|
||||
public static final Identifier MAGIC_CARPET = Identifier.fromNamespaceAndPath("iliketomoveit", "magic_carpet");
|
||||
private static final Set<Identifier> BOSS_REWARDS = Set.of(MAGIC_CLOUD, MAGIC_CARPET);
|
||||
private static final BossUnlockResolver LOCKED = (server, reward) -> false;
|
||||
private static volatile BossUnlockResolver resolver = LOCKED;
|
||||
private static final Map<Identifier, Identifier> IMPLEMENTED_MILESTONES = Map.of(
|
||||
MAGIC_CARPET, SanctuaryNotchMilestone.NOTCH_DEFEATED);
|
||||
|
||||
private OnlyFunBossShopGate() {}
|
||||
|
||||
/** Called later by the module that owns the bosses and their persistent defeat state. */
|
||||
public static synchronized void registerUnlockResolver(BossUnlockResolver value) {
|
||||
Objects.requireNonNull(value, "value");
|
||||
if (resolver != LOCKED && resolver != value) {
|
||||
throw new IllegalStateException("A boss Shop unlock resolver is already registered");
|
||||
}
|
||||
resolver = value;
|
||||
}
|
||||
|
||||
public static boolean requiresBoss(String itemId) {
|
||||
Identifier id = Identifier.tryParse(itemId);
|
||||
return id != null && BOSS_REWARDS.contains(id);
|
||||
@@ -31,11 +24,13 @@ public final class OnlyFunBossShopGate {
|
||||
|
||||
public static boolean isAvailable(MinecraftServer server, String itemId) {
|
||||
Identifier id = Identifier.tryParse(itemId);
|
||||
return id == null || !BOSS_REWARDS.contains(id) || resolver.isUnlocked(server, id);
|
||||
if (id == null || !BOSS_REWARDS.contains(id)) return true;
|
||||
Identifier milestone = requiredMilestone(id);
|
||||
return milestone != null && SanctuaryWorldMilestones.isReached(server, milestone);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface BossUnlockResolver {
|
||||
boolean isUnlocked(MinecraftServer server, Identifier rewardItem);
|
||||
/** Returns null while the corresponding boss contract is not implemented. */
|
||||
public static Identifier requiredMilestone(Identifier rewardItem) {
|
||||
return IMPLEMENTED_MILESTONES.get(rewardItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"minecraft": "~26.2",
|
||||
"java": ">=25",
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"iliketomoveit": "*",
|
||||
"sanctuary": "*"
|
||||
"iliketomoveit": ">=0.0.0-alpha.17",
|
||||
"sanctuary": ">=0.0.0-alpha.129"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
|
||||
+7
-6
@@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.onlyfun.shop;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.policy.OnlyFunItemPolicy;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopOffer;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.shop.ShopOfferCatalog;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.notch.SanctuaryNotchMilestone;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -94,12 +95,12 @@ public final class OnlyFunShopCatalogSmoke {
|
||||
"Magic Cloud must remain boss-gated");
|
||||
require(!OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_carpet")
|
||||
&& !OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_cloud"),
|
||||
"Boss rewards must be unavailable while their bosses do not exist");
|
||||
OnlyFunBossShopGate.registerUnlockResolver((server, reward) ->
|
||||
reward.equals(OnlyFunBossShopGate.MAGIC_CARPET));
|
||||
require(OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_carpet")
|
||||
&& !OnlyFunBossShopGate.isAvailable(null, "iliketomoveit:magic_cloud"),
|
||||
"Each future boss must unlock only its corresponding reward");
|
||||
"Boss rewards must reject lookups without authoritative server state");
|
||||
require(SanctuaryNotchMilestone.NOTCH_DEFEATED.equals(
|
||||
OnlyFunBossShopGate.requiredMilestone(OnlyFunBossShopGate.MAGIC_CARPET)),
|
||||
"Magic Carpet must resolve the public Notch milestone");
|
||||
require(OnlyFunBossShopGate.requiredMilestone(OnlyFunBossShopGate.MAGIC_CLOUD) == null,
|
||||
"Magic Cloud must stay locked until its own boss is implemented");
|
||||
|
||||
for (String protectedId : List.of(
|
||||
"anotherworld:ruby", "anotherworld:ruby_block", "anotherworld:deepslate_ruby_ore",
|
||||
|
||||
+10
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
Ouch possède le combat, les dégâts et la destruction dans Sanctuary 26.2.
|
||||
|
||||
## Version courante — `0.0.0-alpha.5`
|
||||
## Version courante — `0.0.0-alpha.11`
|
||||
|
||||
La première version active migre les quatre objets du mod historique Gunz dans
|
||||
le namespace unique `ouch:*` et les rassemble dans l’onglet créatif
|
||||
@@ -79,6 +79,15 @@ n’ajoutent ni payload réseau, ni donnée persistante, ni munition enregistré
|
||||
L’Arc bêta réutilise volontairement l’apparence statique du bow vanilla et se
|
||||
fabrique sans forme avec un arc, une redstone et une ficelle.
|
||||
|
||||
## Combat de Notch Titan
|
||||
|
||||
Ouch implémente le pont public de combat exposé par It's Alive. Le Titan poursuit
|
||||
ses participants, annonce visuellement ses attaques puis alterne mêlée, boules de
|
||||
feu renvoyables et éclairs visuels avec dégâts serveur. Une boule renvoyée par un
|
||||
joueur inflige 40 dégâts au Titan. Les explosions de projectiles utilisent
|
||||
`ExplosionInteraction.NONE` et les autres attaques ne modifient aucun bloc ;
|
||||
seule la transformation initiale, pilotée par l'entité, peut endommager le décor.
|
||||
|
||||
## Compatibilité
|
||||
|
||||
Les anciens IDs `gunz:*` ne sont pas réenregistrés. Le contrat
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ tasks.register("verifyOuch") {
|
||||
def creeperlockRecipe = new JsonSlurper().parse(file("src/main/resources/data/ouch/recipe/creeperlock.json"))
|
||||
def silverEngineeringMigration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.184-silver-engineering-to-alpha.185.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.10"
|
||||
if (project.version.toString() != "0.0.0-alpha.11"
|
||||
|| mitrailletteRecipe.pattern != ["RRP", "IGI", " B "]
|
||||
|| mitrailletteRecipe.key != [B: "minecraft:stone_button", G: "minecraft:redstone",
|
||||
I: "anotherworld:silver_ingot", P: "anotherworld:silver_plate", R: "anotherworld:silver_rod"]
|
||||
|
||||
@@ -4,6 +4,7 @@ import fr.koka99cab.sanctuary26.ouch.OuchMod;
|
||||
import fr.koka99cab.sanctuary26.ouch.entity.WeatherTntEntity;
|
||||
import fr.koka99cab.sanctuary26.ouch.registry.OuchAbjectionProjectiles;
|
||||
import fr.koka99cab.sanctuary26.ouch.registry.OuchWeatherTnt;
|
||||
import fr.koka99cab.sanctuary26.ouch.notch.OuchNotchCombat;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry;
|
||||
import net.minecraft.client.renderer.entity.EntityRenderer;
|
||||
@@ -16,6 +17,8 @@ public final class OuchClient implements ClientModInitializer {
|
||||
public void onInitializeClient() {
|
||||
EntityRendererRegistry.register(OuchAbjectionProjectiles.ENTITY,
|
||||
context -> new ThrownItemRenderer<>(context));
|
||||
EntityRendererRegistry.register(OuchNotchCombat.FIREBALL,
|
||||
context -> new ThrownItemRenderer<>(context, 3.0F, true));
|
||||
registerWeatherRenderer(OuchWeatherTnt.ENTITY);
|
||||
registerWeatherRenderer(OuchWeatherTnt.REDSTONER_COMPAT_ENTITY);
|
||||
registerWeatherRenderer(OuchWeatherTnt.LEGACY_ENTITY);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package fr.koka99cab.sanctuary26.ouch.entity;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.entity.notch.NotchEntity;
|
||||
import fr.koka99cab.sanctuary26.itsalive.entity.notch.NotchFightRules;
|
||||
import fr.koka99cab.sanctuary26.ouch.notch.OuchNotchCombat;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.entity.projectile.hurtingprojectile.Fireball;
|
||||
import net.minecraft.world.item.enchantment.EnchantmentHelper;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.EntityHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Large, reflectable Titan fireball whose explosion never changes blocks. */
|
||||
public final class NotchFireballEntity extends Fireball {
|
||||
private static final int MAX_LIFETIME_TICKS = 200;
|
||||
private static final float DIRECT_HIT_DAMAGE = 10.0F;
|
||||
private static final float EXPLOSION_POWER = 2.0F;
|
||||
|
||||
public NotchFireballEntity(EntityType<? extends NotchFireballEntity> type, Level level) {
|
||||
super(type, level);
|
||||
}
|
||||
|
||||
public NotchFireballEntity(ServerLevel level, LivingEntity owner, Vec3 direction) {
|
||||
super(OuchNotchCombat.FIREBALL, owner, direction, level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
if (this.tickCount > MAX_LIFETIME_TICKS) this.discard();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHit(HitResult hitResult) {
|
||||
super.onHit(hitResult);
|
||||
if (!(this.level() instanceof ServerLevel level)) return;
|
||||
level.explode(this, this.getX(), this.getY(), this.getZ(), EXPLOSION_POWER, false,
|
||||
Level.ExplosionInteraction.NONE);
|
||||
this.discard();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHitEntity(EntityHitResult hitResult) {
|
||||
super.onHitEntity(hitResult);
|
||||
if (!(this.level() instanceof ServerLevel level)) return;
|
||||
Entity target = hitResult.getEntity();
|
||||
Entity owner = this.getOwner();
|
||||
if (target instanceof NotchEntity && owner instanceof NotchEntity) return;
|
||||
var source = this.damageSources().fireball(this, owner);
|
||||
float damage = target instanceof NotchEntity && owner instanceof Player
|
||||
? NotchFightRules.RETURNED_FIREBALL_DAMAGE : DIRECT_HIT_DAMAGE;
|
||||
if (target.hurtServer(level, source, damage)) {
|
||||
EnchantmentHelper.doPostAttackEffects(level, target, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package fr.koka99cab.sanctuary26.ouch.notch;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.api.notch.NotchCombatApi;
|
||||
import fr.koka99cab.sanctuary26.ouch.OuchMod;
|
||||
import fr.koka99cab.sanctuary26.ouch.entity.NotchFireballEntity;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.entity.LightningBolt;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Ouch-owned implementation of the server-authoritative Notch attack bridge. */
|
||||
public final class OuchNotchCombat implements NotchCombatApi.Engine {
|
||||
private static final Identifier FIREBALL_ID = OuchMod.id("notch_fireball");
|
||||
private static final ResourceKey<EntityType<?>> FIREBALL_KEY = ResourceKey.create(
|
||||
Registries.ENTITY_TYPE, FIREBALL_ID);
|
||||
public static final EntityType<NotchFireballEntity> FIREBALL = Registry.register(
|
||||
BuiltInRegistries.ENTITY_TYPE, FIREBALL_ID,
|
||||
EntityType.Builder.<NotchFireballEntity>of(NotchFireballEntity::new, MobCategory.MISC)
|
||||
.sized(1.0F, 1.0F)
|
||||
.clientTrackingRange(64)
|
||||
.updateInterval(1)
|
||||
.noSave()
|
||||
.noLootTable()
|
||||
.build(FIREBALL_KEY));
|
||||
private static boolean initialized;
|
||||
|
||||
private OuchNotchCombat() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
NotchCombatApi.register(new OuchNotchCombat());
|
||||
OuchMod.LOGGER.debug("[{}] Registered the Notch Titan combat engine.", OuchMod.DISPLAY_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void telegraph(ServerLevel level, LivingEntity boss, LivingEntity target,
|
||||
NotchCombatApi.Attack attack) {
|
||||
if (attack == NotchCombatApi.Attack.FIREBALL) {
|
||||
level.sendParticles(ParticleTypes.FLAME, boss.getX(), boss.getEyeY(), boss.getZ(),
|
||||
28, 0.7D, 1.0D, 0.7D, 0.035D);
|
||||
level.playSound(null, boss.blockPosition(), SoundEvents.BLAZE_SHOOT, SoundSource.HOSTILE,
|
||||
0.9F, 0.55F);
|
||||
} else if (attack == NotchCombatApi.Attack.LIGHTNING) {
|
||||
level.sendParticles(ParticleTypes.ELECTRIC_SPARK, target.getX(), target.getY() + 0.1D,
|
||||
target.getZ(), 42, 1.25D, 0.06D, 1.25D, 0.08D);
|
||||
level.playSound(null, target.blockPosition(), SoundEvents.TRIDENT_THUNDER.value(),
|
||||
SoundSource.HOSTILE, 1.0F, 1.35F);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ServerLevel level, LivingEntity boss, LivingEntity target,
|
||||
NotchCombatApi.Attack attack) {
|
||||
switch (attack) {
|
||||
case MELEE -> melee(level, boss, target);
|
||||
case FIREBALL -> fireball(level, boss, target);
|
||||
case LIGHTNING -> lightning(level, boss, target);
|
||||
}
|
||||
}
|
||||
|
||||
private static void melee(ServerLevel level, LivingEntity boss, LivingEntity target) {
|
||||
boss.swing(InteractionHand.MAIN_HAND);
|
||||
if (!target.hurtServer(level, level.damageSources().mobAttack(boss), 18.0F)) return;
|
||||
pushAway(boss, target, 1.4D, 0.48D);
|
||||
level.sendParticles(ParticleTypes.EXPLOSION, target.getX(), target.getY() + 1.0D,
|
||||
target.getZ(), 4, 0.35D, 0.55D, 0.35D, 0.02D);
|
||||
}
|
||||
|
||||
private static void fireball(ServerLevel level, LivingEntity boss, LivingEntity target) {
|
||||
Vec3 origin = boss.getEyePosition().add(boss.getLookAngle().scale(1.5D));
|
||||
Vec3 aim = target.getEyePosition().subtract(origin).normalize();
|
||||
NotchFireballEntity projectile = new NotchFireballEntity(level, boss, aim);
|
||||
projectile.setPos(origin);
|
||||
level.addFreshEntity(projectile);
|
||||
}
|
||||
|
||||
private static void lightning(ServerLevel level, LivingEntity boss, LivingEntity target) {
|
||||
LightningBolt lightning = EntityTypes.LIGHTNING_BOLT.create(level, EntitySpawnReason.TRIGGERED);
|
||||
if (lightning != null) {
|
||||
lightning.setVisualOnly(true);
|
||||
lightning.setPos(target.position());
|
||||
level.addFreshEntity(lightning);
|
||||
}
|
||||
if (target.hurtServer(level, level.damageSources().lightningBolt(), 7.0F)) {
|
||||
pushAway(boss, target, 1.0D, 0.35D);
|
||||
}
|
||||
}
|
||||
|
||||
private static void pushAway(LivingEntity boss, LivingEntity target, double strength, double lift) {
|
||||
Vec3 horizontal = target.position().subtract(boss.position()).multiply(1.0D, 0.0D, 1.0D);
|
||||
if (horizontal.lengthSqr() < 1.0E-6D) horizontal = boss.getLookAngle().multiply(1.0D, 0.0D, 1.0D);
|
||||
horizontal = horizontal.normalize().scale(strength);
|
||||
target.push(horizontal.x, lift, horizontal.z);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.ouch.registry;
|
||||
|
||||
import fr.koka99cab.sanctuary26.ouch.OuchMod;
|
||||
import fr.koka99cab.sanctuary26.ouch.notch.OuchNotchCombat;
|
||||
|
||||
/**
|
||||
* Single registration entry point for the Ouch namespace.
|
||||
@@ -20,6 +21,7 @@ public final class OuchRegistries {
|
||||
OuchItems.initialize();
|
||||
OuchWeatherTnt.initialize();
|
||||
OuchAbjectionProjectiles.initialize();
|
||||
OuchNotchCombat.initialize();
|
||||
OuchCreativeTab.initialize();
|
||||
OuchMod.LOGGER.debug("[{}] Weapon registry hub initialized.", OuchMod.DISPLAY_NAME);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"anotherworld": ">=0.0.0-alpha.46",
|
||||
"ambiance": ">=0.0.0-alpha.12",
|
||||
"onlyfun": ">=0.0.0-alpha.23",
|
||||
"sanctuary": ">=0.0.0-alpha.110"
|
||||
"itsalive": ">=0.0.0-alpha.37",
|
||||
"sanctuary": ">=0.0.0-alpha.126"
|
||||
},
|
||||
"custom": {
|
||||
"sanctuary26": {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.222",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.124"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.223",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.125"
|
||||
}
|
||||
},
|
||||
"inventory_sort": {
|
||||
"owner": "sanctuary",
|
||||
"default_binding": "middle_mouse",
|
||||
"unlocked_storage_rows_only": true,
|
||||
"packs_from_top": true,
|
||||
"merges_identical_stacks": true,
|
||||
"hotbar_armor_offhand_crafting_and_special_slots_unchanged": true,
|
||||
"server_authoritative": true,
|
||||
"server_cooldown_ticks": 8,
|
||||
"ignored_in_creative": true,
|
||||
"ignored_while_typing": true
|
||||
},
|
||||
"quest_tracker_display": {
|
||||
"owner": "sanctuary",
|
||||
"default_mode": "transparent_text",
|
||||
"mode_toggle_default_key": "N",
|
||||
"original_chris_panel_available": true,
|
||||
"transparent_text_below_f3_left_column": true,
|
||||
"contracts_named_and_sorted_by_reward_tier": true,
|
||||
"reward_order": [
|
||||
"ruby",
|
||||
"sapphire",
|
||||
"emerald_or_other"
|
||||
]
|
||||
},
|
||||
"compatibility": {
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"existing_saves_supported": true,
|
||||
"registry_ids_changed": false,
|
||||
"sanctuary_data_version": 11,
|
||||
"sanctuary_network_protocol": 22
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true,
|
||||
"jei": {
|
||||
"source": "30.29.0.196",
|
||||
"target": "30.29.0.198",
|
||||
"modrinth_version_id": "67Dvn8sL",
|
||||
"packwiz_and_prism_synchronized": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.223",
|
||||
"modules": {
|
||||
"itsalive": "0.0.0-alpha.36",
|
||||
"ouch": "0.0.0-alpha.10",
|
||||
"masterkey": "0.0.0-alpha.8",
|
||||
"iliketomoveit": "0.0.0-alpha.16",
|
||||
"onlyfun": "0.0.0-alpha.25",
|
||||
"sanctuary": "0.0.0-alpha.125"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.224",
|
||||
"modules": {
|
||||
"itsalive": "0.0.0-alpha.37",
|
||||
"ouch": "0.0.0-alpha.11",
|
||||
"masterkey": "0.0.0-alpha.9",
|
||||
"iliketomoveit": "0.0.0-alpha.17",
|
||||
"onlyfun": "0.0.0-alpha.26",
|
||||
"sanctuary": "0.0.0-alpha.128"
|
||||
}
|
||||
},
|
||||
"inventory_sort": {
|
||||
"owner": "sanctuary",
|
||||
"payload_container_flag": "bounded_boolean",
|
||||
"sortable_storage_menus": ["chest", "barrel", "shulker_box", "hopper", "dispenser"],
|
||||
"player_inventory_when_hovered": true,
|
||||
"hotbar_unchanged": true,
|
||||
"hotbar_restored_to_bottom_on_login": true,
|
||||
"hotbar_restored_to_bottom_after_row_purchase": true,
|
||||
"excluded_menus": ["furnace", "shop", "redstoner_storage_terminal"],
|
||||
"server_authoritative": true,
|
||||
"server_validates_open_menu": true
|
||||
},
|
||||
"notch_encounter": {
|
||||
"entity_owner": "itsalive",
|
||||
"combat_owner": "ouch",
|
||||
"milestone_owner": "sanctuary",
|
||||
"diagnostics_owner": "masterkey",
|
||||
"dimension": "sanctuary:alpha",
|
||||
"unique_persistent_resident": true,
|
||||
"legacy_entity": "portals:notch",
|
||||
"legacy_entity_migrated": true,
|
||||
"maximum_participants": 8,
|
||||
"first_transformation_breaks_blocks": true,
|
||||
"later_attacks_break_blocks": false,
|
||||
"returned_fireballs_supported": true,
|
||||
"abandoned_fight_resets": true,
|
||||
"global_victory_monotonic": true,
|
||||
"atlas_boss_marker_for_participants": true,
|
||||
"commands": ["/masterkey notch inspect", "/masterkey notch restore"]
|
||||
},
|
||||
"magic_carpet": {
|
||||
"owner": "iliketomoveit",
|
||||
"canonical_item": "iliketomoveit:magic_carpet",
|
||||
"legacy_item": "portals:notch_totem",
|
||||
"legacy_alias_hidden": true,
|
||||
"legacy_components_preserved": true,
|
||||
"legacy_custom_name_preserved": true,
|
||||
"legacy_locations": ["player_inventory", "container", "item_entity"],
|
||||
"one_legacy_stack_becomes_one_canonical_stack": true,
|
||||
"new_worlds_distribute_legacy_item": false,
|
||||
"temporary_platform": {
|
||||
"size": [3, 1, 3],
|
||||
"block": "minecraft:glass",
|
||||
"air_only_placement": true,
|
||||
"creative_flight_granted": false,
|
||||
"overlap_owner_tracking": true,
|
||||
"cleanup": ["deactivation", "disconnect", "dimension_change", "server_stop"]
|
||||
}
|
||||
},
|
||||
"notch_reward": {
|
||||
"milestone_owner": "sanctuary",
|
||||
"milestone": "sanctuary:notch_defeated",
|
||||
"reservation": "initial_reward_claimed",
|
||||
"completion": "initial_reward_delivered",
|
||||
"delivery_owner": "iliketomoveit",
|
||||
"delivery": "deterministic_chest_near_notch_residence",
|
||||
"firework_signal": true,
|
||||
"idempotent_recovery": true,
|
||||
"maximum_canonical_initial_rewards": 1,
|
||||
"provenance": "sanctuary:notch_reward"
|
||||
},
|
||||
"shop_replacement": {
|
||||
"catalog_owner": "onlyfun",
|
||||
"transaction_owner": "sanctuary",
|
||||
"unlock_resolver": "SanctuaryWorldMilestones",
|
||||
"hidden_before_notch": true,
|
||||
"server_authoritative_gate": true,
|
||||
"configured_ruby_price": 1536,
|
||||
"configured_sapphire_price": 128,
|
||||
"daily_rotation_rules_preserved": true,
|
||||
"mailbox_delivery": true,
|
||||
"recreates_initial_reward": false,
|
||||
"provenance": "sanctuary:shop_purchase",
|
||||
"replacement_logged": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"registry_ids_preserved_or_migrated": true,
|
||||
"client_server_update_required": true,
|
||||
"itsalive_data_version": 4,
|
||||
"itsalive_network_protocol": 4,
|
||||
"ouch_data_version": 1,
|
||||
"ouch_network_protocol": 1,
|
||||
"masterkey_data_version": 1,
|
||||
"masterkey_network_protocol": 2,
|
||||
"iliketomoveit_data_version": 4,
|
||||
"iliketomoveit_network_protocol": 5,
|
||||
"onlyfun_data_version": 2,
|
||||
"onlyfun_network_protocol": 3,
|
||||
"sanctuary_data_version": 13,
|
||||
"sanctuary_notch_state_data_version": 2,
|
||||
"sanctuary_source_network_protocol": 22,
|
||||
"sanctuary_target_network_protocol": 23,
|
||||
"sanctuary_network_protocol_changed": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.224",
|
||||
"modules": {
|
||||
"anotherworld": "0.0.0-alpha.55",
|
||||
"itsalive": "0.0.0-alpha.37",
|
||||
"iliketomoveit": "0.0.0-alpha.17",
|
||||
"onlyfun": "0.0.0-alpha.26",
|
||||
"sanctuary": "0.0.0-alpha.128"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.225",
|
||||
"modules": {
|
||||
"anotherworld": "0.0.0-alpha.56",
|
||||
"itsalive": "0.0.0-alpha.38",
|
||||
"iliketomoveit": "0.0.0-alpha.18",
|
||||
"onlyfun": "0.0.0-alpha.27",
|
||||
"sanctuary": "0.0.0-alpha.129"
|
||||
}
|
||||
},
|
||||
"fixed_issues": [105, 116, 123, 124],
|
||||
"safe_vault_raid": {
|
||||
"owner": "anotherworld",
|
||||
"attacker_requires_registered_vault": true,
|
||||
"validated_when_drilling_starts": true,
|
||||
"revalidated_during_drilling": true,
|
||||
"revalidated_before_balance_transfer": true
|
||||
},
|
||||
"temporary_companions": {
|
||||
"owner": "sanctuary",
|
||||
"public_policy_api": "SanctuaryEntityPolicy",
|
||||
"vanilla_loot_suppressed": true,
|
||||
"experience_suppressed": true,
|
||||
"quest_board_progress_suppressed": true,
|
||||
"community_request_progress_suppressed": true,
|
||||
"onlyfun_lootboxes_suppressed": true
|
||||
},
|
||||
"canaplia_variants": {
|
||||
"owner": "itsalive",
|
||||
"historical_seed_ids_preserved": 16,
|
||||
"placement_preserves_variant": true,
|
||||
"natural_growth_preserves_variant": true,
|
||||
"bonemeal_preserves_variant": true,
|
||||
"upper_crop_preserves_variant": true,
|
||||
"block_state_persistence_preserves_variant": true,
|
||||
"harvest_and_break_drops_preserve_variant": true,
|
||||
"mature_neighbors_preserve_variant": true,
|
||||
"tender_plus_olive_can_produce_gold": true
|
||||
},
|
||||
"magic_carpet_texture": {
|
||||
"owner": "iliketomoveit",
|
||||
"canonical_item": "iliketomoveit:magic_carpet",
|
||||
"legacy_item": "portals:notch_totem",
|
||||
"texture": "iliketomoveit:item/magic_carpet",
|
||||
"historical_totem_art_restored": true,
|
||||
"purple_wool_reference_removed": true,
|
||||
"platform_behavior_changed": false
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"registry_ids_changed": false,
|
||||
"persistent_data_formats_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"client_server_update_required": true,
|
||||
"anotherworld_data_version": 5,
|
||||
"anotherworld_network_protocol": 2,
|
||||
"itsalive_data_version": 4,
|
||||
"itsalive_network_protocol": 4,
|
||||
"iliketomoveit_data_version": 4,
|
||||
"iliketomoveit_network_protocol": 5,
|
||||
"onlyfun_data_version": 2,
|
||||
"onlyfun_network_protocol": 3,
|
||||
"sanctuary_data_version": 13,
|
||||
"sanctuary_network_protocol": 23
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.225",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.129"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.226",
|
||||
"modules": {
|
||||
"sanctuary": "0.0.0-alpha.130"
|
||||
}
|
||||
},
|
||||
"white_cape": {
|
||||
"owner": "sanctuary",
|
||||
"item_id": "sanctuary:white_cape",
|
||||
"crafting_material": "minecraft:white_wool",
|
||||
"crafting_material_count": 6,
|
||||
"crafting_pattern": ["WW", "WW", "WW"],
|
||||
"banner_decoration_recipe": "cape_plus_banner",
|
||||
"accepted_banners": "#minecraft:banners",
|
||||
"base_color_preserved": true,
|
||||
"maximum_pattern_layers": 6,
|
||||
"inventory_pattern_rendering": true,
|
||||
"equipped_pattern_rendering": true,
|
||||
"vanilla_cape_physics": true,
|
||||
"included_in_onlyfun_lootbox": false
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"registry_ids_added": ["sanctuary:white_cape"],
|
||||
"persistent_data_formats_changed": false,
|
||||
"sanctuary_data_version": 13,
|
||||
"sanctuary_source_network_protocol": 23,
|
||||
"sanctuary_target_network_protocol": 24,
|
||||
"sanctuary_network_protocol_changed": true,
|
||||
"client_server_update_required": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.226",
|
||||
"modules": {
|
||||
"anotherworld": "0.0.0-alpha.56",
|
||||
"sanctuary": "0.0.0-alpha.130"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.227",
|
||||
"modules": {
|
||||
"anotherworld": "0.0.0-alpha.57",
|
||||
"sanctuary": "0.0.0-alpha.131"
|
||||
}
|
||||
},
|
||||
"backroom": {
|
||||
"owner": "sanctuary",
|
||||
"dimension": "sanctuary:backroom",
|
||||
"entry_requires_sneaking_at_bed": true,
|
||||
"exit_requires_bed": true,
|
||||
"arrival_radius_blocks": 2000,
|
||||
"entry_inventory_to_mailbox": true,
|
||||
"entry_transfer_atomic": true,
|
||||
"entry_transfer_idempotent": true,
|
||||
"lost_items_persisted": true,
|
||||
"lost_item_registry_bounded": true,
|
||||
"renewable_chests": true,
|
||||
"personal_losses_prioritized": true,
|
||||
"shared_old_losses_supported": true,
|
||||
"anotherworld_material_api": "BackroomMaterialApi"
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"sanctuary_source_data_version": 13,
|
||||
"sanctuary_target_data_version": 14,
|
||||
"sanctuary_network_protocol": 24,
|
||||
"network_protocol_changed": false,
|
||||
"client_server_update_required": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true,
|
||||
"fabric_api": {
|
||||
"source": "0.158.0+26.2",
|
||||
"target": "0.159.0+26.2",
|
||||
"modrinth_version_id": "BgeCGgGZ"
|
||||
},
|
||||
"jei": {
|
||||
"source": "30.29.0.198",
|
||||
"target": "30.29.0.199",
|
||||
"modrinth_version_id": "astSvyl6"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.227",
|
||||
"modules": {
|
||||
"redstoner": "0.0.0-alpha.13",
|
||||
"sanctuary": "0.0.0-alpha.131"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.228",
|
||||
"modules": {
|
||||
"redstoner": "0.0.0-alpha.15",
|
||||
"sanctuary": "0.0.0-alpha.132"
|
||||
}
|
||||
},
|
||||
"backroom": {
|
||||
"level_count": 5,
|
||||
"minimum_y": -64,
|
||||
"maximum_y_exclusive": 384,
|
||||
"entry_floor_y": 367,
|
||||
"monsters_disabled": true,
|
||||
"arrival_torch": true,
|
||||
"matching_vertical_transitions": true,
|
||||
"connected_maze": true,
|
||||
"dense_city_and_highways": true,
|
||||
"three_dimensional_brick_grid": true,
|
||||
"intentional_void_holes": true
|
||||
},
|
||||
"storage_terminal": {
|
||||
"click_to_focus": true,
|
||||
"inventory_key_closes": true,
|
||||
"shulker_contents_searchable": true,
|
||||
"shulker_remains_result": true,
|
||||
"vanilla_shulker_preview_preserved": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"persistent_data_formats_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"redstoner_data_version": 3,
|
||||
"redstoner_network_protocol": 7,
|
||||
"sanctuary_data_version": 14,
|
||||
"sanctuary_network_protocol": 24,
|
||||
"client_server_update_required": true,
|
||||
"existing_backroom_chunks_preserved": true,
|
||||
"new_backroom_generation_applies_to_new_chunks": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"source": {
|
||||
"pack_version": "26.2.0-alpha.228",
|
||||
"modules": {
|
||||
"redstoner": "0.0.0-alpha.15"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"pack_version": "26.2.0-alpha.229",
|
||||
"modules": {
|
||||
"redstoner": "0.0.0-alpha.16"
|
||||
}
|
||||
},
|
||||
"storage_terminal_sorting": {
|
||||
"owner": "redstoner",
|
||||
"modes": [
|
||||
"alphabetical",
|
||||
"count_ascending",
|
||||
"count_descending"
|
||||
],
|
||||
"server_authoritative": true,
|
||||
"explicit_user_action": true,
|
||||
"returns_to_first_page": true,
|
||||
"periodic_refresh_preserves_order": true,
|
||||
"periodic_refresh_preserves_hovered_stack": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"persistent_data_formats_changed": false,
|
||||
"network_protocols_changed": false,
|
||||
"redstoner_data_version": 3,
|
||||
"redstoner_network_protocol": 7,
|
||||
"client_server_update_required": true
|
||||
},
|
||||
"distribution": {
|
||||
"pack_release_updated": true,
|
||||
"packwiz_index_refreshed": true,
|
||||
"prism_modrinth_index_updated": true,
|
||||
"shared_client_server_mrpack": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"module": "sanctuary",
|
||||
"source_module_version": "0.0.0-alpha.130",
|
||||
"target_module_version": "0.0.0-alpha.131",
|
||||
"integration_modules": {
|
||||
"anotherworld": {
|
||||
"source": "0.0.0-alpha.56",
|
||||
"target": "0.0.0-alpha.57",
|
||||
"data_version_changed": false,
|
||||
"network_protocol_changed": false
|
||||
}
|
||||
},
|
||||
"source_data_version": 13,
|
||||
"target_data_version": 14,
|
||||
"migration": {
|
||||
"kind": "monotone_addition",
|
||||
"save_key": "sanctuary:backroom",
|
||||
"existing_player_progress_preserved": true,
|
||||
"existing_shop_data_preserved": true,
|
||||
"existing_mailboxes_preserved": true,
|
||||
"mailbox_transaction_history_default": [],
|
||||
"backroom_loss_registry_default": [],
|
||||
"backroom_chest_registry_default": []
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"network_protocol_changed": false,
|
||||
"registry_ids_added": [
|
||||
"sanctuary:backroom",
|
||||
"sanctuary:backroom_loss_id",
|
||||
"sanctuary:backroom_architecture"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"status": "released",
|
||||
"modules": {
|
||||
"sanctuary": {
|
||||
"source_module_version": "0.0.0-alpha.134",
|
||||
"target_module_version": "0.0.0-alpha.135",
|
||||
"source_data_version": 14,
|
||||
"target_data_version": 15
|
||||
},
|
||||
"redstoner": {
|
||||
"source_module_version": "0.0.0-alpha.16",
|
||||
"target_module_version": "0.0.0-alpha.17",
|
||||
"source_data_version": 3,
|
||||
"target_data_version": 4
|
||||
}
|
||||
},
|
||||
"migration": {
|
||||
"kind": "monotone_addition",
|
||||
"ranking_save_key": "sanctuary:rankings",
|
||||
"vanilla_statistics_imported_once": true,
|
||||
"player_identity": "uuid",
|
||||
"ranking_metric_index_version": 1,
|
||||
"trophy_panel_id": "redstoner:trophy_panel",
|
||||
"trophy_panel_data_version": 1,
|
||||
"existing_redstoner_block_entities_unchanged": true
|
||||
},
|
||||
"compatibility": {
|
||||
"existing_saves_supported": true,
|
||||
"new_world_required": false,
|
||||
"save_reset_required": false,
|
||||
"network_protocols_changed": false,
|
||||
"newer_ranking_data_is_read_only": true,
|
||||
"newer_panel_data_is_read_only": true
|
||||
}
|
||||
}
|
||||
+20
-20
@@ -14,24 +14,24 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
|
||||
|
||||
[[files]]
|
||||
file = "mods/ambiance-0.0.0-alpha.22.jar"
|
||||
hash = "a6b7d98971f411b202f5679aae05c1bc93754ee9a42d10dc903413b17ba97a63"
|
||||
hash = "b4ba14b9ada5b4c1d97b039ee632a158ae266eba2faefee690e710cc5f0f993e"
|
||||
|
||||
[[files]]
|
||||
file = "mods/anotherworld-0.0.0-alpha.55.jar"
|
||||
hash = "cac322006ad9a724bb656b400cf47d84a18e76cd1a35f4bd13c112f443d89275"
|
||||
file = "mods/anotherworld-0.0.0-alpha.57.jar"
|
||||
hash = "4c8a3315ba9db6799fb0f046b45d702d56fba65173abccbd81ab995f56690407"
|
||||
|
||||
[[files]]
|
||||
file = "mods/events-0.0.0-alpha.0.jar"
|
||||
hash = "8d2566ceb838a486fe9d8539acd127446e51d7085dcbefa86ee3c1aea4630a6d"
|
||||
hash = "e7304da48379eb39737b126431316644250ecbc5b6ec8a439cc12e96a9477fa4"
|
||||
|
||||
[[files]]
|
||||
file = "mods/fabric-api.pw.toml"
|
||||
hash = "df032a6b051d0262cf24945c43e0e6b915e8cc1d62af62fe933932e7f35960c4"
|
||||
hash = "b8ea5f403a0cdfbfd5ab1704bb61278e8dcf31b525db9c242b22047852d34f85"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/iliketomoveit-0.0.0-alpha.16.jar"
|
||||
hash = "94e5afefab9485c40a47342dc84f401fa126be40064832929601e781b3a03738"
|
||||
file = "mods/iliketomoveit-0.0.0-alpha.18.jar"
|
||||
hash = "bd0467d186737b59996a8b1589a7c1837c990890f1b21154aaca614fb042c556"
|
||||
|
||||
[[files]]
|
||||
file = "mods/iris.pw.toml"
|
||||
@@ -39,33 +39,33 @@ hash = "84a3cef87679c0d21dceb4641167dc0368b9a3593698f6a3632485e9b3a8d5da"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/itsalive-0.0.0-alpha.36.jar"
|
||||
hash = "94899dba383aff41c39f7f25ee1a6b7998aa1e9889e7b9e9039a984e0bc0882f"
|
||||
file = "mods/itsalive-0.0.0-alpha.38.jar"
|
||||
hash = "dd1869166cfeeb36b19b7e243e7ece32c925d362aa80782076d493decc6e2871"
|
||||
|
||||
[[files]]
|
||||
file = "mods/jei.pw.toml"
|
||||
hash = "115d2239bc86ecdad88705dbc215d27ddb6a08d94f74fa538abb6a67a00bf71e"
|
||||
hash = "9cc4a91f57effc48b94f6acc1f43067bb8eb99fd1df3397612c55761de9ec2c2"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/masterkey-0.0.0-alpha.8.jar"
|
||||
hash = "102c3da5d201452be23ab84338e023c284f4770270ca945b0c63b6a996fb2922"
|
||||
file = "mods/masterkey-0.0.0-alpha.9.jar"
|
||||
hash = "46ef9947e82368fbe60d696174b3a3c99960be29432f3ccd7daf2714927b5c12"
|
||||
|
||||
[[files]]
|
||||
file = "mods/onlyfun-0.0.0-alpha.25.jar"
|
||||
hash = "2676072de7dbc9b27c4fd0e50e797832356fbc903e80c5bd9a5bd55c1a5aff1b"
|
||||
file = "mods/onlyfun-0.0.0-alpha.27.jar"
|
||||
hash = "d01271cbccef9c3c5f66e3d56748cd69d953f9eb53d21c8a1575523d40e77c56"
|
||||
|
||||
[[files]]
|
||||
file = "mods/ouch-0.0.0-alpha.10.jar"
|
||||
hash = "59815a0415c48c92acfc7c18bd2399780bab9b0b5eee69c2b35a66cb1474109b"
|
||||
file = "mods/ouch-0.0.0-alpha.11.jar"
|
||||
hash = "ca612f8e660744af81d5a2612f78f8ad61dacf746a22474837e8b52a22bb3204"
|
||||
|
||||
[[files]]
|
||||
file = "mods/redstoner-0.0.0-alpha.13.jar"
|
||||
hash = "0bd3888b13c894aed806696a5e73e076c7ed985eff2c242f0df9ec64ff3fcb37"
|
||||
file = "mods/redstoner-0.0.0-alpha.16.jar"
|
||||
hash = "e273042c0f7b12ed0a13da3f75f41903a741a29399da353ecd5144e67dcb680e"
|
||||
|
||||
[[files]]
|
||||
file = "mods/sanctuary-0.0.0-alpha.124.jar"
|
||||
hash = "39a3db3d1d9876ac5c7a7135a3b524c82508d0ee2e0c5c410c6515ff8931c6fb"
|
||||
file = "mods/sanctuary-0.0.0-alpha.132.jar"
|
||||
hash = "72a946358be6039c706cf52563ffe61ec70477ffb4e7db80e71116758f5bd519"
|
||||
|
||||
[[files]]
|
||||
file = "mods/simple-voice-chat.pw.toml"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name = "Fabric API"
|
||||
filename = "fabric-api-0.158.0+26.2.jar"
|
||||
filename = "fabric-api-0.159.0+26.2.jar"
|
||||
side = "both"
|
||||
pin = true
|
||||
|
||||
[download]
|
||||
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/NqwNSxwA/fabric-api-0.158.0%2B26.2.jar"
|
||||
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/BgeCGgGZ/fabric-api-0.159.0%2B26.2.jar"
|
||||
hash-format = "sha512"
|
||||
hash = "4c2c1ebe74ffd54875a01ff371b53ba3d8674ac98d561f7dae02a96d3d37fbdbc5f5abc6e820f73b6154d6f873ddd05a442b0998ed2d456863dc0ad972e040a6"
|
||||
hash = "4d5d9c3daafe19be187f638f2d6de3db26420b8720ef5a266cfa81f79d9019cab8c86e6d47478bbb0b0ee0b8f769204b72c843caa2e4aabd94adc41f3dafb518"
|
||||
|
||||
[update]
|
||||
[update.modrinth]
|
||||
mod-id = "P7dR8mSH"
|
||||
version = "NqwNSxwA"
|
||||
version = "BgeCGgGZ"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name = "Just Enough Items (JEI)"
|
||||
filename = "jei-26.2-fabric-30.29.0.196.jar"
|
||||
filename = "jei-26.2-fabric-30.29.0.199.jar"
|
||||
side = "both"
|
||||
pin = true
|
||||
|
||||
[download]
|
||||
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/vvwsu2BR/jei-26.2-fabric-30.29.0.196.jar"
|
||||
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/astSvyl6/jei-26.2-fabric-30.29.0.199.jar"
|
||||
hash-format = "sha512"
|
||||
hash = "c62a3e264d4b0ec398b9aa717a4832eecb8663bbcec4483dc58a9999d5e4e55ca4f5bca285a0d4a003220c698c3e87c8a3294366e085e72545e4afe84aec92c7"
|
||||
hash = "2849ff6a4cc91822798f0c70e7749c298b911c1037fc74e9ab0881734b8d5cb6507fb3169b4a240ef6c37eb6f7c573794c46073ff352f160feee245233b2abe5"
|
||||
|
||||
[update]
|
||||
[update.modrinth]
|
||||
mod-id = "u6dRKJwZ"
|
||||
version = "vvwsu2BR"
|
||||
version = "astSvyl6"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
name = "Sanctuary 26.2"
|
||||
author = "KOKA99CAB"
|
||||
version = "26.2.0-alpha.222"
|
||||
version = "26.2.0-alpha.229"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
file = "index.toml"
|
||||
hash-format = "sha256"
|
||||
hash = "53b5bec1b113ab2b353e6a142e3dbc4617ea7d27003bcbb09b668938dd262ddd"
|
||||
hash = "6a5b1811c2dd2b905a67cd817b8e377a8b68589431650775e1d8aabe04ff32e9"
|
||||
|
||||
[versions]
|
||||
fabric = "0.19.3"
|
||||
|
||||
@@ -3,37 +3,37 @@
|
||||
"game": "minecraft",
|
||||
"name": "Sanctuary 26.2",
|
||||
"summary": "Sanctuary 26.2 — progression, exploration, worldgen and multiplayer systems by KOKA99CAB.",
|
||||
"versionId": "26.2.0-alpha.222",
|
||||
"versionId": "26.2.0-alpha.229",
|
||||
"files": [
|
||||
{
|
||||
"path": "mods/fabric-api-0.158.0+26.2.jar",
|
||||
"path": "mods/fabric-api-0.159.0+26.2.jar",
|
||||
"hashes": {
|
||||
"sha1": "7d0fb560579d28b933bdbc36b47449efe78d3bda",
|
||||
"sha512": "4c2c1ebe74ffd54875a01ff371b53ba3d8674ac98d561f7dae02a96d3d37fbdbc5f5abc6e820f73b6154d6f873ddd05a442b0998ed2d456863dc0ad972e040a6"
|
||||
"sha1": "5307b87f8336f27e8f963263ac514f4bc6175d90",
|
||||
"sha512": "4d5d9c3daafe19be187f638f2d6de3db26420b8720ef5a266cfa81f79d9019cab8c86e6d47478bbb0b0ee0b8f769204b72c843caa2e4aabd94adc41f3dafb518"
|
||||
},
|
||||
"env": {
|
||||
"client": "required",
|
||||
"server": "required"
|
||||
},
|
||||
"downloads": [
|
||||
"https://cdn.modrinth.com/data/P7dR8mSH/versions/NqwNSxwA/fabric-api-0.158.0%2B26.2.jar"
|
||||
"https://cdn.modrinth.com/data/P7dR8mSH/versions/BgeCGgGZ/fabric-api-0.159.0%2B26.2.jar"
|
||||
],
|
||||
"fileSize": 2535765
|
||||
"fileSize": 2535793
|
||||
},
|
||||
{
|
||||
"path": "mods/jei-26.2-fabric-30.29.0.196.jar",
|
||||
"path": "mods/jei-26.2-fabric-30.29.0.199.jar",
|
||||
"hashes": {
|
||||
"sha1": "c2f2c6b5822a560fe9872921b908ffc2f3297d00",
|
||||
"sha512": "c62a3e264d4b0ec398b9aa717a4832eecb8663bbcec4483dc58a9999d5e4e55ca4f5bca285a0d4a003220c698c3e87c8a3294366e085e72545e4afe84aec92c7"
|
||||
"sha1": "1d6ab9269be16f85fb13a34a36b34ad07a9b32a7",
|
||||
"sha512": "2849ff6a4cc91822798f0c70e7749c298b911c1037fc74e9ab0881734b8d5cb6507fb3169b4a240ef6c37eb6f7c573794c46073ff352f160feee245233b2abe5"
|
||||
},
|
||||
"env": {
|
||||
"client": "required",
|
||||
"server": "required"
|
||||
},
|
||||
"downloads": [
|
||||
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/vvwsu2BR/jei-26.2-fabric-30.29.0.196.jar"
|
||||
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/astSvyl6/jei-26.2-fabric-30.29.0.199.jar"
|
||||
],
|
||||
"fileSize": 1838353
|
||||
"fileSize": 1849188
|
||||
},
|
||||
{
|
||||
"path": "mods/voicechat-fabric-2.6.22+26.2.jar",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"pack_version": "26.2.0-alpha.222",
|
||||
"pack_version": "26.2.0-alpha.229",
|
||||
"release_channel": "alpha",
|
||||
"minecraft_version": "26.2",
|
||||
"fabric_loader_version": "0.19.3",
|
||||
@@ -9,7 +9,7 @@
|
||||
"project_id": "FgOqyAXd",
|
||||
"project_slug": "sanctuary-koka99",
|
||||
"project_url": "https://modrinth.com/modpack/sanctuary-koka99",
|
||||
"primary_file": "sanctuary-26.2.0-alpha.222.mrpack",
|
||||
"primary_file": "sanctuary-26.2.0-alpha.229.mrpack",
|
||||
"shared_client_server_pack": true,
|
||||
"update_owner": "packwiz-installer",
|
||||
"check_on_launch": true,
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.slf4j.LoggerFactory;
|
||||
public final class RedStonerMod implements ModInitializer {
|
||||
public static final String MOD_ID = "redstoner";
|
||||
public static final String DISPLAY_NAME = "Red-Stoner";
|
||||
public static final int CURRENT_DATA_VERSION = 3;
|
||||
public static final int CURRENT_DATA_VERSION = 4;
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||
|
||||
public RedStonerMod() {
|
||||
@@ -29,7 +29,7 @@ public final class RedStonerMod implements ModInitializer {
|
||||
RedStonerRegistries.initialize();
|
||||
RedStonerMenus.initialize();
|
||||
RedStonerNetworking.initialize();
|
||||
LOGGER.info("[{}] Computers, Drawer, Particuleur and Storage Terminal ready (data v{}, network v{}).",
|
||||
LOGGER.info("[{}] Computers, Drawer, Particuleur, Storage Terminal and Trophy Panel ready (data v{}, network v{}).",
|
||||
DISPLAY_NAME, CURRENT_DATA_VERSION, RedStonerNetworking.PROTOCOL_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.block;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.TrophyPanelBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingUnit;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.permissions.Permissions;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
import net.minecraft.world.level.ScheduledTickAccess;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.Mirror;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityTicker;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.EnumProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** Wall-mounted, server-authoritative trophy and ranking panel. */
|
||||
public final class TrophyPanelBlock extends BaseEntityBlock {
|
||||
public static final MapCodec<TrophyPanelBlock> CODEC = simpleCodec(TrophyPanelBlock::new);
|
||||
public static final EnumProperty<Direction> FACING = BlockStateProperties.HORIZONTAL_FACING;
|
||||
private static final Map<Direction, VoxelShape> SHAPES = Map.of(
|
||||
Direction.NORTH, Block.box(0.0D, 0.0D, 13.0D, 16.0D, 16.0D, 16.0D),
|
||||
Direction.SOUTH, Block.box(0.0D, 0.0D, 0.0D, 16.0D, 16.0D, 3.0D),
|
||||
Direction.WEST, Block.box(13.0D, 0.0D, 0.0D, 16.0D, 16.0D, 16.0D),
|
||||
Direction.EAST, Block.box(0.0D, 0.0D, 0.0D, 3.0D, 16.0D, 16.0D)
|
||||
);
|
||||
|
||||
public TrophyPanelBlock(BlockBehaviour.Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapCodec<TrophyPanelBlock> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new TrophyPanelBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state,
|
||||
BlockEntityType<T> type) {
|
||||
if (level.isClientSide()) return null;
|
||||
return createTickerHelper(type, RedStonerRegistries.TROPHY_PANEL_BLOCK_ENTITY,
|
||||
TrophyPanelBlockEntity::serverTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.MODEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
Direction facing = context.getClickedFace();
|
||||
if (!facing.getAxis().isHorizontal()) return null;
|
||||
BlockState state = defaultBlockState().setValue(FACING, facing);
|
||||
return state.canSurvive(context.getLevel(), context.getClickedPos()) ? state : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canSurvive(BlockState state, LevelReader level, BlockPos pos) {
|
||||
Direction facing = state.getValue(FACING);
|
||||
BlockPos support = pos.relative(facing.getOpposite());
|
||||
return level.getBlockState(support).isFaceSturdy(level, support, facing);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState updateShape(BlockState state, LevelReader level, ScheduledTickAccess ticks,
|
||||
BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random) {
|
||||
return direction == state.getValue(FACING).getOpposite() && !state.canSurvive(level, pos)
|
||||
? Blocks.AIR.defaultBlockState()
|
||||
: super.updateShape(state, level, ticks, pos, direction, neighborPos, neighborState, random);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
|
||||
return SHAPES.get(state.getValue(FACING));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlacedBy(Level level, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) {
|
||||
super.setPlacedBy(level, pos, state, placer, stack);
|
||||
if (placer instanceof ServerPlayer player
|
||||
&& level.getBlockEntity(pos) instanceof TrophyPanelBlockEntity panel) panel.claim(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos,
|
||||
Player player, InteractionHand hand, BlockHitResult hit) {
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(player instanceof ServerPlayer serverPlayer)
|
||||
|| !(level.getBlockEntity(pos) instanceof TrophyPanelBlockEntity panel)) return InteractionResult.CONSUME;
|
||||
if (player.isSecondaryUseActive()) return cycle(panel, serverPlayer);
|
||||
|
||||
boolean operator = serverPlayer.createCommandSourceStack().permissions()
|
||||
.hasPermission(Permissions.COMMANDS_GAMEMASTER);
|
||||
if (!panel.canConfigure(serverPlayer, operator)) {
|
||||
serverPlayer.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.not_owner"));
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
TrophyPanelBlockEntity.ConfigureResult result = panel.configure(serverPlayer, stack);
|
||||
String key = switch (result) {
|
||||
case SUCCESS -> "configured";
|
||||
case NO_METRIC -> "no_metric";
|
||||
case NEWER_DATA -> "newer_data";
|
||||
};
|
||||
serverPlayer.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel." + key));
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
|
||||
Player player, BlockHitResult hit) {
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(player instanceof ServerPlayer serverPlayer)
|
||||
|| !(level.getBlockEntity(pos) instanceof TrophyPanelBlockEntity panel)) return InteractionResult.CONSUME;
|
||||
if (player.isSecondaryUseActive()) return cycle(panel, serverPlayer);
|
||||
showDetails(panel, serverPlayer);
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
|
||||
builder.add(FACING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState rotate(BlockState state, Rotation rotation) {
|
||||
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState mirror(BlockState state, Mirror mirror) {
|
||||
return state.rotate(mirror.getRotation(state.getValue(FACING)));
|
||||
}
|
||||
|
||||
private static InteractionResult cycle(TrophyPanelBlockEntity panel, ServerPlayer player) {
|
||||
if (!panel.cyclePage()) {
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.newer_data"));
|
||||
return InteractionResult.CONSUME;
|
||||
}
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.page",
|
||||
panel.pageNumber(), panel.pageCount()));
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
|
||||
private static void showDetails(TrophyPanelBlockEntity panel, ServerPlayer player) {
|
||||
TrophyPanelBlockEntity.ClientSnapshot snapshot = panel.visibleSnapshot();
|
||||
RankingUnit unit = snapshot.unit();
|
||||
ItemStack icon = panel.currentPage().icon();
|
||||
Component metric = icon.isEmpty() ? Component.translatable(snapshot.translationKey())
|
||||
: Component.translatable(snapshot.translationKey(), icon.getHoverName());
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.header", metric,
|
||||
panel.pageNumber(), panel.pageCount()));
|
||||
for (TrophyPanelBlockEntity.ClientEntry entry : snapshot.podium()) {
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.podium",
|
||||
entry.rank(), entry.name(), unit.format(entry.value())));
|
||||
}
|
||||
if (snapshot.ownerRank() > 0) {
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.owner_rank",
|
||||
panel.ownerName(), snapshot.ownerRank(), unit.format(snapshot.ownerValue())));
|
||||
} else {
|
||||
player.sendSystemMessage(Component.translatable("message.redstoner.trophy_panel.owner_unranked",
|
||||
panel.ownerName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.block.entity;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.TrophyPanelBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerRegistries;
|
||||
import fr.koka99cab.sanctuary26.redstoner.trophy.TrophyPanelRules;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingMetric;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingSnapshot;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingUnit;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.SanctuaryRankingApi;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.UUIDUtil;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.ClientGamePacketListener;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ItemOwner;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Persistent panel configuration plus a transient, bounded client-visible ranking snapshot. */
|
||||
public final class TrophyPanelBlockEntity extends BlockEntity implements ItemOwner {
|
||||
private static final int DATA_VERSION = 1;
|
||||
private static final int REFRESH_INTERVAL_TICKS = 20;
|
||||
private static final String CLIENT_SNAPSHOT_TAG = "VisibleSnapshot";
|
||||
private static final Codec<Page> PAGE_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.fieldOf("metric").forGetter(Page::metricId),
|
||||
ItemStack.OPTIONAL_CODEC.fieldOf("icon").forGetter(Page::icon)
|
||||
).apply(instance, Page::new));
|
||||
private static final Codec<ClientEntry> CLIENT_ENTRY_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.fieldOf("name").forGetter(ClientEntry::name),
|
||||
Codec.LONG.fieldOf("value").forGetter(ClientEntry::value),
|
||||
Codec.INT.fieldOf("rank").forGetter(ClientEntry::rank)
|
||||
).apply(instance, ClientEntry::new));
|
||||
private static final Codec<ClientSnapshot> CLIENT_SNAPSHOT_CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.fieldOf("metric").forGetter(ClientSnapshot::metricId),
|
||||
Codec.STRING.fieldOf("translation_key").forGetter(ClientSnapshot::translationKey),
|
||||
Codec.STRING.fieldOf("unit").forGetter(ClientSnapshot::unitKey),
|
||||
CLIENT_ENTRY_CODEC.listOf(0, RankingSnapshot.MAX_PODIUM_SIZE).fieldOf("podium")
|
||||
.forGetter(ClientSnapshot::podium),
|
||||
Codec.LONG.fieldOf("owner_value").forGetter(ClientSnapshot::ownerValue),
|
||||
Codec.INT.fieldOf("owner_rank").forGetter(ClientSnapshot::ownerRank)
|
||||
).apply(instance, ClientSnapshot::new));
|
||||
|
||||
private int dataVersion = DATA_VERSION;
|
||||
private UUID owner;
|
||||
private String ownerName = "?";
|
||||
private List<Page> pages = List.of(defaultPage());
|
||||
private int currentPage;
|
||||
private long lastRefreshTick = Long.MIN_VALUE;
|
||||
private ClientSnapshot visibleSnapshot = ClientSnapshot.empty(SanctuaryRankingApi.defaultMetric());
|
||||
|
||||
public TrophyPanelBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(RedStonerRegistries.TROPHY_PANEL_BLOCK_ENTITY, pos, state);
|
||||
}
|
||||
|
||||
public static void serverTick(net.minecraft.world.level.Level level, BlockPos pos, BlockState state,
|
||||
TrophyPanelBlockEntity panel) {
|
||||
if (!(level instanceof ServerLevel serverLevel)) return;
|
||||
long now = level.getGameTime();
|
||||
if (panel.lastRefreshTick != Long.MIN_VALUE && now - panel.lastRefreshTick < REFRESH_INTERVAL_TICKS) return;
|
||||
panel.lastRefreshTick = now;
|
||||
panel.refresh(serverLevel);
|
||||
}
|
||||
|
||||
public void claim(ServerPlayer player) {
|
||||
if (!writable() || owner != null) return;
|
||||
owner = player.getUUID();
|
||||
ownerName = cleanName(player.getGameProfile().name());
|
||||
syncPersistent();
|
||||
}
|
||||
|
||||
public boolean canConfigure(ServerPlayer player, boolean operator) {
|
||||
return writable() && TrophyPanelRules.canConfigure(owner, player.getUUID(), operator);
|
||||
}
|
||||
|
||||
public ConfigureResult configure(ServerPlayer player, ItemStack stack) {
|
||||
if (!writable()) return ConfigureResult.NEWER_DATA;
|
||||
List<RankingMetric> offered = SanctuaryRankingApi.itemMetrics(stack);
|
||||
if (offered.isEmpty()) return ConfigureResult.NO_METRIC;
|
||||
|
||||
List<Identifier> currentIds = pages.stream().map(Page::metricId).toList();
|
||||
List<Identifier> mergedIds = TrophyPanelRules.mergeMetricIds(currentIds,
|
||||
offered.stream().map(RankingMetric::id).toList());
|
||||
List<Page> next = new ArrayList<>();
|
||||
for (Identifier id : mergedIds) {
|
||||
Page existing = pages.stream().filter(page -> page.metricId().equals(id)).findFirst().orElse(null);
|
||||
next.add(existing == null ? new Page(id, stack.copyWithCount(1)) : existing);
|
||||
}
|
||||
pages = List.copyOf(next);
|
||||
currentPage = Math.max(0, pages.indexOf(pages.stream()
|
||||
.filter(page -> offered.stream().anyMatch(metric -> metric.id().equals(page.metricId())))
|
||||
.findFirst().orElse(pages.getFirst())));
|
||||
ownerName = cleanName(player.getGameProfile().name());
|
||||
syncPersistent();
|
||||
if (level instanceof ServerLevel serverLevel) refresh(serverLevel);
|
||||
return ConfigureResult.SUCCESS;
|
||||
}
|
||||
|
||||
public boolean cyclePage() {
|
||||
if (!writable() || pages.isEmpty()) return false;
|
||||
currentPage = TrophyPanelRules.nextPage(currentPage, pages.size());
|
||||
syncPersistent();
|
||||
if (level instanceof ServerLevel serverLevel) refresh(serverLevel);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Page currentPage() {
|
||||
return pages.get(Math.clamp(currentPage, 0, pages.size() - 1));
|
||||
}
|
||||
|
||||
public int pageNumber() {
|
||||
return currentPage + 1;
|
||||
}
|
||||
|
||||
public int pageCount() {
|
||||
return pages.size();
|
||||
}
|
||||
|
||||
public String ownerName() {
|
||||
return ownerName;
|
||||
}
|
||||
|
||||
public ClientSnapshot visibleSnapshot() {
|
||||
return visibleSnapshot;
|
||||
}
|
||||
|
||||
private void refresh(ServerLevel level) {
|
||||
Page page = currentPage();
|
||||
RankingSnapshot snapshot = SanctuaryRankingApi.snapshot(level.getServer(), page.metricId(), owner);
|
||||
ClientSnapshot next = ClientSnapshot.from(snapshot);
|
||||
if (next.equals(visibleSnapshot)) return;
|
||||
visibleSnapshot = next;
|
||||
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), Block.UPDATE_CLIENTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
output.putInt("DataVersion", dataVersion);
|
||||
if (owner != null) output.store("Owner", UUIDUtil.CODEC, owner);
|
||||
output.putString("OwnerName", ownerName);
|
||||
output.store("Pages", PAGE_CODEC.listOf(0, TrophyPanelRules.MAX_PAGES), pages);
|
||||
output.putInt("CurrentPage", currentPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
dataVersion = Math.max(DATA_VERSION, input.getIntOr("DataVersion", 0));
|
||||
owner = input.read("Owner", UUIDUtil.CODEC).orElse(null);
|
||||
ownerName = cleanName(input.getStringOr("OwnerName", "?"));
|
||||
pages = input.read("Pages", PAGE_CODEC.listOf(0, TrophyPanelRules.MAX_PAGES))
|
||||
.map(TrophyPanelBlockEntity::sanitizePages).orElseGet(() -> List.of(defaultPage()));
|
||||
currentPage = Math.clamp(input.getIntOr("CurrentPage", 0), 0, pages.size() - 1);
|
||||
visibleSnapshot = input.read(CLIENT_SNAPSHOT_TAG, CLIENT_SNAPSHOT_CODEC)
|
||||
.orElseGet(() -> ClientSnapshot.empty(metricFor(currentPage().metricId())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Packet<ClientGamePacketListener> getUpdatePacket() {
|
||||
return ClientboundBlockEntityDataPacket.create(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag getUpdateTag(HolderLookup.Provider lookup) {
|
||||
CompoundTag tag = saveCustomOnly(lookup);
|
||||
tag.store(CLIENT_SNAPSHOT_TAG, CLIENT_SNAPSHOT_CODEC, visibleSnapshot);
|
||||
return tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public net.minecraft.world.level.Level level() {
|
||||
return level;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 position() {
|
||||
return Vec3.atCenterOf(worldPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getVisualRotationYInDegrees() {
|
||||
return getBlockState().getValue(TrophyPanelBlock.FACING).toYRot();
|
||||
}
|
||||
|
||||
private void syncPersistent() {
|
||||
setChanged();
|
||||
if (level != null) level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), Block.UPDATE_CLIENTS);
|
||||
}
|
||||
|
||||
private boolean writable() {
|
||||
return dataVersion <= DATA_VERSION;
|
||||
}
|
||||
|
||||
private static List<Page> sanitizePages(List<Page> loaded) {
|
||||
List<Page> valid = loaded.stream().filter(page -> SanctuaryRankingApi.metric(page.metricId()).isPresent())
|
||||
.limit(TrophyPanelRules.MAX_PAGES).toList();
|
||||
return valid.isEmpty() ? List.of(defaultPage()) : List.copyOf(valid);
|
||||
}
|
||||
|
||||
private static Page defaultPage() {
|
||||
return new Page(SanctuaryRankingApi.defaultMetric().id(), ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
private static RankingMetric metricFor(Identifier id) {
|
||||
return SanctuaryRankingApi.metric(id).orElse(SanctuaryRankingApi.defaultMetric());
|
||||
}
|
||||
|
||||
private static String cleanName(String name) {
|
||||
if (name == null || name.isBlank()) return "?";
|
||||
String clean = name.strip();
|
||||
return clean.length() > 32 ? clean.substring(0, 32) : clean;
|
||||
}
|
||||
|
||||
public enum ConfigureResult { SUCCESS, NO_METRIC, NEWER_DATA }
|
||||
|
||||
public record Page(Identifier metricId, ItemStack icon) {
|
||||
public Page {
|
||||
metricId = metricId == null ? SanctuaryRankingApi.defaultMetric().id() : metricId;
|
||||
// The registry item is enough for an icon. Player components (containers, names, lore) never persist or sync.
|
||||
icon = icon == null || icon.isEmpty() ? ItemStack.EMPTY : new ItemStack(icon.getItem());
|
||||
}
|
||||
}
|
||||
|
||||
public record ClientEntry(String name, long value, int rank) {
|
||||
public ClientEntry {
|
||||
name = cleanName(name);
|
||||
value = Math.max(0L, value);
|
||||
rank = Math.max(0, rank);
|
||||
}
|
||||
}
|
||||
|
||||
public record ClientSnapshot(Identifier metricId, String translationKey, String unitKey,
|
||||
List<ClientEntry> podium, long ownerValue, int ownerRank) {
|
||||
public ClientSnapshot {
|
||||
translationKey = translationKey == null || translationKey.isBlank()
|
||||
? "metric.sanctuary.ranking.unknown" : translationKey;
|
||||
unitKey = RankingUnit.byKey(unitKey).key();
|
||||
podium = podium == null ? List.of() : List.copyOf(podium.stream()
|
||||
.limit(RankingSnapshot.MAX_PODIUM_SIZE).toList());
|
||||
ownerValue = Math.max(0L, ownerValue);
|
||||
ownerRank = Math.max(0, ownerRank);
|
||||
}
|
||||
|
||||
static ClientSnapshot from(RankingSnapshot snapshot) {
|
||||
return new ClientSnapshot(snapshot.metric().id(), snapshot.metric().translationKey(),
|
||||
snapshot.metric().unit().key(), snapshot.podium().stream()
|
||||
.map(entry -> new ClientEntry(entry.playerName(), entry.value(), entry.rank())).toList(),
|
||||
snapshot.owner().value(), snapshot.owner().rank());
|
||||
}
|
||||
|
||||
static ClientSnapshot empty(RankingMetric metric) {
|
||||
return new ClientSnapshot(metric.id(), metric.translationKey(), metric.unit().key(), List.of(), 0L, 0);
|
||||
}
|
||||
|
||||
public RankingUnit unit() {
|
||||
return RankingUnit.byKey(unitKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import fr.koka99cab.sanctuary26.redstoner.client.gui.ComputerScreen;
|
||||
import fr.koka99cab.sanctuary26.redstoner.client.gui.PersonalComputerScreen;
|
||||
import fr.koka99cab.sanctuary26.redstoner.client.gui.StorageTerminalScreen;
|
||||
import fr.koka99cab.sanctuary26.redstoner.client.renderer.DrawerBlockEntityRenderer;
|
||||
import fr.koka99cab.sanctuary26.redstoner.client.renderer.TrophyPanelBlockEntityRenderer;
|
||||
import fr.koka99cab.sanctuary26.redstoner.network.OpenComputerPayload;
|
||||
import fr.koka99cab.sanctuary26.redstoner.network.OpenPersonalComputerPayload;
|
||||
import fr.koka99cab.sanctuary26.redstoner.registry.RedStonerMenus;
|
||||
@@ -19,6 +20,8 @@ public final class RedStonerClient implements ClientModInitializer {
|
||||
public void onInitializeClient() {
|
||||
BlockEntityRendererRegistry.register(RedStonerRegistries.DRAWER_BLOCK_ENTITY, DrawerBlockEntityRenderer::new);
|
||||
BlockEntityRendererRegistry.register(RedStonerRegistries.LEGACY_DRAWER_BLOCK_ENTITY, DrawerBlockEntityRenderer::new);
|
||||
BlockEntityRendererRegistry.register(RedStonerRegistries.TROPHY_PANEL_BLOCK_ENTITY,
|
||||
TrophyPanelBlockEntityRenderer::new);
|
||||
MenuScreens.register(RedStonerMenus.STORAGE_TERMINAL, StorageTerminalScreen::new);
|
||||
ClientPlayNetworking.registerGlobalReceiver(OpenComputerPayload.TYPE, (payload, context) ->
|
||||
context.client().execute(() -> context.client().gui.setScreen(new ComputerScreen(payload))));
|
||||
|
||||
+44
-5
@@ -2,12 +2,15 @@ package fr.koka99cab.sanctuary26.redstoner.client.gui;
|
||||
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalMenu;
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalRules;
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalSort;
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalStatus;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
@@ -28,8 +31,10 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
private static final int SCROLL_WIDTH = 4;
|
||||
private int requestedPage;
|
||||
private EditBox searchBox;
|
||||
private Button sortButton;
|
||||
private String searchValue = "";
|
||||
private String sentSearchValue = "";
|
||||
private StorageTerminalSort sortMode = StorageTerminalSort.ALPHABETICAL;
|
||||
|
||||
public StorageTerminalScreen(StorageTerminalMenu menu, Inventory inventory, Component title) {
|
||||
super(menu, inventory, title, 176, 222);
|
||||
@@ -40,13 +45,16 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
protected void init() {
|
||||
super.init();
|
||||
titleLabelY = 6;
|
||||
searchBox = addRenderableWidget(new EditBox(font, leftPos + 8, topPos + 2, 126, 14,
|
||||
searchBox = addRenderableWidget(new EditBox(font, leftPos + 8, topPos + 2, 96, 14,
|
||||
Component.translatable("screen.redstoner.storage_terminal.search")));
|
||||
searchBox.setMaxLength(StorageTerminalRules.MAX_SEARCH_LENGTH);
|
||||
searchBox.setHint(Component.translatable("screen.redstoner.storage_terminal.search"));
|
||||
searchBox.setValue(searchValue);
|
||||
searchBox.setResponder(this::searchChanged);
|
||||
setInitialFocus(searchBox);
|
||||
sortButton = addRenderableWidget(Button.builder(sortButtonLabel(), button -> requestSort())
|
||||
.bounds(leftPos + 108, topPos + 2, 34, 14)
|
||||
.tooltip(Tooltip.create(sortTooltip()))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,6 +122,10 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
if (searchBox != null && searchBox.isFocused()
|
||||
&& !searchBox.isMouseOver(event.x(), event.y())) {
|
||||
clearSearchFocus();
|
||||
}
|
||||
if (event.button() == 0
|
||||
&& isHovering(SCROLL_X - 2, TERMINAL_TOP, SCROLL_WIDTH + 4, TERMINAL_HEIGHT,
|
||||
event.x(), event.y())) {
|
||||
@@ -127,9 +139,9 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(KeyEvent event) {
|
||||
if (searchBox != null && searchBox.isFocused() && event.key() != 256) {
|
||||
searchBox.keyPressed(event);
|
||||
return true;
|
||||
if (searchBox != null && searchBox.isFocused() && minecraft != null
|
||||
&& minecraft.options.keyInventory.matches(event)) {
|
||||
clearSearchFocus();
|
||||
}
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
@@ -148,6 +160,28 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
}
|
||||
}
|
||||
|
||||
private void requestSort() {
|
||||
sortMode = sortMode.next();
|
||||
requestedPage = 0;
|
||||
sortButton.setMessage(sortButtonLabel());
|
||||
sortButton.setTooltip(Tooltip.create(sortTooltip()));
|
||||
sendMenuButton(StorageTerminalMenu.SORT_BUTTON_BASE + sortMode.ordinal());
|
||||
}
|
||||
|
||||
private Component sortButtonLabel() {
|
||||
return Component.literal(switch (sortMode) {
|
||||
case ALPHABETICAL -> "A→Z";
|
||||
case COUNT_ASCENDING -> "1↑";
|
||||
case COUNT_DESCENDING -> "1↓";
|
||||
});
|
||||
}
|
||||
|
||||
private Component sortTooltip() {
|
||||
return Component.translatable("screen.redstoner.storage_terminal.sort",
|
||||
Component.translatable("screen.redstoner.storage_terminal.sort."
|
||||
+ sortMode.translationSuffix()));
|
||||
}
|
||||
|
||||
private void searchChanged(String value) {
|
||||
String previous = sentSearchValue;
|
||||
searchValue = value;
|
||||
@@ -173,6 +207,11 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
}
|
||||
}
|
||||
|
||||
private void clearSearchFocus() {
|
||||
searchBox.setFocused(false);
|
||||
setFocused(null);
|
||||
}
|
||||
|
||||
private void extractScrollBar(GuiGraphicsExtractor graphics) {
|
||||
int x = leftPos + SCROLL_X;
|
||||
int y = topPos + TERMINAL_TOP;
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.client.renderer;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.TrophyPanelBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.TrophyPanelBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.client.renderer.state.TrophyPanelRenderState;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.api.ranking.RankingUnit;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
import net.minecraft.world.item.ItemDisplayContext;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class TrophyPanelBlockEntityRenderer
|
||||
implements BlockEntityRenderer<TrophyPanelBlockEntity, TrophyPanelRenderState> {
|
||||
private final ItemModelResolver itemModelResolver;
|
||||
private final Font font;
|
||||
|
||||
public TrophyPanelBlockEntityRenderer(BlockEntityRendererProvider.Context context) {
|
||||
itemModelResolver = context.itemModelResolver();
|
||||
font = context.font();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrophyPanelRenderState createRenderState() {
|
||||
return new TrophyPanelRenderState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(TrophyPanelBlockEntity panel, TrophyPanelRenderState state,
|
||||
float tickProgress, Vec3 cameraPos, ModelFeatureRenderer.CrumblingOverlay crumblingOverlay) {
|
||||
BlockEntityRenderer.super.extractRenderState(panel, state, tickProgress, cameraPos, crumblingOverlay);
|
||||
state.facing = panel.getBlockState().getValue(TrophyPanelBlock.FACING);
|
||||
state.icon.clear();
|
||||
ItemStack icon = panel.currentPage().icon();
|
||||
state.hasIcon = !icon.isEmpty();
|
||||
if (state.hasIcon) {
|
||||
itemModelResolver.updateForTopItem(state.icon, icon, ItemDisplayContext.FIXED,
|
||||
panel.level(), panel, (int) panel.getBlockPos().asLong());
|
||||
}
|
||||
|
||||
TrophyPanelBlockEntity.ClientSnapshot snapshot = panel.visibleSnapshot();
|
||||
RankingUnit unit = snapshot.unit();
|
||||
state.metric = state.hasIcon ? Component.translatable(snapshot.translationKey(), icon.getHoverName())
|
||||
: Component.translatable(snapshot.translationKey());
|
||||
if (snapshot.podium().isEmpty()) {
|
||||
state.champion = Component.translatable("block.redstoner.trophy_panel.no_score");
|
||||
} else {
|
||||
TrophyPanelBlockEntity.ClientEntry champion = snapshot.podium().getFirst();
|
||||
state.champion = Component.translatable("block.redstoner.trophy_panel.champion",
|
||||
champion.name(), unit.format(champion.value()));
|
||||
}
|
||||
state.owner = snapshot.ownerRank() > 0
|
||||
? Component.translatable("block.redstoner.trophy_panel.owner_rank", panel.ownerName(),
|
||||
snapshot.ownerRank(), unit.format(snapshot.ownerValue()))
|
||||
: Component.translatable("block.redstoner.trophy_panel.owner_unranked", panel.ownerName());
|
||||
state.page = Component.literal(panel.pageNumber() + "/" + panel.pageCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(TrophyPanelRenderState state, PoseStack poseStack,
|
||||
SubmitNodeCollector submitNodeCollector, CameraRenderState cameraRenderState) {
|
||||
float yRotation = -state.facing.toYRot();
|
||||
if (state.hasIcon) submitIcon(state, poseStack, submitNodeCollector, yRotation);
|
||||
submitText(state.metric, 0.72F, 0.0065F, state, poseStack, submitNodeCollector, yRotation, 0xFFE8B4);
|
||||
submitText(state.champion, 0.52F, 0.0060F, state, poseStack, submitNodeCollector, yRotation, 0xFFFFFF);
|
||||
submitText(state.owner, 0.36F, 0.0055F, state, poseStack, submitNodeCollector, yRotation, 0xB8D9FF);
|
||||
submitText(state.page, 0.20F, 0.0050F, state, poseStack, submitNodeCollector, yRotation, 0xA0A0A0);
|
||||
}
|
||||
|
||||
private void submitIcon(TrophyPanelRenderState state, PoseStack poseStack,
|
||||
SubmitNodeCollector submitNodeCollector, float yRotation) {
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(0.5F, 0.89F, 0.5F);
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(yRotation));
|
||||
poseStack.translate(0.0F, 0.0F, -0.33F);
|
||||
poseStack.scale(0.18F, 0.18F, 0.18F);
|
||||
state.icon.submit(poseStack, submitNodeCollector, state.lightCoords, OverlayTexture.NO_OVERLAY, 0);
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
private void submitText(Component component, float y, float scale, TrophyPanelRenderState state,
|
||||
PoseStack poseStack, SubmitNodeCollector submitNodeCollector, float yRotation, int color) {
|
||||
FormattedCharSequence text = component.getVisualOrderText();
|
||||
float x = -font.width(text) / 2.0F;
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(0.5F, y, 0.5F);
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(yRotation));
|
||||
poseStack.translate(0.0F, 0.0F, -0.331F);
|
||||
poseStack.scale(scale, -scale, scale);
|
||||
submitNodeCollector.submitText(poseStack, x, 0.0F, text, false, Font.DisplayMode.POLYGON_OFFSET,
|
||||
color, state.lightCoords, 0, 0x30181818);
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.client.renderer.state;
|
||||
|
||||
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
|
||||
import net.minecraft.client.renderer.item.ItemStackRenderState;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public final class TrophyPanelRenderState extends BlockEntityRenderState {
|
||||
public final ItemStackRenderState icon = new ItemStackRenderState();
|
||||
public Direction facing = Direction.NORTH;
|
||||
public boolean hasIcon;
|
||||
public Component metric = Component.empty();
|
||||
public Component champion = Component.empty();
|
||||
public Component owner = Component.empty();
|
||||
public Component page = Component.empty();
|
||||
}
|
||||
+13
-1
@@ -6,10 +6,12 @@ import fr.koka99cab.sanctuary26.redstoner.block.DrawerBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.PersonalComputerBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.ParticuleurBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.StorageTerminalBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.TrophyPanelBlock;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.ComputerBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.DrawerBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.PersonalComputerBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.ParticuleurBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.redstoner.block.entity.TrophyPanelBlockEntity;
|
||||
import java.util.function.Function;
|
||||
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
|
||||
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;
|
||||
@@ -71,6 +73,12 @@ public final class RedStonerRegistries {
|
||||
BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(4.0F, 6.0F).sound(SoundType.METAL)
|
||||
.requiresCorrectToolForDrops().noOcclusion().setId(blockKey(RedStonerMod.id("storage_terminal")))
|
||||
);
|
||||
public static final TrophyPanelBlock TROPHY_PANEL = registerBlock(
|
||||
RedStonerMod.id("trophy_panel"),
|
||||
TrophyPanelBlock::new,
|
||||
BlockBehaviour.Properties.of().mapColor(MapColor.WOOD).strength(2.0F).sound(SoundType.WOOD)
|
||||
.requiresCorrectToolForDrops().noOcclusion().setId(blockKey(RedStonerMod.id("trophy_panel")))
|
||||
);
|
||||
|
||||
public static final ComputerBlock LEGACY_EIGHT_BIT_COMPUTER = registerBlock(
|
||||
LEGACY_COMPUTER_ID, ComputerBlock::new, computerProperties(LEGACY_COMPUTER_ID, MapColor.COLOR_RED));
|
||||
@@ -92,6 +100,7 @@ public final class RedStonerRegistries {
|
||||
public static final Item PARTICULEUR_ITEM = registerBlockItem(RedStonerMod.id("particuleur"), PARTICULEUR);
|
||||
public static final Item STORAGE_TERMINAL_ITEM = registerBlockItem(
|
||||
RedStonerMod.id("storage_terminal"), STORAGE_TERMINAL);
|
||||
public static final Item TROPHY_PANEL_ITEM = registerBlockItem(RedStonerMod.id("trophy_panel"), TROPHY_PANEL);
|
||||
|
||||
public static final Item LEGACY_EIGHT_BIT_COMPUTER_ITEM = registerBlockItem(LEGACY_COMPUTER_ID, LEGACY_EIGHT_BIT_COMPUTER);
|
||||
public static final Item LEGACY_CONTROLLER_ITEM = registerBlockItem(LEGACY_CONTROLLER_ID, LEGACY_CONTROLLER);
|
||||
@@ -115,6 +124,8 @@ public final class RedStonerRegistries {
|
||||
RedStonerMod.id("particuleur"), ParticuleurBlockEntity::new, PARTICULEUR);
|
||||
public static final BlockEntityType<ParticuleurBlockEntity> LEGACY_PARTICULEUR_BLOCK_ENTITY = registerBlockEntity(
|
||||
LEGACY_PARTICULEUR_ID, ParticuleurBlockEntity::new, LEGACY_PARTICULEUR);
|
||||
public static final BlockEntityType<TrophyPanelBlockEntity> TROPHY_PANEL_BLOCK_ENTITY = registerBlockEntity(
|
||||
RedStonerMod.id("trophy_panel"), TrophyPanelBlockEntity::new, TROPHY_PANEL);
|
||||
|
||||
public static final CreativeModeTab CREATIVE_TAB = Registry.register(
|
||||
BuiltInRegistries.CREATIVE_MODE_TAB,
|
||||
@@ -127,6 +138,7 @@ public final class RedStonerRegistries {
|
||||
output.accept(CONTROLLER_ITEM);
|
||||
output.accept(PARTICULEUR_ITEM);
|
||||
output.accept(STORAGE_TERMINAL_ITEM);
|
||||
output.accept(TROPHY_PANEL_ITEM);
|
||||
})
|
||||
.build()
|
||||
);
|
||||
@@ -142,7 +154,7 @@ public final class RedStonerRegistries {
|
||||
}
|
||||
initialized = true;
|
||||
registerDrawerAttack();
|
||||
RedStonerMod.LOGGER.info("[{}] Registered six active blocks and five legacy block aliases.", RedStonerMod.DISPLAY_NAME);
|
||||
RedStonerMod.LOGGER.info("[{}] Registered seven active blocks and five legacy block aliases.", RedStonerMod.DISPLAY_NAME);
|
||||
}
|
||||
|
||||
public static boolean isController(BlockState state) {
|
||||
|
||||
+6
-1
@@ -27,6 +27,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
public static final int PAGE_BUTTON_BASE = 10;
|
||||
public static final int SEARCH_CLEAR_BUTTON = 100;
|
||||
public static final int SEARCH_BACKSPACE_BUTTON = 101;
|
||||
public static final int SORT_BUTTON_BASE = 200;
|
||||
public static final int SEARCH_CODEPOINT_BUTTON_BASE = 1_000;
|
||||
private static final int PLAYER_INVENTORY_Y = 140;
|
||||
private static final int REFRESH_INTERVAL = 20;
|
||||
@@ -49,6 +50,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
private final StorageTerminalLayout layout = new StorageTerminalLayout();
|
||||
private List<StorageTerminalInventory.Entry> entries = List.of();
|
||||
private String searchQuery = "";
|
||||
private StorageTerminalSort sortMode = StorageTerminalSort.ALPHABETICAL;
|
||||
private int refreshCooldown = REFRESH_INTERVAL;
|
||||
|
||||
public StorageTerminalMenu(int containerId, Inventory inventory) {
|
||||
@@ -131,6 +133,9 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
if (searchQuery.isEmpty()) return true;
|
||||
searchQuery = searchQuery.substring(0,
|
||||
searchQuery.offsetByCodePoints(searchQuery.length(), -1));
|
||||
} else if (buttonId >= SORT_BUTTON_BASE
|
||||
&& buttonId < SORT_BUTTON_BASE + StorageTerminalSort.values().length) {
|
||||
sortMode = StorageTerminalSort.values()[buttonId - SORT_BUTTON_BASE];
|
||||
} else {
|
||||
int codePoint = buttonId - SEARCH_CODEPOINT_BUTTON_BASE;
|
||||
if (!isSearchCodePoint(codePoint)) return false;
|
||||
@@ -211,7 +216,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
if (serverPlayer == null || terminalPos == null) return;
|
||||
discovery = StorageTerminalNetwork.discover(serverPlayer, terminalPos);
|
||||
List<StorageTerminalInventory.Entry> liveEntries =
|
||||
StorageTerminalNetwork.filteredEntries(discovery, searchQuery);
|
||||
sortMode.order(StorageTerminalNetwork.filteredEntries(discovery, searchQuery));
|
||||
layout.update(liveEntries, resetOrder);
|
||||
|
||||
int page = discovery.status() == StorageTerminalStatus.READY
|
||||
|
||||
+21
-4
@@ -5,12 +5,16 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.ShulkerBoxBlock;
|
||||
|
||||
/** Server-side matching for the bounded terminal search field. */
|
||||
public final class StorageTerminalSearch {
|
||||
private static final int SHULKER_SLOT_COUNT = 27;
|
||||
private static final Pattern COMBINING_MARKS = Pattern.compile("\\p{M}+");
|
||||
private static final Pattern NON_ALPHANUMERIC = Pattern.compile("[^\\p{L}\\p{N}]+");
|
||||
|
||||
@@ -38,14 +42,27 @@ public final class StorageTerminalSearch {
|
||||
}
|
||||
|
||||
private static boolean matches(ItemStack stack, String[] terms) {
|
||||
StringBuilder searchable = new StringBuilder(stack.getHoverName().getString());
|
||||
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
|
||||
if (id != null) searchable.append(' ').append(id);
|
||||
searchable.append(' ').append(stack.getItem().getDescriptionId());
|
||||
StringBuilder searchable = new StringBuilder();
|
||||
appendSearchableText(searchable, stack);
|
||||
if (stack.getItem() instanceof BlockItem blockItem
|
||||
&& blockItem.getBlock() instanceof ShulkerBoxBlock) {
|
||||
var contents = stack.get(DataComponents.CONTAINER);
|
||||
if (contents != null) {
|
||||
contents.nonEmptyItemCopyStream().limit(SHULKER_SLOT_COUNT).forEach(contained ->
|
||||
appendSearchableText(searchable, contained));
|
||||
}
|
||||
}
|
||||
String normalized = normalize(searchable.toString());
|
||||
for (String term : terms) {
|
||||
if (!normalized.contains(term)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void appendSearchableText(StringBuilder searchable, ItemStack stack) {
|
||||
searchable.append(' ').append(stack.getHoverName().getString());
|
||||
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
|
||||
if (id != null) searchable.append(' ').append(id);
|
||||
searchable.append(' ').append(stack.getItem().getDescriptionId());
|
||||
}
|
||||
}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.storage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Explicit server-side ordering modes for terminal entries. */
|
||||
public enum StorageTerminalSort {
|
||||
ALPHABETICAL("alphabetical"),
|
||||
COUNT_ASCENDING("count_ascending"),
|
||||
COUNT_DESCENDING("count_descending");
|
||||
|
||||
private final String translationSuffix;
|
||||
|
||||
StorageTerminalSort(String translationSuffix) {
|
||||
this.translationSuffix = translationSuffix;
|
||||
}
|
||||
|
||||
public String translationSuffix() {
|
||||
return translationSuffix;
|
||||
}
|
||||
|
||||
public StorageTerminalSort next() {
|
||||
StorageTerminalSort[] values = values();
|
||||
return values[(ordinal() + 1) % values.length];
|
||||
}
|
||||
|
||||
public List<StorageTerminalInventory.Entry> order(
|
||||
List<StorageTerminalInventory.Entry> entries) {
|
||||
List<StorageTerminalInventory.Entry> ordered = new ArrayList<>(entries);
|
||||
ordered.sort(comparator());
|
||||
return List.copyOf(ordered);
|
||||
}
|
||||
|
||||
private Comparator<StorageTerminalInventory.Entry> comparator() {
|
||||
Comparator<StorageTerminalInventory.Entry> alphabetical = Comparator
|
||||
.comparing((StorageTerminalInventory.Entry entry) -> alphabeticalName(entry.stack()))
|
||||
.thenComparing(entry -> itemId(entry.stack()));
|
||||
return switch (this) {
|
||||
case ALPHABETICAL -> alphabetical;
|
||||
case COUNT_ASCENDING -> Comparator
|
||||
.comparingInt(StorageTerminalInventory.Entry::total).thenComparing(alphabetical);
|
||||
case COUNT_DESCENDING -> Comparator
|
||||
.comparingInt(StorageTerminalInventory.Entry::total).reversed().thenComparing(alphabetical);
|
||||
};
|
||||
}
|
||||
|
||||
private static String alphabeticalName(ItemStack stack) {
|
||||
return StorageTerminalSearch.normalize(stack.getHoverName().getString());
|
||||
}
|
||||
|
||||
private static String itemId(ItemStack stack) {
|
||||
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
|
||||
return id == null ? "" : id.toString();
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.trophy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Pure bounded rules shared by the trophy panel and its smoke tests. */
|
||||
public final class TrophyPanelRules {
|
||||
public static final int MAX_PAGES = 16;
|
||||
|
||||
private TrophyPanelRules() {}
|
||||
|
||||
public static boolean canConfigure(UUID owner, UUID actor, boolean operator) {
|
||||
return operator || owner != null && owner.equals(actor);
|
||||
}
|
||||
|
||||
public static int nextPage(int currentPage, int pageCount) {
|
||||
return pageCount <= 0 ? 0 : Math.floorMod(currentPage + 1, pageCount);
|
||||
}
|
||||
|
||||
public static List<Identifier> mergeMetricIds(List<Identifier> current, List<Identifier> offered) {
|
||||
LinkedHashSet<Identifier> merged = new LinkedHashSet<>();
|
||||
if (current != null) merged.addAll(current);
|
||||
if (offered != null) merged.addAll(offered);
|
||||
return List.copyOf(new ArrayList<>(merged).subList(0, Math.min(MAX_PAGES, merged.size())));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=north": { "model": "redstoner:block/trophy_panel" },
|
||||
"facing=east": { "model": "redstoner:block/trophy_panel", "y": 90 },
|
||||
"facing=south": { "model": "redstoner:block/trophy_panel", "y": 180 },
|
||||
"facing=west": { "model": "redstoner:block/trophy_panel", "y": 270 }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user