blob: 1e17201646676ec1fca55bb2dda8020b4b7327eb [file] [log] [blame]
MG Mud User88f12472016-06-24 23:31:02 +02001// MorgenGrauen MUDlib
2//
3// npc/combat.c -- NPC-spezifische Kampffunktionen
4//
5// $Id: combat.c 9488 2016-02-19 21:24:04Z Arathorn $
6#pragma strong_types
7#pragma save_types
8#pragma range_check
9#pragma no_clone
MG Mud User88f12472016-06-24 23:31:02 +020010
11inherit "std/living/combat";
12
13#include <combat.h>
14#include <language.h>
15#include <properties.h>
16#include <wizlevels.h>
17#include <health.h>
18#include <new_skills.h>
19
20#define NEED_PROTOTYPES 1
21#include <living/life.h>
22#undef NEED_PROTOTYPES
23
24#define HB_CHECK 7
25#define ME this_object()
26#define STATMASTER "/p/service/rochus/guildstat/master"
27
28nosave int heartbeat, beatcount;
29
30private void catch_up_hbs();
31
32protected void create() {
33 ::create();
34 beatcount=1;
35 heartbeat=1;
36}
37
38protected void create_super() {
39 set_next_reset(-1);
40}
41
42// aktuelles Lebewesen, fuer das dieser NPC gerade taetig ist. Default:
43// Spieler, bei dem er als Helfer-NPC registriert ist.
44public object QueryUser()
45{
46 mixed helperdata = QueryProp(P_HELPER_NPC);
47 if (pointerp(helperdata) && objectp(helperdata[0]))
48 return helperdata[0];
49 return 0;
50}
51
52// ggf. Feinde expiren. Soll das Problem verringern, dass Spieler nach Tagen
53// erst die Meldung kriegen, dass der NPC sie nicht mehr jagt, wenn der HB
54// reaktivert wird.
55void reset() {
56 // ggf. die abgeschalteten HBs nachholen.
57 if (!heartbeat)
58 catch_up_hbs();
59 // ggf. P_ENEMY_DAMAGE zuruecksetzen
60 ResetEnemyDamage();
61}
62
63static void _set_max_hp(int i) {
64 Set(P_MAX_HP,i);
65 SetProp(P_HP,i);
66}
67
68static void _set_max_sp(int i) {
69 Set(P_MAX_SP,i);
70 SetProp(P_SP,i);
71}
72
73
74// Check-Funktion fuer die P_NO_ATTACK-QueryMethod
75static mixed _check_immortality()
76{
77 int t;
78
79 if ( !(t = Query("time_to_mortality")) || time() > t ){
80 // Zeit ist abgelaufen - wieder angreifbar machen
81 Set( P_NO_ATTACK, 0, F_QUERY_METHOD );
82 heartbeat = 1;
83 beatcount = 1;
84 set_heart_beat(1);
85
86 return 0;
87 }
88
89 // der NPC ist noch unangreifbar
90 return break_string( capitalize(name( WER, 1 )) + " versteckt sich hinter "
91 "einem Fehler im Raum-Zeit-Gefuege und entgeht so "
92 "voruebergehend allen Angriffen.", 78 );
93}
94
95
96// wenn der HeartBeat buggt, wird diese Funktion vom Master aufgerufen
97public void make_immortal()
98{
99 // fuer 5 Minuten unangreifbar machen
100 Set( P_NO_ATTACK, #'_check_immortality, F_QUERY_METHOD );
101
102 Set( "time_to_mortality", time() + 300, F_VALUE );
103
104 // damit die Spieler keinen Vorteil durch den Bug haben, heilen
105 heal_self(10000);
106
107 // da nun der Heartbeat abgeschaltet ist und normalerweise erst
108 // reaktiviert wird, sobald jemand nach 5min P_NO_ATTACK abfragt, muss man
109 // aber auf Viecher achten, die immer nen Heartbeat haben wollen. In dem
110 // fall per call_out selber die Prop abfragen.
111 if (QueryProp(P_HB))
112 call_out(#'QueryProp, 301, P_NO_ATTACK);
113}
114
115
116// Damit NPCs gegeneinander weiterkaempfen, auch wenn kein Spieler
117// in der Naehe ist:
118static int _query_hb()
119{
120 // TODO: return InFight() || Query(P_HB, F_VALUE), sobald InFight()
121 // geaendert.
122 return (InFight() || Query(P_HB,F_VALUE)) ? 1 : 0;
123}
124
125
126#define SPELL_TOTALRATE 0
Zesstraeb3f95e2018-12-14 02:00:50 +0100127#define SPELL_SINFO 1
MG Mud User88f12472016-06-24 23:31:02 +0200128#define SPELL_TEXT_FOR_ENEMY 2
129#define SPELL_TEXT_FOR_OTHERS 3
MG Mud User88f12472016-06-24 23:31:02 +0200130
Zesstradee2e562018-02-28 23:51:56 +0100131varargs int AddSpell(int rate, int damage,
132 string|<int|string>* TextForEnemy, string|<int|string>* TextForOthers,
Zesstraeb3f95e2018-12-14 02:00:50 +0100133 string|string* dam_type, string|closure func, int|mapping sinfo)
Bugfix7d66c1d2016-11-20 17:27:15 +0100134{
MG Mud User88f12472016-06-24 23:31:02 +0200135 mixed *spells;
136 int total_rates;
Bugfix7d66c1d2016-11-20 17:27:15 +0100137 closure cl;
MG Mud User88f12472016-06-24 23:31:02 +0200138
heull001d3b6c8e2018-02-15 13:17:28 +0100139 if(rate<=0 || damage<0) return 0;
MG Mud User88f12472016-06-24 23:31:02 +0200140
heull001e5777fd2018-02-15 13:21:32 +0100141 // Tatsaechlich ist es immer ein nicht-physischer Angriff, wenn spellarg ein
MG Mud User88f12472016-06-24 23:31:02 +0200142 // int ist, weil wenn spellarg==0 ist der Default nicht-physischer Angriff
143 // und bei spellarg!=0 auch. Nur mit einem mapping kann man einen phys.
144 // Angriff erzeugen.
Zesstraeb3f95e2018-12-14 02:00:50 +0100145 if (intp(sinfo))
146 sinfo = ([ SI_SPELL: ([SP_PHYSICAL_ATTACK: 0]) ]);
147 else
148 {
149 // wenn das sinfo-Mapping nicht den Key SI_SPELL enthaelt, gehen wir davon
150 // aus, dass es ein alter Aufrufer von AddSpell ist, welcher noch davon
151 // ausgeht, dass si_spell an AddSpell() uebergeben werden soll. In diesem
152 // Fall bauen wir ein sinfo und nehmen das uebergebene sinfo als SI_SPELL.
153 if (!member(sinfo,SI_SPELL))
154 sinfo = ([ SI_SPELL: sinfo ]);
155 }
156
157 sinfo[SI_SKILLDAMAGE] = damage;
MG Mud User88f12472016-06-24 23:31:02 +0200158
heull001e5777fd2018-02-15 13:21:32 +0100159 if(stringp(dam_type))
160 dam_type=({dam_type});
161 else if(!pointerp(dam_type))
162 {
Zesstraeb3f95e2018-12-14 02:00:50 +0100163 if(sinfo[SI_SPELL][SP_PHYSICAL_ATTACK])
heull001e5777fd2018-02-15 13:21:32 +0100164 dam_type=({DT_BLUDGEON});
165 else
166 dam_type=({DT_MAGIC});
167 }
heull001e5777fd2018-02-15 13:21:32 +0100168 foreach(string s : dam_type)
169 {
170 if(!VALID_DAMAGE_TYPE(s))
171 {
172 catch(raise_error(
173 "AddSpell(): Ungueltiger Schadenstyp: "+s);
174 publish);
175 }
176 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100177 sinfo[SI_SKILLDAMAGE_TYPE] = dam_type;
178
179 if(!member(sinfo, SI_MAGIC_TYPE))
180 sinfo[SI_MAGIC_TYPE] = ({ MT_ANGRIFF });
heull001e5777fd2018-02-15 13:21:32 +0100181
Bugfix7d66c1d2016-11-20 17:27:15 +0100182 // Falls func ein String ist eine Closure erstellen und diese speichern.
Zesstraeb3f95e2018-12-14 02:00:50 +0100183 if(stringp(func))
Bugfix7d66c1d2016-11-20 17:27:15 +0100184 {
Zesstraeb3f95e2018-12-14 02:00:50 +0100185 if (sizeof(func))
Bugfix7d66c1d2016-11-20 17:27:15 +0100186 {
Zesstraeb3f95e2018-12-14 02:00:50 +0100187 cl=symbol_function(func,this_object());
188 if(!closurep(cl))
189 {
190 catch(raise_error(
191 "AddSpell(): Es konnte keine Closure fuer "+func+" erstellt werden.");
192 publish);
193 }
Bugfix7d66c1d2016-11-20 17:27:15 +0100194 }
195 }
196 else
197 {
Zesstraeb3f95e2018-12-14 02:00:50 +0100198 cl=func;
Bugfix7d66c1d2016-11-20 17:27:15 +0100199 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100200 sinfo[SI_CLOSURE] = cl;
201
heull0019a0c6bc2018-02-15 13:45:14 +0100202 if(damage==0 && !closurep(cl))
203 {
204 catch(raise_error(
205 "AddSpell(): Bei damage=0 muss eine Funktion eingetragen werden.");
206 publish);
207 return 0;
208 }
209
Zesstradee2e562018-02-28 23:51:56 +0100210 if(!sizeof(TextForEnemy) ||
211 (pointerp(TextForEnemy) && !sizeof(TextForEnemy[0])))
212 {
213 TextForEnemy=0;
214 }
215 else if(stringp(TextForEnemy))
216 {
217 TextForEnemy=({TextForEnemy,MT_LOOK});
218 }
219 else if(pointerp(TextForEnemy) &&
220 (!stringp(TextForEnemy[0]) || !intp(TextForEnemy[1])))
221 {
222 raise_error(
223 "AddSpell(): Falsche Datentypen fuer TextForEnemy");
224 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100225
226 if(!sizeof(TextForOthers) ||
Zesstradee2e562018-02-28 23:51:56 +0100227 (pointerp(TextForOthers) && !sizeof(TextForOthers[0])))
228 {
229 TextForOthers=0;
230 }
231 else if(stringp(TextForOthers))
232 {
233 TextForOthers=({TextForOthers,MT_LOOK});
234 }
235 else if(pointerp(TextForOthers) &&
236 (!stringp(TextForOthers[0]) || !intp(TextForOthers[1])))
237 {
238 raise_error(
239 "AddSpell(): Falsche Datentypen fuer TextForOthers");
240 }
Zesstradee2e562018-02-28 23:51:56 +0100241
MG Mud User88f12472016-06-24 23:31:02 +0200242 // Falls vorhanden, alte Syntax auf die von replace_personal() anpassen,
243 // die im heart_beat() beim Ausgeben der Meldung verwendet wird.
Bugfix4eae8572018-03-02 14:38:48 +0100244 if ( pointerp(TextForOthers) && strstr(TextForOthers[0], "@", 0) != -1 )
MG Mud User88f12472016-06-24 23:31:02 +0200245 {
246 // Zeichen nach @WER & Co in runde Klammern einschliessen, damit es als
247 // Sub-Pattern im Ausgabestring wiederholt werden kann. Ansonsten wuerde
248 // es mit ersetzt.
Zesstradee2e562018-02-28 23:51:56 +0100249 TextForOthers[0] = regreplace(TextForOthers[0], "@WER([^1-9QU])", "@WER1\\1", 1);
250 TextForOthers[0] = regreplace(TextForOthers[0], "@WESSEN([^1-9QU])",
MG Mud User88f12472016-06-24 23:31:02 +0200251 "@WESSEN1\\1", 1);
Zesstradee2e562018-02-28 23:51:56 +0100252 TextForOthers[0] = regreplace(TextForOthers[0], "@WEM([^1-9QU])", "@WEM1\\1", 1);
253 TextForOthers[0] = regreplace(TextForOthers[0], "@WEN([^1-9QU])", "@WEN1\\1", 1);
MG Mud User88f12472016-06-24 23:31:02 +0200254 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100255
256 total_rates=Query("npc:total_rates", F_VALUE)+rate;
MG Mud User88f12472016-06-24 23:31:02 +0200257 spells=Query(P_SPELLS);
258 if (!pointerp(spells))
259 spells=({});
Zesstraeb3f95e2018-12-14 02:00:50 +0100260 spells+=({({total_rates, sinfo, TextForEnemy, TextForOthers})});
MG Mud User88f12472016-06-24 23:31:02 +0200261 Set(P_SPELLS,spells);
Zesstraeb3f95e2018-12-14 02:00:50 +0100262 Set("npc:total_rates",total_rates, F_VALUE);
MG Mud User88f12472016-06-24 23:31:02 +0200263 return 1;
264}
265
266int AutoAttack(object ob) {
267 mixed m;
268
269 if (!query_once_interactive(ob))
270 return 0;
271 if (mappingp(m=QueryProp(P_AGGRESSIVE))) {
272 mixed *ind,x,z;
273 float f;
274 int i,n;
275
276 ind=m_indices(m)-({0});n=0;f=0.0;
277 for (i=sizeof(ind)-1;i>=0;i--) {
278 x=ind[i];
279 if ((z=m[x][ob->QueryProp(x)]) || (z=m[x][0])) {
bugfixaf2be4f2020-03-22 19:13:07 +0100280 f=f+z;
MG Mud User88f12472016-06-24 23:31:02 +0200281 n++;
282 }
283 }
284 if (n)
285 m=f/((float)n);
286 else
287 m=m[0];
288 }
289 if (((int)(100*(m+ob->QueryProp(P_AGGRESSIVE))))<=random(100))
290 return 0;
291 if (IS_LEARNER(ob)
292 && (ob->QueryProp(P_INVIS)
293 || ob->QueryProp(P_WANTS_TO_LEARN)))
294 return 0;
295 return 1;
296}
297
298void SpellAttack(object enemy) {
299}
300
301#if 0
302TJ(string s) {
303 object o;
304 if (o=find_player("jof"))
305 tell_object(o,sprintf("%O: %s\n",this_object(),s));
306}
307#else
308#define TJ(x)
309#endif
310
311protected void heart_beat() {
312 int r,i;
313 mixed env,*spells, sinfo;
314 object enemy;
315
316 if ( --beatcount < 0 )
317 beatcount = 0;
318
319 if (!beatcount && !Query(P_HB)) {
320 if (!environment()) {
321 set_heart_beat(0);
322 heartbeat = 0;
323 if( clonep(this_object()) ) remove();
324 return;
325 }
326 if (!QueryProp(P_POISON)) {
327 // Spieler anwesend?
328 env = filter(all_inventory(environment()), #'query_once_interactive);
329 if (!sizeof(env)) {
330 // Nein, HBs abschalten.
331 set_heart_beat(0);
332 heartbeat=0;
333 TJ("OFF\n");
334 beatcount=HB_CHECK;
335 Set("npc:beat_off_num",absolute_hb_count());
336 return;
337 }
338 }
339 }
340 ::heart_beat();
341 if (!ME)
342 return;
343 enemy=SelectEnemy();
344 if (QueryProp(P_AGGRESSIVE)
345 && (!enemy || environment()!=environment(enemy))
346 && !beatcount) {
347 beatcount=HB_CHECK;
348 env=filter(all_inventory(environment()),#'AutoAttack);
349 if (!sizeof(env))
350 return;
351 i=random(sizeof(env));
352 Kill(env[i]);
353 }
354 else if (!beatcount)
355 beatcount=HB_CHECK;
356 if (!objectp(enemy) ||QueryProp(P_DISABLE_ATTACK)>0)
357 return;
358 SpellAttack(enemy);
359
360 if (!pointerp(spells=Query(P_SPELLS))
361 || !sizeof(spells)
362 || !objectp(enemy=SelectEnemy())
363 || environment(enemy)!=environment()
364 || (QueryProp(P_DISABLE_ATTACK)>0)
365 || random(100)>Query(P_SPELLRATE))
366 return;
Zesstraeb3f95e2018-12-14 02:00:50 +0100367 // Spell aussuchen, von oben runterlaufen, bis zum ersten Spell, dessen
368 // SPELL_TOTALRATE grosser ist als r.
369 r=random(Query("npc:total_rates", F_VALUE));
370 for (i=sizeof(spells)-1 ; (i>0 && spells[i-1][SPELL_TOTALRATE] > r); i--)
MG Mud User88f12472016-06-24 23:31:02 +0200371 ;
Zesstraeb3f95e2018-12-14 02:00:50 +0100372
Zesstradee2e562018-02-28 23:51:56 +0100373 <int|string>* akt_spell_mess=spells[i][SPELL_TEXT_FOR_ENEMY];
MG Mud User88f12472016-06-24 23:31:02 +0200374 // Nur, wenn ueberhaupt eine Meldung gesetzt wurde, muss diese verarbeitet
375 // werden.
Zesstradee2e562018-02-28 23:51:56 +0100376 if(pointerp(akt_spell_mess))
MG Mud User88f12472016-06-24 23:31:02 +0200377 {
Zesstradee2e562018-02-28 23:51:56 +0100378 enemy->ReceiveMsg(
379 akt_spell_mess[0],
380 akt_spell_mess[1],
381 MA_SPELL);
382 }
383 akt_spell_mess=spells[i][SPELL_TEXT_FOR_OTHERS];
384 if(pointerp(akt_spell_mess))
385 {
Zesstradee2e562018-02-28 23:51:56 +0100386 send_room(environment(),
Bugfix6074b532018-10-29 12:40:50 +0100387 replace_personal(akt_spell_mess[0], ({enemy}), 1),
Zesstradee2e562018-02-28 23:51:56 +0100388 akt_spell_mess[1],
389 MA_SPELL,
390 0,
391 ({enemy,this_object()}),
392 this_object());
MG Mud User88f12472016-06-24 23:31:02 +0200393 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100394
395 // Tiefe Kopie, damit wir nach Herzenslust fuer diese Ausfuehrung
396 // manipulieren koennen und alle gerufenen Defend und Ruestung etc. nix
397 // dauerhaft in SI_SPELL kaputt machen koennen.
398 sinfo = deep_copy(spells[i][SPELL_SINFO]);
399 sinfo[SI_ENEMY] = enemy;
400
MG Mud User88f12472016-06-24 23:31:02 +0200401 if(!sinfo[SP_PHYSICAL_ATTACK] &&
402 (enemy->SpellDefend(this_object(),sinfo) >
Zesstraeb3f95e2018-12-14 02:00:50 +0100403 random(MAX_ABILITY+QueryProp(P_LEVEL)*50)))
404 {
Zesstradee2e562018-02-28 23:51:56 +0100405 enemy->ReceiveMsg(
406 "Du wehrst den Spruch ab.",
407 MT_NOTIFICATION,
408 MA_SPELL);
409 send_room(environment(),
410 enemy->Name(WER,1)+" wehrt den Spruch ab.",
411 MT_LOOK,
412 MA_SPELL,
413 0,
414 ({ enemy, this_object()}));
MG Mud User88f12472016-06-24 23:31:02 +0200415 return ;
416 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100417 // Bei 0 sparen wir uns das Defend() und rufen nur weiter unter SI_CLOSURE auf.
418 if(sinfo[SI_SKILLDAMAGE])
heull0019a0c6bc2018-02-15 13:45:14 +0100419 {
Zesstraeb3f95e2018-12-14 02:00:50 +0100420 sinfo[SI_SKILLDAMAGE] = random(sinfo[SI_SKILLDAMAGE]) + 1;
421 enemy->Defend(sinfo[SI_SKILLDAMAGE], sinfo[SI_SKILLDAMAGE_TYPE],
422 sinfo[SI_SPELL], this_object());
heull0019a0c6bc2018-02-15 13:45:14 +0100423 }
MG Mud User88f12472016-06-24 23:31:02 +0200424
425 // Falls der Gegner (oder wir) im Defend stirbt, hier abbrechen
Zesstraeb3f95e2018-12-14 02:00:50 +0100426 if ( !objectp(ME) || !objectp(enemy)
MG Mud User88f12472016-06-24 23:31:02 +0200427 || enemy->QueryProp(P_GHOST) ) return;
Zesstraeb3f95e2018-12-14 02:00:50 +0100428
429 closure cl = sinfo[SI_CLOSURE];
Bugfix7d66c1d2016-11-20 17:27:15 +0100430 if (cl)
431 {
432 if (closurep(cl))
Zesstraeb3f95e2018-12-14 02:00:50 +0100433 catch(funcall(cl, enemy, sinfo[SI_SKILLDAMAGE],
434 sinfo[SI_SKILLDAMAGE_TYPE]);publish);
Bugfix7d66c1d2016-11-20 17:27:15 +0100435 else
436 raise_error(sprintf("P_SPELL defekt: SPELL_FUNC in Spell %i ist keine "
437 "Closure.\n", i));
438 }
MG Mud User88f12472016-06-24 23:31:02 +0200439}
440
441// Heartbeats nachholen.
442private void catch_up_hbs() {
443 // gibt es HBs zum nachholen?
444 int beat_off_num = Query("npc:beat_off_num");
445 if (!beat_off_num)
446 return; // nein.
447 // wieviele HBs nachholen?
448 beat_off_num = absolute_hb_count() - beat_off_num;
449
450 if (beat_off_num>0) {
451 // Nicht ausgefuehrtes HEILEN nachholen
452 int rlock=QueryProp(P_NO_REGENERATION);
453 int hp=QueryProp(P_HP);
454 int sp=QueryProp(P_SP);
455 int alc=QueryProp(P_ALCOHOL);
456 if (!(rlock & NO_REG_HP)) {
457 hp+=beat_off_num/HEAL_DELAY+alc/ALCOHOL_DELAY;
458 SetProp(P_HP,hp);
459 }
460 if (!(rlock & NO_REG_SP)) {
461 sp+=beat_off_num/HEAL_DELAY+alc/ALCOHOL_DELAY;
462 SetProp(P_SP,sp);
463 }
464 alc-=beat_off_num/ALCOHOL_DELAY;
465 if ( alc < 0 )
466 alc = 0;
467 SetProp(P_ALCOHOL,alc);
468 int da = QueryProp(P_DISABLE_ATTACK);
469 // Paralysen abbauen
470 if ( da > 0 ) {
471 da -= beat_off_num;
472 if ( da < 0 )
473 da = 0;
474 SetProp( P_DISABLE_ATTACK, da );
475 }
476 // Hunttimes aktualisieren, Feinde expiren
477 update_hunt_times(beat_off_num);
478 if (!heartbeat)
479 // HBs immer noch abgeschaltet, naechstes Mal HBs seit jetzt nachholen.
480 Set("npc:beat_off_num",absolute_hb_count());
481 else
482 // HB laeuft wieder, nix mehr nachholen, bis zur naechsten Abschaltung.
483 Set("npc:beat_off_num",0);
484 }
485}
486
Zesstra5b71ebb2018-03-07 20:50:35 +0100487public varargs void init(object origin)
488{
MG Mud User88f12472016-06-24 23:31:02 +0200489 // ggf. Heartbeats nachholen und wieder einschalten.
490 if (!heartbeat) {
491 set_heart_beat(1);
492 heartbeat=1;
493 catch_up_hbs();
494 }
495
496 if (AutoAttack(this_player()))
497 Kill(this_player());
498}
499
500private nosave closure mod_att_stat;
501
502int Defend(int dam, mixed dam_type, mixed spell, object enemy) {
503 if (objectp(enemy=(enemy||this_player()))
504 && query_once_interactive(enemy)
505 && !IS_LEARNER(enemy)) {
506 if (!objectp(get_type_info(mod_att_stat,2))) {
507 object ma;
508 if (!objectp(ma=find_object(STATMASTER)))
509 return ::Defend(dam,dam_type,spell,enemy);
510 // Keine Statistik wenn Master nicht geladen ist.
511 mod_att_stat=symbol_function("ModifyAttackStat",ma);
512 }
513 funcall(mod_att_stat,
514 enemy->QueryProp(P_GUILD),
515 enemy->QueryProp(P_GUILD_LEVEL),
516 dam,
517 dam_type,
518 spell);
519 }
520
521 return ::Defend(dam,dam_type,spell,enemy);
522}