Render Atlas behind centered Pause controls with a centered North indicator

This commit is contained in:
koka
2026-10-06 22:12:26 +02:00
parent 00d175cb98
commit 5ea7950f23
6 changed files with 97 additions and 60 deletions
@@ -89,7 +89,7 @@ public final class Pause233ClientChecks implements FabricClientGameTest {
}
}
server.runOnServer(s->checked(()->{check(Arrays.equals(saved,Files.readAllBytes(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json"))),"Reading/resizing old and new posts never rewrites stored categories");return null;}));
System.out.println("PAUSE233_PASS: FR/EN GUI 2/3/4, parallel Adventure/Community before Atlas, narrow Community/Adventure stack beside Atlas, Game directly below, centered navigation block, visual Tab order, unified notice composer, descriptive task and photo publication, legacy category preservation");
System.out.println("PAUSE233_PASS: FR/EN GUI 2/3/4, Gazette left and Notice Board right, centered Adventure/Community and Game, fullscreen Atlas background, North below server message, foreground input priority, visual Tab order, unified notice composer, descriptive task and photo publication, legacy category preservation");
}finally {intro.set(false);checked(()->{if(previous==null)Files.deleteIfExists(config);else Files.write(config,previous);Files.deleteIfExists(fixture);return null;});}
}
private FileCommunityStore store(net.minecraft.server.MinecraftServer s)throws Exception{return new FileCommunityStore(s.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("data/sanctuary-community.json"),serverId);}
@@ -112,35 +112,44 @@ public final class Pause233ClientChecks implements FabricClientGameTest {
var worldOptions=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("options.worldOptions.button")).findFirst().orElseThrow();
var quit=s.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->Set.of("menu.returnToMenu","menu.disconnect").contains(key(b))).findFirst().orElseThrow();
var community=widget(s,"pause_group_community");var adventure=widget(s,"pause_group_adventure");var game=widget(s,"pause_group_game");
check(nav.stream().map(CommunityPausePanels::key).toList().equals(unlocked
?List.of("sanctuary.progression.identity","sanctuary.cycle.faction","sanctuary.arena.title","sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.story.title")
:List.of("sanctuary.progression.blocks","sanctuary.progression.identity","sanctuary.progression.progress","sanctuary.cycle.faction","sanctuary.story.title","sanctuary.arena.title")),"Navigation follows visual order in both layouts");
check(nav.stream().map(CommunityPausePanels::key).toList().equals(List.of("sanctuary.progression.blocks","sanctuary.progression.identity","sanctuary.progression.progress","sanctuary.cycle.faction","sanctuary.story.title","sanctuary.arena.title")),"Navigation has the same visual order before and after Atlas");
for(var button:nav)check(s.getFont().width(button.getMessage())+8<=button.getWidth(),"Navigation labels fit with native button padding");
check(nav.stream().allMatch(b->b.getBottom()<game.getY())&&game.getBottom()<options.getY()&&options.getBottom()<worldOptions.getY()&&worldOptions.getBottom()<quit.getY(),"Game follows below Sanctuary navigation");
check(options.getX()==nav.get(0).getX()&&worldOptions.getX()==options.getX()&&quit.getX()==options.getX()&&worldOptions.getWidth()==options.getWidth()&&quit.getWidth()==options.getWidth(),"Native Game actions share the navigation block width");
int bodyTop=s.height<280?46:50,bodyBottom=s.height-39;
check(Math.abs((Math.min(adventure.getY(),community.getY())+quit.getBottom())/2.-(bodyTop+bodyBottom)/2.)<=.5&&quit.getBottom()+8<=bodyBottom,"Complete block centered with clear space below Game");
var map=((PauseAtlasAccess)s).sanctuary$pauseMap();
var north=s.children().stream().filter(StringWidget.class::isInstance).map(StringWidget.class::cast).filter(w->w.getMessage().getString().equals("N ↑")).findFirst();
check(north.isPresent()==unlocked,"North indicator appears with Atlas");
if(north.isPresent()) {
var compass=north.orElseThrow();check(Math.abs(compass.getX()+compass.getWidth()/2.-s.width/2.)<=.5&&compass.getY()==bodyTop+1&&compass.getBottom()<adventure.getY(),"North centered below server message and above navigation");
}
int contentTop=unlocked?bodyTop+14:bodyTop;
check(Math.abs((adventure.getY()+quit.getBottom())/2.-(contentTop+bodyBottom)/2.)<=.5&&quit.getBottom()+8<=s.height-33,"Menu centered with clear space before the footer");
// KeyEvent carries the physical key and the SDL shortcut key separately.
s.setFocused(nav.get(0));s.keyPressed(new KeyEvent(InputConstants.KEY_TAB,'\t',0));check(s.getFocused()==nav.get(1),"Tab follows the second visible action");
s.keyPressed(new KeyEvent(InputConstants.KEY_TAB,'\t',0));check(s.getFocused()==nav.get(2),"Tab follows the third visible action");
if(!unlocked){
check(adventure.getY()==community.getY()&&adventure.getX()==nav.get(0).getX()&&community.getX()==nav.get(1).getX()&&adventure.getBottom()<nav.get(0).getY(),"Both headings align above their columns");
for(int row=0;row<3;row++) {
var left=nav.get(2*row);var right=nav.get(2*row+1);
check(left.getY()==right.getY()&&left.getHeight()==right.getHeight()&&left.getRight()<right.getX(),"Two parallel columns without overlap");
if(row>0)check(nav.get(2*row-2).getBottom()<left.getY(),"Rows remain separated");
}
check(options.getRight()==nav.get(1).getRight(),"Game spans both columns before Atlas");
check(Math.abs((nav.get(0).getX()+nav.get(1).getRight())/2.-s.width/2.)<=.5,"Two-column block centered horizontally");
check(widget(s,"article").getX()==8&&widget(s,"article").getRight()<nav.get(0).getX()&&widget(s,"notice").getX()>nav.get(1).getRight(),"Gazette remains left and Notice Board right");
check(adventure.getY()==community.getY()&&adventure.getX()==nav.get(0).getX()&&community.getX()==nav.get(1).getX()&&adventure.getBottom()<nav.get(0).getY(),"Both headings align above their columns");
for(int row=0;row<3;row++) {
var left=nav.get(2*row);var right=nav.get(2*row+1);
check(left.getY()==right.getY()&&left.getHeight()==right.getHeight()&&left.getRight()<right.getX(),"Two parallel columns without overlap");
if(row>0)check(nav.get(2*row-2).getBottom()<left.getY(),"Rows remain separated");
}
else {
check(community.getBottom()<nav.get(0).getY()&&nav.get(2).getBottom()<adventure.getY()&&adventure.getBottom()<nav.get(3).getY(),"Community then Adventure stacked above Game with Atlas");
for(int i=0;i<nav.size();i++) {
var button=nav.get(i);check(button.getX()==8&&button.getWidth()==options.getWidth()&&button.getWidth()<=132,"All actions use one narrow left column");
if(i>0)check(nav.get(i-1).getBottom()<button.getY(),"Stacked buttons remain separated");
check(options.getRight()==nav.get(1).getRight(),"Game spans both columns");
check(Math.abs((nav.get(0).getX()+nav.get(1).getRight())/2.-s.width/2.)<=.5,"Navigation stays centered horizontally");
check(widget(s,"article").getX()==8&&widget(s,"article").getRight()<nav.get(0).getX()&&widget(s,"notice").getX()>nav.get(1).getRight()&&widget(s,"article").getY()==widget(s,"notice").getY(),"Gazette remains left and Notice Board right at the same height");
if(map!=null) {
check(map.getX()==0&&map.getY()==0&&map.getWidth()==s.width&&map.getHeight()==s.height,"Atlas background covers the whole screen");
check(s.children().getLast()==map,"Map is last in input order behind foreground controls");
for(var button:List.of(nav.get(4),options,worldOptions,quit)) {
double mx=button.getX()+button.getWidth()/2.,my=button.getY()+button.getHeight()/2.;
check(!map.isMouseOver(mx,my)&&s.getChildAt(mx,my).orElseThrow()==button,"Foreground buttons shield the map from clicks and hover");
}
var map=((PauseAtlasAccess)s).sanctuary$pauseMap();check(nav.get(0).getRight()<map.getX()&&map.getRight()<widget(s,"article").getX()&&widget(s,"article").getX()==widget(s,"notice").getX(),"Narrow navigation left, map center and community feeds right");
for(var heading:List.of(widget(s,"article"),widget(s,"notice"))) {
double mx=heading.getX()+heading.getWidth()/2.,my=heading.getY()+heading.getHeight()/2.;
check(!map.isMouseOver(mx,my)&&s.getChildAt(mx,my).orElseThrow()==heading,"Feed headings remain above the map");
}
check(!map.isMouseOver(s.width/2.,20)&&!map.isMouseOver(s.width/2.,s.height-20),"Header and footer shield the background");
s.setFocused(quit);s.keyPressed(new KeyEvent(InputConstants.KEY_TAB,'\t',0));check(s.getFocused()==map,"Tab reaches the map after Quit");
}
}
private void shot(ClientGameTestContext c,String name){var path=c.takeScreenshot("pause233-"+name);checked(()->{var dir=game.resolve("../../../../../build/pause233-screenshots").normalize();Files.createDirectories(dir);Files.copy(path,dir.resolve(name+".png"),StandardCopyOption.REPLACE_EXISTING);return null;});}
@@ -52,8 +52,8 @@ public final class Solo232ClientChecks implements FabricClientGameTest {
var buttons=screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->Set.of("sanctuary.progression.blocks","sanctuary.progression.progress","sanctuary.story.title","sanctuary.progression.identity","sanctuary.cycle.faction","sanctuary.arena.title","menu.options","options.worldOptions.button","menu.returnToMenu","menu.disconnect").contains(CommunityPausePanels.key(b))).toList();
check(buttons.size()==9,"All nine pause menu actions remain present");
var article=heading(screen,"sanctuary.community.article");var notice=heading(screen,"sanctuary.community.notice");
if(!unlocked){check(article.getX()==8&&article.getY()==(screen.height<280?46:50)&&notice.getY()==article.getY(),"Locked map uses full-height Gazette on the left and Notice Board on the right");for(var button:buttons)check(article.getRight()<button.getX()&&notice.getX()>button.getRight(),"Community columns do not overlap centered navigation");}
else check(article.getX()==notice.getX()&&article.getY()<notice.getY(),"Unlocked map retains both community feeds stacked on the right");
check(article.getX()==8&&article.getY()==(screen.height<280?46:50)&&notice.getY()==article.getY(),"Gazette remains on the left and Notice Board on the right before and after Atlas");
for(var button:buttons)check(article.getRight()<button.getX()&&notice.getX()>button.getRight(),"Community columns do not overlap centered navigation");
for(var button:buttons)check(button.getY()>=50&&button.getBottom()<=screen.height-33,"Button stays in pause body");
Pause233ClientChecks.pause(screen,unlocked);
});
@@ -34,15 +34,33 @@ public final class Story234ClientChecks implements FabricClientGameTest {
var server=world.getServer();
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().getAbilities().invulnerable=true);
for(boolean unlocked:new boolean[]{false,true}) {
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var r=ProgressRecord.INITIAL;if(unlocked)r=r.buy("atlas",r.cost("atlas",ProgressionService.session(s).config));p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");});
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var r=ProgressRecord.INITIAL;if(unlocked)r=r.buy("atlas",r.cost("atlas",ProgressionService.session(s).config));p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");
if(unlocked)for(int dz=-2;dz<=2;dz++)for(int dx=-2;dx<=2;dx++) {
var tile=fr.koka.sanctuary.atlas.AtlasSampler.sample(p.level(),p.chunkPosition().x()+dx,p.chunkPosition().z()+dz);
check(tile!=null,"Loaded flat-world tile for the background map");fr.koka.sanctuary.atlas.AtlasService.observations(p).observe(tile);
}
});
c.waitFor(m->ProgressionClient.progress.atlas()==unlocked,200);
String before=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getAttached(ProgressionService.RECORD));
for(String language:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});
c.waitTicks(3);
c.runOnClient(m->Pause233ClientChecks.pause(m.gui.screen(),unlocked));
if(unlocked)c.waitFor(m->((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap().knownPixels()>4096,400);
shot(c,"pause-"+unlocked+"-"+language+"-"+scale);
c.runOnClient(m->{var pause=m.gui.screen();press(pause,"sanctuary.story.title");check(m.gui.screen() instanceof StoryScreen,"Story opens from Pause without Atlas gating");check(m.gui.screen().isPauseScreen(),"Story retains solo pause");check(m.gui.screen().getTitle().getString().equals("Story"),"Story has its own accessible screen title");check(edit(m.gui.screen()).getValue().isEmpty()&&cycle(m.gui.screen()).getValue().toString().equals("ALL"),"Empty search and All on opening");assertEntries(m.gui.screen(),100);layout(m.gui.screen());});
if(unlocked) {
c.runOnClient(m->{var pause=m.gui.screen();var map=((PauseAtlasAccess)pause).sanctuary$pauseMap();
check(pause.mouseClicked(new net.minecraft.client.input.MouseButtonEvent(pause.width/2.,pause.height-48,new net.minecraft.client.input.MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0)),false),"Native click on exposed background handled");
check(m.gui.screen() instanceof SanctuaryAtlasScreen,"Background click opens full Atlas");
m.gui.screen().keyPressed(new KeyEvent(InputConstants.KEY_ESCAPE,0,0));check(m.gui.screen()==pause,"Atlas Escape returns to original Pause");
pause.setFocused(((PauseAtlasAccess)pause).sanctuary$pauseMap());pause.keyPressed(new KeyEvent(InputConstants.KEY_RETURN,'\r',0));check(m.gui.screen() instanceof SanctuaryAtlasScreen,"Focused background opens Atlas with Enter");
press(m.gui.screen(),"gui.done");check(m.gui.screen()==pause,"Atlas Done returns to original Pause");
Pause233ClientChecks.pause(pause,true);
});
}
c.runOnClient(m->{var pause=m.gui.screen();var story=widgets(pause).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("sanctuary.story.title")).findFirst().orElseThrow();
check(pause.mouseClicked(new net.minecraft.client.input.MouseButtonEvent(story.getX()+story.getWidth()/2.,story.getY()+story.getHeight()/2.,new net.minecraft.client.input.MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,0)),false),"Native Story click handled above the map");
check(m.gui.screen() instanceof StoryScreen,"Story opens from Pause without Atlas gating");check(m.gui.screen().isPauseScreen(),"Story retains solo pause");check(m.gui.screen().getTitle().getString().equals("Story"),"Story has its own accessible screen title");check(edit(m.gui.screen()).getValue().isEmpty()&&cycle(m.gui.screen()).getValue().toString().equals("ALL"),"Empty search and All on opening");assertEntries(m.gui.screen(),100);layout(m.gui.screen());});
shot(c,"story-"+language+"-"+scale);
c.runOnClient(m->{var s=m.gui.screen();var search=edit(s);s.setFocused(search);for(int codepoint:"100".codePoints().toArray()){check(s.charTyped(new CharacterEvent(codepoint)),"Native typing handled");check(s.getFocused()==search&&search.isFocused(),"List refresh retains search focus");}check(search.getValue().equals("100")&&search.getCursorPosition()==3,"Continuous typing retains cursor");assertEntries(s,1);check(entries(s).getFirst().getMessage().getString().equals(StoryScreen.label("unknown_entry",100).getString()),"Search selects story 100");search.setValue(" 100 ");assertEntries(s,1);cycle(s).onPress(enter());check(has(s,"sanctuary.story.no_discoveries")&&!has(s,"sanctuary.story.no_results")&&entries(s).isEmpty(),"Discovered has a distinct empty state");cycle(s).onPress(enter());check(cycle(s).getValue().toString().equals("UNKNOWN"),"Third filter is Unknown");assertEntries(s,1);search.setValue("absent-story");check(has(s,"sanctuary.story.no_results")&&!has(s,"sanctuary.story.no_discoveries"),"Search empty state is distinct");search.setValue(language.equals("fr_fr")?"INCONNUE":"UNKNOWN");assertEntries(s,100);var area=area(s);check(s.mouseScrolled(area.getX()+5,area.getY()+5,0,-5)&&area.scrollAmount()>0,"Native wheel scrolls Story");area.setScrollAmount(area.maxScrollAmount());check(entries(s).getLast().getBottom()<=area.getBottom(),"Story 100 can be reached at the end");area.setScrollAmount(100);});
c.runOnClient(m->{m.getWindow().setWindowed(1344,1008);m.resizeGui();});c.waitTicks(3);
@@ -52,12 +70,12 @@ public final class Story234ClientChecks implements FabricClientGameTest {
c.runOnClient(m->{cycle(m.gui.screen()).onPress(enter());edit(m.gui.screen()).setValue("absent-story");});shot(c,"empty-search");
c.runOnClient(m->edit(m.gui.screen()).setValue("100"));shot(c,"search-100");
}
c.runOnClient(m->{var story=(StoryScreen)m.gui.screen();var pause=story.parent;story.keyPressed(new KeyEvent(InputConstants.KEY_ESCAPE,0,0));check(m.gui.screen()==pause,"Escape returns directly to original Pause");press(pause,"sanctuary.story.title");press(m.gui.screen(),"gui.done");check(m.gui.screen()==pause,"Done returns directly to original Pause");m.gui.setScreen(null);});
c.runOnClient(m->{var story=(StoryScreen)m.gui.screen();var pause=story.parent;story.keyPressed(new KeyEvent(InputConstants.KEY_ESCAPE,0,0));check(m.gui.screen()==pause,"Escape returns directly to original Pause");Pause233ClientChecks.pause(pause,unlocked);press(pause,"sanctuary.story.title");press(m.gui.screen(),"gui.done");check(m.gui.screen()==pause,"Done returns directly to original Pause");m.gui.setScreen(null);});
c.waitTicks(2);
}
server.runOnServer(s->check(before.equals(s.getPlayerList().getPlayers().getFirst().getAttached(ProgressionService.RECORD)),"Reading and searching Story leave authoritative progression unchanged"));
}
System.out.println("STORY234_PASS: Vulkan FR/EN GUI 2/3/4, Atlas gating, 100 disabled entries, live search, three filters, distinct empty states, typing focus, wheel, resize, Escape/Done, unchanged progression");
System.out.println("STORY234_PASS: Vulkan FR/EN GUI 2/3/4, centered Pause, surveyed fullscreen Atlas background, North below header, foreground click priority, native background click/Enter and Escape/Done returns, 100 disabled entries, live search, three filters, distinct empty states, typing focus, wheel, resize, unchanged progression");
c.runOnClient(m->m.gui.setScreen(null));
}finally{intro.set(false);}
}
@@ -21,7 +21,7 @@ public final class CommunityPausePanels implements AutoCloseable {
private final PauseAtlasWidget map;
private int ticks;
private boolean closed;
public CommunityPausePanels(Screen screen,Consumer<AbstractWidget> add,List<Button> nativeButtons) {
public CommunityPausePanels(Screen screen,Consumer<AbstractWidget> add,Consumer<AbstractWidget> background,Consumer<AbstractWidget> input,List<Button> nativeButtons) {
this.screen=screen;
boolean compact=screen.height<280;
boolean unlocked=ProgressionClient.progress.atlas();
@@ -30,51 +30,52 @@ public final class CommunityPausePanels implements AutoCloseable {
int columnGap=compact?4:8;
int minimumWidth=Arrays.stream(sections).map(CommunityPausePanels::sectionLabel).mapToInt(Minecraft.getInstance().font::width).max().orElse(0)+8;
int columnWidth=Math.max(minimumWidth,(Math.min(264,screen.width/3)-columnGap)/2);
int menuWidth=unlocked?Math.max(minimumWidth,Math.min(132,Math.max(76,screen.width/5))):2*columnWidth+columnGap;
int right=unlocked?Math.min(244,Math.max(104,screen.width*31/100))
:Math.min(244,(screen.width-2*margin-menuWidth-2*gap)/2);
int centerX=margin+menuWidth+gap,rightX=screen.width-margin-right,centerWidth=rightX-gap-centerX;
int menuX=unlocked?margin:(screen.width-menuWidth)/2;
int communityX=unlocked?menuX:menuX+columnWidth+columnGap;
int navGap=compact?(unlocked?1:2):4,groupGap=compact?(unlocked?2:8):12,titleHeight=compact&&unlocked?10:12;
int groupCount=unlocked?3:2;
int navHeight=Math.min(compact?14:20,Math.max(11,(bottom-top-16-groupCount*titleHeight-(groupCount-1)*groupGap-2*groupCount*navGap)/(3*groupCount)));
int menuWidth=2*columnWidth+columnGap;
int right=Math.min(244,(screen.width-2*margin-menuWidth-2*gap)/2);
int menuX=(screen.width-menuWidth)/2,rightX=screen.width-margin-right;
int communityX=menuX+columnWidth+columnGap;
int navGap=compact?1:4,groupGap=compact?4:12,titleHeight=compact?10:12;
int contentTop=unlocked?top+14:top;
int navHeight=Math.min(compact?14:20,Math.max(11,(bottom-contentTop-16-2*titleHeight-groupGap-4*navGap)/6));
int step=navHeight+navGap;
int sectionHeight=titleHeight+3*navHeight+2*navGap;
int menuHeight=groupCount*sectionHeight+(groupCount-1)*groupGap;
// Use a narrow stack beside the map, or parallel groups before Atlas.
int menuTop=top+Math.max(0,(bottom-top-menuHeight)/2);
int adventureTop=unlocked?menuTop+sectionHeight+groupGap:menuTop;
int menuHeight=2*sectionHeight+groupGap;
// The map is rendered below the foreground widgets, but receives input after them.
map=unlocked?new PauseAtlasWidget(screen,0,0,screen.width,screen.height):null;
if(map!=null)background.accept(map);
int menuTop=contentTop+Math.max(0,(bottom-contentTop-menuHeight)/2);
int adventureTop=menuTop;
int communityTop=menuTop;
int gameTop=menuTop+(groupCount-1)*(sectionHeight+groupGap);
int gameTop=menuTop+sectionHeight+groupGap;
int optionsTop=gameTop+titleHeight;
add.accept(new Frame(screen.width,screen.height));
date=new Calendar(margin,3,Math.min(132,Math.max(76,screen.width/5)));
add.accept(date);
bulletin=new PauseTextButton(centerX,3,screen.width-margin-centerX,38,text("bulletin"),()->open(screen,"bulletin",items("bulletins").isEmpty()?"":str(items("bulletins").get(0).getAsJsonObject(),"id"),false));
bulletin=new PauseTextButton(menuX,3,screen.width-margin-menuX,38,text("bulletin"),()->open(screen,"bulletin",items("bulletins").isEmpty()?"":str(items("bulletins").get(0).getAsJsonObject(),"id"),false));
add.accept(bulletin);
add.accept(new StringWidget(menuX,adventureTop,unlocked?menuWidth:columnWidth,9,text("pause_group_adventure").withStyle(net.minecraft.ChatFormatting.GRAY),Minecraft.getInstance().font));
add.accept(new StringWidget(communityX,communityTop,unlocked?menuWidth:columnWidth,9,text("pause_group_community").withStyle(net.minecraft.ChatFormatting.GRAY),Minecraft.getInstance().font));
if(unlocked) {
var font=Minecraft.getInstance().font;var north=Component.literal("N ↑");int northWidth=font.width(north);
add.accept(new StringWidget((screen.width-northWidth)/2,top+1,northWidth,9,north,font));
}
add.accept(new StringWidget(menuX,adventureTop,columnWidth,9,text("pause_group_adventure").withStyle(net.minecraft.ChatFormatting.GRAY),Minecraft.getInstance().font));
add.accept(new StringWidget(communityX,communityTop,columnWidth,9,text("pause_group_community").withStyle(net.minecraft.ChatFormatting.GRAY),Minecraft.getInstance().font));
add.accept(new StringWidget(menuX,gameTop,menuWidth,9,text("pause_group_game").withStyle(net.minecraft.ChatFormatting.GRAY),Minecraft.getInstance().font));
if(unlocked)sections=new String[]{"identity","faction","battle","blocks","progress","story"};
for(String target:sections) {
boolean adventure=target.equals("blocks")||target.equals("progress")||target.equals("story");
int y=(adventure?adventureTop:communityTop)+titleHeight+step*switch(target){case "blocks","identity"->0;case "progress","faction"->1;default->2;};
add.accept(Button.builder(sectionLabel(target),b->{switch(target){case "faction"->CycleClient.open(screen,"faction");case "battle"->ArenaClient.open(screen);default->ProgressionClient.open(screen,target);}})
.bounds(adventure?menuX:communityX,y,unlocked?menuWidth:columnWidth,navHeight).build());
.bounds(adventure?menuX:communityX,y,columnWidth,navHeight).build());
}
map=unlocked?new PauseAtlasWidget(screen,centerX,top,centerWidth,Math.max(30,bottom-top)):null;
if(map!=null)add.accept(map);
int resumeWidth=Math.min(200,screen.width-24);
place(nativeButtons,add,"menu.returnToGame",(screen.width-resumeWidth)/2,screen.height-26,resumeWidth);
int feedHeight=(bottom-top-gap)/2;
articles=new PauseCommunityFeed(screen,add,unlocked?rightX:margin,top,right,unlocked?feedHeight:bottom-top,"article");
notices=new PauseCommunityFeed(screen,add,rightX,unlocked?top+feedHeight+gap:top,right,unlocked?bottom-top-feedHeight-gap:bottom-top,"notice");
articles=new PauseCommunityFeed(screen,add,margin,top,right,bottom-top,"article");
notices=new PauseCommunityFeed(screen,add,rightX,top,right,bottom-top,"notice");
place(nativeButtons,add,"menu.options",menuX,optionsTop,menuWidth,navHeight);
place(nativeButtons,add,"options.worldOptions.button",menuX,optionsTop+step,menuWidth,navHeight);
for(var button:nativeButtons) if(key(button).equals("menu.returnToMenu") || key(button).equals("menu.disconnect")) {
button.setRectangle(menuWidth,navHeight,menuX,optionsTop+2*step);button.setTooltip(Tooltip.create(button.getMessage()));add.accept(button);
}
if(map!=null)input.accept(map);
updateHeader();
}
private static Component sectionLabel(String target) {
@@ -15,7 +15,7 @@ import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.world.level.material.MapColor;
/** Personal surveyed terrain only. A token and disposable texture belong to this pause instance. */
/** Fullscreen personal survey layer behind Pause. Its token and texture belong to this instance. */
public final class PauseAtlasWidget extends AbstractButton implements AutoCloseable {
private static final int SIZE=AtlasPayloads.PIXELS;
private static final Identifier TEXTURE=SanctuaryMod.id("pause_atlas_viewport");
@@ -74,7 +74,12 @@ public final class PauseAtlasWidget extends AbstractButton implements AutoClosea
return CommunityMapMarkers.project(dimension,wx->getX()+width/2+(int)Math.round((wx-centerX)*zoom()),
wz->getY()+height/2+(int)Math.round((wz-centerZ)*zoom()),getX(),getY()+20,getRight(),getBottom()-20);
}
@Override public boolean isMouseOver(double mx,double my) {
return super.isMouseOver(mx,my) && my>44 && my<owner.height-33
&& owner.children().stream().noneMatch(child->child!=this && child.isMouseOver(mx,my));
}
@Override public boolean mouseClicked(net.minecraft.client.input.MouseButtonEvent event,boolean doubleClick) {
if(!isMouseOver(event.x(),event.y()))return false;
var pin=CommunityMapMarkers.at(questPins(),event.x(),event.y());
if(event.button()==com.mojang.blaze3d.platform.InputConstants.MOUSE_BUTTON_LEFT && pin!=null){CommunityClient.open(owner,"notice",pin.marker().id(),false);return true;}
return super.mouseClicked(event,doubleClick);
@@ -99,12 +104,16 @@ public final class PauseAtlasWidget extends AbstractButton implements AutoClosea
int color=ProgressionClient.identity==null?0xFFFFFFFF:0xFF000000|ProgressionClient.identity.color();
g.fill(px-3,pz-3,px+4,pz+4,0xFF141818);g.fill(px-2,pz-2,px+3,pz+3,color);
}
boolean questHovered=CommunityMapMarkers.render(g,questPins(),mx,my);
setTooltip(questHovered?null:Tooltip.create(Component.translatable("sanctuary.pause.map_hint")));
g.text(font,"N ↑",x+7,y+7,0xFFFFFFFF);
if(pixels==0)g.textWithWordWrap(font,SanctuaryAtlasScreen.label(readable?"empty":status),x+8,y+height/2+12,width-16,0xFFCCCCCC);
g.text(font,font.plainSubstrByWidth(getMessage().getString(),width-14),x+7,getBottom()-16,0xFFF0EEE6);
if(isHoveredOrFocused()) {g.fill(x,y,getRight(),y+1,0xFFD0CCAF);g.fill(x,getBottom()-1,getRight(),getBottom(),0xFFD0CCAF);}
boolean exposed=isMouseOver(mx,my);
boolean questHovered=CommunityMapMarkers.render(g,questPins(),exposed?mx:-1,exposed?my:-1);
// Dim the terrain to keep the foreground labels legible.
g.fill(x,y,getRight(),getBottom(),0x50000000);
setTooltip(questHovered||!(exposed||isFocused())?null:Tooltip.create(Component.translatable("sanctuary.pause.map_hint")));
if(pixels==0) {
var message=SanctuaryAtlasScreen.label(readable?"empty":status);
String visible=font.plainSubstrByWidth(message.getString(),width/3);
g.text(font,visible,x+(width-font.width(visible))/2,getBottom()-48,0xFFCCCCCC);
}
g.disableScissor();
}
@Override protected void updateWidgetNarration(NarrationElementOutput output) {defaultButtonNarrationText(output);}
@@ -25,7 +25,7 @@ public abstract class CommunityPauseMixin extends Screen implements PauseAtlasAc
if (((PauseScreen)(Object)this).showsPauseMenu() && ProgressionClient.active()) {
var nativeButtons=children().stream().filter(Button.class::isInstance).map(Button.class::cast).toList();
clearWidgets();
sanctuary$community=new CommunityPausePanels(this,this::addRenderableWidget,nativeButtons);
sanctuary$community=new CommunityPausePanels(this,this::addRenderableWidget,this::addRenderableOnly,this::addWidget,nativeButtons);
}
}
@Inject(method="tick",at=@At("TAIL"))