Compare commits

..
2 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
10 changed files with 131 additions and 40 deletions
+11
View File
@@ -1,5 +1,16 @@
# 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,
+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`.
+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.163
pack_version=beta.163
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"));
@@ -197,13 +198,16 @@ 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");
@@ -214,13 +218,36 @@ public final class Community158ClientChecks implements FabricClientGameTest {
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("pause163-calendar-"+locale+"-"+scale);
c.takeScreenshot("pause164-calendar-"+locale+"-"+scale);
}
c.runOnClient(m->m.gui.setScreen(null));
System.out.println("PAUSE163_LAYOUT_PASS");
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();});
@@ -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);
@@ -17,14 +17,12 @@ public final class CommunityPausePanels implements AutoCloseable {
private final Screen screen;
private final PauseCommunityFeed articles, notices;
private final Calendar date;
private final StringWidget clock;
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=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;
@@ -33,10 +31,9 @@ public final class CommunityPausePanels implements AutoCloseable {
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);
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));
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","progress","identity"};
for(int i=0;i<sections.length;i++) {
@@ -71,14 +68,12 @@ public final class CommunityPausePanels implements AutoCloseable {
var locale=Locale.forLanguageTag(Minecraft.getInstance().getLanguageManager().getSelected().replace('_','-'));
var now=ZonedDateTime.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");
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")))));
int left=date.getRight()+10,right=clock.getX()-10;
int left=date.getRight()+10,right=screen.width-8;
bulletin.setRectangle(Math.max(24,right-left),38,left,3);
bulletin.active=available();
}
@@ -96,19 +91,24 @@ public final class CommunityPausePanels implements AutoCloseable {
@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);}
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);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)));
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+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);
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() {
@@ -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.163"
version = "beta.165"
pack-format = "packwiz:1.1.0"
[index]