blob: d4b9448c6d1bd5b89de355ab622105ccd752e326 [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
10#pragma pedantic
11
12inherit "std/living/combat";
13
14#include <combat.h>
15#include <language.h>
16#include <properties.h>
17#include <wizlevels.h>
18#include <health.h>
19#include <new_skills.h>
20
21#define NEED_PROTOTYPES 1
22#include <living/life.h>
23#undef NEED_PROTOTYPES
24
25#define HB_CHECK 7
26#define ME this_object()
27#define STATMASTER "/p/service/rochus/guildstat/master"
28
29nosave int heartbeat, beatcount;
30
31private void catch_up_hbs();
32
33protected void create() {
34 ::create();
35 beatcount=1;
36 heartbeat=1;
37}
38
39protected void create_super() {
40 set_next_reset(-1);
41}
42
43// aktuelles Lebewesen, fuer das dieser NPC gerade taetig ist. Default:
44// Spieler, bei dem er als Helfer-NPC registriert ist.
45public object QueryUser()
46{
47 mixed helperdata = QueryProp(P_HELPER_NPC);
48 if (pointerp(helperdata) && objectp(helperdata[0]))
49 return helperdata[0];
50 return 0;
51}
52
53// ggf. Feinde expiren. Soll das Problem verringern, dass Spieler nach Tagen
54// erst die Meldung kriegen, dass der NPC sie nicht mehr jagt, wenn der HB
55// reaktivert wird.
56void reset() {
57 // ggf. die abgeschalteten HBs nachholen.
58 if (!heartbeat)
59 catch_up_hbs();
60 // ggf. P_ENEMY_DAMAGE zuruecksetzen
61 ResetEnemyDamage();
62}
63
64static void _set_max_hp(int i) {
65 Set(P_MAX_HP,i);
66 SetProp(P_HP,i);
67}
68
69static void _set_max_sp(int i) {
70 Set(P_MAX_SP,i);
71 SetProp(P_SP,i);
72}
73
74
75// Check-Funktion fuer die P_NO_ATTACK-QueryMethod
76static mixed _check_immortality()
77{
78 int t;
79
80 if ( !(t = Query("time_to_mortality")) || time() > t ){
81 // Zeit ist abgelaufen - wieder angreifbar machen
82 Set( P_NO_ATTACK, 0, F_QUERY_METHOD );
83 heartbeat = 1;
84 beatcount = 1;
85 set_heart_beat(1);
86
87 return 0;
88 }
89
90 // der NPC ist noch unangreifbar
91 return break_string( capitalize(name( WER, 1 )) + " versteckt sich hinter "
92 "einem Fehler im Raum-Zeit-Gefuege und entgeht so "
93 "voruebergehend allen Angriffen.", 78 );
94}
95
96
97// wenn der HeartBeat buggt, wird diese Funktion vom Master aufgerufen
98public void make_immortal()
99{
100 // fuer 5 Minuten unangreifbar machen
101 Set( P_NO_ATTACK, #'_check_immortality, F_QUERY_METHOD );
102
103 Set( "time_to_mortality", time() + 300, F_VALUE );
104
105 // damit die Spieler keinen Vorteil durch den Bug haben, heilen
106 heal_self(10000);
107
108 // da nun der Heartbeat abgeschaltet ist und normalerweise erst
109 // reaktiviert wird, sobald jemand nach 5min P_NO_ATTACK abfragt, muss man
110 // aber auf Viecher achten, die immer nen Heartbeat haben wollen. In dem
111 // fall per call_out selber die Prop abfragen.
112 if (QueryProp(P_HB))
113 call_out(#'QueryProp, 301, P_NO_ATTACK);
114}
115
116
117// Damit NPCs gegeneinander weiterkaempfen, auch wenn kein Spieler
118// in der Naehe ist:
119static int _query_hb()
120{
121 // TODO: return InFight() || Query(P_HB, F_VALUE), sobald InFight()
122 // geaendert.
123 return (InFight() || Query(P_HB,F_VALUE)) ? 1 : 0;
124}
125
126
127#define SPELL_TOTALRATE 0
Zesstraeb3f95e2018-12-14 02:00:50 +0100128#define SPELL_SINFO 1
MG Mud User88f12472016-06-24 23:31:02 +0200129#define SPELL_TEXT_FOR_ENEMY 2
130#define SPELL_TEXT_FOR_OTHERS 3
MG Mud User88f12472016-06-24 23:31:02 +0200131
Zesstradee2e562018-02-28 23:51:56 +0100132varargs int AddSpell(int rate, int damage,
133 string|<int|string>* TextForEnemy, string|<int|string>* TextForOthers,
Zesstraeb3f95e2018-12-14 02:00:50 +0100134 string|string* dam_type, string|closure func, int|mapping sinfo)
Bugfix7d66c1d2016-11-20 17:27:15 +0100135{
MG Mud User88f12472016-06-24 23:31:02 +0200136 mixed *spells;
137 int total_rates;
Bugfix7d66c1d2016-11-20 17:27:15 +0100138 closure cl;
MG Mud User88f12472016-06-24 23:31:02 +0200139
heull001d3b6c8e2018-02-15 13:17:28 +0100140 if(rate<=0 || damage<0) return 0;
MG Mud User88f12472016-06-24 23:31:02 +0200141
heull001e5777fd2018-02-15 13:21:32 +0100142 // Tatsaechlich ist es immer ein nicht-physischer Angriff, wenn spellarg ein
MG Mud User88f12472016-06-24 23:31:02 +0200143 // int ist, weil wenn spellarg==0 ist der Default nicht-physischer Angriff
144 // und bei spellarg!=0 auch. Nur mit einem mapping kann man einen phys.
145 // Angriff erzeugen.
Zesstraeb3f95e2018-12-14 02:00:50 +0100146 if (intp(sinfo))
147 sinfo = ([ SI_SPELL: ([SP_PHYSICAL_ATTACK: 0]) ]);
148 else
149 {
150 // wenn das sinfo-Mapping nicht den Key SI_SPELL enthaelt, gehen wir davon
151 // aus, dass es ein alter Aufrufer von AddSpell ist, welcher noch davon
152 // ausgeht, dass si_spell an AddSpell() uebergeben werden soll. In diesem
153 // Fall bauen wir ein sinfo und nehmen das uebergebene sinfo als SI_SPELL.
154 if (!member(sinfo,SI_SPELL))
155 sinfo = ([ SI_SPELL: sinfo ]);
156 }
157
158 sinfo[SI_SKILLDAMAGE] = damage;
MG Mud User88f12472016-06-24 23:31:02 +0200159
heull001e5777fd2018-02-15 13:21:32 +0100160 if(stringp(dam_type))
161 dam_type=({dam_type});
162 else if(!pointerp(dam_type))
163 {
Zesstraeb3f95e2018-12-14 02:00:50 +0100164 if(sinfo[SI_SPELL][SP_PHYSICAL_ATTACK])
heull001e5777fd2018-02-15 13:21:32 +0100165 dam_type=({DT_BLUDGEON});
166 else
167 dam_type=({DT_MAGIC});
168 }
heull001e5777fd2018-02-15 13:21:32 +0100169 foreach(string s : dam_type)
170 {
171 if(!VALID_DAMAGE_TYPE(s))
172 {
173 catch(raise_error(
174 "AddSpell(): Ungueltiger Schadenstyp: "+s);
175 publish);
176 }
177 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100178 sinfo[SI_SKILLDAMAGE_TYPE] = dam_type;
179
180 if(!member(sinfo, SI_MAGIC_TYPE))
181 sinfo[SI_MAGIC_TYPE] = ({ MT_ANGRIFF });
heull001e5777fd2018-02-15 13:21:32 +0100182
Bugfix7d66c1d2016-11-20 17:27:15 +0100183 // Falls func ein String ist eine Closure erstellen und diese speichern.
Zesstraeb3f95e2018-12-14 02:00:50 +0100184 if(stringp(func))
Bugfix7d66c1d2016-11-20 17:27:15 +0100185 {
Zesstraeb3f95e2018-12-14 02:00:50 +0100186 if (sizeof(func))
Bugfix7d66c1d2016-11-20 17:27:15 +0100187 {
Zesstraeb3f95e2018-12-14 02:00:50 +0100188 cl=symbol_function(func,this_object());
189 if(!closurep(cl))
190 {
191 catch(raise_error(
192 "AddSpell(): Es konnte keine Closure fuer "+func+" erstellt werden.");
193 publish);
194 }
Bugfix7d66c1d2016-11-20 17:27:15 +0100195 }
196 }
197 else
198 {
Zesstraeb3f95e2018-12-14 02:00:50 +0100199 cl=func;
Bugfix7d66c1d2016-11-20 17:27:15 +0100200 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100201 sinfo[SI_CLOSURE] = cl;
202
heull0019a0c6bc2018-02-15 13:45:14 +0100203 if(damage==0 && !closurep(cl))
204 {
205 catch(raise_error(
206 "AddSpell(): Bei damage=0 muss eine Funktion eingetragen werden.");
207 publish);
208 return 0;
209 }
210
Zesstradee2e562018-02-28 23:51:56 +0100211 if(!sizeof(TextForEnemy) ||
212 (pointerp(TextForEnemy) && !sizeof(TextForEnemy[0])))
213 {
214 TextForEnemy=0;
215 }
216 else if(stringp(TextForEnemy))
217 {
218 TextForEnemy=({TextForEnemy,MT_LOOK});
219 }
220 else if(pointerp(TextForEnemy) &&
221 (!stringp(TextForEnemy[0]) || !intp(TextForEnemy[1])))
222 {
223 raise_error(
224 "AddSpell(): Falsche Datentypen fuer TextForEnemy");
225 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100226
227 if(!sizeof(TextForOthers) ||
Zesstradee2e562018-02-28 23:51:56 +0100228 (pointerp(TextForOthers) && !sizeof(TextForOthers[0])))
229 {
230 TextForOthers=0;
231 }
232 else if(stringp(TextForOthers))
233 {
234 TextForOthers=({TextForOthers,MT_LOOK});
235 }
236 else if(pointerp(TextForOthers) &&
237 (!stringp(TextForOthers[0]) || !intp(TextForOthers[1])))
238 {
239 raise_error(
240 "AddSpell(): Falsche Datentypen fuer TextForOthers");
241 }
Zesstradee2e562018-02-28 23:51:56 +0100242
MG Mud User88f12472016-06-24 23:31:02 +0200243 // Falls vorhanden, alte Syntax auf die von replace_personal() anpassen,
244 // die im heart_beat() beim Ausgeben der Meldung verwendet wird.
Bugfix4eae8572018-03-02 14:38:48 +0100245 if ( pointerp(TextForOthers) && strstr(TextForOthers[0], "@", 0) != -1 )
MG Mud User88f12472016-06-24 23:31:02 +0200246 {
247 // Zeichen nach @WER & Co in runde Klammern einschliessen, damit es als
248 // Sub-Pattern im Ausgabestring wiederholt werden kann. Ansonsten wuerde
249 // es mit ersetzt.
Zesstradee2e562018-02-28 23:51:56 +0100250 TextForOthers[0] = regreplace(TextForOthers[0], "@WER([^1-9QU])", "@WER1\\1", 1);
251 TextForOthers[0] = regreplace(TextForOthers[0], "@WESSEN([^1-9QU])",
MG Mud User88f12472016-06-24 23:31:02 +0200252 "@WESSEN1\\1", 1);
Zesstradee2e562018-02-28 23:51:56 +0100253 TextForOthers[0] = regreplace(TextForOthers[0], "@WEM([^1-9QU])", "@WEM1\\1", 1);
254 TextForOthers[0] = regreplace(TextForOthers[0], "@WEN([^1-9QU])", "@WEN1\\1", 1);
MG Mud User88f12472016-06-24 23:31:02 +0200255 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100256
257 total_rates=Query("npc:total_rates", F_VALUE)+rate;
MG Mud User88f12472016-06-24 23:31:02 +0200258 spells=Query(P_SPELLS);
259 if (!pointerp(spells))
260 spells=({});
Zesstraeb3f95e2018-12-14 02:00:50 +0100261 spells+=({({total_rates, sinfo, TextForEnemy, TextForOthers})});
MG Mud User88f12472016-06-24 23:31:02 +0200262 Set(P_SPELLS,spells);
Zesstraeb3f95e2018-12-14 02:00:50 +0100263 Set("npc:total_rates",total_rates, F_VALUE);
MG Mud User88f12472016-06-24 23:31:02 +0200264 return 1;
265}
266
267int AutoAttack(object ob) {
268 mixed m;
269
270 if (!query_once_interactive(ob))
271 return 0;
272 if (mappingp(m=QueryProp(P_AGGRESSIVE))) {
273 mixed *ind,x,z;
274 float f;
275 int i,n;
276
277 ind=m_indices(m)-({0});n=0;f=0.0;
278 for (i=sizeof(ind)-1;i>=0;i--) {
279 x=ind[i];
280 if ((z=m[x][ob->QueryProp(x)]) || (z=m[x][0])) {
281 f=f+(float)z;
282 n++;
283 }
284 }
285 if (n)
286 m=f/((float)n);
287 else
288 m=m[0];
289 }
290 if (((int)(100*(m+ob->QueryProp(P_AGGRESSIVE))))<=random(100))
291 return 0;
292 if (IS_LEARNER(ob)
293 && (ob->QueryProp(P_INVIS)
294 || ob->QueryProp(P_WANTS_TO_LEARN)))
295 return 0;
296 return 1;
297}
298
299void SpellAttack(object enemy) {
300}
301
302#if 0
303TJ(string s) {
304 object o;
305 if (o=find_player("jof"))
306 tell_object(o,sprintf("%O: %s\n",this_object(),s));
307}
308#else
309#define TJ(x)
310#endif
311
312protected void heart_beat() {
313 int r,i;
314 mixed env,*spells, sinfo;
315 object enemy;
316
317 if ( --beatcount < 0 )
318 beatcount = 0;
319
320 if (!beatcount && !Query(P_HB)) {
321 if (!environment()) {
322 set_heart_beat(0);
323 heartbeat = 0;
324 if( clonep(this_object()) ) remove();
325 return;
326 }
327 if (!QueryProp(P_POISON)) {
328 // Spieler anwesend?
329 env = filter(all_inventory(environment()), #'query_once_interactive);
330 if (!sizeof(env)) {
331 // Nein, HBs abschalten.
332 set_heart_beat(0);
333 heartbeat=0;
334 TJ("OFF\n");
335 beatcount=HB_CHECK;
336 Set("npc:beat_off_num",absolute_hb_count());
337 return;
338 }
339 }
340 }
341 ::heart_beat();
342 if (!ME)
343 return;
344 enemy=SelectEnemy();
345 if (QueryProp(P_AGGRESSIVE)
346 && (!enemy || environment()!=environment(enemy))
347 && !beatcount) {
348 beatcount=HB_CHECK;
349 env=filter(all_inventory(environment()),#'AutoAttack);
350 if (!sizeof(env))
351 return;
352 i=random(sizeof(env));
353 Kill(env[i]);
354 }
355 else if (!beatcount)
356 beatcount=HB_CHECK;
357 if (!objectp(enemy) ||QueryProp(P_DISABLE_ATTACK)>0)
358 return;
359 SpellAttack(enemy);
360
361 if (!pointerp(spells=Query(P_SPELLS))
362 || !sizeof(spells)
363 || !objectp(enemy=SelectEnemy())
364 || environment(enemy)!=environment()
365 || (QueryProp(P_DISABLE_ATTACK)>0)
366 || random(100)>Query(P_SPELLRATE))
367 return;
Zesstraeb3f95e2018-12-14 02:00:50 +0100368 // Spell aussuchen, von oben runterlaufen, bis zum ersten Spell, dessen
369 // SPELL_TOTALRATE grosser ist als r.
370 r=random(Query("npc:total_rates", F_VALUE));
371 for (i=sizeof(spells)-1 ; (i>0 && spells[i-1][SPELL_TOTALRATE] > r); i--)
MG Mud User88f12472016-06-24 23:31:02 +0200372 ;
Zesstraeb3f95e2018-12-14 02:00:50 +0100373
Zesstradee2e562018-02-28 23:51:56 +0100374 <int|string>* akt_spell_mess=spells[i][SPELL_TEXT_FOR_ENEMY];
MG Mud User88f12472016-06-24 23:31:02 +0200375 // Nur, wenn ueberhaupt eine Meldung gesetzt wurde, muss diese verarbeitet
376 // werden.
Zesstradee2e562018-02-28 23:51:56 +0100377 if(pointerp(akt_spell_mess))
MG Mud User88f12472016-06-24 23:31:02 +0200378 {
Zesstradee2e562018-02-28 23:51:56 +0100379 enemy->ReceiveMsg(
380 akt_spell_mess[0],
381 akt_spell_mess[1],
382 MA_SPELL);
383 }
384 akt_spell_mess=spells[i][SPELL_TEXT_FOR_OTHERS];
385 if(pointerp(akt_spell_mess))
386 {
Zesstradee2e562018-02-28 23:51:56 +0100387 send_room(environment(),
Bugfix6074b532018-10-29 12:40:50 +0100388 replace_personal(akt_spell_mess[0], ({enemy}), 1),
Zesstradee2e562018-02-28 23:51:56 +0100389 akt_spell_mess[1],
390 MA_SPELL,
391 0,
392 ({enemy,this_object()}),
393 this_object());
MG Mud User88f12472016-06-24 23:31:02 +0200394 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100395
396 // Tiefe Kopie, damit wir nach Herzenslust fuer diese Ausfuehrung
397 // manipulieren koennen und alle gerufenen Defend und Ruestung etc. nix
398 // dauerhaft in SI_SPELL kaputt machen koennen.
399 sinfo = deep_copy(spells[i][SPELL_SINFO]);
400 sinfo[SI_ENEMY] = enemy;
401
MG Mud User88f12472016-06-24 23:31:02 +0200402 if(!sinfo[SP_PHYSICAL_ATTACK] &&
403 (enemy->SpellDefend(this_object(),sinfo) >
Zesstraeb3f95e2018-12-14 02:00:50 +0100404 random(MAX_ABILITY+QueryProp(P_LEVEL)*50)))
405 {
Zesstradee2e562018-02-28 23:51:56 +0100406 enemy->ReceiveMsg(
407 "Du wehrst den Spruch ab.",
408 MT_NOTIFICATION,
409 MA_SPELL);
410 send_room(environment(),
411 enemy->Name(WER,1)+" wehrt den Spruch ab.",
412 MT_LOOK,
413 MA_SPELL,
414 0,
415 ({ enemy, this_object()}));
MG Mud User88f12472016-06-24 23:31:02 +0200416 return ;
417 }
Zesstraeb3f95e2018-12-14 02:00:50 +0100418 // Bei 0 sparen wir uns das Defend() und rufen nur weiter unter SI_CLOSURE auf.
419 if(sinfo[SI_SKILLDAMAGE])
heull0019a0c6bc2018-02-15 13:45:14 +0100420 {
Zesstraeb3f95e2018-12-14 02:00:50 +0100421 sinfo[SI_SKILLDAMAGE] = random(sinfo[SI_SKILLDAMAGE]) + 1;
422 enemy->Defend(sinfo[SI_SKILLDAMAGE], sinfo[SI_SKILLDAMAGE_TYPE],
423 sinfo[SI_SPELL], this_object());
heull0019a0c6bc2018-02-15 13:45:14 +0100424 }
MG Mud User88f12472016-06-24 23:31:02 +0200425
426 // Falls der Gegner (oder wir) im Defend stirbt, hier abbrechen
Zesstraeb3f95e2018-12-14 02:00:50 +0100427 if ( !objectp(ME) || !objectp(enemy)
MG Mud User88f12472016-06-24 23:31:02 +0200428 || enemy->QueryProp(P_GHOST) ) return;
Zesstraeb3f95e2018-12-14 02:00:50 +0100429
430 closure cl = sinfo[SI_CLOSURE];
Bugfix7d66c1d2016-11-20 17:27:15 +0100431 if (cl)
432 {
433 if (closurep(cl))
Zesstraeb3f95e2018-12-14 02:00:50 +0100434 catch(funcall(cl, enemy, sinfo[SI_SKILLDAMAGE],
435 sinfo[SI_SKILLDAMAGE_TYPE]);publish);
Bugfix7d66c1d2016-11-20 17:27:15 +0100436 else
437 raise_error(sprintf("P_SPELL defekt: SPELL_FUNC in Spell %i ist keine "
438 "Closure.\n", i));
439 }
MG Mud User88f12472016-06-24 23:31:02 +0200440}
441
442// Heartbeats nachholen.
443private void catch_up_hbs() {
444 // gibt es HBs zum nachholen?
445 int beat_off_num = Query("npc:beat_off_num");
446 if (!beat_off_num)
447 return; // nein.
448 // wieviele HBs nachholen?
449 beat_off_num = absolute_hb_count() - beat_off_num;
450
451 if (beat_off_num>0) {
452 // Nicht ausgefuehrtes HEILEN nachholen
453 int rlock=QueryProp(P_NO_REGENERATION);
454 int hp=QueryProp(P_HP);
455 int sp=QueryProp(P_SP);
456 int alc=QueryProp(P_ALCOHOL);
457 if (!(rlock & NO_REG_HP)) {
458 hp+=beat_off_num/HEAL_DELAY+alc/ALCOHOL_DELAY;
459 SetProp(P_HP,hp);
460 }
461 if (!(rlock & NO_REG_SP)) {
462 sp+=beat_off_num/HEAL_DELAY+alc/ALCOHOL_DELAY;
463 SetProp(P_SP,sp);
464 }
465 alc-=beat_off_num/ALCOHOL_DELAY;
466 if ( alc < 0 )
467 alc = 0;
468 SetProp(P_ALCOHOL,alc);
469 int da = QueryProp(P_DISABLE_ATTACK);
470 // Paralysen abbauen
471 if ( da > 0 ) {
472 da -= beat_off_num;
473 if ( da < 0 )
474 da = 0;
475 SetProp( P_DISABLE_ATTACK, da );
476 }
477 // Hunttimes aktualisieren, Feinde expiren
478 update_hunt_times(beat_off_num);
479 if (!heartbeat)
480 // HBs immer noch abgeschaltet, naechstes Mal HBs seit jetzt nachholen.
481 Set("npc:beat_off_num",absolute_hb_count());
482 else
483 // HB laeuft wieder, nix mehr nachholen, bis zur naechsten Abschaltung.
484 Set("npc:beat_off_num",0);
485 }
486}
487
Zesstra5b71ebb2018-03-07 20:50:35 +0100488public varargs void init(object origin)
489{
MG Mud User88f12472016-06-24 23:31:02 +0200490 // ggf. Heartbeats nachholen und wieder einschalten.
491 if (!heartbeat) {
492 set_heart_beat(1);
493 heartbeat=1;
494 catch_up_hbs();
495 }
496
497 if (AutoAttack(this_player()))
498 Kill(this_player());
499}
500
501private nosave closure mod_att_stat;
502
503int Defend(int dam, mixed dam_type, mixed spell, object enemy) {
504 if (objectp(enemy=(enemy||this_player()))
505 && query_once_interactive(enemy)
506 && !IS_LEARNER(enemy)) {
507 if (!objectp(get_type_info(mod_att_stat,2))) {
508 object ma;
509 if (!objectp(ma=find_object(STATMASTER)))
510 return ::Defend(dam,dam_type,spell,enemy);
511 // Keine Statistik wenn Master nicht geladen ist.
512 mod_att_stat=symbol_function("ModifyAttackStat",ma);
513 }
514 funcall(mod_att_stat,
515 enemy->QueryProp(P_GUILD),
516 enemy->QueryProp(P_GUILD_LEVEL),
517 dam,
518 dam_type,
519 spell);
520 }
521
522 return ::Defend(dam,dam_type,spell,enemy);
523}