Files
DM67-Developer 4a850d3a24 Add files via upload
Add initial NPC Framework files
2025-12-08 13:43:33 -06:00

292 lines
12 KiB
Plaintext

// Gestures.lsl — Gesture Animation System for Delphina NPC
// Handles facial expressions and body language animations
//
// TIMER COORDINATION: Acts on Cycle 3 of 4-cycle master timer system
// Cycle 0: Actions.lsl (movement updates)
// Cycle 1: Senses.lsl (environment scanning)
// Cycle 2: Chat.lsl (HTTP timeout detection)
// Cycle 3: Gestures.lsl (animation management) ← THIS SCRIPT
//
// This script receives LM_GESTURE_TRIGGER messages from Chat.lsl when the AI
// model outputs gesture markers (e.g., %smile%, %wave%, %nod_head%) and plays
// the corresponding NPC animations with automatic duration tracking and cleanup.
// ---- Opcodes (match other modules)
integer LM_INIT = 1;
integer LM_CONFIG = 300;
integer LM_GESTURE_TRIGGER = 600; // str: gesture_type, id: target_key
integer LM_TIMER_TICK = 250; // Master timer tick broadcast
integer LM_TIMER_CONFIG = 251; // Timer configuration update
// ---- Config and runtime
key NPC = NULL_KEY;
string CONFIG_CARD = "general.cfg";
float MASTER_TIMER_INTERVAL = 1.0;
integer GESTURE_CYCLE_OFFSET = 3; // Act on cycle 3
// ---- Gesture Configuration
integer GESTURE_ENABLED = TRUE;
integer GESTURE_AUTO_STOP = TRUE; // Auto-stop previous gesture when starting new one
float GESTURE_DURATION = 3.0; // Default duration in seconds (0.0 = play full animation)
integer GESTURE_DEBUG = FALSE;
// ---- Gesture Markers (configurable via general.cfg)
string ACTION_MARKER_SMILE = "%smile%";
string ACTION_MARKER_FROWN = "%frown%";
string ACTION_MARKER_CRY = "%cry%";
string ACTION_MARKER_APPLAUSE = "%applause%";
string ACTION_MARKER_SHAKE_HEAD = "%shake_head%";
string ACTION_MARKER_NOD_HEAD = "%nod_head%";
string ACTION_MARKER_NOD_HEAD_SMILE = "%nod_head_smile%";
string ACTION_MARKER_SHRUG = "%shrug%";
string ACTION_MARKER_CURTSY = "%curtsy%";
string ACTION_MARKER_WAVE = "%wave%";
// ---- Gesture Animation Names (configurable via general.cfg, must match inventory)
// Note: Animation files use capitalized first letter and snake_case for multi-word names
string GESTURE_ANIM_SMILE = "Smile";
string GESTURE_ANIM_FROWN = "Frown";
string GESTURE_ANIM_CRY = "Cry";
string GESTURE_ANIM_APPLAUSE = "Applause";
string GESTURE_ANIM_SHAKE_HEAD = "Shake_Head";
string GESTURE_ANIM_NOD_HEAD = "Nod_Head";
string GESTURE_ANIM_NOD_HEAD_SMILE = "Nod_Head_Smile";
string GESTURE_ANIM_SHRUG = "Shrug";
string GESTURE_ANIM_CURTSY = "Curtsy";
string GESTURE_ANIM_WAVE = "Wave";
// ---- Runtime tracking
string current_gesture = "";
string current_anim_name = "";
float gesture_start_time = 0.0;
integer gesture_timer_active = FALSE;
// ---- Cycle tracking for master timer coordination
integer master_cycle = 0;
// ---- Helper Functions
string get_card(string name) {
if (llGetInventoryType(name) != INVENTORY_NOTECARD) return "";
return osGetNotecard(name);
}
list split_pipe(string s) {
return llParseString2List(s, ["|"], []);
}
// ---- Configuration Loading
integer load_config() {
string txt = get_card(CONFIG_CARD);
if (txt == "") return FALSE;
list lines = llParseString2List(txt, ["\n"], []);
integer i;
for (i = 0; i < llGetListLength(lines); i++) {
string line = llStringTrim(llList2String(lines, i), STRING_TRIM);
if (line == "") jump cont;
if (llGetSubString(line, 0, 0) == "#") jump cont;
if (llGetSubString(line, 0, 1) == "//") jump cont;
integer eq = llSubStringIndex(line, "=");
if (eq <= 0) jump cont;
string k = llStringTrim(llGetSubString(line, 0, eq-1), STRING_TRIM);
string v = llStringTrim(llGetSubString(line, eq+1, -1), STRING_TRIM);
// Gesture System Configuration
if (k == "GESTURE_ENABLED") GESTURE_ENABLED = (integer)v;
else if (k == "GESTURE_AUTO_STOP") GESTURE_AUTO_STOP = (integer)v;
else if (k == "GESTURE_DURATION") {
GESTURE_DURATION = llFabs((float)v); // Prevent negative durations
}
else if (k == "GESTURE_DEBUG") GESTURE_DEBUG = (integer)v;
// Gesture Markers
else if (k == "ACTION_MARKER_SMILE") ACTION_MARKER_SMILE = v;
else if (k == "ACTION_MARKER_FROWN") ACTION_MARKER_FROWN = v;
else if (k == "ACTION_MARKER_CRY") ACTION_MARKER_CRY = v;
else if (k == "ACTION_MARKER_APPLAUSE") ACTION_MARKER_APPLAUSE = v;
else if (k == "ACTION_MARKER_SHAKE_HEAD") ACTION_MARKER_SHAKE_HEAD = v;
else if (k == "ACTION_MARKER_NOD_HEAD") ACTION_MARKER_NOD_HEAD = v;
else if (k == "ACTION_MARKER_NOD_HEAD_SMILE") ACTION_MARKER_NOD_HEAD_SMILE = v;
else if (k == "ACTION_MARKER_SHRUG") ACTION_MARKER_SHRUG = v;
else if (k == "ACTION_MARKER_CURTSY") ACTION_MARKER_CURTSY = v;
else if (k == "ACTION_MARKER_WAVE") ACTION_MARKER_WAVE = v;
// Gesture Animation Names
else if (k == "GESTURE_ANIM_SMILE") GESTURE_ANIM_SMILE = v;
else if (k == "GESTURE_ANIM_FROWN") GESTURE_ANIM_FROWN = v;
else if (k == "GESTURE_ANIM_CRY") GESTURE_ANIM_CRY = v;
else if (k == "GESTURE_ANIM_APPLAUSE") GESTURE_ANIM_APPLAUSE = v;
else if (k == "GESTURE_ANIM_SHAKE_HEAD") GESTURE_ANIM_SHAKE_HEAD = v;
else if (k == "GESTURE_ANIM_NOD_HEAD") GESTURE_ANIM_NOD_HEAD = v;
else if (k == "GESTURE_ANIM_NOD_HEAD_SMILE") GESTURE_ANIM_NOD_HEAD_SMILE = v;
else if (k == "GESTURE_ANIM_SHRUG") GESTURE_ANIM_SHRUG = v;
else if (k == "GESTURE_ANIM_CURTSY") GESTURE_ANIM_CURTSY = v;
else if (k == "GESTURE_ANIM_WAVE") GESTURE_ANIM_WAVE = v;
@cont;
}
return TRUE;
}
// ---- Animation Management
string get_gesture_animation(string gesture_type) {
if (gesture_type == "smile") return GESTURE_ANIM_SMILE;
else if (gesture_type == "frown") return GESTURE_ANIM_FROWN;
else if (gesture_type == "cry") return GESTURE_ANIM_CRY;
else if (gesture_type == "applause") return GESTURE_ANIM_APPLAUSE;
else if (gesture_type == "shake_head") return GESTURE_ANIM_SHAKE_HEAD;
else if (gesture_type == "nod_head") return GESTURE_ANIM_NOD_HEAD;
else if (gesture_type == "nod_head_smile") return GESTURE_ANIM_NOD_HEAD_SMILE;
else if (gesture_type == "shrug") return GESTURE_ANIM_SHRUG;
else if (gesture_type == "curtsy") return GESTURE_ANIM_CURTSY;
else if (gesture_type == "wave") return GESTURE_ANIM_WAVE;
return "";
}
integer start_gesture(string gesture_type) {
if (!GESTURE_ENABLED || NPC == NULL_KEY) return FALSE;
string anim_name = get_gesture_animation(gesture_type);
if (anim_name == "") {
llOwnerSay("Gestures: Unknown gesture type '" + gesture_type + "'");
return FALSE;
}
// IDEMPOTENCE CHECK: Don't restart if same animation still active
// Check if the SAME animation is currently playing AND not expired
// This is more robust than checking gesture_type because:
// 1. It handles config changes (new anim name for same gesture type)
// 2. It validates temporal state (is the animation still active?)
// 3. It prevents stale state from blocking new gestures
// ═══════════════════════════════════════════════════════════════
if (current_anim_name == anim_name && current_anim_name != "") {
// Animation name matches, but is it still actively playing?
float elapsed = llGetTime() - gesture_start_time;
if (GESTURE_DURATION > 0.0 && elapsed < GESTURE_DURATION) {
// Animation is still within its duration window
if (GESTURE_DEBUG) {
llOwnerSay("Gestures: Animation '" + anim_name + "' still playing (" +
(string)((integer)(GESTURE_DURATION - elapsed)) + "s remaining), skipping restart");
}
return TRUE; // Return success, but don't restart
}
else if (GESTURE_DURATION <= 0.0) {
// Infinite duration gestures - always skip restart if same anim
if (GESTURE_DEBUG) {
llOwnerSay("Gestures: Infinite animation '" + anim_name + "' already playing, skipping restart");
}
return TRUE;
}
// If we reach here: elapsed >= GESTURE_DURATION
// The animation should have expired, so allow restart
if (GESTURE_DEBUG) {
llOwnerSay("Gestures: Animation '" + anim_name + "' expired (" +
(string)((integer)elapsed) + "s elapsed), allowing restart");
}
}
// Check if animation exists in inventory
if (llGetInventoryType(anim_name) != INVENTORY_ANIMATION) {
llOwnerSay("Gestures: Animation '" + anim_name + "' not found in inventory");
return FALSE;
}
// Stop current gesture if auto-stop is enabled
if (GESTURE_AUTO_STOP && current_anim_name != "") {
stop_current_gesture();
}
// Start the new animation
osNpcPlayAnimation(NPC, anim_name);
current_gesture = gesture_type;
current_anim_name = anim_name;
gesture_start_time = llGetTime();
// Enable duration tracking (handled by master timer cycle now)
if (GESTURE_DURATION > 0.0) {
gesture_timer_active = TRUE;
}
if (GESTURE_DEBUG) {
llOwnerSay("Gestures: Started '" + gesture_type + "' (" + anim_name + ")");
}
return TRUE;
}
// No return value - stops current NPC animation
stop_current_gesture() {
if (current_anim_name != "" && NPC != NULL_KEY) {
// Store animation name before clearing (in case OSSL call fails)
string stopping_anim = current_anim_name;
string stopping_gesture = current_gesture;
// Clear state BEFORE OSSL call to prevent partial cleanup
current_gesture = "";
current_anim_name = "";
gesture_start_time = 0.0;
gesture_timer_active = FALSE;
// Now attempt to stop the animation
// If this fails, state is already cleared (fail-safe)
osNpcStopAnimation(NPC, stopping_anim);
if (GESTURE_DEBUG) {
llOwnerSay("Gestures: Stopped '" + stopping_gesture + "' (" + stopping_anim + ")");
}
}
}
// ---- DEFAULT STATE
default {
state_entry() {
load_config();
llOwnerSay("Gestures: Gesture Animation System Ready");
if (!GESTURE_ENABLED) {
llOwnerSay("Gestures: System disabled via config");
}
}
link_message(integer sender, integer num, string str, key id) {
if (num == LM_CONFIG) {
NPC = id;
llOwnerSay("Gestures: NPC key configured: " + (string)NPC);
}
else if (num == LM_TIMER_CONFIG) {
list L = split_pipe(str);
integer i;
for (i = 0; i < llGetListLength(L); i++) {
string kv = llList2String(L, i);
integer eq = llSubStringIndex(kv, "=");
if (eq >= 0) {
string k = llStringTrim(llGetSubString(kv, 0, eq-1), STRING_TRIM);
string v = llStringTrim(llGetSubString(kv, eq+1, -1), STRING_TRIM);
if (k == "MASTER_INTERVAL") MASTER_TIMER_INTERVAL = (float)v;
}
}
}
else if (num == LM_TIMER_TICK) {
master_cycle++;
// Cycle gate: act only once every four master ticks
if ((master_cycle % 4) != GESTURE_CYCLE_OFFSET) return;
// Check for gesture expiration using master timer pacing
if (gesture_timer_active && current_anim_name != "") {
float elapsed = llGetTime() - gesture_start_time;
if (elapsed >= GESTURE_DURATION) {
stop_current_gesture();
gesture_timer_active = FALSE;
if (GESTURE_DEBUG) llOwnerSay("Gestures: Auto-stopped expired gesture");
}
}
}
else if (num == LM_GESTURE_TRIGGER) {
// Direct gesture trigger from other modules
start_gesture(str);
}
}
}