Compare commits

...
2 Commits
Author SHA1 Message Date
koka 905b3bcfa1 Reshape pause navigation and frame for beta.163
Build Sanctuary / build (push) Waiting to run
2026-09-24 13:36:56 +02:00
koka 0c55a37562 Deliver beta.162 notice author scene and side conversation
Build Sanctuary / build (push) Waiting to run
2026-09-24 00:38:09 +02:00
13 changed files with 352 additions and 52 deletions
+13
View File
@@ -1,5 +1,18 @@
# Sanctuary
## beta.163 — menu pause encadré
Calendrier, intendance et horloge en en-tête ; deux groupes de navigation,
séparateurs natifs et reprise centrée dans le footer.
[Résultat et vérifications](docs/pause-frame-beta163.md).
## beta.162 — tableau en scène et conversation
Portrait de l’auteur tourné vers la souris, bulle aux couleurs de l’annonce,
demande à gauche et réponses à droite. Retour/Actualiser restent en haut ;
la conversation passe dessous sur les GUI étroits.
[Résultat et vérifications](docs/notice-scene-beta162.md).
## beta.161 — correctifs de séance et site vivant
Liste des joueurs sur U, suivi des demandes expliqué, intendance compacte et
+66
View File
@@ -0,0 +1,66 @@
# beta.162 — scène du tableau et conversation
Ticket issu du retour de séance du 24 septembre : conserver Retour/Actualiser en
haut, montrer l’auteur avec son skin orienté vers la souris et sa demande dans
une bulle colorée, puis les réponses à droite (dessous sur GUI étroit).
Modification client uniquement, sans migration, écriture dans les mondes ni
changement des contrats fichier/MariaDB. Réponses, modération et suivi gardent
leurs validations serveur. Le portrait est un rendu GUI, pas une entité.
## Présentation
- Retour et Actualiser restent hors de la zone défilante.
- Le portrait utilise le skin de la connexion de l’auteur s’il est présent,
sinon le cache de profils Minecraft asynchrone. Le skin natif de repli apparaît
pendant le chargement ou si le profil ne peut pas être résolu (notamment les
comptes fictifs/hors ligne). Aucun téléchargement synchrone dans le rendu.
- Une bulle persistante encadrée montre catégorie/état, titre puis texte complet.
Information : jaune clair sur fond sombre jaune ; les autres catégories gardent
leurs couleurs. Les cartes de liste et du menu pause suivent aussi ce jaune.
- Le portrait tourne doucement dans des angles bornés selon la position de la
souris, sans capturer ses mouvements et sans clic requis.
- Demande et discussion défilent séparément si l’espace le permet ; le champ de
réponse et son bouton restent en bas de la discussion. Sur GUI étroit/bas,
un défilement unique garde les réponses sous la scène.
- Matériaux, lieu, récompense, suivi, visages des réponses et modération sont
conservés. Les détails d’une demande vide ne sont plus affichés artificiellement.
- Aucune animation de parole ni synchronisation vocale : « parle » désigne ici
la mise en scène visuelle avec la bulle.
## Vérification
- Compilation Java 25 / Minecraft 26.3 réussie.
- Parcours natif Vulkan : `NOTICE162_SCENE_PASS`, puis
`SEARCH159_CLIENT_PASS mode=file`, build réussi en 1 min 59 s.
- FR/EN, GUI 2/3/4 : scène, passage en colonne unique, boutons de retour au-dessus,
réponse au-dessus du footer en deux colonnes, angles du portrait bornés et
changement d’orientation avec la souris.
- Brouillon conservé pendant les redimensionnements ; publication d’une réponse
et lecture de son contenu après rechargement serveur vérifiées.
- Régressions communautaires : photos, recherche, suivi/désabonnement, marqueur
de carte et infobulle, restrictions de l’Intendance toujours validés.
- Captures relues : `build/qa-beta162/0042_notice162-scene-fr_fr-2.png` et
`0043_notice162-scene-fr_fr-3.png`. Conversation : `0048_notice162-conversation.png`.
- `./gradlew check build assemblePack` lancé : 252 GameTests terminés, 229 réussis
et 23 échecs, exactement les mêmes qu’en beta.161 (aucun ajouté/retiré).
Liste comparée : `build/session162-server-failures.json`.
- Contrôles restants et assemblage avec exclusion ponctuelle de `runGameTest` :
réussis en 3 min 16 s (`check build assemblePack -x :sanctuary:runGameTest`).
Aucune assertion désactivée dans les sources. Log : `build/beta162-assembly.log`.
Aucune modification de persistance : cette passe native utilise le mode fichier ;
le laboratoire MariaDB partagé a été relancé avec la même interface, KokaLab
connecté sur le monde existant `duo-flat-160`, Alice/Bob recréés, shader désactivé. Les figurants
et profils hors ligne peuvent afficher un skin Minecraft de repli.
## Livraison locale
JAR : `mods/sanctuary/build/libs/sanctuary-beta.162.jar`.
Pack assemblé : `build/packwiz`.
SHA-256 : `35c5d82eba110ee2150f73baf6341efc90f87971965a3b4dd2bcd04add0a59ff`.
Branche `codex/notice-scene-beta162`, tag local `beta.162`. Anciens JAR conservés.
Aucune publication distante ni synchronisation de Prism dans cette livraison.
Les 23 échecs préexistants restent ouverts ; les essais graphiques ne démontrent
pas la résolution réseau d’un skin officiel absent dans ce laboratoire hors ligne.
+27
View File
@@ -0,0 +1,27 @@
# beta.163 — cadre du menu pause
Retour de séance : Découverte/Progression séparés de Habitant/Factions/Combat,
bouton Monde retiré, séparateurs natifs de Progression, reprise centrée dans le
footer, calendrier à gauche, message serveur au centre et horloge à droite.
Interface cliente uniquement, sans migration ni modification de règles ou de
mondes. « Options du monde » reste dans les réglages en bas à gauche ; seul le
raccourci de navigation Monde disparaît. La carte centrale reste disponible.
Vérifications :
- Test client natif Vulkan réussi : FR/EN aux échelles GUI 2, 3 et 4,
calendrier, séparation des groupes, limites des widgets et reprise centrée.
Captures dans `build/qa-beta163/` ; journal `build/beta163-client-retry.log`.
- Le scénario communautaire vérifie aussi la scène du tableau et les échanges.
Une première exécution a expiré pendant une publication ; le test respecte
désormais le délai serveur entre deux écritures.
- `check build assemblePack --continue` : 23 échecs GameTest, ensemble identique
à beta.162, aucun nouvel échec. Les autres tâches ont terminé ; la suite
complète reste donc en échec. Comparaison dans `build/session163-server-failures.json`.
- Assemblage réussi avec `build assemblePack :sanctuary-test:exportDuoLaunch
-x :sanctuary:check`, après la vérification complète.
JAR local : `mods/sanctuary/build/libs/sanctuary-beta.163.jar`.
SHA-256 : `72df10885d1bcb3b623c46bddf361839c23f06dfd543119f91ac5e006a8f961e`.
Aucune publication distante ; anciens JAR conservés.
+2 -2
View File
@@ -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.161
pack_version=beta.161
mod_version=beta.163
pack_version=beta.163
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
@@ -141,6 +141,8 @@ public final class Community158ClientChecks implements FabricClientGameTest {
c.runOnClient(m->{press(m.gui.screen(),"sanctuary.community.save");press(m.gui.screen(),"sanctuary.community.audience.shared");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");press(m.gui.screen(),"sanctuary.community.publish");});
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.follow")&&!CommunityClient.pending(),400);
c.takeScreenshot("cards158-notice-details");
noticeSceneChecks(c);
world.getServer().runOnServer(server->{var inventory=server.getPlayerList().getPlayers().getFirst().getInventory();check(inventory.getItem(6).getCount()==17&&inventory.getItem(6).is(net.minecraft.world.item.Items.OAK_LOG)&&inventory.getItem(7).getCount()==3&&inventory.getItem(7).is(net.minecraft.world.item.Items.DIAMOND),"Descriptive chest preserves real inventory");});
c.runOnClient(m->{check(text(m.gui.screen(),"Un repas au village"),"Custom reward round trip");CommunityClient.open(new PauseScreen(true),"notice",noticeId,false);});
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.follow")&&!CommunityClient.pending(),300);c.waitTicks(25);
@@ -182,6 +184,7 @@ public final class Community158ClientChecks implements FabricClientGameTest {
c.runOnClient(m->{widgets(m.gui.screen()).filter(AbstractButton.class::isInstance).map(AbstractButton.class::cast).filter(b->b.getMessage().getString().startsWith("Allow Commands")).findFirst().orElseThrow().onPress(enter());widgets(m.gui.screen()).filter(AbstractButton.class::isInstance).map(AbstractButton.class::cast).filter(b->b.getMessage().getString().equals("Apply Changes")).findFirst().orElseThrow().onPress(enter());});
c.waitFor(m->ProgressionClient.operator,150);c.runOnClient(m->{m.gui.setScreen(null);m.getConnection().sendCommand("sanctuary community admin");});
c.waitFor(m->m.gui.screen() instanceof CommunityScreen&&has(m.gui.screen(),"sanctuary.community.publish")&&!CommunityClient.pending(),200);
c.waitTicks(25); // Respect the server write cooldown after the preceding unfollow.
c.runOnClient(m->{press(m.gui.screen(),"sanctuary.community.publish");edit(m.gui.screen()).setValue("Message officiel");body(m.gui.screen()).setValue("Bienvenue !");press(m.gui.screen(),"sanctuary.community.publish");});
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.back")&&!CommunityClient.pending(),250);
c.runOnClient(m->{check(!has(m.gui.screen(),"sanctuary.community.follow")&&!has(m.gui.screen(),"sanctuary.community.reply"),"Administrative bulletin has no quest or replies");CommunityClient.open(null,"bulletin","",false);});
@@ -197,16 +200,56 @@ public final class Community158ClientChecks implements FabricClientGameTest {
var date=widgets(m.gui.screen()).filter(StringWidget.class::isInstance).map(StringWidget.class::cast).filter(w->w.getMessage().getString().endsWith(java.time.Year.now().toString())).findFirst().orElseThrow();
var official=widgets(m.gui.screen()).filter(PauseTextButton.class::isInstance).map(PauseTextButton.class::cast).filter(w->w.getMessage().getString().contains("Message officiel")).findFirst().orElseThrow();
check(date.getRight()<official.getX(),"Full date and server message do not overlap");
var screen=m.gui.screen();
check(date.getX()==8&&date.getHeight()==38&&date.getY()==3,"Calendar framed at top left");
check(!has(screen,"sanctuary.progression.history"),"World navigation removed");
var discovery=pauseButton(screen,"sanctuary.progression.blocks");var progression=pauseButton(screen,"sanctuary.progression.progress");var inhabitant=pauseButton(screen,"sanctuary.progression.identity");
check(inhabitant.getY()-progression.getBottom()>progression.getY()-discovery.getBottom(),"Personal and social navigation groups separated");
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");
var resume=pauseButton(screen,"menu.returnToGame");
check(Math.abs(resume.getX()+resume.getWidth()/2-screen.width/2)<=1&&resume.getY()>screen.height-33,"Resume centered in footer");
check(pauseButton(screen,"options.worldOptions.button").getBottom()<screen.height-39,"World settings remain in body");
for(var child:screen.children())if(child instanceof AbstractWidget w)check(w.getX()>=0&&w.getRight()<=screen.width&&w.getY()>=0&&w.getBottom()<=screen.height,"Pause widget within viewport");
});
c.takeScreenshot("search159-pause-year-"+locale+"-"+scale);
c.takeScreenshot("pause163-calendar-"+locale+"-"+scale);
}
c.runOnClient(m->m.gui.setScreen(null));
System.out.println("PAUSE163_LAYOUT_PASS");
System.out.println("SEARCH159_CLIENT_PASS mode="+(database?"database":"file"));
} finally {intro.set(false);}
}
private static void noticeSceneChecks(ClientGameTestContext c) {
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();});
c.waitTicks(3);
c.runOnClient(m->{
var s=m.gui.screen();var scene=widgets(s).filter(CommunityNoticeScene.class::isInstance).map(CommunityNoticeScene.class::cast).findFirst().orElseThrow();
check(button(s,"back").getBottom()<=scene.getY(),"Back/refresh fixed above scene");
if(s.width>=522) {
check(body(s).getX()>=scene.getRight(),"Conversation to the right of author scene");
check(button(s,"reply").getBottom()<=s.height-33,"Reply stays above footer");
} else check(body(s).getY()>scene.getBottom(),"Narrow GUI stacks conversation below scene");
body(s).setValue("Brouillon scène162");hover(m,0,0);
});
c.waitTicks(3);
var left=new java.util.concurrent.atomic.AtomicReference<Float>();
c.runOnClient(m->{var scene=widgets(m.gui.screen()).filter(CommunityNoticeScene.class::isInstance).map(CommunityNoticeScene.class::cast).findFirst().orElseThrow();left.set(scene.yaw());hover(m,m.gui.screen().width,m.gui.screen().height);});
c.waitTicks(3);
c.runOnClient(m->{var scene=widgets(m.gui.screen()).filter(CommunityNoticeScene.class::isInstance).map(CommunityNoticeScene.class::cast).findFirst().orElseThrow();check(scene.yaw()>left.get()&&Math.abs(scene.pitch())<50,"Portrait tracks pointer within bounded angles");});
c.takeScreenshot("notice162-scene-"+locale+"-"+scale);
}
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();check(body(m.gui.screen()).getValue().equals("Brouillon scène162"),"Draft survives GUI resize");body(m.gui.screen()).setValue("Réponse scène162");press(m.gui.screen(),"sanctuary.community.reply");});
c.waitFor(m->!CommunityClient.pending()&&widgets(m.gui.screen()).filter(CommunityBubble.class::isInstance).map(CommunityBubble.class::cast).anyMatch(w->w.getMessage().getString().equals("Réponse scène162")),300);
c.takeScreenshot("notice162-conversation");
System.out.println("NOTICE162_SCENE_PASS");
}
private static void hover(net.minecraft.client.Minecraft m,double x,double y) {
var w=m.getWindow();double sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();
m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();
}
private static Button pauseButton(Screen screen,String key) {return screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals(key)).findFirst().orElseThrow();}
private static AbstractButton button(Screen screen,String key) {return widgets(screen).filter(AbstractButton.class::isInstance).map(AbstractButton.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.community."+key)).findFirst().orElseThrow();}
}
@@ -54,14 +54,14 @@ public final class Pause155ClientChecks implements FabricClientGameTest {
c.runOnClient(m->{
var s=m.gui.screen();
check(s instanceof PauseAtlasAccess,"Pause atlas access");
for(String key:List.of("sanctuary.progression.blocks","sanctuary.progression.history","sanctuary.progression.progress","sanctuary.progression.identity","menu.options","options.worldOptions.button","menu.returnToMenu","menu.returnToGame"))check(has(s,key),"Destination "+key);
var navigation=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getX()==8&&b.getY()>=40&&b.getY()<s.height-76).sorted(java.util.Comparator.comparingInt(Button::getY)).toList();
check(navigation.size()==6,"Six real navigation buttons");
for(int i=1;i<navigation.size();i++)check(navigation.get(i).getY()-navigation.get(i-1).getBottom()==4,"Uniform 4-pixel navigation spacing");
for(String key:List.of("sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.progression.identity","menu.options","options.worldOptions.button","menu.returnToMenu","menu.returnToGame"))check(has(s,key),"Destination "+key);
var navigation=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getX()==8&&b.getY()>=50&&b.getY()<nativeButton(s,"menu.options").getY()).sorted(java.util.Comparator.comparingInt(Button::getY)).toList();
check(navigation.size()==5&&!has(s,"sanctuary.progression.history"),"Five real navigation buttons without World");
for(int i=1;i<navigation.size();i++)check(navigation.get(i).getY()-navigation.get(i-1).getBottom()>=(i==2?8:3),"Two spaced navigation groups");
var options=nativeButton(s,"menu.options");var worldOptions=nativeButton(s,"options.worldOptions.button");var quit=nativeButton(s,"menu.returnToMenu");var resume=nativeButton(s,"menu.returnToGame");
check(options.getX()==8 && worldOptions.getX()==8 && quit.getX()==8 && worldOptions.getY()-options.getBottom()==4 && quit.getY()-worldOptions.getBottom()==4 && quit.getBottom()==s.height-8,"Options and quit stacked at bottom left");
check(options.getX()==8 && worldOptions.getX()==8 && quit.getX()==8 && worldOptions.getY()-options.getBottom()==4 && quit.getY()-worldOptions.getBottom()==4 && quit.getBottom()==s.height-39,"Options and quit stacked at bottom left");
var feed=areas(s).getFirst();
check(resume.getX()==feed.getX() && resume.getWidth()==feed.getWidth() && resume.getBottom()==s.height-8 && areas(s).getLast().getBottom()<resume.getY(),"Resume aligned below right community column");
check(Math.abs(resume.getX()+resume.getWidth()/2-s.width/2)<=1 && resume.getY()>s.height-33 && areas(s).getLast().getBottom()<resume.getY(),"Resume centered in native footer");
for(var e:s.children())if(e instanceof Button b && b.getX()==8 && b.getY()<options.getY())check(b.getBottom()<options.getY(),"Navigation does not overlap options");
check(!has(s,"gui.advancements") && !has(s,"gui.stats") && !has(s,"menu.game"),"Old grid removed");
for(var e:s.children())if(e instanceof AbstractWidget w)check(w.getX()>=0&&w.getRight()<=s.width&&w.getY()>=0&&w.getBottom()<=s.height,"Widget bounds "+w.getClass().getSimpleName()+" "+w.getX()+","+w.getY()+" "+w.getWidth()+"x"+w.getHeight()+" / "+s.width+"x"+s.height);
@@ -18,8 +18,8 @@ final class CommunityCard extends AbstractButton implements AutoCloseable {
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->0xff80b2d2;};}
private static int background(String category) {return switch(category){case "event"->0xff2e253a;case "work"->0xff322b20;case "need"->0xff20322c;default->0xff202d37;};}
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"))));
@@ -0,0 +1,75 @@
package fr.koka.sanctuary.client;
import com.google.gson.JsonObject;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.components.PlayerSkinWidget;
import net.minecraft.client.gui.narration.NarrationElementOutput;
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.util.FormattedCharSequence;
import net.minecraft.world.entity.player.PlayerSkin;
import net.minecraft.world.item.component.ResolvableProfile;
import static fr.koka.sanctuary.client.CommunityClient.*;
/** A GUI-only author portrait and persistent speech bubble; no world entity is created. */
final class CommunityNoticeScene extends AbstractWidget {
private final JsonObject entry;
private final List<FormattedCharSequence> heading,title,body;
private final Portrait portrait;
private final int portraitWidth,bubbleX;
CommunityNoticeScene(int width,JsonObject entry) {
super(0,0,width,160,Component.literal(str(entry,"title")).append("\n").append(str(entry,"body")));
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);
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);
portrait=new Portrait(portraitWidth,140,skin(entry));active=false;
}
private static Supplier<PlayerSkin> skin(JsonObject entry) {
var m=Minecraft.getInstance();var uuid=UUID.fromString(str(entry,"authorId"));
var fallback=m.playerSkinRenderCache().createLookup(ResolvableProfile.createUnresolved(uuid));
return ()->{
var connection=m.getConnection();var online=connection==null?null:connection.getPlayerInfo(uuid);
return online!=null?online.getSkin():fallback.get().playerSkin();
};
}
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"));
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.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;
for(var line:heading){g.text(Minecraft.getInstance().font,line,bx+9,top,accent,false);top+=11;}
top+=5;
for(var line:title){g.text(Minecraft.getInstance().font,line,bx+9,top,0xfffaf5e6,false);top+=11;}
top+=5;
for(var line:body){g.text(Minecraft.getInstance().font,line,bx+9,top,0xffeeeae1,false);top+=11;}
}
@Override protected void updateWidgetNarration(NarrationElementOutput output){defaultButtonNarrationText(output);}
private static final class Portrait extends PlayerSkinWidget {
private float pitch=-5,yaw=30;
Portrait(int width,int height,Supplier<PlayerSkin> skin){super(width,height,Minecraft.getInstance().getEntityModels(),skin);active=false;}
@Override protected void extractWidgetRenderState(GuiGraphicsExtractor g,int mx,int my,float delta) {
float nextYaw=(float)Math.toDegrees(Math.atan((mx-getX()-getWidth()/2f)/160f))*0.55f;
float nextPitch=-(float)Math.toDegrees(Math.atan((my-getY()-30)/180f))*0.4f;
// Native 26.3 skin widget applies 2.5 degrees per drag unit; use bounded absolute targets.
super.onDrag(null,(nextYaw-yaw)/2.5,(pitch-nextPitch)/2.5);
yaw=nextYaw;pitch=nextPitch;
super.extractWidgetRenderState(g,mx,my,delta);
}
@Override protected void onDrag(MouseButtonEvent event,double dx,double dy) { }
}
}
@@ -16,7 +16,8 @@ import static fr.koka.sanctuary.client.CommunityClient.*;
public final class CommunityPausePanels implements AutoCloseable {
private final Screen screen;
private final PauseCommunityFeed articles, notices;
private final StringWidget date, clock;
private final Calendar date;
private final StringWidget clock;
private final PauseTextButton bulletin;
private final PauseAtlasWidget map;
private int ticks;
@@ -24,47 +25,52 @@ public final class CommunityPausePanels implements AutoCloseable {
public CommunityPausePanels(Screen screen,Consumer<AbstractWidget> add,List<Button> nativeButtons) {
this.screen=screen;
var font=Minecraft.getInstance().font;
int margin=8,gap=10,top=40,bottom=screen.height-38;
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;
date=new StringWidget(margin,8,left,20,Component.empty(),font);
clock=new StringWidget(rightX,8,right,20,Component.empty(),font);
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));
int step=navHeight+navGap;
add.accept(new Frame(screen.width,screen.height,left,top+2*step+(groupGap-navGap)/2));
date=new Calendar(margin,3,Math.min(82,screen.width/4));
clock=new StringWidget(screen.width-52,12,44,20,Component.empty(),font);
add.accept(date);add.accept(clock);
int headerSide=Math.max(84,screen.width/5);
bulletin=new PauseTextButton(headerSide,3,screen.width-2*headerSide,32,text("bulletin"),()->open(screen,"bulletin",items("bulletins").isEmpty()?"":str(items("bulletins").get(0).getAsJsonObject(),"id"),false));
bulletin=new PauseTextButton(date.getRight()+10,3,Math.max(24,screen.width-date.getRight()-76),38,text("bulletin"),()->open(screen,"bulletin",items("bulletins").isEmpty()?"":str(items("bulletins").get(0).getAsJsonObject(),"id"),false));
add.accept(bulletin);
String[] sections={"blocks","history","progress","identity"};
int optionsTop=screen.height-76;
int navHeight=Math.min(20,Math.max(16,(optionsTop-top-28)/6));
int step=navHeight+4;
String[] sections={"blocks","progress","identity"};
for(int i=0;i<sections.length;i++) {
String target=sections[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,top+i*step,left,navHeight).build());
.bounds(margin,y,left,navHeight).build());
}
add.accept(Button.builder(fr.koka.sanctuary.cycle.CycleService.label("faction"),b->CycleClient.open(screen,"faction")).bounds(margin,top+4*step,left,navHeight).build());
add.accept(Button.builder(fr.koka.sanctuary.arena.Arenas.text("title"),b->ArenaClient.open(screen)).bounds(margin,top+5*step,left,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);
place(nativeButtons,add,"menu.returnToGame",rightX,screen.height-28,right);
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);
place(nativeButtons,add,"options.worldOptions.button",margin,optionsTop+24,left);
place(nativeButtons,add,"menu.options",margin,optionsTop,left,optionHeight);
place(nativeButtons,add,"options.worldOptions.button",margin,optionsTop+optionHeight+4,left,optionHeight);
for(var button:nativeButtons) if(key(button).equals("menu.returnToMenu") || key(button).equals("menu.disconnect")) {
button.setRectangle(left,20,margin,optionsTop+48);button.setTooltip(Tooltip.create(button.getMessage()));add.accept(button);
button.setRectangle(left,optionHeight,margin,optionsTop+2*(optionHeight+4));button.setTooltip(Tooltip.create(button.getMessage()));add.accept(button);
}
updateHeader();
}
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) {
buttons.stream().filter(b->key(b).equals(key)).findFirst().ifPresent(b->{b.setRectangle(width,20,x,y);b.setTooltip(Tooltip.create(b.getMessage()));add.accept(b);});
place(buttons,add,key,x,y,width,20);
}
private static void place(List<Button> buttons,Consumer<AbstractWidget> add,String key,int x,int y,int width,int height) {
buttons.stream().filter(b->key(b).equals(key)).findFirst().ifPresent(b->{b.setRectangle(width,height,x,y);b.setTooltip(Tooltip.create(b.getMessage()));add.accept(b);});
}
private static JsonArray items(String key) { return overview.has(key)?overview.getAsJsonArray(key):new JsonArray(); }
private void updateHeader() {
var locale=Locale.forLanguageTag(Minecraft.getInstance().getLanguageManager().getSelected().replace('_','-'));
var now=ZonedDateTime.now();
date.setMessage(Component.literal(DateTimeFormatter.ofPattern("d MMMM yyyy",locale).format(now)));
date.update(now,locale);
clock.setMessage(Component.literal(DateTimeFormatter.ofPattern("HH:mm",locale).format(now)));
clock.setX(screen.width-8-Minecraft.getInstance().font.width(clock.getMessage()));clock.setWidth(Minecraft.getInstance().font.width(clock.getMessage()));
var list=items("bulletins");
@@ -72,11 +78,39 @@ public final class CommunityPausePanels implements AutoCloseable {
if(!overviewError.isEmpty())message=text(overviewError);
bulletin.setMessage(message);
bulletin.setTooltip(Tooltip.create(text("bulletin").append("\n").append(list.isEmpty()?message:Component.literal(str(list.get(0).getAsJsonObject(),"body")))));
date.setWidth(Minecraft.getInstance().font.width(date.getMessage()));
int left=Math.max(date.getRight()+8,screen.width/5),right=screen.width-Math.max(56,screen.width/5);
bulletin.setRectangle(Math.max(24,right-left),32,left,3);
int left=date.getRight()+10,right=clock.getX()-10;
bulletin.setRectangle(Math.max(24,right-left),38,left,3);
bulletin.active=available();
}
/** 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;}
@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) { }
}
private static final class Calendar extends StringWidget {
private String day="",month="",year="";
Calendar(int x,int y,int width){super(x,y,width,38,Component.empty(),Minecraft.getInstance().font);}
void update(ZonedDateTime now,Locale locale){
day=DateTimeFormatter.ofPattern("d",locale).format(now);month=DateTimeFormatter.ofPattern("MMMM",locale).format(now);year=DateTimeFormatter.ofPattern("yyyy",locale).format(now);
setMessage(Component.literal(DateTimeFormatter.ofPattern("d MMMM yyyy",locale).format(now)));
}
@Override public void extractWidgetRenderState(net.minecraft.client.gui.GuiGraphicsExtractor g,int mx,int my,float delta) {
var font=Minecraft.getInstance().font;int x=getX(),y=getY();
g.fill(x,y,getRight(),getBottom(),0xA0202529);g.outline(x,y,width,height,0xFF66716E);
g.pose().pushMatrix();g.pose().translate(x+width/2f,y+3);g.pose().scale(1.4f,1.4f);
g.text(font,day,-font.width(day)/2,0,0xFFF0EEE6,false);g.pose().popMatrix();
g.centeredText(font,month,x+width/2,y+18,0xFFE0DED6);
g.centeredText(font,year,x+width/2,y+28,0xFF9FAAA4);
}
}
public void tick() {
if(closed)return;
map.tick(); updateHeader();
@@ -55,6 +55,7 @@ public final class CommunityScreen extends BlocodexFrame {
@Override protected void init() {
searchBox=null;
closePhotos();initFrame("history",text(kind)); contentX=(width-contentWidth)/2; int w = contentWidth - 28;
if(!composing && kind.equals("notice") && data.has("thread")) {noticeDetail(data.getAsJsonObject("thread"),w);return;}
var layout = LinearLayout.vertical().spacing(8);
if (!message.isEmpty()) line(layout,w,text(message));
if (!available()) line(layout,w,text("unavailable"));
@@ -100,23 +101,57 @@ public final class CommunityScreen extends BlocodexFrame {
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("bulletin")) {
line(layout,w,text("replies"));
var replies = thread.getAsJsonObject("replies");
for (var item : replies.getAsJsonArray("entries")) message(layout,w,item.getAsJsonObject());
pages(layout,w,replies);
if (str(entry,"status").equals("open")) {
useDraft("reply:"+id);
var replyDraft=draft;
var box=layout.addChild(MultiLineEditBox.builder().setPlaceholder(text("reply_hint")).build(font,w,52,text("reply")));
box.setCharacterLimit(1000); box.setValue(replyDraft.body); box.setValueListener(value -> replyDraft.body=value);
button(layout,100,"reply",() -> request(this,"create",replyDraft.operation,id,"reply","info","",replyDraft.body,0,0,result -> {
if (result.has("error")) { message=str(result,"error"); rebuildWidgets(); return; }
DRAFTS.values().removeIf(value -> value==replyDraft); message="saved"; needsLoad=true; stale=true;
}));
}
if (!kind.equals("bulletin")) { replies(layout,w,thread);replyComposer(layout,w,entry); }
}
private void replies(LinearLayout layout,int w,JsonObject thread) {
line(layout,w,text("replies"));
var replies=thread.getAsJsonObject("replies");
for(var item:replies.getAsJsonArray("entries"))message(layout,w,item.getAsJsonObject());
if(replies.getAsJsonArray("entries").isEmpty())line(layout,w,text("conversation_empty"));
pages(layout,w,replies);
}
private void replyComposer(LinearLayout layout,int w,JsonObject entry) {
if(!str(entry,"status").equals("open"))return;
useDraft("reply:"+id);var replyDraft=draft;
var box=layout.addChild(MultiLineEditBox.builder().setPlaceholder(text("reply_hint")).build(font,w,52,text("reply")));
box.setCharacterLimit(1000);box.setValue(replyDraft.body);box.setValueListener(value->replyDraft.body=value);
button(layout,Math.min(w,100),"reply",()->request(this,"create",replyDraft.operation,id,"reply","info","",replyDraft.body,0,0,result->{
if(result.has("error")){message=str(result,"error");rebuildWidgets();return;}
DRAFTS.values().removeIf(value->value==replyDraft);message="saved";needsLoad=true;stale=true;
}));
}
private void noticeDetail(JsonObject thread,int w) {
var entry=thread.getAsJsonObject("entry");
var toolbar=LinearLayout.horizontal().spacing(5);
button(toolbar,90,"back",()->{id="";page=0;data=new JsonObject();needsLoad=true;rebuildWidgets();});
button(toolbar,90,"refresh",()->needsLoad=true);
toolbar.setPosition(contentX+8,contentTop);toolbar.arrangeElements();toolbar.visitWidgets(this::addRenderableWidget);
int top=contentTop+28,availableHeight=contentBottom-top;
boolean columns=w>=470 && availableHeight>=180;
int leftWidth=columns?(w-20)*3/5:w;
var scene=LinearLayout.vertical().spacing(8);
if(!message.isEmpty())line(scene,leftWidth,text(message));
if(!available())line(scene,leftWidth,text("unavailable"));
scene.addChild(new CommunityNoticeScene(leftWidth,entry));
author(scene,leftWidth,entry,true);
if(expandedId.equals(id))controls(scene,leftWidth,entry);
taskDetails(scene,leftWidth,entry);
if(columns) {
scrollColumn(scene,leftWidth,contentX+8,top,availableHeight);
int rightWidth=w-leftWidth-20,rightX=contentX+8+leftWidth+20;
var conversation=LinearLayout.vertical().spacing(8);replies(conversation,rightWidth,thread);
int composeHeight=str(entry,"status").equals("open")?88:0;
scrollColumn(conversation,rightWidth,rightX,top,availableHeight-composeHeight);
if(composeHeight>0){var compose=LinearLayout.vertical().spacing(8);replyComposer(compose,rightWidth,entry);compose.setPosition(rightX,contentBottom-80);compose.arrangeElements();compose.visitWidgets(this::addRenderableWidget);}
} else {
replies(scene,w,thread);replyComposer(scene,w,entry);
scrollColumn(scene,w,contentX+8,top,availableHeight);
}
}
private void scrollColumn(LinearLayout layout,int w,int x,int y,int h) {
var scroll=new ScrollableLayout(minecraft,layout,Math.max(24,h),ScrollableLayout.ReserveStrategy.RIGHT);
scroll.setMinWidth(w);scroll.setPosition(x,y);scroll.arrangeElements();scroll.visitWidgets(this::addRenderableWidget);
}
private boolean own(JsonObject entry) { return minecraft.player!=null && str(entry,"authorId").equals(minecraft.player.getStringUUID()); }
private void articleAuthor(LinearLayout layout,int w,JsonObject entry) {
var row=layout.addChild(LinearLayout.horizontal().spacing(6));
@@ -209,6 +244,8 @@ public final class CommunityScreen extends BlocodexFrame {
}
private void taskDetails(LinearLayout layout,int w,JsonObject entry) {
var task=readTask(entry);
if(!task.equals(fr.koka.sanctuary.community.CommunityTask.EMPTY)) {
line(layout,w,text("request_details"));
if(task.location()!=null) {
line(layout,w,text("location_value",task.location().dimension(),task.location().x(),task.location().y(),task.location().z()));
var map=layout.addChild(Button.builder(text("map_show"),b->minecraft.gui.setScreen(new SanctuaryAtlasScreen(this,task.location()))).width(Math.min(w,220)).build());
@@ -220,6 +257,7 @@ public final class CommunityScreen extends BlocodexFrame {
if(task.reward().equals("items"))CommunityMaterialsScreen.showItems(layout,w,task.rewards());
if(task.reward().equals("custom"))line(layout,w,Component.literal(task.customReward()));
line(layout,w,text("exchange_help"));
}
int count=data.has("members")?data.getAsJsonArray("members").size():0;
line(layout,w,text("followers",count));
if(data.has("members"))for(var member:data.getAsJsonArray("members"))line(layout,w,Component.literal("• "+str(member.getAsJsonObject(),"playerName")));
@@ -2442,5 +2442,7 @@
"sanctuary.community.notice_full": "This request has no available places.",
"sanctuary.community.follow_map_active": "Following: a map marker appears when a location is provided.",
"sanctuary.community.follow_map_help": "Replying does not subscribe you. Follow this request to display its map marker.",
"sanctuary.community.notice_full_help": "The single place is taken. You can reply, but cannot follow this request."
"sanctuary.community.notice_full_help": "The single place is taken. You can reply, but cannot follow this request.",
"sanctuary.community.conversation_empty": "No replies yet.",
"sanctuary.community.request_details": "The request"
}
@@ -2442,5 +2442,7 @@
"sanctuary.community.notice_full": "Cette demande n’a plus de place disponible.",
"sanctuary.community.follow_map_active": "Demande suivie : son repère apparaît sur la carte si un lieu est indiqué.",
"sanctuary.community.follow_map_help": "Répondre ne vous abonne pas. Suivez la demande pour afficher son repère sur la carte.",
"sanctuary.community.notice_full_help": "La place unique est occupée. Vous pouvez répondre, mais pas suivre cette demande."
"sanctuary.community.notice_full_help": "La place unique est occupée. Vous pouvez répondre, mais pas suivre cette demande.",
"sanctuary.community.conversation_empty": "Aucune réponse pour le moment.",
"sanctuary.community.request_details": "La demande"
}
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.161"
version = "beta.163"
pack-format = "packwiz:1.1.0"
[index]