1 struct rpgobj;
2 
3 struct rpgaction
4 {
5     rpgaction *next;
6     char *initiate, *script;
7     rpgquest *q;
8     bool used;
9 
nextrpgaction10     rpgaction(char *_i = NULL, char *_s = NULL, rpgaction *_n = NULL) : next(_n), initiate(_i), script(_s), q(NULL), used(false) {}
11 
~rpgactionrpgaction12     ~rpgaction() { DELETEP(next); }
13 
execrpgaction14     void exec(rpgobj *obj, rpgobj *target, rpgobj *user)
15     {
16         if(!*script) return;
17         pushobj(user);
18         pushobj(target);
19         pushobj(obj);
20         execute(script);
21         used = true;
22     }
23 };
24 
25 struct rpgobj : g3d_callback, stats
26 {
27     rpgobj *parent;     // container object, if not top level
28     rpgobj *inventory;  // contained objects, if any
29     rpgobj *sibling;    // used for linking, if contained
30 
31     rpgent *ent;        // representation in the world, if top level
32 
33     const char *name;   // name it was spawned as
34     const char *model;  // what to display it as
35 
36     enum
37     {
38         IF_INVENTORY = 1,   // parent owns this object, will contribute to parent stats
39         IF_LOOT      = 2,   // if parent dies, this object should drop to the ground
40         IF_TRADE     = 4,   // parent has this item available for trade, for player, all currently unused weapons etc are of this type
41     };
42 
43     enum
44     {
45         MENU_DEFAULT,
46         MENU_BUY,
47         MENU_SELL
48     };
49 
50     int itemflags;
51 
52     rpgaction *actions, action_use;
53     char *abovetext;
54 
55     int menutime, menutab, menuwhich;
56 
57     #define loopinventory() for(rpgobj *o = inventory; o; o = o->sibling)
58     #define loopinventorytype(T) loopinventory() if(o->itemflags&(T))
59 
rpgobjrpgobj60     rpgobj(const char *_name) : parent(NULL), inventory(NULL), sibling(NULL), ent(NULL), name(_name), model(NULL), itemflags(IF_INVENTORY),
61         actions(NULL), abovetext(NULL), menutime(0), menutab(1), menuwhich(MENU_DEFAULT) {}
62 
~rpgobjrpgobj63     ~rpgobj()
64     {
65         DELETEP(inventory);
66         DELETEP(sibling);
67         DELETEP(ent);
68         DELETEP(actions);
69     }
70 
scriptinitrpgobj71     void scriptinit()
72     {
73         DELETEP(inventory);
74         defformatstring(aliasname, "spawn_%s", name);
75         execident(aliasname);
76     }
77 
decontainrpgobj78     void decontain()
79     {
80         if(parent) parent->remove(this);
81     }
82 
addrpgobj83     void add(rpgobj *o, int itemflags)
84     {
85         o->sibling = inventory;
86         o->parent = this;
87         inventory = o;
88         o->itemflags = itemflags;
89 
90         if(itemflags&IF_INVENTORY) recalcstats();
91     }
92 
removerpgobj93     void remove(rpgobj *o)
94     {
95         for(rpgobj **l = &inventory; *l; )
96             if(*l==o)
97             {
98                 *l = o->sibling;
99                 o->sibling = o->parent = NULL;
100             }
101             else l = &(*l)->sibling;
102 
103         if(o->itemflags&IF_INVENTORY) recalcstats();
104     }
105 
recalcstatsrpgobj106     void recalcstats()
107     {
108         st_reset();
109         loopinventorytype(IF_INVENTORY) st_accumulate(*o);
110     }
111 
selectedweaponrpgobj112     rpgobj &selectedweapon()
113     {
114         if(this==playerobj) return selected ? *selected : *this;
115         else { loopinventorytype(IF_INVENTORY) if(o->s_usetype) return *o; };
116         return *this;
117     }
118 
placedinworldrpgobj119     void placedinworld(rpgent *_ent)
120     {
121         if(!model) model = "tentus/moneybag";
122         ent = _ent;
123         setbbfrommodel(ent, model);
124         entinmap(ent);
125         //ASSERT(!(ent->o.x<0 || ent->o.y<0 || ent->o.z<0 || ent->o.x>4096 || ent->o.y>4096 || ent->o.z>4096));
126         st_init();
127     }
128 
renderrpgobj129     void render()
130     {
131         if(s_ai)
132         {
133             float sink = 0;
134             if(ent->physstate>=PHYS_SLIDE)
135                 sink = raycube(ent->o, vec(0, 0, -1), 2*ent->eyeheight)-ent->eyeheight;
136             ent->sink = ent->sink*0.8 + sink*0.2;
137             //if(ent->blocked) particle_splash(PART_SPARK, 100, 100, ent->o, 0xB49B4B, 0.24f);
138             float oldheight = ent->eyeheight;
139             ent->eyeheight += ent->sink;
140             renderclient(ent, model, NULL, 0, ANIM_ATTACK1, 300, ent->lastaction, 0);
141             ent->eyeheight = oldheight;
142             if(s_hp<eff_maxhp() && ent->state==CS_ALIVE) particle_meter(ent->abovehead(), s_hp/(float)eff_maxhp(), PART_METER, 1, 0xFF1932, 0xFFFFFF, 2.0f);
143 
144         }
145         else
146         {
147             rendermodel(NULL, model, ANIM_MAPMODEL|ANIM_LOOP, vec(ent->o).sub(vec(0, 0, ent->eyeheight)), ent->yaw+90, 0, MDL_CULL_VFC | MDL_CULL_DIST | MDL_CULL_OCCLUDED | MDL_LIGHT, ent);
148         }
149     }
150 
updaterpgobj151     void update()
152     {
153         float dist = ent->o.dist(player1->o);
154         if(s_ai) { ent->update(dist); st_update(); };
155         moveplayer(ent, 1, true);    // 10 or above gets blocked less, because physics accuracy doesn't need extra tests
156         //ASSERT(!(ent->o.x<0 || ent->o.y<0 || ent->o.z<0 || ent->o.x>4096 || ent->o.y>4096 || ent->o.z>4096));
157         if(!menutime && dist<(s_ai ? 40 : 24) && ent->state==CS_ALIVE && s_ai<2) { menutime = starttime(); menuwhich = MENU_DEFAULT; }
158         else if(dist>(s_ai ? 96 : 48)) menutime = 0;
159     }
160 
addactionrpgobj161     void addaction(char *initiate, char *script, bool startquest)
162     {
163         for(rpgaction *a = actions; a; a = a->next) if(strcmp(a->initiate, initiate)==0) return;
164         actions = new rpgaction(initiate, script, actions);
165         if(startquest) actions->q = addquest(abovetext, name);
166     }
167 
droplootrpgobj168     void droploot()
169     {
170         loopinventorytype(IF_LOOT)
171         {
172             o->decontain();
173             pushobj(o);
174             placeinworld(ent->o, rnd(360));
175             droploot();
176             return;
177         }
178     }
179 
takerpgobj180     rpgobj *take(char *name)
181     {
182         loopinventory() if(strcmp(o->name, name)==0)
183         {
184             o->decontain();
185             return o;
186         }
187         return NULL;
188     }
189 
takedamagerpgobj190     void takedamage(int damage, rpgobj &attacker)
191     {
192         ent->enemy = attacker.ent;
193 
194         particle_splash(PART_BLOOD, max(damage/10, 1), 1000, ent->o, 0x60FFFF, 2.96f);
195         defformatstring(ds, "%d", damage);
196         particle_textcopy(ent->abovehead(), ds, PART_TEXT, 2000, 0xFF4B19, 4.0f, -8);
197 
198         if((s_hp -= damage)<=0)
199         {
200             s_hp = 0;
201             ent->state = CS_DEAD;
202             ent->attacking = false;
203             ent->lastaction = lastmillis;
204             menutime = 0;
205             conoutf("%s killed: %s", attacker.name, name);
206             droploot();
207         }
208     }
209 
usesoundrpgobj210     void usesound(rpgent *user)
211     {
212         if(s_usesound) playsound(s_usesound, &user->o);
213     }
214 
usemanarpgobj215     bool usemana(rpgobj &o)
216     {
217         if(o.s_manacost>s_mana) { if(this==playerobj) conoutf("\f2not enough mana"); return false; };
218         s_mana -= o.s_manacost;
219         o.usesound(ent);
220         return true;
221     }
222 
useactionrpgobj223     void useaction(rpgobj &target, rpgent &initiator, bool chargemana)
224     {
225         if(action_use.script && (!chargemana || initiator.ro->usemana(*this)))
226         {
227             action_use.exec(this, &target, initiator.ro);
228             if(s_useamount && !--s_useamount) removefromsystem(this);
229         }
230     }
231 
selectuserpgobj232     void selectuse()
233     {
234         if(s_usetype)
235         {
236             conoutf("\f2using: %s", name);
237             selected = this;
238         }
239         else
240         {
241             useaction(*playerobj, *playerobj->ent, true);
242         }
243     }
244 
guiactionrpgobj245     void guiaction(g3d_gui &g, rpgaction *a)
246     {
247         if(!a) return;
248         guiaction(g, a->next);
249         if(g.button(a->initiate, a->used ? 0xAAAAAA : 0xFFFFFF, "chat")&G3D_UP)
250         {
251             currentquest = a->q;
252             a->exec(this, playerobj, playerobj);
253             currentquest = NULL;
254         }
255     }
256 
guirpgobj257     void gui(g3d_gui &g, bool firstpass)
258     {
259         g.start(menutime, 0.015f, &menutab);
260         switch(menuwhich)
261         {
262             case MENU_DEFAULT:
263             {
264                 g.tab(name, 0xFFFFFF);
265                 if(abovetext) g.text(abovetext, 0xDDFFDD);
266 
267                 guiaction(g, actions);
268 
269                 if(s_ai)
270                 {
271                     bool trader = false;
272                     loopinventorytype(IF_TRADE) trader = true;
273                     if(trader)
274                     {
275                         if(g.button("buy",  0xFFFFFF, "coins")&G3D_UP) menuwhich = MENU_BUY;
276                         if(g.button("sell", 0xFFFFFF, "coins")&G3D_UP) menuwhich = MENU_SELL;
277                     }
278                 }
279                 else
280                 {
281                     defformatstring(wtext, "worth %d", s_worth);
282                     g.text(wtext, 0xAAAAAA, "coins");
283                     if(g.button("take", 0xFFFFFF, "hand")&G3D_UP)
284                     {
285                         conoutf("\f2you take a %s (worth %d gold)", name, s_worth);
286                         taken(this, playerobj);
287                     }
288                     if(!s_usetype && g.button("use", 0xFFFFFF, "hand")&G3D_UP)
289                     {
290                         selectuse();
291                     }
292                 }
293                 break;
294             }
295 
296             case MENU_BUY:
297             {
298                 defformatstring(info, "buying from: %s", name);
299                 g.tab(info, 0xFFFFFF);
300                 loopinventorytype(IF_TRADE)
301                 {
302                     int price = o->s_worth;
303                     defformatstring(info, "%s (%d)", o->name, price);
304                     int ret = g.button(info, 0xFFFFFF, "coins");
305                     if(ret&G3D_UP)
306                     {
307                         if(playerobj->s_gold>=price)
308                         {
309                             conoutf("\f2you bought %s for %d gold", o->name, price);
310                             playerobj->s_gold -= price;
311                             s_gold += price;
312                             o->decontain();
313                             playerobj->add(o, IF_INVENTORY);
314                         }
315                         else
316                         {
317                             conoutf("\f2you cannot afford this item!");
318                         }
319                     }
320                 }
321                 formatstring(info, "you have %d gold", playerobj->s_gold);
322                 g.text(info, 0xAAAAAA, "info");
323                 if(g.button("done buying", 0xFFFFFF, "coins")&G3D_UP) menuwhich = MENU_DEFAULT;
324                 break;
325             }
326 
327             case MENU_SELL:
328             {
329                 defformatstring(info, "selling to: %s", name);
330                 g.tab(info, 0xFFFFFF);
331                 playerobj->invgui(g, this);
332                 if(g.button("done selling", 0xFFFFFF, "coins")&G3D_UP) menuwhich = MENU_DEFAULT;
333                 break;
334             }
335         }
336         g.end();
337     }
338 
339     void invgui(g3d_gui &g, rpgobj *buyer = NULL)
340     {
loopinventoryrpgobj341         loopinventory()
342         {
343             int price = o->s_worth/2;
344             defformatstring(info, "%s (%d)", o->name, price);
345             int ret = g.button(info, 0xFFFFFF, "coins");
346             if(ret&G3D_UP)
347             {
348                 if(buyer)
349                 {
350                     if(price>buyer->s_gold)
351                     {
352                         conoutf("\f2%s cannot afford to buy %s from you!", buyer->name, o->name);
353                     }
354                     else
355                     {
356                         if(price)
357                         {
358                             conoutf("\f2you sold %s for %d gold", o->name, price);
359                             s_gold += price;
360                             buyer->s_gold -= price;
361                             o->decontain();
362                             buyer->add(o, IF_TRADE);
363                         }
364                         else
365                         {
366                             conoutf("\f2you cannot sell %s", o->name);
367                         }
368                     }
369                 }
370                 else    // player wants to use this item
371                 {
372                     o->selectuse();
373                 }
374             }
375         }
376         defformatstring(info, "you have %d gold", playerobj->s_gold);
377         g.text(info, 0xAAAAAA, "info");
378     }
379 
g3d_menurpgobj380     void g3d_menu()
381     {
382         if(!menutime) return;
383         g3d_addgui(this, vec(ent->o).add(vec(0, 0, 2)));
384     }
385 };
386