106 lines
3.4 KiB
Python
106 lines
3.4 KiB
Python
"""
|
|
Region / bone -> reaction mapping for /send_click_interaction.
|
|
|
|
Pure data + selection logic. Returns broadcast-ready dicts (sound, animation,
|
|
idle) so server.py can hand them straight to notify_clients without any HTTP
|
|
self-calls or threading.
|
|
|
|
The payload shapes here intentionally mirror what /talk and /animate already
|
|
broadcast, so the client sees the same message types regardless of source.
|
|
"""
|
|
import random
|
|
from pathlib import Path
|
|
from typing import Tuple
|
|
|
|
|
|
SOUND_DIR = Path("public/sounds")
|
|
ANIM_VRMA_DIR = Path("animations/vrma_xr")
|
|
ANIM_MIXAMO_DIR = Path("animations/mixamo")
|
|
|
|
FEEDBACK_SOUNDS = [
|
|
("oh.wav", "oh?"),
|
|
("hey.wav", "hey"),
|
|
]
|
|
|
|
|
|
def _talk_payload(audio_path: Path, text: str, duration: int = 1, expression: str = "relaxed") -> dict:
|
|
return {
|
|
"type": "start_animation",
|
|
"audio_path": str(audio_path),
|
|
"expression": expression,
|
|
"audio_text": text,
|
|
"audio_duraction": duration,
|
|
}
|
|
|
|
|
|
def _vrma_payload(animation_path: Path, crop_start: float = 0.0, crop_end: float = 0.0) -> dict:
|
|
return {
|
|
"type": "start_vrma",
|
|
"animation_url": str(animation_path),
|
|
"play_once": False,
|
|
"crop_start": crop_start,
|
|
"crop_end": crop_end,
|
|
"lock_position": False,
|
|
"track_position": True,
|
|
}
|
|
|
|
|
|
def _idle_payload() -> dict:
|
|
return {
|
|
"type": "start_mixamo",
|
|
"animation_url": str(ANIM_MIXAMO_DIR / "Idle.fbx"),
|
|
"play_once": False,
|
|
"crop_start": 0.0,
|
|
"crop_end": 0.0,
|
|
"lock_position": False,
|
|
"track_position": True,
|
|
}
|
|
|
|
|
|
def _pick_animation(region: str, bone: str) -> Tuple[Path, float, float]:
|
|
"""Choose (animation_path, crop_start, idle_delay) from region/bone."""
|
|
region_l = (region or "").lower()
|
|
bone_l = bone or ""
|
|
|
|
if region_l in {"chest", "bust", "belly"}:
|
|
return ANIM_VRMA_DIR / "woah.vrma", 0.85, 3.0
|
|
|
|
if bone_l in {"left_cat_ear", "right_cat_ear"}:
|
|
return ANIM_VRMA_DIR / "touch_ears.vrma", 0.95, 3.0
|
|
|
|
right_parts = ("right_hand", "right_arm", "right_shoulder", "right_thigh", "right_shin", "right_foot")
|
|
if any(part in region_l for part in right_parts):
|
|
return ANIM_VRMA_DIR / "lookright.vrma", 0.72, 2.2
|
|
|
|
left_parts = ("left_hand", "left_arm", "left_shoulder", "left_thigh", "left_shin", "left_foot")
|
|
if any(part in region_l for part in left_parts):
|
|
return ANIM_VRMA_DIR / "lookleft.vrma", 0.82, 2.1
|
|
|
|
if region_l in {"head", "neck", "hair"}:
|
|
return ANIM_VRMA_DIR / "headpat_cover.vrma", 0.72, 2.8
|
|
|
|
return ANIM_VRMA_DIR / "stop_it.vrma", 0.82, 1.7
|
|
|
|
|
|
def build_click_reaction(region: str, bone: str) -> dict:
|
|
"""Build the sound + animation + idle payloads for a click interaction.
|
|
|
|
Returns a dict with keys:
|
|
sound broadcast payload for the feedback voice line
|
|
animation broadcast payload for the reaction animation
|
|
idle broadcast payload that returns to idle
|
|
idle_delay seconds to wait before sending the idle payload
|
|
"""
|
|
sound_file, sound_text = random.choice(FEEDBACK_SOUNDS)
|
|
sound_payload = _talk_payload(SOUND_DIR / sound_file, sound_text)
|
|
|
|
anim_path, crop_start, idle_delay = _pick_animation(region, bone)
|
|
anim_payload = _vrma_payload(anim_path, crop_start=crop_start, crop_end=0.0)
|
|
|
|
return {
|
|
"sound": sound_payload,
|
|
"animation": anim_payload,
|
|
"idle": _idle_payload(),
|
|
"idle_delay": idle_delay,
|
|
}
|