blob: 1e17201646676ec1fca55bb2dda8020b4b7327eb [file] [log] [blame]
// MorgenGrauen MUDlib
//
// npc/combat.c -- NPC-spezifische Kampffunktionen
//
// $Id: combat.c 9488 2016-02-19 21:24:04Z Arathorn $
#pragma strong_types
#pragma save_types
#pragma range_check
#pragma no_clone
inherit "std/living/combat";
#include <combat.h>
#include <language.h>
#include <properties.h>
#include <wizlevels.h>
#include <health.h>
#include <new_skills.h>
#define NEED_PROTOTYPES 1
#include <living/life.h>
#undef NEED_PROTOTYPES
#define HB_CHECK 7
#define ME this_object()
#define STATMASTER "/p/service/rochus/guildstat/master"
nosave int heartbeat, beatcount;
private void catch_up_hbs();
protected void create() {
::create();
beatcount=1;
heartbeat=1;
}
protected void create_super() {
set_next_reset(-1);
}
// aktuelles Lebewesen, fuer das dieser NPC gerade taetig ist. Default:
// Spieler, bei dem er als Helfer-NPC registriert ist.
public object QueryUser()
{
mixed helperdata = QueryProp(P_HELPER_NPC);
if (pointerp(helperdata) && objectp(helperdata[0]))
return helperdata[0];
return 0;
}
// ggf. Feinde expiren. Soll das Problem verringern, dass Spieler nach Tagen
// erst die Meldung kriegen, dass der NPC sie nicht mehr jagt, wenn der HB
// reaktivert wird.
void reset() {
// ggf. die abgeschalteten HBs nachholen.
if (!heartbeat)
catch_up_hbs();
// ggf. P_ENEMY_DAMAGE zuruecksetzen
ResetEnemyDamage();
}
static void _set_max_hp(int i) {
Set(P_MAX_HP,i);
SetProp(P_HP,i);
}
static void _set_max_sp(int i) {
Set(P_MAX_SP,i);
SetProp(P_SP,i);
}
// Check-Funktion fuer die P_NO_ATTACK-QueryMethod
static mixed _check_immortality()
{
int t;
if ( !(t = Query("time_to_mortality")) || time() > t ){
// Zeit ist abgelaufen - wieder angreifbar machen
Set( P_NO_ATTACK, 0, F_QUERY_METHOD );
heartbeat = 1;
beatcount = 1;
set_heart_beat(1);
return 0;
}
// der NPC ist noch unangreifbar
return break_string( capitalize(name( WER, 1 )) + " versteckt sich hinter "
"einem Fehler im Raum-Zeit-Gefuege und entgeht so "
"voruebergehend allen Angriffen.", 78 );
}
// wenn der HeartBeat buggt, wird diese Funktion vom Master aufgerufen
public void make_immortal()
{
// fuer 5 Minuten unangreifbar machen
Set( P_NO_ATTACK, #'_check_immortality, F_QUERY_METHOD );
Set( "time_to_mortality", time() + 300, F_VALUE );
// damit die Spieler keinen Vorteil durch den Bug haben, heilen
heal_self(10000);
// da nun der Heartbeat abgeschaltet ist und normalerweise erst
// reaktiviert wird, sobald jemand nach 5min P_NO_ATTACK abfragt, muss man
// aber auf Viecher achten, die immer nen Heartbeat haben wollen. In dem
// fall per call_out selber die Prop abfragen.
if (QueryProp(P_HB))
call_out(#'QueryProp, 301, P_NO_ATTACK);
}
// Damit NPCs gegeneinander weiterkaempfen, auch wenn kein Spieler
// in der Naehe ist:
static int _query_hb()
{
// TODO: return InFight() || Query(P_HB, F_VALUE), sobald InFight()
// geaendert.
return (InFight() || Query(P_HB,F_VALUE)) ? 1 : 0;
}
#define SPELL_TOTALRATE 0
#define SPELL_SINFO 1
#define SPELL_TEXT_FOR_ENEMY 2
#define SPELL_TEXT_FOR_OTHERS 3
varargs int AddSpell(int rate, int damage,
string|<int|string>* TextForEnemy, string|<int|string>* TextForOthers,
string|string* dam_type, string|closure func, int|mapping sinfo)
{
mixed *spells;
int total_rates;
closure cl;
if(rate<=0 || damage<0) return 0;
// Tatsaechlich ist es immer ein nicht-physischer Angriff, wenn spellarg ein
// int ist, weil wenn spellarg==0 ist der Default nicht-physischer Angriff
// und bei spellarg!=0 auch. Nur mit einem mapping kann man einen phys.
// Angriff erzeugen.
if (intp(sinfo))
sinfo = ([ SI_SPELL: ([SP_PHYSICAL_ATTACK: 0]) ]);
else
{
// wenn das sinfo-Mapping nicht den Key SI_SPELL enthaelt, gehen wir davon
// aus, dass es ein alter Aufrufer von AddSpell ist, welcher noch davon
// ausgeht, dass si_spell an AddSpell() uebergeben werden soll. In diesem
// Fall bauen wir ein sinfo und nehmen das uebergebene sinfo als SI_SPELL.
if (!member(sinfo,SI_SPELL))
sinfo = ([ SI_SPELL: sinfo ]);
}
sinfo[SI_SKILLDAMAGE] = damage;
if(stringp(dam_type))
dam_type=({dam_type});
else if(!pointerp(dam_type))
{
if(sinfo[SI_SPELL][SP_PHYSICAL_ATTACK])
dam_type=({DT_BLUDGEON});
else
dam_type=({DT_MAGIC});
}
foreach(string s : dam_type)
{
if(!VALID_DAMAGE_TYPE(s))
{
catch(raise_error(
"AddSpell(): Ungueltiger Schadenstyp: "+s);
publish);
}
}
sinfo[SI_SKILLDAMAGE_TYPE] = dam_type;
if(!member(sinfo, SI_MAGIC_TYPE))
sinfo[SI_MAGIC_TYPE] = ({ MT_ANGRIFF });
// Falls func ein String ist eine Closure erstellen und diese speichern.
if(stringp(func))
{
if (sizeof(func))
{
cl=symbol_function(func,this_object());
if(!closurep(cl))
{
catch(raise_error(
"AddSpell(): Es konnte keine Closure fuer "+func+" erstellt werden.");
publish);
}
}
}
else
{
cl=func;
}
sinfo[SI_CLOSURE] = cl;
if(damage==0 && !closurep(cl))
{
catch(raise_error(
"AddSpell(): Bei damage=0 muss eine Funktion eingetragen werden.");
publish);
return 0;
}
if(!sizeof(TextForEnemy) ||
(pointerp(TextForEnemy) && !sizeof(TextForEnemy[0])))
{
TextForEnemy=0;
}
else if(stringp(TextForEnemy))
{
TextForEnemy=({TextForEnemy,MT_LOOK});
}
else if(pointerp(TextForEnemy) &&
(!stringp(TextForEnemy[0]) || !intp(TextForEnemy[1])))
{
raise_error(
"AddSpell(): Falsche Datentypen fuer TextForEnemy");
}
if(!sizeof(TextForOthers) ||
(pointerp(TextForOthers) && !sizeof(TextForOthers[0])))
{
TextForOthers=0;
}
else if(stringp(TextForOthers))
{
TextForOthers=({TextForOthers,MT_LOOK});
}
else if(pointerp(TextForOthers) &&
(!stringp(TextForOthers[0]) || !intp(TextForOthers[1])))
{
raise_error(
"AddSpell(): Falsche Datentypen fuer TextForOthers");
}
// Falls vorhanden, alte Syntax auf die von replace_personal() anpassen,
// die im heart_beat() beim Ausgeben der Meldung verwendet wird.
if ( pointerp(TextForOthers) && strstr(TextForOthers[0], "@", 0) != -1 )
{
// Zeichen nach @WER & Co in runde Klammern einschliessen, damit es als
// Sub-Pattern im Ausgabestring wiederholt werden kann. Ansonsten wuerde
// es mit ersetzt.
TextForOthers[0] = regreplace(TextForOthers[0], "@WER([^1-9QU])", "@WER1\\1", 1);
TextForOthers[0] = regreplace(TextForOthers[0], "@WESSEN([^1-9QU])",
"@WESSEN1\\1", 1);
TextForOthers[0] = regreplace(TextForOthers[0], "@WEM([^1-9QU])", "@WEM1\\1", 1);
TextForOthers[0] = regreplace(TextForOthers[0], "@WEN([^1-9QU])", "@WEN1\\1", 1);
}
total_rates=Query("npc:total_rates", F_VALUE)+rate;
spells=Query(P_SPELLS);
if (!pointerp(spells))
spells=({});
spells+=({({total_rates, sinfo, TextForEnemy, TextForOthers})});
Set(P_SPELLS,spells);
Set("npc:total_rates",total_rates, F_VALUE);
return 1;
}
int AutoAttack(object ob) {
mixed m;
if (!query_once_interactive(ob))
return 0;
if (mappingp(m=QueryProp(P_AGGRESSIVE))) {
mixed *ind,x,z;
float f;
int i,n;
ind=m_indices(m)-({0});n=0;f=0.0;
for (i=sizeof(ind)-1;i>=0;i--) {
x=ind[i];
if ((z=m[x][ob->QueryProp(x)]) || (z=m[x][0])) {
f=f+z;
n++;
}
}
if (n)
m=f/((float)n);
else
m=m[0];
}
if (((int)(100*(m+ob->QueryProp(P_AGGRESSIVE))))<=random(100))
return 0;
if (IS_LEARNER(ob)
&& (ob->QueryProp(P_INVIS)
|| ob->QueryProp(P_WANTS_TO_LEARN)))
return 0;
return 1;
}
void SpellAttack(object enemy) {
}
#if 0
TJ(string s) {
object o;
if (o=find_player("jof"))
tell_object(o,sprintf("%O: %s\n",this_object(),s));
}
#else
#define TJ(x)
#endif
protected void heart_beat() {
int r,i;
mixed env,*spells, sinfo;
object enemy;
if ( --beatcount < 0 )
beatcount = 0;
if (!beatcount && !Query(P_HB)) {
if (!environment()) {
set_heart_beat(0);
heartbeat = 0;
if( clonep(this_object()) ) remove();
return;
}
if (!QueryProp(P_POISON)) {
// Spieler anwesend?
env = filter(all_inventory(environment()), #'query_once_interactive);
if (!sizeof(env)) {
// Nein, HBs abschalten.
set_heart_beat(0);
heartbeat=0;
TJ("OFF\n");
beatcount=HB_CHECK;
Set("npc:beat_off_num",absolute_hb_count());
return;
}
}
}
::heart_beat();
if (!ME)
return;
enemy=SelectEnemy();
if (QueryProp(P_AGGRESSIVE)
&& (!enemy || environment()!=environment(enemy))
&& !beatcount) {
beatcount=HB_CHECK;
env=filter(all_inventory(environment()),#'AutoAttack);
if (!sizeof(env))
return;
i=random(sizeof(env));
Kill(env[i]);
}
else if (!beatcount)
beatcount=HB_CHECK;
if (!objectp(enemy) ||QueryProp(P_DISABLE_ATTACK)>0)
return;
SpellAttack(enemy);
if (!pointerp(spells=Query(P_SPELLS))
|| !sizeof(spells)
|| !objectp(enemy=SelectEnemy())
|| environment(enemy)!=environment()
|| (QueryProp(P_DISABLE_ATTACK)>0)
|| random(100)>Query(P_SPELLRATE))
return;
// Spell aussuchen, von oben runterlaufen, bis zum ersten Spell, dessen
// SPELL_TOTALRATE grosser ist als r.
r=random(Query("npc:total_rates", F_VALUE));
for (i=sizeof(spells)-1 ; (i>0 && spells[i-1][SPELL_TOTALRATE] > r); i--)
;
<int|string>* akt_spell_mess=spells[i][SPELL_TEXT_FOR_ENEMY];
// Nur, wenn ueberhaupt eine Meldung gesetzt wurde, muss diese verarbeitet
// werden.
if(pointerp(akt_spell_mess))
{
enemy->ReceiveMsg(
akt_spell_mess[0],
akt_spell_mess[1],
MA_SPELL);
}
akt_spell_mess=spells[i][SPELL_TEXT_FOR_OTHERS];
if(pointerp(akt_spell_mess))
{
send_room(environment(),
replace_personal(akt_spell_mess[0], ({enemy}), 1),
akt_spell_mess[1],
MA_SPELL,
0,
({enemy,this_object()}),
this_object());
}
// Tiefe Kopie, damit wir nach Herzenslust fuer diese Ausfuehrung
// manipulieren koennen und alle gerufenen Defend und Ruestung etc. nix
// dauerhaft in SI_SPELL kaputt machen koennen.
sinfo = deep_copy(spells[i][SPELL_SINFO]);
sinfo[SI_ENEMY] = enemy;
if(!sinfo[SP_PHYSICAL_ATTACK] &&
(enemy->SpellDefend(this_object(),sinfo) >
random(MAX_ABILITY+QueryProp(P_LEVEL)*50)))
{
enemy->ReceiveMsg(
"Du wehrst den Spruch ab.",
MT_NOTIFICATION,
MA_SPELL);
send_room(environment(),
enemy->Name(WER,1)+" wehrt den Spruch ab.",
MT_LOOK,
MA_SPELL,
0,
({ enemy, this_object()}));
return ;
}
// Bei 0 sparen wir uns das Defend() und rufen nur weiter unter SI_CLOSURE auf.
if(sinfo[SI_SKILLDAMAGE])
{
sinfo[SI_SKILLDAMAGE] = random(sinfo[SI_SKILLDAMAGE]) + 1;
enemy->Defend(sinfo[SI_SKILLDAMAGE], sinfo[SI_SKILLDAMAGE_TYPE],
sinfo[SI_SPELL], this_object());
}
// Falls der Gegner (oder wir) im Defend stirbt, hier abbrechen
if ( !objectp(ME) || !objectp(enemy)
|| enemy->QueryProp(P_GHOST) ) return;
closure cl = sinfo[SI_CLOSURE];
if (cl)
{
if (closurep(cl))
catch(funcall(cl, enemy, sinfo[SI_SKILLDAMAGE],
sinfo[SI_SKILLDAMAGE_TYPE]);publish);
else
raise_error(sprintf("P_SPELL defekt: SPELL_FUNC in Spell %i ist keine "
"Closure.\n", i));
}
}
// Heartbeats nachholen.
private void catch_up_hbs() {
// gibt es HBs zum nachholen?
int beat_off_num = Query("npc:beat_off_num");
if (!beat_off_num)
return; // nein.
// wieviele HBs nachholen?
beat_off_num = absolute_hb_count() - beat_off_num;
if (beat_off_num>0) {
// Nicht ausgefuehrtes HEILEN nachholen
int rlock=QueryProp(P_NO_REGENERATION);
int hp=QueryProp(P_HP);
int sp=QueryProp(P_SP);
int alc=QueryProp(P_ALCOHOL);
if (!(rlock & NO_REG_HP)) {
hp+=beat_off_num/HEAL_DELAY+alc/ALCOHOL_DELAY;
SetProp(P_HP,hp);
}
if (!(rlock & NO_REG_SP)) {
sp+=beat_off_num/HEAL_DELAY+alc/ALCOHOL_DELAY;
SetProp(P_SP,sp);
}
alc-=beat_off_num/ALCOHOL_DELAY;
if ( alc < 0 )
alc = 0;
SetProp(P_ALCOHOL,alc);
int da = QueryProp(P_DISABLE_ATTACK);
// Paralysen abbauen
if ( da > 0 ) {
da -= beat_off_num;
if ( da < 0 )
da = 0;
SetProp( P_DISABLE_ATTACK, da );
}
// Hunttimes aktualisieren, Feinde expiren
update_hunt_times(beat_off_num);
if (!heartbeat)
// HBs immer noch abgeschaltet, naechstes Mal HBs seit jetzt nachholen.
Set("npc:beat_off_num",absolute_hb_count());
else
// HB laeuft wieder, nix mehr nachholen, bis zur naechsten Abschaltung.
Set("npc:beat_off_num",0);
}
}
public varargs void init(object origin)
{
// ggf. Heartbeats nachholen und wieder einschalten.
if (!heartbeat) {
set_heart_beat(1);
heartbeat=1;
catch_up_hbs();
}
if (AutoAttack(this_player()))
Kill(this_player());
}
private nosave closure mod_att_stat;
int Defend(int dam, mixed dam_type, mixed spell, object enemy) {
if (objectp(enemy=(enemy||this_player()))
&& query_once_interactive(enemy)
&& !IS_LEARNER(enemy)) {
if (!objectp(get_type_info(mod_att_stat,2))) {
object ma;
if (!objectp(ma=find_object(STATMASTER)))
return ::Defend(dam,dam_type,spell,enemy);
// Keine Statistik wenn Master nicht geladen ist.
mod_att_stat=symbol_function("ModifyAttackStat",ma);
}
funcall(mod_att_stat,
enemy->QueryProp(P_GUILD),
enemy->QueryProp(P_GUILD_LEVEL),
dam,
dam_type,
spell);
}
return ::Defend(dam,dam_type,spell,enemy);
}