diff --git a/manifest.json b/manifest.json index 3804b5a..008b691 100644 --- a/manifest.json +++ b/manifest.json @@ -5,8 +5,34 @@ "author": "ct", "description": "The story content of Cthangover: scenes, scenarios, characters, items, quests and the world map of Marao's very bad night. The machinery lives in engine mods.", "category": "story", - "depends": ["shell2d_base>=1", "dialog_base>=1", "interactive_base>=1", "skills_base>=1", "map_base>=1", "ui_base>=1", "characters_base>=1", "items_base>=1", "quests_base>=1", "town_pack>=1", "music_pack>=1"], + "depends": [ + "shell2d_base>=1", + "dialog_base>=1", + "interactive_base>=1", + "skills_base>=1", + "map_base>=1", + "ui_base>=1", + "characters_base>=1", + "items_base>=1", + "quests_base>=1", + "town_pack>=1", + "music_pack>=1" + ], + "sources": [ + "src/**/*.cs" + ], "test_suites": [ - {"id": "site_shots", "activate": ["--itest=site_shots"]} + { + "id": "site_shots", + "activate": [ + "--itest=site_shots" + ] + }, + { + "id": "marao_avatars", + "activate": [ + "--itest=marao_avatars" + ] + } ] } diff --git a/mod.png b/mod.png index 2edfb90..5b90ecb 100644 Binary files a/mod.png and b/mod.png differ diff --git a/src/StarterPartyInitializer.cs b/src/StarterPartyInitializer.cs new file mode 100644 index 0000000..c8eb63c --- /dev/null +++ b/src/StarterPartyInitializer.cs @@ -0,0 +1,69 @@ +using Cthangover.Core.Events; +using Cthangover.Core.Mods; +using Cthangover.Core.Settings; +using Cthangover.Mods.Skills; + +namespace Cthangover.Mods.StarterParty +{ + /// + /// Populates the initial player party with the default starter + /// character "Marao" when the cthangover_party mod finishes loading. + /// + /// The party can only be populated during OnModResourcesReady, + /// not earlier. OnModLoaded and the constructor fire inside + /// ModRegistry.Initialize → ModCompiler.LoadModCode, + /// where mod JSON resources are not yet guaranteed to be accessible. + /// AddCharacterToParty chains into CharacterFactory.Get, + /// which triggers the shared entity resolver's first scan — this requires + /// file providers to be fully set up, which only happens after the + /// entire ModRegistry.Initialize pipeline completes. + /// + /// OnModResourcesReady is invoked by ModInitializerRegistry + /// .NotifyResourcesReady at the end of SceneManager.Initialize, + /// after ModRegistry finishes discovery, SceneEventRegistry + /// is primed, and scene definitions are collected. At that point all + /// factories can safely read mod JSON data. + /// + /// Mods that introduce additional starter characters should implement + /// their own IModInitializer and call AddCharacterToParty + /// from OnModResourcesReady for their character IDs. + /// + public class StarterPartyInitializer : IModInitializer + { + private static bool newGameHookInstalled; + + /// + /// No-op: cross-mod lifecycle notifications are not needed for + /// starter party setup. Each mod's OnModResourcesReady + /// handles its own default characters independently. + /// + public void OnModLoaded(string modId) { } + + /// + /// Adds the cthangover_party mod's default starter character "Marao" + /// to both Characters and BattleSet via + /// CharacterData.AddCharacterToParty. Called once, after + /// all mod file providers and JSON resources are guaranteed to + /// be accessible. Also installs the one-time + /// hook so the starter party is + /// restored after ResetRuntime wipes it at the start of a + /// new game. Both operations are idempotent. + /// + public void OnModResourcesReady() + { + SeedStarterParty(); + + if (newGameHookInstalled) + return; + + newGameHookInstalled = true; + ModEventBus.Subscribe(_ => SeedStarterParty()); + } + + private static void SeedStarterParty() + { + GameData.Instance.Runtime.CharacterData.AddCharacterToParty("Marao"); + SkillService.GrantSkill("slave_master"); + } + } +} diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/01_front_master.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/01_front_master.png new file mode 100644 index 0000000..75ba7a6 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/01_front_master.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/02_closed_angle.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/02_closed_angle.png new file mode 100644 index 0000000..1587ec2 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/02_closed_angle.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/03_back_spine.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/03_back_spine.png new file mode 100644 index 0000000..cbcf788 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/03_back_spine.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/04_open.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/04_open.png new file mode 100644 index 0000000..7c02740 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/04_open.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/05_emotion_open_mouth.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/05_emotion_open_mouth.png new file mode 100644 index 0000000..71a4116 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/05_emotion_open_mouth.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/06_emotion_eye_roll.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/06_emotion_eye_roll.png new file mode 100644 index 0000000..a41e09f Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/06_emotion_eye_roll.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/07_emotion_confused.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/07_emotion_confused.png new file mode 100644 index 0000000..8c8db8f Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/07_emotion_confused.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/08_emotion_angry_scream.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/08_emotion_angry_scream.png new file mode 100644 index 0000000..16aa292 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/08_emotion_angry_scream.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/09_emotion_fear.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/09_emotion_fear.png new file mode 100644 index 0000000..dcf5def Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/09_emotion_fear.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/10_emotion_embarrassed.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/10_emotion_embarrassed.png new file mode 100644 index 0000000..91c462e Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/10_emotion_embarrassed.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/11_emotion_gloating.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/11_emotion_gloating.png new file mode 100644 index 0000000..e2ed77d Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/11_emotion_gloating.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/12_emotion_squeamish.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/12_emotion_squeamish.png new file mode 100644 index 0000000..b120b21 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/12_emotion_squeamish.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/13_emotion_disgust.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/13_emotion_disgust.png new file mode 100644 index 0000000..425a7d9 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/13_emotion_disgust.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/14_talking_a.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/14_talking_a.png new file mode 100644 index 0000000..ad5c7a1 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/14_talking_a.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/15_talking_b.png b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/15_talking_b.png new file mode 100644 index 0000000..c56e453 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/al_azif_test/15_talking_b.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/bad_smell.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/bad_smell.png new file mode 100644 index 0000000..9d3810f Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/bad_smell.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/deadpan_azif.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/deadpan_azif.png new file mode 100644 index 0000000..231bebc Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/deadpan_azif.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_confused.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_confused.png new file mode 100644 index 0000000..8bd3f99 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_confused.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_embarrassed.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_embarrassed.png new file mode 100644 index 0000000..4872c00 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_embarrassed.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_eye_roll.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_eye_roll.png new file mode 100644 index 0000000..578cf49 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_eye_roll.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_open_mouth.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_open_mouth.png new file mode 100644 index 0000000..ff090e5 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_open_mouth.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_squeamish.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_squeamish.png new file mode 100644 index 0000000..d1765c5 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/emotion_squeamish.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/hangover.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/hangover.png new file mode 100644 index 0000000..8c61fe9 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/hangover.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/neutral.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/neutral.png new file mode 100644 index 0000000..18ffb84 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/neutral.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/pants_panic.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/pants_panic.png new file mode 100644 index 0000000..7be57eb Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/pants_panic.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/poetry_reader.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/poetry_reader.png new file mode 100644 index 0000000..03b783e Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/poetry_reader.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/police_panic.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/police_panic.png new file mode 100644 index 0000000..fc129ae Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/police_panic.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/protective.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/protective.png new file mode 100644 index 0000000..0808602 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/protective.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/rag_emperor.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/rag_emperor.png new file mode 100644 index 0000000..d9b9d9f Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/rag_emperor.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/suspicious.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/suspicious.png new file mode 100644 index 0000000..f56f067 Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/suspicious.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/talking_a.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/talking_a.png new file mode 100644 index 0000000..61cb6aa Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/talking_a.png differ diff --git a/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/talking_b.png b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/talking_b.png new file mode 100644 index 0000000..3bdfb8c Binary files /dev/null and b/test_suites/marao_avatars/avatar_layers/cthangover/marao_test/talking_b.png differ diff --git a/test_suites/marao_avatars/avatars/al_azif.json b/test_suites/marao_avatars/avatars/al_azif.json new file mode 100644 index 0000000..8f66f22 --- /dev/null +++ b/test_suites/marao_avatars/avatars/al_azif.json @@ -0,0 +1,220 @@ +{ + "Kind": "avatar", + "Items": [ + { + "Id": "cthangover/al_azif_test", + "Renderer": "layers", + "BaseValues": { + "canvas": { + "Width": 512, + "Height": 768 + }, + "layers/character": { + "Image": "cthangover/al_azif_test/01_front_master.png", + "Order": 0, + "X": 0.0, + "Y": 143.0471771075019, + "Width": 512.0, + "Height": 481.9056457849962 + } + }, + "Groups": { + "emotion": { + "Default": "auto", + "Priority": 20, + "Presets": { + "auto": {}, + "neutral": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/01_front_master.png", + "Order": 0, + "X": 0.0, + "Y": 143.0471771075019, + "Width": 512.0, + "Height": 481.9056457849962 + } + } + }, + "open_mouth": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/05_emotion_open_mouth.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "eye_roll": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/06_emotion_eye_roll.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "confused": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/07_emotion_confused.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "angry_scream": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/08_emotion_angry_scream.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "fear": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/09_emotion_fear.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "embarrassed": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/10_emotion_embarrassed.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "gloating": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/11_emotion_gloating.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "squeamish": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/12_emotion_squeamish.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "disgust": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/13_emotion_disgust.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "talking_a": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/14_talking_a.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "talking_b": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/15_talking_b.png", + "Order": 0, + "X": 0.0, + "Y": 143.43122102009275, + "Width": 512.0, + "Height": 481.1375579598145 + } + } + } + } + }, + "view": { + "Default": "auto", + "Priority": 100, + "Presets": { + "auto": {}, + "front": {}, + "angled": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/02_closed_angle.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "back": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/03_back_spine.png", + "Order": 0, + "X": 0.0, + "Y": 143.24516627996906, + "Width": 512.0, + "Height": 481.5096674400619 + } + } + }, + "open": { + "Values": { + "layers/character": { + "Image": "cthangover/al_azif_test/04_open.png", + "Order": 0, + "X": 0.0, + "Y": 185.34831460674158, + "Width": 512.0, + "Height": 397.30337078651684 + } + } + } + } + } + } + } + ] +} diff --git a/test_suites/marao_avatars/avatars/marao.json b/test_suites/marao_avatars/avatars/marao.json new file mode 100644 index 0000000..d47edf8 --- /dev/null +++ b/test_suites/marao_avatars/avatars/marao.json @@ -0,0 +1,294 @@ +{ + "Kind": "avatar", + "Items": [ + { + "Id": "cthangover/marao_test", + "Renderer": "layers", + "BaseValues": { + "canvas": { + "Width": 512, + "Height": 768 + } + }, + "Groups": { + "reaction": { + "Default": "neutral", + "Priority": 0, + "Presets": { + "neutral": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/neutral.png", + "Order": 0, + "X": 0.0, + "Y": 0.0, + "Width": 512.0, + "Height": 768.0 + } + } + }, + "hangover": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/hangover.png", + "Order": 0, + "X": 0.0, + "Y": 0.0, + "Width": 512.0, + "Height": 768.0 + } + } + }, + "pants_panic": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/pants_panic.png", + "Order": 0, + "X": 0.0, + "Y": 0.0, + "Width": 512.0, + "Height": 768.0 + } + } + }, + "rag_emperor": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/rag_emperor.png", + "Order": 0, + "X": 0.0, + "Y": 0.0, + "Width": 512.0, + "Height": 768.0 + } + } + }, + "deadpan_azif": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/deadpan_azif.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "poetry_reader": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/poetry_reader.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "police_panic": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/police_panic.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "suspicious": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/suspicious.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "bad_smell": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/bad_smell.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "protective": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/protective.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + } + } + }, + "emotion": { + "Default": "auto", + "Priority": 20, + "Presets": { + "auto": {}, + "neutral": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/neutral.png", + "Order": 0, + "X": 0.0, + "Y": 0.0, + "Width": 512.0, + "Height": 768.0 + } + } + }, + "open_mouth": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/emotion_open_mouth.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "eye_roll": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/emotion_eye_roll.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "confused": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/emotion_confused.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "angry_scream": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/pants_panic.png", + "Order": 0, + "X": 0.0, + "Y": 0.0, + "Width": 512.0, + "Height": 768.0 + } + } + }, + "fear": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/police_panic.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "embarrassed": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/emotion_embarrassed.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "gloating": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/rag_emperor.png", + "Order": 0, + "X": 0.0, + "Y": 0.0, + "Width": 512.0, + "Height": 768.0 + } + } + }, + "squeamish": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/emotion_squeamish.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "disgust": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/bad_smell.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "talking_a": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/talking_a.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + }, + "talking_b": { + "Values": { + "layers/character": { + "Image": "cthangover/marao_test/talking_b.png", + "Order": 0, + "X": 0.0, + "Y": 256.0, + "Width": 512.0, + "Height": 512.0 + } + } + } + } + } + } + } + ] +} diff --git a/test_suites/marao_avatars/docs/en.md b/test_suites/marao_avatars/docs/en.md new file mode 100644 index 0000000..a5c5b1c --- /dev/null +++ b/test_suites/marao_avatars/docs/en.md @@ -0,0 +1,57 @@ +# Marao reaction auditions + +Opt-in content sample for `avatar_base` + `avatar_layers`, activated by `--itest=marao_avatars`. +The virtual mod `cthangover_game__marao_avatars` supplies `cthangover/marao_test` with ten named +`reaction` presets. Four are full-body images; six are waist-up portraits, fitted without +stretching into a 512x768 canvas and aligned at the bottom. Every PNG has actual alpha. + +This is an audition set of complete acting poses, not a modular facial rig. A reaction selects +one complete character layer. Eyes, mouth, clothes and pose cannot be independently combined +in these particular assets. The underlying renderer remains capable of multiple independent layers. + +Sources, exact cloud prompts, rejected-request records, story references and the offline preview +are archived outside repositories in `art_sources/cthangover/marao_avatar_test_20261030/`. +Source images are copied byte-for-byte into the test suite. No production dialogue references change. + +```csharp +IAvatar avatar = AvatarCatalog.Create("cthangover/marao_test"); +avatar.ApplyPresets(new Dictionary { ["reaction"] = "deadpan_azif" }); +IAvatarRenderer renderer = AvatarRenderers.Create(avatar); +host.AddChild(renderer.View); +// Dispose renderer before destroying host. +``` + +Presets: neutral, hangover, pants_panic, rag_emperor, deadpan_azif, poetry_reader, +police_panic, suspicious, bad_smell, protective. + +Runtime audition and comparison export (windowed, via the workspace Windows job bridge): + +```text +godot --path . --rendering-method gl_compatibility -- --frontend --with-mods=cthangover_game --itest=marao_avatars --run-tests --mod=cthangover_game__marao_avatars +``` + +The two runtime tests apply every preset, verify distinct rendered output and alpha, round-trip +saved selections and export a comparison sheet plus individual normalized PNGs under +`gen/marao_avatar_test_20261030/`. Raw generated images are retained separately. + +## Paired Al-Azif update + +The suite also supplies `cthangover/al_azif_test`. Both avatars expose `emotion` with +`auto`, `neutral`, `open_mouth`, `eye_roll`, `confused`, `angry_scream`, `fear`, `embarrassed`, +`gloating`, `squeamish`, `disgust`, `talking_a`, `talking_b`. Default `auto` contributes nothing: +Marao keeps the selected story reaction, Al-Azif uses its neutral baseline. An explicit emotion +at priority 20 replaces the complete character image; select auto to return to story reactions. + +Al-Azif additionally exposes `view`: auto/front keep emotion control; angled/back/open at +priority 100 select the requested structural view, temporarily overriding the emotional image. +Select view=auto to return to the chosen emotion. No mirroring is applied to book images. +The two talking presets are not an automatically played or pixel-aligned animation. + +The four full-body Marao poses now use softer legs and loose white shorts. Both held-book +portraits use the canonical Al-Azif design. Selected source PNGs and the updated offline gallery +live in `art_sources/cthangover/marao_azif_avatar_update_20261030/`. The original generated +versions remain archived in the earlier source batch. Three page-pulling story frames are +standalone illustrations in the updated source batch; they do not replace production scenes. + +Additional runtime tests exercise the shared emotion vocabulary, all Al-Azif views, JSON +restore and export `gen/marao_azif_avatar_update_20261030/emotion_comparison.png`. diff --git a/test_suites/marao_avatars/docs/ru.md b/test_suites/marao_avatars/docs/ru.md new file mode 100644 index 0000000..7d9556d --- /dev/null +++ b/test_suites/marao_avatars/docs/ru.md @@ -0,0 +1,42 @@ +# Пробы реакций Марао + +Тестовый набор включается флагом `--itest=marao_avatars`. Ресурс аватара: +`cthangover/marao_test`, группа пресетов: `reaction`. + +| Пресет | Реакция | +| --- | --- | +| neutral | Спокойный, собранный Марао | +| hangover | «Кузнец в голове»: похмельная гримаса | +| pants_panic | «Где мои штаны?!»: возмущённое недоумение | +| rag_emperor | Самодовольный император красной тряпки | +| deadpan_azif | «Ты бумажка»: равнодушие перед Аль-Азифом | +| poetry_reader | Уверенный чтец «казахских стихов» | +| police_panic | «Я же одет… почти»: комическая паника | +| suspicious | Подозрение у виллы «Забвение» | +| bad_smell | «Ну хоть не падалью»: отвращение к запаху | +| protective | Серьёзная решимость защитить другого | + +Первые четыре варианта полноростовые, остальные шесть — поясные. Каждый пресет +выбирает целую позу с выражением лица: это подборка для выбора образов, а не +раздельные детали глаз и рта. PNG прозрачные, исходные диалоговые аватарки не заменены. + +[Формат и запуск проверки](en.md). Галерея с сюжетными цитатами находится вне +репозитория: `art_sources/cthangover/marao_avatar_test_20261030/index.html`. + +## Обновление: общий набор с Аль-Азифом + +Добавлен аватар `cthangover/al_azif_test`. Оба аватара поддерживают группу `emotion`: +`auto`, `neutral`, `open_mouth`, `eye_roll`, `confused`, `angry_scream`, `fear`, `embarrassed`, +`gloating`, `squeamish`, `disgust`, `talking_a`, `talking_b`. + +По умолчанию emotion=auto сохраняет сюжетную реакцию Марао или спокойную книгу. +Явная эмоция выбирает готовое изображение целиком и перекрывает сюжетную реакцию; +для возврата нужно снова выбрать auto. Два кадра речи не образуют готовый lip sync. + +У Аль-Азифа есть группа `view`: auto/front оставляют управление эмоции, angled/back/open +показывают другой ракурс и перекрывают эмоциональную картинку. Для возврата — view=auto. + +Четыре полноростовые позы Марао обновлены: ноги мягче, одежда — свободные белые шорты. +В двух портретах исправлена книга. Обновлённая галерея: +`art_sources/cthangover/marao_azif_avatar_update_20261030/index.html`. +Три кадра попытки вырвать страницу находятся там же как отдельные иллюстрации. diff --git a/test_suites/marao_avatars/manifest.json b/test_suites/marao_avatars/manifest.json new file mode 100644 index 0000000..3d8cf79 --- /dev/null +++ b/test_suites/marao_avatars/manifest.json @@ -0,0 +1,13 @@ +{ + "id": "cthangover_game__marao_avatars", + "name": "Marao avatar auditions", + "version": "1.0.0", + "author": "ct", + "description": "Ten story-inspired anime reaction presets for the layered avatar renderer.", + "depends": [ + "avatar_layers>=1" + ], + "test_sources": [ + "tests/*.cs" + ] +} diff --git a/test_suites/marao_avatars/tests/MaraoAvatarTests.cs b/test_suites/marao_avatars/tests/MaraoAvatarTests.cs new file mode 100644 index 0000000..54c45d9 --- /dev/null +++ b/test_suites/marao_avatars/tests/MaraoAvatarTests.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text.Json; +using System.Threading.Tasks; +using Cthangover.Core.Mods.Resolvers; +using Cthangover.Core.Mods.Testing; +using Cthangover.Core.Utils; +using Cthangover.Mods.Avatars; +using Godot; + +namespace Cthangover.Mods.MaraoAvatars.Tests +{ + /// Auditions the actual story PNGs through preset state and the installed layered renderer. + public sealed class MaraoAvatarTests + { + private const string AvatarId = "cthangover/marao_test"; + private const string Output = "res://gen/marao_avatar_test_20261030/"; + + private static string OutputFile(string name) + { + // The picture folder may be missing on a fresh checkout (the host has no gen/ subfolder). + var dir = ProjectSettings.GlobalizePath(Output); + DirAccess.MakeDirRecursiveAbsolute(dir); + return dir + name; + } + + [ModTest("marao_avatars/all_ten_reactions_render_and_restore", TimeoutSeconds = 45)] + public static async Task Reactions(ModTestContext ctx) + { + ModAssert.True(GameEnvironment.IsRuntime, "visual audition requires the Godot runtime"); + var tree = (SceneTree)Engine.GetMainLoop(); + var viewport = new SubViewport { Size = new Vector2I(512, 768), TransparentBg = true, + Disable3D = true, RenderTargetUpdateMode = SubViewport.UpdateMode.Always }; + var avatar = AvatarCatalog.Create(AvatarId); + var independent = AvatarCatalog.Create(AvatarId); + var definition = Entities.Collect()[AvatarId]; + ModAssert.Equal(10, definition.Groups["reaction"].Presets.Count, "all requested reactions installed"); + using var renderer = AvatarRenderers.Create(avatar); + var hashes = new HashSet(StringComparer.Ordinal); + try + { + tree.Root.AddChild(viewport); + viewport.AddChild(renderer.View); + foreach (var preset in definition.Groups["reaction"].Presets.Keys) + { + avatar.ApplyPresets(new Dictionary { ["reaction"] = preset }); + renderer.Apply(avatar.Resolve()); + await tree.ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw); + using var image = viewport.GetTexture().GetImage(); + var visible = 0; var transparent = 0; + for (var y = 0; y < image.GetHeight(); y += 16) + for (var x = 0; x < image.GetWidth(); x += 16) + { + var alpha = image.GetPixel(x, y).A; + if (alpha > 0.9f) visible++; + if (alpha < 0.01f) transparent++; + } + ModAssert.True(visible > 100, preset + ": character has visible pixels"); + ModAssert.True(transparent > 100, preset + ": transparent surroundings preserved"); + ModAssert.True(hashes.Add(Convert.ToHexString(SHA256.HashData(image.GetData()))), + preset + ": reaction produces a distinct rendered image"); + ModAssert.True(image.SavePng(OutputFile(preset + ".render.png")) == Error.Ok, + preset + ": rendered audition exported"); + var saved = JsonSerializer.Deserialize(JsonSerializer.Serialize(avatar.CaptureState())); + var restored = AvatarCatalog.Create(AvatarId, saved); + ModAssert.Equal(preset, restored.CaptureState().Presets["reaction"], "JSON save restores selection"); + } + ModAssert.Equal("neutral", independent.CaptureState().Presets["reaction"], "another instance stays unchanged"); + } + finally + { + renderer.Dispose(); viewport.GetParent()?.RemoveChild(viewport); viewport.Free(); + } + } + + [ModTest("marao_avatars/comparison_sheet", TimeoutSeconds = 45)] + public static async Task Comparison(ModTestContext ctx) + { + ModAssert.True(GameEnvironment.IsRuntime, "comparison sheet requires the Godot runtime"); + var tree = (SceneTree)Engine.GetMainLoop(); + var viewport = new SubViewport { Size = new Vector2I(1440, 960), TransparentBg = false, + Disable3D = true, RenderTargetUpdateMode = SubViewport.UpdateMode.Always }; + var renderers = new List(); + try + { + tree.Root.AddChild(viewport); + viewport.AddChild(new ColorRect { Color = new Color("e4ded4"), Size = new Vector2(1440, 960) }); + var index = 0; + foreach (var preset in Entities.Collect()[AvatarId].Groups["reaction"].Presets.Keys) + { + var x = (index % 5) * 288; var y = (index / 5) * 480; + viewport.AddChild(new ColorRect { Color = new Color(index % 2 == 0 ? "e4ded4" : "c5c6c5"), + Position = new Vector2(x, y), Size = new Vector2(288, 480) }); + var avatar = AvatarCatalog.Create(AvatarId); + avatar.ApplyPresets(new Dictionary { ["reaction"] = preset }); + var renderer = AvatarRenderers.Create(avatar); renderers.Add(renderer); + viewport.AddChild(renderer.View); + renderer.View.Scale = new Vector2(0.5f, 0.5f); + renderer.View.Position = new Vector2(x + 16, y + 24); + var label = new Label { Text = (index + 1) + ". " + preset, Position = new Vector2(x + 12, y + 426), + Size = new Vector2(264, 36), HorizontalAlignment = HorizontalAlignment.Center }; + label.AddThemeColorOverride("font_color", new Color("24232b")); + label.AddThemeFontSizeOverride("font_size", 18); + viewport.AddChild(label); + index++; + } + await tree.ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw); + using var sheet = viewport.GetTexture().GetImage(); + ModAssert.True(sheet.SavePng(OutputFile("comparison.png")) == Error.Ok, + "comparison sheet exported from actual avatar renderer"); + } + finally + { + foreach (var renderer in renderers) renderer.Dispose(); + viewport.GetParent()?.RemoveChild(viewport); viewport.Free(); + } + } + } +} diff --git a/test_suites/marao_avatars/tests/PairedAvatarTests.cs b/test_suites/marao_avatars/tests/PairedAvatarTests.cs new file mode 100644 index 0000000..a56f53d --- /dev/null +++ b/test_suites/marao_avatars/tests/PairedAvatarTests.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Cthangover.Core.Mods.Resolvers; +using Cthangover.Core.Mods.Testing; +using Cthangover.Core.Utils; +using Cthangover.Mods.Avatars; +using Godot; + +namespace Cthangover.Mods.MaraoAvatars.Tests +{ + /// Both content avatars share the same named emotions while retaining independent view/pose state. + public sealed class PairedAvatarTests + { + private const string Marao = "cthangover/marao_test"; + private const string Book = "cthangover/al_azif_test"; + private const string Output = "res://gen/marao_azif_avatar_update_20261030/"; + + private static string OutputFile(string name) + { + // The picture folder may be missing on a fresh checkout (the host has no gen/ subfolder). + var dir = ProjectSettings.GlobalizePath(Output); + DirAccess.MakeDirRecursiveAbsolute(dir); + return dir + name; + } + + [ModTest("paired_avatars/common_emotions_render_and_restore", TimeoutSeconds = 60)] + public static async Task CommonEmotions(ModTestContext ctx) + { + ModAssert.True(GameEnvironment.IsRuntime); + var definitions = Entities.Collect(); + var states = definitions[Marao].Groups["emotion"].Presets.Keys.OrderBy(Identity, StringComparer.Ordinal).ToArray(); + var bookStates = definitions[Book].Groups["emotion"].Presets.Keys.OrderBy(Identity, StringComparer.Ordinal).ToArray(); + ModAssert.Equal(13, states.Length, "twelve shared states plus auto"); + ModAssert.Equal(string.Join(",", states), string.Join(",", bookStates), "same public emotion names"); + var tree = (SceneTree)Engine.GetMainLoop(); + var viewport = new SubViewport { Size = new Vector2I(512, 768), TransparentBg = true, + Disable3D = true, RenderTargetUpdateMode = SubViewport.UpdateMode.Always }; + try + { + tree.Root.AddChild(viewport); + foreach (var id in new[] { Marao, Book }) + { + var avatar = AvatarCatalog.Create(id); + using var renderer = AvatarRenderers.Create(avatar); + viewport.AddChild(renderer.View); + foreach (var state in states) + { + avatar.ApplyPresets(new Dictionary { ["emotion"] = state }); + renderer.Apply(avatar.Resolve()); + await tree.ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw); + using var image = viewport.GetTexture().GetImage(); + var visible = 0; var transparent = 0; + for (var y = 0; y < 768; y += 24) + for (var x = 0; x < 512; x += 24) + { + var alpha = image.GetPixel(x, y).A; + if (alpha > 0.9f) visible++; + if (alpha < 0.01f) transparent++; + } + ModAssert.True(visible > 40 && transparent > 40, id + "/" + state + ": rendered with alpha"); + var saved = JsonSerializer.Deserialize(JsonSerializer.Serialize(avatar.CaptureState())); + var restored = AvatarCatalog.Create(id, saved); + ModAssert.Equal(state, restored.CaptureState().Presets["emotion"]); + } + } + var book = AvatarCatalog.Create(Book); + foreach (var view in new[] { "front", "angled", "back", "open" }) + { + book.ApplyPresets(new Dictionary { ["view"] = view, ["emotion"] = "angry_scream" }); + using var renderer = AvatarRenderers.Create(book); + viewport.AddChild(renderer.View); + await tree.ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw); + using var image = viewport.GetTexture().GetImage(); + ModAssert.True(image.SavePng(OutputFile("al_azif_" + view + ".render.png")) == Error.Ok); + var state = JsonSerializer.Deserialize(JsonSerializer.Serialize(book.CaptureState())); + ModAssert.Equal(view, AvatarCatalog.Create(Book, state).CaptureState().Presets["view"]); + } + } + finally { viewport.GetParent()?.RemoveChild(viewport); viewport.Free(); } + } + + [ModTest("paired_avatars/emotion_comparison_sheet", TimeoutSeconds = 60)] + public static async Task Comparison(ModTestContext ctx) + { + ModAssert.True(GameEnvironment.IsRuntime); + var tree = (SceneTree)Engine.GetMainLoop(); + var viewport = new SubViewport { Size = new Vector2I(1920, 1920), TransparentBg = false, + Disable3D = true, RenderTargetUpdateMode = SubViewport.UpdateMode.Always }; + var renderers = new List(); + try + { + tree.Root.AddChild(viewport); + viewport.AddChild(new ColorRect { Size = new Vector2(1920, 1920), Color = new Color("e4ded4") }); + var index = 0; + foreach (var emotion in Entities.Collect()[Marao].Groups["emotion"].Presets.Keys) + { + if (emotion == "auto") continue; + var x = (index % 3) * 640; var y = (index / 3) * 480; + viewport.AddChild(new ColorRect { Position = new Vector2(x, y), Size = new Vector2(640, 480), + Color = new Color(index % 2 == 0 ? "e4ded4" : "c6c7c5") }); + var side = 0; + foreach (var id in new[] { Marao, Book }) + { + var avatar = AvatarCatalog.Create(id); + avatar.ApplyPresets(new Dictionary { ["emotion"] = emotion }); + var renderer = AvatarRenderers.Create(avatar); renderers.Add(renderer); + viewport.AddChild(renderer.View); + renderer.View.Scale = new Vector2(0.5f, 0.5f); + renderer.View.Position = new Vector2(x + 24 + side * 320, y + 24); + side++; + } + var label = new Label { Text = emotion, Position = new Vector2(x + 12, y + 428), + Size = new Vector2(616, 36), HorizontalAlignment = HorizontalAlignment.Center }; + label.AddThemeColorOverride("font_color", new Color("24232b")); + label.AddThemeFontSizeOverride("font_size", 22); + viewport.AddChild(label); index++; + } + await tree.ToSignal(RenderingServer.Singleton, RenderingServer.SignalName.FramePostDraw); + using var image = viewport.GetTexture().GetImage(); + ModAssert.True(image.SavePng(OutputFile("emotion_comparison.png")) == Error.Ok); + } + finally + { + foreach (var renderer in renderers) renderer.Dispose(); + viewport.GetParent()?.RemoveChild(viewport); viewport.Free(); + } + } + + private static string Identity(string value) => value; + } +}