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;
+ }
+}