blob: 110d0b032aca0adacfabac614c68c0a5d3ab6a20 [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
128#define SPELL_DAMAGE 1
129#define SPELL_TEXT_FOR_ENEMY 2
130#define SPELL_TEXT_FOR_OTHERS 3
131#define SPELL_DAMTYPE 4
132#define SPELL_FUNC 5
133#define SPELL_ARG 6
134
Zesstradee2e562018-02-28 23:51:56 +0100135varargs int AddSpell(int rate, int damage,
136 string|<int|string>* TextForEnemy, string|<int|string>* TextForOthers,
137 string|string* dam_type, string|closure func, int|mapping spellarg)
Bugfix7d66c1d2016-11-20 17:27:15 +0100138{
MG Mud User88f12472016-06-24 23:31:02 +0200139 mixed *spells;
140 int total_rates;
Bugfix7d66c1d2016-11-20 17:27:15 +0100141 closure cl;
MG Mud User88f12472016-06-24 23:31:02 +0200142
heull001d3b6c8e2018-02-15 13:17:28 +0100143 if(rate<=0 || damage<0) return 0;
MG Mud User88f12472016-06-24 23:31:02 +0200144
heull001e5777fd2018-02-15 13:21:32 +0100145 // Tatsaechlich ist es immer ein nicht-physischer Angriff, wenn spellarg ein
MG Mud User88f12472016-06-24 23:31:02 +0200146 // int ist, weil wenn spellarg==0 ist der Default nicht-physischer Angriff
147 // und bei spellarg!=0 auch. Nur mit einem mapping kann man einen phys.
148 // Angriff erzeugen.
149 if (intp(spellarg))
150 spellarg = ([SP_PHYSICAL_ATTACK: 0]);
151
heull001e5777fd2018-02-15 13:21:32 +0100152 if(stringp(dam_type))
153 dam_type=({dam_type});
154 else if(!pointerp(dam_type))
155 {
156 if(spellarg[SP_PHYSICAL_ATTACK])
157 dam_type=({DT_BLUDGEON});
158 else
159 dam_type=({DT_MAGIC});
160 }
161
162 foreach(string s : dam_type)
163 {
164 if(!VALID_DAMAGE_TYPE(s))
165 {
166 catch(raise_error(
167 "AddSpell(): Ungueltiger Schadenstyp: "+s);
168 publish);
169 }
170 }
171
Bugfix7d66c1d2016-11-20 17:27:15 +0100172 // Falls func ein String ist eine Closure erstellen und diese speichern.
Bugfix9cba67c2017-01-01 14:55:42 +0100173 if(stringp(func) && sizeof(func))
Bugfix7d66c1d2016-11-20 17:27:15 +0100174 {
175 cl=symbol_function(func,this_object());
Bugfix49681542016-12-30 21:36:08 +0100176 if(!closurep(cl))
Bugfix7d66c1d2016-11-20 17:27:15 +0100177 {
178 catch(raise_error(
179 "AddSpell(): Es konnte keine Closure fuer "+func+" erstellt werden.");
180 publish);
181 }
182 }
183 else
184 {
Bugfixb59c0882017-01-08 18:50:15 +0100185 // Leerstrings durch 0 ersetzen.
186 if(stringp(func))
187 {
188 cl=0;
189 }
190 else
191 {
192 cl=func;
193 }
Bugfix7d66c1d2016-11-20 17:27:15 +0100194 }
195
heull0019a0c6bc2018-02-15 13:45:14 +0100196 if(damage==0 && !closurep(cl))
197 {
198 catch(raise_error(
199 "AddSpell(): Bei damage=0 muss eine Funktion eingetragen werden.");
200 publish);
201 return 0;
202 }
203
Zesstradee2e562018-02-28 23:51:56 +0100204 if(!sizeof(TextForEnemy) ||
205 (pointerp(TextForEnemy) && !sizeof(TextForEnemy[0])))
206 {
207 TextForEnemy=0;
208 }
209 else if(stringp(TextForEnemy))
210 {
211 TextForEnemy=({TextForEnemy,MT_LOOK});
212 }
213 else if(pointerp(TextForEnemy) &&
214 (!stringp(TextForEnemy[0]) || !intp(TextForEnemy[1])))
215 {
216 raise_error(
217 "AddSpell(): Falsche Datentypen fuer TextForEnemy");
218 }
219
220 if(!sizeof(TextForOthers) ||
221 (pointerp(TextForOthers) && !sizeof(TextForOthers[0])))
222 {
223 TextForOthers=0;
224 }
225 else if(stringp(TextForOthers))
226 {
227 TextForOthers=({TextForOthers,MT_LOOK});
228 }
229 else if(pointerp(TextForOthers) &&
230 (!stringp(TextForOthers[0]) || !intp(TextForOthers[1])))
231 {
232 raise_error(
233 "AddSpell(): Falsche Datentypen fuer TextForOthers");
234 }
235
236
MG Mud User88f12472016-06-24 23:31:02 +0200237 // Falls vorhanden, alte Syntax auf die von replace_personal() anpassen,
238 // die im heart_beat() beim Ausgeben der Meldung verwendet wird.
Bugfix4eae8572018-03-02 14:38:48 +0100239 if ( pointerp(TextForOthers) && strstr(TextForOthers[0], "@", 0) != -1 )
MG Mud User88f12472016-06-24 23:31:02 +0200240 {
241 // Zeichen nach @WER & Co in runde Klammern einschliessen, damit es als
242 // Sub-Pattern im Ausgabestring wiederholt werden kann. Ansonsten wuerde
243 // es mit ersetzt.
Zesstradee2e562018-02-28 23:51:56 +0100244 TextForOthers[0] = regreplace(TextForOthers[0], "@WER([^1-9QU])", "@WER1\\1", 1);
245 TextForOthers[0] = regreplace(TextForOthers[0], "@WESSEN([^1-9QU])",
MG Mud User88f12472016-06-24 23:31:02 +0200246 "@WESSEN1\\1", 1);
Zesstradee2e562018-02-28 23:51:56 +0100247 TextForOthers[0] = regreplace(TextForOthers[0], "@WEM([^1-9QU])", "@WEM1\\1", 1);
248 TextForOthers[0] = regreplace(TextForOthers[0], "@WEN([^1-9QU])", "@WEN1\\1", 1);
MG Mud User88f12472016-06-24 23:31:02 +0200249 }
250 total_rates=Query("npc:total_rates")+rate;
251 spells=Query(P_SPELLS);
252 if (!pointerp(spells))
253 spells=({});
254 spells+=({({total_rates, damage, TextForEnemy, TextForOthers,
Bugfix7d66c1d2016-11-20 17:27:15 +0100255 dam_type, cl, spellarg})});
MG Mud User88f12472016-06-24 23:31:02 +0200256 Set(P_SPELLS,spells);
257 Set("npc:total_rates",total_rates);
258 return 1;
259}
260
261int AutoAttack(object ob) {
262 mixed m;
263
264 if (!query_once_interactive(ob))
265 return 0;
266 if (mappingp(m=QueryProp(P_AGGRESSIVE))) {
267 mixed *ind,x,z;
268 float f;
269 int i,n;
270
271 ind=m_indices(m)-({0});n=0;f=0.0;
272 for (i=sizeof(ind)-1;i>=0;i--) {
273 x=ind[i];
274 if ((z=m[x][ob->QueryProp(x)]) || (z=m[x][0])) {
275 f=f+(float)z;
276 n++;
277 }
278 }
279 if (n)
280 m=f/((float)n);
281 else
282 m=m[0];
283 }
284 if (((int)(100*(m+ob->QueryProp(P_AGGRESSIVE))))<=random(100))
285 return 0;
286 if (IS_LEARNER(ob)
287 && (ob->QueryProp(P_INVIS)
288 || ob->QueryProp(P_WANTS_TO_LEARN)))
289 return 0;
290 return 1;
291}
292
293void SpellAttack(object enemy) {
294}
295
296#if 0
297TJ(string s) {
298 object o;
299 if (o=find_player("jof"))
300 tell_object(o,sprintf("%O: %s\n",this_object(),s));
301}
302#else
303#define TJ(x)
304#endif
305
306protected void heart_beat() {
307 int r,i;
308 mixed env,*spells, sinfo;
309 object enemy;
310
311 if ( --beatcount < 0 )
312 beatcount = 0;
313
314 if (!beatcount && !Query(P_HB)) {
315 if (!environment()) {
316 set_heart_beat(0);
317 heartbeat = 0;
318 if( clonep(this_object()) ) remove();
319 return;
320 }
321 if (!QueryProp(P_POISON)) {
322 // Spieler anwesend?
323 env = filter(all_inventory(environment()), #'query_once_interactive);
324 if (!sizeof(env)) {
325 // Nein, HBs abschalten.
326 set_heart_beat(0);
327 heartbeat=0;
328 TJ("OFF\n");
329 beatcount=HB_CHECK;
330 Set("npc:beat_off_num",absolute_hb_count());
331 return;
332 }
333 }
334 }
335 ::heart_beat();
336 if (!ME)
337 return;
338 enemy=SelectEnemy();
339 if (QueryProp(P_AGGRESSIVE)
340 && (!enemy || environment()!=environment(enemy))
341 && !beatcount) {
342 beatcount=HB_CHECK;
343 env=filter(all_inventory(environment()),#'AutoAttack);
344 if (!sizeof(env))
345 return;
346 i=random(sizeof(env));
347 Kill(env[i]);
348 }
349 else if (!beatcount)
350 beatcount=HB_CHECK;
351 if (!objectp(enemy) ||QueryProp(P_DISABLE_ATTACK)>0)
352 return;
353 SpellAttack(enemy);
354
355 if (!pointerp(spells=Query(P_SPELLS))
356 || !sizeof(spells)
357 || !objectp(enemy=SelectEnemy())
358 || environment(enemy)!=environment()
359 || (QueryProp(P_DISABLE_ATTACK)>0)
360 || random(100)>Query(P_SPELLRATE))
361 return;
362 r=random(Query("npc:total_rates"));
363 for (i=sizeof(spells)-1;(i>0 && spells[i-1][SPELL_TOTALRATE]>r);i--)
364 ;
Zesstradee2e562018-02-28 23:51:56 +0100365 <int|string>* akt_spell_mess=spells[i][SPELL_TEXT_FOR_ENEMY];
MG Mud User88f12472016-06-24 23:31:02 +0200366 // Nur, wenn ueberhaupt eine Meldung gesetzt wurde, muss diese verarbeitet
367 // werden.
Zesstradee2e562018-02-28 23:51:56 +0100368 if(pointerp(akt_spell_mess))
MG Mud User88f12472016-06-24 23:31:02 +0200369 {
Zesstradee2e562018-02-28 23:51:56 +0100370 enemy->ReceiveMsg(
371 akt_spell_mess[0],
372 akt_spell_mess[1],
373 MA_SPELL);
374 }
375 akt_spell_mess=spells[i][SPELL_TEXT_FOR_OTHERS];
376 if(pointerp(akt_spell_mess))
377 {
Zesstradee2e562018-02-28 23:51:56 +0100378 send_room(environment(),
Bugfix6074b532018-10-29 12:40:50 +0100379 replace_personal(akt_spell_mess[0], ({enemy}), 1),
Zesstradee2e562018-02-28 23:51:56 +0100380 akt_spell_mess[1],
381 MA_SPELL,
382 0,
383 ({enemy,this_object()}),
384 this_object());
MG Mud User88f12472016-06-24 23:31:02 +0200385 }
386 sinfo = deep_copy(spells[i][SPELL_ARG]);
387 if(!mappingp(sinfo))
388 sinfo=([ SI_MAGIC_TYPE :({ MT_ANGRIFF }) ]);
389 else if(!sinfo[SI_MAGIC_TYPE])
390 sinfo[ SI_MAGIC_TYPE]=({ MT_ANGRIFF });
391 if(!sinfo[SP_PHYSICAL_ATTACK] &&
392 (enemy->SpellDefend(this_object(),sinfo) >
393 random(MAX_ABILITY+QueryProp(P_LEVEL)*50))){
Zesstradee2e562018-02-28 23:51:56 +0100394 enemy->ReceiveMsg(
395 "Du wehrst den Spruch ab.",
396 MT_NOTIFICATION,
397 MA_SPELL);
398 send_room(environment(),
399 enemy->Name(WER,1)+" wehrt den Spruch ab.",
400 MT_LOOK,
401 MA_SPELL,
402 0,
403 ({ enemy, this_object()}));
MG Mud User88f12472016-06-24 23:31:02 +0200404 return ;
405 }
heull0019a0c6bc2018-02-15 13:45:14 +0100406 int damage;
407 // Bei 0 sparen wir uns das Defend() und rufen nur die Funktion auf.
408 if(spells[i][SPELL_DAMAGE])
409 {
410 damage=random(spells[i][SPELL_DAMAGE])+1;
411 enemy->Defend(damage, spells[i][SPELL_DAMTYPE],
412 spells[i][SPELL_ARG],
413 this_object());
414 }
MG Mud User88f12472016-06-24 23:31:02 +0200415
416 // Falls der Gegner (oder wir) im Defend stirbt, hier abbrechen
417 if ( !objectp(ME) || !objectp(enemy)
418 || enemy->QueryProp(P_GHOST) ) return;
419
Bugfix7d66c1d2016-11-20 17:27:15 +0100420 closure cl = spells[i][SPELL_FUNC];
421 if (cl)
422 {
423 if (closurep(cl))
424 catch(funcall(cl, enemy, damage, spells[i][SPELL_DAMTYPE]);publish);
425 else
426 raise_error(sprintf("P_SPELL defekt: SPELL_FUNC in Spell %i ist keine "
427 "Closure.\n", i));
428 }
MG Mud User88f12472016-06-24 23:31:02 +0200429}
430
431// Heartbeats nachholen.
432private void catch_up_hbs() {
433 // gibt es HBs zum nachholen?
434 int beat_off_num = Query("npc:beat_off_num");
435 if (!beat_off_num)
436 return; // nein.
437 // wieviele HBs nachholen?
438 beat_off_num = absolute_hb_count() - beat_off_num;
439
440 if (beat_off_num>0) {
441 // Nicht ausgefuehrtes HEILEN nachholen
442 int rlock=QueryProp(P_NO_REGENERATION);
443 int hp=QueryProp(P_HP);
444 int sp=QueryProp(P_SP);
445 int alc=QueryProp(P_ALCOHOL);
446 if (!(rlock & NO_REG_HP)) {
447 hp+=beat_off_num/HEAL_DELAY+alc/ALCOHOL_DELAY;
448 SetProp(P_HP,hp);
449 }
450 if (!(rlock & NO_REG_SP)) {
451 sp+=beat_off_num/HEAL_DELAY+alc/ALCOHOL_DELAY;
452 SetProp(P_SP,sp);
453 }
454 alc-=beat_off_num/ALCOHOL_DELAY;
455 if ( alc < 0 )
456 alc = 0;
457 SetProp(P_ALCOHOL,alc);
458 int da = QueryProp(P_DISABLE_ATTACK);
459 // Paralysen abbauen
460 if ( da > 0 ) {
461 da -= beat_off_num;
462 if ( da < 0 )
463 da = 0;
464 SetProp( P_DISABLE_ATTACK, da );
465 }
466 // Hunttimes aktualisieren, Feinde expiren
467 update_hunt_times(beat_off_num);
468 if (!heartbeat)
469 // HBs immer noch abgeschaltet, naechstes Mal HBs seit jetzt nachholen.
470 Set("npc:beat_off_num",absolute_hb_count());
471 else
472 // HB laeuft wieder, nix mehr nachholen, bis zur naechsten Abschaltung.
473 Set("npc:beat_off_num",0);
474 }
475}
476
Zesstra5b71ebb2018-03-07 20:50:35 +0100477public varargs void init(object origin)
478{
MG Mud User88f12472016-06-24 23:31:02 +0200479 // ggf. Heartbeats nachholen und wieder einschalten.
480 if (!heartbeat) {
481 set_heart_beat(1);
482 heartbeat=1;
483 catch_up_hbs();
484 }
485
486 if (AutoAttack(this_player()))
487 Kill(this_player());
488}
489
490private nosave closure mod_att_stat;
491
492int Defend(int dam, mixed dam_type, mixed spell, object enemy) {
493 if (objectp(enemy=(enemy||this_player()))
494 && query_once_interactive(enemy)
495 && !IS_LEARNER(enemy)) {
496 if (!objectp(get_type_info(mod_att_stat,2))) {
497 object ma;
498 if (!objectp(ma=find_object(STATMASTER)))
499 return ::Defend(dam,dam_type,spell,enemy);
500 // Keine Statistik wenn Master nicht geladen ist.
501 mod_att_stat=symbol_function("ModifyAttackStat",ma);
502 }
503 funcall(mod_att_stat,
504 enemy->QueryProp(P_GUILD),
505 enemy->QueryProp(P_GUILD_LEVEL),
506 dam,
507 dam_type,
508 spell);
509 }
510
511 return ::Defend(dam,dam_type,spell,enemy);
512}