feat(itsalive): transformer le Codex en Livre interdit (#65) #70
+4
-2
@@ -32,9 +32,11 @@ Ambiance possède les trois payloads historiques canaplia:hemp_visual_sync, cana
|
||||
|
||||
## Codex
|
||||
|
||||
Les monstres tués par un joueur peuvent laisser une `canaplia:codex_page` et, très rarement, le `canaplia:codex` complet. Quatre pages récupérées permettent aussi de relier le Codex autour d'un livre. L'objet utilise la texture du livre Minecraft et son écran repose sur les pages vanilla. Il distingue les découvertes personnelles des silhouettes connues par la communauté ; les origines génériques et les conditions détaillées se révèlent progressivement sans envoyer de payload de découverte client-vers-serveur.
|
||||
Les monstres tués par un joueur peuvent laisser une `canaplia:codex_page` et, très rarement, le `canaplia:codex` complet. Quatre pages récupérées permettent aussi d'assembler le Livre interdit autour d'un livre vanilla. Un clic droit sur une page découvre au hasard une entrée encore inconnue et ne consomme la page qu'en cas de nouvelle découverte.
|
||||
|
||||
Les données canaplia:codex sont versionnées. Les clés futures syntaxiquement valides restent conservées lors d'un aller-retour afin qu'une migration ne perde pas les découvertes ajoutées par une version ultérieure.
|
||||
L'ID historique `canaplia:codex` est conservé, mais l'objet s'affiche désormais comme le « Livre interdit » : il utilise la couleur du livre enchanté, ouvre directement les seize entrées et les présente sur des pages noires. En main, son contenu est strictement personnel. Posé sur un pupitre, il est temporairement inscrit au nom du joueur qui le pose et partage ses découvertes avec tous les lecteurs ; le récupérer en s'accroupissant ou casser le pupitre efface cette inscription.
|
||||
|
||||
Les données `canaplia:codex` restent en version 1. Les clés futures syntaxiquement valides et l'ancien champ partagé restent conservés lors d'un aller-retour afin qu'une migration ne perde aucune découverte, mais les nouvelles découvertes ne sont plus publiées globalement. Aucun payload de découverte n'est accepté du client vers le serveur et le protocole It's Alive reste en version 4.
|
||||
|
||||
## Tickets livrés
|
||||
|
||||
|
||||
+4
-1
@@ -8,10 +8,13 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
|
||||
|
||||
## Non distribué
|
||||
|
||||
Module modifié : **I Like To Move It `0.0.0-alpha.15`**. Le pack distribué reste en `26.2.0-alpha.209` jusqu’à la prochaine release.
|
||||
Modules modifiés : **I Like To Move It `0.0.0-alpha.15`** et **It's Alive `0.0.0-alpha.35`**. Le pack distribué reste en `26.2.0-alpha.209` jusqu’à la prochaine release.
|
||||
|
||||
- double la vitesse de croisière du Happy Ghast lorsqu’un joueur le pilote, sans accélérer ses déplacements autonomes ;
|
||||
- conserve les données v3 et le protocole réseau 5, sans migration de sauvegarde ni nouvel ID.
|
||||
- transforme le Codex Canaplia en « Livre interdit » aux pages noires, sans page d'accueil, et fait découvrir une entrée aléatoire encore inconnue à chaque page consommée ;
|
||||
- garde les découvertes du Livre interdit personnelles en main et partage temporairement celles du propriétaire uniquement lorsque le livre est posé sur un pupitre ;
|
||||
- conserve les IDs et les anciennes données partagées, ainsi que les données v3 et le protocole réseau 4 d'It's Alive, sans migration de sauvegarde.
|
||||
|
||||
## `26.2.0-alpha.209`
|
||||
|
||||
|
||||
+12
-10
@@ -4172,7 +4172,7 @@ tasks.register("verifyCuisineCropsRelease") {
|
||||
def rice = file("itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/RiceCropBlock.java").text
|
||||
def bean = file("itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/BeanCropBlock.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.35"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.193"
|
||||
|| migration.source?.modules != [itsalive: "0.0.0-alpha.26"]
|
||||
@@ -4313,7 +4313,7 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") {
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.35"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.196"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.197"
|
||||
@@ -4363,7 +4363,7 @@ tasks.register("verifyFermentationBarrelAutomationRelease") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def itsAliveManifest = new JsonSlurper().parse(file("itsalive/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.35"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.197"
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.198"
|
||||
|| migration.target?.modules != [itsalive: "0.0.0-alpha.30", sanctuary: "0.0.0-alpha.114"]
|
||||
@@ -4413,7 +4413,7 @@ tasks.register("verifyQuestRequestContractsRelease") {
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.35"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.198"
|
||||
|| migration.source?.modules?.sanctuary != "0.0.0-alpha.114"
|
||||
@@ -4589,7 +4589,7 @@ tasks.register("verifyWaystoneShopCulinaryAlpha202Release") {
|
||||
"sanctuary/src/main/resources/fabric.mod.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.35"
|
||||
|| rootProject.iliketomoveit_version != "0.0.0-alpha.15"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.117"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.201"
|
||||
@@ -5238,7 +5238,7 @@ tasks.register("verifyCanapliaFoundation") {
|
||||
def visualApiSource = file(
|
||||
"ambiance/src/main/java/fr/koka99cab/sanctuary26/ambiance/api/visual/AmbianceVisualEffectApi.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.35"
|
||||
|| rootProject.ambiance_version != "0.0.0-alpha.14"
|
||||
|| migration.issue != 35
|
||||
|| migration.source?.fabric_id != "canaplia"
|
||||
@@ -5349,8 +5349,10 @@ tasks.register("verifyCanapliaGameplay") {
|
||||
def resinState = new JsonSlurper().parse(new File(generated, "assets/canaplia/blockstates/resin_block.json"))
|
||||
def codexRecipe = new JsonSlurper().parse(new File(generated, "data/canaplia/recipe/codex.json"))
|
||||
def requiredCodexKeys = [
|
||||
"screen.canaplia.codex.introduction", "screen.canaplia.codex.entry_number",
|
||||
"screen.canaplia.codex.origin", "item.canaplia.codex_page"
|
||||
"screen.canaplia.codex.title", "screen.canaplia.codex.page",
|
||||
"screen.canaplia.codex.entry_number", "screen.canaplia.codex.origin",
|
||||
"screen.canaplia.codex.hidden", "message.canaplia.codex_page.complete",
|
||||
"tooltip.canaplia.codex.personal", "item.canaplia.codex", "item.canaplia.codex_page"
|
||||
]
|
||||
def languageKeysMissing = ["fr_fr", "en_us", "ru_ru"].collectMany { locale ->
|
||||
def lang = new JsonSlurper().parse(file(
|
||||
@@ -5446,7 +5448,7 @@ tasks.register("verifyCanapliaGameplay") {
|
||||
|| !codexScreen.contains("extends BookViewScreen")
|
||||
|| !harvesting.contains("NEIGHBOR_RADIUS = 1")
|
||||
|| !effects.contains("ServerPlayConnectionEvents.DISCONNECT")
|
||||
|| !knowledge.contains("unknown future keys survive round-trips")
|
||||
|| !knowledge.contains("the legacy shared field is preserved")
|
||||
|| !policy.contains('namespace.equals("canaplia")')) {
|
||||
throw new GradleException(
|
||||
"The complete Canaplia gameplay contract is incomplete; missing languages="
|
||||
@@ -5486,7 +5488,7 @@ tasks.register("verifyCanapliaAlpha209Release") {
|
||||
onlyfun: "0.0.0-alpha.20"]
|
||||
if (rootProject.pack_version != "26.2.0-alpha.209"
|
||||
|| rootProject.ambiance_version != expectedTarget.ambiance
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.34"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.35"
|
||||
|| rootProject.onlyfun_version != expectedTarget.onlyfun
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.207"
|
||||
|| migration.source?.modules != expectedSource
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ redstoner_version=0.0.0-alpha.10
|
||||
redstoner_lifecycle=active
|
||||
anotherworld_version=0.0.0-alpha.55
|
||||
anotherworld_lifecycle=active
|
||||
itsalive_version=0.0.0-alpha.34
|
||||
itsalive_version=0.0.0-alpha.35
|
||||
itsalive_lifecycle=active
|
||||
ouch_version=0.0.0-alpha.9
|
||||
ouch_lifecycle=active
|
||||
|
||||
+72
-6
@@ -153,7 +153,7 @@ tasks.register("generateCanapliaResources") {
|
||||
writeJson("data/c/tags/item/seeds.json", [replace: false, values: seedIds])
|
||||
writeJson("data/canaplia/tags/item/hemp_buds.json", [replace: false, values: budIds])
|
||||
|
||||
itemDefinition("codex", "minecraft:item/book")
|
||||
itemDefinition("codex", "minecraft:item/enchanted_book")
|
||||
itemDefinition("codex_page", "minecraft:item/paper")
|
||||
writeJson("data/canaplia/recipe/codex.json", [
|
||||
type: "minecraft:crafting_shaped", category: "misc",
|
||||
@@ -556,7 +556,7 @@ tasks.register("verifyItsAlive") {
|
||||
def climateSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ClimateCropForaging.java").text
|
||||
def pineappleSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/PineappleItem.java").text
|
||||
def worldgenSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ItsAliveAgricultureWorldGeneration.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.34"
|
||||
if (project.version.toString() != "0.0.0-alpha.35"
|
||||
|| climateMigration.target?.pack_version != "26.2.0-alpha.157"
|
||||
|| climateMigration.target?.modules?.itsalive != "0.0.0-alpha.24"
|
||||
|| !climateSource.contains("PlayerBlockBreakEvents.AFTER.register")
|
||||
@@ -1313,7 +1313,7 @@ tasks.register("verifyCulinaryReliability") {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.34"
|
||||
if (project.version.toString() != "0.0.0-alpha.35"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.28"
|
||||
|| migration.culinary_fixes?.food_press_contents_destroyed_on_break != true
|
||||
|| migration.culinary_fixes?.comparator_contract?.blocks?.size() != 6
|
||||
@@ -1351,7 +1351,7 @@ tasks.register("verifyCauldronBatching") {
|
||||
def recipe = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java").text
|
||||
def cauldron = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java").text
|
||||
def timed = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractTimedCulinaryBlockEntity.java").text
|
||||
if (project.version.toString() != "0.0.0-alpha.34"
|
||||
if (project.version.toString() != "0.0.0-alpha.35"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.29"
|
||||
|| migration.cauldron_batches?.logical_stack_limit != 64
|
||||
|| migration.cauldron_batches?.identity != "same_item_same_components"
|
||||
@@ -1389,7 +1389,7 @@ tasks.register("verifyFermentationBarrelAutomation") {
|
||||
def partialMatching = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatching.java").text
|
||||
def blockstate = new JsonSlurper().parse(
|
||||
file("src/main/resources/assets/itsalive/blockstates/fermentation_barrel.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.34"
|
||||
if (project.version.toString() != "0.0.0-alpha.35"
|
||||
|| migration.source?.modules?.itsalive != "0.0.0-alpha.29"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.30"
|
||||
|| migration.fermentation_barrel?.maximum_units_per_input_slot != 1
|
||||
@@ -1437,7 +1437,7 @@ tasks.register("verifyCulinaryLecternRelease") {
|
||||
[(locale): new JsonSlurper().parse(file(
|
||||
"src/main/resources/assets/itsalive/lang/${locale}.json"))]
|
||||
}
|
||||
if (project.version.toString() != "0.0.0-alpha.34"
|
||||
if (project.version.toString() != "0.0.0-alpha.35"
|
||||
|| migration.target?.modules?.itsalive != "0.0.0-alpha.31"
|
||||
|| migration.culinary_lectern?.book_tag != "minecraft:lectern_books"
|
||||
|| migration.culinary_lectern?.owner_catalog_shared_while_placed != true
|
||||
@@ -1461,9 +1461,75 @@ tasks.register("verifyCulinaryLecternRelease") {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("verifyCanapliaForbiddenBook") {
|
||||
group = "verification"
|
||||
description = "Checks personal Forbidden Books, random page learning and lectern sharing."
|
||||
dependsOn tasks.named("generateCanapliaResources")
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaKnowledge.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaKnowledgeData.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaLectern.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/item/CanapliaCodexPageItem.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/client/CanapliaCodexScreen.java"))
|
||||
inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/registry/ItsAliveDataComponents.java"))
|
||||
inputs.file(file("src/main/resources/data/minecraft/tags/item/lectern_books.json"))
|
||||
inputs.files(fileTree("src/main/resources/assets/canaplia/lang") { include "*.json" })
|
||||
inputs.file(rootProject.file("CANAPLIA.md"))
|
||||
|
||||
doLast {
|
||||
def knowledge = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaKnowledge.java").text
|
||||
def knowledgeData = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaKnowledgeData.java").text
|
||||
def lectern = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/CanapliaLectern.java").text
|
||||
def pageItem = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/item/CanapliaCodexPageItem.java").text
|
||||
def screen = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/canaplia/client/CanapliaCodexScreen.java").text
|
||||
def components = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/itsalive/registry/ItsAliveDataComponents.java").text
|
||||
def lecternBooks = new JsonSlurper().parse(file(
|
||||
"src/main/resources/data/minecraft/tags/item/lectern_books.json"))
|
||||
def generatedCodex = new JsonSlurper().parse(new File(
|
||||
generatedCanapliaResources.get().asFile, "assets/canaplia/items/codex.json"))
|
||||
def locales = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
|
||||
[(locale): new JsonSlurper().parse(file(
|
||||
"src/main/resources/assets/canaplia/lang/${locale}.json"))]
|
||||
}
|
||||
def expectedNames = [
|
||||
fr_fr: "Livre interdit",
|
||||
en_us: "Forbidden Book",
|
||||
ru_ru: "Запретная книга"]
|
||||
if (project.version.toString() != "0.0.0-alpha.35"
|
||||
|| !pageItem.contains("CanapliaKnowledge.learnRandomUnknown(serverPlayer)")
|
||||
|| !knowledge.contains("unknownVariants(data.personal(player.getUUID()))")
|
||||
|| !knowledge.contains("data.personal(inscription.ownerId())")
|
||||
|| knowledge.contains("data.shared()")
|
||||
|| !knowledgeData.contains("the legacy shared field is preserved")
|
||||
|| !lectern.contains("UseBlockCallback.EVENT.register")
|
||||
|| !lectern.contains("PlayerBlockBreakEvents.BEFORE.register")
|
||||
|| !lectern.contains("CanapliaKnowledge.open(serverPlayer, book)")
|
||||
|| !lectern.contains("CanapliaKnowledge.forgetInscription(taken)")
|
||||
|| !screen.contains("PAGE_BACKGROUND = 0xFF09070E")
|
||||
|| screen.contains("screen.canaplia.codex.introduction")
|
||||
|| !components.contains('ItsAliveMod.id("canaplia_codex")')
|
||||
|| lecternBooks["values"] != ["itsalive:culinary_recipe_book", "canaplia:codex"]
|
||||
|| generatedCodex.model?.model != "minecraft:item/enchanted_book"
|
||||
|| locales.any { locale, values ->
|
||||
values["item.canaplia.codex"] != expectedNames[locale]
|
||||
|| values["screen.canaplia.codex.title"] != expectedNames[locale]
|
||||
|| !values.containsKey("message.canaplia.codex_page.complete")
|
||||
|| !values.containsKey("tooltip.canaplia.codex.personal")
|
||||
}) {
|
||||
throw new GradleException("The Canaplia Forbidden Book contract is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("verifyItsAlive")
|
||||
dependsOn tasks.named("canapliaFoundationSmoke")
|
||||
dependsOn tasks.named("verifyCanapliaForbiddenBook")
|
||||
dependsOn tasks.named("verifyCulinaryReliability")
|
||||
dependsOn tasks.named("verifyCauldronBatching")
|
||||
dependsOn tasks.named("culinaryBatchingSmoke")
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.UUIDUtil;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
/** Binds a placed Forbidden Book to the player whose knowledge it shares. */
|
||||
public record CanapliaCodexInscription(UUID ownerId, String ownerName) {
|
||||
private static final int MAX_NAME_LENGTH = 32;
|
||||
public static final Codec<CanapliaCodexInscription> CODEC = RecordCodecBuilder.create(instance ->
|
||||
instance.group(
|
||||
UUIDUtil.CODEC.fieldOf("owner_id").forGetter(CanapliaCodexInscription::ownerId),
|
||||
Codec.STRING.fieldOf("owner_name").forGetter(CanapliaCodexInscription::ownerName)
|
||||
).apply(instance, CanapliaCodexInscription::new)
|
||||
);
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, CanapliaCodexInscription> STREAM_CODEC =
|
||||
StreamCodec.composite(
|
||||
UUIDUtil.STREAM_CODEC,
|
||||
CanapliaCodexInscription::ownerId,
|
||||
ByteBufCodecs.STRING_UTF8,
|
||||
CanapliaCodexInscription::ownerName,
|
||||
CanapliaCodexInscription::new
|
||||
);
|
||||
|
||||
public CanapliaCodexInscription {
|
||||
if (ownerId == null) throw new IllegalArgumentException("Forbidden Book owner is required");
|
||||
ownerName = sanitizeName(ownerName);
|
||||
}
|
||||
|
||||
public static CanapliaCodexInscription of(UUID ownerId, String ownerName) {
|
||||
return new CanapliaCodexInscription(ownerId, ownerName);
|
||||
}
|
||||
|
||||
private static String sanitizeName(String ownerName) {
|
||||
String cleaned = ownerName == null ? "" : ownerName.replaceAll("\\p{Cntrl}", "").trim();
|
||||
if (cleaned.length() > MAX_NAME_LENGTH) cleaned = cleaned.substring(0, MAX_NAME_LENGTH);
|
||||
return cleaned.isEmpty() ? "?" : cleaned;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -6,6 +6,7 @@ import fr.koka99cab.sanctuary26.itsalive.canaplia.block.CanapliaHempTopBlock;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.block.CanapliaResinBlock;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.block.CanapliaResinBlockEntity;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.item.CanapliaCodexItem;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.item.CanapliaCodexPageItem;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.item.CanapliaJointItem;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.item.CanapliaVariantBlockItem;
|
||||
import java.util.ArrayList;
|
||||
@@ -37,7 +38,7 @@ public final class CanapliaContent {
|
||||
public static final BlockEntityType<CanapliaResinBlockEntity> RESIN_BLOCK_ENTITY_TYPE = registerResinBlockEntity();
|
||||
private static final Map<CanapliaVariantKey, Family> FAMILIES = registerFamilies();
|
||||
public static final Item CODEX_PAGE = registerItem("codex_page", key ->
|
||||
new Item(new Item.Properties().setId(key)));
|
||||
new CanapliaCodexPageItem(new Item.Properties().setId(key)));
|
||||
public static final Item CODEX = registerItem("codex", key ->
|
||||
new CanapliaCodexItem(new Item.Properties().stacksTo(1).setId(key)));
|
||||
private static final List<Item> CREATIVE_ITEMS = createCreativeItems();
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ public final class CanapliaFoundation {
|
||||
throw new IllegalStateException("Unsupported Ambiance visual contract");
|
||||
}
|
||||
CanapliaContent.register();
|
||||
CanapliaLectern.initialize();
|
||||
CanapliaCodexDrops.initialize();
|
||||
CanapliaDiscoverySources.register();
|
||||
CanapliaDiscovery.initialize();
|
||||
|
||||
+47
-2
@@ -1,11 +1,16 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.network.CanapliaCodexPayload;
|
||||
import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveDataComponents;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryProgressionService;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Trusted discovery service and compact Codex snapshot builder. */
|
||||
public final class CanapliaKnowledge {
|
||||
@@ -22,10 +27,50 @@ public final class CanapliaKnowledge {
|
||||
}
|
||||
|
||||
public static void open(ServerPlayer player) {
|
||||
open(player, player.getUUID(), Set.of());
|
||||
}
|
||||
|
||||
public static void open(ServerPlayer player, ItemStack book) {
|
||||
CanapliaCodexInscription inscription = book.get(ItsAliveDataComponents.CANAPLIA_CODEX);
|
||||
if (inscription == null) {
|
||||
open(player);
|
||||
return;
|
||||
}
|
||||
CanapliaKnowledgeData data = CanapliaKnowledgeData.get(player.level().getServer());
|
||||
open(player, player.getUUID(), data.personal(inscription.ownerId()));
|
||||
}
|
||||
|
||||
public static boolean learnRandomUnknown(ServerPlayer player) {
|
||||
CanapliaKnowledgeData data = CanapliaKnowledgeData.get(player.level().getServer());
|
||||
List<CanapliaVariantKey> unknown = unknownVariants(data.personal(player.getUUID()));
|
||||
if (unknown.isEmpty()) {
|
||||
player.sendSystemMessage(Component.translatable("message.canaplia.codex_page.complete"));
|
||||
return false;
|
||||
}
|
||||
CanapliaVariantKey selected = unknown.get(player.getRandom().nextInt(unknown.size()));
|
||||
return discover(player, selected, "codex_page");
|
||||
}
|
||||
|
||||
public static void inscribe(ItemStack stack, ServerPlayer player) {
|
||||
if (!stack.is(CanapliaContent.CODEX)) return;
|
||||
stack.set(ItsAliveDataComponents.CANAPLIA_CODEX, CanapliaCodexInscription.of(
|
||||
player.getUUID(), player.getGameProfile().name()));
|
||||
}
|
||||
|
||||
public static void forgetInscription(ItemStack stack) {
|
||||
if (stack.is(CanapliaContent.CODEX)) stack.remove(ItsAliveDataComponents.CANAPLIA_CODEX);
|
||||
}
|
||||
|
||||
static List<CanapliaVariantKey> unknownVariants(Set<String> learned) {
|
||||
return Arrays.stream(CanapliaVariantKey.values())
|
||||
.filter(variant -> !learned.contains(variant.key()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static void open(ServerPlayer player, UUID personalOwner, Set<String> shared) {
|
||||
if (!ServerPlayNetworking.canSend(player, CanapliaCodexPayload.ID)) return;
|
||||
CanapliaKnowledgeData data = CanapliaKnowledgeData.get(player.level().getServer());
|
||||
Set<String> personal = data.personal(player.getUUID());
|
||||
Set<String> shared = data.shared();
|
||||
Set<String> personal = data.personal(personalOwner);
|
||||
int personalMask = mask(personal);
|
||||
int sharedMask = mask(shared);
|
||||
int originHints = 0;
|
||||
|
||||
+2
-3
@@ -15,7 +15,7 @@ import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** Versioned personal and shared discoveries; unknown future keys survive round-trips. */
|
||||
/** Versioned personal discoveries; the legacy shared field is preserved but no longer exposed globally. */
|
||||
public final class CanapliaKnowledgeData extends SavedData {
|
||||
public static final int DATA_VERSION = 1;
|
||||
private static final Codec<CanapliaKnowledgeData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
@@ -57,8 +57,7 @@ public final class CanapliaKnowledgeData extends SavedData {
|
||||
public boolean discover(UUID player, String key) {
|
||||
if (!validKey(key)) return false;
|
||||
boolean personal = players.computeIfAbsent(player.toString(), ignored -> new LinkedHashSet<>()).add(key);
|
||||
boolean community = shared.add(key);
|
||||
if (personal || community) setDirty();
|
||||
if (personal) setDirty();
|
||||
return personal;
|
||||
}
|
||||
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia;
|
||||
|
||||
import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents;
|
||||
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.LecternBlock;
|
||||
import net.minecraft.world.level.block.entity.LecternBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
/** Shares only the placer's Canaplia knowledge while the Forbidden Book remains on a lectern. */
|
||||
public final class CanapliaLectern {
|
||||
private CanapliaLectern() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
UseBlockCallback.EVENT.register(CanapliaLectern::use);
|
||||
PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> {
|
||||
if (blockEntity instanceof LecternBlockEntity lectern) {
|
||||
CanapliaKnowledge.forgetInscription(lectern.getBook());
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private static InteractionResult use(Player player, Level level, InteractionHand hand, BlockHitResult hit) {
|
||||
if (hand != InteractionHand.MAIN_HAND || player.isSpectator()) return InteractionResult.PASS;
|
||||
BlockPos pos = hit.getBlockPos();
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (!state.is(Blocks.LECTERN)) return InteractionResult.PASS;
|
||||
|
||||
ItemStack held = player.getItemInHand(hand);
|
||||
if (!state.getValue(LecternBlock.HAS_BOOK)) {
|
||||
if (held.is(CanapliaContent.CODEX)
|
||||
&& !level.isClientSide()
|
||||
&& player instanceof ServerPlayer serverPlayer) {
|
||||
CanapliaKnowledge.inscribe(held, serverPlayer);
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
if (!(level.getBlockEntity(pos) instanceof LecternBlockEntity lectern)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
ItemStack book = lectern.getBook();
|
||||
if (!book.is(CanapliaContent.CODEX)) return InteractionResult.PASS;
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(player instanceof ServerPlayer serverPlayer) || !level.mayInteract(serverPlayer, pos)) {
|
||||
return InteractionResult.FAIL;
|
||||
}
|
||||
if (serverPlayer.isSecondaryUseActive()) {
|
||||
return takeBook(serverPlayer, level, pos, state, lectern);
|
||||
}
|
||||
CanapliaKnowledge.open(serverPlayer, book);
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
private static InteractionResult takeBook(ServerPlayer player, Level level, BlockPos pos,
|
||||
BlockState state, LecternBlockEntity lectern) {
|
||||
ItemStack taken = lectern.getBook().copy();
|
||||
if (taken.isEmpty()) return InteractionResult.PASS;
|
||||
CanapliaKnowledge.forgetInscription(taken);
|
||||
lectern.setBook(ItemStack.EMPTY);
|
||||
LecternBlock.resetBookState(player, level, pos, state, false);
|
||||
player.getInventory().placeItemBackInInventory(taken);
|
||||
level.playSound(null, pos, SoundEvents.ITEM_FRAME_REMOVE_ITEM, SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
+49
-11
@@ -5,19 +5,23 @@ import fr.koka99cab.sanctuary26.itsalive.network.CanapliaCodexPayload;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.screens.inventory.BookViewScreen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
|
||||
/** Vanilla book presentation of the server-authored Canaplia catalogue. */
|
||||
public final class CanapliaCodexScreen extends BookViewScreen {
|
||||
private static final int PAGE_BACKGROUND = 0xFF09070E;
|
||||
private static final int PAGE_BORDER = 0xFF5B3268;
|
||||
private int displayedPage;
|
||||
|
||||
public CanapliaCodexScreen(CanapliaCodexPayload snapshot) {
|
||||
super(new BookAccess(pages(snapshot)));
|
||||
}
|
||||
|
||||
private static List<Component> pages(CanapliaCodexPayload snapshot) {
|
||||
List<Component> pages = new ArrayList<>(CanapliaVariantKey.values().length + 1);
|
||||
pages.add(Component.translatable("screen.canaplia.codex.introduction"));
|
||||
List<Component> pages = new ArrayList<>(CanapliaVariantKey.values().length);
|
||||
CanapliaVariantKey[] variants = CanapliaVariantKey.values();
|
||||
for (int index = 0; index < variants.length; index++) {
|
||||
pages.add(variantPage(snapshot, variants[index], index));
|
||||
@@ -25,6 +29,40 @@ public final class CanapliaCodexScreen extends BookViewScreen {
|
||||
return List.copyOf(pages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
super.extractBackground(graphics, mouseX, mouseY, partialTick);
|
||||
int left = (this.width - IMAGE_WIDTH) / 2;
|
||||
graphics.fill(left + 18, 14, left + 174, 177, PAGE_BACKGROUND);
|
||||
graphics.outline(left + 18, 14, 156, 163, PAGE_BORDER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
super.extractRenderState(graphics, mouseX, mouseY, partialTick);
|
||||
graphics.nextStratum();
|
||||
int left = (this.width - IMAGE_WIDTH) / 2;
|
||||
graphics.fill(left + 94, 14, left + 174, 27, PAGE_BACKGROUND);
|
||||
graphics.text(this.font, Component.translatable("screen.canaplia.codex.page",
|
||||
displayedPage + 1, CanapliaVariantKey.values().length), left + 99, 18, 0xFFE3C7ED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setPage(int page) {
|
||||
displayedPage = Math.clamp(page, 0, CanapliaVariantKey.values().length - 1);
|
||||
return super.setPage(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void pageBack() {
|
||||
setPage(displayedPage - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void pageForward() {
|
||||
setPage(displayedPage + 1);
|
||||
}
|
||||
|
||||
private static Component variantPage(CanapliaCodexPayload snapshot,
|
||||
CanapliaVariantKey variant, int index) {
|
||||
boolean personal = bit(snapshot.personalMask(), index);
|
||||
@@ -33,22 +71,22 @@ public final class CanapliaCodexScreen extends BookViewScreen {
|
||||
boolean conditionHint = bit(snapshot.conditionHintMask(), index);
|
||||
|
||||
MutableComponent page = Component.translatable("screen.canaplia.codex.entry_number", index + 1)
|
||||
.withStyle(ChatFormatting.DARK_GRAY).append("\n\n");
|
||||
.withStyle(ChatFormatting.LIGHT_PURPLE).append("\n\n");
|
||||
if (!personal && !shared) {
|
||||
return page.append(Component.translatable("screen.canaplia.codex.hidden")
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
|
||||
page.append((personal
|
||||
? Component.translatable("variant.canaplia." + variant.key())
|
||||
: Component.translatable("screen.canaplia.codex.silhouette"))
|
||||
.withStyle(personal ? ChatFormatting.DARK_GREEN : ChatFormatting.GRAY));
|
||||
if (personal || originHint) {
|
||||
page.append(Component.translatable("variant.canaplia." + variant.key())
|
||||
.withStyle(personal ? ChatFormatting.GREEN : ChatFormatting.LIGHT_PURPLE));
|
||||
if (personal || shared || originHint) {
|
||||
page.append("\n\n").append(Component.translatable("screen.canaplia.codex.origin",
|
||||
Component.translatable("origin.canaplia." + variant.key())));
|
||||
Component.translatable("origin.canaplia." + variant.key()))
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
if (personal || conditionHint) {
|
||||
page.append("\n\n").append(Component.translatable("hint.canaplia." + variant.key()));
|
||||
if (personal || shared || conditionHint) {
|
||||
page.append("\n\n").append(Component.translatable("hint.canaplia." + variant.key())
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
+13
@@ -1,11 +1,17 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaKnowledge;
|
||||
import java.util.function.Consumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Opens the server-authored personal/shared Canaplia collection snapshot. */
|
||||
@@ -20,4 +26,11 @@ public final class CanapliaCodexItem extends Item {
|
||||
if (player instanceof ServerPlayer serverPlayer) CanapliaKnowledge.open(serverPlayer);
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
tooltip.accept(Component.translatable("tooltip.canaplia.codex.personal")
|
||||
.withStyle(ChatFormatting.DARK_PURPLE));
|
||||
}
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.canaplia.item;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaKnowledge;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Consumable unidentified page which discovers one random unknown Canaplia entry. */
|
||||
public final class CanapliaCodexPageItem extends Item {
|
||||
public CanapliaCodexPageItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(Level level, Player player, InteractionHand hand) {
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) return InteractionResult.PASS;
|
||||
if (CanapliaKnowledge.learnRandomUnknown(serverPlayer)) {
|
||||
ItemStack held = player.getItemInHand(hand);
|
||||
if (!player.getAbilities().instabuild) held.shrink(1);
|
||||
}
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
}
|
||||
}
|
||||
+10
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.itsalive.registry;
|
||||
|
||||
import fr.koka99cab.sanctuary26.itsalive.ItsAliveMod;
|
||||
import fr.koka99cab.sanctuary26.itsalive.canaplia.CanapliaCodexInscription;
|
||||
import fr.koka99cab.sanctuary26.itsalive.culinary.knowledge.CulinaryBookInscription;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
@@ -8,6 +9,15 @@ import net.minecraft.core.registries.BuiltInRegistries;
|
||||
|
||||
/** Persistent item components owned by It's Alive. */
|
||||
public final class ItsAliveDataComponents {
|
||||
public static final DataComponentType<CanapliaCodexInscription> CANAPLIA_CODEX = Registry.register(
|
||||
BuiltInRegistries.DATA_COMPONENT_TYPE,
|
||||
ItsAliveMod.id("canaplia_codex"),
|
||||
DataComponentType.<CanapliaCodexInscription>builder()
|
||||
.persistent(CanapliaCodexInscription.CODEC)
|
||||
.networkSynchronized(CanapliaCodexInscription.STREAM_CODEC)
|
||||
.cacheEncoding()
|
||||
.build()
|
||||
);
|
||||
public static final DataComponentType<CulinaryBookInscription> CULINARY_BOOK = Registry.register(
|
||||
BuiltInRegistries.DATA_COMPONENT_TYPE,
|
||||
ItsAliveMod.id("culinary_book"),
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"description.canaplia": "Complete Canaplia gameplay: sixteen plants, server-authoritative discovery, hybrids, effects and an Ambiance-owned visual layer.",
|
||||
"screen.canaplia.codex.title": "Canaplia Codex",
|
||||
"screen.canaplia.codex.title": "Forbidden Book",
|
||||
"screen.canaplia.codex.page": "Page %s / %s",
|
||||
"screen.canaplia.codex.introduction": "CANAPLIA CODEX\n\nSlay monsters to recover scattered pages and, very rarely, a complete Codex.\n\nGrow and discover varieties to fill its pages.",
|
||||
"screen.canaplia.codex.entry_number": "Entry %s",
|
||||
"screen.canaplia.codex.origin": "Origin: %s",
|
||||
"screen.canaplia.codex.hidden": "Undiscovered",
|
||||
"screen.canaplia.codex.silhouette": "Known silhouette",
|
||||
"message.canaplia.discovered": "Canaplia discovered: %s",
|
||||
"message.canaplia.codex_page.complete": "You already know every entry in the Forbidden Book.",
|
||||
"message.canaplia.composter.mixed": "This composter already contains another Canaplia variant.",
|
||||
"tooltip.canaplia.codex.personal": "Personal in hand; shares its owner's knowledge on a lectern",
|
||||
"block.canaplia.hemp_crop": "Canaplia Plant",
|
||||
"block.canaplia.hemp_top": "Canaplia Plant Top",
|
||||
"block.canaplia.resin_block": "Canaplia Resin Block",
|
||||
"item.canaplia.codex": "Canaplia Codex",
|
||||
"item.canaplia.codex": "Forbidden Book",
|
||||
"item.canaplia.codex_page": "Canaplia Page",
|
||||
"variant.canaplia.common": "Common",
|
||||
"origin.canaplia.common": "Wild",
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"description.canaplia": "Gameplay Canaplia complet : seize plantes, découvertes, hybrides et effets autoritaires côté serveur, avec une couche visuelle gérée par Ambiance.",
|
||||
"screen.canaplia.codex.title": "Codex Canaplia",
|
||||
"screen.canaplia.codex.title": "Livre interdit",
|
||||
"screen.canaplia.codex.page": "Page %s / %s",
|
||||
"screen.canaplia.codex.introduction": "CODEX CANAPLIA\n\nTuez des monstres pour retrouver des pages éparses et, très rarement, un Codex complet.\n\nCultivez et découvrez les variétés pour remplir ses pages.",
|
||||
"screen.canaplia.codex.entry_number": "Entrée %s",
|
||||
"screen.canaplia.codex.origin": "Origine : %s",
|
||||
"screen.canaplia.codex.hidden": "Non découverte",
|
||||
"screen.canaplia.codex.silhouette": "Silhouette connue",
|
||||
"message.canaplia.discovered": "Canaplia découverte : %s",
|
||||
"message.canaplia.codex_page.complete": "Tu connais déjà toutes les entrées du Livre interdit.",
|
||||
"message.canaplia.composter.mixed": "Ce composteur contient déjà une autre variante de Canaplia.",
|
||||
"tooltip.canaplia.codex.personal": "Personnel en main ; partage le savoir du propriétaire sur un pupitre",
|
||||
"block.canaplia.hemp_crop": "Plant de Canaplia",
|
||||
"block.canaplia.hemp_top": "Sommet de Canaplia",
|
||||
"block.canaplia.resin_block": "Bloc de résine Canaplia",
|
||||
"item.canaplia.codex": "Codex Canaplia",
|
||||
"item.canaplia.codex": "Livre interdit",
|
||||
"item.canaplia.codex_page": "Page de Canaplia",
|
||||
"variant.canaplia.common": "Commune",
|
||||
"origin.canaplia.common": "Sauvage",
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"description.canaplia": "Полная механика Canaplia: шестнадцать растений, серверные открытия, гибриды и эффекты с визуальным слоем Ambiance.",
|
||||
"screen.canaplia.codex.title": "Кодекс Canaplia",
|
||||
"screen.canaplia.codex.title": "Запретная книга",
|
||||
"screen.canaplia.codex.page": "Страница %s / %s",
|
||||
"screen.canaplia.codex.introduction": "КОДЕКС CANAPLIA\n\nУбивайте монстров, чтобы находить отдельные страницы и, очень редко, полный Кодекс.\n\nВыращивайте и открывайте сорта, чтобы заполнить его.",
|
||||
"screen.canaplia.codex.entry_number": "Запись %s",
|
||||
"screen.canaplia.codex.origin": "Происхождение: %s",
|
||||
"screen.canaplia.codex.hidden": "Не открыто",
|
||||
"screen.canaplia.codex.silhouette": "Известный силуэт",
|
||||
"message.canaplia.discovered": "Открыта Canaplia: %s",
|
||||
"message.canaplia.codex_page.complete": "Вы уже знаете все записи Запретной книги.",
|
||||
"message.canaplia.composter.mixed": "В этом компостере уже находится другой вариант Canaplia.",
|
||||
"tooltip.canaplia.codex.personal": "Личная в руке; на кафедре делится знаниями владельца",
|
||||
"block.canaplia.hemp_crop": "Растение Canaplia",
|
||||
"block.canaplia.hemp_top": "Верхушка Canaplia",
|
||||
"block.canaplia.resin_block": "Блок смолы Canaplia",
|
||||
"item.canaplia.codex": "Кодекс Canaplia",
|
||||
"item.canaplia.codex": "Запретная книга",
|
||||
"item.canaplia.codex_page": "Страница Canaplia",
|
||||
"variant.canaplia.common": "Обычная",
|
||||
"origin.canaplia.common": "Дикая",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"itsalive:culinary_recipe_book"
|
||||
"itsalive:culinary_recipe_book",
|
||||
"canaplia:codex"
|
||||
]
|
||||
}
|
||||
|
||||
+22
@@ -97,6 +97,28 @@ public final class CanapliaGameplaySmoke {
|
||||
"unknown future personal discovery preserved");
|
||||
assertTrue(futureKnowledge.shared().contains("future_variant_17"),
|
||||
"unknown future shared discovery preserved");
|
||||
assertTrue(futureKnowledge.discover(owner, CanapliaVariantKey.COMMON.key()),
|
||||
"new personal discovery recorded");
|
||||
assertTrue(futureKnowledge.personal(owner).contains(CanapliaVariantKey.COMMON.key()),
|
||||
"new discovery remains personal");
|
||||
assertTrue(!futureKnowledge.shared().contains(CanapliaVariantKey.COMMON.key()),
|
||||
"new discovery is not published globally");
|
||||
|
||||
assertEquals(16, CanapliaKnowledge.unknownVariants(Set.of()).size(),
|
||||
"blank Codex page can select every variant");
|
||||
Set<String> almostComplete = new HashSet<>(CanapliaVariants.all().stream()
|
||||
.map(definition -> definition.key().key()).toList());
|
||||
almostComplete.remove(CanapliaVariantKey.EMBER.key());
|
||||
assertEquals(java.util.List.of(CanapliaVariantKey.EMBER),
|
||||
CanapliaKnowledge.unknownVariants(almostComplete), "last unknown Codex entry");
|
||||
assertTrue(CanapliaKnowledge.unknownVariants(
|
||||
CanapliaVariants.all().stream().map(definition -> definition.key().key()).collect(
|
||||
java.util.stream.Collectors.toSet())).isEmpty(), "complete Codex has no unknown entry");
|
||||
|
||||
CanapliaCodexInscription inscription = CanapliaCodexInscription.of(owner,
|
||||
" Can\u0000aplia owner with a deliberately long name ");
|
||||
assertTrue(inscription.ownerName().length() <= 32, "Codex owner name bound");
|
||||
assertTrue(!inscription.ownerName().contains("\u0000"), "Codex owner control characters removed");
|
||||
|
||||
CanapliaCodexPayload payload = new CanapliaCodexPayload(99, -1, 0x1FFFF, -2, Integer.MAX_VALUE);
|
||||
assertEquals(CanapliaCodexPayload.PROTOCOL_VERSION, payload.protocol(), "codex protocol bound");
|
||||
|
||||
Reference in New Issue
Block a user