Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00d175cb98 | ||
|
|
a7bdfe6998 | ||
|
|
f9c2ce8835 | ||
|
|
3d56d7de7f | ||
|
|
f2ac23b4f4 | ||
|
|
9c096b2563 | ||
|
|
bc27606b3e | ||
|
|
8531bb3476 | ||
|
|
8e9b8fbd62 | ||
|
|
308c0dc2ff | ||
|
|
50aaa5c135 | ||
|
|
cf5699cfe1 | ||
|
|
d9979100a9 | ||
|
|
5f3d110773 |
@@ -6,6 +6,57 @@ Créer un monde **Sanctuary** en Petit, Moyen ou Grand, puis activer les ancres
|
||||
correspondantes. Le module Sanctuary Test n’est pas requis.
|
||||
[Intégration et vérification du pack beta.224](docs/four-islands-base224.md).
|
||||
|
||||
## beta.235 — Essai réversible du menu Pause
|
||||
|
||||
[Disposition adaptative](docs/pause-columns-beta235.md) : avant Atlas, Aventure
|
||||
à gauche et Communauté à droite, avec Partie / Game juste dessous. Avec Atlas,
|
||||
une colonne étroite à gauche, Communauté → Aventure → Game, carte au centre.
|
||||
Bloc centré verticalement, libellés de navigation entiers et ordre clavier
|
||||
adapté. Parcours Pause et Story Vulkan FR/EN GUI 2/3/4, `check build assemblePack`
|
||||
et quatre GameTests ciblés réussis.
|
||||
Le commit de disposition `a7bdfe6` peut être annulé indépendamment de la
|
||||
livraison. [MRpack local beta.235](build/Sanctuary-beta.235.mrpack) vérifié,
|
||||
sans publication ni mise à jour personnelle. Le rendu reste à apprécier.
|
||||
|
||||
## beta.234 — Story, recherche et filtres
|
||||
|
||||
[Premier menu Story](docs/story-beta234.md), sous Progression dans Aventure :
|
||||
100 histoires inconnues et désactivées, recherche instantanée et filtre
|
||||
Toutes/Découvertes/Inconnues, cadre de Discovery et retour à Pause. Saisie et
|
||||
défilement conservés au redimensionnement ; trois boutons Aventure centrés
|
||||
avec ou sans Atlas. [Guide](docs/story/README.md) et
|
||||
[modèle Markdown](docs/story/TEMPLATE.md) pour les futurs récits personnels.
|
||||
Textes et déblocages restent à intégrer. `check build assemblePack`, quatre
|
||||
GameTests requis et parcours Vulkan FR/EN GUI 2/3/4 réussis.
|
||||
[MRpack local beta.234](build/Sanctuary-beta.234.mrpack) vérifié ; aucune
|
||||
publication ni mise à jour d'installation personnelle.
|
||||
|
||||
## beta.233 — Navigation centrée et rédaction simplifiée
|
||||
|
||||
[Ajustements du menu et des formulaires](docs/pause-community-beta233.md) : même
|
||||
ordre avec et sans carte, Communauté (Habitant/Faction/Battle) en haut,
|
||||
Aventure (Discovery/Progression) au centre et Partie (options/sortie) en bas,
|
||||
sans barre sous Communauté. Notice Board sans choix de catégorie, notices
|
||||
uniformes, demandes et récompenses sous le texte, puis Publish. Photo et
|
||||
Choose a screenshot de la Gazette avant le titre. Brouillons et anciennes
|
||||
catégories conservés ; parcours de publication et essais Vulkan FR/EN réussis.
|
||||
`check build assemblePack` avec la suite menus : quatre GameTests requis et
|
||||
contrôles communautaires réussis. [MRpack local beta.233](build/Sanctuary-beta.233.mrpack)
|
||||
vérifié ; aucune installation personnelle ni mise à jour du canal.
|
||||
|
||||
## beta.232 — Session solo et progression
|
||||
|
||||
[Réglages et correctifs](docs/solo-gameplay-beta232.md) : fuseau persistant via
|
||||
`/sanctuary time timezone Europe/Paris` ou `auto`, disponible au propriétaire
|
||||
solo sans commandes opérateur. Portée de Construction de 3 à 7 blocs, contour
|
||||
et pose cohérents ; vein mining maintenu enchaînant sur le bloc regardé.
|
||||
Aucun diamant généré par les coffres de l'île initiale ; inventaires déjà
|
||||
remplis et terrain conservés. Avant Atlas : Gazette à gauche, huit boutons
|
||||
centrés, Notice Board à droite, aucune World Map. Build, 26 GameTests requis
|
||||
et essais client Vulkan FR/EN réussis.
|
||||
[MRpack local beta.232](build/Sanctuary-beta.232.mrpack) vérifié ; aucune
|
||||
installation personnelle ni mise à jour du canal.
|
||||
|
||||
## beta.231 — Sous-bois pâles et archipel complet
|
||||
|
||||
[Correctif et visite](docs/diagonal-understory-beta231.md) : herbes, fleurs,
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# Backlog Sanctuary
|
||||
|
||||
## PAUSE-235 — groupes côte à côte avant Atlas — essai livré localement
|
||||
|
||||
Branche `codex/pause-columns-beta235`, base beta.234. Avant Atlas : Aventure à
|
||||
gauche, Communauté à droite et Game juste dessous, bloc centré. Avec Atlas :
|
||||
une colonne étroite à gauche dans l'ordre Communauté → Aventure → Game,
|
||||
carte centrale et fils à droite. [Contrat et vérifications](pause-columns-beta235.md),
|
||||
parcours Vulkan Pause et Story FR/EN GUI 2/3/4, build, quatre GameTests ciblés
|
||||
et MRpack local vérifiés ; rendu à apprécier.
|
||||
Commit de disposition `a7bdfe6` séparé des métadonnées pour permettre son retour.
|
||||
|
||||
## STORY-234 — recueil personnel avec recherche — livré localement
|
||||
|
||||
Branche `codex/story-beta234`, base beta.233. Story sous Progression dans
|
||||
Aventure ; 100 histoires inconnues, barre de recherche et filtre de découverte.
|
||||
Cadre commun avec Discovery, état de recherche et défilement conservés au
|
||||
redimensionnement, accès avant et après Atlas. [Résultat et vérifications](story-beta234.md) :
|
||||
build, quatre GameTests requis et parcours Vulkan FR/EN GUI 2/3/4 réussis.
|
||||
[Modèle Markdown](story/TEMPLATE.md) pour les textes de l'auteur.
|
||||
Les récits et leurs conditions de découverte restent des tickets suivants ;
|
||||
aucun moteur de quête, format persistant ou déploiement ajouté.
|
||||
|
||||
## WG-RESTORE-191 — annuler les plateaux et leurs affleurements
|
||||
|
||||
La visite beta.190 ne valide pas le relief corrigé. [Retour exact à la base
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# PAUSE-235 — Aventure et Communauté côte à côte
|
||||
|
||||
Branche d'essai `codex/pause-columns-beta235`, base beta.234, commit `f9c2ce8`,
|
||||
Minecraft 26.3. Le rendu reste à apprécier par le créateur.
|
||||
|
||||
## Disposition et réversibilité
|
||||
|
||||
Avant Atlas, Aventure et Communauté occupent deux colonnes au même niveau :
|
||||
Discovery, Progression et Story à gauche ; Habitant, Faction et Battle à droite.
|
||||
Partie / Game suit juste dessous avec Options, World Options et la sortie
|
||||
native. L'ensemble est centré verticalement dans le corps de Pause, avec de
|
||||
l'espace sous Game ; Reprendre la partie reste dans le pied de page.
|
||||
|
||||
Sans Atlas, ce bloc est centré entre Gazette à gauche et Notice Board à droite.
|
||||
Avec Atlas, une seule colonne étroite est serrée à gauche : Communauté, puis
|
||||
Aventure, puis Partie / Game, selon l'ordre confirmé par le créateur. La carte
|
||||
garde la région centrale et les deux fils communautaires restent à droite.
|
||||
Le bloc complet reste centré verticalement. La largeur minimale des boutons
|
||||
de navigation tient compte des libellés localisés et de leur marge native.
|
||||
Aux petites hauteurs, boutons et espacements se compactent. Tab suit l'ordre
|
||||
visuel : par rangée de gauche à droite avant Atlas, puis de haut en bas avec
|
||||
Atlas. Les libellés FR/EN existants sont réutilisés.
|
||||
|
||||
Le changement de disposition et les assertions correspondantes sont isolés
|
||||
dans le commit `a7bdfe6`, séparé de la livraison et de ses métadonnées. La branche
|
||||
`codex/story-beta234` conserve la disposition précédente. Un retour s'effectue
|
||||
avec `git revert a7bdfe6` ; une éventuelle nouvelle
|
||||
livraison doit prendre le numéro beta suivant, sans diminuer le compteur.
|
||||
L'application inverse du patch a été vérifiée avec `git apply --reverse --check`,
|
||||
sans modifier les fichiers de travail.
|
||||
|
||||
Ce ticket ne change ni génération, sauvegarde, données communautaires, protocole,
|
||||
conditions de progression ou contenus Story. Aucune installation personnelle
|
||||
ou publication n'est demandée.
|
||||
|
||||
## Vérifications
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryPause233ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryStory234ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
|
||||
./gradlew check build assemblePack -PsanctuaryFocusedTests=menus -PsanctuaryAtlasOnly=true
|
||||
```
|
||||
|
||||
Le parcours Pause final réussit en 1 min 22 s (`PAUSE233_PASS`), dans un monde
|
||||
plat neuf de développement, Survie sans commandes. FR/EN, GUI 2/3/4, fenêtre
|
||||
1280 × 960, avant et après Atlas : six actions Sanctuary, alignement et absence
|
||||
de chevauchement, largeur des libellés avec la marge native, ordre de Tab et
|
||||
Game sous les groupes, avec de l'espace avant le pied. Le test attend désormais
|
||||
un créneau de requête communautaire pour son amorçage et envoie le code de
|
||||
raccourci natif de Tab, requis par Minecraft 26.3.
|
||||
|
||||
Le parcours communautaire existant passe également : publication et photo,
|
||||
retours des sélecteurs, brouillons, édition d'une ancienne catégorie, inventaire
|
||||
réel conservé et aucun changement du fichier communautaire pendant les lectures.
|
||||
La configuration temporaire est restaurée à la fin du banc.
|
||||
Log : `build/pause235-client-vulkan-final.log`. 38 captures finales copiées dans
|
||||
`build/pause235-screenshots/`. Captures inspectées : sans Atlas FR/4, avec Atlas
|
||||
FR/2 et EN/4. Vulkan confirmé : MoltenVK 1.4.2 sur Apple M1.
|
||||
|
||||
Le parcours Story final réussit en 29 s (`STORY234_PASS`), avec les mêmes
|
||||
langues, GUI et états Atlas. Accès depuis les deux dispositions de Pause,
|
||||
100 entrées désactivées, recherche/filtres, saisie continue et focus, molette,
|
||||
redimensionnement à 1344 × 1008 et retours Échap / Terminé vérifiés. Le record
|
||||
de progression serveur ne change pas. Log :
|
||||
`build/pause235-story-client-vulkan-final.log` ; 21 captures copiées dans
|
||||
`build/pause235-story-screenshots/`.
|
||||
|
||||
Aux GUI étroits, les longs libellés natifs des options/sortie et les titres des
|
||||
fils gardent leur défilement ou découpage existant ; les boutons de navigation
|
||||
Sanctuary montrent leur libellé entier. Les infobulles des actions Game sont
|
||||
conservées. Aucun essai OpenGL, Windows ou monde personnel n'est revendiqué.
|
||||
|
||||
Le contrôle `check build assemblePack` réussit en 3 min 1 s : quatre GameTests
|
||||
requis et contrôles purs du dépôt, dont les quatre contrôles communautaires
|
||||
fichier/photos/demandes/recherche. Log : `build/pause235-check-build-pack.log`.
|
||||
Le banc menus conserve sa sélection Atlas existante ; le conflit de générateur
|
||||
sur le chemin natif par défaut, documenté en beta.232, reste hors de ce ticket.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
`mod_version`, `pack_version` et `packwiz/pack.toml` sont alignés sur beta.235.
|
||||
Le commit de livraison conserve ces métadonnées et cette documentation séparées
|
||||
du commit de disposition.
|
||||
|
||||
Export [Sanctuary-beta.235.mrpack](../build/Sanctuary-beta.235.mrpack),
|
||||
12 776 222 octets : intégrité ZIP, Minecraft 26.3 et Fabric Loader 0.19.5,
|
||||
un seul JAR Sanctuary embarqué, identique au JAR construit, sans monde ni module
|
||||
de test. Les 3 303 fichiers d'assets, données et resourcepacks sont identiques
|
||||
à beta.234, traductions incluses. Story reste dans le JAR de production et les
|
||||
classes des bancs client en sont absentes. Reçu :
|
||||
`build/pause235-mrpack-receipt.json`.
|
||||
|
||||
- JAR : `aad94732c749161d775ee634bdd85de70de511ef875c6c00fa167d230d0d600b`.
|
||||
- MRpack : `5f1bd18aa8a4145b7e708da0abfff22068365847dddbc95ae2b490b88bbb305e`.
|
||||
|
||||
Aucune publication distante, modification du canal packwiz, synchronisation
|
||||
Prism ou ouverture d'un monde personnel effectuée.
|
||||
@@ -0,0 +1,86 @@
|
||||
# PAUSE-233 — navigation centrée et rédaction communautaire
|
||||
|
||||
Branche `codex/pause-community-beta233`, base beta.232, Minecraft 26.3.
|
||||
|
||||
## Résultat et compatibilité
|
||||
|
||||
Avec ou sans Atlas, les boutons gardent les mêmes groupes et le même ordre :
|
||||
|
||||
- Communauté : Habitant, Faction et Battle en haut de la colonne.
|
||||
- Aventure : Discovery et Progression centrés verticalement dans l'écran.
|
||||
- Partie : Options, World Options et Save and Quit en bas.
|
||||
|
||||
Sans carte, cette colonne est centrée horizontalement entre Gazette à gauche
|
||||
et Notice Board à droite. Avec carte, elle reste à gauche, carte au centre et
|
||||
les deux fils communautaires à droite. L'ordre clavier suit l'ordre visuel.
|
||||
Les boutons deviennent plus compacts sur les petites hauteurs GUI pour conserver
|
||||
les titres et l'espacement entre groupes. La petite barre sous les boutons
|
||||
Communauté est supprimée dans les deux dispositions. En-tête et reprise en pied
|
||||
conservés.
|
||||
|
||||
Le formulaire Notice Board ne propose plus Information/Project/Request/Event.
|
||||
Toutes les notices ont la même présentation, y compris les anciennes catégories.
|
||||
Les demandes et récompenses restent facultatives et descriptives : après le
|
||||
texte viennent Request and rewards, son résumé, puis Publish. Le formulaire
|
||||
Gazette place la photo et Choose a screenshot en premier, avant le titre,
|
||||
puis le texte et Publish.
|
||||
Photo obligatoire, brouillons, édition, retours des sélecteurs, suivis et
|
||||
validations serveur restent actifs.
|
||||
|
||||
Le contrat fichier/MariaDB et le protocole ne changent pas. Les nouvelles notices
|
||||
gardent la valeur compatible `info` ; éditer une ancienne annonce conserve sa
|
||||
catégorie enregistrée sans l'afficher. La lecture n'entraîne aucune réécriture
|
||||
des posts. Aucune migration, génération ou opération d'inventaire n'est ajoutée.
|
||||
Les anciennes traductions de catégories restent disponibles pour les données
|
||||
historiques. Titres ajoutés en français/anglais : Communauté/Community,
|
||||
Aventure/Adventure et Partie/Game ; les autres libellés existent déjà.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
`./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryPause233ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan`
|
||||
Réussite en 47 s (`PAUSE233_PASS`), après suppression de la barre sous Communauté.
|
||||
Monde plat de développement, Survie sans Allow Commands, stockage fichier isolé.
|
||||
|
||||
- FR/EN, GUI 2/3/4, fenêtre 1280×960, avant et après Atlas : même ordre des
|
||||
actions et des groupes, titres sans chevauchement, paire Discovery/Progression
|
||||
centrée verticalement, navigation et options séparées.
|
||||
- Ordre titre → texte → demande et résumé → publication, ou photo et sélecteur
|
||||
→ titre → texte → publication ; aucune catégorie visible. Défilement du formulaire sur GUI
|
||||
étroit vérifié. 38 captures dans `build/pause233-screenshots/`.
|
||||
- Retours réels des sélecteurs : texte conservé, 128 bûches demandées et
|
||||
récompense personnalisée publiées ; les 17 bûches et 3 diamants d'inventaire
|
||||
réel restent présents. Photo toujours obligatoire, article et photo relus
|
||||
depuis un nouveau lecteur du stockage fichier après publication.
|
||||
- Ancienne catégorie `event` conservée lors de l'édition. Lecture des posts,
|
||||
redimensionnements et changements de langue n'altèrent aucun octet du fichier
|
||||
communautaire. Configuration du banc restaurée à la fin.
|
||||
- Captures inspectées : pause verrouillée FR/2 et EN/4, pause déverrouillée FR/2,
|
||||
notice configurée, Gazette avec photo assignée et bas du formulaire Notice Board
|
||||
EN/4. Notices anciennes et nouvelles ont le même cadre et ne montrent que leur état.
|
||||
|
||||
Log client final : `build/pause233-client-vulkan-titled.log`. Aucun essai OpenGL ou Windows
|
||||
et aucune migration MariaDB ne sont revendiqués ; le code serveur est conservé.
|
||||
|
||||
`./gradlew check build assemblePack -PsanctuaryFocusedTests=menus
|
||||
-PsanctuaryAtlasOnly=true`
|
||||
Réussite en 2 min 39 s : quatre GameTests requis, contrôles purs communautaires
|
||||
(`COMMUNITY154_FILE_PASS`, `COMMUNITY157_PHOTOS_PASS`, `COMMUNITY158_TASKS_PASS`,
|
||||
`COMMUNITY159_SEARCH_PASS`), build et assemblage du pack.
|
||||
Log : `build/pause233-check-build-pack-final.log`.
|
||||
Ce contrôle utilise le mode existant du banc pour les menus. Le chemin natif par
|
||||
défaut présente toujours le conflit de générateur documenté en beta.232 ; cette
|
||||
livraison ne revendique pas une nouvelle suite complète de génération.
|
||||
|
||||
Export local `build/Sanctuary-beta.233.mrpack` vérifié : intégrité ZIP, versions
|
||||
Minecraft 26.3/Fabric Loader 0.19.5, un seul JAR Sanctuary embarqué, exactement
|
||||
identique au JAR construit, sans module de test ni monde. Les 3 146 fichiers
|
||||
de données et assets hors traductions sont identiques à beta.232 ; seules les
|
||||
trois traductions de titres de groupes sont ajoutées en FR/EN, sans changer les
|
||||
libellés existants. Reçu : `build/pause233-mrpack-receipt.json`.
|
||||
|
||||
- JAR : `23a148c197126ede552865f9847ce381700884d5ff087469a13e36540e8f23f9`.
|
||||
- MRpack : `d95aeeca5f255db4444a22879f465477c0c6a2a111dd4e1c777b20944a283e2b`.
|
||||
|
||||
Livraison locale beta.233 ; aucune installation personnelle, publication
|
||||
distante, ouverture d'un monde personnel ou mise à jour du canal packwiz.
|
||||
@@ -0,0 +1,114 @@
|
||||
# SOLO-232 — réglages et progression en solo
|
||||
|
||||
Branche `codex/solo-time-building-vein`, base beta.231, Minecraft 26.3.
|
||||
|
||||
## Comportement et contrat de sauvegarde
|
||||
|
||||
`/sanctuary time timezone <fuseau IANA>` choisit le fuseau et sa ville solaire
|
||||
de référence pour ce monde. `timezone auto` reprend le réglage automatique
|
||||
solo ou la configuration dédiée. Le propriétaire solo peut faire ce choix
|
||||
sans Allow Commands ; les autres joueurs et le serveur dédié exigent les
|
||||
droits opérateur. Le statut affiche le fuseau et la commande propose les
|
||||
identifiants disponibles sans accès réseau.
|
||||
|
||||
Le choix persiste dans le champ opaque `reference` du contrat solaire existant,
|
||||
avec le préfixe `timezone:`. Le schéma 1 et les autres champs restent inchangés.
|
||||
Les anciennes références restent lisibles. Le changement réancre le calendrier
|
||||
à son cycle courant, écrit atomiquement avant l'application solaire et ne
|
||||
change jamais `gameTime`. Le mode Vanilla reste Vanilla. `auto` retire le
|
||||
préfixe ; une ancienne version reprendrait sa référence configurée au prochain
|
||||
rechargement. La configuration globale et les autres mondes ne sont pas modifiés.
|
||||
Seule la commande explicitement exécutée par le joueur change son contrat.
|
||||
|
||||
Construction utilise les portées 3 / 3,5 / 4 / 4,5 / 5 / 5,5 / 6 / 7 blocs.
|
||||
Avec un bloc en main principale ou secondaire, la visée suit cette portée :
|
||||
un contour hors portée native reste un support de pose. Minage, contenants,
|
||||
combat, protections, collisions et stock conservent leurs validations propres.
|
||||
Une pose visible peut être refusée pour ces raisons ordinaires, mais pas par
|
||||
une portée de construction différente de celle de la visée.
|
||||
|
||||
R + clic gauche maintenus terminent le groupe verrouillé, puis sélectionnent
|
||||
le nouveau bloc regardé et démarrent un nouveau groupe. Chaque groupe conserve
|
||||
sa durée native, son coût et la validation serveur. Une confirmation serveur
|
||||
de réussite permet la reprise ; refus, outil cassé ou remplacé, interruption,
|
||||
menu ou relâchement arrêtent le geste. Un maintien ne rejoue jamais le même
|
||||
groupe terminé. Aucun groupe n'est lancé sans cible valide.
|
||||
|
||||
## Menu pause et butin de l'île
|
||||
|
||||
Avant l'achat Atlas, aucun widget World Map ni requête
|
||||
de carte n'est créé dans le menu pause. Discovery, Progression, Inhabitant,
|
||||
Faction, Battle, Options, World Options et Save and Quit sont centrés.
|
||||
Gazette est à gauche des boutons et Notice Board à droite, chacune sur toute
|
||||
la hauteur du corps du menu. Calendrier et bulletin conservent leurs emplacements.
|
||||
Après déblocage, la disposition avec carte
|
||||
et les deux fils à droite revient à la prochaine ouverture du menu.
|
||||
|
||||
Les diamants sont retirés lors du premier remplissage
|
||||
des coffres et wagonnets à coffre situés sur l'île initiale du générateur
|
||||
Sanctuary, quelle que soit la table de butin (mines natives comprises).
|
||||
Les expansions, autres dimensions et mondes ordinaires conservent leurs tables.
|
||||
Les coffres déjà ouverts et les objets déposés manuellement sont conservés.
|
||||
Les caches non ouvertes de mondes existants suivent cette nouvelle règle à leur
|
||||
ouverture ; aucune visite ou réécriture de chunks n'a lieu. Aucun minerai ni
|
||||
profil de génération ne change. La graine n'affecte pas l'exclusion des diamants.
|
||||
|
||||
La règle s'applique au générateur courant NoiseBased et à ses trois tailles,
|
||||
ainsi qu'aux anciens profils Sanctuary. Le filtre intercepte uniquement
|
||||
`LootTable.fill`, sans changer les tables elles-mêmes ni leurs autres usages.
|
||||
Les 794 ressources de génération et de butin du JAR beta.231 restent identiques.
|
||||
La génération demeure 231 ; graines de butin vérifiées : 0, 42, 232, puis
|
||||
256 tirages de la table native abandoned_mineshaft.
|
||||
|
||||
## Livraison et vérifications
|
||||
|
||||
Version mod et pack : `beta.232`, Minecraft 26.3, Fabric Loader 0.19.5, Java 25.
|
||||
Le nouveau message serveur `sanctuary:mining_result` autorise la reprise d'un
|
||||
groupe terminé ; client et serveur doivent utiliser cette livraison ensemble.
|
||||
Le client attend aussi la mise à jour du bloc initial avant de recalculer sa
|
||||
visée, car la confirmation peut arriver avant les blocs détruits.
|
||||
|
||||
`./gradlew check build assemblePack -PsanctuaryFocusedTests=realtime,mining,building
|
||||
-PsanctuaryAtlasOnly=true` réussit en 2 min 36 s : 24 GameTests requis et les
|
||||
contrôles purs du dépôt passent. Fuseau persistant/idempotent, fuseau invalide
|
||||
et contrat modifié extérieurement refusés, mode Vanilla et horloge native
|
||||
préservés ; validations serveur du minage et de la construction conservées.
|
||||
Log : `build/solo232-check-build-pack.log`.
|
||||
|
||||
`./gradlew :sanctuary:runGameTest -PsanctuaryFocusedTests=solo232
|
||||
-PsanctuaryTestGeneration=13` réussit en 37 s : 2 GameTests requis, remplissage
|
||||
réel avec le mixin, conservation des diamants déposés et du fer, butin distant
|
||||
et contexte absent préservés, identification des trois profils courants.
|
||||
Log : `build/solo232-loot-native.log`.
|
||||
|
||||
Les contrôles utilisent les modes existants du banc d'essai. Son chemin par
|
||||
défaut tente encore de convertir le preset courant NoiseBased en ancien
|
||||
SanctuaryChunkGenerator et échoue avant les tests. Ce ticket ne corrige pas
|
||||
ce banc historique et ne revendique pas une suite complète de génération.
|
||||
|
||||
`./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuarySolo232ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan`
|
||||
réussit en 1 min 3 s, Vulkan confirmé avec MoltenVK 1.4.2. Un propriétaire
|
||||
Survie sans Allow Commands utilise réellement la commande de fuseau. Deux
|
||||
groupes successifs sont minés avec R + clic gauche maintenus, huit destructions
|
||||
et huit usages natifs d'outil ; un outil cassé interrompt le geste après une
|
||||
destruction sans redémarrer. L'essai Balance080 couvre aussi pose native aux
|
||||
deux mains, portée initiale, contour et pose à 6,75 blocs, refus d'un paquet
|
||||
forgé à 7,25 blocs, indépendance Minage/Construction, contenants, nénuphars,
|
||||
construction groupée et bonus externes.
|
||||
|
||||
Le menu pause est contrôlé avant/après achat Atlas, en FR/EN, aux échelles
|
||||
GUI 2/3/4 dans une fenêtre 1280×960 : huit actions présentes et dans le corps,
|
||||
colonnes sans chevauchement, carte uniquement après achat. Douze captures
|
||||
sont conservées dans `build/solo232-screenshots/`, avec inspection visuelle
|
||||
du mode verrouillé FR/2 et EN/4, et du mode débloqué FR/2. Les libellés longs
|
||||
gardent les infobulles natives aux petites largeurs.
|
||||
Log : `build/solo232-client-vulkan-final.log`, marqueurs `SOLO232_PASS` et
|
||||
`BALANCE080_PASS`. Aucun essai OpenGL ou Windows n'est revendiqué.
|
||||
|
||||
MRpack local : `build/Sanctuary-beta.232.mrpack`, intégrité ZIP, versions,
|
||||
dépendances exactes, JAR construit identique, absence de monde et d'entrée
|
||||
Sanctuary Test vérifiées. Reçu : `build/solo232-mrpack-receipt.json`.
|
||||
SHA-256 pack : `03e588c3866ad513d057162e9fea4ba9ad07e33a522841e2e7bbd66356b153f8`.
|
||||
SHA-256 JAR : `a208ccce6318ceccf04d35d983e4a9ea1d46e872fda202d47a37000faa740d9f`.
|
||||
Aucun monde personnel, installation de jeu ou canal packwiz n'est modifié.
|
||||
@@ -0,0 +1,80 @@
|
||||
# STORY-234 — recherche et filtres
|
||||
|
||||
Branche `codex/story-beta234`, base beta.233, commit `9c096b2`, Minecraft 26.3.
|
||||
|
||||
## Contrat
|
||||
|
||||
Story apparaît sous Progression dans Aventure, avant et après Atlas. Il reprend
|
||||
le cadre de Discovery : titre, séparateurs et Terminé / Done. La recherche et
|
||||
le filtre Toutes / Découvertes / Inconnues remplacent les boutons de catégories.
|
||||
|
||||
La liste réserve 100 histoires numérotées et inconnues, toutes désactivées.
|
||||
Toutes et Inconnues montrent les entrées correspondant à la recherche, dans
|
||||
l'ordre numérique ; Découvertes indique qu'aucune histoire n'est découverte.
|
||||
Une recherche sans résultat dispose d'un autre message. La recherche porte sur
|
||||
le numéro et le libellé visible, ignore la casse et les espaces extérieurs.
|
||||
Recherche et filtre se combinent et ramènent le défilement en haut. La saisie
|
||||
garde son focus et son curseur ; redimensionner garde recherche, filtre et
|
||||
défilement. Terminé et Échap reviennent directement au menu Pause d'origine.
|
||||
|
||||
Aventure comprend maintenant Discovery, Progression et Story. Les trois groupes
|
||||
de Pause conservent leur ordre avec ou sans carte. Aux petites hauteurs, les
|
||||
boutons et espaces se compactent pour éviter les chevauchements.
|
||||
|
||||
[Guide de rédaction](story/README.md) et [modèle Markdown](story/TEMPLATE.md) :
|
||||
un fichier `001.md` à `100.md`, avec numéro, titre, texte et idée de découverte
|
||||
facultative. Aucun récit n'est inventé, chargé ou révélé dans ce lot. Leur
|
||||
intégration et leurs déblocages seront des tickets suivants. Pas de nouveau
|
||||
paquet réseau, dépendance, sauvegarde ou changement de génération.
|
||||
|
||||
## Vérifications
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack -PsanctuaryFocusedTests=menus -PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryStory234ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
|
||||
```
|
||||
|
||||
Build final réussi en 2 min 34 s : quatre GameTests requis et contrôles purs du
|
||||
dépôt, dont les contrôles communautaires fichier, photos, demandes et recherche.
|
||||
Log : `build/story234-check-build-pack-final.log`. Le banc menus utilise la
|
||||
sélection Atlas existante ; le conflit du générateur sur le chemin par défaut,
|
||||
documenté en beta.232, n'est pas modifié par ce lot.
|
||||
|
||||
Parcours client final réussi en 30 s (`STORY234_PASS`), sur un monde plat neuf
|
||||
de développement en Survie sans commandes. Vulkan confirmé par le journal :
|
||||
MoltenVK 1.4.2 sur macOS, Apple M1. Log : `build/story234-client-vulkan-final.log`.
|
||||
|
||||
- FR/EN, GUI 2/3/4, fenêtre 1280 × 960, avec et sans Atlas : six actions
|
||||
Sanctuary dans l'ordre visuel et clavier, trois groupes sans chevauchement,
|
||||
groupe Discovery/Progression/Story centré verticalement.
|
||||
- 100 numéros consécutifs sans doublon, entrées désactivées, recherche du numéro
|
||||
100, espaces extérieurs et recherche en majuscules dans le libellé localisé.
|
||||
Recherche et filtre combinés, deux états vides distincts.
|
||||
- Saisie par événements natifs, focus et curseur conservés, molette et accès à
|
||||
la dernière entrée ; défilement remis en haut après recherche ou filtre.
|
||||
Redimensionnement réel à 1344 × 1008 : recherche, filtre et défilement conservés.
|
||||
- Terminé et Échap reviennent à la même Pause. La page reste une pause solo ;
|
||||
le record de progression serveur est identique avant et après les consultations.
|
||||
|
||||
21 captures dans `build/story234-screenshots/`. Captures inspectées : Story FR/2
|
||||
et EN/4, Pause sans Atlas FR/4 et avec Atlas EN/3, états Découvertes et recherche
|
||||
vides, recherche de l'histoire 100. L'indication de recherche reste visible
|
||||
lorsque le champ vide a le focus. Aucun essai OpenGL ou Windows n'est revendiqué.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
`mod_version`, `pack_version` et `packwiz/pack.toml` sont alignés sur beta.234.
|
||||
Export [Sanctuary-beta.234.mrpack](../build/Sanctuary-beta.234.mrpack),
|
||||
12 775 502 octets : intégrité ZIP, Minecraft 26.3, Fabric Loader 0.19.5,
|
||||
un seul JAR Sanctuary embarqué, identique au JAR construit, sans monde ni
|
||||
module de test. Le JAR de production inclut Story et aucune classe de son banc
|
||||
client. Assets et données identiques à beta.233, hors les neuf traductions
|
||||
ajoutées dans chacune des langues FR/EN ; les libellés existants sont conservés.
|
||||
Reçu : `build/story234-mrpack-receipt.json`.
|
||||
|
||||
- JAR : `25dc161468679379e642f9746761905045d1135b6a300e3bc3f58f37a4c8f9a8`.
|
||||
- MRpack : `0b16514e4dec16d14061fd25c7cb42d53fdb28c73f06a1c6361d16d059977c69`.
|
||||
|
||||
Les trois commits couvrent l'interface, les tests/documentation et la livraison.
|
||||
Aucune publication distante, modification de canal packwiz, synchronisation
|
||||
Prism ou ouverture de monde personnel effectuée.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Écrire les histoires de Sanctuary
|
||||
|
||||
Story accueille des textes personnels sur Sanctuary, son développement, Steve,
|
||||
Minecraft et ce que signifie habiter un jeu. Les numéros de 1 à 100 sont des
|
||||
repères durables ; ils n'imposent ni un ordre de lecture ni une chronologie.
|
||||
|
||||
Copier [le modèle](TEMPLATE.md) dans un fichier nommé `001.md` à `100.md`, puis
|
||||
remplacer le titre et écrire librement dans la section Texte. Les paragraphes,
|
||||
les hésitations et la voix de l'auteur peuvent être conservés tels quels.
|
||||
La section Idée de découverte est facultative et peut rester vide.
|
||||
|
||||
Transmettre les fichiers au fil des séances pour préparer les prochains tickets.
|
||||
Ne pas renuméroter une histoire déjà intégrée. La traduction éventuelle des récits
|
||||
se décide avec l'auteur ; les interfaces du jeu disposent de leurs libellés FR/EN.
|
||||
|
||||
En beta.234, les 100 histoires sont encore inconnues et désactivées. Ces fichiers
|
||||
sont des sources de rédaction : le mod ne les charge pas automatiquement.
|
||||
L'intégration des premiers textes et leurs conditions de découverte restent à
|
||||
implémenter, avec autorité serveur si elles dépendent de la partie.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Titre de l'histoire
|
||||
|
||||
Numéro : 001
|
||||
|
||||
## Texte
|
||||
|
||||
Écrire ici le texte, avec autant de paragraphes que nécessaire.
|
||||
|
||||
## Idée de découverte (facultative)
|
||||
|
||||
Décrire éventuellement un geste, une rencontre, un lieu ou une autre idée qui
|
||||
pourrait révéler cette histoire. Cette note est une intention de conception.
|
||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
||||
fabric_api_version=0.160.5+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.231
|
||||
pack_version=beta.231
|
||||
mod_version=beta.235
|
||||
pack_version=beta.235
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -228,7 +228,7 @@ tasks.named('processGametestResources') {
|
||||
|
||||
def villageOnly = providers.gradleProperty('sanctuaryVillageOnly').map { it.toBoolean() }.getOrElse(false)
|
||||
def focused = providers.gradleProperty('sanctuaryFocusedTests').getOrElse('').split(',').findAll { !it.isEmpty() }
|
||||
def focusClasses = [diagonal231: 'Diagonal231GameTests', diagonal230: 'Diagonal230GameTests', diagonal229: 'Diagonal229GameTests', diagonal228: 'Diagonal228GameTests', diagonal227: 'Diagonal227GameTests', diagonal226: 'Diagonal226GameTests', diagonal225: 'Diagonal225GameTests', east224: 'East224GameTests', east223: 'East223GameTests', rock222: 'YellowRock222GameTests', west221: 'West221GameTests', west220: 'West220GameTests', west219: 'West219GameTests', west218: 'West218GameTests', west217: 'West217GameTests', south216: 'South216GameTests', relays215: 'Relays215GameTests', south214: 'South214GameTests', south213: 'South213GameTests', south212: 'South212GameTests', south211: 'South211GameTests', south210: 'South210GameTests', south209: 'South209GameTests', north208: 'North208GameTests', north207: 'North207GameTests', north206: 'North206GameTests', north205: 'North205GameTests', base204: 'Base204GameTests', palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', plans072: 'Plans072GameTests', starter052: 'Starter052GameTests', dispenser051: 'Dispenser051GameTests', head050: 'Head050GameTests', boatshead: 'BoatsHead049GameTests', poultry: 'Poultry048GameTests', starinspect: 'StarInspection047GameTests', skyflight: 'SkyFlight046GameTests', eggaudit: 'Companion044GameTests', headblocks: 'HeadBlocks043GameTests', operator: 'OperatorStars040GameTests', starter: 'Starter039GameTests', gravelayout: 'GraveInventory039GameTests', foodknowledge: 'FoodKnowledge039GameTests', graves: 'GravesFood038GameTests', realtime: 'Realtime037GameTests', corrections: 'Corrections036GameTests', collections: 'Collections035GameTests', notifications: 'Notifications034GameTests', menus: 'Menus033GameTests', aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests', expeditions: 'AncientExpeditions001GameTests', progression: 'Progression003GameTests', map: 'Atlas005GameTests', demeure: 'Demeure006GameTests', mining: 'Mining008GameTests', building: 'Building009GameTests', inventory: 'Inventory010GameTests', cycle: 'Cycle011GameTests', inventoryflow: 'Inventory012GameTests', sorting: 'Inventory014GameTests', tutorial: 'Tutorial016GameTests', movement: 'Movement017GameTests', accessories: 'Accessory018GameTests', companions: 'Companion019GameTests', familiar: 'Familiar020GameTests', carry: 'Carry027GameTests', familiarhit: 'Familiar029GameTests', refonte: 'Companion032GameTests', lights: 'Dynamic030GameTests', cosmetics: 'HeadCosmetic021GameTests', startup: 'StartupPerformanceGameTests', recipes: 'Recipes024GameTests']
|
||||
def focusClasses = [solo232: 'Solo232GameTests', diagonal231: 'Diagonal231GameTests', diagonal230: 'Diagonal230GameTests', diagonal229: 'Diagonal229GameTests', diagonal228: 'Diagonal228GameTests', diagonal227: 'Diagonal227GameTests', diagonal226: 'Diagonal226GameTests', diagonal225: 'Diagonal225GameTests', east224: 'East224GameTests', east223: 'East223GameTests', rock222: 'YellowRock222GameTests', west221: 'West221GameTests', west220: 'West220GameTests', west219: 'West219GameTests', west218: 'West218GameTests', west217: 'West217GameTests', south216: 'South216GameTests', relays215: 'Relays215GameTests', south214: 'South214GameTests', south213: 'South213GameTests', south212: 'South212GameTests', south211: 'South211GameTests', south210: 'South210GameTests', south209: 'South209GameTests', north208: 'North208GameTests', north207: 'North207GameTests', north206: 'North206GameTests', north205: 'North205GameTests', base204: 'Base204GameTests', palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', plans072: 'Plans072GameTests', starter052: 'Starter052GameTests', dispenser051: 'Dispenser051GameTests', head050: 'Head050GameTests', boatshead: 'BoatsHead049GameTests', poultry: 'Poultry048GameTests', starinspect: 'StarInspection047GameTests', skyflight: 'SkyFlight046GameTests', eggaudit: 'Companion044GameTests', headblocks: 'HeadBlocks043GameTests', operator: 'OperatorStars040GameTests', starter: 'Starter039GameTests', gravelayout: 'GraveInventory039GameTests', foodknowledge: 'FoodKnowledge039GameTests', graves: 'GravesFood038GameTests', realtime: 'Realtime037GameTests', corrections: 'Corrections036GameTests', collections: 'Collections035GameTests', notifications: 'Notifications034GameTests', menus: 'Menus033GameTests', aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests', expeditions: 'AncientExpeditions001GameTests', progression: 'Progression003GameTests', map: 'Atlas005GameTests', demeure: 'Demeure006GameTests', mining: 'Mining008GameTests', building: 'Building009GameTests', inventory: 'Inventory010GameTests', cycle: 'Cycle011GameTests', inventoryflow: 'Inventory012GameTests', sorting: 'Inventory014GameTests', tutorial: 'Tutorial016GameTests', movement: 'Movement017GameTests', accessories: 'Accessory018GameTests', companions: 'Companion019GameTests', familiar: 'Familiar020GameTests', carry: 'Carry027GameTests', familiarhit: 'Familiar029GameTests', refonte: 'Companion032GameTests', lights: 'Dynamic030GameTests', cosmetics: 'HeadCosmetic021GameTests', startup: 'StartupPerformanceGameTests', recipes: 'Recipes024GameTests']
|
||||
focused.each { if (!focusClasses.containsKey(it)) throw new GradleException('Unknown focused native test: ' + it) }
|
||||
inputs.property('focusedNative', focused)
|
||||
def yellowRock222Client = providers.gradleProperty('sanctuaryYellowRock222ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
@@ -331,6 +331,15 @@ tasks.named('processGametestResources') {
|
||||
def weather082Client = providers.gradleProperty('sanctuaryWeather082ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryWeather082ClientTests', weather082Client)
|
||||
if (weather082Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Weather082ClientChecks') }
|
||||
def solo232Client = providers.gradleProperty('sanctuarySolo232ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuarySolo232ClientTests', solo232Client)
|
||||
if (solo232Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Solo232ClientChecks') }
|
||||
def pause233Client = providers.gradleProperty('sanctuaryPause233ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPause233ClientTests', pause233Client)
|
||||
if (pause233Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Pause233ClientChecks') }
|
||||
def story234Client = providers.gradleProperty('sanctuaryStory234ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryStory234ClientTests', story234Client)
|
||||
if (story234Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Story234ClientChecks') }
|
||||
def balance080Client = providers.gradleProperty('sanctuaryBalance080ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryBalance080ClientTests', balance080Client)
|
||||
if (balance080Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Balance080ClientChecks') }
|
||||
|
||||
+15
-13
@@ -29,14 +29,15 @@ import net.minecraft.world.phys.*;
|
||||
/** Actual input, native prediction, play packets and authoritative blocks in a disposable solo world. */
|
||||
public final class Balance080ClientChecks implements FabricClientGameTest {
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
Starter039TestSupport.selectFirst(screen);
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Balance080 "+UUID.randomUUID());s.setSeed("80");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Balance080 "+UUID.randomUUID());s.setSeed("80");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getInventory().clearContent();p.getAbilities().invulnerable=true;
|
||||
@@ -45,7 +46,8 @@ public final class Balance080ClientChecks implements FabricClientGameTest {
|
||||
});
|
||||
// The base skill places nearby, rejects a normal 4.25-block click, and consumes no stock on rejection.
|
||||
setup(c,server,0,0,3,Items.COBBLESTONE,false);use(c);server.waitFor(s->player(s).level().getBlockState(pos(3).north()).is(Blocks.COBBLESTONE),100);
|
||||
setup(c,server,0,0,5,Items.COBBLESTONE,false);use(c);c.waitTicks(12);
|
||||
setup(c,server,0,0,5,Items.COBBLESTONE,false);
|
||||
c.runOnClient(m->check(!(m.hitResult instanceof BlockHitResult h&&h.getType()==HitResult.Type.BLOCK&&h.getBlockPos().equals(pos(5))),"Rank-zero construction does not show an unreachable support outline"));use(c);c.waitTicks(12);
|
||||
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(pos(5).north()).isAir()&&p.getMainHandItem().getCount()==64,"Rank zero rejects far placement without consuming stock");});
|
||||
// Construction alone reaches 5.25 blocks; Mining stays at its own rank-zero reach.
|
||||
setup(c,server,0,7,6,Items.COBBLESTONE,false);
|
||||
@@ -54,15 +56,15 @@ public final class Balance080ClientChecks implements FabricClientGameTest {
|
||||
server.runOnServer(s->check(player(s).level().getBlockState(pos(6)).is(Blocks.STONE),"Construction does not extend mining"));
|
||||
use(c);server.waitFor(s->player(s).level().getBlockState(pos(6).north()).is(Blocks.COBBLESTONE),100);
|
||||
setup(c,server,0,7,6,Items.COBBLESTONE,true);use(c);server.waitFor(s->player(s).level().getBlockState(pos(6).north()).is(Blocks.COBBLESTONE),100);
|
||||
// beta.081: the newly earned half-block works through the actual pick and play packet.
|
||||
setup(c,server,0,7,6,Items.COBBLESTONE,false);server.runOnServer(s->player(s).teleportTo(.5,1,.25));
|
||||
c.waitFor(m->Math.abs(m.player.getZ()-.25)<.01,100);c.getInput().lookAt(pos(6));c.waitTicks(5);use(c);
|
||||
server.waitFor(s->player(s).level().getBlockState(pos(6).north()).is(Blocks.COBBLESTONE),100);
|
||||
// Outside six blocks, a forged hit is rejected even within the native packet tolerance.
|
||||
setup(c,server,0,7,6,Items.COBBLESTONE,false);server.runOnServer(s->player(s).teleportTo(.5,1,-.25));
|
||||
// beta.232: the seventh block of reach works through the actual pick and play packet.
|
||||
setup(c,server,0,7,7,Items.COBBLESTONE,false);server.runOnServer(s->player(s).teleportTo(.5,1,.25));
|
||||
c.waitFor(m->Math.abs(m.player.getZ()-.25)<.01,100);c.getInput().lookAt(pos(7));c.waitTicks(5);use(c);
|
||||
server.waitFor(s->player(s).level().getBlockState(pos(7).north()).is(Blocks.COBBLESTONE),100);
|
||||
// Outside seven blocks, a forged hit is rejected even within the native packet tolerance.
|
||||
setup(c,server,0,7,7,Items.COBBLESTONE,false);server.runOnServer(s->player(s).teleportTo(.5,1,-.25));
|
||||
c.waitFor(m->Math.abs(m.player.getZ()+.25)<.01,100);c.getInput().lookAt(pos(6));c.waitTicks(5);
|
||||
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(.5,2.5,6),Direction.NORTH,pos(6),false)));c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(pos(6).north()).isAir()&&p.getMainHandItem().getCount()==64,"Six-block maximum is enforced by the server");});
|
||||
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(.5,2.5,7),Direction.NORTH,pos(7),false)));c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=player(s);check(p.level().getBlockState(pos(7).north()).isAir()&&p.getMainHandItem().getCount()==64,"Seven-block maximum is enforced by the server");});
|
||||
// A deliberately forged out-of-range native packet cannot place or consume an item.
|
||||
setup(c,server,7,7,8,Items.COBBLESTONE,false);
|
||||
c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(new Vec3(.5,2.5,8),Direction.NORTH,pos(8),false)));c.waitTicks(15);
|
||||
@@ -105,13 +107,13 @@ public final class Balance080ClientChecks implements FabricClientGameTest {
|
||||
}
|
||||
}
|
||||
private static void curve(ServerPlayer p) {
|
||||
double[] speed={.75,.85,.95,1,1.075,1.15,1.225,1.30},reach={3,3.5,4,4.5,4.75,5,5.25,5.5},buildReach={3,3.5,4,4.5,4.75,5,5.5,6};
|
||||
double[] speed={.75,.85,.95,1,1.075,1.15,1.225,1.30},reach={3,3.5,4,4.5,4.75,5,5.25,5.5},buildReach={3,3.5,4,4.5,5,5.5,6,7};
|
||||
var extra=SanctuaryMod.id("test/balance080");var attribute=p.getAttribute(Attributes.BLOCK_BREAK_SPEED);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.DIAMOND_PICKAXE));setRanks(p,3,3);
|
||||
var state=Blocks.STONE.defaultBlockState();float baseline=state.getDestroyProgress(p,p.level(),new BlockPos(0,0,0));
|
||||
for(int rank=0;rank<=7;rank++) {setRanks(p,rank,rank);near(attribute.getValue(),speed[rank],"Mining speed rank "+rank);near(state.getDestroyProgress(p,p.level(),new BlockPos(0,0,0))/baseline,speed[rank],"Native block breaking progress rank "+rank);near(BuildReach.range(p),buildReach[rank],"Build reach rank "+rank);near(MiningReach.range(p),reach[rank],"Mining reach rank "+rank);}
|
||||
attribute.addTransientModifier(new AttributeModifier(extra,.5,AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL));setRanks(p,7,7);near(attribute.getValue(),1.95,"Mining composes with external boost");attribute.removeModifier(extra);
|
||||
var blockRange=p.getAttribute(Attributes.BLOCK_INTERACTION_RANGE);blockRange.addTransientModifier(new AttributeModifier(extra,2,AttributeModifier.Operation.ADD_VALUE));near(BuildReach.range(p),8,"Construction preserves external range bonuses");near(MiningReach.range(p),7.5,"Mining preserves external range bonuses");blockRange.removeModifier(extra);
|
||||
var blockRange=p.getAttribute(Attributes.BLOCK_INTERACTION_RANGE);blockRange.addTransientModifier(new AttributeModifier(extra,2,AttributeModifier.Operation.ADD_VALUE));near(BuildReach.range(p),9,"Construction preserves external range bonuses");near(MiningReach.range(p),7.5,"Mining preserves external range bonuses");blockRange.removeModifier(extra);
|
||||
near(p.blockInteractionRange(),4.5,"Base interactions remain native");near(p.entityInteractionRange(),3,"Entity interactions remain native");
|
||||
}
|
||||
private static void setup(ClientGameTestContext c,TestServerContext server,int mining,int building,int z,Item item,boolean offhand) {
|
||||
|
||||
+1
-1
@@ -208,7 +208,7 @@ public final class Community158ClientChecks implements FabricClientGameTest {
|
||||
check(date.getWidth()==discovery.getWidth(),"Calendar matches navigation width");
|
||||
check(official.getX()==date.getRight()+10&&official.getRight()==screen.width-8,"Bulletin spans map and community panels");
|
||||
check(date.getMessage().getString().matches(".*\\d{2}:\\d{2}$"),"Clock included in calendar");
|
||||
check(inhabitant.getY()-progression.getBottom()>progression.getY()-discovery.getBottom(),"Personal and social navigation groups separated");
|
||||
check(inhabitant.getY()<pauseButton(screen,"sanctuary.cycle.faction").getY()&&pauseButton(screen,"sanctuary.arena.title").getBottom()<discovery.getY()&&progression.getY()>discovery.getBottom(),"Community group precedes the separate adventure group");
|
||||
var navigation=screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getX()==8&&b.getY()>=50&&b.getY()<pauseButton(screen,"menu.options").getY()).toList();
|
||||
check(navigation.size()==5,"Five navigation destinations");
|
||||
check(navigation.stream().allMatch(b->b.getBottom()<pauseButton(screen,"menu.options").getY()),"Navigation clears settings");
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.community.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.*;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Native layout, selector returns and publication packets in an isolated file-backed solo world. */
|
||||
public final class Pause233ClientChecks implements FabricClientGameTest {
|
||||
private final Path config=FabricLoader.getInstance().getConfigDir().resolve("sanctuary-community.json");
|
||||
private final Path game=FabricLoader.getInstance().getGameDir();
|
||||
private final String serverId=UUID.randomUUID().toString();
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
byte[] previous=checked(()->Files.exists(config)?Files.readAllBytes(config):null);
|
||||
var intro=new AtomicBoolean(true);
|
||||
Path fixture=game.resolve("screenshots/zz-pause233.png");
|
||||
checked(()->{
|
||||
Files.createDirectories(config.getParent());Files.writeString(config,new Gson().toJson(new CommunityConfig("file",serverId,"jdbc:mariadb://localhost/sanctuary","root","SANCTUARY_COMMUNITY_DB_PASSWORD")));
|
||||
Files.createDirectories(fixture.getParent());var image=new java.awt.image.BufferedImage(320,180,java.awt.image.BufferedImage.TYPE_INT_RGB);
|
||||
for(int y=0;y<180;y++)for(int x=0;x<320;x++)image.setRGB(x,y,x<160?0x567445:0xe5cf86);
|
||||
javax.imageio.ImageIO.write(image,"png",fixture.toFile());return null;
|
||||
});
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Pause233 "+UUID.randomUUID());s.setSeed("233");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.getInventory().setItem(6,new ItemStack(Items.OAK_LOG,17));p.getInventory().setItem(7,new ItemStack(Items.DIAMOND,3));});
|
||||
String legacy=UUID.randomUUID().toString();
|
||||
// Wait and dispatch together so the periodic following request cannot take the slot between them.
|
||||
c.waitFor(m->CommunityClient.request(null,"create",legacy,"","notice","event","Ancienne annonce","Texte historique",0,0,r->check(!r.has("error"),"Legacy-compatible notice seeded through the server")),200);
|
||||
c.waitFor(m->!CommunityClient.pending(),200);c.waitTicks(24);
|
||||
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(2);m.resizeGui();CommunityClient.open(new PauseScreen(true),"notice","",true);edit(m.gui.screen()).setValue("Du bois pour le pont");body(m.gui.screen()).setValue("Une annonce commune, avec une demande facultative.");composer(m.gui.screen(),"notice");press(m.gui.screen(),"sanctuary.community.task_configure");press(m.gui.screen(),"sanctuary.community.location_current");press(m.gui.screen(),"sanctuary.community.materials");edit(m.gui.screen()).setValue("minecraft:oak_log");});
|
||||
c.waitTicks(3);
|
||||
c.runOnClient(m->{widgets(m.gui.screen()).filter(CommunityMaterialsScreen.Slot.class::isInstance).map(CommunityMaterialsScreen.Slot.class::cast).filter(w->w.getMessage().getString().startsWith("minecraft:oak_log")).findFirst().orElseThrow().onPress(enter());widgets(m.gui.screen()).filter(EditBox.class::isInstance).map(EditBox.class::cast).toList().getLast().setValue("128");press(m.gui.screen(),"sanctuary.community.save");press(m.gui.screen(),"sanctuary.community.reward.none");press(m.gui.screen(),"sanctuary.community.reward.items");edit(m.gui.screen()).setValue("Un repas au village");press(m.gui.screen(),"sanctuary.community.save");check(body(m.gui.screen()).getValue().startsWith("Une annonce commune"),"Text survives task/material selector returns");composer(m.gui.screen(),"notice");});
|
||||
shot(c,"notice-configured");
|
||||
c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.publish"));
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.follow")&&!CommunityClient.pending(),400);
|
||||
server.runOnServer(s->checked(()->{var entry=store(s).list("notice","",0).entries().stream().filter(e->e.title().equals("Du bois pour le pont")).findFirst().orElseThrow();check(entry.category().equals("info")&&entry.task().materials().equals(List.of(new CommunityTask.Material("minecraft:oak_log",128)))&&entry.task().customReward().equals("Un repas au village"),"Requests and rewards persist without notice category selection");check(s.getPlayerList().getPlayers().getFirst().getInventory().getItem(6).getCount()==17&&s.getPlayerList().getPlayers().getFirst().getInventory().getItem(7).getCount()==3,"Publication never consumes real inventory");return null;}));
|
||||
c.waitTicks(24);
|
||||
c.runOnClient(m->CommunityClient.open(new PauseScreen(true),"notice",legacy,false));
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.actions")&&!CommunityClient.pending(),300);
|
||||
c.runOnClient(m->{press(m.gui.screen(),"sanctuary.community.actions");press(m.gui.screen(),"sanctuary.community.edit");composer(m.gui.screen(),"notice");body(m.gui.screen()).setValue("Texte historique corrigé");press(m.gui.screen(),"sanctuary.community.save");});
|
||||
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.follow")&&!CommunityClient.pending(),400);
|
||||
server.runOnServer(s->checked(()->{var entry=store(s).thread(legacy,0).entry();check(entry.category().equals("event")&&entry.body().equals("Texte historique corrigé"),"Editing preserves the hidden historical category");return null;}));
|
||||
c.waitTicks(24);
|
||||
|
||||
c.runOnClient(m->{CommunityClient.open(new PauseScreen(true),"article","",true);edit(m.gui.screen()).setValue("Gazette233");body(m.gui.screen()).setValue("La photo vient avant le titre et le texte.");composer(m.gui.screen(),"article");check(!button(m.gui.screen(),"publish").active,"Screenshot is still mandatory");press(m.gui.screen(),"sanctuary.community.photo_choose");});
|
||||
c.waitFor(m->widgets(m.gui.screen()).anyMatch(CommunityPhotoWidget.class::isInstance),500);
|
||||
c.runOnClient(m->widgets(m.gui.screen()).filter(CommunityPhotoWidget.class::isInstance).map(CommunityPhotoWidget.class::cast).findFirst().orElseThrow().onPress(enter()));
|
||||
c.runOnClient(m->{press(m.gui.screen(),"sanctuary.community.photo_assign");check(body(m.gui.screen()).getValue().equals("La photo vient avant le titre et le texte."),"Text survives screenshot selector return");composer(m.gui.screen(),"article");check(button(m.gui.screen(),"publish").active,"Assigned screenshot enables publication");});
|
||||
shot(c,"gazette-photo-assigned");c.runOnClient(m->press(m.gui.screen(),"sanctuary.community.publish"));
|
||||
c.waitFor(m->widgets(m.gui.screen()).anyMatch(CommunityArticle.class::isInstance)&&!CommunityClient.pending(),500);
|
||||
server.runOnServer(s->checked(()->{var entry=store(s).list("article","",0).entries().getFirst();check(entry.title().equals("Gazette233")&&entry.body().equals("La photo vient avant le titre et le texte.")&&!entry.photo().isEmpty(),"Article text and assigned photo survive publication");return null;}));
|
||||
|
||||
byte[] saved=server.computeOnServer(s->checked(()->Files.readAllBytes(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json"))));
|
||||
for(boolean unlocked:new boolean[]{false,true}) {
|
||||
server.runOnServer(s->atlas(s.getPlayerList().getPlayers().getFirst(),unlocked));c.waitFor(m->ProgressionClient.progress.atlas()==unlocked,200);
|
||||
for(String language:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});
|
||||
c.waitFor(m->widgets(m.gui.screen()).anyMatch(CommunityCard.class::isInstance),300);
|
||||
c.runOnClient(m->pause(m.gui.screen(),unlocked));shot(c,"pause-"+(unlocked?"unlocked-":"locked-")+language+"-"+scale);
|
||||
for(String kind:List.of("notice","article")) {
|
||||
c.runOnClient(m->{CommunityClient.open(new PauseScreen(true),kind,"",true);edit(m.gui.screen()).setValue("Brouillon233");body(m.gui.screen()).setValue("Le brouillon suit les changements de langue et de taille.");composer(m.gui.screen(),kind);});
|
||||
if(!unlocked){shot(c,"compose-"+kind+"-"+language+"-"+scale);c.runOnClient(m->{widgets(m.gui.screen()).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).filter(a->a.maxScrollAmount()>0).findFirst().ifPresent(a->a.setScrollAmount(a.maxScrollAmount()));});shot(c,"compose-bottom-"+kind+"-"+language+"-"+scale);}
|
||||
}
|
||||
}
|
||||
}
|
||||
server.runOnServer(s->checked(()->{check(Arrays.equals(saved,Files.readAllBytes(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json"))),"Reading/resizing old and new posts never rewrites stored categories");return null;}));
|
||||
System.out.println("PAUSE233_PASS: FR/EN GUI 2/3/4, parallel Adventure/Community before Atlas, narrow Community/Adventure stack beside Atlas, Game directly below, centered navigation block, visual Tab order, unified notice composer, descriptive task and photo publication, legacy category preservation");
|
||||
}finally {intro.set(false);checked(()->{if(previous==null)Files.deleteIfExists(config);else Files.write(config,previous);Files.deleteIfExists(fixture);return null;});}
|
||||
}
|
||||
private FileCommunityStore store(net.minecraft.server.MinecraftServer s)throws Exception{return new FileCommunityStore(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json"),serverId);}
|
||||
private static void atlas(ServerPlayer p,boolean unlocked){var r=ProgressRecord.INITIAL;if(unlocked)r=r.buy("atlas",r.cost("atlas",ProgressionService.session(p.level().getServer()).config));p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static AbstractWidget widget(Screen s,String key){return widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->key(w).equals("sanctuary.community."+key)).findFirst().orElseThrow();}
|
||||
private static Button button(Screen s,String key){return (Button)widget(s,key);}
|
||||
private static void composer(Screen s,String kind){
|
||||
check(widgets(s).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).noneMatch(w->key(w).startsWith("sanctuary.community.category.")),"No notice category control or label");
|
||||
var body=body(s);var extra=button(s,kind.equals("notice")?"task_configure":has(s,"sanctuary.community.photo_change")?"photo_change":"photo_choose");
|
||||
var publish=button(s,has(s,"sanctuary.community.save")?"save":"publish");
|
||||
if(kind.equals("notice")){var summary=widget(s,"task_summary");check(extra.getY()>body.getBottom()&&summary.getY()>extra.getBottom()&&publish.getY()>summary.getBottom(),"Text, request configuration, summary and Publish are in order");}
|
||||
else {check(extra.getBottom()<edit(s).getY()&&publish.getY()>body.getBottom(),"Screenshot selector is before title, then text and Publish");widgets(s).filter(CommunityPhotoWidget.class::isInstance).map(CommunityPhotoWidget.class::cast).forEach(w->check(w.getBottom()<extra.getY(),"Photo preview is first, before selector and title"));}
|
||||
}
|
||||
static void pause(Screen s,boolean unlocked){
|
||||
check((((PauseAtlasAccess)s).sanctuary$pauseMap()!=null)==unlocked,"Pause map appears only after Atlas purchase");
|
||||
var keys=Set.of("sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.story.title","sanctuary.progression.identity","sanctuary.cycle.faction","sanctuary.arena.title");
|
||||
var nav=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->keys.contains(key(b))).toList();check(nav.size()==6,"Six Sanctuary navigation actions");
|
||||
var options=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("menu.options")).findFirst().orElseThrow();
|
||||
var worldOptions=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("options.worldOptions.button")).findFirst().orElseThrow();
|
||||
var quit=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->Set.of("menu.returnToMenu","menu.disconnect").contains(key(b))).findFirst().orElseThrow();
|
||||
var community=widget(s,"pause_group_community");var adventure=widget(s,"pause_group_adventure");var game=widget(s,"pause_group_game");
|
||||
check(nav.stream().map(CommunityPausePanels::key).toList().equals(unlocked
|
||||
?List.of("sanctuary.progression.identity","sanctuary.cycle.faction","sanctuary.arena.title","sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.story.title")
|
||||
:List.of("sanctuary.progression.blocks","sanctuary.progression.identity","sanctuary.progression.progress","sanctuary.cycle.faction","sanctuary.story.title","sanctuary.arena.title")),"Navigation follows visual order in both layouts");
|
||||
for(var button:nav)check(s.getFont().width(button.getMessage())+8<=button.getWidth(),"Navigation labels fit with native button padding");
|
||||
check(nav.stream().allMatch(b->b.getBottom()<game.getY())&&game.getBottom()<options.getY()&&options.getBottom()<worldOptions.getY()&&worldOptions.getBottom()<quit.getY(),"Game follows below Sanctuary navigation");
|
||||
check(options.getX()==nav.get(0).getX()&&worldOptions.getX()==options.getX()&&quit.getX()==options.getX()&&worldOptions.getWidth()==options.getWidth()&&quit.getWidth()==options.getWidth(),"Native Game actions share the navigation block width");
|
||||
int bodyTop=s.height<280?46:50,bodyBottom=s.height-39;
|
||||
check(Math.abs((Math.min(adventure.getY(),community.getY())+quit.getBottom())/2.-(bodyTop+bodyBottom)/2.)<=.5&&quit.getBottom()+8<=bodyBottom,"Complete block centered with clear space below Game");
|
||||
// KeyEvent carries the physical key and the SDL shortcut key separately.
|
||||
s.setFocused(nav.get(0));s.keyPressed(new KeyEvent(InputConstants.KEY_TAB,'\t',0));check(s.getFocused()==nav.get(1),"Tab follows the second visible action");
|
||||
s.keyPressed(new KeyEvent(InputConstants.KEY_TAB,'\t',0));check(s.getFocused()==nav.get(2),"Tab follows the third visible action");
|
||||
if(!unlocked){
|
||||
check(adventure.getY()==community.getY()&&adventure.getX()==nav.get(0).getX()&&community.getX()==nav.get(1).getX()&&adventure.getBottom()<nav.get(0).getY(),"Both headings align above their columns");
|
||||
for(int row=0;row<3;row++) {
|
||||
var left=nav.get(2*row);var right=nav.get(2*row+1);
|
||||
check(left.getY()==right.getY()&&left.getHeight()==right.getHeight()&&left.getRight()<right.getX(),"Two parallel columns without overlap");
|
||||
if(row>0)check(nav.get(2*row-2).getBottom()<left.getY(),"Rows remain separated");
|
||||
}
|
||||
check(options.getRight()==nav.get(1).getRight(),"Game spans both columns before Atlas");
|
||||
check(Math.abs((nav.get(0).getX()+nav.get(1).getRight())/2.-s.width/2.)<=.5,"Two-column block centered horizontally");
|
||||
check(widget(s,"article").getX()==8&&widget(s,"article").getRight()<nav.get(0).getX()&&widget(s,"notice").getX()>nav.get(1).getRight(),"Gazette remains left and Notice Board right");
|
||||
}
|
||||
else {
|
||||
check(community.getBottom()<nav.get(0).getY()&&nav.get(2).getBottom()<adventure.getY()&&adventure.getBottom()<nav.get(3).getY(),"Community then Adventure stacked above Game with Atlas");
|
||||
for(int i=0;i<nav.size();i++) {
|
||||
var button=nav.get(i);check(button.getX()==8&&button.getWidth()==options.getWidth()&&button.getWidth()<=132,"All actions use one narrow left column");
|
||||
if(i>0)check(nav.get(i-1).getBottom()<button.getY(),"Stacked buttons remain separated");
|
||||
}
|
||||
var map=((PauseAtlasAccess)s).sanctuary$pauseMap();check(nav.get(0).getRight()<map.getX()&&map.getRight()<widget(s,"article").getX()&&widget(s,"article").getX()==widget(s,"notice").getX(),"Narrow navigation left, map center and community feeds right");
|
||||
}
|
||||
}
|
||||
private void shot(ClientGameTestContext c,String name){var path=c.takeScreenshot("pause233-"+name);checked(()->{var dir=game.resolve("../../../../../build/pause233-screenshots").normalize();Files.createDirectories(dir);Files.copy(path,dir.resolve(name+".png"),StandardCopyOption.REPLACE_EXISTING);return null;});}
|
||||
private interface Checked<T>{T run()throws Exception;}
|
||||
private static <T>T checked(Checked<T> action){try{return action.run();}catch(Exception e){throw new AssertionError(e);}}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.mining.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.realtime.RealtimeService;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.PauseScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Held input and real command/result packets, with a non-operator integrated owner. */
|
||||
public final class Solo232ClientChecks implements FabricClientGameTest {
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(8);m.options.simulationDistance().set(8);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Solo232 "+UUID.randomUUID());s.setSeed("232");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;check(!fr.koka.sanctuary.operator.OperatorService.allowed(p),"Test owner has no operator commands");});
|
||||
c.runOnClient(m->{var time=m.player.connection.getCommands().getRoot().getChild("sanctuary").getChild("time");check(time.getChild("timezone")!=null&&time.getChild("vanilla")==null,"Solo owner sees timezone but no operator rule controls");m.player.connection.sendCommand("sanctuary time timezone America/New_York");});
|
||||
server.waitFor(s->RealtimeService.civilTime(s).getZone().equals(ZoneId.of("America/New_York")),200);
|
||||
server.runOnServer(s->checked(()->{RealtimeService.reload(s);check(RealtimeService.civilTime(s).getZone().equals(ZoneId.of("America/New_York")),"Timezone survives reload without op");}));
|
||||
c.runOnClient(m->m.player.connection.sendCommand("sanctuary time timezone auto"));
|
||||
server.waitFor(s->RealtimeService.civilTime(s).getZone().equals(ZoneId.systemDefault()),200);
|
||||
for(boolean unlocked:new boolean[]{false,true}) {
|
||||
server.runOnServer(s->ranks(s.getPlayerList().getPlayers().getFirst(),unlocked));
|
||||
c.waitFor(m->ProgressionClient.progress.atlas()==unlocked,200);
|
||||
for(String language:List.of("fr_fr","en_us")) for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});
|
||||
c.waitForScreen(PauseScreen.class);c.waitTicks(3);
|
||||
c.runOnClient(m->{var screen=m.gui.screen();var map=((PauseAtlasAccess)screen).sanctuary$pauseMap();check((map!=null)==unlocked,"Map widget exists only after Atlas purchase");
|
||||
var buttons=screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->Set.of("sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.story.title","sanctuary.progression.identity","sanctuary.cycle.faction","sanctuary.arena.title","menu.options","options.worldOptions.button","menu.returnToMenu","menu.disconnect").contains(CommunityPausePanels.key(b))).toList();
|
||||
check(buttons.size()==9,"All nine pause menu actions remain present");
|
||||
var article=heading(screen,"sanctuary.community.article");var notice=heading(screen,"sanctuary.community.notice");
|
||||
if(!unlocked){check(article.getX()==8&&article.getY()==(screen.height<280?46:50)&¬ice.getY()==article.getY(),"Locked map uses full-height Gazette on the left and Notice Board on the right");for(var button:buttons)check(article.getRight()<button.getX()&¬ice.getX()>button.getRight(),"Community columns do not overlap centered navigation");}
|
||||
else check(article.getX()==notice.getX()&&article.getY()<notice.getY(),"Unlocked map retains both community feeds stacked on the right");
|
||||
for(var button:buttons)check(button.getY()>=50&&button.getBottom()<=screen.height-33,"Button stays in pause body");
|
||||
Pause233ClientChecks.pause(screen,unlocked);
|
||||
});
|
||||
String shot="solo232-pause-"+(unlocked?"unlocked-":"locked-")+language+"-"+scale;
|
||||
var image=c.takeScreenshot(shot);checked(()->{var dir=net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir().resolve("../../../../../build/solo232-screenshots").normalize();java.nio.file.Files.createDirectories(dir);java.nio.file.Files.copy(image,dir.resolve(shot+".png"),java.nio.file.StandardCopyOption.REPLACE_EXISTING);});
|
||||
c.runOnClient(m->m.gui.setScreen(null));c.waitTicks(2);
|
||||
}
|
||||
}
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setOnGround(true);
|
||||
for(int x=-3;x<=3;x++)for(int z=-2;z<=8;z++){p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.BEDROCK.defaultBlockState());for(int y=1;y<=5;y++)p.level().setBlockAndUpdate(new BlockPos(x,y,z),Blocks.AIR.defaultBlockState());}
|
||||
for(int x=0;x<=1;x++)for(int y=2;y<=3;y++){p.level().setBlockAndUpdate(new BlockPos(x,y,3),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(new BlockPos(x,y,5),Blocks.COBBLESTONE.defaultBlockState());}
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Items.DIAMOND_PICKAXE));p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Items.DIAMOND_PICKAXE)&&m.level.getBlockState(new BlockPos(0,2,3)).is(Blocks.STONE),200);c.waitTicks(6);
|
||||
c.getInput().lookAt(new BlockPos(0,2,3));c.getInput().holdKey(InputConstants.KEY_R);c.waitFor(m->MiningClient.previewSize()==4,200);
|
||||
c.getInput().holdKey(o->o.keyAttack);server.waitFor(s->MiningManager.active(s.getPlayerList().getPlayers().getFirst()),200);
|
||||
c.getInput().lookAt(new BlockPos(0,2,5));
|
||||
server.waitFor(s->s.overworld().getBlockState(new BlockPos(0,2,5)).isAir(),500);
|
||||
c.getInput().releaseKey(o->o.keyAttack);c.getInput().releaseKey(InputConstants.KEY_R);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();for(int x=0;x<=1;x++)for(int y=2;y<=3;y++)for(int z:new int[]{3,5})check(p.level().getBlockState(new BlockPos(x,y,z)).isAir(),"Both successive groups are mined while input stays held");check(p.getMainHandItem().getDamageValue()==8,"Eight breaks cost eight native tool uses");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();MiningManager.forget(p);for(int x=0;x<=1;x++)for(int y=2;y<=3;y++){p.level().setBlockAndUpdate(new BlockPos(x,y,3),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(new BlockPos(x,y,5),Blocks.COBBLESTONE.defaultBlockState());}var fragile=new ItemStack(Items.DIAMOND_PICKAXE);fragile.setDamageValue(fragile.getMaxDamage()-1);p.setItemInHand(InteractionHand.MAIN_HAND,fragile);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->m.player.getMainHandItem().is(Items.DIAMOND_PICKAXE)&&m.level.getBlockState(new BlockPos(0,2,3)).is(Blocks.STONE),200);c.waitTicks(4);
|
||||
c.getInput().lookAt(new BlockPos(0,2,3));c.getInput().holdKey(InputConstants.KEY_R);c.waitFor(m->MiningClient.previewSize()==4,200);c.getInput().holdKey(o->o.keyAttack);
|
||||
server.waitFor(s->s.getPlayerList().getPlayers().getFirst().getMainHandItem().isEmpty(),300);c.waitTicks(35);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();int remaining=0;for(int x=0;x<=1;x++)for(int y=2;y<=3;y++){if(p.level().getBlockState(new BlockPos(x,y,3)).is(Blocks.STONE))remaining++;check(p.level().getBlockState(new BlockPos(x,y,5)).is(Blocks.COBBLESTONE),"Broken tool cannot restart on another group");}check(remaining==3&&!MiningManager.active(p),"Tool break interrupts held mining after one paid break");});
|
||||
c.getInput().releaseKey(o->o.keyAttack);c.getInput().releaseKey(InputConstants.KEY_R);
|
||||
System.out.println("SOLO232_PASS: timezone owner permissions and real commands, persistent override, FR/EN pause map gating and centered buttons, held mining follows next target with native costs");
|
||||
}finally{intro.set(false);c.getInput().releaseKey(o->o.keyAttack);c.getInput().releaseKey(InputConstants.KEY_R);}
|
||||
new Balance080ClientChecks().runTest(c);
|
||||
}
|
||||
private static void ranks(ServerPlayer p,boolean atlas){var r=ProgressRecord.INITIAL;var config=ProgressionService.session(p.level().getServer()).config;for(int i=0;i<7;i++)r=r.buy("mining",r.cost("mining",config));if(atlas)r=r.buy("atlas",r.cost("atlas",config));p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");}
|
||||
private static AbstractWidget heading(net.minecraft.client.gui.screens.Screen screen,String key){return screen.children().stream().filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).filter(w->w.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow();}
|
||||
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private interface Checked {void run()throws Exception;}
|
||||
private static void checked(Checked action){try{action.run();}catch(Exception e){throw new RuntimeException(e);}}
|
||||
private static void check(boolean ok,String reason){if(!ok)throw new AssertionError(reason);}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.CharacterEvent;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Real native events on Story and Pause, using a fresh non-operator solo world. */
|
||||
public final class Story234ClientChecks implements FabricClientGameTest {
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
var intro=new AtomicBoolean(true);
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Story234 "+UUID.randomUUID());s.setSeed("234");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().getAbilities().invulnerable=true);
|
||||
for(boolean unlocked:new boolean[]{false,true}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var r=ProgressRecord.INITIAL;if(unlocked)r=r.buy("atlas",r.cost("atlas",ProgressionService.session(s).config));p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas()==unlocked,200);
|
||||
String before=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getAttached(ProgressionService.RECORD));
|
||||
for(String language:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});
|
||||
c.waitTicks(3);
|
||||
c.runOnClient(m->Pause233ClientChecks.pause(m.gui.screen(),unlocked));
|
||||
shot(c,"pause-"+unlocked+"-"+language+"-"+scale);
|
||||
c.runOnClient(m->{var pause=m.gui.screen();press(pause,"sanctuary.story.title");check(m.gui.screen() instanceof StoryScreen,"Story opens from Pause without Atlas gating");check(m.gui.screen().isPauseScreen(),"Story retains solo pause");check(m.gui.screen().getTitle().getString().equals("Story"),"Story has its own accessible screen title");check(edit(m.gui.screen()).getValue().isEmpty()&&cycle(m.gui.screen()).getValue().toString().equals("ALL"),"Empty search and All on opening");assertEntries(m.gui.screen(),100);layout(m.gui.screen());});
|
||||
shot(c,"story-"+language+"-"+scale);
|
||||
c.runOnClient(m->{var s=m.gui.screen();var search=edit(s);s.setFocused(search);for(int codepoint:"100".codePoints().toArray()){check(s.charTyped(new CharacterEvent(codepoint)),"Native typing handled");check(s.getFocused()==search&&search.isFocused(),"List refresh retains search focus");}check(search.getValue().equals("100")&&search.getCursorPosition()==3,"Continuous typing retains cursor");assertEntries(s,1);check(entries(s).getFirst().getMessage().getString().equals(StoryScreen.label("unknown_entry",100).getString()),"Search selects story 100");search.setValue(" 100 ");assertEntries(s,1);cycle(s).onPress(enter());check(has(s,"sanctuary.story.no_discoveries")&&!has(s,"sanctuary.story.no_results")&&entries(s).isEmpty(),"Discovered has a distinct empty state");cycle(s).onPress(enter());check(cycle(s).getValue().toString().equals("UNKNOWN"),"Third filter is Unknown");assertEntries(s,1);search.setValue("absent-story");check(has(s,"sanctuary.story.no_results")&&!has(s,"sanctuary.story.no_discoveries"),"Search empty state is distinct");search.setValue(language.equals("fr_fr")?"INCONNUE":"UNKNOWN");assertEntries(s,100);var area=area(s);check(s.mouseScrolled(area.getX()+5,area.getY()+5,0,-5)&&area.scrollAmount()>0,"Native wheel scrolls Story");area.setScrollAmount(area.maxScrollAmount());check(entries(s).getLast().getBottom()<=area.getBottom(),"Story 100 can be reached at the end");area.setScrollAmount(100);});
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1344,1008);m.resizeGui();});c.waitTicks(3);
|
||||
c.runOnClient(m->{var s=m.gui.screen();check(edit(s).getValue().equals(language.equals("fr_fr")?"INCONNUE":"UNKNOWN")&&cycle(s).getValue().toString().equals("UNKNOWN"),"Resize retains query and filter");check(area(s).scrollAmount()==100,"Resize retains scroll position");layout(s);edit(s).setValue("100");check(area(s).scrollAmount()==0,"Search resets scroll");edit(s).setValue("");area(s).setScrollAmount(100);cycle(s).onPress(enter());check(cycle(s).getValue().toString().equals("ALL")&&area(s).scrollAmount()==0,"Filter resets scroll and cycles back to All");assertEntries(s,100);});
|
||||
if(!unlocked&&language.equals("fr_fr")&&scale==2) {
|
||||
c.runOnClient(m->cycle(m.gui.screen()).onPress(enter()));shot(c,"empty-discovered");
|
||||
c.runOnClient(m->{cycle(m.gui.screen()).onPress(enter());edit(m.gui.screen()).setValue("absent-story");});shot(c,"empty-search");
|
||||
c.runOnClient(m->edit(m.gui.screen()).setValue("100"));shot(c,"search-100");
|
||||
}
|
||||
c.runOnClient(m->{var story=(StoryScreen)m.gui.screen();var pause=story.parent;story.keyPressed(new KeyEvent(InputConstants.KEY_ESCAPE,0,0));check(m.gui.screen()==pause,"Escape returns directly to original Pause");press(pause,"sanctuary.story.title");press(m.gui.screen(),"gui.done");check(m.gui.screen()==pause,"Done returns directly to original Pause");m.gui.setScreen(null);});
|
||||
c.waitTicks(2);
|
||||
}
|
||||
server.runOnServer(s->check(before.equals(s.getPlayerList().getPlayers().getFirst().getAttached(ProgressionService.RECORD)),"Reading and searching Story leave authoritative progression unchanged"));
|
||||
}
|
||||
System.out.println("STORY234_PASS: Vulkan FR/EN GUI 2/3/4, Atlas gating, 100 disabled entries, live search, three filters, distinct empty states, typing focus, wheel, resize, Escape/Done, unchanged progression");
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
|
||||
private static List<Button> entries(Screen s){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("sanctuary.story.unknown_entry")).toList();}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static CycleButton<?> cycle(Screen s){return widgets(s).filter(CycleButton.class::isInstance).map(CycleButton.class::cast).findFirst().orElseThrow();}
|
||||
private static AbstractScrollArea area(Screen s){return widgets(s).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).findFirst().orElseThrow();}
|
||||
private static void assertEntries(Screen s,int expected){var rows=entries(s);check(rows.size()==expected,"Expected "+expected+" entries, got "+rows.size());check(rows.stream().allMatch(b->!b.active),"Unknown entries stay disabled");if(expected==100)for(int i=0;i<100;i++)check(rows.get(i).getMessage().getString().equals(StoryScreen.label("unknown_entry",i+1).getString()),"Numbers stay ordered without gaps or duplicates");}
|
||||
private static void layout(Screen s){var search=edit(s);var filter=cycle(s);var list=area(s);check(search.getBottom()<filter.getY()&&filter.getBottom()<list.getY(),"Search, filter and list do not overlap");for(var child:s.children())if(child instanceof AbstractWidget w)check(w.getX()>=0&&w.getRight()<=s.width&&w.getY()>=0&&w.getBottom()<=s.height,"Top-level widget stays inside the screen");var done=widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("gui.done")).findFirst().orElseThrow();check(list.getBottom()<done.getY(),"List leaves room for native footer");}
|
||||
private static void shot(ClientGameTestContext c,String name){var source=c.takeScreenshot("story234-"+name);try{var dir=FabricLoader.getInstance().getGameDir().resolve("../../../../../build/story234-screenshots").normalize();Files.createDirectories(dir);Files.copy(source,dir.resolve(name+".png"),StandardCopyOption.REPLACE_EXISTING);}catch(Exception e){throw new AssertionError(e);}}
|
||||
}
|
||||
@@ -46,6 +46,20 @@ public final class Realtime037GameTests {
|
||||
h.assertTrue(RealtimeService.setMode(s,false),"Return to native clock");var nativeState=clocks.getInstance(over).packState();h.assertTrue(nativeState.rate()==2&&nativeState.paused()&&nativeState.totalTicks()==first.totalTicks(),"Native parameters restored at current time");
|
||||
clocks.getInstance(over).loadFrom(new ClockState(nativeState.totalTicks(),0,3,false));RealtimeService.setMode(s,false);RealtimeService.reload(s);
|
||||
h.assertTrue(clocks.getInstance(over).rate()==3&&!clocks.getInstance(over).isPaused(),"Repeated vanilla command/reload preserves subsequent operator edits");
|
||||
var contract=s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-realtime.json");
|
||||
var vanillaBefore=clocks.getInstance(over).packState();
|
||||
h.assertTrue(!node.getChild("timezone").canUse(p.createCommandSourceStack()),"Dedicated non-operator cannot change the world's timezone");
|
||||
h.assertTrue(commands.execute("sanctuary time timezone Asia/Tokyo",source)==1,"Timezone command accepts IANA names including slash");
|
||||
h.assertTrue(RealtimeService.civilTime(s).getZone().equals(ZoneId.of("Asia/Tokyo"))&&clocks.getInstance(over).packState().equals(vanillaBefore),"Timezone choice preserves Vanilla mode and live clock");
|
||||
byte[] chosen=Files.readAllBytes(contract);
|
||||
h.assertTrue(commands.execute("sanctuary time timezone Asia/Tokyo",source)==1&&java.util.Arrays.equals(chosen,Files.readAllBytes(contract)),"Repeated timezone choice is idempotent");
|
||||
h.assertTrue(commands.execute("sanctuary time timezone Unknown/Nowhere",source)==0&&java.util.Arrays.equals(chosen,Files.readAllBytes(contract)),"Unknown timezone preserves saved contract");
|
||||
RealtimeService.reload(s);
|
||||
h.assertTrue(RealtimeService.civilTime(s).getZone().equals(ZoneId.of("Asia/Tokyo")),"World override survives reload");
|
||||
Files.writeString(contract,"{external-conflict");
|
||||
h.assertTrue(commands.execute("sanctuary time timezone Europe/London",source)==0&&RealtimeService.civilTime(s).getZone().equals(ZoneId.of("Asia/Tokyo"))&&Files.readString(contract).equals("{external-conflict"),"Atomic conflict keeps current timezone and external data");
|
||||
Files.write(contract,chosen);
|
||||
h.assertTrue(commands.execute("sanctuary time timezone auto",source)==1&&RealtimeService.civilTime(s).getZone().equals(new RealtimeConfig(true,"Europe/Paris","auto").zone(s.isSingleplayer())),"Auto restores configured timezone");
|
||||
commands.execute("time set 7000",source);h.assertTrue(clocks.getInstance(over).totalTicks()==7000,"Native command works again after opt-out");
|
||||
RealtimeService.setMode(s,true);Files.writeString(configPath,new RealtimeConfig(false,"Europe/Paris","auto").json());RealtimeService.reload(s);
|
||||
h.assertTrue(!RealtimeService.active(h.getLevel())&&clocks.getInstance(over).rate()==3,"Global disable restores the last native rate");
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.worldgen.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.storage.loot.*;
|
||||
import net.minecraft.world.level.storage.loot.entries.LootItem;
|
||||
import net.minecraft.world.level.storage.loot.parameters.*;
|
||||
import net.minecraft.world.level.storage.loot.providers.number.ints.ConstantValue;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public final class Solo232GameTests {
|
||||
@GameTest public void originContainersExcludeDiamondsAndPreserveOtherLoot(GameTestHelper h) {
|
||||
var level=h.getLevel();
|
||||
h.assertTrue(level.getChunkSource().getGenerator() instanceof SanctuaryChunkGenerator,"Exercise actual Sanctuary generator");
|
||||
var generator=(SanctuaryChunkGenerator)level.getChunkSource().getGenerator();
|
||||
var presets=level.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.WORLD_PRESET);
|
||||
for(var size:fr.koka.sanctuarytest.SharedIsland196.Size.values()){
|
||||
var current=presets.getValue(size.id()).createWorldDimensions().overworld();
|
||||
h.assertTrue(IslandChestLoot.restricted(current,new Vec3(0,180,0)),"Current noise-based profile excludes island diamonds: "+size);
|
||||
h.assertFalse(IslandChestLoot.restricted(current,new Vec3(size.diameter*3,180,size.diameter*3)),"Current profile preserves distant region loot: "+size);
|
||||
}
|
||||
var root=generator.islandAt(0,0);
|
||||
h.assertTrue(root!=null&&root.origin(),"Initial island is registered");
|
||||
var table=LootTable.lootTable().setParamSet(LootContextParamSets.CHEST)
|
||||
.withPool(LootPool.lootPool().setRolls(net.minecraft.core.Holder.direct(new ConstantValue(1))).add(LootItem.lootTableItem(Items.DIAMOND)))
|
||||
.withPool(LootPool.lootPool().setRolls(net.minecraft.core.Holder.direct(new ConstantValue(1))).add(LootItem.lootTableItem(Items.IRON_INGOT))).build();
|
||||
for(long seed:new long[]{0,42,232}) {
|
||||
var origin=new Vec3(0,180,0);
|
||||
var params=new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN,origin).create(LootContextParamSets.CHEST);
|
||||
h.assertTrue(table.getRandomItems(params,seed).stream().anyMatch(s->s.is(Items.DIAMOND)),"Raw loot outside container fill remains native");
|
||||
var chest=new SimpleContainer(27);chest.setItem(0,new ItemStack(Items.DIAMOND,9));
|
||||
table.fill(chest,params,seed);
|
||||
h.assertTrue(chest.countItem(Items.DIAMOND)==9&&chest.countItem(Items.IRON_INGOT)==1,"Only generated diamonds are removed; deposited diamonds and iron stay");
|
||||
var outside=new Vec3(root.centerX()+root.diameter()*3,180,root.centerZ()+root.diameter()*3);
|
||||
h.assertFalse(IslandChestLoot.restricted(level,outside),"Void or expansion outside origin is unrestricted");
|
||||
var other=new SimpleContainer(27);table.fill(other,new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN,outside).create(LootContextParamSets.CHEST),seed);
|
||||
h.assertTrue(other.countItem(Items.DIAMOND)==1&&other.countItem(Items.IRON_INGOT)==1,"Other regions retain original chest loot");
|
||||
}
|
||||
h.assertFalse(IslandChestLoot.restricted(level,null),"Missing context never suppresses unrelated loot");
|
||||
var mine=level.getServer().reloadableRegistries().getLootTable(BuiltInLootTables.ABANDONED_MINESHAFT);
|
||||
var params=new LootParams.Builder(level).withParameter(LootContextParams.ORIGIN,new Vec3(0,180,0)).create(LootContextParamSets.CHEST);
|
||||
boolean nativeDiamond=false;
|
||||
for(int seed=1;seed<=256;seed++) {
|
||||
nativeDiamond|=mine.getRandomItems(params,seed).stream().anyMatch(s->s.is(Items.DIAMOND));
|
||||
var container=new SimpleContainer(27);mine.fill(container,params,seed);
|
||||
h.assertTrue(container.countItem(Items.DIAMOND)==0,"Native mineshaft chest seed "+seed+" contains no diamonds");
|
||||
}
|
||||
h.assertTrue(nativeDiamond,"Native mineshaft table would actually have yielded diamonds");
|
||||
h.succeed();
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,8 @@ public final class BuildReach {
|
||||
return new AABB(support).distanceToSqr(player.getEyePosition())<range*range;
|
||||
}
|
||||
public static double pickRange(Player player) {
|
||||
double base=Math.max(player.blockInteractionRange(),fr.koka.sanctuary.mining.MiningReach.range(player));
|
||||
return player.getMainHandItem().getItem() instanceof BlockItem || player.getOffhandItem().getItem() instanceof BlockItem
|
||||
? Math.max(base,range(player)) : base;
|
||||
? range(player) : Math.max(player.blockInteractionRange(),fr.koka.sanctuary.mining.MiningReach.range(player));
|
||||
}
|
||||
public static boolean grouped(Player player) {
|
||||
return player instanceof ServerPlayer server && BuildingManager.placing(server);
|
||||
|
||||
@@ -3,7 +3,7 @@ package fr.koka.sanctuary.building;
|
||||
/** Surface size and placement reach progress independently; entity reach stays native. */
|
||||
public final class BuildingLimits {
|
||||
public static final int MAX_RANK=7;
|
||||
private static final double[] REACH={3,3.5,4,4.5,4.75,5,5.5,6};
|
||||
private static final double[] REACH={3,3.5,4,4.5,5,5.5,6,7};
|
||||
private BuildingLimits() { }
|
||||
public static double reach(int rank) {return REACH[Math.clamp(rank,0,MAX_RANK)];}
|
||||
public static int diameter(int rank) {return 1+2*Math.clamp(rank,0,MAX_RANK);}
|
||||
|
||||
@@ -17,7 +17,11 @@ abstract class BlocodexFrame extends Screen {
|
||||
private HeaderAndFooterLayout frame;
|
||||
|
||||
protected BlocodexFrame(Screen parent) {
|
||||
super(ProgressionService.label("title"));
|
||||
this(parent,ProgressionService.label("title"));
|
||||
}
|
||||
|
||||
protected BlocodexFrame(Screen parent,net.minecraft.network.chat.Component pageTitle) {
|
||||
super(pageTitle);
|
||||
this.parent=parent;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,14 @@ import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.component.ResolvableProfile;
|
||||
import static fr.koka.sanctuary.client.CommunityClient.*;
|
||||
|
||||
/** A single accessible card, with a face/title row and a category-coloured frame. */
|
||||
/** A single accessible card, with a face/title row and a shared notice style. */
|
||||
final class CommunityCard extends AbstractButton implements AutoCloseable {
|
||||
static final int NOTICE_COLOR=0xffead47e,NOTICE_BACKGROUND=0xff34301f;
|
||||
private final JsonObject entry;
|
||||
private final Runnable action;
|
||||
private final PlayerFaceWidget face;
|
||||
private final boolean compact,article;
|
||||
private CommunityPhotoWidget photo;
|
||||
static int color(String category) {return switch(category){case "event"->0xffba93dd;case "work"->0xffd4ac71;case "need"->0xff80c1a4;default->0xffead47e;};}
|
||||
static int background(String category) {return switch(category){case "event"->0xff2e253a;case "work"->0xff322b20;case "need"->0xff20322c;default->0xff34301f;};}
|
||||
CommunityCard(int width,JsonObject entry,boolean compact,Runnable action) {
|
||||
super(0,0,width,compact?44:90,Component.literal(str(entry,"title")));this.entry=entry;this.compact=compact;this.action=action;article=str(entry,"kind").equals("article");
|
||||
face=new PlayerFaceWidget(compact?20:24,ResolvableProfile.createUnresolved(UUID.fromString(str(entry,"authorId"))));
|
||||
@@ -35,8 +34,8 @@ final class CommunityCard extends AbstractButton implements AutoCloseable {
|
||||
PlayerFaceWidget face(){return face;}
|
||||
@Override public void onPress(InputWithModifiers input){action.run();}
|
||||
@Override protected void extractContents(GuiGraphicsExtractor g,int mx,int my,float delta) {
|
||||
int x=getX(),y=getY(),accent=article?0xffaebfae:color(str(entry,"category"));var font=Minecraft.getInstance().font;
|
||||
g.fill(x,y,getRight(),getBottom(),article?0xff202720:background(str(entry,"category")));
|
||||
int x=getX(),y=getY(),accent=article?0xffaebfae:NOTICE_COLOR;var font=Minecraft.getInstance().font;
|
||||
g.fill(x,y,getRight(),getBottom(),article?0xff202720:NOTICE_BACKGROUND);
|
||||
g.fill(x,y,x+3,getBottom(),accent);
|
||||
if(isHoveredOrFocused()){g.fill(x,y,getRight(),y+1,accent);g.fill(x,getBottom()-1,getRight(),getBottom(),accent);g.fill(getRight()-1,y,getRight(),getBottom(),accent);}
|
||||
int top=y+6;
|
||||
@@ -48,11 +47,11 @@ final class CommunityCard extends AbstractButton implements AutoCloseable {
|
||||
int tx=x+(compact?33:37),tw=width-(compact?40:44);
|
||||
var lines=font.split(getMessage(),tw);
|
||||
for(int i=0;i<Math.min(compact?1:2,lines.size());i++)g.text(font,lines.get(i),tx,top+(compact?6:i*10),0xfff4f0e8,false);
|
||||
if(compact)g.text(font,font.plainSubstrByWidth(text("category."+str(entry,"category")).append(" · ").append(text(str(entry,"status"))).getString(),width-14),x+7,y+31,accent,false);
|
||||
if(compact)g.text(font,font.plainSubstrByWidth(text(str(entry,"status")).getString(),width-14),x+7,y+31,accent,false);
|
||||
else {
|
||||
g.text(font,font.plainSubstrByWidth(str(entry,"authorName"),width-14),x+7,top+30,0xffd6d4c9,false);
|
||||
g.text(font,font.plainSubstrByWidth(CommunityScreen.date(entry),width-14),x+7,top+42,0xffa2aaa4,false);
|
||||
if(!article)g.text(font,font.plainSubstrByWidth(text("category."+str(entry,"category")).append(" · ").append(text(str(entry,"status"))).getString(),width-14),x+7,top+59,accent,false);
|
||||
if(!article)g.text(font,font.plainSubstrByWidth(text(str(entry,"status")).getString(),width-14),x+7,top+59,accent,false);
|
||||
}
|
||||
}
|
||||
@Override protected void updateWidgetNarration(NarrationElementOutput output){defaultButtonNarrationText(output);}
|
||||
|
||||
@@ -23,7 +23,7 @@ final class CommunityMapMarkers {
|
||||
var e=value.getAsJsonObject();
|
||||
if(!CommunityClient.str(e,"status").equals("open") || !e.has("task") || e.get("task").isJsonNull())continue;
|
||||
var task=new com.google.gson.Gson().fromJson(e.get("task"),CommunityTask.class);
|
||||
if(task.location()!=null)next.add(new Marker(CommunityClient.str(e,"id"),CommunityClient.str(e,"title"),CommunityClient.str(e,"authorName"),CommunityClient.str(e,"summary"),task,CommunityCard.color(CommunityClient.str(e,"category"))));
|
||||
if(task.location()!=null)next.add(new Marker(CommunityClient.str(e,"id"),CommunityClient.str(e,"title"),CommunityClient.str(e,"authorName"),CommunityClient.str(e,"summary"),task,CommunityCard.NOTICE_COLOR));
|
||||
}
|
||||
markers=List.copyOf(next);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ final class CommunityNoticeScene extends AbstractWidget {
|
||||
this.entry=entry;
|
||||
portraitWidth=Math.clamp(width/4,56,94);bubbleX=portraitWidth+10;
|
||||
var font=Minecraft.getInstance().font;int wrap=Math.max(36,width-bubbleX-18);
|
||||
heading=font.split(text("category."+str(entry,"category")).append(" · ").append(text(str(entry,"status"))),wrap);
|
||||
heading=font.split(text(str(entry,"status")),wrap);
|
||||
title=font.split(Component.literal(str(entry,"title")).withStyle(net.minecraft.ChatFormatting.BOLD),wrap);
|
||||
body=font.split(Component.literal(str(entry,"body")),wrap);
|
||||
height=Math.max(154,28+(heading.size()+title.size()+body.size())*11);
|
||||
@@ -44,10 +44,10 @@ final class CommunityNoticeScene extends AbstractWidget {
|
||||
float yaw(){return portrait.yaw;}
|
||||
float pitch(){return portrait.pitch;}
|
||||
@Override protected void extractWidgetRenderState(GuiGraphicsExtractor g,int mx,int my,float delta) {
|
||||
int x=getX(),y=getY(),bx=x+bubbleX,accent=CommunityCard.color(str(entry,"category"));
|
||||
int x=getX(),y=getY(),bx=x+bubbleX,accent=CommunityCard.NOTICE_COLOR;
|
||||
g.fill(x,y,getRight(),getBottom(),0xA013171C);
|
||||
portrait.setPosition(x,y+6);portrait.extractRenderState(g,mx,my,delta);
|
||||
g.fill(bx,y,getRight(),getBottom(),CommunityCard.background(str(entry,"category")));
|
||||
g.fill(bx,y,getRight(),getBottom(),CommunityCard.NOTICE_BACKGROUND);
|
||||
g.outline(bx,y,width-bubbleX,height,accent);
|
||||
for(int step=0;step<6;step++)g.fill(bx-step-1,y+39+step,bx-step,y+50-step,accent);
|
||||
int top=y+9;
|
||||
|
||||
@@ -23,39 +23,63 @@ public final class CommunityPausePanels implements AutoCloseable {
|
||||
private boolean closed;
|
||||
public CommunityPausePanels(Screen screen,Consumer<AbstractWidget> add,List<Button> nativeButtons) {
|
||||
this.screen=screen;
|
||||
int margin=8,gap=10,top=50,bottom=screen.height-39;
|
||||
int left=Math.min(132,Math.max(76,screen.width/5)),right=Math.min(244,Math.max(104,screen.width*31/100));
|
||||
int centerX=margin+left+gap,rightX=screen.width-margin-right,centerWidth=rightX-gap-centerX;
|
||||
int optionHeight=screen.height<280?16:20,navGap=screen.height<280?3:4,groupGap=screen.height<280?8:12;
|
||||
int optionsTop=bottom-(3*optionHeight+8);
|
||||
int navHeight=Math.min(20,Math.max(11,(optionsTop-top-8-4*navGap-groupGap)/5));
|
||||
boolean compact=screen.height<280;
|
||||
boolean unlocked=ProgressionClient.progress.atlas();
|
||||
int margin=8,gap=10,top=compact?46:50,bottom=screen.height-39;
|
||||
String[] sections={"blocks","identity","progress","faction","story","battle"};
|
||||
int columnGap=compact?4:8;
|
||||
int minimumWidth=Arrays.stream(sections).map(CommunityPausePanels::sectionLabel).mapToInt(Minecraft.getInstance().font::width).max().orElse(0)+8;
|
||||
int columnWidth=Math.max(minimumWidth,(Math.min(264,screen.width/3)-columnGap)/2);
|
||||
int menuWidth=unlocked?Math.max(minimumWidth,Math.min(132,Math.max(76,screen.width/5))):2*columnWidth+columnGap;
|
||||
int right=unlocked?Math.min(244,Math.max(104,screen.width*31/100))
|
||||
:Math.min(244,(screen.width-2*margin-menuWidth-2*gap)/2);
|
||||
int centerX=margin+menuWidth+gap,rightX=screen.width-margin-right,centerWidth=rightX-gap-centerX;
|
||||
int menuX=unlocked?margin:(screen.width-menuWidth)/2;
|
||||
int communityX=unlocked?menuX:menuX+columnWidth+columnGap;
|
||||
int navGap=compact?(unlocked?1:2):4,groupGap=compact?(unlocked?2:8):12,titleHeight=compact&&unlocked?10:12;
|
||||
int groupCount=unlocked?3:2;
|
||||
int navHeight=Math.min(compact?14:20,Math.max(11,(bottom-top-16-groupCount*titleHeight-(groupCount-1)*groupGap-2*groupCount*navGap)/(3*groupCount)));
|
||||
int step=navHeight+navGap;
|
||||
add.accept(new Frame(screen.width,screen.height,left,top+2*step+(groupGap-navGap)/2));
|
||||
date=new Calendar(margin,3,left);
|
||||
int sectionHeight=titleHeight+3*navHeight+2*navGap;
|
||||
int menuHeight=groupCount*sectionHeight+(groupCount-1)*groupGap;
|
||||
// Use a narrow stack beside the map, or parallel groups before Atlas.
|
||||
int menuTop=top+Math.max(0,(bottom-top-menuHeight)/2);
|
||||
int adventureTop=unlocked?menuTop+sectionHeight+groupGap:menuTop;
|
||||
int communityTop=menuTop;
|
||||
int gameTop=menuTop+(groupCount-1)*(sectionHeight+groupGap);
|
||||
int optionsTop=gameTop+titleHeight;
|
||||
add.accept(new Frame(screen.width,screen.height));
|
||||
date=new Calendar(margin,3,Math.min(132,Math.max(76,screen.width/5)));
|
||||
add.accept(date);
|
||||
bulletin=new PauseTextButton(centerX,3,screen.width-margin-centerX,38,text("bulletin"),()->open(screen,"bulletin",items("bulletins").isEmpty()?"":str(items("bulletins").get(0).getAsJsonObject(),"id"),false));
|
||||
add.accept(bulletin);
|
||||
String[] sections={"blocks","progress","identity"};
|
||||
for(int i=0;i<sections.length;i++) {
|
||||
String target=sections[i];int y=top+i*step+(i>=2?groupGap:0);
|
||||
add.accept(Button.builder(fr.koka.sanctuary.progression.ProgressionService.label(target),b->ProgressionClient.open(screen,target))
|
||||
.bounds(margin,y,left,navHeight).build());
|
||||
add.accept(new StringWidget(menuX,adventureTop,unlocked?menuWidth:columnWidth,9,text("pause_group_adventure").withStyle(net.minecraft.ChatFormatting.GRAY),Minecraft.getInstance().font));
|
||||
add.accept(new StringWidget(communityX,communityTop,unlocked?menuWidth:columnWidth,9,text("pause_group_community").withStyle(net.minecraft.ChatFormatting.GRAY),Minecraft.getInstance().font));
|
||||
add.accept(new StringWidget(menuX,gameTop,menuWidth,9,text("pause_group_game").withStyle(net.minecraft.ChatFormatting.GRAY),Minecraft.getInstance().font));
|
||||
if(unlocked)sections=new String[]{"identity","faction","battle","blocks","progress","story"};
|
||||
for(String target:sections) {
|
||||
boolean adventure=target.equals("blocks")||target.equals("progress")||target.equals("story");
|
||||
int y=(adventure?adventureTop:communityTop)+titleHeight+step*switch(target){case "blocks","identity"->0;case "progress","faction"->1;default->2;};
|
||||
add.accept(Button.builder(sectionLabel(target),b->{switch(target){case "faction"->CycleClient.open(screen,"faction");case "battle"->ArenaClient.open(screen);default->ProgressionClient.open(screen,target);}})
|
||||
.bounds(adventure?menuX:communityX,y,unlocked?menuWidth:columnWidth,navHeight).build());
|
||||
}
|
||||
add.accept(Button.builder(fr.koka.sanctuary.cycle.CycleService.label("faction"),b->CycleClient.open(screen,"faction")).bounds(margin,top+3*step+groupGap,left,navHeight).build());
|
||||
add.accept(Button.builder(fr.koka.sanctuary.arena.Arenas.text("title"),b->ArenaClient.open(screen)).bounds(margin,top+4*step+groupGap,left,navHeight).build());
|
||||
map=new PauseAtlasWidget(screen,centerX,top,centerWidth,Math.max(30,bottom-top));add.accept(map);
|
||||
map=unlocked?new PauseAtlasWidget(screen,centerX,top,centerWidth,Math.max(30,bottom-top)):null;
|
||||
if(map!=null)add.accept(map);
|
||||
int resumeWidth=Math.min(200,screen.width-24);
|
||||
place(nativeButtons,add,"menu.returnToGame",(screen.width-resumeWidth)/2,screen.height-26,resumeWidth);
|
||||
int feedHeight=(bottom-top-gap)/2;
|
||||
articles=new PauseCommunityFeed(screen,add,rightX,top,right,feedHeight,"article");
|
||||
notices=new PauseCommunityFeed(screen,add,rightX,top+feedHeight+gap,right,bottom-top-feedHeight-gap,"notice");
|
||||
place(nativeButtons,add,"menu.options",margin,optionsTop,left,optionHeight);
|
||||
place(nativeButtons,add,"options.worldOptions.button",margin,optionsTop+optionHeight+4,left,optionHeight);
|
||||
articles=new PauseCommunityFeed(screen,add,unlocked?rightX:margin,top,right,unlocked?feedHeight:bottom-top,"article");
|
||||
notices=new PauseCommunityFeed(screen,add,rightX,unlocked?top+feedHeight+gap:top,right,unlocked?bottom-top-feedHeight-gap:bottom-top,"notice");
|
||||
place(nativeButtons,add,"menu.options",menuX,optionsTop,menuWidth,navHeight);
|
||||
place(nativeButtons,add,"options.worldOptions.button",menuX,optionsTop+step,menuWidth,navHeight);
|
||||
for(var button:nativeButtons) if(key(button).equals("menu.returnToMenu") || key(button).equals("menu.disconnect")) {
|
||||
button.setRectangle(left,optionHeight,margin,optionsTop+2*(optionHeight+4));button.setTooltip(Tooltip.create(button.getMessage()));add.accept(button);
|
||||
button.setRectangle(menuWidth,navHeight,menuX,optionsTop+2*step);button.setTooltip(Tooltip.create(button.getMessage()));add.accept(button);
|
||||
}
|
||||
updateHeader();
|
||||
}
|
||||
private static Component sectionLabel(String target) {
|
||||
return switch(target){case "story"->StoryScreen.label("title");case "faction"->fr.koka.sanctuary.cycle.CycleService.label("faction");case "battle"->fr.koka.sanctuary.arena.Arenas.text("title");default->fr.koka.sanctuary.progression.ProgressionService.label(target);};
|
||||
}
|
||||
public static String key(Button button) { return button.getMessage().getContents() instanceof TranslatableContents t?t.getKey():""; }
|
||||
private static void place(List<Button> buttons,Consumer<AbstractWidget> add,String key,int x,int y,int width) {
|
||||
place(buttons,add,key,x,y,width,20);
|
||||
@@ -79,14 +103,12 @@ public final class CommunityPausePanels implements AutoCloseable {
|
||||
}
|
||||
/** Shares Progression's native header/footer separator textures. */
|
||||
private static final class Frame extends AbstractWidget {
|
||||
private final int left,groupY;
|
||||
Frame(int width,int height,int left,int groupY){super(0,0,width,height,Component.empty());this.left=left;this.groupY=groupY;active=false;}
|
||||
Frame(int width,int height){super(0,0,width,height,Component.empty());active=false;}
|
||||
@Override public void extractWidgetRenderState(net.minecraft.client.gui.GuiGraphicsExtractor g,int mx,int my,float delta) {
|
||||
var texture=Minecraft.getInstance().level==null?Screen.HEADER_SEPARATOR:Screen.INWORLD_HEADER_SEPARATOR;
|
||||
g.blit(net.minecraft.client.renderer.RenderPipelines.GUI_TEXTURED,texture,0,44,0,0,width,2,32,2);
|
||||
texture=Minecraft.getInstance().level==null?Screen.FOOTER_SEPARATOR:Screen.INWORLD_FOOTER_SEPARATOR;
|
||||
g.blit(net.minecraft.client.renderer.RenderPipelines.GUI_TEXTURED,texture,0,height-33,0,0,width,2,32,2);
|
||||
g.fill(12,groupY,left+4,groupY+1,0x665F6869);
|
||||
}
|
||||
@Override protected void updateWidgetNarration(net.minecraft.client.gui.narration.NarrationElementOutput out) { }
|
||||
}
|
||||
@@ -113,11 +135,11 @@ public final class CommunityPausePanels implements AutoCloseable {
|
||||
}
|
||||
public void tick() {
|
||||
if(closed)return;
|
||||
map.tick(); updateHeader();
|
||||
if(map!=null)map.tick(); updateHeader();
|
||||
if(++ticks%6!=0 || pending() || !available())return;
|
||||
if(ticks==6 || ticks%240==0){overview(screen);if(ticks>6){articles.refresh();notices.refresh();}}
|
||||
else if(!articles.load())notices.load();
|
||||
}
|
||||
public PauseAtlasWidget map() { return map; }
|
||||
@Override public void close() {closed=true;articles.close();notices.close();map.close();}
|
||||
@Override public void close() {closed=true;articles.close();notices.close();if(map!=null)map.close();}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ public final class CommunityScreen extends BlocodexFrame {
|
||||
line(layout,w,Component.literal(str(entry,"title")).withStyle(net.minecraft.ChatFormatting.BOLD));message(layout,w,entry);
|
||||
if(kind.equals("notice"))taskDetails(layout,w,entry);
|
||||
}
|
||||
if (kind.equals("notice")) line(layout,w,text("category."+str(entry,"category")).append(" · ").append(text(str(entry,"status"))));
|
||||
if (kind.equals("notice")) line(layout,w,text(str(entry,"status")));
|
||||
if (!kind.equals("bulletin")) { replies(layout,w,thread);replyComposer(layout,w,entry); }
|
||||
}
|
||||
private void replies(LinearLayout layout,int w,JsonObject thread) {
|
||||
@@ -199,26 +199,22 @@ public final class CommunityScreen extends BlocodexFrame {
|
||||
}
|
||||
private void compose(LinearLayout layout,int w) {
|
||||
boolean reply = !id.isEmpty() && (editingId.isEmpty() || !editingId.equals(id));
|
||||
if (!reply) {
|
||||
line(layout,w,text("title_label")); var titleBox=layout.addChild(new EditBox(font,w,20,text("title_label")));
|
||||
titleBox.setMaxLength(120); titleBox.setValue(draft.title); titleBox.setResponder(value -> draft.title=value);
|
||||
if (kind.equals("notice")) button(layout,w,"category."+draft.category,() -> {
|
||||
String[] categories={"info","work","need","event"}; int i=java.util.Arrays.asList(categories).indexOf(draft.category);
|
||||
draft.category=categories[(i+1)%categories.length]; rebuildWidgets();
|
||||
});
|
||||
}
|
||||
if(!reply && kind.equals("article")) {
|
||||
photo(layout,w,draft.photo);
|
||||
button(layout,w,draft.photo.isEmpty()?"photo_choose":"photo_change",()->minecraft.gui.setScreen(new CommunityScreenshotsScreen(this,value->draft.photo=value)));
|
||||
if(draft.photo.isEmpty())line(layout,w,text("photo_required"));
|
||||
}
|
||||
if(!reply && kind.equals("notice")) {
|
||||
button(layout,w,"task_configure",()->minecraft.gui.setScreen(new CommunityTaskScreen(this,draft.task,value->draft.task=value)));
|
||||
line(layout,w,text("task_summary",draft.task.materials().size(),text("audience."+draft.task.audience())));
|
||||
if (!reply) {
|
||||
line(layout,w,text("title_label")); var titleBox=layout.addChild(new EditBox(font,w,20,text("title_label")));
|
||||
titleBox.setMaxLength(120); titleBox.setValue(draft.title); titleBox.setResponder(value -> draft.title=value);
|
||||
}
|
||||
line(layout,w,text("body_label"));
|
||||
var bodyBox=layout.addChild(MultiLineEditBox.builder().setPlaceholder(text("body_hint")).build(font,w,Math.max(60,Math.min(150,height/3)),text("body_label")));
|
||||
bodyBox.setCharacterLimit(reply?1000:4000); bodyBox.setValue(draft.body); bodyBox.setValueListener(value -> draft.body=value);
|
||||
if(!reply && kind.equals("notice")) {
|
||||
button(layout,w,"task_configure",()->minecraft.gui.setScreen(new CommunityTaskScreen(this,draft.task,value->draft.task=value)));
|
||||
line(layout,w,text("task_summary",draft.task.materials().size(),text("audience."+draft.task.audience())));
|
||||
}
|
||||
var publish=layout.addChild(Button.builder(text(publishing?"photo_uploading":editingId.isEmpty()?"publish":"save"),b -> {
|
||||
if(pending() || publishing)return;
|
||||
String action=editingId.isEmpty()?"create":"edit";
|
||||
|
||||
@@ -15,6 +15,7 @@ import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
|
||||
@@ -25,12 +26,23 @@ public final class MiningClient {
|
||||
private static BlockPos active;
|
||||
private static Item tool;
|
||||
private static int slot;
|
||||
private static boolean engaged;
|
||||
private static boolean engaged,completed;
|
||||
private static BlockState anchorState;
|
||||
private static List<BlockPos> locked=List.of();
|
||||
private static List<BlockPos> preview=List.of();
|
||||
private MiningClient() { }
|
||||
public static void register(KeyMapping.Category category) {
|
||||
key=KeyMappingHelper.registerKeyMapping(new KeyMapping("sanctuary.mining.key",InputConstants.Type.KEYBOARD,InputConstants.KEY_R,category));
|
||||
ClientPlayNetworking.registerGlobalReceiver(MiningResultPayload.TYPE,(payload,context)->context.client().execute(()->{
|
||||
var c=context.client();
|
||||
if(active==null||payload.gesture()!=gesture)return;
|
||||
boolean resume=payload.completed()&&enabled(c)&&c.options.keyAttack.isDown()
|
||||
&&c.player.getMainHandItem().getItem()==tool&&c.player.getInventory().getSelectedSlot()==slot;
|
||||
// The result can precede the tick's queued block updates. Keep the old anchor until
|
||||
// its removal arrives, otherwise a stale raycast would restart a mined block.
|
||||
if(resume)completed=true;
|
||||
else {stop();preview=List.of();}
|
||||
}));
|
||||
ClientPreAttackCallback.EVENT.register((client,player,clicks) -> {
|
||||
if (!enabled(client)) return false;
|
||||
// Even a rejected/empty selection consumes the modified gesture: it must not mine individually.
|
||||
@@ -64,6 +76,7 @@ public final class MiningClient {
|
||||
if (active!=null && (c.player.getMainHandItem().getItem()!=tool || c.player.getInventory().getSelectedSlot()!=slot)) {
|
||||
stop(); // Keep the gesture latched until release, even after an interruption.
|
||||
}
|
||||
if(completed&&active!=null&&c.level.getBlockState(active)!=anchorState){stop();engaged=false;}
|
||||
if (BuildingClient.previewMode(c)) {stop();preview=List.of();return;}
|
||||
BlockPos target=c.hitResult instanceof BlockHitResult hit && hit.getType()!=HitResult.Type.MISS && fr.koka.sanctuary.mining.MiningReach.within(c.player,hit.getBlockPos(),0) ? hit.getBlockPos() : null;
|
||||
preview=engaged ? locked.stream().filter(p -> !c.level.getBlockState(p).isAir()).toList()
|
||||
@@ -71,9 +84,10 @@ public final class MiningClient {
|
||||
MiningPreviewRenderer.collect(c,preview,0xFFE4B85F);
|
||||
if (!c.options.keyAttack.isDown()) return;
|
||||
if (!engaged) {
|
||||
engaged=true;
|
||||
if (target==null || preview.isEmpty()) return;
|
||||
engaged=true;
|
||||
active=target.immutable();locked=List.copyOf(preview);tool=c.player.getMainHandItem().getItem();slot=c.player.getInventory().getSelectedSlot();
|
||||
anchorState=c.level.getBlockState(active);
|
||||
gesture=gesture==Integer.MAX_VALUE ? 1 : gesture+1;heartbeat=0;animation=0;
|
||||
c.gameMode.stopDestroyBlock();send(true);swing(c);
|
||||
} else if (active!=null && ++heartbeat>=5) {heartbeat=0;send(true);}
|
||||
@@ -98,8 +112,8 @@ public final class MiningClient {
|
||||
private static void send(boolean down) {
|
||||
if (active!=null && ClientPlayNetworking.canSend(MiningPayload.TYPE)) ClientPlayNetworking.send(new MiningPayload(gesture,down,active,selected));
|
||||
}
|
||||
private static void stop() {if (active!=null) send(false);active=null;tool=null;locked=List.of();heartbeat=0;animation=0;}
|
||||
private static void reset() {active=null;tool=null;preview=List.of();locked=List.of();selected=1;earnedBefore=0;heartbeat=0;animation=0;engaged=false;}
|
||||
private static void stop() {if (active!=null) send(false);active=null;tool=null;anchorState=null;completed=false;locked=List.of();heartbeat=0;animation=0;}
|
||||
private static void reset() {active=null;tool=null;anchorState=null;completed=false;preview=List.of();locked=List.of();selected=1;earnedBefore=0;heartbeat=0;animation=0;engaged=false;}
|
||||
public static Component label(String name,Object... args) {return Component.translatable("sanctuary.mining."+name,args);}
|
||||
public static boolean modifierDown() {return key!=null && key.isDown();}
|
||||
public static Component modifierLabel() {return key==null ? Component.literal("R") : key.getTranslatedKeyMessage();}
|
||||
|
||||
@@ -54,7 +54,7 @@ public final class NotificationClient {
|
||||
Component detail=CommunityClient.text(closed?"closed":"open");
|
||||
if(task.location()!=null)detail=detail.copy().append(" · "+task.location().x()+", "+task.location().y()+", "+task.location().z());
|
||||
if(!task.materials().isEmpty()) {var item=task.materials().getFirst();var name=BuiltInRegistries.ITEM.getOptional(Identifier.parse(item.item())).map(i->i.getDefaultInstance().getHoverName()).orElse(Component.literal(item.item()));detail=detail.copy().append(" · "+item.count()+" × ").append(name);}
|
||||
var track=new Track(Component.literal(CommunityClient.str(e,"title")),detail,closed?1:0,CommunityCard.color(CommunityClient.str(e,"category"))&0xffffff);
|
||||
var track=new Track(Component.literal(CommunityClient.str(e,"title")),detail,closed?1:0,CommunityCard.NOTICE_COLOR&0xffffff);
|
||||
snapshots.add(new ObjectiveTracker.Objective<>(CommunityClient.str(e,"id"),track,closed?1:0,closed));if(!closed&&tracks.size()<3)tracks.add(track);
|
||||
}
|
||||
objectives("community",snapshots);communityTracks=List.copyOf(tracks);
|
||||
|
||||
@@ -30,6 +30,7 @@ public final class ProgressionClient {
|
||||
public static void open(Screen parent,String section) {
|
||||
var gui=Minecraft.getInstance().gui;
|
||||
if (section.equals("blocks")) gui.setScreen(new SanctuaryBlocodexScreen(parent));
|
||||
else if (section.equals("story") && active()) gui.setScreen(new StoryScreen(parent));
|
||||
else if (active()) gui.setScreen(section.equals("history") ? new SanctuaryAtlasScreen(parent) : new ProgressionScreen(parent,section));
|
||||
}
|
||||
public static void register() {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import java.util.Locale;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.layouts.LinearLayout;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** The first Story catalogue reserves 100 unknown stories without inventing discoveries. */
|
||||
public final class StoryScreen extends BlocodexFrame {
|
||||
private enum Filter {
|
||||
ALL, DISCOVERED, UNKNOWN;
|
||||
Component label() { return StoryScreen.label("filter_"+name().toLowerCase(Locale.ROOT)); }
|
||||
}
|
||||
|
||||
private String query="";
|
||||
private Filter filter=Filter.ALL;
|
||||
private ScrollableLayout scroll;
|
||||
|
||||
public StoryScreen(Screen parent) { super(parent,label("title")); }
|
||||
|
||||
public static Component label(String key,Object... args) {
|
||||
return Component.translatable("sanctuary.story."+key,args);
|
||||
}
|
||||
|
||||
@Override protected void init() {
|
||||
initFrame("story",label("title"));
|
||||
var search=addRenderableWidget(new EditBox(font,contentX,contentTop,contentWidth,20,label("search")));
|
||||
search.setMaxLength(128);
|
||||
search.setValue(query);
|
||||
// Native hints disappear on focus; a suggestion keeps the empty search identifiable.
|
||||
search.setSuggestion(query.isEmpty()?label("search").getString():null);
|
||||
search.setResponder(value->{query=value;search.setSuggestion(value.isEmpty()?label("search").getString():null);refreshList(0);});
|
||||
addRenderableWidget(CycleButton.builder(Filter::label,filter).withValues(Filter.values())
|
||||
.create(contentX,contentTop+24,contentWidth,20,label("filter"),(button,value)->{
|
||||
filter=value;refreshList(0);
|
||||
}));
|
||||
contentTop+=50;
|
||||
refreshList(scroll==null?0:scroll.getScrollAmount());
|
||||
setInitialFocus(search);
|
||||
}
|
||||
|
||||
/** Replace only list widgets: search/cursor and keyboard focus remain untouched. */
|
||||
private void refreshList(double offset) {
|
||||
if(scroll!=null)scroll.visitWidgets(this::removeWidget);
|
||||
var contents=LinearLayout.vertical().spacing(4);
|
||||
String needle=query.strip().toLowerCase(Locale.ROOT);
|
||||
int count=0;
|
||||
if(filter!=Filter.DISCOVERED)for(int number=1;number<=100;number++) {
|
||||
Component name=label("unknown_entry",number);
|
||||
if(!name.getString().toLowerCase(Locale.ROOT).contains(needle))continue;
|
||||
var entry=contents.addChild(Button.builder(name,button->{}).width(contentWidth-18).build());
|
||||
entry.active=false;
|
||||
count++;
|
||||
}
|
||||
if(count==0)contents.addChild(new MultiLineTextWidget(label(filter==Filter.DISCOVERED?"no_discoveries":"no_results"),font)
|
||||
.setMaxWidth(contentWidth-18));
|
||||
int height=Math.max(20,contentBottom-contentTop);
|
||||
scroll=new ScrollableLayout(minecraft,contents,height,ScrollableLayout.ReserveStrategy.RIGHT);
|
||||
scroll.setMinHeight(height);
|
||||
scroll.arrangeElements();scroll.setX(contentX);scroll.setY(contentTop);
|
||||
scroll.visitWidgets(widget->{addRenderableWidget(widget);if(widget instanceof AbstractScrollArea area)area.setScrollAmount(offset);});
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ public final class MiningManager {
|
||||
private MiningManager() { }
|
||||
public static void register() {
|
||||
PayloadTypeRegistry.serverboundPlay().register(MiningPayload.TYPE,MiningPayload.CODEC);
|
||||
PayloadTypeRegistry.clientboundPlay().register(MiningResultPayload.TYPE,MiningResultPayload.CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(MiningPayload.TYPE,(p,c) -> c.server().execute(() -> handle(c.player(),p)));
|
||||
ServerTickEvents.END_SERVER_TICK.register(MiningManager::tick);
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler,server) -> forget(handler.player));
|
||||
@@ -64,6 +65,7 @@ public final class MiningManager {
|
||||
((MiningVanillaAccess)player.gameMode).sanctuary$cancelVanillaMining();
|
||||
}
|
||||
ATTEMPTS.put(player,new Attempt(payload,rank,next));
|
||||
if(next==null)result(player,payload.gesture(),false);
|
||||
}
|
||||
public static void tick(MinecraftServer server) {
|
||||
ATTEMPTS.forEach((player,attempt) -> {
|
||||
@@ -72,9 +74,14 @@ public final class MiningManager {
|
||||
}
|
||||
public static void tick(ServerPlayer player) {
|
||||
var attempt=ATTEMPTS.get(player);
|
||||
if (attempt!=null && attempt.session!=null && !attempt.session.tick()) attempt.stop();
|
||||
if (attempt!=null && attempt.session!=null && !attempt.session.tick()) {
|
||||
boolean completed=attempt.session.completed();attempt.stop();result(player,attempt.gesture,completed);
|
||||
}
|
||||
}
|
||||
public static void interrupt(ServerPlayer player) {var attempt=ATTEMPTS.get(player);if (attempt!=null&&attempt.session!=null) {attempt.stop();result(player,attempt.gesture,false);}}
|
||||
private static void result(ServerPlayer player,int gesture,boolean completed) {
|
||||
if(ServerPlayNetworking.canSend(player,MiningResultPayload.TYPE))ServerPlayNetworking.send(player,new MiningResultPayload(gesture,completed));
|
||||
}
|
||||
public static void interrupt(ServerPlayer player) {var attempt=ATTEMPTS.get(player);if (attempt!=null) attempt.stop();}
|
||||
public static void forget(ServerPlayer player) {interrupt(player);ATTEMPTS.remove(player);LAST_START.remove(player);}
|
||||
public static boolean active(ServerPlayer player) {var a=ATTEMPTS.get(player);return a!=null && a.session!=null;}
|
||||
public static int selectedCount(ServerPlayer player) {var a=ATTEMPTS.get(player);return a==null || a.session==null ? 0 : a.session.size();}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package fr.koka.sanctuary.mining;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
/** Only successful, fully paid server work may unlock the next held selection. */
|
||||
public record MiningResultPayload(int gesture,boolean completed) implements CustomPacketPayload {
|
||||
public static final Type<MiningResultPayload> TYPE=new Type<>(SanctuaryMod.id("mining_result"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,MiningResultPayload> CODEC=CustomPacketPayload.codec(
|
||||
(p,b)->{b.writeVarInt(p.gesture);b.writeBoolean(p.completed);},
|
||||
b->new MiningResultPayload(b.readVarInt(),b.readBoolean()));
|
||||
public Type<MiningResultPayload> type(){return TYPE;}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ final class MiningSession {
|
||||
private ItemStack expected;
|
||||
private int lastHeartbeat,chargeIndex,nextBreak,crack=-2;
|
||||
private double progress;
|
||||
private boolean breaking,sounded;
|
||||
private boolean breaking,sounded,completed;
|
||||
private MiningSession(ServerPlayer player,BlockPos origin,int rank,List<BlockPos> blocks) {
|
||||
this.player=player;level=player.level();this.origin=origin.immutable();this.rank=rank;this.blocks=blocks;
|
||||
states=blocks.stream().map(level::getBlockState).toList();
|
||||
@@ -42,6 +42,7 @@ final class MiningSession {
|
||||
return blocks.isEmpty() ? null : new MiningSession(player,origin,rank,blocks);
|
||||
}
|
||||
int size() {return blocks.size();}
|
||||
boolean completed() {return completed;}
|
||||
void refresh() {lastHeartbeat=level.getServer().getTickCount();}
|
||||
boolean tick() {
|
||||
var earned=ProgressEffects.get(player);
|
||||
@@ -92,7 +93,8 @@ final class MiningSession {
|
||||
if (!expected.isEmpty() && tool.isEmpty()) return false; // Tool broke: never continue with a bare hand.
|
||||
expected=tool.copy();
|
||||
}
|
||||
return nextBreak<blocks.size();
|
||||
completed=nextBreak==blocks.size();
|
||||
return !completed;
|
||||
}
|
||||
void stop() {updateCracks(-1);}
|
||||
private void updateCracks(int stage) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.*;
|
||||
import fr.koka.sanctuary.worldgen.IslandChestLoot;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.storage.loot.LootContext;
|
||||
import net.minecraft.world.level.storage.loot.LootTable;
|
||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
@Mixin(LootTable.class)
|
||||
public abstract class IslandChestLootMixin {
|
||||
@WrapOperation(method="fill",at=@At(value="INVOKE",target="Lnet/minecraft/world/level/storage/loot/LootTable;getRandomItems(Lnet/minecraft/world/level/storage/loot/LootContext;)Lit/unimi/dsi/fastutil/objects/ObjectArrayList;"))
|
||||
private ObjectArrayList<ItemStack> sanctuary$islandLoot(LootTable table,LootContext context,Operation<ObjectArrayList<ItemStack>> original) {
|
||||
var loot=original.call(table,context);
|
||||
IslandChestLoot.filter(context.getLevel(),context.getOptional(LootContextParams.ORIGIN),loot);
|
||||
return loot;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package fr.koka.sanctuary.realtime;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
@@ -30,16 +31,27 @@ public final class RealtimeService {
|
||||
final ZoneId zone;
|
||||
final SolarReference reference;
|
||||
final SolarTime sun;
|
||||
final boolean customTimezone;
|
||||
final RealtimeStore store;
|
||||
// Clock injection keeps deterministic time tests outside the game's command surface.
|
||||
Clock clock;
|
||||
boolean healthy=true;
|
||||
Session(MinecraftServer server,RealtimeConfig config,Clock clock)throws Exception{
|
||||
this.server=server;this.config=config;this.clock=clock;zone=config.zone(server.isSingleplayer());reference=config.reference(server.isSingleplayer(),clock.instant());sun=new SolarTime(zone,reference);
|
||||
definition=server.registryAccess().lookupOrThrow(Registries.WORLD_CLOCK).getOrThrow(WorldClocks.OVERWORLD);
|
||||
store=new RealtimeStore(server.getWorldPath(LevelResource.ROOT).resolve("data/sanctuary-realtime.json"),instance().packState(),sun.sample(clock.instant()),key(),config.enabled());
|
||||
this(server,config,clock,null,null);
|
||||
}
|
||||
String key(){return zone.getId()+"|"+reference.name()+"|"+reference.latitude()+"|"+reference.longitude();}
|
||||
Session(MinecraftServer server,RealtimeConfig config,Clock clock,RealtimeStore existing,String choice)throws Exception{
|
||||
this.server=server;this.config=config;this.clock=clock;
|
||||
definition=server.registryAccess().lookupOrThrow(Registries.WORLD_CLOCK).getOrThrow(WorldClocks.OVERWORLD);
|
||||
var defaultZone=config.zone(server.isSingleplayer());var defaultReference=config.reference(server.isSingleplayer(),clock.instant());
|
||||
var defaultSun=new SolarTime(defaultZone,defaultReference);
|
||||
store=existing!=null?existing:new RealtimeStore(server.getWorldPath(LevelResource.ROOT).resolve("data/sanctuary-realtime.json"),instance().packState(),defaultSun.sample(clock.instant()),referenceKey(defaultZone,defaultReference),config.enabled());
|
||||
if(choice==null&&store.state().reference().startsWith("timezone:"))choice=store.state().reference().substring(9).split("\\|",-1)[0];
|
||||
customTimezone=choice!=null&&!choice.equals("auto");
|
||||
zone=customTimezone?ZoneId.of(choice):defaultZone;
|
||||
reference=customTimezone?SolarReference.resolve(zone,"auto",clock.instant()):defaultReference;
|
||||
sun=new SolarTime(zone,reference);
|
||||
}
|
||||
String key(){return (customTimezone?"timezone:":"")+referenceKey(zone,reference);}
|
||||
public boolean active(){return healthy&&config.enabled()&&store.state().realtime();}
|
||||
ServerClockManager.ServerClockInstance instance(){return server.clockManager().getInstance(definition);}
|
||||
ClockState savedNative(ClockState current){return new ClockState(current.totalTicks(),current.partialTick(),store.state().nativeRate(),store.state().nativePaused());}
|
||||
@@ -56,6 +68,7 @@ public final class RealtimeService {
|
||||
}catch(Exception error){healthy=false;restoreRate();SanctuaryMod.LOGGER.error("Realtime clock stopped; contract retained and native speed restored",error);}
|
||||
}
|
||||
}
|
||||
private static String referenceKey(ZoneId zone,SolarReference reference){return zone.getId()+"|"+reference.name()+"|"+reference.latitude()+"|"+reference.longitude();}
|
||||
public static boolean southernHemisphere(MinecraftServer server){var s=SESSIONS.get(server);return s!=null&&s.reference.latitude()<0;}
|
||||
public static Session session(MinecraftServer server){return SESSIONS.get(server);}
|
||||
/** Civil timestamps share the configured sun timezone without changing the game clock. */
|
||||
@@ -68,10 +81,35 @@ public final class RealtimeService {
|
||||
ServerLifecycleEvents.SERVER_STOPPED.register(SESSIONS::remove);
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher,context,environment)->dispatcher.register(Commands.literal("sanctuary").then(Commands.literal("time")
|
||||
.executes(c->status(c.getSource()))
|
||||
.then(Commands.literal("timezone").requires(RealtimeService::canChangeTimezone).executes(c->status(c.getSource()))
|
||||
.then(Commands.argument("zone",StringArgumentType.greedyString())
|
||||
.suggests((c,b)->SharedSuggestionProvider.suggest(java.util.stream.Stream.concat(java.util.stream.Stream.of("auto"),ZoneId.getAvailableZoneIds().stream().sorted()),b))
|
||||
.executes(c->timezone(c.getSource(),StringArgumentType.getString(c,"zone")))))
|
||||
.then(Commands.literal("realtime").requires(fr.koka.sanctuary.operator.OperatorService::canCommand).executes(c->change(c.getSource(),true)))
|
||||
.then(Commands.literal("vanilla").requires(fr.koka.sanctuary.operator.OperatorService::canCommand).executes(c->change(c.getSource(),false)))
|
||||
.then(Commands.literal("reload").requires(fr.koka.sanctuary.operator.OperatorService::canCommand).executes(c->{try{reload(c.getSource().getServer());return status(c.getSource());}catch(Exception e){c.getSource().sendFailure(label("invalid"));SanctuaryMod.LOGGER.error("Realtime reload refused",e);return 0;}})))));
|
||||
}
|
||||
public static boolean canChangeTimezone(CommandSourceStack source){
|
||||
return source.getEntity() instanceof net.minecraft.server.level.ServerPlayer player
|
||||
&& source.getServer().isSingleplayer()&&source.getServer().isSingleplayerOwner(player.nameAndId())
|
||||
|| fr.koka.sanctuary.operator.OperatorService.canCommand(source);
|
||||
}
|
||||
public static boolean setTimezone(MinecraftServer server,String choice)throws Exception{
|
||||
if(!choice.equals("auto"))ZoneId.of(choice);
|
||||
var previous=session(server);if(previous==null||!previous.healthy)return false;
|
||||
var next=new Session(server,previous.config,previous.clock,previous.store,choice);
|
||||
if(previous.key().equals(next.key()))return true;
|
||||
var nativeClock=previous.active()?previous.savedNative(previous.instance().packState()):previous.instance().packState();
|
||||
next.store.reanchor(next.store.state().realtime(),nativeClock,next.sample(),next.key());
|
||||
SESSIONS.put(server,next);next.tick();sync(server);return true;
|
||||
}
|
||||
private static int timezone(CommandSourceStack source,String choice){
|
||||
try{
|
||||
if(!setTimezone(source.getServer(),choice)){source.sendFailure(label("inactive"));return 0;}
|
||||
return status(source);
|
||||
}catch(DateTimeException e){source.sendFailure(label("timezone_invalid",choice));return 0;}
|
||||
catch(Exception e){source.sendFailure(label("invalid"));SanctuaryMod.LOGGER.error("Realtime timezone change refused",e);return 0;}
|
||||
}
|
||||
public static void reload(MinecraftServer server)throws Exception{
|
||||
if(!supported(server))return;
|
||||
var previous=session(server);var config=RealtimeConfig.load(CONFIG);
|
||||
@@ -98,6 +136,7 @@ public final class RealtimeService {
|
||||
if(!s.healthy){source.sendFailure(label("invalid"));return 0;}
|
||||
var sample=s.sample();String rise=sample.day().sunrise()==null?"—":HOUR.format(sample.day().sunrise().atZone(s.zone));String set=sample.day().sunset()==null?"—":HOUR.format(sample.day().sunset().atZone(s.zone));
|
||||
source.sendSuccess(()->label(s.active()?"status":"vanilla",CIVIL.format(s.clock.instant().atZone(s.zone)),rise,set),false);
|
||||
source.sendSuccess(()->label("timezone",s.zone.getId()),false);
|
||||
if(s.reference.approximate())source.sendSuccess(()->label("approximate"),false);return 1;
|
||||
}
|
||||
public static Component label(String key,Object... args){return Component.translatable("sanctuary.realtime."+key,args);}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package fr.koka.sanctuary.worldgen;
|
||||
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import fr.koka.sanctuary.expansion.ExpansionIsland;
|
||||
import fr.koka.sanctuarytest.LabExpansions199;
|
||||
import fr.koka.sanctuarytest.SharedIsland196;
|
||||
|
||||
/** A loot rule at first container fill; existing inventories and terrain are never rewritten. */
|
||||
public final class IslandChestLoot {
|
||||
private IslandChestLoot() { }
|
||||
public static boolean restricted(ServerLevel level,Vec3 origin) {
|
||||
return restricted(level.getChunkSource().getGenerator(),origin);
|
||||
}
|
||||
public static boolean restricted(ChunkGenerator generator,Vec3 origin) {
|
||||
if(origin==null)return false;
|
||||
var pos=BlockPos.containing(origin);var adapter=LabExpansions199.get(generator);
|
||||
if(adapter!=null){var island=adapter.islandAt(pos.getX(),pos.getZ());return island!=null&&island.origin();}
|
||||
if(generator instanceof SanctuaryChunkGenerator historical){var island=historical.islandAt(pos.getX(),pos.getZ());return island!=null&&island.origin();}
|
||||
var size=SharedIsland196.size(generator);
|
||||
return size!=null&&ExpansionIsland.origin(size.diameter,0,24).contains(pos.getX(),pos.getZ());
|
||||
}
|
||||
public static void filter(ServerLevel level,Vec3 origin,List<ItemStack> loot) {
|
||||
if(restricted(level,origin))loot.removeIf(stack->stack.is(Items.DIAMOND));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"sanctuary.story.title": "Story",
|
||||
"sanctuary.story.search": "Search stories…",
|
||||
"sanctuary.story.filter": "Discovery",
|
||||
"sanctuary.story.filter_all": "All",
|
||||
"sanctuary.story.filter_discovered": "Discovered",
|
||||
"sanctuary.story.filter_unknown": "Unknown",
|
||||
"sanctuary.story.unknown_entry": "Story %s — Unknown",
|
||||
"sanctuary.story.no_discoveries": "No stories discovered yet.",
|
||||
"sanctuary.story.no_results": "No stories match the search.",
|
||||
"biome.sanctuary.diagonal_lush227": "Mangrove Lush Caves",
|
||||
"biome.sanctuary.diagonal_dripstone227": "Mangrove Dripstone Caves",
|
||||
"biome.sanctuary.diagonal_swamp226": "Cool Swamp",
|
||||
@@ -436,7 +445,7 @@
|
||||
"sanctuary.atlas.zoom_in": "Zoom in",
|
||||
"sanctuary.atlas.zoom_out": "Zoom out",
|
||||
"sanctuary.mining.key": "Group mining and building (hold)",
|
||||
"sanctuary.mining.controls": "Hold %s to preview the vein. Hold left click to lock and mine, with free look. Release to select another group. Scroll before clicking to choose size.",
|
||||
"sanctuary.mining.controls": "Hold %s to preview the vein. Hold left click to mine the group, then the one you are looking at. Scroll before clicking to choose size. Release to stop.",
|
||||
"sanctuary.mining.no_target": "No vein selected",
|
||||
"sanctuary.mining.selection": "%s / %s blocks",
|
||||
"sanctuary.mining.size": "Mining · rank %s · up to %s blocks",
|
||||
@@ -1226,6 +1235,8 @@
|
||||
"sanctuary.realtime.invalid": "The real-time configuration or world contract is invalid. See the server log.",
|
||||
"sanctuary.realtime.approximate": "No reference city for this time zone: using an approximate equatorial sun.",
|
||||
"sanctuary.realtime.managed": "Real time controls this clock. Use /sanctuary time vanilla before changing it.",
|
||||
"sanctuary.realtime.timezone": "Time zone: %s · /sanctuary time timezone <zone> · auto: default setting",
|
||||
"sanctuary.realtime.timezone_invalid": "Unknown time zone: %s. Choose a suggested identifier, such as Europe/Paris, or auto.",
|
||||
"sanctuary.notifications.objective_done": "Objective completed: %s",
|
||||
"sanctuary.notifications.objective_progress": "Objective progress: %s",
|
||||
"sanctuary.food_details.title": "Food information",
|
||||
@@ -2380,6 +2391,9 @@
|
||||
"sanctuary.shaders.ssr_distance_help": "Independent SSR range: 16, 32, 64, 128 or 256 blocks. Default: 64 blocks. Limited to visible terrain and mesh budget.",
|
||||
"sanctuary.community.article": "Gazette",
|
||||
"sanctuary.community.notice": "Notice board",
|
||||
"sanctuary.community.pause_group_community": "Community",
|
||||
"sanctuary.community.pause_group_adventure": "Adventure",
|
||||
"sanctuary.community.pause_group_game": "Game",
|
||||
"sanctuary.community.notice_tab": "Notice board",
|
||||
"sanctuary.community.map_followed": "Followed request · click for discussion",
|
||||
"sanctuary.community.map_more_materials": "+ %s more materials",
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"sanctuary.story.title": "Story",
|
||||
"sanctuary.story.search": "Rechercher une histoire…",
|
||||
"sanctuary.story.filter": "Découverte",
|
||||
"sanctuary.story.filter_all": "Toutes",
|
||||
"sanctuary.story.filter_discovered": "Découvertes",
|
||||
"sanctuary.story.filter_unknown": "Inconnues",
|
||||
"sanctuary.story.unknown_entry": "Histoire %s — Inconnue",
|
||||
"sanctuary.story.no_discoveries": "Aucune histoire découverte pour le moment.",
|
||||
"sanctuary.story.no_results": "Aucune histoire ne correspond à la recherche.",
|
||||
"biome.sanctuary.diagonal_lush227": "Grottes luxuriantes des mangroves",
|
||||
"biome.sanctuary.diagonal_dripstone227": "Grottes à concrétions des mangroves",
|
||||
"biome.sanctuary.diagonal_swamp226": "Marais frais",
|
||||
@@ -436,7 +445,7 @@
|
||||
"sanctuary.atlas.zoom_in": "Zoom avant",
|
||||
"sanctuary.atlas.zoom_out": "Zoom arrière",
|
||||
"sanctuary.mining.key": "Minage et construction groupés (maintenir)",
|
||||
"sanctuary.mining.controls": "Maintiens %s pour voir la veine. Clic gauche maintenu : verrouiller et miner, regard libre. Relâche pour choisir un autre groupe. Molette avant le clic : taille.",
|
||||
"sanctuary.mining.controls": "Maintiens %s pour voir la veine. Clic gauche maintenu : miner le groupe, puis celui que tu regardes. Molette avant le clic : taille. Relâche pour arrêter.",
|
||||
"sanctuary.mining.no_target": "Aucune veine sélectionnée",
|
||||
"sanctuary.mining.selection": "%s / %s blocs",
|
||||
"sanctuary.mining.size": "Minage · rang %s · %s blocs maximum",
|
||||
@@ -1226,6 +1235,8 @@
|
||||
"sanctuary.realtime.invalid": "La configuration du temps réel ou le contrat du monde est invalide. Voir le journal serveur.",
|
||||
"sanctuary.realtime.approximate": "Aucune ville de référence pour ce fuseau : soleil équatorial approximatif.",
|
||||
"sanctuary.realtime.managed": "Le temps réel contrôle cette horloge. Utiliser /sanctuary time vanilla avant de la modifier.",
|
||||
"sanctuary.realtime.timezone": "Fuseau : %s · /sanctuary time timezone <fuseau> · auto : réglage par défaut",
|
||||
"sanctuary.realtime.timezone_invalid": "Fuseau inconnu : %s. Choisis un identifiant proposé, par exemple Europe/Paris, ou auto.",
|
||||
"sanctuary.notifications.objective_done": "Objectif accompli : %s",
|
||||
"sanctuary.notifications.objective_progress": "Objectif en progrès : %s",
|
||||
"sanctuary.food_details.title": "Informations alimentaires",
|
||||
@@ -2380,6 +2391,9 @@
|
||||
"sanctuary.shaders.ssr_distance_help": "Portée indépendante du SSR : 16, 32, 64, 128 ou 256 blocs. Défaut : 64 blocs. Limitée au terrain visible et au budget de maillage.",
|
||||
"sanctuary.community.article": "Gazette",
|
||||
"sanctuary.community.notice": "Tableau",
|
||||
"sanctuary.community.pause_group_community": "Communauté",
|
||||
"sanctuary.community.pause_group_adventure": "Aventure",
|
||||
"sanctuary.community.pause_group_game": "Partie",
|
||||
"sanctuary.community.notice_tab": "Tableau",
|
||||
"sanctuary.community.map_followed": "Demande suivie · cliquer pour la discussion",
|
||||
"sanctuary.community.map_more_materials": "+ %s autres matériaux",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"package": "fr.koka.sanctuary.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"IslandChestLootMixin",
|
||||
"BadlandsBands211Accessor",
|
||||
"WestMonument217Accessor",
|
||||
"WrenchTextureEditMixin",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.231"
|
||||
version = "beta.235"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user