Refine clay workshop and orient statues on placement for beta.111
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -20,10 +20,11 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
|||||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||||
à implémenter.
|
à implémenter.
|
||||||
|
|
||||||
La version **beta.110** réunit les développements jusqu’à l’atelier d’argile,
|
La version **beta.111** reprend ce socle et réorganise l’atelier d’argile avec un
|
||||||
aux œufs et à la neige saisonnière. Le [suivi de livraison](docs/clay-workshop-integration-beta110.md)
|
cadre Minecraft nine-slice ; les statuaires suivent le regard à la pose.
|
||||||
précise les vérifications et confirme la publication ainsi que la mise à jour
|
Le [ticket](docs/clay-workshop-ui-beta111.md) décrit la compatibilité et les essais.
|
||||||
de l’instance **Sanctuary Beta**, en conservant ses sauvegardes et réglages.
|
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
|
||||||
|
l’atelier, les œufs et la neige saisonnière dans l’instance **Sanctuary Beta**.
|
||||||
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
|
||||||
|
|
||||||
Les [archives pour le site](archives/web/README.md) conservent la maquette
|
Les [archives pour le site](archives/web/README.md) conservent la maquette
|
||||||
@@ -32,6 +33,14 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
|||||||
l’étude de Xaero et des alternatives libres.
|
l’étude de Xaero et des alternatives libres.
|
||||||
|
|
||||||
|
|
||||||
|
## beta.111 — Atelier d’argile et pose orientée
|
||||||
|
|
||||||
|
Cadre nine-slice, aperçu, fabrication et inventaire dans un menu commun.
|
||||||
|
Les statuaires s’orientent selon le regard horizontal à chaque pose ; les
|
||||||
|
anciens modèles gardent leur apparence. [Contrat et vérifications](docs/clay-workshop-ui-beta111.md).
|
||||||
|
[Pack normal](build/Sanctuary-beta.111.mrpack) ·
|
||||||
|
[Pack de test](build/Sanctuary-Test-beta.111.mrpack).
|
||||||
|
|
||||||
## beta.110 — Atelier d’argile et neige saisonnière réunis
|
## beta.110 — Atelier d’argile et neige saisonnière réunis
|
||||||
|
|
||||||
Le Clay Workshop / atelier d’argile, les statuaires et les modèles 3D rejoignent
|
Le Clay Workshop / atelier d’argile, les statuaires et les modèles 3D rejoignent
|
||||||
@@ -2053,7 +2062,7 @@ Depuis beta.090, la cible est **Minecraft 26.3 finale**, publiée le 15 septembr
|
|||||||
| Fabric API | 0.160.5+26.3 |
|
| Fabric API | 0.160.5+26.3 |
|
||||||
| Fabric Loom | 1.17.20 |
|
| Fabric Loom | 1.17.20 |
|
||||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||||
| Sanctuary / pack | beta.101 |
|
| Sanctuary / pack | beta.111 |
|
||||||
|
|
||||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||||
@@ -2082,7 +2091,7 @@ décrits dans [Validation](docs/testing.md).
|
|||||||
|
|
||||||
Résultats :
|
Résultats :
|
||||||
|
|
||||||
- `mods/sanctuary/build/libs/sanctuary-beta.092.jar` : mod à installer avec
|
- `mods/sanctuary/build/libs/sanctuary-beta.111.jar` : mod à installer avec
|
||||||
Fabric API sur la version Minecraft indiquée.
|
Fabric API sur la version Minecraft indiquée.
|
||||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# Backlog Sanctuary
|
# Backlog Sanctuary
|
||||||
|
|
||||||
|
## STAT-02 — Atelier d’argile et orientation — beta.111
|
||||||
|
|
||||||
|
Branche `codex/clay-workshop-ui-beta111`. [Contrat et vérifications](clay-workshop-ui-beta111.md).
|
||||||
|
Cadre nine-slice fourni, inventaire intégré et pose selon le regard horizontal.
|
||||||
|
Propriété `facing` additive ; les anciens statuaires gardent leur apparence.
|
||||||
|
|
||||||
## STAT-01 — Atelier d’argile et modèles 3D — beta.106
|
## STAT-01 — Atelier d’argile et modèles 3D — beta.106
|
||||||
|
|
||||||
Branche `codex/statuary-import-beta106`. [Contrat et validation](statuary-beta106.md).
|
Branche `codex/statuary-import-beta106`. [Contrat et validation](statuary-beta106.md).
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# STAT-02 — Atelier d’argile et orientation — beta.111
|
||||||
|
|
||||||
|
Contrat du 17 septembre 2026, branche `codex/clay-workshop-ui-beta111`.
|
||||||
|
|
||||||
|
## Résultat attendu
|
||||||
|
|
||||||
|
Le menu utilise le cadre `task_frame_unobtained.png` fourni par le créateur,
|
||||||
|
sans retouche, comme sprite Sanctuary avec les métadonnées nine-slice natives
|
||||||
|
de Minecraft. Les coins restent à leur taille d’origine. Catalogue, aperçu,
|
||||||
|
fabrication, explications et inventaire évolutif tiennent dans ce cadre.
|
||||||
|
La texture reste remplaçable par un pack de ressources.
|
||||||
|
|
||||||
|
La pose d’un statuaire suit le regard horizontal du joueur, dans les quatre
|
||||||
|
directions. Le bouton Tourner règle toujours l’orientation du modèle importé.
|
||||||
|
Le placement ajoute ensuite l’orientation du bloc sans réécrire ses voxels.
|
||||||
|
|
||||||
|
## Contrat de compatibilité préalable
|
||||||
|
|
||||||
|
Ajout de la propriété native `facing` au bloc `sanctuary:statuary`, avec `south`
|
||||||
|
par défaut : cette orientation correspond au rendu historique, sans rotation.
|
||||||
|
Les anciens états sans propriété prennent cette valeur à la lecture et gardent
|
||||||
|
leur apparence. Aucun parcours ni réécriture des anciens chunks n’est effectué.
|
||||||
|
Le schéma 1 de la sculpture, ses voxels, ses identifiants et les données des
|
||||||
|
objets restent identiques. Casser puis reposer utilise le nouveau regard,
|
||||||
|
sans cumuler les orientations précédentes. Rotation et miroir de structures
|
||||||
|
transforment l’état du bloc. La collision cubique reste celle déjà livrée.
|
||||||
|
|
||||||
|
## Texture
|
||||||
|
|
||||||
|
Le fichier fourni, 26 × 26 pixels, est conservé octet pour octet sous
|
||||||
|
`assets/sanctuary/textures/gui/sprites/container/clay_workshop/frame.png`.
|
||||||
|
Le fichier adjacent `.png.mcmeta` utilise le moteur `nine_slice` de Minecraft
|
||||||
|
avec une bordure fixe de quatre pixels et un centre extensible. L’identifiant
|
||||||
|
du sprite est `sanctuary:container/clay_workshop/frame` ; aucun sprite global
|
||||||
|
Minecraft n’est remplacé. Les emplacements utilisent `minecraft:container/slot`.
|
||||||
|
|
||||||
|
## Vérifications natives
|
||||||
|
|
||||||
|
`Statuary106ClientChecks`, étendu aux cas beta.111, passe en **1 min 3 s** sur
|
||||||
|
Minecraft 26.3, avec serveur intégré et monde plat jetable de graine 106 :
|
||||||
|
|
||||||
|
- Fabrication payée, restitutions, Maj-clic et teintes des seize argiles.
|
||||||
|
- Menu FR/EN aux échelles 2 et 3, cases et actions contenues dans le cadre.
|
||||||
|
- Six rangées d’inventaire à l’échelle 3 : affichage borné et défilement.
|
||||||
|
- Vrais clics de pose aux quatre points cardinaux : état côté serveur,
|
||||||
|
transmission au client et rotation du rendu alignés sur le regard.
|
||||||
|
- Rotation et miroir de structures ; lecture d’un ancien état sans `facing`.
|
||||||
|
- Butin d’une statue orientée au nord puis repose vers l’ouest : nouvelle
|
||||||
|
orientation, mêmes voxels. Sauvegarde/réouverture et transmission sans GLB.
|
||||||
|
- Conversion au Métabli et annulation d’import conservées.
|
||||||
|
|
||||||
|
Commande :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./gradlew :sanctuary:runClientGameTest \
|
||||||
|
-PsanctuaryClientTests=true -PsanctuaryStatuary106ClientTests=true \
|
||||||
|
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Marqueurs `STATUARY106_PASS`, `STATUARY106_INTEROP_PASS` et `STATUARY111_PASS`
|
||||||
|
dans `build/clay-ui111-client.log`. Captures dans `build/clay-ui111-evidence/`,
|
||||||
|
dont les menus français et anglais relus visuellement.
|
||||||
|
|
||||||
|
Le GameTest dédié reste exclu conformément au refus antérieur de son EULA.
|
||||||
|
Les essais tournent sur macOS avec serveur intégré ; pas de validation depuis
|
||||||
|
deux ordinateurs ni sur Windows. Aucun monde personnel n’est ouvert.
|
||||||
|
|
||||||
|
## Livraison vérifiée
|
||||||
|
|
||||||
|
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
|
||||||
|
passe en **2 min 33 s**, 125 tâches. Les sources JAR correspondent exactement
|
||||||
|
aux fichiers Java du dépôt. Le contenu du mod hors classes et ressources de
|
||||||
|
ce ticket est identique à beta.110 ; œufs, neige et fonte restent inclus.
|
||||||
|
Les deux MRpack contiennent le même JAR vérifié, sans monde, GLB ni classe de test.
|
||||||
|
|
||||||
|
- Pack normal : `build/Sanctuary-beta.111.mrpack`, 10 169 372 octets,
|
||||||
|
SHA-256 `f83098f80ac2edb8e32877bf8d5a362cd75f2fa9020593c2c7e4f9abe8b3e9f6`.
|
||||||
|
- Pack Test : `build/Sanctuary-Test-beta.111.mrpack`, 10 188 298 octets,
|
||||||
|
SHA-256 `1234b10eb4685d99aa7421de46bc54fd114b791f1b4d63803a09afc926d2c9d9`.
|
||||||
|
- JAR Sanctuary : SHA-256
|
||||||
|
`b3e967db7b336b970e4121981e3ef8140fe90f0ecc5850ce90a70caaf3772684`.
|
||||||
|
|
||||||
|
Reçu local : `build/clay-ui111-artifact.json`. Publication et synchronisation
|
||||||
|
de l’instance stable préparées après ces contrôles.
|
||||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
|||||||
fabric_api_version=0.160.5+26.3
|
fabric_api_version=0.160.5+26.3
|
||||||
|
|
||||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||||
mod_version=beta.110
|
mod_version=beta.111
|
||||||
pack_version=beta.110
|
pack_version=beta.111
|
||||||
resource_pack_version=beta.090
|
resource_pack_version=beta.090
|
||||||
maven_group=fr.koka.sanctuary
|
maven_group=fr.koka.sanctuary
|
||||||
jei_version=30.32.0-sanctuary.3
|
jei_version=30.32.0-sanctuary.3
|
||||||
|
|||||||
+54
-1
@@ -31,6 +31,7 @@ import net.minecraft.world.phys.*;
|
|||||||
|
|
||||||
/** Actual GLB texture/scene import, paid native crafting, placement, drops, reconnect and plan coexistence. */
|
/** Actual GLB texture/scene import, paid native crafting, placement, drops, reconnect and plan coexistence. */
|
||||||
public final class Statuary106ClientChecks implements FabricClientGameTest {
|
public final class Statuary106ClientChecks implements FabricClientGameTest {
|
||||||
|
private static final Map<Direction,BlockPos> DIRECTIONS=Map.of(Direction.NORTH,new BlockPos(0,1,4),Direction.SOUTH,new BlockPos(4,1,4),Direction.EAST,new BlockPos(-4,1,4),Direction.WEST,new BlockPos(4,1,-2));
|
||||||
private static final BlockPos WORKSHOP=new BlockPos(2,1,2),SCULPTURE=new BlockPos(2,1,0),TABLE=new BlockPos(-3,1,2);
|
private static final BlockPos WORKSHOP=new BlockPos(2,1,2),SCULPTURE=new BlockPos(2,1,0),TABLE=new BlockPos(-3,1,2);
|
||||||
private static final AtomicReference<VoxelSculpture> ORIGINAL=new AtomicReference<>();
|
private static final AtomicReference<VoxelSculpture> ORIGINAL=new AtomicReference<>();
|
||||||
private static final AtomicReference<String> MODEL=new AtomicReference<>();
|
private static final AtomicReference<String> MODEL=new AtomicReference<>();
|
||||||
@@ -61,7 +62,9 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
|||||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||||
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
saved=world.getWorldSave();c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||||
var server=world.getServer();
|
var server=world.getServer();
|
||||||
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.getInventory().clearContent();p.setGameMode(GameType.SURVIVAL);for(int x=-4;x<=6;x++)for(int z=-2;z<=6;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState());
|
server.runOnServer(s->{var p=player(s);p.teleportTo(.5,1,.5);p.getInventory().clearContent();p.setGameMode(GameType.SURVIVAL);for(int x=-7;x<=9;x++)for(int z=-5;z<=9;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());p.level().setBlockAndUpdate(WORKSHOP,Statuary.WORKSHOP.defaultBlockState());
|
||||||
|
var legacy=net.minecraft.nbt.NbtUtils.writeBlockState(Statuary.BLOCK.defaultBlockState());legacy.remove("properties");
|
||||||
|
check(net.minecraft.nbt.NbtUtils.readBlockState(net.minecraft.core.registries.BuiltInRegistries.BLOCK,legacy).getValue(Statuary.FACING)==Direction.SOUTH,"Legacy state defaults to unchanged model orientation");
|
||||||
for(var q:Metatable.members(TABLE))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());check(Metatable.assemble(p,TABLE),"Metabli");
|
for(var q:Metatable.members(TABLE))p.level().setBlockAndUpdate(q,Blocks.CRAFTING_TABLE.defaultBlockState());check(Metatable.assemble(p,TABLE),"Metabli");
|
||||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");});
|
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");});
|
||||||
c.waitFor(m->m.level.getBlockState(WORKSHOP).is(Statuary.WORKSHOP)&&!m.player.isCreative(),200);
|
c.waitFor(m->m.level.getBlockState(WORKSHOP).is(Statuary.WORKSHOP)&&!m.player.isCreative(),200);
|
||||||
@@ -71,6 +74,17 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
|||||||
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.getSlot(1).getItem().isEmpty(),"No clay, no result");menu.getSlot(0).set(new ItemStack(Items.CLAY,3));menu.broadcastChanges();});
|
server.runOnServer(s->{var p=player(s);var menu=(ClayWorkshopMenu)p.containerMenu;check(menu.getSlot(1).getItem().isEmpty(),"No clay, no result");menu.getSlot(0).set(new ItemStack(Items.CLAY,3));menu.broadcastChanges();});
|
||||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);
|
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);
|
||||||
c.takeScreenshot("statuary106-workshop-en");
|
c.takeScreenshot("statuary106-workshop-en");
|
||||||
|
checkWorkshopLayout(c);
|
||||||
|
c.runOnClient(m->{m.options.guiScale().set(3);m.resizeGui();});c.waitTicks(3);checkWorkshopLayout(c);c.takeScreenshot("statuary111-workshop-en-scale3");
|
||||||
|
// Six recovery rows still fit a small GUI and remain scrollable.
|
||||||
|
server.runOnServer(s->{var p=player(s);fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,new ItemStack(Items.STONE));p.containerMenu.broadcastChanges();});
|
||||||
|
c.waitFor(m->SanctuaryInventoryClient.visibleRows()==6,100);c.runOnClient(m->button(m.gui.screen(),"sanctuary.statuary.refresh").onPress(enter()));checkWorkshopLayout(c);
|
||||||
|
c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();var geometry=(SanctuaryContainerScreenGeometry)(Object)screen;var refresh=button(screen,"sanctuary.statuary.refresh");
|
||||||
|
check(screen.mouseScrolled(refresh.getX()-10+geometry.sanctuary$panelLeft()+30,refresh.getY()-127+geometry.sanctuary$panelTop()+35,0,1),"Extended workshop inventory can scroll");});
|
||||||
|
c.takeScreenshot("statuary111-workshop-en-six-rows");
|
||||||
|
server.runOnServer(s->{var p=player(s);fr.koka.sanctuary.inventory.SanctuaryOverflowAccess.of(p).setItem(17,ItemStack.EMPTY);p.containerMenu.broadcastChanges();});
|
||||||
|
c.waitFor(m->SanctuaryInventoryClient.visibleRows()==1,100);
|
||||||
|
c.runOnClient(m->{m.options.guiScale().set(2);m.resizeGui();});
|
||||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;ORIGINAL.set(StatuaryEntity.sculpture(menu.getSlot(1).getItem()));check(ORIGINAL.get().equals(source),"Plain clay keeps exact colors and model ID");
|
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;ORIGINAL.set(StatuaryEntity.sculpture(menu.getSlot(1).getItem()));check(ORIGINAL.get().equals(source),"Plain clay keeps exact colors and model ID");
|
||||||
for(var family:ColoredBricks.families()){var colored=ClayWorkshopMenu.material(source,new ItemStack(family.clay()));check(colored.modelId().equals(source.modelId())&&colored.cells().size()==source.cells().size()&&colored.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()==1,"All 16 clays keep geometry and ID, recolor uniformly");}});
|
for(var family:ColoredBricks.families()){var colored=ClayWorkshopMenu.material(source,new ItemStack(family.clay()));check(colored.modelId().equals(source.modelId())&&colored.cells().size()==source.cells().size()&&colored.cells().stream().map(VoxelSculpture.Cell::color).distinct().count()==1,"All 16 clays keep geometry and ID, recolor uniformly");}});
|
||||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
|
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.PICKUP,m.player));
|
||||||
@@ -87,11 +101,39 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
|||||||
server.runOnServer(s->{var p=player(s);p.level().destroyBlock(SCULPTURE,true);var dropped=p.level().getEntitiesOfClass(ItemEntity.class,new AABB(SCULPTURE).inflate(1)).stream().filter(e->e.getItem().is(Statuary.ITEM)).findFirst().orElseThrow();
|
server.runOnServer(s->{var p=player(s);p.level().destroyBlock(SCULPTURE,true);var dropped=p.level().getEntitiesOfClass(ItemEntity.class,new AABB(SCULPTURE).inflate(1)).stream().filter(e->e.getItem().is(Statuary.ITEM)).findFirst().orElseThrow();
|
||||||
check(StatuaryEntity.sculpture(dropped.getItem()).equals(source),"Native drop keeps name, ID and voxels");p.getInventory().setItem(p.getInventory().getSelectedSlot(),dropped.getItem().copy());dropped.discard();p.inventoryMenu.broadcastFullState();});
|
check(StatuaryEntity.sculpture(dropped.getItem()).equals(source),"Native drop keeps name, ID and voxels");p.getInventory().setItem(p.getInventory().getSelectedSlot(),dropped.getItem().copy());dropped.discard();p.inventoryMenu.broadcastFullState();});
|
||||||
c.waitFor(m->m.level.getBlockState(SCULPTURE).isAir()&&m.player.getMainHandItem().is(Statuary.ITEM),100);place(c,SCULPTURE);server.waitFor(s->player(s).level().getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);
|
c.waitFor(m->m.level.getBlockState(SCULPTURE).isAir()&&m.player.getMainHandItem().is(Statuary.ITEM),100);place(c,SCULPTURE);server.waitFor(s->player(s).level().getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);
|
||||||
|
// Real use-key placements must follow the gaze on both sides of the network.
|
||||||
|
for(var direction:Direction.Plane.HORIZONTAL){
|
||||||
|
var pos=DIRECTIONS.get(direction);double px=pos.getX()+.5-direction.getStepX()*2,pz=pos.getZ()+.5-direction.getStepZ()*2;
|
||||||
|
server.runOnServer(s->{var p=player(s);p.teleportTo(px,1,pz);p.getInventory().setItem(p.getInventory().getSelectedSlot(),StatuaryEntity.item(source));p.inventoryMenu.broadcastFullState();});
|
||||||
|
c.waitFor(m->m.player.distanceToSqr(px,1,pz)<.05&&m.player.getMainHandItem().is(Statuary.ITEM),150);place(c,pos);
|
||||||
|
server.waitFor(s->player(s).level().getBlockState(pos).is(Statuary.BLOCK),100);
|
||||||
|
server.runOnServer(s->{var state=player(s).level().getBlockState(pos);check(state.getValue(Statuary.FACING)==direction,"Native placement faces gaze: "+direction);
|
||||||
|
check(state.rotate(net.minecraft.world.level.block.Rotation.CLOCKWISE_90).getValue(Statuary.FACING)==direction.getClockWise(),"Structure rotation follows native directions");
|
||||||
|
check(state.mirror(net.minecraft.world.level.block.Mirror.LEFT_RIGHT).getValue(Statuary.FACING)==(direction.getAxis()==Direction.Axis.Z?direction.getOpposite():direction),"Structure mirror follows native directions");
|
||||||
|
check(((StatuaryEntity)player(s).level().getBlockEntity(pos)).sculpture().equals(source),"Placement never rewrites model voxels");});
|
||||||
|
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&source.equals(e.sculpture()),100);
|
||||||
|
c.runOnClient(m->{var entity=(StatuaryEntity)m.level.getBlockEntity(pos);check(entity.getBlockState().getValue(Statuary.FACING)==direction,"Client receives direction");
|
||||||
|
var renderer=new StatuaryRenderer();var state=renderer.createRenderState();renderer.extractRenderState(entity,state,0,Vec3.ZERO,null);
|
||||||
|
var forward=new org.joml.Vector3f(0,0,1).rotate(com.mojang.math.Axis.YP.rotationDegrees(state.yaw));
|
||||||
|
check(Math.abs(forward.x()-direction.getStepX())<.001&&Math.abs(forward.z()-direction.getStepZ())<.001,"Rendered model faces gaze: "+direction);});
|
||||||
|
c.takeScreenshot("statuary111-facing-"+direction.getSerializedName());
|
||||||
|
}
|
||||||
|
// Re-place a dropped north-facing model while looking west: no baked or cumulative turn.
|
||||||
|
var north=DIRECTIONS.get(Direction.NORTH);var west=DIRECTIONS.get(Direction.WEST);
|
||||||
|
server.runOnServer(s->{var p=player(s);p.level().destroyBlock(north,true);p.level().removeBlock(west,false);
|
||||||
|
var drop=p.level().getEntitiesOfClass(ItemEntity.class,new AABB(north).inflate(1)).stream().filter(e->e.getItem().is(Statuary.ITEM)).findFirst().orElseThrow();
|
||||||
|
check(source.equals(StatuaryEntity.sculpture(drop.getItem())),"Direction does not alter dropped item");
|
||||||
|
p.getInventory().setItem(p.getInventory().getSelectedSlot(),drop.getItem().copy());drop.discard();p.teleportTo(west.getX()+2.5,1,west.getZ()+.5);p.inventoryMenu.broadcastFullState();});
|
||||||
|
c.waitFor(m->m.player.distanceToSqr(west.getX()+2.5,1,west.getZ()+.5)<.05&&m.player.getMainHandItem().is(Statuary.ITEM)&&m.level.getBlockState(west).isAir(),100);place(c,west);
|
||||||
|
server.waitFor(s->player(s).level().getBlockState(west).is(Statuary.BLOCK),100);
|
||||||
|
server.runOnServer(s->check(player(s).level().getBlockState(west).getValue(Statuary.FACING)==Direction.WEST,"Dropped statue takes new gaze"));
|
||||||
|
server.runOnServer(s->player(s).teleportTo(.5,1,.5));c.waitFor(m->m.player.distanceToSqr(.5,1,.5)<.05,100);
|
||||||
// Reuse the same file with colored clay; server, rather than the client, chooses the tint.
|
// Reuse the same file with colored clay; server, rather than the client, chooses the tint.
|
||||||
server.runOnServer(s->player(s).getInventory().setItem(player(s).getInventory().getSelectedSlot(),ItemStack.EMPTY));c.waitTicks(4);open(c);
|
server.runOnServer(s->player(s).getInventory().setItem(player(s).getInventory().getSelectedSlot(),ItemStack.EMPTY));c.waitTicks(4);open(c);
|
||||||
c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).load(file));c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
c.runOnClient(m->((ClayWorkshopScreen)m.gui.screen()).load(file));c.waitFor(m->!((ClayWorkshopScreen)m.gui.screen()).busy()&&((ClayWorkshopScreen)m.gui.screen()).selected()!=null,200);c.waitTicks(10);
|
||||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=ColoredBricks.families().stream().filter(f->f.color()==DyeColor.BLUE).findFirst().orElseThrow();menu.getSlot(0).set(new ItemStack(blue.clay(),2));menu.broadcastChanges();});
|
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=ColoredBricks.families().stream().filter(f->f.color()==DyeColor.BLUE).findFirst().orElseThrow();menu.getSlot(0).set(new ItemStack(blue.clay(),2));menu.broadcastChanges();});
|
||||||
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);language(c,"fr_fr");c.takeScreenshot("statuary106-workshop-fr-blue");
|
c.waitFor(m->m.player.containerMenu.getSlot(1).getItem().is(Statuary.ITEM),100);language(c,"fr_fr");c.takeScreenshot("statuary106-workshop-fr-blue");
|
||||||
|
checkWorkshopLayout(c);
|
||||||
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=StatuaryEntity.sculpture(menu.getSlot(1).getItem());check(blue.modelId().equals(MODEL.get())&&blue.cells().stream().allMatch(v->v.color()==0xFFA0A7BC),"Blue input overrides uploaded model colors");});
|
server.runOnServer(s->{var menu=(ClayWorkshopMenu)player(s).containerMenu;var blue=StatuaryEntity.sculpture(menu.getSlot(1).getItem());check(blue.modelId().equals(MODEL.get())&&blue.cells().stream().allMatch(v->v.color()==0xFFA0A7BC),"Blue input overrides uploaded model colors");});
|
||||||
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(10);
|
c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,1,0,ContainerInput.QUICK_MOVE,m.player));c.waitTicks(10);
|
||||||
server.runOnServer(s->{var p=player(s);check(p.containerMenu.getSlot(0).getItem().isEmpty(),"Shift crafting consumes stock");check(count(p,Statuary.ITEM)==2,"Shift crafting produces exactly paid quantity");});close(c);
|
server.runOnServer(s->{var p=player(s);check(p.containerMenu.getSlot(0).getItem().isEmpty(),"Shift crafting consumes stock");check(count(p,Statuary.ITEM)==2,"Shift crafting produces exactly paid quantity");});close(c);
|
||||||
@@ -110,13 +152,24 @@ public final class Statuary106ClientChecks implements FabricClientGameTest {
|
|||||||
try(var world=saved.open()){
|
try(var world=saved.open()){
|
||||||
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();
|
c.waitFor(m->m.player!=null&&m.gui.screen()==null,1200);var server=world.getServer();
|
||||||
server.runOnServer(s->{var entity=(StatuaryEntity)player(s).level().getBlockEntity(SCULPTURE);check(entity.sculpture().equals(ORIGINAL.get()),"Server save and reload preserve source-independent voxels");check(count(player(s),Statuary.ITEM)==2,"Colored inventory statues survive reconnect");});
|
server.runOnServer(s->{var entity=(StatuaryEntity)player(s).level().getBlockEntity(SCULPTURE);check(entity.sculpture().equals(ORIGINAL.get()),"Server save and reload preserve source-independent voxels");check(count(player(s),Statuary.ITEM)==2,"Colored inventory statues survive reconnect");});
|
||||||
|
for(var direction:List.of(Direction.SOUTH,Direction.EAST,Direction.WEST)){
|
||||||
|
var pos=DIRECTIONS.get(direction);
|
||||||
|
server.runOnServer(s->check(player(s).level().getBlockState(pos).getValue(Statuary.FACING)==direction&&((StatuaryEntity)player(s).level().getBlockEntity(pos)).sculpture().equals(ORIGINAL.get()),"Direction and model survive server save/reload"));
|
||||||
|
c.waitFor(m->m.level.getBlockEntity(pos) instanceof StatuaryEntity e&&ORIGINAL.get().equals(e.sculpture())&&e.getBlockState().getValue(Statuary.FACING)==direction,150);
|
||||||
|
}
|
||||||
c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&e.sculpture()!=null,150);c.runOnClient(m->check(((StatuaryEntity)m.level.getBlockEntity(SCULPTURE)).sculpture().equals(ORIGINAL.get()),"Fresh client receives server model without file"));
|
c.waitFor(m->m.level.getBlockEntity(SCULPTURE) instanceof StatuaryEntity e&&e.sculpture()!=null,150);c.runOnClient(m->check(((StatuaryEntity)m.level.getBlockEntity(SCULPTURE)).sculpture().equals(ORIGINAL.get()),"Fresh client receives server model without file"));
|
||||||
c.getInput().lookAt(SCULPTURE);c.takeScreenshot("statuary106-reload-no-source");
|
c.getInput().lookAt(SCULPTURE);c.takeScreenshot("statuary106-reload-no-source");
|
||||||
}
|
}
|
||||||
System.out.println("STATUARY106_PASS GLB textured scene, bounded import, shared folder filters, native paid crafting, 16 clay colors, model ID, shift-click, place/drop/re-place, Metabli plan, cancellation, server save/reconnect without source");
|
System.out.println("STATUARY106_PASS GLB textured scene, bounded import, shared folder filters, native paid crafting, 16 clay colors, model ID, shift-click, place/drop/re-place, Metabli plan, cancellation, server save/reconnect without source");
|
||||||
|
System.out.println("STATUARY111_PASS nine-slice GUI FR/EN scales 2/3, four native gaze placements, renderer directions, structure rotation/mirror, legacy state default, drop/re-place and persisted orientation");
|
||||||
}catch(IOException e){throw new AssertionError(e);}finally{try{Files.deleteIfExists(file);}catch(IOException ignored){}}
|
}catch(IOException e){throw new AssertionError(e);}finally{try{Files.deleteIfExists(file);}catch(IOException ignored){}}
|
||||||
}
|
}
|
||||||
private static ServerPlayer player(net.minecraft.server.MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
private static ServerPlayer player(net.minecraft.server.MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||||
|
private static void checkWorkshopLayout(ClientGameTestContext c){c.runOnClient(m->{var screen=(ClayWorkshopScreen)m.gui.screen();var refresh=button(screen,"sanctuary.statuary.refresh");int left=refresh.getX()-10,top=refresh.getY()-127;
|
||||||
|
check(top>=0&&top+screen.panelHeight()<=screen.height,"Workshop frame fits GUI height");
|
||||||
|
for(var slot:screen.getMenu().slots)if(slot.x>=0&&slot.y>=0)check(slot.x>3&&slot.x+17<310&&slot.y>3&&slot.y+17<screen.panelHeight(),"Every visible slot fits inside nine-slice frame");
|
||||||
|
for(var widget:screen.children())if(widget instanceof Button b)check(b.getX()>=left&&b.getRight()<=left+310&&b.getY()>=top&&b.getBottom()<=top+145,"Every action stays clear of explanations and inventory");
|
||||||
|
});}
|
||||||
private static void close(ClientGameTestContext c){c.runOnClient(m->m.gui.screen().onClose());c.waitFor(m->m.gui.screen()==null,100);}
|
private static void close(ClientGameTestContext c){c.runOnClient(m->m.gui.screen().onClose());c.waitFor(m->m.gui.screen()==null,100);}
|
||||||
private static void open(ClientGameTestContext c){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(WORKSHOP),Direction.UP,WORKSHOP,false)));c.waitForScreen(ClayWorkshopScreen.class);}
|
private static void open(ClientGameTestContext c){c.runOnClient(m->m.gameMode.useItemOn(m.player,InteractionHand.MAIN_HAND,new BlockHitResult(Vec3.atCenterOf(WORKSHOP),Direction.UP,WORKSHOP,false)));c.waitForScreen(ClayWorkshopScreen.class);}
|
||||||
private static void place(ClientGameTestContext c,BlockPos pos){
|
private static void place(ClientGameTestContext c,BlockPos pos){
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ import net.minecraft.world.item.ItemStack;
|
|||||||
|
|
||||||
/** File selection is local; the two ordinary inventory slots and every crafted item are server-owned. */
|
/** File selection is local; the two ordinary inventory slots and every crafted item are server-owned. */
|
||||||
public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorkshopMenu> {
|
public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorkshopMenu> {
|
||||||
|
private static final Identifier FRAME=fr.koka.sanctuary.SanctuaryMod.id("container/clay_workshop/frame");
|
||||||
|
private static final Identifier SLOT=Identifier.withDefaultNamespace("container/slot");
|
||||||
private static final ExecutorService WORKER=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"Sanctuary clay models");t.setDaemon(true);return t;});
|
private static final ExecutorService WORKER=Executors.newSingleThreadExecutor(r->{var t=new Thread(r,"Sanctuary clay models");t.setDaemon(true);return t;});
|
||||||
private List<Path> files=List.of();private int page,generation,ticks;private boolean busy,pending;
|
private List<Path> files=List.of();private int page,generation,ticks;private boolean busy,pending;
|
||||||
private VoxelSculpture source,previewSource;private ItemStack preview=ItemStack.EMPTY,lastClay=ItemStack.EMPTY;
|
private VoxelSculpture source,previewSource;private ItemStack preview=ItemStack.EMPTY,lastClay=ItemStack.EMPTY;
|
||||||
private Component status=text("choose_model");private Button rotate;
|
private Component status=text("choose_model");private Button rotate;
|
||||||
public ClayWorkshopScreen(ClayWorkshopMenu menu,Inventory inv,Component title){super(menu,inv,title,310,242);inventoryLabelX=138;inventoryLabelY=130;}
|
public ClayWorkshopScreen(ClayWorkshopMenu menu,Inventory inv,Component title){super(menu,inv,title,310,286);inventoryLabelX=75;inventoryLabelY=186;}
|
||||||
public static Component text(String key,Object...args){return Component.translatable("sanctuary.statuary."+key,args);}
|
public static Component text(String key,Object...args){return Component.translatable("sanctuary.statuary."+key,args);}
|
||||||
public static void register(){
|
public static void register(){
|
||||||
net.minecraft.client.gui.screens.MenuScreens.register(Statuary.MENU,ClayWorkshopScreen::new);StatuaryRenderer.register();
|
net.minecraft.client.gui.screens.MenuScreens.register(Statuary.MENU,ClayWorkshopScreen::new);StatuaryRenderer.register();
|
||||||
@@ -34,12 +36,13 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
|
|||||||
try{files=CreationLibrary.files(CreationLibrary.Kind.MODEL);}catch(Exception e){status=ModelImportErrors.message(e);}
|
try{files=CreationLibrary.files(CreationLibrary.Kind.MODEL);}catch(Exception e){status=ModelImportErrors.message(e);}
|
||||||
page=Math.min(page,Math.max(0,(files.size()-1)/4));
|
page=Math.min(page,Math.max(0,(files.size()-1)/4));
|
||||||
for(int i=0;i<4;i++){int at=page*4+i;if(at>=files.size())break;var file=files.get(at);
|
for(int i=0;i<4;i++){int at=page*4+i;if(at>=files.size())break;var file=files.get(at);
|
||||||
var label=Component.literal(file.getFileName().toString());var b=addRenderableWidget(Button.builder(label,p->load(file)).bounds(leftPos+10,topPos+29+i*18,128,17).build());b.setTooltip(Tooltip.create(label));}
|
var label=Component.literal(file.getFileName().toString());var b=addRenderableWidget(Button.builder(label,p->load(file)).bounds(leftPos+10,topPos+32+i*18,128,17).build());b.setTooltip(Tooltip.create(label));}
|
||||||
addRenderableWidget(Button.builder(Component.literal("←"),b->{page--;rebuildWidgets();}).bounds(leftPos+10,topPos+103,22,18).build()).active=page>0;
|
addRenderableWidget(Button.builder(Component.literal("←"),b->{page--;rebuildWidgets();}).bounds(leftPos+10,topPos+106,22,18).build()).active=page>0;
|
||||||
addRenderableWidget(Button.builder(text("folder"),b->folder()).bounds(leftPos+35,topPos+103,78,18).build());
|
addRenderableWidget(Button.builder(text("folder"),b->folder()).bounds(leftPos+35,topPos+106,78,18).build());
|
||||||
addRenderableWidget(Button.builder(Component.literal("→"),b->{page++;rebuildWidgets();}).bounds(leftPos+116,topPos+103,22,18).build()).active=(page+1)*4<files.size();
|
addRenderableWidget(Button.builder(Component.literal("→"),b->{page++;rebuildWidgets();}).bounds(leftPos+116,topPos+106,22,18).build()).active=(page+1)*4<files.size();
|
||||||
addRenderableWidget(Button.builder(text("refresh"),b->rebuildWidgets()).bounds(leftPos+10,topPos+123,128,18).build());
|
addRenderableWidget(Button.builder(text("refresh"),b->rebuildWidgets()).bounds(leftPos+10,topPos+127,128,18).build());
|
||||||
rotate=addRenderableWidget(Button.builder(text("rotate"),b->{if(source!=null){source=source.rotate();pending=true;}}).bounds(leftPos+215,topPos+112,92,18).build());rotate.active=source!=null&&!busy;
|
rotate=addRenderableWidget(Button.builder(text("rotate"),b->{if(source!=null){source=source.rotate();pending=true;}}).bounds(leftPos+148,topPos+127,152,18).build());rotate.active=source!=null&&!busy;
|
||||||
|
rotate.setTooltip(Tooltip.create(text("placement")));
|
||||||
}
|
}
|
||||||
private void folder(){try{Files.createDirectories(CreationLibrary.directory());Blaze3D.openPath(CreationLibrary.directory());}catch(Exception e){status=ModelImportErrors.message(e);}}
|
private void folder(){try{Files.createDirectories(CreationLibrary.directory());Blaze3D.openPath(CreationLibrary.directory());}catch(Exception e){status=ModelImportErrors.message(e);}}
|
||||||
public void load(Path file){
|
public void load(Path file){
|
||||||
@@ -62,19 +65,27 @@ public final class ClayWorkshopScreen extends AbstractContainerScreen<ClayWorksh
|
|||||||
}
|
}
|
||||||
@Override public void onFilesDrop(List<Path> files){if(!files.isEmpty())load(files.getFirst());}
|
@Override public void onFilesDrop(List<Path> files){if(!files.isEmpty())load(files.getFirst());}
|
||||||
@Override public void removed(){generation++;super.removed();}
|
@Override public void removed(){generation++;super.removed();}
|
||||||
|
int panelHeight(){
|
||||||
|
// The inventory mixin moves or hides rows according to the player's capacity.
|
||||||
|
return Math.max(177,menu.slots.stream().filter(s->s.x>=0&&s.y>=0).mapToInt(s->s.y+30).max().orElse(177));
|
||||||
|
}
|
||||||
@Override public void extractBackground(GuiGraphicsExtractor g,int mx,int my,float partial){
|
@Override public void extractBackground(GuiGraphicsExtractor g,int mx,int my,float partial){
|
||||||
g.fill(leftPos,topPos,leftPos+imageWidth,topPos+142,0xFFE2DDD3);g.fill(leftPos+2,topPos+2,leftPos+imageWidth-2,topPos+140,0xFFC6C6C6);
|
g.blitSprite(RenderPipelines.GUI_TEXTURED,FRAME,leftPos,topPos,imageWidth,panelHeight());
|
||||||
g.fill(leftPos+147,topPos+24,leftPos+imageWidth-10,topPos+87,0xFF92958E);
|
g.fill(leftPos+148,topPos+23,leftPos+300,topPos+92,0xFF8B8B8B);
|
||||||
for(var slot:menu.slots)if(slot.x> -1000)g.blitSprite(RenderPipelines.GUI_TEXTURED,Identifier.withDefaultNamespace("container/slot"),leftPos+slot.x-1,topPos+slot.y-1,18,18);
|
g.fill(leftPos+149,topPos+24,leftPos+299,topPos+91,0xFF373737);
|
||||||
|
for(var slot:menu.slots)if(slot.x>=0&&slot.y>=0)g.blitSprite(RenderPipelines.GUI_TEXTURED,SLOT,leftPos+slot.x-1,topPos+slot.y-1,18,18);
|
||||||
if(!preview.isEmpty()){
|
if(!preview.isEmpty()){
|
||||||
g.pose().pushMatrix();g.pose().translate(leftPos+195,topPos+28);g.pose().scale(3.5f,3.5f);g.item(preview,0,0);g.pose().popMatrix();
|
g.pose().pushMatrix();g.pose().translate(leftPos+194,topPos+27);g.pose().scale(3.75f,3.75f);g.item(preview,0,0);g.pose().popMatrix();
|
||||||
}
|
}
|
||||||
g.text(font,Component.literal("→"),leftPos+254,topPos+97,0xFF404040,false);
|
g.text(font,Component.literal("→"),leftPos+254,topPos+111,0xFF404040,false);
|
||||||
var detail=font.split(status,128);for(int i=0;i<Math.min(4,detail.size());i++)g.text(font,detail.get(i),leftPos+10,topPos+150+i*font.lineHeight,0xFFE7E7E7);
|
var detail=font.split(status,290);for(int i=0;i<Math.min(2,detail.size());i++)g.text(font,detail.get(i),leftPos+10,topPos+150+i*font.lineHeight,0xFF404040,false);
|
||||||
|
if(detail.size()>2&&mx>=leftPos+10&&mx<leftPos+300&&my>=topPos+148&&my<topPos+174)g.setTooltipForNextFrame(font,status,mx,my);
|
||||||
if(files.isEmpty())g.text(font,text("empty"),leftPos+10,topPos+32,0xFF404040,false);
|
if(files.isEmpty())g.text(font,text("empty"),leftPos+10,topPos+32,0xFF404040,false);
|
||||||
}
|
}
|
||||||
@Override protected void extractLabels(GuiGraphicsExtractor g,int mx,int my){
|
@Override protected void extractLabels(GuiGraphicsExtractor g,int mx,int my){
|
||||||
g.text(font,title,10,8,0xFF303030,false);g.text(font,text("input"),203,80,0xFF303030,false);g.text(font,text("output"),imageWidth-10-font.width(text("output")),80,0xFF303030,false);
|
g.text(font,title,10,8,0xFF303030,false);g.text(font,text("models"),10,22,0xFF404040,false);
|
||||||
|
g.text(font,text("input"),230-font.width(text("input"))/2,95,0xFF303030,false);g.text(font,text("output"),imageWidth-10-font.width(text("output")),95,0xFF303030,false);
|
||||||
|
if(inventoryLabelY>=0)g.text(font,playerInventoryTitle,inventoryLabelX,inventoryLabelY,0xFF303030,false);
|
||||||
}
|
}
|
||||||
public boolean busy(){return busy;}
|
public boolean busy(){return busy;}
|
||||||
public VoxelSculpture selected(){return source;}
|
public VoxelSculpture selected(){return source;}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import fr.koka.sanctuary.SanctuaryMod;
|
|||||||
import fr.koka.sanctuary.plans.*;
|
import fr.koka.sanctuary.plans.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import com.mojang.blaze3d.vertex.PoseStack;
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
import com.mojang.math.Axis;
|
||||||
import com.mojang.serialization.MapCodec;
|
import com.mojang.serialization.MapCodec;
|
||||||
import net.minecraft.client.renderer.*;
|
import net.minecraft.client.renderer.*;
|
||||||
import net.minecraft.client.renderer.blockentity.*;
|
import net.minecraft.client.renderer.blockentity.*;
|
||||||
@@ -34,12 +35,16 @@ public final class StatuaryRenderer implements BlockEntityRenderer<StatuaryEntit
|
|||||||
{0,0,1, 0,0,0, 1,0,0, 1,0,1, 0,-1,0}
|
{0,0,1, 0,0,0, 1,0,0, 1,0,1, 0,-1,0}
|
||||||
};
|
};
|
||||||
private static final int[][] STEP={{0,0,-1},{0,0,1},{-1,0,0},{1,0,0},{0,1,0},{0,-1,0}};
|
private static final int[][] STEP={{0,0,-1},{0,0,1},{-1,0,0},{1,0,0},{0,1,0},{0,-1,0}};
|
||||||
public static final class State extends BlockEntityRenderState {List<Quad> mesh=List.of();}
|
public static final class State extends BlockEntityRenderState {List<Quad> mesh=List.of();float yaw;}
|
||||||
public State createRenderState(){return new State();}
|
public State createRenderState(){return new State();}
|
||||||
@Override public void extractRenderState(StatuaryEntity entity,State state,float delta,Vec3 camera,ModelFeatureRenderer.CrumblingOverlay breakProgress){
|
@Override public void extractRenderState(StatuaryEntity entity,State state,float delta,Vec3 camera,ModelFeatureRenderer.CrumblingOverlay breakProgress){
|
||||||
BlockEntityRenderer.super.extractRenderState(entity,state,delta,camera,breakProgress);state.mesh=mesh(entity.sculpture());
|
BlockEntityRenderer.super.extractRenderState(entity,state,delta,camera,breakProgress);state.mesh=mesh(entity.sculpture());
|
||||||
|
state.yaw=-entity.getBlockState().getValue(Statuary.FACING).toYRot();
|
||||||
|
}
|
||||||
|
public void submit(State state,PoseStack pose,SubmitNodeCollector collector,CameraRenderState camera){
|
||||||
|
pose.pushPose();pose.translate(.5,0,.5);pose.rotateDegrees(Axis.YP,state.yaw);pose.translate(-.5,0,-.5);
|
||||||
|
draw(state.mesh,pose,collector,state.lightCoords,OverlayTexture.NO_OVERLAY);pose.popPose();
|
||||||
}
|
}
|
||||||
public void submit(State state,PoseStack pose,SubmitNodeCollector collector,CameraRenderState camera){draw(state.mesh,pose,collector,state.lightCoords,OverlayTexture.NO_OVERLAY);}
|
|
||||||
private static List<Quad> mesh(VoxelSculpture sculpture){
|
private static List<Quad> mesh(VoxelSculpture sculpture){
|
||||||
if(sculpture==null)return List.of();var cached=MESHES.get(sculpture);if(cached!=null)return cached;
|
if(sculpture==null)return List.of();var cached=MESHES.get(sculpture);if(cached!=null)return cached;
|
||||||
var occupied=new BitSet(4096);for(var cell:sculpture.cells())occupied.set(cell.key());var quads=new ArrayList<Quad>();
|
var occupied=new BitSet(4096);for(var cell:sculpture.cells())occupied.set(cell.key());var quads=new ArrayList<Quad>();
|
||||||
|
|||||||
+3
-2
@@ -190,7 +190,8 @@ abstract class AbstractContainerScreenMixin implements SanctuaryContainerScreenG
|
|||||||
if (!sanctuary$integratedInventory) return;
|
if (!sanctuary$integratedInventory) return;
|
||||||
Minecraft minecraft = Minecraft.getInstance();
|
Minecraft minecraft = Minecraft.getInstance();
|
||||||
int panelHeight = PANEL_BASE_HEIGHT + sanctuary$visibleRows * BANK_ROW_STEP;
|
int panelHeight = PANEL_BASE_HEIGHT + sanctuary$visibleRows * BANK_ROW_STEP;
|
||||||
graphics.blit(RenderPipelines.GUI_TEXTURED,
|
// The workshop draws one nine-slice frame around its controls and all visible rows.
|
||||||
|
if (!((Object)this instanceof fr.koka.sanctuary.client.ClayWorkshopScreen)) graphics.blit(RenderPipelines.GUI_TEXTURED,
|
||||||
SanctuaryMod.id("textures/gui/container/" + sanctuary$textureProfile
|
SanctuaryMod.id("textures/gui/container/" + sanctuary$textureProfile
|
||||||
+ "/inventory_" + sanctuary$visibleRows + ".png"),
|
+ "/inventory_" + sanctuary$visibleRows + ".png"),
|
||||||
sanctuary$panelLeft, sanctuary$panelTop,
|
sanctuary$panelLeft, sanctuary$panelTop,
|
||||||
@@ -331,7 +332,7 @@ abstract class AbstractContainerScreenMixin implements SanctuaryContainerScreenG
|
|||||||
// These screens paint previews or controls below their lowest machine
|
// These screens paint previews or controls below their lowest machine
|
||||||
// slot. Their explicit anchors keep the common panel from covering them.
|
// slot. Their explicit anchors keep the common panel from covering them.
|
||||||
if (screen instanceof StonecutterScreen) return 70;
|
if (screen instanceof StonecutterScreen) return 70;
|
||||||
if (screen instanceof fr.koka.sanctuary.client.ClayWorkshopScreen) return 142;
|
if (screen instanceof fr.koka.sanctuary.client.ClayWorkshopScreen) return 176;
|
||||||
if (screen instanceof CartographyTableScreen) return 81;
|
if (screen instanceof CartographyTableScreen) return 81;
|
||||||
if (screen instanceof EnchantmentScreen) return 73;
|
if (screen instanceof EnchantmentScreen) return 73;
|
||||||
if (screen instanceof SmithingScreen) return 82;
|
if (screen instanceof SmithingScreen) return 82;
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ public final class ClayWorkshopMenu extends AbstractContainerMenu {
|
|||||||
public ClayWorkshopMenu(int id,Inventory inv,View view){this(id,inv,view,null);}
|
public ClayWorkshopMenu(int id,Inventory inv,View view){this(id,inv,view,null);}
|
||||||
public ClayWorkshopMenu(int id,Inventory inv,View view,Level level){
|
public ClayWorkshopMenu(int id,Inventory inv,View view,Level level){
|
||||||
super(Statuary.MENU,id);this.view=view;this.level=level;
|
super(Statuary.MENU,id);this.view=view;this.level=level;
|
||||||
addSlot(new Slot(input,0,222,92){@Override public boolean mayPlace(ItemStack stack){return clay(stack);}});
|
addSlot(new Slot(input,0,222,106){@Override public boolean mayPlace(ItemStack stack){return clay(stack);}});
|
||||||
addSlot(new Slot(result,0,282,92){
|
addSlot(new Slot(result,0,282,106){
|
||||||
@Override public boolean mayPlace(ItemStack stack){return false;}
|
@Override public boolean mayPlace(ItemStack stack){return false;}
|
||||||
@Override public boolean mayPickup(Player p){return mayCraft(p)&&clay(input.getItem(0))&&!getItem().isEmpty();}
|
@Override public boolean mayPickup(Player p){return mayCraft(p)&&clay(input.getItem(0))&&!getItem().isEmpty();}
|
||||||
@Override public void onTake(Player p,ItemStack stack){input.removeItem(0,1);input.setChanged();super.onTake(p,stack);}
|
@Override public void onTake(Player p,ItemStack stack){input.removeItem(0,1);input.setChanged();super.onTake(p,stack);}
|
||||||
});
|
});
|
||||||
addStandardInventorySlots(inv,138,142);
|
addStandardInventorySlots(inv,75,198);
|
||||||
}
|
}
|
||||||
// Colors match the published clay palette (tools/colored-bricks-palette.json), in native dye order.
|
// Colors match the published clay palette (tools/colored-bricks-palette.json), in native dye order.
|
||||||
private static final int[] CLAY_COLORS={0xC6C4C8,0xD3AA8D,0xD39FD0,0x91B0CA,0xC8BF98,0xA2C69A,0xDAC4DD,0x74767D,0xA9A8AC,0xA8C5C1,0xBE91E3,0xA0A7BC,0xA49A91,0xA0B499,0xB49A99,0x444541};
|
private static final int[] CLAY_COLORS={0xC6C4C8,0xD3AA8D,0xD39FD0,0x91B0CA,0xC8BF98,0xA2C69A,0xDAC4DD,0x74767D,0xA9A8AC,0xA8C5C1,0xBE91E3,0xA0A7BC,0xA49A91,0xA0B499,0xB49A99,0x444541};
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ import net.minecraft.world.*;
|
|||||||
import net.minecraft.world.entity.player.*;
|
import net.minecraft.world.entity.player.*;
|
||||||
import net.minecraft.world.inventory.*;
|
import net.minecraft.world.inventory.*;
|
||||||
import net.minecraft.world.item.*;
|
import net.minecraft.world.item.*;
|
||||||
|
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||||
import net.minecraft.world.level.*;
|
import net.minecraft.world.level.*;
|
||||||
import net.minecraft.world.level.block.*;
|
import net.minecraft.world.level.block.*;
|
||||||
import net.minecraft.world.level.block.entity.*;
|
import net.minecraft.world.level.block.entity.*;
|
||||||
import net.minecraft.world.level.block.state.*;
|
import net.minecraft.world.level.block.state.*;
|
||||||
|
import net.minecraft.world.level.block.state.properties.*;
|
||||||
import net.minecraft.world.level.material.PushReaction;
|
import net.minecraft.world.level.material.PushReaction;
|
||||||
import net.minecraft.world.level.storage.loot.LootParams;
|
import net.minecraft.world.level.storage.loot.LootParams;
|
||||||
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
import net.minecraft.world.level.storage.loot.parameters.LootContextParams;
|
||||||
@@ -25,6 +27,7 @@ import net.minecraft.network.chat.Component;
|
|||||||
|
|
||||||
/** The clay workshop crafts portable sculptures; a placed sculpture is only a decoration. */
|
/** The clay workshop crafts portable sculptures; a placed sculpture is only a decoration. */
|
||||||
public final class Statuary {
|
public final class Statuary {
|
||||||
|
public static final EnumProperty<Direction> FACING=BlockStateProperties.HORIZONTAL_FACING;
|
||||||
public static final Block BLOCK=block("statuary",new SculptureBlock());
|
public static final Block BLOCK=block("statuary",new SculptureBlock());
|
||||||
public static final Item ITEM=blockItem("statuary",BLOCK);
|
public static final Item ITEM=blockItem("statuary",BLOCK);
|
||||||
public static final Block WORKSHOP=block("clay_workshop",new WorkshopBlock());
|
public static final Block WORKSHOP=block("clay_workshop",new WorkshopBlock());
|
||||||
@@ -56,7 +59,11 @@ public final class Statuary {
|
|||||||
private static Item blockItem(String name,Block block){return Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id(name),new BlockItem(block,new Item.Properties().setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id(name))).useBlockDescriptionPrefix()));}
|
private static Item blockItem(String name,Block block){return Registry.register(BuiltInRegistries.ITEM,SanctuaryMod.id(name),new BlockItem(block,new Item.Properties().setId(ResourceKey.create(Registries.ITEM,SanctuaryMod.id(name))).useBlockDescriptionPrefix()));}
|
||||||
private static BlockBehaviour.Properties properties(String id){return BlockBehaviour.Properties.of().strength(1.5f).noOcclusion().noLootTable().pushReaction(PushReaction.IMMOVEABLE).setId(ResourceKey.create(Registries.BLOCK,SanctuaryMod.id(id)));}
|
private static BlockBehaviour.Properties properties(String id){return BlockBehaviour.Properties.of().strength(1.5f).noOcclusion().noLootTable().pushReaction(PushReaction.IMMOVEABLE).setId(ResourceKey.create(Registries.BLOCK,SanctuaryMod.id(id)));}
|
||||||
private static final class SculptureBlock extends Block implements EntityBlock {
|
private static final class SculptureBlock extends Block implements EntityBlock {
|
||||||
SculptureBlock(){super(Statuary.properties("statuary"));}
|
SculptureBlock(){super(Statuary.properties("statuary"));registerDefaultState(stateDefinition.any().setValue(FACING,Direction.SOUTH));}
|
||||||
|
@Override protected void createBlockStateDefinition(StateDefinition.Builder<Block,BlockState> builder){builder.add(FACING);}
|
||||||
|
@Override public BlockState getStateForPlacement(BlockPlaceContext context){return defaultBlockState().setValue(FACING,context.getHorizontalDirection());}
|
||||||
|
@Override protected BlockState rotate(BlockState state,Rotation rotation){return state.setValue(FACING,rotation.rotate(state.getValue(FACING)));}
|
||||||
|
@Override protected BlockState mirror(BlockState state,Mirror mirror){return state.rotate(mirror.getRotation(state.getValue(FACING)));}
|
||||||
public BlockEntity newBlockEntity(BlockPos pos,BlockState state){return new StatuaryEntity(pos,state);}
|
public BlockEntity newBlockEntity(BlockPos pos,BlockState state){return new StatuaryEntity(pos,state);}
|
||||||
@Override protected List<ItemStack> getDrops(BlockState s,LootParams.Builder context){return List.of(context.getOptionalParameter(LootContextParams.BLOCK_ENTITY) instanceof StatuaryEntity entity?entity.item():new ItemStack(ITEM));}
|
@Override protected List<ItemStack> getDrops(BlockState s,LootParams.Builder context){return List.of(context.getOptionalParameter(LootContextParams.BLOCK_ENTITY) instanceof StatuaryEntity entity?entity.item():new ItemStack(ITEM));}
|
||||||
@Override protected ItemStack getCloneItemStack(LevelReader l,BlockPos pos,BlockState s,boolean data){return l.getBlockEntity(pos) instanceof StatuaryEntity entity?entity.item():new ItemStack(ITEM);}
|
@Override protected ItemStack getCloneItemStack(LevelReader l,BlockPos pos,BlockState s,boolean data){return l.getBlockEntity(pos) instanceof StatuaryEntity entity?entity.item():new ItemStack(ITEM);}
|
||||||
|
|||||||
@@ -2160,6 +2160,8 @@
|
|||||||
"sanctuary.statuary.choose_model": "Choose a GLB model. 1 clay block = 1 statue. Plain clay keeps model colors; colored clay gives a solid color.",
|
"sanctuary.statuary.choose_model": "Choose a GLB model. 1 clay block = 1 statue. Plain clay keeps model colors; colored clay gives a solid color.",
|
||||||
"sanctuary.statuary.folder": "Shared folder",
|
"sanctuary.statuary.folder": "Shared folder",
|
||||||
"sanctuary.statuary.refresh": "Refresh",
|
"sanctuary.statuary.refresh": "Refresh",
|
||||||
|
"sanctuary.statuary.models": "GLB models",
|
||||||
|
"sanctuary.statuary.placement": "The statue is placed facing the direction you look. Rotate adjusts the model before crafting.",
|
||||||
"sanctuary.statuary.rotate": "Rotate",
|
"sanctuary.statuary.rotate": "Rotate",
|
||||||
"sanctuary.statuary.working": "Voxelizing to 16 × 16 × 16…",
|
"sanctuary.statuary.working": "Voxelizing to 16 × 16 × 16…",
|
||||||
"sanctuary.statuary.ready": "1 clay = 1 statue. Plain clay keeps model colors. Colored clay gives a solid color.",
|
"sanctuary.statuary.ready": "1 clay = 1 statue. Plain clay keeps model colors. Colored clay gives a solid color.",
|
||||||
|
|||||||
@@ -2160,6 +2160,8 @@
|
|||||||
"sanctuary.statuary.choose_model": "Choisissez un modèle GLB. 1 bloc d’argile = 1 statue. Argile normale : couleurs du modèle ; argile colorée : teinte unie.",
|
"sanctuary.statuary.choose_model": "Choisissez un modèle GLB. 1 bloc d’argile = 1 statue. Argile normale : couleurs du modèle ; argile colorée : teinte unie.",
|
||||||
"sanctuary.statuary.folder": "Dossier commun",
|
"sanctuary.statuary.folder": "Dossier commun",
|
||||||
"sanctuary.statuary.refresh": "Actualiser",
|
"sanctuary.statuary.refresh": "Actualiser",
|
||||||
|
"sanctuary.statuary.models": "Modèles GLB",
|
||||||
|
"sanctuary.statuary.placement": "La statue se pose dans le sens de votre regard. Tourner ajuste le modèle avant fabrication.",
|
||||||
"sanctuary.statuary.rotate": "Tourner",
|
"sanctuary.statuary.rotate": "Tourner",
|
||||||
"sanctuary.statuary.working": "Voxelisation en 16 × 16 × 16…",
|
"sanctuary.statuary.working": "Voxelisation en 16 × 16 × 16…",
|
||||||
"sanctuary.statuary.ready": "1 argile = 1 statue. Normale : couleurs du modèle. Colorée : teinte unie.",
|
"sanctuary.statuary.ready": "1 argile = 1 statue. Normale : couleurs du modèle. Colorée : teinte unie.",
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 106 B |
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"gui": {
|
||||||
|
"scaling": {
|
||||||
|
"type": "nine_slice",
|
||||||
|
"width": 26,
|
||||||
|
"height": 26,
|
||||||
|
"border": 4,
|
||||||
|
"stretch_inner": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
name = "Sanctuary"
|
name = "Sanctuary"
|
||||||
author = "KOKA99CAB"
|
author = "KOKA99CAB"
|
||||||
version = "beta.110"
|
version = "beta.111"
|
||||||
pack-format = "packwiz:1.1.0"
|
pack-format = "packwiz:1.1.0"
|
||||||
|
|
||||||
[index]
|
[index]
|
||||||
|
|||||||
Reference in New Issue
Block a user