Reorganize Blueprint categories under ASTERION namespace, move bAutoStartBehavior to PersonalityComponent, add user documentation
- Bulk rename Category/ClassGroup strings to ASTERION|PS_AI_ConvAgent|... and ASTERION|PS_AI_Behavior|... (284 occurrences, 24 files) so functions/properties/components group cleanly in Blueprint menus - Move bAutoStartBehavior from AIController to PersonalityComponent so the same AIController class can drive multiple NPC archetypes with per-instance scripted-mode override - Add PowerPoint user documentation for both plugins in Docs/ with generator scripts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BIN
Docs/PS_AI_Behavior_Documentation.pdf
Normal file
BIN
Docs/PS_AI_Behavior_Documentation.pdf
Normal file
Binary file not shown.
BIN
Docs/PS_AI_Behavior_Documentation.pptx
Normal file
BIN
Docs/PS_AI_Behavior_Documentation.pptx
Normal file
Binary file not shown.
BIN
Docs/PS_AI_ConvAgent_Documentation.pdf
Normal file
BIN
Docs/PS_AI_ConvAgent_Documentation.pdf
Normal file
Binary file not shown.
BIN
Docs/PS_AI_ConvAgent_Documentation.pptx
Normal file
BIN
Docs/PS_AI_ConvAgent_Documentation.pptx
Normal file
Binary file not shown.
691
Docs/gen_behavior_doc.js
Normal file
691
Docs/gen_behavior_doc.js
Normal file
@@ -0,0 +1,691 @@
|
||||
// PS_AI_Behavior User Documentation Generator
|
||||
// Palette: Warm Terracotta (terracotta #B85042, sand #E7E8D1, sage #A7BEAE)
|
||||
|
||||
const pptxgen = require("pptxgenjs");
|
||||
const pres = new pptxgen();
|
||||
|
||||
pres.layout = "LAYOUT_WIDE"; // 13.333 x 7.5 inch
|
||||
|
||||
// ─── Color Palette ─────────────────────────────────
|
||||
const TERRA = "B85042";
|
||||
const TERRA_D = "7A3229";
|
||||
const SAND = "E7E8D1";
|
||||
const SAND_LT = "F5F5EA";
|
||||
const SAGE = "A7BEAE";
|
||||
const SAGE_D = "5F7566";
|
||||
const WHITE = "FFFFFF";
|
||||
const CHARCOAL = "2E2A26";
|
||||
const GOLD = "D4A84B";
|
||||
|
||||
// ─── Font ──────────────────────────────────────────
|
||||
const FONT_H = "Calibri";
|
||||
const FONT_B = "Calibri";
|
||||
|
||||
// ─── Master slide backgrounds ──────────────────────
|
||||
pres.defineSlideMaster({
|
||||
title: "CONTENT",
|
||||
background: { color: SAND_LT },
|
||||
objects: [
|
||||
{ rect: { x: 0, y: 0, w: 13.333, h: 0.35, fill: { color: TERRA } } },
|
||||
{ rect: { x: 0, y: 7.15, w: 13.333, h: 0.35, fill: { color: TERRA } } },
|
||||
{ text: {
|
||||
text: "PS_AI_Behavior — User Documentation",
|
||||
options: { x: 0.3, y: 7.18, w: 8, h: 0.3, fontSize: 10, color: SAND, fontFace: FONT_B }
|
||||
} },
|
||||
{ text: {
|
||||
text: "ASTERION",
|
||||
options: { x: 11.5, y: 7.18, w: 1.5, h: 0.3, fontSize: 10, color: SAND, fontFace: FONT_B, align: "right", bold: true }
|
||||
} },
|
||||
],
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 1 — TITLE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
let s = pres.addSlide();
|
||||
s.background = { color: TERRA };
|
||||
s.addShape(pres.ShapeType.rect, { x: 0, y: 3.3, w: 13.333, h: 0.05, fill: { color: GOLD } });
|
||||
s.addText("PS_AI_Behavior", { x: 0.6, y: 2.2, w: 12, h: 1.2, fontSize: 60, bold: true, color: WHITE, fontFace: FONT_H });
|
||||
s.addText("Personality-Driven NPC AI for Unreal Engine", { x: 0.6, y: 3.5, w: 12, h: 0.6, fontSize: 22, color: SAND, fontFace: FONT_B, italic: true });
|
||||
s.addText("User Documentation", { x: 0.6, y: 4.4, w: 12, h: 0.5, fontSize: 18, color: SAND, fontFace: FONT_B });
|
||||
s.addText("Civilians • Enemies • Protectors • Cover System • Splines", { x: 0.6, y: 5.2, w: 12, h: 0.4, fontSize: 14, color: SAND, fontFace: FONT_B });
|
||||
s.addText("ASTERION • 2026", { x: 0.6, y: 6.6, w: 12, h: 0.4, fontSize: 12, color: SAND, fontFace: FONT_B });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 2 — OVERVIEW
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Plugin Overview", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
|
||||
s.addText("What it does", { x: 0.5, y: 1.7, w: 6, h: 0.4, fontSize: 18, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Personality-driven AI for NPCs", options: { bullet: true, fontSize: 14 } },
|
||||
{ text: "Three archetypes: Civilian, Enemy, Protector", options: { bullet: true, fontSize: 14 } },
|
||||
{ text: "Trait-based reactions: Courage, Aggressivity, Caution", options: { bullet: true, fontSize: 14 } },
|
||||
{ text: "Tactical combat + cover system + EQS", options: { bullet: true, fontSize: 14 } },
|
||||
{ text: "Spline-based patrol networks", options: { bullet: true, fontSize: 14 } },
|
||||
{ text: "Interface-driven — no Pawn class dependency", options: { bullet: true, fontSize: 14 } },
|
||||
], { x: 0.5, y: 2.1, w: 6, h: 3, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 8 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 7, y: 1.7, w: 5.8, h: 4.8, fill: { color: TERRA }, rectRadius: 0.1 });
|
||||
s.addText("Primary Use Case", { x: 7.2, y: 1.9, w: 5.4, h: 0.4, fontSize: 16, bold: true, color: GOLD, fontFace: FONT_H });
|
||||
s.addText("Populate your world with intelligent NPCs that perceive threats, evaluate risk based on personality, and respond appropriately — civilians flee from gunfire, enemies engage in tactical combat, protectors defend civilians.", {
|
||||
x: 7.2, y: 2.4, w: 5.4, h: 1.7, fontSize: 13, color: SAND, fontFace: FONT_B, italic: true
|
||||
});
|
||||
s.addText("Key Differentiators", { x: 7.2, y: 4.2, w: 5.4, h: 0.4, fontSize: 16, bold: true, color: GOLD, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Data-driven personality profiles", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Built-in cover cycle state machine", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Spline network with junction detection", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Gaze bridge to ConvAgent (reflection)", options: { bullet: true, fontSize: 12 } },
|
||||
], { x: 7.2, y: 4.7, w: 5.4, h: 1.7, color: SAND, fontFace: FONT_B, paraSpaceAfter: 4 });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 3 — CORE ARCHITECTURE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Core Architecture", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
|
||||
s.addText("The plugin is built around four concepts:", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
const pillars = [
|
||||
{ title: "Interface", subtitle: "IPS_AI_Behavior_Interface", desc: "Implemented on host project Pawns. Zero compile dependency — identity, team, combat, movement." },
|
||||
{ title: "NPC Type", subtitle: "Civilian / Enemy / Protector", desc: "Determines team, spline access, default tree. Enemies can be disguised (hostile=false)." },
|
||||
{ title: "State", subtitle: "Behavior State enum", desc: "Idle, Patrol, Alerted, Combat, Fleeing, TakingCover, Dead, Scripted. Written to Blackboard." },
|
||||
{ title: "Profile", subtitle: "PersonalityProfile data asset", desc: "Traits, thresholds, target priority, speed per state, default BT." },
|
||||
];
|
||||
pillars.forEach((p, i) => {
|
||||
const col = i % 2;
|
||||
const row = Math.floor(i / 2);
|
||||
const x = 0.5 + col * 6.3;
|
||||
const y = 2.2 + row * 2.3;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 6.0, h: 2.1, fill: { color: TERRA }, rectRadius: 0.1 });
|
||||
s.addText(p.title, { x: x + 0.2, y: y + 0.15, w: 5.6, h: 0.45, fontSize: 20, bold: true, color: GOLD, fontFace: FONT_H });
|
||||
s.addText(p.subtitle, { x: x + 0.2, y: y + 0.6, w: 5.6, h: 0.35, fontSize: 12, color: SAND, italic: true, fontFace: "Consolas" });
|
||||
s.addText(p.desc, { x: x + 0.2, y: y + 1.0, w: 5.6, h: 1.0, fontSize: 12, color: SAND, fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 4 — NPC TYPES & TEAM ID
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("NPC Types & TeamId", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Identity drives perception, targeting, and access control.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
// Three NPC cards
|
||||
const npcs = [
|
||||
{ name: "CIVILIAN", color: SAGE, id: "0x00", desc: "Non-hostile. Flees. Allied with Protector." },
|
||||
{ name: "ENEMY", color: TERRA, id: "0x10", desc: "Hostile. Tactical combat. Can be disguised." },
|
||||
{ name: "PROTECTOR", color: SAGE_D, id: "0x20", desc: "Police / guard. Defends Civilians." },
|
||||
];
|
||||
npcs.forEach((n, i) => {
|
||||
const x = 0.5 + i * 4.3;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: 2.15, w: 4.0, h: 2.2, fill: { color: n.color }, rectRadius: 0.1 });
|
||||
s.addText(n.name, { x: x + 0.2, y: 2.3, w: 3.6, h: 0.5, fontSize: 22, bold: true, color: WHITE, align: "center", fontFace: FONT_H });
|
||||
s.addText(`TeamId = ${n.id}`, { x: x + 0.2, y: 2.85, w: 3.6, h: 0.35, fontSize: 14, color: WHITE, align: "center", italic: true, fontFace: "Consolas" });
|
||||
s.addText(n.desc, { x: x + 0.2, y: 3.3, w: 3.6, h: 0.95, fontSize: 12, color: WHITE, align: "center", fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// TeamId encoding
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 4.6, w: 12.3, h: 2.3, fill: { color: TERRA }, rectRadius: 0.1 });
|
||||
s.addText("TeamId Encoding", { x: 0.7, y: 4.7, w: 12, h: 0.4, fontSize: 16, bold: true, color: GOLD, fontFace: FONT_H });
|
||||
s.addText("uint8 = (high nibble: NPCType) | (low nibble: Faction 0-15)", { x: 0.7, y: 5.1, w: 12, h: 0.3, fontSize: 13, color: SAND, italic: true, fontFace: "Consolas" });
|
||||
|
||||
s.addText([
|
||||
{ text: "Same type + same faction → Friendly (allies)", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Same type + different faction → Hostile (rival gangs)", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Civilian ↔ Protector → always Friendly", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Everything else → Hostile", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Disguised Enemy (hostile=false) → TeamId 0x01 (civilian disguise)", options: { bullet: true, fontSize: 12 } },
|
||||
], { x: 0.7, y: 5.45, w: 12, h: 1.4, color: SAND, fontFace: FONT_B, paraSpaceAfter: 3 });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 5 — BEHAVIOR STATES
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Behavior States", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Enum written to Blackboard, drives BT branching.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
const states = [
|
||||
{ name: "Idle", desc: "Resting / waiting. Proximity gaze active." },
|
||||
{ name: "Patrol", desc: "Wandering on waypoints or splines." },
|
||||
{ name: "Alerted", desc: "Suspicious, heightened awareness." },
|
||||
{ name: "Combat", desc: "Engaged with a threat, attacking." },
|
||||
{ name: "Fleeing", desc: "Running away from danger." },
|
||||
{ name: "TakingCover", desc: "Behind tactical cover during combat." },
|
||||
{ name: "Dead", desc: "NPC deceased, all systems shut down." },
|
||||
{ name: "Scripted", desc: "Manual override (conversations, cinematics)." },
|
||||
];
|
||||
states.forEach((state, i) => {
|
||||
const col = i % 4;
|
||||
const row = Math.floor(i / 4);
|
||||
const x = 0.5 + col * 3.15;
|
||||
const y = 2.2 + row * 2.3;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 2.95, h: 2.1, fill: { color: SAND }, line: { color: TERRA, width: 1 }, rectRadius: 0.08 });
|
||||
s.addShape(pres.ShapeType.rect, { x: x, y: y, w: 2.95, h: 0.55, fill: { color: TERRA } });
|
||||
s.addText(state.name, { x: x, y: y + 0.05, w: 2.95, h: 0.5, fontSize: 16, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
s.addText(state.desc, { x: x + 0.15, y: y + 0.7, w: 2.65, h: 1.3, fontSize: 11, color: CHARCOAL, valign: "top", fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 6 — AI CONTROLLER
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("AI Controller", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("APS_AI_Behavior_AIController — the central orchestrator.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Auto-setup on Possession", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Discovers PersonalityComponent on Pawn", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Computes TeamId from NPCType + Faction", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Initializes Blackboard with required keys", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Binds gaze bridge to ConvAgent (if present)", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Starts BT (unless bAutoStartBehavior = false)", options: { bullet: true, fontSize: 12 } },
|
||||
], { x: 0.5, y: 2.55, w: 6, h: 2.5, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 4 });
|
||||
|
||||
s.addText("Key Methods", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "StartBehavior() / StopBehavior()", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "SetBehaviorState() / GetBehaviorState()", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "SetTeamId() — runtime team change", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "GetBehaviorPerception()", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "GetPersonalityComponent()", options: { bullet: true, fontSize: 12 } },
|
||||
], { x: 7, y: 2.55, w: 6, h: 2.5, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 4 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.2, w: 12.3, h: 1.8, fill: { color: TERRA }, rectRadius: 0.1 });
|
||||
s.addText("Gaze Bridge (Proximity)", { x: 0.7, y: 5.3, w: 12, h: 0.35, fontSize: 14, bold: true, color: GOLD, fontFace: FONT_H });
|
||||
s.addText("Reflection-based integration with PS_AI_ConvAgent. During Idle/Patrol, NPC glances at nearby actors within GazeProximityRadius. Configurable duration and cooldown. Auto-pauses during active conversations.", {
|
||||
x: 0.7, y: 5.7, w: 12, h: 1.25, fontSize: 12, color: SAND, italic: true, fontFace: FONT_B
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 7 — PERSONALITY COMPONENT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Personality Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Runtime traits & state reactions for the NPC.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
// Traits triangle
|
||||
s.addText("The Three Traits", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
|
||||
const traits = [
|
||||
{ name: "Courage", desc: "0 = coward, 1 = fearless", effect: "Gates flee threshold & cover advancement", color: "C9463A" },
|
||||
{ name: "Aggressivity", desc: "0 = peaceful, 1 = violent", effect: "Drives attack threshold, combat/cover cycle", color: "D4803A" },
|
||||
{ name: "Caution", desc: "0 = reckless, 1 = prudent", effect: "Flee threshold, cover duration, spline threat", color: "7C9D7B" },
|
||||
];
|
||||
traits.forEach((t, i) => {
|
||||
const y = 2.6 + i * 0.95;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 1.8, h: 0.8, fill: { color: t.color }, rectRadius: 0.08 });
|
||||
s.addText(t.name, { x: 0.5, y: y + 0.1, w: 1.8, h: 0.3, fontSize: 14, bold: true, color: WHITE, align: "center", fontFace: FONT_H });
|
||||
s.addText(t.desc, { x: 0.5, y: y + 0.45, w: 1.8, h: 0.3, fontSize: 10, color: WHITE, align: "center", italic: true, fontFace: FONT_B });
|
||||
s.addText(t.effect, { x: 2.4, y: y + 0.15, w: 4.2, h: 0.6, fontSize: 12, color: CHARCOAL, valign: "middle", fontFace: FONT_B });
|
||||
});
|
||||
|
||||
s.addText("Properties & Events", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Profile — PersonalityProfile DataAsset", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "bAutoStartBehavior — per-NPC override", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "RuntimeTraits — editable at runtime", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "PerceivedThreatLevel — replicated", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "CurrentState — replicated with OnRep", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "EvaluateReaction() / ApplyReaction()", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "ModifyTrait() / GetTrait()", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "ForceState() — scripting override", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "OnBehaviorStateChanged delegate", options: { bullet: true, fontSize: 12 } },
|
||||
], { x: 7, y: 2.55, w: 6, h: 4.5, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 3 });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 8 — PERSONALITY PROFILE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("PersonalityProfile DataAsset", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 32, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("The behavioral recipe — one profile per NPC archetype.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
const profileCats = [
|
||||
{ title: "Identity", items: ["ProfileName", "NPCType", "Faction (0-15)"] },
|
||||
{ title: "Traits", items: ["Courage (0-1)", "Aggressivity (0-1)", "Caution (0-1)"] },
|
||||
{ title: "Thresholds", items: ["FleeThreshold", "AttackThreshold", "AlertThreshold", "ThreatDecayRate"] },
|
||||
{ title: "Combat", items: ["TargetPriority[]", "MinAttackRange / MaxAttackRange", "CombatCoverCycleDuration"] },
|
||||
{ title: "Movement", items: ["SpeedPerState map", "DefaultWalkSpeed"] },
|
||||
{ title: "Behavior", items: ["DefaultBehaviorTree (soft ref)"] },
|
||||
];
|
||||
profileCats.forEach((c, i) => {
|
||||
const col = i % 3;
|
||||
const row = Math.floor(i / 3);
|
||||
const x = 0.5 + col * 4.15;
|
||||
const y = 2.2 + row * 2.3;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 3.95, h: 2.1, fill: { color: SAND }, line: { color: TERRA, width: 1 }, rectRadius: 0.08 });
|
||||
s.addText(c.title, { x: x + 0.15, y: y + 0.1, w: 3.65, h: 0.35, fontSize: 14, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: x + 0.15, y: y + 0.45, w: 0.35, h: 0.04, fill: { color: GOLD } });
|
||||
s.addText(c.items.map(t => ({ text: t, options: { bullet: true, fontSize: 11 } })),
|
||||
{ x: x + 0.15, y: y + 0.55, w: 3.65, h: 1.4, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 3 });
|
||||
});
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 6.85, w: 12.3, h: 0.35, fill: { color: TERRA }, rectRadius: 0.04 });
|
||||
s.addText("Example presets: Cowardly Villager • Aggressive Guard • Rival Gang Member", { x: 0.7, y: 6.88, w: 12, h: 0.3, fontSize: 11, color: SAND, italic: true, fontFace: FONT_B });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 9 — COMPONENTS OVERVIEW
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Components", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Attach to Pawns as needed — most are optional.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
const components = [
|
||||
{ name: "Personality", desc: "Traits, profile, state reactions", required: true },
|
||||
{ name: "Perception", desc: "AI Perception wrapper, threat computation", required: "auto" },
|
||||
{ name: "Team", desc: "Identity for non-AI pawns (player)", required: false },
|
||||
{ name: "Combat", desc: "Attack events, fire/kill delegates", required: false },
|
||||
{ name: "Spline Follower", desc: "Spline walking, junction detection", required: false },
|
||||
];
|
||||
components.forEach((c, i) => {
|
||||
const y = 2.2 + i * 0.9;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 3.5, h: 0.8, fill: { color: TERRA }, rectRadius: 0.08 });
|
||||
s.addText(c.name, { x: 0.5, y: y, w: 3.5, h: 0.8, fontSize: 16, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 4.2, y: y, w: 7.0, h: 0.8, fill: { color: SAND }, line: { color: TERRA, width: 1 }, rectRadius: 0.06 });
|
||||
s.addText(c.desc, { x: 4.4, y: y + 0.1, w: 6.8, h: 0.6, fontSize: 13, color: CHARCOAL, valign: "middle", fontFace: FONT_B });
|
||||
|
||||
let tagText = c.required === true ? "Required" : (c.required === "auto" ? "Auto" : "Optional");
|
||||
let tagColor = c.required === true ? TERRA : (c.required === "auto" ? SAGE_D : GOLD);
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 11.4, y: y + 0.2, w: 1.4, h: 0.4, fill: { color: tagColor }, rectRadius: 0.08 });
|
||||
s.addText(tagText, { x: 11.4, y: y + 0.2, w: 1.4, h: 0.4, fontSize: 11, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 10 — PERCEPTION COMPONENT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Perception Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Sight, hearing, damage senses — auto-configured.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Senses", { x: 0.5, y: 2.15, w: 4, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Sight — vision cone + line of sight", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Hearing — noise events with tags", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Damage — damage received tracking", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 0.5, y: 2.55, w: 4, h: 2, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Threat Scoring", { x: 4.8, y: 2.15, w: 4, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Target priority (from Profile)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Recent damage weight", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Proximity weight", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Target persistence (80% score hold)", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 4.8, y: 2.55, w: 4.2, h: 2.2, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Key Methods", { x: 9.3, y: 2.15, w: 3.5, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "GetHighestThreatActor()", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "CalculateThreatLevel()", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "GetThreatLocation()", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "ClassifyActor()", options: { bullet: true, fontSize: 12 } },
|
||||
], { x: 9.3, y: 2.55, w: 3.5, h: 2, color: CHARCOAL, fontFace: "Consolas", paraSpaceAfter: 4 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.15, w: 12.3, h: 1.85, fill: { color: TERRA }, rectRadius: 0.1 });
|
||||
s.addText("Perception / Threat Resolution", { x: 0.7, y: 5.25, w: 12, h: 0.35, fontSize: 14, bold: true, color: GOLD, fontFace: FONT_H });
|
||||
s.addText("Detects ALL affiliations → filters by Hostile attitude. Supports separate ThreatTarget (e.g. AimTargetActor sphere) vs. owning Pawn. Line-of-sight tracking with last-known-position memory.", {
|
||||
x: 0.7, y: 5.65, w: 12, h: 1.3, fontSize: 12, color: SAND, italic: true, fontFace: FONT_B
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 11 — COMBAT COMPONENT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Combat Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Attack execution, cooldown, damage events.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Properties", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "AttackRange — max distance (cm)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "AttackCooldown — minimum between attacks", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "AttackDamage — base damage value", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "CurrentTarget — replicated", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 0.5, y: 2.55, w: 6, h: 2.3, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Methods & Events", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "CanAttack() — cooldown check", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "IsInAttackRange() — distance test", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "ExecuteAttack() — apply damage + multicast", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "OnAttackExecuted (multicast)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "OnDamageReceived (server-only)", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 7, y: 2.55, w: 6, h: 2.5, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.3, w: 12.3, h: 1.7, fill: { color: TERRA }, rectRadius: 0.1 });
|
||||
s.addText("Combat Type", { x: 0.7, y: 5.4, w: 12, h: 0.35, fontSize: 14, bold: true, color: GOLD, fontFace: FONT_H });
|
||||
s.addText("NPC's combat type (Melee / Ranged) comes from IPS_AI_Behavior::GetBehaviorCombatType(). Melee = rush the target (no cooldown between re-positions). Ranged = maintain distance, use cover cycle, peek to fire.", {
|
||||
x: 0.7, y: 5.75, w: 12, h: 1.2, fontSize: 12, color: SAND, italic: true, fontFace: FONT_B
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 12 — WORLD ACTORS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("World Actors", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Placed by level designers — drive navigation and tactics.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
const actors = [
|
||||
{ name: "SplinePath", desc: "Placeable spline defining a navigation corridor. Filtered by NPCType. Bidirectional, with priority for junction selection." },
|
||||
{ name: "SplineNetwork", desc: "WorldSubsystem — auto-detects intersections between SplinePath actors. Provides navigation queries, threat-aware switching." },
|
||||
{ name: "CoverPoint", desc: "Manually placed tactical position. Cover or HidingSpot type, quality score, crouch flag, max occupants, type filter." },
|
||||
];
|
||||
actors.forEach((a, i) => {
|
||||
const y = 2.2 + i * 1.5;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 12.3, h: 1.35, fill: { color: SAND }, line: { color: TERRA, width: 1 }, rectRadius: 0.1 });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: y, w: 3.5, h: 1.35, fill: { color: TERRA } });
|
||||
s.addText(a.name, { x: 0.5, y: y, w: 3.5, h: 1.35, fontSize: 20, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
s.addText(a.desc, { x: 4.2, y: y + 0.15, w: 8.4, h: 1.05, fontSize: 13, color: CHARCOAL, valign: "middle", fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 13 — BEHAVIOR TREE SERVICES & TASKS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Behavior Tree — Services & Tasks", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 30, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Reusable BT nodes — build custom trees per NPC type.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Services (root-level)", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
const services = [
|
||||
{ name: "UpdateThreat", desc: "Queries perception, writes threat BB keys" },
|
||||
{ name: "EvaluateReaction", desc: "Computes state from traits + threat" },
|
||||
{ name: "UpdateGaze", desc: "Manages eye contact, ConvAgent bridge" },
|
||||
];
|
||||
services.forEach((svc, i) => {
|
||||
const y = 2.55 + i * 0.7;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 2.3, h: 0.55, fill: { color: SAGE_D }, rectRadius: 0.06 });
|
||||
s.addText(svc.name, { x: 0.5, y: y, w: 2.3, h: 0.55, fontSize: 12, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
s.addText(svc.desc, { x: 2.9, y: y, w: 3.7, h: 0.55, fontSize: 11, color: CHARCOAL, valign: "middle", fontFace: FONT_B });
|
||||
});
|
||||
|
||||
s.addText("Tasks (leaves)", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
const tasks = [
|
||||
{ name: "FollowSpline", desc: "Drives SplineFollower" },
|
||||
{ name: "FindAndFollowSpline", desc: "Auto-locate + follow" },
|
||||
{ name: "Patrol", desc: "Cycle PatrolPoints" },
|
||||
{ name: "FindCover", desc: "Manual + procedural cover" },
|
||||
{ name: "CoverShootCycle", desc: "Cover → peek → fire cycle" },
|
||||
{ name: "Attack", desc: "Chase + attack (Melee/Ranged)" },
|
||||
{ name: "FleeFrom", desc: "Move away from threat" },
|
||||
];
|
||||
tasks.forEach((task, i) => {
|
||||
const y = 2.55 + i * 0.58;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 7, y: y, w: 2.3, h: 0.45, fill: { color: TERRA }, rectRadius: 0.06 });
|
||||
s.addText(task.name, { x: 7, y: y, w: 2.3, h: 0.45, fontSize: 11, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
s.addText(task.desc, { x: 9.4, y: y, w: 3.5, h: 0.45, fontSize: 10, color: CHARCOAL, valign: "middle", fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 14 — COVER SYSTEM
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Cover System", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Manual CoverPoints + procedural raycast fallback + EQS refinement.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
const coverCols = [
|
||||
{
|
||||
title: "Manual CoverPoints",
|
||||
items: ["Placed by level designer", "Type: Cover or HidingSpot", "Quality score (0–1)", "MaxOccupants, Crouch flag", "Type filter (Any / Civ / Enemy / Protector)"]
|
||||
},
|
||||
{
|
||||
title: "Procedural",
|
||||
items: ["Fallback if no manual points", "Raycast sampling around threat", "LOS blockage test", "Advancement bias toward threat"]
|
||||
},
|
||||
{
|
||||
title: "EQS Refinement",
|
||||
items: ["OnCircle generator around cover", "LineOfSight filter", "Distance + quality tests", "Optional firing position refine"]
|
||||
},
|
||||
];
|
||||
coverCols.forEach((c, i) => {
|
||||
const x = 0.5 + i * 4.28;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: 2.2, w: 4.08, h: 3.1, fill: { color: SAND }, line: { color: TERRA, width: 1 }, rectRadius: 0.1 });
|
||||
s.addText(c.title, { x: x + 0.2, y: 2.3, w: 3.78, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: x + 0.2, y: 2.72, w: 0.4, h: 0.04, fill: { color: GOLD } });
|
||||
s.addText(c.items.map(t => ({ text: t, options: { bullet: true, fontSize: 11 } })),
|
||||
{ x: x + 0.2, y: 2.85, w: 3.78, h: 2.3, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 3 });
|
||||
});
|
||||
|
||||
// Cover cycle state machine
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.55, w: 12.3, h: 1.45, fill: { color: TERRA }, rectRadius: 0.1 });
|
||||
s.addText("Cover Cycle State Machine (CombatSubState)", { x: 0.7, y: 5.65, w: 12, h: 0.35, fontSize: 14, bold: true, color: GOLD, fontFace: FONT_H });
|
||||
|
||||
const subStates = ["Engaging", "AtCover", "MovingToFire", "Peeking", "ReturningToCover", "Advancing"];
|
||||
subStates.forEach((sub, i) => {
|
||||
const x = 0.7 + i * 2.0;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: 6.15, w: 1.9, h: 0.7, fill: { color: TERRA_D }, rectRadius: 0.05 });
|
||||
s.addText(sub, { x: x, y: 6.15, w: 1.9, h: 0.7, fontSize: 11, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 15 — INTERFACE (IMPLEMENTERS GUIDE)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("IPS_AI_Behavior_Interface", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("What your Pawn must implement (C++ or Blueprint).", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
const ifCats = [
|
||||
{
|
||||
title: "Identity",
|
||||
items: ["GetBehaviorNPCType()", "SetBehaviorNPCType()", "IsBehaviorHostile()", "SetBehaviorHostile()"]
|
||||
},
|
||||
{
|
||||
title: "Movement",
|
||||
items: ["SetBehaviorMovementSpeed()", "GetBehaviorMovementSpeed()", "SetBehaviorCrouch()"]
|
||||
},
|
||||
{
|
||||
title: "State Feedback",
|
||||
items: ["OnBehaviorStateChanged()", "IsTargetActorValid()", "GetBehaviorThreatActor()"]
|
||||
},
|
||||
{
|
||||
title: "Combat",
|
||||
items: ["BehaviorStartAttack()", "BehaviorStopAttack()", "CanBehaviorAttack()", "GetBehaviorCombatType()"]
|
||||
},
|
||||
];
|
||||
ifCats.forEach((c, i) => {
|
||||
const col = i % 2;
|
||||
const row = Math.floor(i / 2);
|
||||
const x = 0.5 + col * 6.3;
|
||||
const y = 2.2 + row * 2.4;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 6.0, h: 2.2, fill: { color: TERRA }, rectRadius: 0.1 });
|
||||
s.addText(c.title, { x: x + 0.2, y: y + 0.15, w: 5.6, h: 0.4, fontSize: 16, bold: true, color: GOLD, fontFace: FONT_H });
|
||||
s.addText(c.items.map(t => ({ text: t, options: { bullet: true, fontSize: 12 } })),
|
||||
{ x: x + 0.2, y: y + 0.6, w: 5.6, h: 1.55, color: SAND, fontFace: "Consolas", paraSpaceAfter: 3 });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 16 — SETUP WORKFLOW
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Setup Workflow", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("From zero to a behaving NPC in 6 steps.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
const bsteps = [
|
||||
{ num: "1", title: "Personality Profile", desc: "Create DataAsset — traits, thresholds, DefaultBehaviorTree." },
|
||||
{ num: "2", title: "Implement Interface", desc: "Pawn: implement IPS_AI_Behavior_Interface (identity, combat, movement)." },
|
||||
{ num: "3", title: "Add Components", desc: "Pawn: add PersonalityComponent. Optional: Combat, SplineFollower." },
|
||||
{ num: "4", title: "Behavior Tree", desc: "Branch on BehaviorState. Add services UpdateThreat + EvaluateReaction at root." },
|
||||
{ num: "5", title: "Set AIController", desc: "Pawn's AIController class = APS_AI_Behavior_AIController." },
|
||||
{ num: "6", title: "Place World Actors", desc: "Optional: SplinePaths, CoverPoints, SplineNetwork (auto)." },
|
||||
];
|
||||
bsteps.forEach((step, i) => {
|
||||
const y = 2.2 + i * 0.78;
|
||||
s.addShape(pres.ShapeType.ellipse, { x: 0.5, y: y, w: 0.7, h: 0.7, fill: { color: GOLD } });
|
||||
s.addText(step.num, { x: 0.5, y: y, w: 0.7, h: 0.7, fontSize: 24, bold: true, color: TERRA, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 1.4, y: y, w: 11.4, h: 0.7, fill: { color: SAND }, line: { color: TERRA, width: 1 }, rectRadius: 0.05 });
|
||||
s.addText(step.title, { x: 1.6, y: y + 0.05, w: 3.5, h: 0.3, fontSize: 13, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText(step.desc, { x: 1.6, y: y + 0.35, w: 11.2, h: 0.35, fontSize: 11, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 17 — BLACKBOARD KEYS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Blackboard Keys", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Auto-created by the AIController — use in your BT decorators and tasks.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
const bbKeys = [
|
||||
{ name: "BehaviorState", type: "Enum", desc: "Current state (Idle, Combat, etc.)" },
|
||||
{ name: "ThreatActor", type: "Object", desc: "Current highest-scored threat" },
|
||||
{ name: "ThreatLocation", type: "Vector", desc: "Last known threat position" },
|
||||
{ name: "ThreatLevel", type: "Float", desc: "Perceived threat (0–1+)" },
|
||||
{ name: "CoverLocation", type: "Vector", desc: "Target cover position" },
|
||||
{ name: "CoverPoint", type: "Object", desc: "Current CoverPoint actor" },
|
||||
{ name: "PatrolIndex", type: "Int", desc: "Current patrol waypoint index" },
|
||||
{ name: "HomeLocation", type: "Vector", desc: "Spawn / home point" },
|
||||
{ name: "CurrentSpline", type: "Object", desc: "Active SplinePath" },
|
||||
{ name: "SplineProgress", type: "Float", desc: "Distance along current spline" },
|
||||
{ name: "CombatSubState", type: "Enum", desc: "Cover cycle sub-state (Peeking, etc.)" },
|
||||
{ name: "LastKnownTargetPosition", type: "Vector", desc: "LOS-lost target memory" },
|
||||
{ name: "PreferCover", type: "Bool", desc: "Combat/cover cycle toggle" },
|
||||
{ name: "ConversationPaused", type: "Bool", desc: "Active dialogue pause flag" },
|
||||
];
|
||||
bbKeys.forEach((k, i) => {
|
||||
const col = i % 2;
|
||||
const row = Math.floor(i / 2);
|
||||
const x = 0.5 + col * 6.3;
|
||||
const y = 2.2 + row * 0.62;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 1.8, h: 0.5, fill: { color: TERRA }, rectRadius: 0.05 });
|
||||
s.addText(k.name, { x: x, y: y, w: 1.8, h: 0.5, fontSize: 10, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: "Consolas" });
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x + 1.85, y: y + 0.1, w: 0.6, h: 0.3, fill: { color: GOLD }, rectRadius: 0.05 });
|
||||
s.addText(k.type, { x: x + 1.85, y: y + 0.1, w: 0.6, h: 0.3, fontSize: 9, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
s.addText(k.desc, { x: x + 2.55, y: y, w: 3.7, h: 0.5, fontSize: 10, color: CHARCOAL, valign: "middle", italic: true, fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 18 — NETWORKING
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Networking", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Server-authoritative AI, minimal replication for visuals.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Server-only", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "All AI planning & decision-making", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Threat evaluation, state transitions", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Perception stimuli processing", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Path selection, cover choice", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Combat targeting", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 0.5, y: 2.55, w: 6, h: 2.7, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Replicated to clients", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "CurrentState (OnRep fires delegate)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "PerceivedThreatLevel (HUD/debug)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "CurrentTarget (anims/visuals)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "CurrentSpline, bIsFollowing", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "OnAttackExecuted (multicast)", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 7, y: 2.55, w: 6, h: 2.7, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.4, w: 12.3, h: 1.6, fill: { color: TERRA }, rectRadius: 0.1 });
|
||||
s.addText("Character movement syncs via CharacterMovementComponent. Spline follower updates server-side, position replicates naturally. Clients receive enough state for animation and HUD without any AI logic running locally.", {
|
||||
x: 0.7, y: 5.6, w: 12, h: 1.2, fontSize: 13, color: SAND, italic: true, fontFace: FONT_B, valign: "middle"
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 19 — INTEGRATION WITH CONVAGENT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Integration with PS_AI_ConvAgent", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 28, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Optional — behavior and conversation plugins talk via reflection.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
const integrations = [
|
||||
{ title: "Gaze Bridge", desc: "AIController discovers GazeComponent on the Pawn, sets target during Idle/Patrol (proximity gaze) and forwards current threat during Combat." },
|
||||
{ title: "Conversation Pause", desc: "When ConvAgent is conversing, AI planning pauses — NPC stops moving, waits. Local VAD detects user speech." },
|
||||
{ title: "Disguise Reveal", desc: "Enemy NPC with IsBehaviorHostile=false starts as Civilian TeamId. Flips to hostile via interface when triggered by dialogue." },
|
||||
{ title: "Forced State", desc: "ConvAgent LLM can invoke tools that call ForceState(Scripted) or ForceState(Fleeing) for narrative control." },
|
||||
];
|
||||
integrations.forEach((item, i) => {
|
||||
const col = i % 2;
|
||||
const row = Math.floor(i / 2);
|
||||
const x = 0.5 + col * 6.3;
|
||||
const y = 2.2 + row * 2.3;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 6.0, h: 2.1, fill: { color: SAND }, line: { color: TERRA, width: 1 }, rectRadius: 0.1 });
|
||||
s.addText(item.title, { x: x + 0.2, y: y + 0.15, w: 5.6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: x + 0.2, y: y + 0.58, w: 0.4, h: 0.04, fill: { color: GOLD } });
|
||||
s.addText(item.desc, { x: x + 0.2, y: y + 0.7, w: 5.6, h: 1.3, fontSize: 12, color: CHARCOAL, fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 20 — DEBUG TOOLS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Debug Tools", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: GOLD } });
|
||||
s.addText("Visualize state, perception, and targeting at runtime.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: CHARCOAL, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Per-NPC Debug", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "PersonalityComponent.bDebug — floating text", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Name, NPCType, TeamId, State", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "ThreatLevel, ThreatActor", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Hostile, active spline", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "SplineFollower debug sphere (green)", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 0.5, y: 2.55, w: 6, h: 2.8, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Global Logging", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: TERRA, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "CVar: ps.ai.Behavior.Debug 1", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Toggles verbose logs globally", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "LogPS_AI_Behavior category", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "BTView / AIDebug (Unreal built-in)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "EQS Visualizer for cover queries", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 7, y: 2.55, w: 6, h: 2.8, color: CHARCOAL, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.5, w: 12.3, h: 1.5, fill: { color: TERRA }, rectRadius: 0.1 });
|
||||
s.addText("Pro tip", { x: 0.7, y: 5.6, w: 12, h: 0.35, fontSize: 14, bold: true, color: GOLD, fontFace: FONT_H });
|
||||
s.addText("Enable bDebug only on specific NPCs (or via a debug HUD key). Global debug logs can be overwhelming in crowded scenes. The floating text is cheap but accumulates.",
|
||||
{ x: 0.7, y: 5.95, w: 12, h: 1.0, fontSize: 12, color: SAND, italic: true, fontFace: FONT_B });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 21 — SUMMARY
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide();
|
||||
s.background = { color: TERRA };
|
||||
s.addShape(pres.ShapeType.rect, { x: 0, y: 2.5, w: 13.333, h: 0.05, fill: { color: GOLD } });
|
||||
s.addText("Summary", { x: 0.6, y: 0.8, w: 12, h: 1.2, fontSize: 54, bold: true, color: WHITE, fontFace: FONT_H });
|
||||
s.addText("Complete NPC behavior stack for tactical game AI.", { x: 0.6, y: 2.0, w: 12, h: 0.5, fontSize: 18, color: SAND, italic: true, fontFace: FONT_B });
|
||||
|
||||
const bsummary = [
|
||||
{ h: "Personality-driven", t: "Three traits modulate every decision" },
|
||||
{ h: "Cover-aware", t: "Manual CoverPoints + procedural fallback + EQS" },
|
||||
{ h: "Spline navigation", t: "Network with junction detection, type filtering" },
|
||||
{ h: "Perception built-in", t: "Sight + hearing + damage, priority-based threat" },
|
||||
{ h: "Server-authoritative", t: "Minimal replication, client just animates" },
|
||||
{ h: "Plugin-friendly", t: "Gaze bridge to ConvAgent, interface-only dependency" },
|
||||
];
|
||||
bsummary.forEach((item, i) => {
|
||||
const col = i % 2;
|
||||
const row = Math.floor(i / 2);
|
||||
const x = 0.6 + col * 6.2;
|
||||
const y = 3.0 + row * 1.4;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 5.9, h: 1.2, fill: { color: TERRA_D }, line: { color: GOLD, width: 1 }, rectRadius: 0.1 });
|
||||
s.addText(item.h, { x: x + 0.2, y: y + 0.1, w: 5.5, h: 0.4, fontSize: 18, bold: true, color: GOLD, fontFace: FONT_H });
|
||||
s.addText(item.t, { x: x + 0.2, y: y + 0.55, w: 5.5, h: 0.6, fontSize: 12, color: SAND, italic: true, fontFace: FONT_B });
|
||||
});
|
||||
|
||||
s.addText("ASTERION • PS_AI_Behavior • 2026", { x: 0.6, y: 6.9, w: 12, h: 0.4, fontSize: 12, color: SAND, fontFace: FONT_B });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SAVE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
pres.writeFile({ fileName: "PS_AI_Behavior_Documentation.pptx" })
|
||||
.then(name => console.log(`Generated: ${name}`))
|
||||
.catch(err => { console.error(err); process.exit(1); });
|
||||
711
Docs/gen_convagent_doc.js
Normal file
711
Docs/gen_convagent_doc.js
Normal file
@@ -0,0 +1,711 @@
|
||||
// PS_AI_ConvAgent User Documentation Generator
|
||||
// Palette: Midnight Executive (navy #1E2761, ice blue #CADCFC, white)
|
||||
|
||||
const pptxgen = require("pptxgenjs");
|
||||
const pres = new pptxgen();
|
||||
|
||||
pres.layout = "LAYOUT_WIDE"; // 13.333 x 7.5 inch
|
||||
|
||||
// ─── Color Palette ─────────────────────────────────
|
||||
const NAVY = "1E2761";
|
||||
const NAVY_DARK = "141B45";
|
||||
const ICE = "CADCFC";
|
||||
const ICE_DEEP = "7892D6";
|
||||
const WHITE = "FFFFFF";
|
||||
const GREY = "3A3F5C";
|
||||
const LIGHT_GREY= "F5F7FC";
|
||||
const ACCENT = "F9A825"; // warm gold accent
|
||||
|
||||
// ─── Font ──────────────────────────────────────────
|
||||
const FONT_H = "Calibri";
|
||||
const FONT_B = "Calibri";
|
||||
|
||||
// ─── Master slide backgrounds ──────────────────────
|
||||
pres.defineSlideMaster({
|
||||
title: "CONTENT",
|
||||
background: { color: LIGHT_GREY },
|
||||
objects: [
|
||||
{ rect: { x: 0, y: 0, w: 13.333, h: 0.35, fill: { color: NAVY } } },
|
||||
{ rect: { x: 0, y: 7.15, w: 13.333, h: 0.35, fill: { color: NAVY } } },
|
||||
{ text: {
|
||||
text: "PS_AI_ConvAgent — User Documentation",
|
||||
options: { x: 0.3, y: 7.18, w: 8, h: 0.3, fontSize: 10, color: ICE, fontFace: FONT_B }
|
||||
} },
|
||||
{ text: {
|
||||
text: "ASTERION",
|
||||
options: { x: 11.5, y: 7.18, w: 1.5, h: 0.3, fontSize: 10, color: ICE, fontFace: FONT_B, align: "right", bold: true }
|
||||
} },
|
||||
],
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 1 — TITLE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
let s = pres.addSlide();
|
||||
s.background = { color: NAVY };
|
||||
s.addShape(pres.ShapeType.rect, { x: 0, y: 3.3, w: 13.333, h: 0.05, fill: { color: ACCENT } });
|
||||
s.addText("PS_AI_ConvAgent", { x: 0.6, y: 2.2, w: 12, h: 1.2, fontSize: 60, bold: true, color: WHITE, fontFace: FONT_H });
|
||||
s.addText("Conversational AI for MetaHumans in Unreal Engine", { x: 0.6, y: 3.5, w: 12, h: 0.6, fontSize: 22, color: ICE, fontFace: FONT_B, italic: true });
|
||||
s.addText("User Documentation", { x: 0.6, y: 4.4, w: 12, h: 0.5, fontSize: 18, color: ICE_DEEP, fontFace: FONT_B });
|
||||
s.addText("ElevenLabs • MetaHuman • Listen Server", { x: 0.6, y: 5.2, w: 12, h: 0.4, fontSize: 14, color: ICE_DEEP, fontFace: FONT_B });
|
||||
s.addText("ASTERION • 2026", { x: 0.6, y: 6.6, w: 12, h: 0.4, fontSize: 12, color: ICE_DEEP, fontFace: FONT_B });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 2 — OVERVIEW
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Plugin Overview", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
|
||||
s.addText("What it does", { x: 0.5, y: 1.7, w: 6, h: 0.4, fontSize: 18, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Full conversational AI NPC integration for Unreal Engine", options: { bullet: true, fontSize: 14 } },
|
||||
{ text: "Real-time voice capture, turn-taking, TTS playback", options: { bullet: true, fontSize: 14 } },
|
||||
{ text: "Emotion-driven MetaHuman animations (face, body, gaze)", options: { bullet: true, fontSize: 14 } },
|
||||
{ text: "Multiplayer support with speaker arbitration", options: { bullet: true, fontSize: 14 } },
|
||||
{ text: "Tool system for LLM-triggered game actions", options: { bullet: true, fontSize: 14 } },
|
||||
], { x: 0.5, y: 2.1, w: 6, h: 2.6, color: GREY, fontFace: FONT_B, paraSpaceAfter: 8 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 7, y: 1.7, w: 5.8, h: 4.8, fill: { color: NAVY }, rectRadius: 0.1 });
|
||||
s.addText("Primary Use Case", { x: 7.2, y: 1.9, w: 5.4, h: 0.4, fontSize: 16, bold: true, color: ACCENT, fontFace: FONT_H });
|
||||
s.addText("Interactive, emotionally expressive voice NPCs that listen to players, respond with contextual answers, express emotion through facial animation and body language, and trigger physical actions based on conversation flow.", {
|
||||
x: 7.2, y: 2.4, w: 5.4, h: 1.7, fontSize: 13, color: ICE, fontFace: FONT_B, italic: true
|
||||
});
|
||||
s.addText("Key Differentiators", { x: 7.2, y: 4.2, w: 5.4, h: 0.4, fontSize: 16, bold: true, color: ACCENT, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Full-duplex Server VAD or push-to-talk", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Adaptive pre-buffering & speculative TTS", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Spectral lip sync + artist-driven poses", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Opus codec + distance LOD for multiplayer", options: { bullet: true, fontSize: 12 } },
|
||||
], { x: 7.2, y: 4.7, w: 5.4, h: 1.7, color: ICE, fontFace: FONT_B, paraSpaceAfter: 4 });
|
||||
|
||||
s.addText("\"Drop components on a MetaHuman, configure a few data assets, and talk.\"", {
|
||||
x: 0.5, y: 5.3, w: 6, h: 0.6, fontSize: 13, color: NAVY, italic: true, fontFace: FONT_B, bold: true
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 3 — ARCHITECTURE DIAGRAM
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Architecture", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
|
||||
s.addText("Component chain on a MetaHuman NPC", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
// Input row
|
||||
const boxY = 2.4;
|
||||
const boxH = 1.0;
|
||||
const boxes = [
|
||||
{ x: 0.5, label: "Microphone\nCapture", sub: "VAD • 16kHz" },
|
||||
{ x: 2.6, label: "Interaction\nComponent", sub: "Agent select\n(player side)" },
|
||||
{ x: 4.7, label: "ElevenLabs\nComponent", sub: "WebSocket\nTurn-taking" },
|
||||
{ x: 6.8, label: "Lip Sync\nComponent", sub: "Spectral\nFFT → visemes" },
|
||||
{ x: 8.9, label: "Facial Expr.\nComponent", sub: "Emotion\ncurves" },
|
||||
{ x: 11.0, label: "Body Expr.\nComponent", sub: "Gesture\nanim blend" },
|
||||
];
|
||||
boxes.forEach((b, i) => {
|
||||
s.addShape(pres.ShapeType.roundRect, { x: b.x, y: boxY, w: 2.0, h: boxH, fill: { color: NAVY }, rectRadius: 0.1 });
|
||||
s.addText(b.label, { x: b.x, y: boxY + 0.1, w: 2.0, h: 0.5, fontSize: 13, bold: true, color: WHITE, align: "center", fontFace: FONT_H });
|
||||
s.addText(b.sub, { x: b.x, y: boxY + 0.58, w: 2.0, h: 0.4, fontSize: 9, color: ICE, align: "center", fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// Arrows between boxes
|
||||
for (let i = 0; i < boxes.length - 1; i++) {
|
||||
const ax = boxes[i].x + 2.0;
|
||||
s.addShape(pres.ShapeType.line, { x: ax, y: boxY + 0.5, w: 0.1, h: 0, line: { color: ACCENT, width: 2 } });
|
||||
}
|
||||
|
||||
// Gaze row
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 4.0, w: 2.0, h: boxH, fill: { color: ICE_DEEP }, rectRadius: 0.1 });
|
||||
s.addText("Gaze\nComponent", { x: 0.5, y: 4.1, w: 2.0, h: 0.5, fontSize: 13, bold: true, color: WHITE, align: "center", fontFace: FONT_H });
|
||||
s.addText("Eyes • Head\n• Body", { x: 0.5, y: 4.58, w: 2.0, h: 0.4, fontSize: 9, color: WHITE, align: "center", fontFace: FONT_B });
|
||||
|
||||
// AnimGraph pipeline
|
||||
s.addText("→ AnimGraph (Face + Body)", { x: 2.8, y: 4.2, w: 4.5, h: 0.5, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText("FacialExpression → LipSync | Gaze → BodyExpression", { x: 2.8, y: 4.65, w: 8, h: 0.4, fontSize: 12, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
// Project settings layer
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.6, w: 12.3, h: 0.9, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 });
|
||||
s.addText("PROJECT SETTINGS — API Key • Server Region • Global Context Prompt", { x: 0.7, y: 5.75, w: 12, h: 0.3, fontSize: 13, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText("Data Assets: AgentConfig • EmotionPoseMap • LipSyncPoseMap • BodyPoseMap • ActionSet • Tool", { x: 0.7, y: 6.1, w: 12, h: 0.3, fontSize: 11, color: GREY, fontFace: FONT_B });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 4 — ELEVENLABS COMPONENT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("ElevenLabs Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("The core conversation engine — WebSocket, turn-taking, audio pipeline.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Key Properties", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "AgentConfig — identity, voice, tools", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "TurnMode — ServerVAD / Client (push-to-talk)", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "MicChunkDurationMs (50–250 ms)", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "AudioPreBufferMs / bAdaptivePreBuffer", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "bAllowInterruption, bPersistentSession", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "SoundAttenuation (optional 3D audio)", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "AudioLODCullDistance / LipSyncLODDistance", options: { bullet: true, fontSize: 12 } },
|
||||
], { x: 0.5, y: 2.55, w: 6, h: 3.6, color: GREY, fontFace: FONT_B, paraSpaceAfter: 4 });
|
||||
|
||||
s.addText("Key Methods", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "StartConversation() / EndConversation()", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "StartListening() / StopListening()", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "SendTextMessage() — text without voice", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "InterruptAgent() — force stop mid-speech", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "ForceDisableConversation(Action) — blend to neutral", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "ForceEnableConversation() — re-enable", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "FeedExternalAudio() — external mic source", options: { bullet: true, fontSize: 12 } },
|
||||
], { x: 7, y: 2.55, w: 6, h: 3.6, color: GREY, fontFace: FONT_B, paraSpaceAfter: 4 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 6.2, w: 12.3, h: 0.75, fill: { color: NAVY }, rectRadius: 0.08 });
|
||||
s.addText("Major events: OnAgentConnected • OnAgentTranscript • OnAgentTextResponse • OnAgentStartedSpeaking • OnAgentEmotionChanged • OnAgentActionRequested • OnActiveSpeakerChanged • OnReadyForAction", { x: 0.7, y: 6.3, w: 12, h: 0.55, fontSize: 11, color: ICE, fontFace: FONT_B, italic: true });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 5 — MICROPHONE CAPTURE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Microphone Capture Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Captures, resamples, and performs local voice activity detection.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Audio Pipeline", { x: 0.5, y: 2.2, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "WASAPI 48kHz stereo float32 input", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Resample to 16kHz mono float", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Convert to int16 LE byte stream", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Delivered via multicast delegate", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 0.5, y: 2.6, w: 6, h: 2.4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 6 });
|
||||
|
||||
s.addText("Voice Activity Detection", { x: 7, y: 2.2, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "VolumeMultiplier — mic gain", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "VoiceActivityThreshold — RMS threshold", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "VoiceOnsetTime — debounce on speech start", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "VoiceSilenceTime — debounce on end", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "OnUserVoiceActivityChanged event", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 7, y: 2.6, w: 6, h: 2.4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 6 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.4, w: 12.3, h: 1.4, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 });
|
||||
s.addText("Public API", { x: 0.7, y: 5.5, w: 12, h: 0.3, fontSize: 13, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText("StartCapture() • StopCapture() • IsCapturing()", { x: 0.7, y: 5.85, w: 12, h: 0.3, fontSize: 13, color: GREY, fontFace: "Consolas" });
|
||||
s.addText("The component auto-opens the default input device. Output is consumed by ElevenLabsComponent (or InteractionComponent, which relays via Server RPC in networked games).", { x: 0.7, y: 6.2, w: 12, h: 0.55, fontSize: 11, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 6 — LIP SYNC
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Lip Sync Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Spectral analysis of agent audio → 15 OVR visemes → ARKit blendshapes.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("How it works", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "FFT on agent audio stream", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Estimates 15 OVR viseme weights", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Maps to ARKit blendshapes (jawOpen, mouthFunnel…)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Optional: custom PoseMap with artist-crafted poses", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Thread-safe curve output for AnimGraph", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 0.5, y: 2.55, w: 6, h: 3.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Key Properties", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "LipSyncStrength — overall intensity", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "SmoothingSpeed (35–65) — viseme interp", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "SpeechBlendDuration — fade in/out", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "EmotionExpressionBlend (0–1) — emotion bleed through mouth", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "EnvelopeAttackMs / EnvelopeReleaseMs", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "PoseMap — optional artist override", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 7, y: 2.55, w: 6, h: 3.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 6.2, w: 12.3, h: 0.75, fill: { color: NAVY }, rectRadius: 0.08 });
|
||||
s.addText("AnimGraph placement (Face AnimBP): Live Link → FacialExpression → LipSync → mh_arkit_mapping_pose", { x: 0.7, y: 6.3, w: 12, h: 0.55, fontSize: 12, color: ICE, fontFace: FONT_B, italic: true, bold: true });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 7 — FACIAL EXPRESSION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Facial Expression Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Emotion-driven face animation via real-time AnimSequence sampling.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Design", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Plays AnimSequences in real-time (looping)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Extracts emotion curves (eyes, brows, cheeks, mouth mood)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Injects into AnimGraph via FacialExpression AnimNode", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Lip sync overrides mouth area during speech", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "3 intensity levels: Normal / Medium / Extreme", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Crossfades between emotions", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 0.5, y: 2.55, w: 6, h: 3.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Properties", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "EmotionPoseMap — data asset of AnimSequences", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "ActivationBlendDuration (default 0.5s)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "EmotionBlendDuration — crossfade time", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "bActive — auto-managed by conversation", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 7, y: 2.55, w: 6, h: 2.4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 7, y: 5.0, w: 5.8, h: 1.5, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 });
|
||||
s.addText("Emotions covered", { x: 7.15, y: 5.1, w: 5.6, h: 0.3, fontSize: 13, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText("Joy • Sadness • Anger • Fear • Surprise • Disgust • Neutral", { x: 7.15, y: 5.45, w: 5.6, h: 0.9, fontSize: 12, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 8 — BODY EXPRESSION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Body Expression Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Upper-body gestures and posture driven by emotion + speaking state.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Animation lists per emotion", { x: 0.5, y: 2.15, w: 6.3, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
|
||||
const animLists = [
|
||||
{ name: "Idle", desc: "Played while listening (not speaking)" },
|
||||
{ name: "Normal", desc: "Low-intensity speaking anims" },
|
||||
{ name: "Medium", desc: "Medium-intensity speaking" },
|
||||
{ name: "Extreme", desc: "High-intensity speaking" },
|
||||
];
|
||||
animLists.forEach((a, i) => {
|
||||
const y = 2.7 + i * 0.75;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 1.5, h: 0.55, fill: { color: NAVY }, rectRadius: 0.05 });
|
||||
s.addText(a.name, { x: 0.5, y: y, w: 1.5, h: 0.55, fontSize: 13, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
s.addText(a.desc, { x: 2.15, y: y + 0.05, w: 4.6, h: 0.45, fontSize: 12, color: GREY, valign: "middle", fontFace: FONT_B });
|
||||
});
|
||||
|
||||
s.addText("Key Properties", { x: 7.2, y: 2.15, w: 5.8, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "BodyPoseMap — emotion → anim lists", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "ActivationBlendDuration (0.5s)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "EmotionBlendDuration (1.0s)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "BlendWeight (0–1)", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 7.2, y: 2.55, w: 5.8, h: 2, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("AnimNode behaviour", { x: 7.2, y: 4.7, w: 5.8, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Override mode, per-bone mask", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Default BlendRootBone = spine_02", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Excludes neck_01 (Gaze handles it)", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "Lower body passes through upstream", options: { bullet: true, fontSize: 12 } },
|
||||
], { x: 7.2, y: 5.1, w: 5.8, h: 2, color: GREY, fontFace: FONT_B, paraSpaceAfter: 3 });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 9 — GAZE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Gaze Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Multi-layer look-at: body • head • eyes.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
const layers = [
|
||||
{ name: "BODY", ratio: "full 360°", desc: "Actor yaw — follows target around the character" },
|
||||
{ name: "HEAD", ratio: "limited", desc: "Head bone yaw + pitch, clamped" },
|
||||
{ name: "EYES", ratio: "limited", desc: "ARKit eye curves, fine targeting" },
|
||||
];
|
||||
layers.forEach((l, i) => {
|
||||
const y = 2.3 + i * 1.1;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 2.2, h: 0.9, fill: { color: NAVY }, rectRadius: 0.08 });
|
||||
s.addText(l.name, { x: 0.5, y: y + 0.05, w: 2.2, h: 0.4, fontSize: 18, bold: true, color: WHITE, align: "center", fontFace: FONT_H });
|
||||
s.addText(l.ratio, { x: 0.5, y: y + 0.48, w: 2.2, h: 0.3, fontSize: 11, color: ACCENT, align: "center", italic: true, fontFace: FONT_B });
|
||||
s.addText(l.desc, { x: 3.0, y: y + 0.15, w: 5.0, h: 0.7, fontSize: 13, color: GREY, valign: "middle", fontFace: FONT_B });
|
||||
});
|
||||
|
||||
s.addText("Key Properties", { x: 8.5, y: 2.15, w: 4.5, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "TargetActor — who to look at", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "MaxHeadYaw / MaxHeadPitch", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "MaxEyeHorizontal / MaxEyeVertical", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "BodyInterpSpeed / HeadInterpSpeed / EyeInterpSpeed", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "HeadAnimationCompensation (0.9)", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "EyeAnimationCompensation (0.6)", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "BodyDriftCompensation (0.8)", options: { bullet: true, fontSize: 12 } },
|
||||
{ text: "NeckBoneChain — multi-bone distribution", options: { bullet: true, fontSize: 12 } },
|
||||
], { x: 8.5, y: 2.55, w: 4.5, h: 4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 3 });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 10 — INTERACTION COMPONENT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Interaction Component (Player)", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 32, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Placed on the player pawn — selects NPCs, routes mic, manages gaze.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Selection Logic", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Proximity + view cone detection", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "MaxInteractionDistance (300 cm default)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "ViewConeHalfAngle (45° default)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "SelectionStickyAngle — hysteresis", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "ConversationSwitchDelay — anti-flicker", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 0.5, y: 2.55, w: 6, h: 3, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Auto-management", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "bAutoStartConversation — open WS on select", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "bAutoManageListening — mic open/close", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "bAutoManageGaze — set agent gaze to player", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "bAutoPassiveGaze — out-of-range passive tracking", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 7, y: 2.55, w: 6, h: 2.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.8, w: 12.3, h: 1.2, fill: { color: NAVY }, rectRadius: 0.08 });
|
||||
s.addText("Events: OnAgentSelected • OnAgentDeselected • OnNoAgentInRange", { x: 0.7, y: 5.95, w: 12, h: 0.3, fontSize: 13, bold: true, color: ACCENT, fontFace: FONT_H });
|
||||
s.addText("Methods: GetSelectedAgent() • ForceSelectAgent() • StartConversationWithSelectedAgent() • SendTextToSelectedAgent() • ClearSelection()", { x: 0.7, y: 6.3, w: 12, h: 0.55, fontSize: 11, color: ICE, fontFace: FONT_B });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 11 — DATA ASSETS OVERVIEW
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Data Assets", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Reusable configuration assets for agent identity, animations, and tools.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
const dataAssets = [
|
||||
{ name: "AgentConfig", desc: "Agent identity, voice, LLM model, tools", color: NAVY },
|
||||
{ name: "EmotionPoseMap", desc: "Emotion → facial AnimSequences (3 levels)", color: NAVY },
|
||||
{ name: "LipSyncPoseMap", desc: "15 OVR visemes → mouth poses", color: NAVY },
|
||||
{ name: "BodyPoseMap", desc: "Emotion → body gesture anim lists", color: NAVY },
|
||||
{ name: "ActionSet", desc: "Actions the agent can trigger", color: ICE_DEEP },
|
||||
{ name: "Tool", desc: "Custom LLM tools with parameters", color: ICE_DEEP },
|
||||
];
|
||||
dataAssets.forEach((d, i) => {
|
||||
const col = i % 2;
|
||||
const row = Math.floor(i / 2);
|
||||
const x = 0.5 + col * 6.3;
|
||||
const y = 2.2 + row * 1.5;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 6.0, h: 1.3, fill: { color: d.color }, rectRadius: 0.1 });
|
||||
s.addText(d.name, { x: x + 0.2, y: y + 0.2, w: 5.6, h: 0.4, fontSize: 18, bold: true, color: ACCENT, fontFace: FONT_H });
|
||||
s.addText(d.desc, { x: x + 0.2, y: y + 0.65, w: 5.6, h: 0.6, fontSize: 12, color: ICE, italic: true, fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 12 — AGENT CONFIG DETAILS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("AgentConfig (ElevenLabs)", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Complete agent definition — editable in Content Browser with API buttons.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
const configCats = [
|
||||
{ title: "Identity", items: ["AgentID", "AgentName"] },
|
||||
{ title: "Voice", items: ["VoiceID / VoiceName", "TTSModelID", "Stability • Similarity • Speed"] },
|
||||
{ title: "LLM", items: ["LLMModel (gemini-2.5-flash default)", "Language (en default)"] },
|
||||
{ title: "Behavior", items: ["CharacterPrompt (personality)", "FirstMessage (greeting)", "TurnTimeout, MaxTurns"] },
|
||||
{ title: "Latency", items: ["TurnEagerness (Eager/Normal/Patient)", "bSpeculativeTurn"] },
|
||||
{ title: "Tools", items: ["Tools[] array of Tool assets", "ActionSet integration"] },
|
||||
];
|
||||
|
||||
configCats.forEach((c, i) => {
|
||||
const col = i % 3;
|
||||
const row = Math.floor(i / 3);
|
||||
const x = 0.5 + col * 4.15;
|
||||
const y = 2.2 + row * 2.15;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 3.95, h: 1.95, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 });
|
||||
s.addText(c.title, { x: x + 0.15, y: y + 0.1, w: 3.65, h: 0.35, fontSize: 14, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: x + 0.15, y: y + 0.45, w: 0.35, h: 0.04, fill: { color: ACCENT } });
|
||||
s.addText(c.items.map(t => ({ text: t, options: { bullet: true, fontSize: 11 } })),
|
||||
{ x: x + 0.15, y: y + 0.55, w: 3.65, h: 1.3, color: GREY, fontFace: FONT_B, paraSpaceAfter: 2 });
|
||||
});
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 6.55, w: 12.3, h: 0.45, fill: { color: NAVY }, rectRadius: 0.05 });
|
||||
s.addText("Editor: Voice picker • Model picker • LLM picker • Create / Update / Fetch Agent buttons", { x: 0.7, y: 6.6, w: 12, h: 0.35, fontSize: 11, color: ICE, fontFace: FONT_B, italic: true, bold: true });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 13 — POSE MAPS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Pose Maps", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Three pose-map data assets map abstract states → AnimSequences.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
// Emotion
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 2.15, w: 4.0, h: 4.5, fill: { color: NAVY }, rectRadius: 0.1 });
|
||||
s.addText("EmotionPoseMap", { x: 0.6, y: 2.3, w: 3.8, h: 0.4, fontSize: 17, bold: true, color: ACCENT, fontFace: FONT_H });
|
||||
s.addText("Emotion → facial AnimSequences\n(3 intensity levels per emotion)", { x: 0.6, y: 2.7, w: 3.8, h: 0.8, fontSize: 11, color: ICE, italic: true, fontFace: FONT_B });
|
||||
s.addText([
|
||||
{ text: "Joy (N / M / E)", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Sadness (N / M / E)", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Anger (N / M / E)", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Fear (N / M / E)", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Surprise (N / M / E)", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Disgust (N / M / E)", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Neutral (N / M / E)", options: { bullet: true, fontSize: 11 } },
|
||||
], { x: 0.7, y: 3.7, w: 3.7, h: 2.8, color: ICE, fontFace: FONT_B, paraSpaceAfter: 3 });
|
||||
|
||||
// LipSync
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 4.7, y: 2.15, w: 4.0, h: 4.5, fill: { color: NAVY }, rectRadius: 0.1 });
|
||||
s.addText("LipSyncPoseMap", { x: 4.8, y: 2.3, w: 3.8, h: 0.4, fontSize: 17, bold: true, color: ACCENT, fontFace: FONT_H });
|
||||
s.addText("15 OVR visemes →\nphoneme AnimSequences", { x: 4.8, y: 2.7, w: 3.8, h: 0.7, fontSize: 11, color: ICE, italic: true, fontFace: FONT_B });
|
||||
s.addText([
|
||||
{ text: "sil • PP • FF • TH • DD", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "kk • CH • SS • nn • RR", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "aa • E • ih • oh • ou", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "MetaHuman MHF_* recommended", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Shareable across characters", options: { bullet: true, fontSize: 11 } },
|
||||
], { x: 4.9, y: 3.5, w: 3.7, h: 2.9, color: ICE, fontFace: FONT_B, paraSpaceAfter: 4 });
|
||||
|
||||
// Body
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 8.9, y: 2.15, w: 4.0, h: 4.5, fill: { color: NAVY }, rectRadius: 0.1 });
|
||||
s.addText("BodyPoseMap", { x: 9.0, y: 2.3, w: 3.8, h: 0.4, fontSize: 17, bold: true, color: ACCENT, fontFace: FONT_H });
|
||||
s.addText("Emotion → body anim lists\n(Idle / N / M / E per emotion)", { x: 9.0, y: 2.7, w: 3.8, h: 0.8, fontSize: 11, color: ICE, italic: true, fontFace: FONT_B });
|
||||
s.addText([
|
||||
{ text: "Idle: listening animations", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Normal: light speaking", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Medium: expressive speaking", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Extreme: emphatic speaking", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Random selection per loop", options: { bullet: true, fontSize: 11 } },
|
||||
{ text: "Continuous variety", options: { bullet: true, fontSize: 11 } },
|
||||
], { x: 9.0, y: 3.7, w: 3.8, h: 2.8, color: ICE, fontFace: FONT_B, paraSpaceAfter: 3 });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 14 — TOOLS & ACTIONSETS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Tools & ActionSets", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("LLM-triggered actions that bridge conversation → gameplay.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Tool asset", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "ToolName — snake_case identifier", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "ToolDescription — when LLM should use it", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "PromptFragment — appended system prompt", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Parameters[] — typed LLM arguments", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "ToolID auto-populated on Create", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 0.5, y: 2.55, w: 6, h: 2.8, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Built-in Tools", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "set_emotion(emotion, intensity)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "perform_action(action) — from ActionSet", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Custom tools — any parameters", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 7, y: 2.55, w: 6, h: 2, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.4, w: 12.3, h: 1.6, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 });
|
||||
s.addText("Action flow", { x: 0.7, y: 5.5, w: 12, h: 0.3, fontSize: 14, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText("1. LLM decides to call a tool (e.g. perform_action(\"flee\"))\n2. Plugin fires OnAgentActionRequested(\"flee\")\n3. Game calls ForceDisableConversation(\"flee\") → animations blend to neutral\n4. OnReadyForAction(\"flee\") fires → play gameplay montage\n5. Game calls ForceEnableConversation() when done", { x: 0.7, y: 5.85, w: 12, h: 1.1, fontSize: 11, color: GREY, fontFace: "Consolas" });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 15 — ANIMGRAPH INTEGRATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("AnimGraph Integration", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Four AnimNodes auto-discover components — no manual wiring.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
// Face AnimBP chain
|
||||
s.addText("Face AnimBP", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
const faceChain = [
|
||||
{ label: "Live Link", color: ICE_DEEP },
|
||||
{ label: "Facial\nExpression", color: "E88B3E" },
|
||||
{ label: "Gaze\n(eyes only)", color: "5E7AC7" },
|
||||
{ label: "Lip Sync", color: "3EB086" },
|
||||
{ label: "mh_arkit\n_mapping", color: GREY },
|
||||
];
|
||||
faceChain.forEach((n, i) => {
|
||||
const x = 0.5 + i * 2.55;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: 2.65, w: 2.3, h: 1.0, fill: { color: n.color }, rectRadius: 0.1 });
|
||||
s.addText(n.label, { x: x, y: 2.75, w: 2.3, h: 0.8, fontSize: 12, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
});
|
||||
|
||||
// Body AnimBP chain
|
||||
s.addText("Body AnimBP", { x: 0.5, y: 4.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
const bodyChain = [
|
||||
{ label: "Locomotion\n/ Idle", color: ICE_DEEP },
|
||||
{ label: "Gaze\n(head rot)", color: "5E7AC7" },
|
||||
{ label: "Body\nExpression", color: "B85042" },
|
||||
{ label: "Output Pose", color: GREY },
|
||||
];
|
||||
bodyChain.forEach((n, i) => {
|
||||
const x = 0.5 + i * 3.2;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: 4.65, w: 2.9, h: 1.0, fill: { color: n.color }, rectRadius: 0.1 });
|
||||
s.addText(n.label, { x: x, y: 4.75, w: 2.9, h: 0.8, fontSize: 12, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
});
|
||||
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 6.15, w: 12.3, h: 0.85, fill: { color: NAVY }, rectRadius: 0.08 });
|
||||
s.addText("Why this order?", { x: 0.7, y: 6.2, w: 12, h: 0.3, fontSize: 13, bold: true, color: ACCENT, fontFace: FONT_H });
|
||||
s.addText("Emotion curves set mood (brows, eyes, cheeks). Lip sync overrides mouth during speech. Gaze head rotation runs before body expression so it doesn't fight arms.", { x: 0.7, y: 6.55, w: 12, h: 0.4, fontSize: 11, color: ICE, italic: true, fontFace: FONT_B });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 16 — PROJECT SETTINGS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Project Settings", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Project Settings → Plugins → PS AI ConvAgent - ElevenLabs", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
const settings = [
|
||||
{ name: "API_Key", desc: "ElevenLabs API key — required for authentication" },
|
||||
{ name: "ServerRegion", desc: "Global / US / EU / India" },
|
||||
{ name: "CustomWebSocketURL", desc: "Override WebSocket URL (advanced)" },
|
||||
{ name: "bVerboseLogging", desc: "Verbose WebSocket logging (development)" },
|
||||
{ name: "GlobalContextPrompt", desc: "Shared context injected as dynamic variables for all agents" },
|
||||
];
|
||||
|
||||
settings.forEach((setting, i) => {
|
||||
const y = 2.3 + i * 0.85;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 3.5, h: 0.65, fill: { color: NAVY }, rectRadius: 0.05 });
|
||||
s.addText(setting.name, { x: 0.5, y: y, w: 3.5, h: 0.65, fontSize: 13, bold: true, color: ACCENT, align: "center", valign: "middle", fontFace: "Consolas" });
|
||||
s.addText(setting.desc, { x: 4.2, y: y + 0.05, w: 8.6, h: 0.55, fontSize: 12, color: GREY, valign: "middle", fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 17 — SETUP WORKFLOW
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Setup Workflow", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("From zero to a talking MetaHuman in 5 steps.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
const steps = [
|
||||
{ num: "1", title: "Project Settings", desc: "Enter API_Key, pick ServerRegion." },
|
||||
{ num: "2", title: "Data Assets", desc: "Create AgentConfig, EmotionPoseMap, LipSyncPoseMap, BodyPoseMap." },
|
||||
{ num: "3", title: "NPC Components", desc: "Add ElevenLabs + Mic + LipSync + FacialExpr + BodyExpr + Gaze on the MetaHuman." },
|
||||
{ num: "4", title: "AnimGraphs", desc: "Wire AnimNodes in Face + Body AnimBPs. Components auto-discover." },
|
||||
{ num: "5", title: "Player Pawn", desc: "Add InteractionComponent. Bind selection events to UI. Play." },
|
||||
];
|
||||
steps.forEach((step, i) => {
|
||||
const y = 2.25 + i * 0.9;
|
||||
s.addShape(pres.ShapeType.ellipse, { x: 0.5, y: y, w: 0.8, h: 0.8, fill: { color: ACCENT } });
|
||||
s.addText(step.num, { x: 0.5, y: y, w: 0.8, h: 0.8, fontSize: 28, bold: true, color: NAVY, align: "center", valign: "middle", fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 1.6, y: y, w: 11.2, h: 0.8, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.05 });
|
||||
s.addText(step.title, { x: 1.8, y: y + 0.1, w: 3.5, h: 0.3, fontSize: 14, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText(step.desc, { x: 1.8, y: y + 0.4, w: 11, h: 0.35, fontSize: 11, color: GREY, italic: true, fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 18 — NETWORKING
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Networking & Multiplayer", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Listen Server model — server owns WebSocket, clients relay via RPCs.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
s.addText("Audio Pipeline", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "Client mic → Server RPC (Opus-encoded)", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Server forwards to ElevenLabs WebSocket", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Agent audio → Multicast RPC to all clients", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Clients decode Opus → procedural sound wave", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 0.5, y: 2.55, w: 6, h: 2.4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Speaker Arbitration", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "SpeakerSwitchHysteresis — min silence before switching", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "SpeakerIdleTimeout — clear active speaker", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "NetActiveSpeakerPawn replicated for gaze", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Only active speaker's audio forwarded to LLM", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 7, y: 2.55, w: 6, h: 2.4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Level of Detail (LOD)", { x: 0.5, y: 5.1, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "AudioLODCullDistance — skip audio at distance", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "LipSyncLODDistance — skip lip sync at distance", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "Active speaker always full quality", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 0.5, y: 5.5, w: 6, h: 1.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
s.addText("Replicated State", { x: 7, y: 5.1, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addText([
|
||||
{ text: "bNetIsConversing", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "NetConnectedPawns[]", options: { bullet: true, fontSize: 13 } },
|
||||
{ text: "CurrentEmotion + CurrentEmotionIntensity", options: { bullet: true, fontSize: 13 } },
|
||||
], { x: 7, y: 5.5, w: 6, h: 1.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 19 — FORCE DISABLE FLOW
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("ForceDisableConversation Flow", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 30, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Clean handoff between conversation and gameplay actions.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
const flow = [
|
||||
{ step: "LLM Tool Call", desc: "perform_action(\"flee\")" },
|
||||
{ step: "OnAgentActionRequested", desc: "Event fires on ElevenLabs component" },
|
||||
{ step: "ForceDisableConversation", desc: "Blend out face/body/gaze animations" },
|
||||
{ step: "OnReadyForAction", desc: "All components have reached neutral" },
|
||||
{ step: "Play Gameplay", desc: "Montage / behavior switch" },
|
||||
{ step: "ForceEnableConversation", desc: "Resume conversation when done" },
|
||||
];
|
||||
flow.forEach((f, i) => {
|
||||
const y = 2.2 + i * 0.75;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 4, h: 0.6, fill: { color: NAVY }, rectRadius: 0.08 });
|
||||
s.addText(f.step, { x: 0.5, y: y, w: 4, h: 0.6, fontSize: 13, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: "Consolas" });
|
||||
s.addText("→", { x: 4.6, y: y + 0.05, w: 0.5, h: 0.5, fontSize: 22, color: ACCENT, align: "center", valign: "middle", bold: true, fontFace: FONT_H });
|
||||
s.addText(f.desc, { x: 5.2, y: y + 0.05, w: 7.6, h: 0.5, fontSize: 13, color: GREY, italic: true, valign: "middle", fontFace: FONT_B });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 20 — EVENTS CHEAT SHEET
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide({ masterName: "CONTENT" });
|
||||
s.addText("Events Cheat Sheet", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } });
|
||||
s.addText("Blueprint-assignable delegates on ElevenLabs component.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B });
|
||||
|
||||
const eventCols = [
|
||||
{
|
||||
title: "Connection",
|
||||
items: ["OnAgentConnected", "OnAgentDisconnected", "OnAgentError"]
|
||||
},
|
||||
{
|
||||
title: "Dialogue",
|
||||
items: ["OnAgentTranscript", "OnAgentTextResponse", "OnAgentPartialResponse", "OnAgentStartedGenerating"]
|
||||
},
|
||||
{
|
||||
title: "Audio",
|
||||
items: ["OnAgentStartedSpeaking", "OnAudioPlaybackStarted", "OnAgentStoppedSpeaking", "OnAgentInterrupted"]
|
||||
},
|
||||
{
|
||||
title: "State / Actions",
|
||||
items: ["OnAgentEmotionChanged", "OnAgentActionRequested", "OnAgentClientToolCall", "OnActiveSpeakerChanged", "OnReadyForAction", "OnAgentResponseTimeout"]
|
||||
},
|
||||
];
|
||||
eventCols.forEach((c, i) => {
|
||||
const col = i % 2;
|
||||
const row = Math.floor(i / 2);
|
||||
const x = 0.5 + col * 6.3;
|
||||
const y = 2.15 + row * 2.45;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 6.0, h: 2.25, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 });
|
||||
s.addText(c.title, { x: x + 0.2, y: y + 0.1, w: 5.6, h: 0.3, fontSize: 14, bold: true, color: NAVY, fontFace: FONT_H });
|
||||
s.addShape(pres.ShapeType.rect, { x: x + 0.2, y: y + 0.42, w: 0.35, h: 0.04, fill: { color: ACCENT } });
|
||||
s.addText(c.items.map(t => ({ text: t, options: { bullet: true, fontSize: 11 } })),
|
||||
{ x: x + 0.2, y: y + 0.52, w: 5.6, h: 1.7, color: GREY, fontFace: "Consolas", paraSpaceAfter: 2 });
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SLIDE 21 — CLOSING / SUMMARY
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
s = pres.addSlide();
|
||||
s.background = { color: NAVY };
|
||||
s.addShape(pres.ShapeType.rect, { x: 0, y: 2.5, w: 13.333, h: 0.05, fill: { color: ACCENT } });
|
||||
s.addText("Summary", { x: 0.6, y: 0.8, w: 12, h: 1.2, fontSize: 54, bold: true, color: WHITE, fontFace: FONT_H });
|
||||
s.addText("A complete conversational AI stack for Unreal Engine.", { x: 0.6, y: 2.0, w: 12, h: 0.5, fontSize: 18, color: ICE, italic: true, fontFace: FONT_B });
|
||||
|
||||
const summary = [
|
||||
{ h: "Real-time", t: "Spectral lip sync, speculative TTS, adaptive pre-buffering" },
|
||||
{ h: "Expressive", t: "Emotion-driven face/body animation via AnimSequences" },
|
||||
{ h: "Multiplayer", t: "Listen Server + Opus codec + distance LOD + speaker arbitration" },
|
||||
{ h: "Data-driven", t: "Reusable PoseMaps, AgentConfigs, Tools, ActionSets" },
|
||||
{ h: "Bridgeable", t: "Behavior plugin gaze bridge, ForceDisable handoff for gameplay" },
|
||||
{ h: "BP-friendly", t: "All components blueprintable, rich event system" },
|
||||
];
|
||||
summary.forEach((item, i) => {
|
||||
const col = i % 2;
|
||||
const row = Math.floor(i / 2);
|
||||
const x = 0.6 + col * 6.2;
|
||||
const y = 3.0 + row * 1.4;
|
||||
s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 5.9, h: 1.2, fill: { color: NAVY_DARK }, line: { color: ACCENT, width: 1 }, rectRadius: 0.1 });
|
||||
s.addText(item.h, { x: x + 0.2, y: y + 0.1, w: 5.5, h: 0.4, fontSize: 18, bold: true, color: ACCENT, fontFace: FONT_H });
|
||||
s.addText(item.t, { x: x + 0.2, y: y + 0.55, w: 5.5, h: 0.6, fontSize: 12, color: ICE, italic: true, fontFace: FONT_B });
|
||||
});
|
||||
|
||||
s.addText("ASTERION • PS_AI_ConvAgent • 2026", { x: 0.6, y: 6.9, w: 12, h: 0.4, fontSize: 12, color: ICE_DEEP, fontFace: FONT_B });
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SAVE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
pres.writeFile({ fileName: "PS_AI_ConvAgent_Documentation.pptx" })
|
||||
.then(name => console.log(`Generated: ${name}`))
|
||||
.catch(err => { console.error(err); process.exit(1); });
|
||||
Reference in New Issue
Block a user