Compare commits

...
3 Commits
Author SHA1 Message Date
koka dbb9e3428d Lay out Gazette article and conversation for beta.165
Build Sanctuary / build (push) Waiting to run
2026-09-24 16:34:38 +02:00
koka 9a429c40a1 Align pause calendar and server bulletin for beta.164
Build Sanctuary / build (push) Waiting to run
2026-09-24 13:56:27 +02:00
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
12 changed files with 229 additions and 55 deletions
+17
View File
@@ -1,5 +1,22 @@
# Sanctuary
## beta.165 — article Gazette et conversation
Grande photo et texte dans un même conteneur à gauche, auteur dessous, réponses
à droite ou sous l’article sur petit écran.
[Résultat et vérifications](docs/gazette-scene-beta165.md).
## beta.164 — calendrier et intendance
Date et heure réunies à gauche ; titre serveur aligné sur la carte et les panneaux.
[Résultat et vérifications](docs/pause-calendar-beta164.md).
## 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,
+26
View File
@@ -0,0 +1,26 @@
# beta.165 — Gazette en article et conversation
Photo en tête, titre et description dessous dans le même conteneur sombre.
Auteur, visage, date et actions sous le conteneur. Réponses à droite avec
défilement indépendant et saisie fixe ; sur GUI étroit, réponses sous l’article.
Retour et Actualiser restent en haut, comme pour le tableau.
Interface cliente uniquement, sans changement de stockage ni de monde.
Test client Vulkan réussi en FR/EN aux échelles GUI 2, 3 et 4 :
position du panneau de réponses, repli sous l’article, défilement disponible,
brouillon conservé après redimensionnement et réponse envoyée puis relue.
Le scénario existant du tableau et celui du menu pause passent également.
Captures conservées dans `build/qa-beta165/` (image synthétique de test).
Inspection visuelle FR GUI 2 et GUI 4 effectuée.
Journal : `build/beta165-client.log`.
`check build assemblePack --continue` exécuté : mêmes 23 GameTests en échec
qu’en beta.164, aucun nouvel échec. Comparaison enregistrée dans
`build/session165-server-failures.json`. La vérification générale reste en échec.
Assemblage séparé réussi avec `build assemblePack :sanctuary-test:exportDuoLaunch
-x :sanctuary:check` après cette vérification.
JAR local : `mods/sanctuary/build/libs/sanctuary-beta.165.jar`.
SHA-256 : `03a954caf68089862b1d237dcf1ebaa501e3ce3cfedb1f5184e9412e73fd711d`.
Anciens JAR conservés ; aucune publication distante.
+25
View File
@@ -0,0 +1,25 @@
# beta.164 — calendrier compact et intendance alignée
Calendrier à la largeur de la navigation : jour agrandi et heure sur la première
ligne, mois et année ensemble dessous. Le titre serveur est aligné à gauche
et couvre la carte ainsi que les panneaux Gazette/Tableau.
Interface cliente uniquement. Aucun changement de stockage ou de monde.
Vérification client Vulkan réussie : FR/EN aux échelles GUI 2, 3 et 4.
Le test contrôle la largeur du calendrier, l’heure dans son libellé accessible,
les limites de l’intendance et les interactions communautaires existantes.
Captures conservées dans `build/qa-beta164/` et journal dans
`build/beta164-client-retry.log`. Inspection visuelle FR GUI 4 et EN GUI 2 effectuée.
Le premier test a détecté que StringWidget redimensionnait le calendrier lors
du changement de texte ; la largeur de navigation est maintenant réappliquée.
`check build assemblePack --continue` exécuté : les mêmes 23 GameTests qu’en
beta.163 échouent, aucun nouvel échec (comparaison enregistrée dans
`build/session164-server-failures.json`). La suite complète reste en échec.
L’assemblage séparé `build assemblePack :sanctuary-test:exportDuoLaunch
-x :sanctuary:check` réussit après ce contrôle.
Anciennes archives conservées ; aucune publication distante.
JAR : `mods/sanctuary/build/libs/sanctuary-beta.164.jar`.
SHA-256 : `5bdfb097004a7932a075fbce9013556266f76ddd491ca21a24e12609c8600e53`.
+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.162
pack_version=beta.162
mod_version=beta.165
pack_version=beta.165
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
@@ -95,6 +95,7 @@ public final class Community158ClientChecks implements FabricClientGameTest {
c.runOnClient(m->{check(button(m.gui.screen(),"publish").active,"Photo enables publishing");check(edit(m.gui.screen()).getValue().equals("Une journée au village"),"Selection retains draft");press(m.gui.screen(),"sanctuary.community.publish");});
c.waitFor(m->has(m.gui.screen(),"sanctuary.community.reply")&&!CommunityClient.pending(),600);
c.runOnClient(m->{var article=widgets(m.gui.screen()).filter(CommunityArticle.class::isInstance).map(CommunityArticle.class::cast).findFirst().orElseThrow();var face=widgets(m.gui.screen()).filter(PlayerFaceWidget.class::isInstance).map(PlayerFaceWidget.class::cast).findFirst().orElseThrow();check(face.getY()>=article.getBottom(),"Author metadata below the article");check(button(m.gui.screen(),"actions").getY()==face.getY(),"Author metadata next to ellipsis");check(widgets(m.gui.screen()).anyMatch(w->w instanceof AbstractWidget widget && widget.getMessage().getString().contains("/"+java.time.Year.now()+" ")),"Article date includes year");});
articleSceneChecks(c);
c.takeScreenshot("cards158-article"+(database?"-database":"-file"));
world.getServer().runOnServer(s->{try {
var config=CommunityConfig.load(net.fabricmc.loader.api.FabricLoader.getInstance().getConfigDir().resolve("sanctuary-community.json"));
@@ -184,6 +185,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);});
@@ -196,16 +198,56 @@ public final class Community158ClientChecks implements FabricClientGameTest {
c.runOnClient(m->{m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});
c.waitTicks(20);
c.runOnClient(m->{
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 date=widgets(m.gui.screen()).filter(StringWidget.class::isInstance).map(StringWidget.class::cast).filter(w->w.getMessage().getString().contains(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(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");
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("pause164-calendar-"+locale+"-"+scale);
}
c.runOnClient(m->m.gui.setScreen(null));
System.out.println("PAUSE164_LAYOUT_PASS");
System.out.println("SEARCH159_CLIENT_PASS mode="+(database?"database":"file"));
} finally {intro.set(false);}
}
private static void articleSceneChecks(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 article=widgets(s).filter(CommunityArticle.class::isInstance).map(CommunityArticle.class::cast).findFirst().orElseThrow();
check(button(s,"back").getBottom()<=article.getY(),"Gazette toolbar fixed above article");
if(s.width>=522) {
check(body(s).getX()>=article.getRight(),"Gazette conversation on the right");
check(button(s,"reply").getBottom()<=s.height-33,"Gazette composer above footer");
} else {
check(body(s).getY()>article.getBottom(),"Narrow Gazette stacks conversation below article");
check(widgets(s).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).anyMatch(a->a.maxScrollAmount()>0),"Narrow article and conversation scroll");
}
body(s).setValue("Brouillon Gazette165");
});
c.takeScreenshot("gazette165-article-"+locale+"-"+scale);
}
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();check(body(m.gui.screen()).getValue().equals("Brouillon Gazette165"),"Gazette draft survives resize");body(m.gui.screen()).setValue("Réponse Gazette165");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 Gazette165")),300);
c.takeScreenshot("gazette165-conversation");
System.out.println("GAZETTE165_SCENE_PASS");
}
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();});
@@ -235,5 +277,6 @@ public final class Community158ClientChecks implements FabricClientGameTest {
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);
@@ -8,30 +8,27 @@ import net.minecraft.client.gui.components.*;
import net.minecraft.client.gui.narration.NarrationElementOutput;
import net.minecraft.network.chat.Component;
import net.minecraft.util.FormattedCharSequence;
import net.minecraft.world.item.component.ResolvableProfile;
import static fr.koka.sanctuary.client.CommunityClient.*;
/** Photo left, generous headline and smaller text right, followed by the separate discussion. */
/** Full-width hero photo, headline and description share one dark article container. */
final class CommunityArticle extends AbstractWidget implements AutoCloseable {
private final JsonObject entry;
private final int imageWidth,textX;
private final int imageWidth,imageHeight;
private final float titleScale=1.35f;
private final List<FormattedCharSequence> title,body;
private CommunityPhotoWidget photo;
CommunityArticle(int width,JsonObject entry) {
super(0,0,width,100,Component.literal(str(entry,"title")));this.entry=entry;imageWidth=Math.max(72,width*42/100-16);textX=imageWidth+24;
super(0,0,width,100,Component.literal(str(entry,"title")));imageWidth=width-20;imageHeight=Math.max(40,imageWidth*9/16);
var font=Minecraft.getInstance().font;
title=font.split(getMessage().copy().withStyle(net.minecraft.ChatFormatting.BOLD),Math.max(30,(int)((width-textX-12)/titleScale)));
body=font.split(Component.literal(str(entry,"body")),Math.max(40,width-textX-12));
height=Math.max(imageWidth*9/16+24,24+title.size()*15+12+body.size()*11);
title=font.split(getMessage().copy().withStyle(net.minecraft.ChatFormatting.BOLD),Math.max(30,(int)((width-24)/titleScale)));
body=font.split(Component.literal(str(entry,"body")),Math.max(40,width-24));
height=12+imageHeight+14+title.size()*15+10+body.size()*11+12;
try {
if(!str(entry,"photo").isEmpty())photo=new CommunityPhotoWidget(imageWidth,Math.max(40,imageWidth*9/16),fr.koka.sanctuary.community.CommunityPhoto.decode(Base64.getDecoder().decode(str(entry,"photo")),960,540),text("photo"),false,null);
if(!str(entry,"photo").isEmpty())photo=new CommunityPhotoWidget(imageWidth,imageHeight,fr.koka.sanctuary.community.CommunityPhoto.decode(Base64.getDecoder().decode(str(entry,"photo")),960,540),text("photo"),false,null);
}catch(Exception ignored){}
active=false;
}
int textOffset(){return textX;}
@Override protected void extractWidgetRenderState(GuiGraphicsExtractor g,int mx,int my,float delta) {
var font=Minecraft.getInstance().font;int x=getX(),y=getY(),tx=x+textX,top=y+12;
var font=Minecraft.getInstance().font;int x=getX(),y=getY(),tx=x+12,top=y+12+imageHeight+14;
g.fill(x,y,getRight(),getBottom(),0xed171d23);
if(photo!=null){photo.setPosition(x+10,y+12);photo.extractRenderState(g,mx,my,delta);}
else g.text(font,text("photo_unavailable"),x+10,y+12,0xffaaaaaa,false);
@@ -16,67 +16,101 @@ 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 PauseTextButton bulletin;
private final PauseAtlasWidget map;
private int ticks;
private boolean closed;
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);
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));
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,left);
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","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)));
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()));
date.update(now,locale);
var list=items("bulletins");
Component message=list.isEmpty()?text("bulletin_empty"):Component.literal(str(list.get(0).getAsJsonObject(),"title"));
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=screen.width-8;
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="",monthYear="",time="";
private final int calendarWidth;
Calendar(int x,int y,int width){super(x,y,width,38,Component.empty(),Minecraft.getInstance().font);calendarWidth=width;}
void update(ZonedDateTime now,Locale locale){
day=DateTimeFormatter.ofPattern("d",locale).format(now);monthYear=DateTimeFormatter.ofPattern("MMMM yyyy",locale).format(now);time=DateTimeFormatter.ofPattern("HH:mm",locale).format(now);
setMessage(Component.literal(DateTimeFormatter.ofPattern("d MMMM yyyy HH:mm",locale).format(now)));
setWidth(calendarWidth);
}
@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+6,y+4);g.pose().scale(1.8f,1.8f);
g.text(font,day,0,0,0xFFF0EEE6,false);g.pose().popMatrix();
g.text(font,time,getRight()-6-font.width(time),y+9,0xFFE0DED6,false);
// Keep the month and year together even at the smallest GUI width.
float scale=Math.min(1f,(width-12f)/Math.max(1,font.width(monthYear)));
g.pose().pushMatrix();g.pose().translate(x+6,y+25);g.pose().scale(scale,scale);
g.text(font,monthYear,0,0,0xFFE0DED6,false);g.pose().popMatrix();
}
}
public void tick() {
if(closed)return;
map.tick(); updateHeader();
@@ -55,7 +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;}
if(!composing && (kind.equals("notice") || kind.equals("article")) && data.has("thread")) {communityDetail(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"));
@@ -120,7 +120,7 @@ public final class CommunityScreen extends BlocodexFrame {
DRAFTS.values().removeIf(value->value==replyDraft);message="saved";needsLoad=true;stale=true;
}));
}
private void noticeDetail(JsonObject thread,int w) {
private void communityDetail(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();});
@@ -132,10 +132,15 @@ public final class CommunityScreen extends BlocodexFrame {
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(kind.equals("article")) {
var article=new CommunityArticle(leftWidth,entry);cards.add(article);scene.addChild(article);
articleAuthor(scene,leftWidth,entry);
} else {
scene.addChild(new CommunityNoticeScene(leftWidth,entry));
author(scene,leftWidth,entry,true);
}
if(expandedId.equals(id))controls(scene,leftWidth,entry);
taskDetails(scene,leftWidth,entry);
if(kind.equals("notice"))taskDetails(scene,leftWidth,entry);
if(columns) {
scrollColumn(scene,leftWidth,contentX+8,top,availableHeight);
int rightWidth=w-leftWidth-20,rightX=contentX+8+leftWidth+20;
@@ -17,7 +17,7 @@ final class PauseTextButton extends AbstractButton {
var font=Minecraft.getInstance().font;
if(height>24) {
var lines=font.split(getMessage(),width-10);int count=Math.min(2,lines.size());
for(int i=0;i<count;i++)g.text(font,lines.get(i),getX()+(width-font.width(lines.get(i)))/2,getY()+(height-count*10)/2+i*10,active?0xFFF0EEE6:0xFF969696);
for(int i=0;i<count;i++)g.text(font,lines.get(i),getX()+5,getY()+(height-count*10)/2+i*10,active?0xFFF0EEE6:0xFF969696);
return;
}
g.text(font,font.plainSubstrByWidth(getMessage().getString(),width-10),getX()+5,getY()+(height-9)/2,active?0xFFF0EEE6:0xFF969696);
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.162"
version = "beta.165"
pack-format = "packwiz:1.1.0"
[index]