OpenGothic
Open source reimplementation of Gothic I and II
Loading...
Searching...
No Matches
interactive.cpp
Go to the documentation of this file.
1#include "interactive.h"
2
3#include <Tempest/Painter>
4#include <Tempest/Log>
5
6#include <zenkit/vobs/MovableObject.hh>
7
8#include "game/serialize.h"
10#include "utils/string_frm.h"
12#include "world/objects/npc.h"
13#include "world/world.h"
14#include "utils/dbgpainter.h"
15#include "gothic.h"
16
18 switch(dir) {
19 case Interactive::Anim::In: return Npc::Anim::InteractIn;
20 case Interactive::Anim::Active: return Npc::Anim::InteractIn;
21 case Interactive::Anim::Out: return Npc::Anim::InteractOut;
22 case Interactive::Anim::ToStand: return Npc::Anim::InteractToStand;
23 case Interactive::Anim::FromStand: return Npc::Anim::InteractFromStand;
24 }
25 return Npc::Anim::InteractIn;
26 }
27
28Interactive::Interactive(Vob* parent, World &world, const zenkit::VMovableObject& vob, Flags flags)
29 : Vob(parent,world,vob,flags) {
30
31 vobName = vob.vob_name;
32 focName = vob.name;
33 bbox[0] = {vob.bbox.min.x, vob.bbox.min.y, vob.bbox.min.z};
34 bbox[1] = {vob.bbox.max.x, vob.bbox.max.y, vob.bbox.max.z};
35 owner = vob.owner;
36 focOver = vob.focus_override;
37 showVisual = vob.show_visual;
38
39 auto p = position();
40 displayOffset = Tempest::Vec3(0,bbox[1].y-p.y,0);
41
42 if(vob.type != zenkit::VirtualObjectType::oCMOB) {
43 // TODO: These might be movable
44 auto& inter = reinterpret_cast<const zenkit::VInteractiveObject&>(vob);
45 stateNum = inter.state;
46 triggerTarget = inter.target;
47 useWithItem = inter.item;
48 conditionFunc = inter.condition_function;
49 onStateFunc = inter.on_state_change_function;
50 rewind = inter.rewind;
51 }
52
53 for(auto& i:owner)
54 i = char(std::toupper(i));
55
56 if(vobType==zenkit::VirtualObjectType::oCMobDoor) {
57 auto& door = reinterpret_cast<const zenkit::VDoor&>(vob);
58 locked = door.locked;
59 keyInstance = door.key;
60 pickLockStr = door.pick_string;
61 isLockCracked = !door.locked;
62 }
63
64 if(isContainer() && (flags&Flags::Startup)==Flags::Startup) {
65 auto& container = reinterpret_cast<const zenkit::VContainer&>(vob);
66 locked = container.locked;
67 keyInstance = container.key;
68 pickLockStr = container.pick_string;
69 isLockCracked = !container.locked;
70
71 auto items = std::move(container.contents);
72 if(items.size()>0) {
73 char* it = &items[0];
74 for(auto i=it;;++i) {
75 if(*i==','){
76 *i='\0';
77 implAddItem(it);
78 it=i+1;
79 }
80 else if(*i=='\0'){
81 implAddItem(it);
82 it=i+1;
83 break;
84 }
85 }
86 }
87 }
88
89 setVisual(vob);
90 mdlVisual = vob.visual->name;
91
92 if(isLadder() && !mdlVisual.empty()) {
93 // NOTE: there must be else way to determinate steps count, but for now - we parse filename
94 size_t at = mdlVisual.size()-1;
95 while(at>0 && !std::isdigit(mdlVisual[at]))
96 --at;
97 while(at>0 && std::isdigit(mdlVisual[at-1]))
98 --at;
99 stepsCount = std::atoi(mdlVisual.c_str()+at);
100 stateNum = stepsCount;
101
102 transform().project(displayOffset);
103 displayOffset -= position();
104 }
105
106 world.addInteractive(this);
107 }
108
110 Vob::load(fin);
111
112 fin.read(vobName,focName,mdlVisual);
113 fin.read(bbox[0],bbox[1],owner);
114 fin.read(focOver,showVisual);
115
116 fin.read(stateNum,triggerTarget,useWithItem,conditionFunc,onStateFunc);
117 fin.read(locked,keyInstance,pickLockStr);
118 fin.read(state,reverseState,loopState,isLockCracked);
119
120 uint32_t sz=0;
121 fin.read(sz);
122 for(size_t i=0; i<sz; ++i) {
123 std::string name;
124 Npc* user = nullptr;
125 bool attachMode = false;
126 Phase started = NotStarted;
127
128 fin.read(name,user,attachMode,reinterpret_cast<uint8_t&>(started));
129
130 for(auto& a:attPos)
131 if(a.name==name) {
132 a.user = user;
133 a.attachMode = attachMode;
134 a.started = started;
135 }
136 }
137
138 if(fin.setEntry("worlds/",fin.worldName(),"/mobsi/",vobObjectID,"/inventory"))
139 invent.load(fin,*this,world); else
140 invent.clear(world.script(),*this,true);
141
142 fin.setEntry("worlds/", fin.worldName(), "/mobsi/", vobObjectID, "/visual");
143 visual.load(fin, *this);
144 visual.setObjMatrix(transform());
145 visual.syncPhysics();
146 }
147
148void Interactive::save(Serialize &fout) const {
149 Vob::save(fout);
150
151 fout.write(vobName,focName,mdlVisual);
152 fout.write(bbox[0],bbox[1],owner);
153 fout.write(focOver,showVisual);
154
155 fout.write(stateNum,triggerTarget,useWithItem,conditionFunc,onStateFunc);
156 fout.write(locked,keyInstance,pickLockStr);
157 fout.write(state,reverseState,loopState,isLockCracked);
158
159 fout.write(uint32_t(attPos.size()));
160 for(auto& i:attPos) {
161 fout.write(i.name,i.user,i.attachMode,uint8_t(i.started));
162 }
163
164 if(!invent.isEmpty()) {
165 fout.setEntry("worlds/",fout.worldName(),"/mobsi/",vobObjectID,"/inventory");
166 invent.save(fout);
167 }
168
169 fout.setEntry("worlds/",fout.worldName(),"/mobsi/",vobObjectID,"/visual");
170 visual.save(fout,*this);
171 }
172
174 for(auto& i:attPos)
175 if(i.user!=nullptr && i.user->interactive()!=this)
176 i.user = nullptr;
177 if(visual.updateAnimation(nullptr,this,world,0,true))
178 animChanged = true;
179 }
180
182 for(auto& i:attPos)
183 if(i.user!=nullptr && i.user->isPlayer())
184 return;
185
186 loopState = false;
187 string_frm buf("S_S0");
188 if(state>0)
189 buf = string_frm("S_S",int(state));
190 visual.startAnimAndGet(buf,world.tickCount(),true);
191 setState(state);
192 }
193
194void Interactive::setVisual(const zenkit::VirtualObject& vob) {
195 visual.setVisual(vob,world,true);
196 visual.setObjMatrix(transform());
197 visual.setInteractive(this);
198 animChanged = true;
199 if(auto mesh = visual.protoMesh()) {
200 attPos.resize(mesh->pos.size());
201 for(size_t i=0;i<attPos.size();++i){
202 attPos[i].name = mesh->pos[i].name;
203 attPos[i].pos = mesh->pos[i].transform;
204 attPos[i].node = mesh->pos[i].node;
205 }
206 }
207 setAnim(Interactive::Active); // setup default anim
208 }
209
211 if(visual.updateAnimation(nullptr,this,world,dt,false))
212 animChanged = true;
213 }
214
215void Interactive::tick(uint64_t dt) {
216 visual.processLayers(world);
217
218 if(animChanged) {
219 visual.syncPhysics();
220 animChanged = false;
221 }
222
223 if(world.tickCount()<waitAnim)
224 return;
225
226 Pos* p = nullptr;
227 for(auto& i:attPos) {
228 if(i.user!=nullptr) {
229 p = &i;
230 }
231 }
232
233 if(p==nullptr) {
234 // Note: oCMobInter::rewind, oCMobInter with killed user has to go back to state=-1
235 // All other cases, oCMobFire, oCMobDoor in particular - preserve old state
236 const int destSt = -1;
237 for(auto& i:attPos) {
238 if(destSt!=state && (i.started==Quit || rewind)) {
239 if(!setAnim(nullptr,Anim::Out))
240 return;
241 setState(state-1);
242 i.started = NotStarted;
243 }
244 }
245 return;
246 }
247
248 if(p->user==nullptr && (state==-1 && !p->attachMode))
249 return;
250 if(p->user==nullptr && (state==stateNum && p->attachMode))
251 return;
252
253 if(isLadder() && p->started==Started && p->user!=nullptr && p->user->isAiQueueEmpty())
254 return;
255 implTick(*p);
256 }
257
259 if(world.tickCount()<waitAnim)
260 return;
261
262 Pos* p = nullptr;
263 for(auto& i:attPos) {
264 if(i.user!=nullptr) {
265 p = &i;
266 }
267 }
268 if(p==nullptr)
269 return;
270
271 auto& npc = *p->user;
272 assert(npc.isPlayer());
273
274 if(act==KeyCodec::ActionGeneric) {
275 npc.setInteraction(nullptr);
276 npc.stopAnim("");
277 p->user = nullptr;
278 return;
279 }
280
281 if(act!=KeyCodec::Forward && act!=KeyCodec::Back)
282 return;
283
284 reverseState = (act==KeyCodec::Back);
285 implTick(*p);
286 }
287
288void Interactive::implTick(Pos& p) {
289 if(p.user==nullptr)
290 return;
291
292 static bool dbg = false;
293 static int kId = -1;
294 if(dbg && !p.user->isPlayer() && p.user->handle().id!=kId)
295 return;
296
297 Npc& npc = *p.user;
298 const bool attach = (p.attachMode^reverseState);
299
300 if(p.started==NotStarted) {
301 const bool omit = (!isLadder() && reverseState && state>0);
302 if(omit) {
303 loopState = false;
304 p.started = Started;
305 return;
306 }
307
308 // STAND -> S0
309 if(!setAnim(&npc, Anim::FromStand)) {
310 // some mobsi have no animations in G2 - ignore them
311 p.started = Quit;
312 p.attachMode = false;
313 return;
314 }
315
316 if(attach)
317 setState(std::min(stateNum,state+1)); else
318 setState(std::max(0,state-1));
319
320 loopState = false;
321 p.started = Started;
322 }
323
324 if(p.started==Quit) {
325 npc.quitInteraction();
326 loopState = false;
327 p.user = nullptr;
328 p.started = NotStarted;
329 return;
330 }
331
332 if(!loopState) {
333 if(stateNum==state && attach) {
334 invokeStateFunc(npc);
335 loopState = true;
336 }
337 if(0==state && !p.attachMode) {
338 invokeStateFunc(npc);
339 loopState = true;
340 }
341 }
342
343 if((state<=0 && !attach) || (isLadder() && (attach && state>=stepsCount-1))) {
344 implQuitInteract(p);
345 return;
346 }
347
348 if(needToLockpick(npc)) {
349 if(p.attachMode) {
350 npc.world().sendPassivePerc(npc,npc,PERC_ASSESSUSEMOB);
351 return; // chest is locked - need to crack lock first
352 }
353 }
354
355 Anim dir = (attach ? Anim::In : Anim::Out);
356 if(state==stateNum && attach)
357 dir = Anim::Active;
358
359 if(!loopState) {
360 if(!setAnim(&npc, dir))
361 return;
362 }
363
364 if(state==0 && p.attachMode) {
365 npc.world().sendPassivePerc(npc,npc,PERC_ASSESSUSEMOB);
367 }
368
369 if(state==stateNum && p.attachMode && reverseState) {
370 npc.world().sendPassivePerc(npc,npc,PERC_ASSESSUSEMOB);
372 }
373
374 if(npc.isPlayer() && !loopState && attach) {
375 invokeStateFunc(npc);
376 }
377
378 const int prev = state;
379 if(attach)
380 setState(std::min(stateNum,state+1)); else
381 setState(std::max(0,state-1));
382 loopState = (prev==state);
383 }
384
385void Interactive::implQuitInteract(Interactive::Pos &p) {
386 if(p.user==nullptr)
387 return;
388 // S[i] -> STAND
389 if(!setAnim(p.user, Anim::ToStand))
390 return;
391 // quit will be triggered with delay depend on animation
392 p.started = Quit;
393 }
394
395std::string_view Interactive::tag() const {
396 return vobName;
397 }
398
399std::string_view Interactive::focusName() const {
400 return focName;
401 }
402
403bool Interactive::checkMobName(std::string_view dest) const {
404 std::string_view scheme = schemeName();
405 if(scheme==dest)
406 return true;
407 return false;
408 }
409
410std::string_view Interactive::ownerName() const {
411 return owner;
412 }
413
415 return focOver;
416 }
417
418Tempest::Vec3 Interactive::displayPosition() const {
419 auto p = position();
420 return p+displayOffset;
421 }
422
423std::string_view Interactive::displayName() const {
424 if(focName.empty())
425 return "";
426
427 string_frm strId(focName);
428 if(world.script().findSymbolIndex(strId)==size_t(-1))
429 strId = string_frm("MOBNAME_",strId);
430
431 if(world.script().findSymbolIndex(strId)==size_t(-1))
432 return "";
433
434 auto* s=world.script().findSymbol(strId);
435 if(s==nullptr)
436 return "";
437
438 return s->get_string();
439 }
440
441const Tempest::Vec3* Interactive::bBox() const {
442 return bbox;
443 }
444
445bool Interactive::setMobState(std::string_view scheme, int32_t st) {
446 const bool ret = Vob::setMobState(scheme,st);
447 if(state==st)
448 return true;
449
450 if(schemeName()!=scheme)
451 return ret;
452
453 string_frm name("S_S",st);
454 if(visual.startAnimAndGet(name,world.tickCount())!=nullptr || !visual.isAnimExist(name)) {
455 setState(st);
456 return ret;
457 }
458 return false;
459 }
460
461void Interactive::invokeStateFunc(Npc& npc) {
462 if(onStateFunc.empty() || state<0)
463 return;
464 if(loopState)
465 return;
466
467 // TODO: hero only?
468 string_frm func(onStateFunc,"_S",state);
469 auto& sc = npc.world().script();
470 sc.useInteractive(npc.handlePtr(), func);
471 }
472
474 if(triggerTarget.empty())
475 return;
476 const TriggerEvent evt(triggerTarget,vobName,waitAnim,type);
477 world.triggerEvent(evt);
478 }
479
480void Interactive::emitSoundEffect(std::string_view sound, float range, bool freeSlot) {
481 auto sfx = ::Sound(world,::Sound::T_Regular,sound,position(),range,freeSlot);
482 sfx.play();
483 }
484
485std::string_view Interactive::schemeName() const {
486 if(auto mesh = visual.protoMesh())
487 return mesh->scheme;
488 Tempest::Log::i("unable to recognize mobsi{",focName,", ",mdlVisual,"}");
489 return "";
490 }
491
492std::string_view Interactive::posSchemeName() const {
493 for(auto& i:attPos)
494 if(i.user!=nullptr) {
495 return i.posTag();
496 }
497 return "";
498 }
499
501 return vobType==zenkit::VirtualObjectType::oCMobContainer;
502 }
503
505 return vobType==zenkit::VirtualObjectType::oCMobDoor;
506 }
507
508bool Interactive::isTrueDoor(const Npc& npc) const {
509 if(!isDoor())
510 return false;
511 for(int i=1; i<=stateNum; ++i)
512 if(canQuitAtState(npc,i))
513 return true;
514 return false;
515 }
516
518 return vobType==zenkit::VirtualObjectType::oCMobLadder;
519 }
520
521bool Interactive::needToLockpick(const Npc& pl) const {
522 const size_t keyInst = keyInstance.empty() ? size_t(-1) : world.script().findSymbolIndex(keyInstance);
523 if(keyInst!=size_t(-1) && pl.inventory().itemCount(keyInst)>0)
524 return false;
525 return !(pickLockStr.empty() || isLockCracked);
526 }
527
529 return invent;
530 }
531
532void Interactive::setSlotItem(MeshObjects::Mesh&& itm, std::string_view slot) {
533 visual.setSlotItem(std::move(itm),slot);
534 }
535
536uint32_t Interactive::stateMask() const {
537 std::string_view s = schemeName();
538 return world.script().schemeToBodystate(s);
539 }
540
541bool Interactive::canSeeNpc(const Npc& npc, bool freeLos) const {
542 for(auto& i:attPos){
543 auto pos = nodePosition(npc,i);
544 if(npc.canSeeNpc(pos,freeLos))
545 return true;
546 }
547
548 // graves
549 if(attPos.size()==0){
550 auto pos = displayPosition();
551 if(npc.canSeeNpc(pos,freeLos))
552 return true;
553 }
554 return false;
555 }
556
557Tempest::Vec3 Interactive::nearestPoint(const Npc& to) const {
558 if(auto p = findNearest(to))
559 return worldPos(*p);
560 return displayPosition();
561 }
562
563template<class P, class Inter>
564P* Interactive::findNearest(Inter& in, const Npc& to) {
565 float dist = 0;
566 P* p = nullptr;
567 for(auto& i:in.attPos) {
568 if(i.user || !i.isAttachPoint())
569 continue;
570 float d = in.qDistTo(to,i);
571 if(d<dist || p==nullptr) {
572 p = &i;
573 dist = d;
574 }
575 }
576 return p;
577 }
578
579const Interactive::Pos* Interactive::findNearest(const Npc& to) const {
580 return findNearest<const Interactive::Pos, const Interactive>(*this,to);
581 }
582
583Interactive::Pos* Interactive::findNearest(const Npc& to) {
584 return findNearest<Interactive::Pos, Interactive>(*this,to);
585 }
586
587void Interactive::implAddItem(std::string_view name) {
588 size_t sep = name.find(':');
589 if(sep!=std::string::npos) {
590 auto itm = name.substr(0,sep);
591 long count = std::strtol(name.data()+sep+1,nullptr,10);
592 if(count>0)
593 invent.addItem(itm,size_t(count),world);
594 } else {
595 invent.addItem(name,1,world);
596 }
597 }
598
599void Interactive::autoDetachNpc() {
600 for(auto& i:attPos)
601 if(i.user!=nullptr) {
602 if(!i.user->world().isInDialog())
603 i.user->setInteraction(nullptr);
604 }
605 }
606
607bool Interactive::checkUseConditions(Npc& npc) {
608 const bool isPlayer = npc.isPlayer();
609
610 auto& sc = world.script();
611
612 if(isPlayer) {
613 const bool g1 = Gothic::inst().version().game==1;
614 const size_t ItKE_lockpick = sc.lockPickId();
615 const size_t lockPickCnt = npc.inventory().itemCount(ItKE_lockpick);
616 const bool canLockPick = ((g1 || npc.talentSkill(TALENT_PICKLOCK)!=0) && lockPickCnt>0);
617
618 const size_t keyInst = keyInstance.empty() ? size_t(-1) : sc.findSymbolIndex(keyInstance);
619 const bool needToPicklock = (pickLockStr.size()>0);
620
621 if(keyInst!=size_t(-1) && (isLockCracked || npc.itemCount(keyInst)>0)) {
622 isLockCracked = true;
623 return true;
624 }
625 if(needToPicklock && (isLockCracked || canLockPick)) {
626 return true;
627 }
628
629 if(keyInst!=size_t(-1) && needToPicklock) { // key+lockpick
630 sc.printMobMissingKeyOrLockpick(npc);
631 return false;
632 }
633 else if(keyInst!=size_t(-1)) { // key-only
634 sc.printMobMissingKey(npc);
635 return false;
636 }
637 else if(needToPicklock) { // lockpick only
638 sc.printMobMissingLockpick(npc);
639 return false;
640 }
641
642 if(!conditionFunc.empty()) {
643 const int check = sc.invokeCond(npc,conditionFunc);
644 if(check==0)
645 return false;
646 }
647
648 if(!useWithItem.empty()) {
649 size_t it = sc.findSymbolIndex(useWithItem);
650 if(it!=size_t(-1) && npc.itemCount(it)==0) {
651 sc.printMobMissingItem(npc);
652 return false;
653 }
654 }
655 }
656 return true;
657 }
658
659const Interactive::Pos *Interactive::findFreePos() const {
660 for(auto& i:attPos)
661 if(i.user==nullptr && i.isAttachPoint()) {
662 return &i;
663 }
664 return nullptr;
665 }
666
667Interactive::Pos *Interactive::findFreePos() {
668 for(auto& i:attPos)
669 if(i.user==nullptr && i.isAttachPoint()) {
670 return &i;
671 }
672 return nullptr;
673 }
674
675Tempest::Vec3 Interactive::worldPos(const Interactive::Pos &to) const {
676 auto mesh = visual.protoMesh();
677 if(mesh==nullptr)
678 return Tempest::Vec3();
679
680 auto mat = transform();
681 auto pos = mesh->mapToRoot(to.node);
682 mat.mul(pos);
683
684 Tempest::Vec3 ret = {};
685 mat.project(ret);
686 return ret;
687 }
688
690 for(auto& i:attPos)
691 if(i.user!=nullptr)
692 return false;
693 return findFreePos()!=nullptr;
694 }
695
697 for(auto& i:attPos)
698 if(i.user!=nullptr) {
699 if(i.attachMode && i.started==Started)
700 return loopState;
701 return false;
702 }
703 return loopState;
704 }
705
706bool Interactive::isDetachState(const Npc& npc) const {
707 for(auto& i:attPos)
708 if(i.user==&npc)
709 return !(i.attachMode ^ reverseState);
710 return !reverseState;
711 }
712
713bool Interactive::canQuitAtState(const Npc& npc, int32_t state) const {
714 if(state<0)
715 return true;
716
717 auto scheme = schemeName();
718 auto pos = posSchemeName();
719
720 string_frm anim;
721 if(pos.empty())
722 anim = string_frm("T_",scheme,"_S",state,"_2_STAND"); else
723 anim = string_frm("T_",scheme,"_",pos,"_S",state,"_2_STAND");
724
725 // should match with this->animNpc(npc,Interactive::ToStand);
726 if(npc.hasAnim(anim))
727 return true;
728 return state==stateNum && reverseState;
729 }
730
731bool Interactive::attach(Npc& npc, Interactive::Pos& to) {
732 assert(to.user==nullptr);
733
734 auto mat = nodeTranform(npc,to);
735 float x=0, y=0, z=0;
736 mat.project(x,y,z);
737
738 const Tempest::Vec3 mv = {x,y-npc.translateY(),z};
739
740 if((npc.position()-mv).quadLength()>MAX_AI_USE_DISTANCE*MAX_AI_USE_DISTANCE) {
741 if(npc.isPlayer()) {
742 auto& sc = npc.world().script();
743 sc.printMobTooFar(npc);
744 }
745 if(npc.isPlayer())
746 return false; // TODO: same for npc
747 }
748
749 if(!checkUseConditions(npc))
750 return false;
751
752 if(!useWithItem.empty()) {
753 size_t it = world.script().findSymbolIndex(useWithItem);
754 npc.setCurrentItem(it);
755 }
756
757 if(!setPos(npc,mv))
758 return false;
759
760 setDir(npc,mat);
761
762 if(vobType==zenkit::VirtualObjectType::oCMobLadder) {
763 if(&to!=&attPos[0])
764 state = -1; else
765 state = stepsCount;
766 loopState = false;
767 }
768
769 if(state>0) {
770 reverseState = (state>0);
771 } else {
772 reverseState = false;
773 state = -1;
774 }
775
776 to.user = &npc;
777 to.started = NotStarted;
778 to.attachMode = true;
779 return true;
780 }
781
783 for(auto& i:attPos)
784 if(i.user==&npc)
785 return true;
786
787 if(!isAvailable()) {
788 if(npc.isPlayer() && !attPos.empty())
790 return false;
791 }
792
793 auto p = findNearest(npc);
794 if(p!=nullptr)
795 return attach(npc,*p);
796
797 if(npc.isPlayer() && !attPos.empty())
799 return false;
800 }
801
802bool Interactive::detach(Npc &npc, bool quick) {
803 for(auto& i:attPos) {
804 if(i.user==&npc && i.attachMode) {
805 if(quick || canQuitAtState(*i.user,state)) {
806 if(!quick) {
807 auto sq = npc.setAnimAngGet(Npc::Anim::InteractToStand);
808 if(sq==nullptr)
809 return false;
810 }
811 i.user = nullptr;
812 i.started = (canQuitAtState(npc,state) || !quick) ? NotStarted : Quit;
813 i.attachMode = false;
814 loopState = false;
815 npc.quitInteraction();
816 return true;
817 }
818 else {
819 i.attachMode = false;
820 loopState = false;
821 return false;
822 }
823 return false;
824 }
825 }
826
827 return npc.interactive()==nullptr;
828 }
829
831 for(auto& i:attPos)
832 if(i.user==&to)
833 return true;
834 return false;
835 }
836
837bool Interactive::setPos(Npc &npc,const Tempest::Vec3& pos) {
838 auto prev = npc.position();
839 npc.setPosition(pos);
840 world.script().fixNpcPosition(npc,0,0);
841 if(npc.hasCollision()) {
842 npc.setPosition(prev);
843 return false;
844 }
845 return true;
846 }
847
848void Interactive::setDir(Npc &npc, const Tempest::Matrix4x4 &mat) {
849 float x0=0,y0=0,z0=0;
850 float x1=0,y1=0,z1=1;
851
852 mat.project(x0,y0,z0);
853 mat.project(x1,y1,z1);
854
855 npc.setDirection(Tempest::Vec3(x1-x0, y1-y0, z1-z0));
856 }
857
858float Interactive::qDistTo(const Npc &npc, const Interactive::Pos &to) const {
859 auto p = worldPos(to);
860 return npc.qDistTo(p);
861 }
862
863Tempest::Matrix4x4 Interactive::nodeTranform(const Npc &npc, const Pos& p) const {
864 auto mesh = visual.protoMesh();
865 if(mesh==nullptr)
866 return Tempest::Matrix4x4();
867
868 auto nodeId = mesh->findNode(p.name);
869 if(p.isDistPos()) {
870 auto pos = position();
871 Tempest::Matrix4x4 npos;
872 if(nodeId!=size_t(-1)) {
873 npos = visual.bone(nodeId);
874 } else {
875 npos.identity();
876 }
877 float nodeX = npos.at(3,0) - pos.x;
878 float nodeY = npos.at(3,1) - pos.y;
879 float nodeZ = npos.at(3,2) - pos.z;
880 float dist = std::sqrt(nodeX*nodeX + nodeZ*nodeZ);
881
882 float npcX = npc.position().x - pos.x;
883 float npcZ = npc.position().z - pos.z;
884 float npcA = 180.f*std::atan2(npcZ,npcX)/float(M_PI);
885
886 npos.identity();
887 npos.rotateOY(-npcA);
888 npos.translate(dist,nodeY,0);
889 npos.rotateOY(-90);
890
891 float x = pos.x+npos.at(3,0);
892 float y = pos.y+npos.at(3,1);
893 float z = pos.z+npos.at(3,2);
894 npos.set(3,0,x);
895 npos.set(3,1,y);
896 npos.set(3,2,z);
897 return npos;
898 }
899
900 if(nodeId!=size_t(-1))
901 return visual.bone(nodeId);
902
903 return transform();
904 }
905
906Tempest::Vec3 Interactive::nodePosition(const Npc &npc, const Pos &p) const {
907 auto mat = nodeTranform(npc,p);
908 float x = mat.at(3,0);
909 float y = mat.at(3,1);
910 float z = mat.at(3,2);
911 return {x,y,z};
912 }
913
914Tempest::Matrix4x4 Interactive::nodeTranform(std::string_view nodeName) const {
915 auto mesh = visual.protoMesh();
916 if(mesh==nullptr || mesh->skeleton==nullptr)
917 return Tempest::Matrix4x4();
918
919 auto id = mesh->skeleton->findNode(nodeName);
920 auto ret = transform();
921 if(id!=size_t(-1))
922 ret = visual.bone(id);
923 return ret;
924 }
925
926const Animation::Sequence* Interactive::setAnim(Interactive::Anim t) {
927 int dir = (t==Anim::Out || t==Anim::ToStand) ? -1 : 1;
928 int st[] = {state,state+dir};
929 char ss[2][12] = {};
930
931 st[1] = std::max(0,std::min(st[1],stateNum));
932
933 char buf[256]={};
934 for(int i=0;i<2;++i) {
935 if(st[i]<0)
936 std::snprintf(ss[i],sizeof(ss[i]),"S0"); else
937 std::snprintf(ss[i],sizeof(ss[i]),"S%d",st[i]);
938 }
939
940 if(st[0]<0 || st[1]<0)
941 std::snprintf(buf,sizeof(buf),"S_S0"); else
942 if(st[0]==st[1])
943 std::snprintf(buf,sizeof(buf),"S_%s",ss[0]); else
944 std::snprintf(buf,sizeof(buf),"T_%s_2_%s",ss[0],ss[1]);
945
946 return visual.startAnimAndGet(buf,world.tickCount());
947 }
948
949bool Interactive::setAnim(Npc* npc, Anim dir) {
950 const Npc::Anim dest = toNpcAnim(dir);
951 const Animation::Sequence* sqNpc = nullptr;
952 const Animation::Sequence* sqMob = nullptr;
953
954 if(npc!=nullptr) {
955 sqNpc = npc->setAnimAngGet(dest);
956 // NOTE: Book-stand has no 'out' animation
957 if(sqNpc==nullptr && !(vobType==zenkit::VirtualObjectType::oCMobInter && dir==Anim::Out))
958 return false;
959 }
960 sqMob = setAnim(dir);
961 if(sqMob==nullptr && sqNpc==nullptr && dir!=Anim::Out)
962 return false;
963
964 uint64_t aniT = sqNpc==nullptr ? 0 : uint64_t(sqNpc->totalTime());
965 if(aniT==0) {
966 /* Note: testing shows that in vanilla only npc animation matters.
967 * testcase: chest animation
968 * modsi timings only here for completeness
969 */
970 aniT = sqMob==nullptr ? 0 : uint64_t(sqMob->totalTime());
971 }
972
973 if(dir!=Anim::Active)
974 waitAnim = world.tickCount()+aniT;
975 return true;
976 }
977
978void Interactive::setState(int st) {
979 state = st;
981 }
982
984 std::string_view tag = schemeName();
985 int st[] = {state,state+t};
986 string_frm<12> ss[2] = {};
987 string_frm pointBuf = {};
988 string_frm point = {};
989
990 if(t==Anim::FromStand) {
991 st[0] = -1;
992 st[1] = state<1 ? 0 : stateNum - 1;
993 }
994 else if(t==Anim::ToStand) {
995 st[0] = state<1 ? 0 : state;
996 st[1] = -1;
997 }
998
999 for(auto& i:attPos)
1000 if(i.user!=nullptr) {
1001 point = string_frm("_",i.posTag());
1002 }
1003
1004 st[1] = std::max(-1,std::min(st[1],stateNum));
1005
1006 for(int i=0;i<2;++i) {
1007 if(st[i]<0)
1008 ss[i] = "STAND"; else
1009 ss[i] = string_frm("S",st[i]);
1010 }
1011
1012 string_frm buf = {};
1013 for(auto pt:{std::string_view(point),std::string_view()}) {
1014 if(st[0]==st[1])
1015 buf = string_frm("S_",tag,pt,ss[0]); else
1016 buf = string_frm("T_",tag,pt,"_",ss[0],"_2_",ss[1]);
1017 if(auto ret = solver.solveFrm(buf))
1018 return ret;
1019 }
1020 return nullptr;
1021 }
1022
1024 p.setBrush(Tempest::Color(1.0,0,0,1));
1025
1026 for(auto& m:attPos) {
1027 auto pos = worldPos(m);
1028
1029 float x = pos.x;
1030 float y = pos.y;
1031 float z = pos.z;
1032 p.mvp.project(x,y,z);
1033
1034 x = (0.5f*x+0.5f)*float(p.w);
1035 y = (0.5f*y+0.5f)*float(p.h);
1036
1037 p.painter.drawRect(int(x),int(y),1,1);
1038 p.drawText(int(x), int(y), schemeName().data());
1039 }
1040
1041 if(attPos.size()==0) {
1042 auto pos = displayPosition();
1043
1044 float x = pos.x;
1045 float y = pos.y;
1046 float z = pos.z;
1047 p.mvp.project(x,y,z);
1048
1049 x = (0.5f*x+0.5f)*float(p.w);
1050 y = (0.5f*y+0.5f)*float(p.h);
1051
1052 p.painter.drawRect(int(x),int(y),1,1);
1053 p.drawText(int(x), int(y), schemeName().data());
1054 }
1055 }
1056
1059 visual.setObjMatrix(transform());
1060 }
1061
1063 float x = bbox[1].x-bbox[0].x;
1064 float y = bbox[1].y-bbox[0].y;
1065 float z = bbox[1].z-bbox[0].z;
1066
1067 return std::max(x,std::max(y,z));
1068 }
1069
1070std::string_view Interactive::Pos::posTag() const {
1071 if(name.rfind("_FRONT")==name.size()-6)
1072 return "FRONT";
1073 if(name.rfind("_BACK")==name.size()-5)
1074 return "BACK";
1075 return "";
1076 }
1077
1078bool Interactive::Pos::isAttachPoint() const {
1079 /*
1080 ZS_POS0, ZS_POS0_FRONT, ZS_POS0_BACK, ZS_POS0_DIST
1081 ZS_POS1, ZS_POS1_FRONT, ZS_POS1_BACK,
1082 ZS_POS2, ZS_POS3, ...
1083 */
1084 return name.find("ZS_POS")==0;
1085 }
1086
1087bool Interactive::Pos::isDistPos() const {
1088 return name.rfind("_DIST")==name.size()-5;
1089 }
const Animation::Sequence * solveFrm(std::string_view format) const
void setBrush(const Tempest::Brush &brush)
void drawText(int x, int y, std::string_view txt)
const int w
Definition dbgpainter.h:20
Tempest::Painter & painter
Definition dbgpainter.h:18
const int h
Definition dbgpainter.h:21
const Tempest::Matrix4x4 mvp
Definition dbgpainter.h:19
size_t findSymbolIndex(std::string_view s)
void printMobAnotherIsUsing(Npc &npc)
void fixNpcPosition(Npc &npc, float angle0, float distBias)
zenkit::DaedalusSymbol * findSymbol(std::string_view s)
BodyState schemeToBodystate(std::string_view sc)
static Gothic & inst()
Definition gothic.cpp:249
auto version() const -> const VersionInfo &
Definition gothic.cpp:263
void postValidate()
bool setMobState(std::string_view scheme, int32_t st) override
Interactive(Vob *parent, World &world, const zenkit::VMovableObject &vob, Flags flags)
bool isDoor() const
bool canSeeNpc(const Npc &npc, bool freeLos) const
float extendedSearchRadius() const override
bool isAvailable() const
bool attach(Npc &npc)
std::string_view tag() const
bool checkMobName(std::string_view dest) const
std::string_view ownerName() const
Tempest::Vec3 nearestPoint(const Npc &to) const
std::string_view posSchemeName() const
bool isContainer() const
Tempest::Matrix4x4 nodeTranform(std::string_view nodeName) const
bool canQuitAtState(const Npc &npc, int32_t state) const
bool isLadder() const
bool isDetachState(const Npc &npc) const
virtual void onStateChanged()
Definition interactive.h:92
void tick(uint64_t dt)
void resetPositionToTA(int32_t state)
bool overrideFocus() const
void setSlotItem(MeshObjects::Mesh &&itm, std::string_view slot)
bool isTrueDoor(const Npc &npc) const
void emitSoundEffect(std::string_view sound, float range, bool freeSlot)
void onKeyInput(KeyCodec::Action act)
void emitTriggerEvent(TriggerEvent::Type type) const
bool detach(Npc &npc, bool quick)
uint32_t stateMask() const
std::string_view schemeName() const
auto animNpc(const AnimationSolver &solver, Anim t) const -> const Animation::Sequence *
bool isStaticState() const
std::string_view focusName() const
Inventory & inventory()
Tempest::Vec3 displayPosition() const
void load(Serialize &fin) override
bool isAttached(const Npc &to)
void save(Serialize &fout) const override
void updateAnimation(uint64_t dt)
std::string_view displayName() const
void moveEvent() override
bool needToLockpick(const Npc &pl) const
auto bBox() const -> const Tempest::Vec3 *
void marchInteractives(DbgPainter &p) const
void clear(GameScript &vm, Npc &owner, bool includeMissionItm=false)
bool isEmpty() const
Definition inventory.cpp:90
Item * addItem(std::unique_ptr< Item > &&p)
void load(Serialize &s, Npc &owner)
void save(Serialize &s) const
@ ActionGeneric
Definition keycodec.h:45
@ Forward
Definition keycodec.h:37
Definition npc.h:25
float qDistTo(const Tempest::Vec3 pos) const
float translateY() const
Definition npc.cpp:680
const std::shared_ptr< zenkit::INpc > & handlePtr() const
Definition npc.h:328
bool canSeeNpc(const Npc &oth, bool freeLos) const
Definition npc.cpp:4403
size_t itemCount(size_t id) const
Definition npc.cpp:3454
void setDirection(const Tempest::Vec3 &pos)
Definition npc.cpp:436
int32_t talentSkill(Talent t) const
Definition npc.cpp:1132
bool setPosition(float x, float y, float z)
Definition npc.cpp:388
bool hasAnim(std::string_view scheme) const
Definition npc.cpp:991
auto setAnimAngGet(Anim a) -> const Animation::Sequence *
Definition npc.cpp:945
auto inventory() const -> const Inventory &
Definition npc.h:330
auto interactive() const -> Interactive *
Definition npc.h:294
bool isPlayer() const
Definition npc.cpp:539
auto world() -> World &
Definition npc.cpp:624
auto position() const -> Tempest::Vec3
Definition npc.cpp:628
void setCurrentItem(size_t item)
Definition npc.cpp:3478
bool hasCollision() const
Definition npc.h:320
void quitInteraction()
Definition npc.cpp:4124
void setObjMatrix(const Tempest::Matrix4x4 &obj)
void syncPhysics()
void save(Serialize &fout, const Interactive &mob) const
Definition objvisual.cpp:63
void load(Serialize &fin, Interactive &mob)
Definition objvisual.cpp:68
void setSlotItem(MeshObjects::Mesh &&itm, std::string_view slot)
bool updateAnimation(Npc *npc, Interactive *mobsi, World &world, uint64_t dt, bool force)
bool isAnimExist(std::string_view name) const
void setVisual(const zenkit::IItem &visual, World &world, bool staticDraw)
void setInteractive(Interactive *it)
const ProtoMesh * protoMesh() const
const Animation::Sequence * startAnimAndGet(std::string_view name, uint64_t tickCount, bool force=false)
void processLayers(World &world)
const Tempest::Matrix4x4 & bone(size_t i) const
void write(const Arg &... a)
Definition serialize.h:76
bool setEntry(const Args &... args)
Definition serialize.h:57
void read(Arg &... a)
Definition serialize.h:81
std::string_view worldName() const
Definition serialize.cpp:87
Definition sound.h:5
@ T_Regular
Definition sound.h:8
Definition vob.h:11
virtual void save(Serialize &fout) const
Definition vob.cpp:258
virtual bool setMobState(std::string_view scheme, int32_t st)
Definition vob.cpp:78
uint32_t vobObjectID
Definition vob.h:47
virtual void moveEvent()
Definition vob.cpp:85
zenkit::VirtualObjectType vobType
Definition vob.h:46
Flags
Definition vob.h:13
@ Startup
Definition vob.h:15
World & world
Definition vob.h:45
static std::unique_ptr< Vob > load(Vob *parent, World &world, const zenkit::VirtualObject &vob, Flags flags)
Definition vob.cpp:127
auto transform() const -> const Tempest::Matrix4x4 &
Definition vob.h:34
Tempest::Vec3 position() const
Definition vob.cpp:57
Definition world.h:31
void triggerEvent(const TriggerEvent &e)
Definition world.cpp:495
uint64_t tickCount() const
Definition world.cpp:387
GameScript & script() const
Definition world.cpp:1019
void addInteractive(Interactive *inter)
Definition world.cpp:808
@ TALENT_PICKLOCK
Definition constants.h:441
@ MAX_AI_USE_DISTANCE
Definition constants.h:129
@ PERC_ASSESSUSEMOB
Definition constants.h:431
static Npc::Anim toNpcAnim(Interactive::Anim dir)
float totalTime() const