extends Object class_name DanceRoutines ## The five emotes, as data. ## ## They are procedural rather than authored clips because the shared animation ## library ships exactly one `Dance_Loop`, and five copies of one clip is not ## five dances. What the runtime DOES have is a working procedural pose layer ## over a real skeleton with spring-driven hair and cloth, which is enough to ## build a dance out of if the motion is constructed the way an animator would ## construct it rather than the way a programmer reaches for first. ## ## ── Why not just wire sine waves to the bones ─────────────────────────────── ## ## Because that is what "programmer animation" looks like, and everyone can tell. ## A raw sine moves fastest through the middle and slowest at the ends by exactly ## the same amount on every channel, all in phase, forever. The result floats. It ## has no weight, no accent, and no sense that one part of the body is driving and ## the rest is following. ## ## Four principles fix that, and all four are cheap: ## ## OVERLAP the body is a chain, and a chain does not move as one piece. ## Hips lead, spine follows a beat later, chest later still, ## head last. `lag` below is that, in seconds per link. It is ## the single largest difference between "a rig oscillating" ## and "a person moving", and the spring bones then carry it ## out through the hair and the skirt for free. ## ACCENT a dance HITS poses. `shape` bends the wave so it hangs at ## the extremes and snaps between them — the same asymmetry a ## key-and-breakdown pass produces by hand. `sin` is shape 1.0; ## above that it gets punchier. ## WEIGHT a body that never leaves its own axis reads as a puppet. Real ## dances move the HIPS — side to side, up and down — and the ## rest of the body reacts. `hip_swing` and `hip_bob` are ## translations, not rotations, and they are what make the legs ## look like they are carrying someone. ## ANTICIPATION the counter-move before the move. Handled per routine by ## running a channel at a fraction of a beat AHEAD of the one it ## precedes, rather than by a separate mechanism. ## ## ── The shape of a routine ────────────────────────────────────────────────── ## ## Everything is derived from one `bpm`, so no two channels can drift apart no ## matter how long the emote runs — which is the other thing that goes wrong when ## channels are given independent frequencies that are not exact ratios. ## ## Channel amounts are radians (rotations) or metres (the two hip translations). ## `beats` is how many beats that channel takes for one full cycle, so 2 is a ## side-to-side that takes two beats to return, 1 is once per beat, and 0.5 is ## twice per beat. Fractions of a beat are how a routine gets a cross-rhythm ## without leaving the grid. ## `id` is what gets networked, so these strings must stay stable. const ROUTINES := [ { "id": "two_step", "name": "Two-Step", "icon": "♪", "bpm": 104.0, "lag": 0.055, "shape": 1.35, # The foundation step: weight rocks side to side, the shoulders counter # the hips, the arms hang and swing off the shoulders a beat behind. "hip_swing": 0.075, "hip_bob": 0.022, "hip_bob_beats": 1.0, "hip_roll": 0.16, "hip_yaw": 0.20, "spine_roll": 0.13, "spine_counter": 0.55, "head_roll": 0.22, "head_bob": 0.10, "arm_swing": 0.55, "arm_out": 0.42, "arm_beats": 2.0, "elbow": 0.75, "knee": 0.30, }, { "id": "body_wave", "name": "Body Wave", "icon": "〜", "bpm": 88.0, # The whole point of this one is the lag: a wave travelling up the spine # IS overlap, made visible. At 0.13 s per link the crest takes most of a # beat to get from the hips to the head. "lag": 0.13, "shape": 1.15, "hip_swing": 0.03, "hip_bob": 0.045, "hip_bob_beats": 2.0, "hip_pitch": 0.26, "spine_pitch": 0.30, "spine_counter": 0.0, "head_pitch": 0.22, "head_roll": 0.06, "arm_out": 0.85, "arm_swing": 0.20, "arm_beats": 4.0, "elbow": 0.55, "knee": 0.18, }, { "id": "robot", "name": "Robot", "icon": "▣", "bpm": 112.0, # No lag, and the motion is QUANTISED — see `steps`. Both are deliberate # violations of everything above, and they work for exactly that reason: # the robot reads as mechanical because the viewer has been shown four # other routines that do not. "lag": 0.0, "shape": 1.0, "steps": 4, "hip_swing": 0.04, "hip_bob": 0.012, "hip_bob_beats": 1.0, "hip_yaw": 0.30, "spine_yaw": 0.34, "spine_counter": 0.0, "head_yaw": 0.42, "arm_out": 1.05, "arm_swing": 0.85, "arm_beats": 2.0, "elbow": 1.35, "knee": 0.10, }, { "id": "bounce", "name": "Bounce", "icon": "▲", "bpm": 128.0, "lag": 0.035, # The punchiest shape in the set. A bounce lives entirely in the accent: # the body hangs at the top and slams through the bottom, which is a # gravity read, and a plain sine cannot express it. "shape": 2.2, "hip_swing": 0.03, "hip_bob": 0.070, "hip_bob_beats": 1.0, "hip_roll": 0.08, "spine_pitch": 0.14, "spine_counter": 0.30, "head_bob": 0.16, "head_roll": 0.10, "arm_swing": 0.95, "arm_out": 0.30, "arm_beats": 1.0, "elbow": 1.05, # Deep knees. This is the routine where the legs do the work, and a bounce # with straight legs looks like a character being shaken. "knee": 0.85, }, { "id": "spin", "name": "Spin", "icon": "↻", "bpm": 96.0, "lag": 0.07, "shape": 1.5, "hip_swing": 0.05, "hip_bob": 0.030, "hip_bob_beats": 2.0, "hip_yaw": 0.85, "spine_yaw": 0.30, "spine_counter": 0.0, "spine_roll": 0.14, # SPOTTING: the head holds its heading while the body turns under it, then # whips round to catch up. It is what a real dancer does to keep from # getting dizzy, and it is the most recognisable thing about a turn. See # `spot` in DanceModifier — this is not a wave, it is a hold and a snap. "spot": 1.0, "head_yaw": 0.0, "arm_out": 1.15, "arm_swing": 0.25, "arm_beats": 4.0, "elbow": 0.35, "knee": 0.22, }, ] static func count() -> int: return ROUTINES.size() ## A routine by index, wrapped so an out-of-range network value cannot crash a ## remote peer's model. static func get_routine(index: int) -> Dictionary: if ROUTINES.is_empty(): return {} return ROUTINES[posmod(index, ROUTINES.size())] static func name_of(index: int) -> String: return String(get_routine(index).get("name", "")) static func index_of(id: String) -> int: for i in ROUTINES.size(): if ROUTINES[i]["id"] == id: return i return 0