Update cthangover_game implementation, assets and documentation
No files matched your search
@@ -5,8 +5,34 @@
|
|||||||
"author": "ct",
|
"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.",
|
"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",
|
"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": [
|
"test_suites": [
|
||||||
{"id": "site_shots", "activate": ["--itest=site_shots"]}
|
{
|
||||||
|
"id": "site_shots",
|
||||||
|
"activate": [
|
||||||
|
"--itest=site_shots"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "marao_avatars",
|
||||||
|
"activate": [
|
||||||
|
"--itest=marao_avatars"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,69 @@
|
|||||||
|
using Cthangover.Core.Events;
|
||||||
|
using Cthangover.Core.Mods;
|
||||||
|
using Cthangover.Core.Settings;
|
||||||
|
using Cthangover.Mods.Skills;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.StarterParty
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Populates the initial player party with the default starter
|
||||||
|
/// character <c>"Marao"</c> when the cthangover_party mod finishes loading.
|
||||||
|
///
|
||||||
|
/// The party can only be populated during <c>OnModResourcesReady</c>,
|
||||||
|
/// not earlier. <c>OnModLoaded</c> and the constructor fire inside
|
||||||
|
/// <c>ModRegistry.Initialize</c> → <c>ModCompiler.LoadModCode</c>,
|
||||||
|
/// where mod JSON resources are not yet guaranteed to be accessible.
|
||||||
|
/// <c>AddCharacterToParty</c> chains into <c>CharacterFactory.Get</c>,
|
||||||
|
/// which triggers the shared entity resolver's first scan — this requires
|
||||||
|
/// file providers to be fully set up, which only happens after the
|
||||||
|
/// entire <c>ModRegistry.Initialize</c> pipeline completes.
|
||||||
|
///
|
||||||
|
/// <c>OnModResourcesReady</c> is invoked by <c>ModInitializerRegistry
|
||||||
|
/// .NotifyResourcesReady</c> at the end of <c>SceneManager.Initialize</c>,
|
||||||
|
/// after <c>ModRegistry</c> finishes discovery, <c>SceneEventRegistry</c>
|
||||||
|
/// 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 <c>IModInitializer</c> and call <c>AddCharacterToParty</c>
|
||||||
|
/// from <c>OnModResourcesReady</c> for their character IDs.
|
||||||
|
/// </summary>
|
||||||
|
public class StarterPartyInitializer : IModInitializer
|
||||||
|
{
|
||||||
|
private static bool newGameHookInstalled;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No-op: cross-mod lifecycle notifications are not needed for
|
||||||
|
/// starter party setup. Each mod's <c>OnModResourcesReady</c>
|
||||||
|
/// handles its own default characters independently.
|
||||||
|
/// </summary>
|
||||||
|
public void OnModLoaded(string modId) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the cthangover_party mod's default starter character <c>"Marao"</c>
|
||||||
|
/// to both <c>Characters</c> and <c>BattleSet</c> via
|
||||||
|
/// <c>CharacterData.AddCharacterToParty</c>. Called once, after
|
||||||
|
/// all mod file providers and JSON resources are guaranteed to
|
||||||
|
/// be accessible. Also installs the one-time
|
||||||
|
/// <see cref="NewGameStartedEvent"/> hook so the starter party is
|
||||||
|
/// restored after <c>ResetRuntime</c> wipes it at the start of a
|
||||||
|
/// new game. Both operations are idempotent.
|
||||||
|
/// </summary>
|
||||||
|
public void OnModResourcesReady()
|
||||||
|
{
|
||||||
|
SeedStarterParty();
|
||||||
|
|
||||||
|
if (newGameHookInstalled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
newGameHookInstalled = true;
|
||||||
|
ModEventBus.Subscribe<NewGameStartedEvent>(_ => SeedStarterParty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SeedStarterParty()
|
||||||
|
{
|
||||||
|
GameData.Instance.Runtime.CharacterData.AddCharacterToParty("Marao");
|
||||||
|
SkillService.GrantSkill("slave_master");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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<string, string> { ["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`.
|
||||||
@@ -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`.
|
||||||
|
Три кадра попытки вырвать страницу находятся там же как отдельные иллюстрации.
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>Auditions the actual story PNGs through preset state and the installed layered renderer.</summary>
|
||||||
|
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<AvatarDefinition>()[AvatarId];
|
||||||
|
ModAssert.Equal(10, definition.Groups["reaction"].Presets.Count, "all requested reactions installed");
|
||||||
|
using var renderer = AvatarRenderers.Create(avatar);
|
||||||
|
var hashes = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tree.Root.AddChild(viewport);
|
||||||
|
viewport.AddChild(renderer.View);
|
||||||
|
foreach (var preset in definition.Groups["reaction"].Presets.Keys)
|
||||||
|
{
|
||||||
|
avatar.ApplyPresets(new Dictionary<string, string> { ["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<AvatarState>(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<IAvatarRenderer>();
|
||||||
|
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<AvatarDefinition>()[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<string, string> { ["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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>Both content avatars share the same named emotions while retaining independent view/pose state.</summary>
|
||||||
|
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<AvatarDefinition>();
|
||||||
|
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<string, string> { ["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<AvatarState>(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<string, string> { ["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<AvatarState>(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<IAvatarRenderer>();
|
||||||
|
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<AvatarDefinition>()[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<string, string> { ["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;
|
||||||
|
}
|
||||||
|
}
|
||||||