27std::string floatToString(
double v) {
29 std::snprintf(buf,
sizeof(buf),
"%g",
v);
33bool squirrelToVarValueAt(
HSQUIRRELVM vm, SQInteger
idx, Dialogue::VarValue &out) {
34 switch (sq_gettype(
vm,
idx)) {
37 if (SQ_FAILED(sq_getinteger(
vm,
idx, &
v)))
return false;
43 if (SQ_FAILED(sq_getfloat(
vm,
idx, &
v)))
return false;
49 if (SQ_FAILED(sq_getbool(
vm,
idx, &
v)))
return false;
54 const SQChar *
v =
nullptr;
55 if (SQ_FAILED(sq_getstring(
vm,
idx, &
v)))
return false;
66 switch (sq_gettype(
vm,
idx)) {
72 if (SQ_FAILED(sq_getinteger(
vm,
idx, &
v)))
return false;
78 if (SQ_FAILED(sq_getfloat(
vm,
idx, &
v)))
return false;
84 if (SQ_FAILED(sq_getbool(
vm,
idx, &
v)))
return false;
89 const SQChar *
s =
nullptr;
90 if (SQ_FAILED(sq_getstring(
vm,
idx, &
s)))
return false;
95 const SQInteger size = sq_getsize(
vm,
idx);
96 const SQInteger absIdx =
idx > 0 ?
idx : sq_gettop(
vm) +
idx + 1;
97 std::vector<DataValue> items;
98 for (SQInteger i = 0; i < size; ++i) {
99 sq_pushinteger(
vm, i);
100 if (SQ_FAILED(sq_get(
vm, absIdx)))
return false;
102 const bool ok = squirrelToDataValue(
vm, -1, item);
104 if (!
ok)
return false;
105 items.push_back(std::move(item));
111 std::vector<std::pair<std::string, DataValue>> fields;
113 while (SQ_SUCCEEDED(sq_next(
vm, -2))) {
115 if (sq_gettype(
vm, -2) == OT_STRING) {
116 const SQChar *k =
nullptr;
117 if (SQ_SUCCEEDED(sq_getstring(
vm, -2, &k)) && k) key = k;
120 const bool ok = !key.empty() && squirrelToDataValue(
vm, -1,
value);
126 fields.emplace_back(std::move(key), std::move(
value));
137bool objectToDataValue(
HSQUIRRELVM vm,
const ssq::Object &obj, DataValue &out) {
138 if (!
vm)
return false;
139 const SQInteger top = sq_gettop(
vm);
140 sq_pushobject(
vm, obj.getRaw());
141 const bool ok = squirrelToDataValue(
vm, -1, out);
146bool objectToVarParams(
HSQUIRRELVM vm,
const ssq::Object &obj,
147 std::unordered_map<std::string, Dialogue::VarValue> &out) {
149 if (!
vm)
return true;
150 const HSQOBJECT raw = obj.getRaw();
151 if (raw._type != OT_TABLE)
return true;
152 const SQInteger top = sq_gettop(
vm);
153 sq_pushobject(
vm, raw);
155 while (SQ_SUCCEEDED(sq_next(
vm, -2))) {
156 const SQChar *k =
nullptr;
157 if (sq_gettype(
vm, -2) == OT_STRING && SQ_SUCCEEDED(sq_getstring(
vm, -2, &k)) && k) {
158 Dialogue::VarValue
v;
159 if (squirrelToVarValueAt(
vm, -1,
v)) out[k] = std::move(
v);
170 sq_pushinteger(
vm, SQInteger(
v.i));
173 sq_pushfloat(
vm, SQFloat(
v.f));
176 sq_pushbool(
vm,
v.b ? SQTrue : SQFalse);
179 sq_pushstring(
vm,
v.s.c_str(),
v.s.size());
184void pushVarTable(
HSQUIRRELVM vm,
const std::unordered_map<std::string, Dialogue::VarValue> &
vars) {
186 for (
const auto &kv :
vars) {
187 sq_pushstring(
vm, kv.first.c_str(), kv.first.size());
188 pushVarValue(
vm, kv.second);
189 sq_newslot(
vm, -3, SQFalse);
193std::vector<Dialogue*>& liveDialogues() {
194 static std::vector<Dialogue*> instances;
198StateValue varValueToState(
const Dialogue::VarValue&
v) {
208bool stateToVarValue(
const StateValue&
v, Dialogue::VarValue& out) {
214 default:
return false;
221 const char* stateKind()
const override {
return "dialogue"; }
223 bool captureState(StateValue& out)
override {
225 for (Dialogue*
d : liveDialogues()) {
227 if (
d->captureState(sv)) arr.pushBack(std::move(sv));
229 out = std::move(arr);
233 bool restoreState(
const StateValue& in, std::string* err)
override {
235 if (err) *err =
"dialogue: expected array of instances";
238 const size_t n = std::min(in.arraySize(), liveDialogues().size());
239 for (
size_t i = 0; i <
n; ++i) {
240 if (!liveDialogues()[i]->restoreState(in.at(i), err))
return false;
245 bool resetToDefaults()
override {
246 for (Dialogue*
d : liveDialogues())
d->reset();
253 static DialogueStateProvider provider;
269 x.
type = Type::Float;
283 x.
type = Type::String;
305 return std::to_string(i);
307 return floatToString(
f);
309 return b ?
"true" :
"false";
319 liveDialogues().erase(std::remove(liveDialogues().begin(), liveDialogues().end(),
this), liveDialogues().end());
321 for (
auto &kv : predicates_) sq_release(vm_, &kv.second);
330std::unordered_map<std::string, Dialogue::VarValue> *Dialogue::varsForScope(
331 const std::string &
scope) {
332 if (
scope ==
"global")
return &globalVars_;
333 if (
scope ==
"scene")
return &sceneVars_;
337const std::unordered_map<std::string, Dialogue::VarValue> *Dialogue::varsForScope(
338 const std::string &
scope)
const {
339 if (
scope ==
"global")
return &globalVars_;
340 if (
scope ==
"scene")
return &sceneVars_;
345 const std::string &
scope) {
346 auto *
m = varsForScope(
scope);
347 if (!
m ||
name.empty())
return false;
353 if (!vm_)
return false;
354 const SQInteger top = sq_gettop(vm_);
355 sq_pushobject(vm_,
value.getRaw());
357 const bool ok = squirrelToVarValueAt(vm_, -1,
v);
359 if (!
ok)
return false;
364 const auto *
m = varsForScope(
scope);
366 const auto it =
m->find(
name);
383 const std::string &
scope)
const {
392 const std::string &
scope)
const {
401 const std::string &
scope)
const {
407 const auto *
m = varsForScope(
scope);
408 return m &&
m->find(
name) !=
m->end();
412 auto *
m = varsForScope(
scope);
413 return m &&
m->erase(
name) > 0;
417 if (
scope ==
"global") {
419 }
else if (
scope ==
"scene") {
421 }
else if (
scope ==
"all") {
432 if (!vm_ ||
name.empty())
return false;
433 HSQOBJECT raw =
fn.getRaw();
435 if (raw._type != OT_CLOSURE && raw._type != OT_NATIVECLOSURE)
return false;
436 const auto it = predicates_.find(
name);
437 if (it != predicates_.end()) {
438 sq_release(vm_, &it->second);
439 predicates_.erase(it);
441 sq_addref(vm_, &raw);
442 predicates_[
name] = raw;
447 const auto it = predicates_.find(
name);
448 if (it == predicates_.end())
return false;
449 if (vm_) sq_release(vm_, &it->second);
450 predicates_.erase(it);
456 if (!objectToDataValue(vm_, table,
v))
return false;
460bool Dialogue::parseCondition(
const DataValue &
v, Condition &out, std::string &
error) {
462 out.kind = Condition::Kind::All;
463 for (
const auto &item :
v.arr) {
465 if (!parseCondition(item, child,
error))
return false;
466 out.
children.push_back(std::move(child));
471 error =
"condition must be a table";
474 if (
const DataValue *script =
v.find(
"script")) {
476 error =
"script condition name must be a string";
479 out.kind = Condition::Kind::Script;
480 out.script = script->s;
483 if (
const DataValue *all =
v.find(
"all")) {
485 error =
"all must be an array";
488 out.kind = Condition::Kind::All;
489 for (
const auto &item : all->arr) {
491 if (!parseCondition(item, child,
error))
return false;
492 out.
children.push_back(std::move(child));
496 if (
const DataValue *any =
v.find(
"any")) {
498 error =
"any must be an array";
501 out.kind = Condition::Kind::Any;
502 for (
const auto &item : any->arr) {
504 if (!parseCondition(item, child,
error))
return false;
505 out.
children.push_back(std::move(child));
509 if (
const DataValue *notc =
v.find(
"not")) {
510 out.kind = Condition::Kind::Not;
511 out.children.emplace_back();
512 return parseCondition(*notc, out.children.back(),
error);
514 if (
const DataValue *var =
v.find(
"var")) {
515 const DataValue *op =
v.find(
"op");
518 error =
"var/op condition requires string var and op";
521 static const char *kOps[] = {
"eq",
"ne",
"gt",
"ge",
"lt",
"le",
"has",
"missing"};
522 const std::string opStr = op->s;
523 if (std::find(std::begin(kOps), std::end(kOps), opStr) == std::end(kOps)) {
524 error =
"unknown condition op: " + opStr;
527 out.kind = Condition::Kind::Cmp;
531 if (
const DataValue *val =
v.find(
"value")) {
551 error =
"unrecognized condition table";
555bool Dialogue::compareEq(
const VarValue &
a,
const VarValue &
b)
const {
556 if (
a.isNumeric() &&
b.isNumeric()) {
567bool Dialogue::compareOrder(
const std::string &op,
const VarValue &
a,
const VarValue &
b)
const {
568 if (!
a.isNumeric() || !
b.isNumeric())
return false;
571 if (op ==
"gt")
return x >
y;
572 if (op ==
"ge")
return x >=
y;
573 if (op ==
"lt")
return x <
y;
574 if (op ==
"le")
return x <=
y;
578bool Dialogue::evalConditionInternal(
const Condition &
c,
579 const std::unordered_map<std::string, VarValue> &merged,
580 const std::unordered_map<std::string, VarValue> ¶ms,
581 const std::string &lineId)
const {
583 case Condition::Kind::Always:
585 case Condition::Kind::Cmp: {
586 const auto it = merged.find(
c.var);
587 const bool present = it != merged.end();
588 if (
c.op ==
"has")
return present;
589 if (
c.op ==
"missing")
return !present;
590 if (!present)
return false;
591 if (
c.op ==
"eq")
return compareEq(it->second,
c.value);
592 if (
c.op ==
"ne")
return !compareEq(it->second,
c.value);
593 return compareOrder(
c.op, it->second,
c.value);
595 case Condition::Kind::All:
597 if (!evalConditionInternal(
child, merged, params, lineId)) return false;
599 case Condition::Kind::Any:
601 if (evalConditionInternal(
child, merged, params, lineId)) return true;
603 case Condition::Kind::Not:
604 return c.children.empty() ||
605 !evalConditionInternal(
c.children.front(), merged, params, lineId);
606 case Condition::Kind::Script:
607 return evalScriptPredicate(
c.script, merged, params, lineId);
615 if (!parseCondition(cond,
c,
error))
return false;
616 return evalConditionInternal(
c, mergedVars({}), {},
"");
619bool Dialogue::evalScriptPredicate(
const std::string &
name,
620 const std::unordered_map<std::string, VarValue> &merged,
621 const std::unordered_map<std::string, VarValue> ¶ms,
622 const std::string &lineId)
const {
623 if (!vm_)
return false;
624 const auto it = predicates_.find(
name);
625 if (it == predicates_.end())
return false;
627 const SQInteger top = sq_gettop(vm_);
628 sq_pushobject(vm_, it->second);
631 sq_pushstring(vm_,
"vars", -1);
632 pushVarTable(vm_, merged);
633 sq_newslot(vm_, -3, SQFalse);
635 sq_pushstring(vm_,
"params", -1);
636 pushVarTable(vm_, params);
637 sq_newslot(vm_, -3, SQFalse);
639 sq_pushstring(vm_,
"lineId", -1);
640 sq_pushstring(vm_, lineId.c_str(), lineId.size());
641 sq_newslot(vm_, -3, SQFalse);
643 if (SQ_FAILED(sq_call(vm_, 1, SQTrue, SQTrue))) {
647 SQBool result = SQFalse;
648 if (SQ_FAILED(sq_getbool(vm_, -1, &result))) {
662 if (!objectToDataValue(vm_, table, root)) {
663 lastPoolsError_ =
"pools root must be a table";
669bool Dialogue::parseLineData(
const std::string &poolId,
const DataValue &
v,
int index, Line &
line,
670 std::string &
error) {
672 error =
"line must be a table";
675 if (
const DataValue *
id =
v.find(
"id"))
677 if (
line.id.empty())
line.id = poolId +
"." + std::to_string(index);
679 if (
const DataValue *sp =
v.find(
"speaker"))
681 if (
const DataValue *tx =
v.find(
"text"))
683 if (
const DataValue *ik =
v.find(
"i18n"))
685 if (
line.text.empty() &&
line.i18nKey.empty()) {
686 error =
"line '" +
line.id +
"': text or i18n required";
690 if (
const DataValue *
w =
v.find(
"weight")) {
694 error =
"line '" +
line.id +
"': weight must be a number";
699 if (
const DataValue *when =
v.find(
"when")) {
700 if (!parseCondition(*when,
line.when,
error)) {
706 if (
const DataValue *meta =
v.find(
"meta")) {
708 error =
"line '" +
line.id +
"': meta must be a table";
711 for (
const auto &mkv : meta->obj) {
713 switch (mkv.second.kind) {
718 val = std::to_string(mkv.second.i);
721 val = floatToString(mkv.second.f);
724 val = mkv.second.b ?
"true" :
"false";
729 line.meta[mkv.first] = val;
733 if (
const DataValue *tags =
v.find(
"tags")) {
735 error =
"line '" +
line.id +
"': tags must be an array";
738 for (
const auto &t : tags->arr)
739 if (t.
kind == DataValue::Kind::String)
line.tags.push_back(t.
s);
745 lastPoolsError_.clear();
747 lastPoolsError_ =
"pools root must be an object";
752 lastPoolsError_ =
"missing pools object";
757 for (
const auto &kv : pools->
obj) {
761 lastPoolsError_ =
"pool '" + kv.first +
"': expected object";
767 if (pool.noRepeat < 0) pool.noRepeat = 0;
771 lastPoolsError_ =
"pool '" + kv.first +
"': missing lines array";
775 for (
const auto &lv : lines->
arr) {
778 if (!parseLineData(pool.id, lv, lineIndex,
line,
error)) {
779 if (lastPoolsError_.empty()) lastPoolsError_ =
error;
782 pool.lines.push_back(std::move(
line));
785 if (Pool *existing = findPool(pool.id)) *existing = std::move(pool);
786 else pools_.push_back(std::move(pool));
795 if (!
parseDnut(source, path.empty() ?
"<dnut>" : path, root,
error)) {
796 lastPoolsError_ =
error;
803 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>(
"Filesystem");
804 if (!fs) fs = eve::filesystem::Filesystem::create();
810 lastPoolsError_ = path +
": 读取失败";
813 if (fd ==
nullptr || fd->
getData() ==
nullptr || fd->
getSize() == 0) {
815 lastPoolsError_ = path +
": 读取失败";
818 const std::string text(
static_cast<const char *
>(fd->
getData()), fd->
getSize());
828 if (index < 0 ||
size_t(index) >= pools_.size())
return {};
829 return pools_[size_t(index)].id;
834Dialogue::Pool *Dialogue::findPool(
const std::string &
id) {
835 for (
auto &
p : pools_)
836 if (
p.
id ==
id) return &
p;
840const Dialogue::Pool *Dialogue::findPool(
const std::string &
id)
const {
841 for (
const auto &
p : pools_)
842 if (
p.
id ==
id) return &
p;
846Dialogue::Line *Dialogue::findLine(
const std::string &
id) {
847 for (
auto &
p : pools_)
848 for (auto &l :
p.lines)
849 if (l.
id ==
id) return &l;
853const Dialogue::Line *Dialogue::findLine(
const std::string &
id)
const {
854 for (
const auto &
p : pools_)
855 for (const auto &l :
p.lines)
856 if (l.
id ==
id) return &l;
864std::unordered_map<std::string, Dialogue::VarValue> Dialogue::mergedVars(
865 const std::unordered_map<std::string, VarValue> ¶ms)
const {
866 std::unordered_map<std::string, VarValue>
m = globalVars_;
867 for (
const auto &kv : sceneVars_)
m[kv.first] = kv.second;
868 for (
const auto &kv : params)
m[kv.first] = kv.second;
872std::unordered_map<std::string, std::string> Dialogue::stringParams(
873 const std::unordered_map<std::string, VarValue> &
vars)
const {
874 std::unordered_map<std::string, std::string> out;
875 for (
const auto &kv :
vars) out[kv.first] = kv.second.toString();
879std::string Dialogue::interpolate(
const std::string &tpl,
880 const std::unordered_map<std::string, VarValue> &
vars)
const {
882 out.reserve(tpl.size());
883 for (
size_t i = 0; i < tpl.size();) {
885 const size_t close = tpl.find(
'}', i + 1);
886 if (close != std::string::npos) {
887 const std::string
name = tpl.substr(i + 1, close - i - 1);
889 if (it !=
vars.end()) {
890 out += it->second.toString();
905uint32_t Dialogue::nextRandom() {
906 uint32_t
x = rngState_;
914double Dialogue::nextUnit() {
return (nextRandom() & 0xFFFFFFu) / 16777216.0; }
917 rngState_ =
seed ? uint32_t(
seed) : 1u;
918 for (
auto &
p : pools_)
p.recent.clear();
922 std::unordered_map<std::string, VarValue>
p;
923 objectToVarParams(vm_, params,
p);
928 const std::string &poolId,
const std::unordered_map<std::string, VarValue> ¶ms) {
929 Pool *pool = findPool(poolId);
930 if (!pool)
return "";
931 const auto merged = mergedVars(params);
933 auto collect = [&](std::vector<size_t> &out,
bool allowRecent) {
934 for (
size_t i = 0; i < pool->lines.size(); ++i) {
935 const Line &
line = pool->lines[i];
936 if (
line.weight <= 0.0)
continue;
937 if (!evalConditionInternal(
line.when, merged, params,
line.id))
continue;
938 if (!allowRecent && pool->noRepeat > 0 &&
939 std::find(pool->recent.begin(), pool->recent.end(), i) != pool->recent.end())
945 std::vector<size_t> candidates;
946 collect(candidates,
false);
947 if (candidates.empty()) collect(candidates,
true);
948 if (candidates.empty())
return "";
951 for (
const size_t i : candidates) total += pool->lines[i].weight;
952 double roll = nextUnit() * total;
953 size_t picked = candidates.front();
955 for (
const size_t i : candidates) {
956 acc += pool->lines[i].weight;
962 if (pool->noRepeat > 0) {
963 pool->recent.push_back(picked);
964 if (pool->recent.size() > size_t(pool->noRepeat)) pool->recent.erase(pool->recent.begin());
966 return pool->lines[picked].id;
970 std::unordered_map<std::string, VarValue>
p;
971 objectToVarParams(vm_, params,
p);
976 const std::unordered_map<std::string, VarValue> ¶ms) {
977 Line *
line = findLine(lineId);
978 if (!
line)
return false;
979 const auto merged = mergedVars(params);
982 if (!
line->text.empty()) {
983 text = interpolate(
line->text, merged);
985 auto *i18n = eve::ModuleManager::getInstance<eve::i18n::I18n>(
"I18n");
986 text = i18n ? i18n->getWithParams(
line->i18nKey, stringParams(merged)) :
line->i18nKey;
992 currentLineId_ =
line->id;
993 currentLineMeta_ =
line->meta;
994 currentLineTags_ =
line->tags;
995 applyLineMeta(*
line);
1000 std::unordered_map<std::string, VarValue>
p;
1001 objectToVarParams(vm_, params,
p);
1006 const std::unordered_map<std::string, VarValue> ¶ms) {
1012 const auto it = currentLineMeta_.find(field);
1013 return it == currentLineMeta_.end() ? std::string{} : it->second;
1018void Dialogue::applyLineMeta(
const Line &
line) {
1019 if (
line.speaker.empty())
return;
1020 const auto expr =
line.meta.find(
"expression");
1021 const auto motion =
line.meta.find(
"motion");
1022 if (expr ==
line.meta.end() && motion ==
line.meta.end())
return;
1023 Character *
c = findCharacter(
line.speaker);
1024 if (!
c || !
c->avatar)
return;
1025 if (expr !=
line.meta.end())
c->avatar->setExpression(expr->second);
1026 if (motion !=
line.meta.end())
c->avatar->setMotion(motion->second);
1033void Dialogue::pollSceneChange() {
1034 auto *scn = eve::ModuleManager::getInstance<eve::scene::Scene>(
"Scene");
1037 const std::string
name = host ? host->
getName() :
"";
1038 if (!
name.empty() &&
name != lastSceneName_) sceneVars_.clear();
1039 if (!
name.empty()) lastSceneName_ =
name;
1042Dialogue::Character *Dialogue::findCharacter(
const std::string &
id) {
1043 for (
auto &
c : characters_)
1044 if (
c.
id ==
id) return &
c;
1048const Dialogue::Character *Dialogue::findCharacter(
const std::string &
id)
const {
1049 for (
const auto &
c : characters_)
1050 if (
c.
id ==
id) return &
c;
1055 if (
id.empty())
return false;
1056 if (
auto *
c = findCharacter(
id)) {
1057 c->displayName = displayName.empty() ?
id : displayName;
1062 c.displayName = displayName.empty() ?
id : displayName;
1063 characters_.push_back(
c);
1070 const Character *
c = findCharacter(
id);
1071 return c ?
c->displayName : std::string{};
1075 Character *
c = findCharacter(
id);
1076 if (!
c)
return false;
1082 const Character *
c = findCharacter(
id);
1083 return c ?
c->avatar :
nullptr;
1089 if (index < 0 ||
size_t(index) >= characters_.size())
return {};
1090 return characters_[size_t(index)].id;
1094 Character *
c = findCharacter(
id);
1095 if (!
c)
return false;
1097 c->slot = slot.empty() ?
"center" : slot;
1098 if (
c->avatar)
c->avatar->setVisible(
true);
1103 Character *
c = findCharacter(
id);
1104 if (!
c)
return false;
1106 if (
c->avatar)
c->avatar->setVisible(
false);
1111 const Character *
c = findCharacter(
id);
1112 return c &&
c->shown;
1116 const Character *
c = findCharacter(
id);
1117 return c ?
c->slot : std::string{};
1121 if (slot.empty())
return;
1122 slotX_[slot] = xNorm;
1126 auto it = slotX_.find(slot);
1127 if (it != slotX_.end())
return it->second;
1128 if (slot ==
"left")
return 0.25f;
1129 if (slot ==
"right")
return 0.75f;
1130 if (slot ==
"center")
return 0.5f;
1135 Character *
c = findCharacter(
id);
1136 if (!
c || !
c->avatar)
return false;
1137 c->avatar->setExpression(expression);
1142 Character *
c = findCharacter(
id);
1143 if (!
c || !
c->avatar)
return false;
1144 c->avatar->setMotion(motion);
1150 for (Character &
c : characters_) {
1151 if (!
c.avatar)
continue;
1152 c.avatar->setVisible(
c.shown);
1158 const float x = xn * stageWidth;
1160 c.avatar->setPosition(
x,
c.avatar->getY());
1165void Dialogue::beginLine(
const std::string &speakerId,
const std::string &text) {
1166 speakerId_ = speakerId;
1169 selectedChoiceId_.clear();
1171 if (typeSpeed_ <= 0.f) {
1173 phase_ = Phase::WaitingAdvance;
1174 lipSyncValue_ = 0.f;
1176 phase_ = Phase::Typing;
1181 beginLine(speakerId, text);
1189 if (phase_ == Phase::Typing) {
1191 phase_ = Phase::WaitingAdvance;
1202 if (phase_ == Phase::Typing) {
1206 if (phase_ == Phase::WaitingAdvance) phase_ = Phase::Idle;
1210 if (speakerId_.empty())
return {};
1215 if (fullText_.empty())
return {};
1217 size_t n = size_t(std::floor(typed_ + 1e-4f));
1218 if (
n == 0)
return {};
1219 if (
n >= total)
return fullText_;
1229 case Phase::WaitingAdvance:
1230 return "waiting_advance";
1231 case Phase::WaitingChoice:
1232 return "waiting_choice";
1239 selectedChoiceId_.clear();
1243 if (
id.empty())
return false;
1244 for (
auto &ch : choices_) {
1250 choices_.push_back(Choice{
id, label});
1255 if (choices_.empty()) {
1256 phase_ = Phase::Idle;
1260 phase_ = Phase::WaitingChoice;
1268 if (index < 0 ||
size_t(index) >= choices_.size())
return {};
1269 return choices_[size_t(index)].id;
1273 if (index < 0 ||
size_t(index) >= choices_.size())
return {};
1274 return choices_[size_t(index)].label;
1278 if (phase_ != Phase::WaitingChoice)
return false;
1279 if (index < 0 ||
size_t(index) >= choices_.size())
return false;
1280 selectedChoiceId_ = choices_[size_t(index)].id;
1281 phase_ = Phase::Idle;
1288 if (!
name.empty()) lipSyncParameter_ =
name;
1292 if (amplitude < 0.f) amplitude = 0.f;
1293 if (amplitude > 2.f) amplitude = 2.f;
1294 lipSyncAmplitude_ = amplitude;
1297void Dialogue::updateLipSync(
float dt) {
1298 if (dt < 0.f) dt = 0.f;
1299 if (lipSyncEnabled_ && phase_ == Phase::Typing && !speakerId_.empty()) {
1302 const float wave = std::fabs(std::sin(lipSyncTime_ * 14.f));
1303 lipSyncValue_ = lipSyncAmplitude_ * (0.25f + 0.75f * wave);
1306 lipSyncValue_ *= std::max(0.f, 1.f - dt * 8.f);
1307 if (lipSyncValue_ < 0.01f) lipSyncValue_ = 0.f;
1309 applyLipSyncToSpeaker();
1312void Dialogue::applyLipSyncToSpeaker() {
1313 if (!lipSyncEnabled_ || speakerId_.empty() || lipSyncParameter_.empty())
return;
1314 Character *
c = findCharacter(speakerId_);
1315 if (!
c || !
c->avatar)
return;
1316 c->avatar->setParameter(lipSyncParameter_, lipSyncValue_);
1320 if (dt < 0.f) dt = 0.f;
1321 if (phase_ == Phase::Typing) {
1323 typed_ += typeSpeed_ * dt;
1324 if (typed_ >= total) {
1326 phase_ = Phase::WaitingAdvance;
1334 phase_ = Phase::Idle;
1339 selectedChoiceId_.clear();
1340 lipSyncValue_ = 0.f;
1342 currentLineId_.clear();
1343 currentLineMeta_.clear();
1344 currentLineTags_.clear();
1345 for (Character &
c : characters_) {
1347 if (
c.avatar)
c.avatar->setVisible(
false);
1366 for (
const auto& kv : globalVars_) global.
set(kv.first, varValueToState(kv.second));
1367 out.
set(
"globalVars", std::move(global));
1370 for (
const auto& kv : sceneVars_) scene.
set(kv.first, varValueToState(kv.second));
1371 out.
set(
"sceneVars", std::move(scene));
1374 for (
const auto&
c : choices_) {
1380 out.
set(
"choices", std::move(choices));
1383 for (
const auto&
c : characters_) {
1391 out.
set(
"characters", std::move(chars));
1395 out.
set(
"slotX", std::move(slotX));
1401 if (err) *err =
"dialogue: state is not an object";
1405 if (!phase || !phase->
isString()) {
1406 if (err) *err =
"dialogue: missing phase";
1409 const std::string phaseName = phase->
asString();
1410 if (phaseName ==
"idle")
1411 phase_ = Phase::Idle;
1412 else if (phaseName ==
"typing")
1413 phase_ = Phase::Typing;
1414 else if (phaseName ==
"waiting_advance")
1415 phase_ = Phase::WaitingAdvance;
1416 else if (phaseName ==
"waiting_choice")
1417 phase_ = Phase::WaitingChoice;
1419 if (err) *err =
"dialogue: unknown phase '" + phaseName +
"'";
1423 if (
const StateValue*
v = in.
find(
"speakerId");
v &&
v->isString()) speakerId_ =
v->asString();
1426 typed_ =
static_cast<float>(
v->isInt() ? double(
v->asInt()) :
v->asDouble());
1428 typeSpeed_ =
static_cast<float>(
v->isInt() ? double(
v->asInt()) :
v->asDouble());
1431 if (
const StateValue*
v = in.
find(
"lipSyncAmplitude");
v && (
v->isInt() ||
v->isFloat()))
1432 lipSyncAmplitude_ =
static_cast<float>(
v->isInt() ? double(
v->asInt()) :
v->asDouble());
1433 if (
const StateValue*
v = in.
find(
"rngState");
v &&
v->
isInt()) rngState_ =
static_cast<uint32_t
>(
v->asInt());
1437 globalVars_.clear();
1439 for (
const auto& key :
vars->keys()) {
1441 if (stateToVarValue(*
vars->find(key), val)) globalVars_[key] = val;
1446 for (
const auto& key :
vars->keys()) {
1448 if (stateToVarValue(*
vars->find(key), val)) sceneVars_[key] = val;
1454 for (
size_t i = 0; i < choices->arraySize(); ++i) {
1458 if (
id &&
id->isString() && label && label->
isString()) {
1459 choices_.push_back(Choice{
id->asString(), label->
asString()});
1465 for (
size_t i = 0; i < chars->arraySize(); ++i) {
1468 if (!
id || !
id->isString())
continue;
1469 Character *
c = findCharacter(
id->asString());
1471 characters_.push_back(Character{});
1472 c = &characters_.back();
1473 c->id =
id->asString();
1475 c->displayName =
name->asString();
1478 c->slot = slot->asString();
1480 c->shown = shown->asBool();
1481 if (
c->avatar)
c->avatar->setVisible(
c->shown);
1488 for (
const auto &key : slots->keys()) {
1490 if (
v && (
v->isInt() ||
v->isFloat()))
1491 slotX_[key] =
static_cast<float>(
v->isInt() ? double(
v->asInt()) :
v->asDouble());
1502void Dialogue::expose(ssq::Table &table) {
1503 if (
Dialogue *self = Dialogue::create()) self->vm_ = table.getHandle();
1504 auto cls = table.addClass(
name, Dialogue::create,
false);
1508void Dialogue::expose(ssq::Class &
cls) {