// TMW2 scripts.
// Authors:
// Jesusalva
// Description:
// Town Siege utilities
// Siege Spawn
// Can be used anywhere to spawn on the whole map, margins respected.
// siege_spawn( map, mobID, Amount, eventID )
function script siege_spawn {
.@mp$=getarg(0);
.@mid=getarg(1);
.@qnt=getarg(2);
.@ev$=getarg(3);
areamonster(.@mp$, 20, 20, getmapinfo(MAPINFO_SIZE_X, .@mp$)-20, getmapinfo(MAPINFO_SIZE_Y, .@mp$)-20, strmobinfo(1, .@mid), .@mid, .@qnt, .@ev$);
return;
}
// Calculate player average level
// flag 1 - Don't count dead players
// flag 2 - Return highest player level, instead of average.
// flag 4 - Return the total sum of levels instead.
// siege_calcdiff ( map{, flags} )
function script siege_calcdiff {
.@bsum=0;
if (getarg(1,0) & 1)
.@deadcount=true;
if (getarg(1,0) & 2)
.@onlyhighest=true;
if (getarg(1,0) & 4)
.@onlytotal=true;
.@c = getunits(BL_PC, .@players, false, getarg(0));
.@skip=0;
// There is at least one player, do things properly
for (.@i = 0; .@i < .@c; .@i++) {
// Dead players are not counted
if (.@deadcount) {
if (ispcdead(strcharinfo(0, "", .@players[.@i]))) {
.@skip+=1;
continue;
}
}
.@b=readparam(BaseLevel, .@players[.@i]);
// GMs are not counted
if (getgroupid(.@players[.@i]) >= 60) {
.@skip+=1;
.@b=0;
}
.@bsum+=.@b;
if (.@b > .@highest)
.@highest=.@b;
}
// Sanitize and fallback if needed
.@c-=.@skip;
if (!.@c)
.@c=1;
//debugmes "calcdiff: Total %d Average %d Highest %d", .@bsum, (.@bsum/.@c), .@highest;
if (.@onlyhighest)
return .@highest;
else if (.@onlytotal)
return .@bsum;
else
return (.@bsum/.@c);
}
// push to $@SIEGE_TMPMOBS the <mobID> if their level is within a x levels range
// above or below <level>
// siege_push ( mobID, {level{, variation}} )
function script siege_push {
.@mi=getarg(0);
.@lv=getarg(1,0);
.@var=getarg(2,14); // Old Default: 15, and then 10
if ( is_between(.@lv-.@var, .@lv+(.@var/2), strmobinfo(3, .@mi)) ) {
//debugmes "Monster %s (%d) level %d ~= %d", strmobinfo(1, .@mi), .@mi, strmobinfo(3, .@mi), .@lv;
array_push($@SIEGE_TMPMOBS, .@mi);
}
return;
}
// Selects a monster based on player average strength and base difficulty
// It'll select monsters from 20 levels below to 20 levels above
// tp_mask is the same as TP_* constants and changes mobs present. Only TP_TULIM,
// TP_HURNS and TP_NIVAL are supported. You can use them with the "|" operand, eg.,
// TP_TULIM | TP_HURNS.
//
// It currently only creates $@SIEGE_TMPMOBS, you need to use any_of() manually.
// siege_selectmob ( blvl, difficulty{, tp_mask} )
function script siege_selectmob {
.@blv=getarg(0);
.@dif=getarg(1, 0);
.@tp=getarg(2, 0);
// We don't need .@dif, so we convert difficulty to levels
.@blv+=(.@dif/any(2,2,3));
deletearray $@SIEGE_TMPMOBS;
setarray $@SIEGE_TMPMOBS, ManaGhost, CandiedSlime, Bif, SlimeBlast;
// Now we must select mobs, using array_push() to $@SIEGE_TMPMOBS
// First, mobs on all envs
siege_push(BlackScorpion, .@blv);
siege_push(GreenSlime, .@blv);
siege_push(CaveMaggot, .@blv);
siege_push(MagicGoblin, .@blv);
siege_push(AngryBat, .@blv);
siege_push(BlackSlime, .@blv);
siege_push(BlackScorpion, .@blv);
siege_push(Forain, .@blv);
siege_push(Terranite, .@blv);
siege_push(JackO, .@blv);
siege_push(BlackMamba, .@blv);
siege_push(TerraniteProtector, .@blv);
siege_push(Reaper, .@blv);
// What if we are trying to select a boss and they're... overleveled?
// We need a level 105, 120 and a level 135 monsters
siege_push(FallenKing2, .@blv);
siege_push(TerraniteKing, .@blv);
// Now, mobs on only certain envs
if (.@tp & TP_TULIM) {
siege_push(AngryScorpion, .@blv);
siege_push(AngryRedScorpion, .@blv);
siege_push(DesertBandit, .@blv);
siege_push(LavaSlime, .@blv);
siege_push(OldSnake, .@blv);
siege_push(Snake, .@blv);
}
if (.@tp & TP_HURNS) {
siege_push(RedSlime, .@blv);
siege_push(Bandit, .@blv);
siege_push(RedMushroom, .@blv);
siege_push(RobinBandit, .@blv);
siege_push(AngryYellowSlime, .@blv);
siege_push(GrassSnake, .@blv);
siege_push(WickedMushroom, .@blv);
siege_push(GreenDragon, .@blv);
}
if (.@tp & TP_NIVAL) {
siege_push(Bluepar, .@blv);
siege_push(Wolvern, .@blv);
siege_push(Yeti, .@blv);
siege_push(Moggun, .@blv); // PS. Not aggressive
}
// Removed
//siege_push(DarkLizard, .@blv);
//siege_push(Crafty, .@blv);
return;
}
/////////////////////////////////////////////////////////////
// Prepare a siege with optional announce
// siege_setup ( map )
function script siege_setup {
.@m$=getarg(0);
// Save old map zone
if (getmapinfo(MAPINFO_ID, .@m$) < 1) {
return Exception("SIEGE ERROR, INVALID MAP ID: "+.@m$, RB_ISFATAL|RB_DEBUGMES|RB_IRCBROADCAST|RB_GLOBALANNOUNCE);
}
//$@MZONE$[getmapinfo(MAPINFO_ID, .@m$)]=getmapinfo(MAPINFO_ZONE, .@m$);
// Apply changes
addmapmask .@m$, MASK_MATTACK;
changemusic .@m$, any("mythica.ogg", "eric_matyas_ghouls.ogg", "misuse.ogg", "Arabesque.ogg");
disablenpc("Mana Stone");
if (.@m$ != "003-1")
pvpon(.@m$);
setmapflag(.@m$,mf_zone,"MMO"); // MMO Zone: Overrides GM Commands
setmapflag(.@m$,mf_bexp,rand2(135,142)); // 35~42% EXP UP on siege maps
.@tn$ = MapToLoc(.@m$, false);
if (getd("$"+.@tn$+"_SIEGEXP")) {
setmapflag(.@m$, mf_bexp, 300); // Triple EXP is on
kamibroadcast("Experience for siege map has been set to 3×!", "INFORMATION");
setd("$"+.@tn$+"_SIEGEXP", 0);
}
return;
}
// Check if boss was killed or not
// siege_check ( map )
function script siege_check {
.@m$=getarg(0);
.@mb=0;
.@mb+=mobcount(.@m$, "#SiegeCtrl::OnSergeantDeath");
.@mb+=mobcount(.@m$, "#SiegeCtrl::OnLieutenantDeath");
.@mb+=mobcount(.@m$, "#SiegeCtrl::OnCaptainDeath");
.@mb+=mobcount(.@m$, "#SiegeCtrl::OnColonelDeath");
.@mb+=mobcount(.@m$, "#SiegeCtrl::OnGeneralDeath");
// Players failed, so reduce score in 1~5 (like Sergeant~General).
// In future, it could be inverse proportion (-9 for sergeant, -1 for general)
if (.@mb) {
if ($GAME_STORYLINE == 2)
$MK_TEMPVAR-=rand2(1, 5);
kamibroadcast("Players failed to defend the city!!");
debugmes "Number of boss grade monsters found: %d", .@mb;
$SIEGE_DIFFICULTY=max(1, ($SIEGE_DIFFICULTY/2));
// Lower the town exports in 5% (but never more than 25 GP)
.@var$="$"+strtoupper(MapToLoc(.@m$))+"_EXPORT";
.@pen=min(25, getd(.@var$)/20);
setd(.@var$, getd(.@var$)-.@pen);
} else {
kamibroadcast("The city was defended with success! GG, everyone!");
$SIEGE_DIFFICULTY+=1;
}
return;
}
// Revert what siege_setup did
// siege_revert ( map )
function script siege_revert {
.@m$=getarg(0);
// Revert map zone (to town, or to blank) and delete backup
removemapflag(.@m$,mf_zone);
//setmapflag(.@m$,mf_zone,$@MZONE$[getmapinfo(MAPINFO_ID, .@m$)]);
setmapflag(.@m$,mf_zone,"Normal2"); // Normal doesn't works...
//setmapflag(.@m$,mf_zone,"All");
//$@MZONE$[getmapinfo(MAPINFO_ID, .@m$)]="";
removemapmask .@m$, MASK_MATTACK;
changemusic .@m$, "caketown.ogg"; // :>
pvpoff(.@m$);
removemapflag(.@m$,mf_bexp);
removemapflag(.@m$,mf_nosave);
setmapflag(.@m$,mf_bexp,100);
killmonsterall(.@m$);
return;
}
// Create the Siege Boss for #SiegeCtrl utility, DO NEVER CAST TWICE
// siege_boss ( map, difficulty )
function script siege_boss {
.@m$=getarg(0);
.@s=getarg(1,0);
// We now select based on player average level and a seed of randomness
.@val=siege_calcdiff(.@m$)+.@s;
// Nobody is on map: Be TRULY random
if (.@val < 10) {
.@val=rand(20,97)+.@s;
}
// Switch an adequate boss, almost always stronger
if (.@val <= 25) {
.@mobId=MonsterSergeant;
.@ts$="Sergeant";
} else if (.@val <= 45) {
.@mobId=MonsterLieutenant;
.@ts$="Lieutenant";
} else if (.@val <= 67) {
.@mobId=MonsterCaptain;
.@ts$="Captain";
} else if (.@val <= 90) {
.@mobId=MonsterColonel;
.@ts$="Colonel";
} else {
.@mobId=MonsterGeneral;
.@ts$="General";
}
// We want spawn point to be fixed
.@lx=array_find($@LOCMASTER_MAP$, .@m$);
.@xm=$@LOCMASTER_X[.@lx];
.@ym=$@LOCMASTER_Y[.@lx];
.@xm=.@xm+rand(-1,1);
.@ym=.@ym+rand(-1,1);
// Announce and spawn
.@mg=monster(.@m$, .@xm, .@ym, strmobinfo(1, .@mobId), .@mobId, 1, "#SiegeCtrl::On"+.@ts$+"Death");
// Boost the boss stats based on difficulty and nº of players online
.@bhp=getunitdata(.@mg, UDT_MAXHP);
.@bat=getunitdata(.@mg, UDT_ATKMAX);
.@bai=getunitdata(.@mg, UDT_ATKMIN);
.@bdf=getunitdata(.@mg, UDT_DEF);
.@bcr=getunitdata(.@mg, UDT_CRIT);
.@s+=getusers(1);
setunitdata(.@mg, UDT_MAXHP, .@bhp+(.@s*250)+.@val*5*getmapusers(.@m$));
setunitdata(.@mg, UDT_HP, .@bhp+(.@s*250)+.@val*5*getmapusers(.@m$));
setunitdata(.@mg, UDT_ATKMAX, .@bat+(.@s*5));
setunitdata(.@mg, UDT_DEF, .@bdf+(.@s*4));
setunitdata(.@mg, UDT_CRIT, .@bcr+(.@s*3));
setunitdata(.@mg, UDT_ATKMIN, .@bai+(.@s*2));
// Spawn some scouts
areamonster(.@m$, .@xm-1, .@ym-1, .@xm+1, .@ym+1, "Scout", any(GreenSlime,RedSlime,AngryYellowSlime), 2);
areamonster(.@m$, .@xm-1, .@ym-1, .@xm+1, .@ym+1, "Scout", any(GreenSlime,RedSlime,AngryYellowSlime), 2);
areamonster(.@m$, .@xm-1, .@ym-1, .@xm+1, .@ym+1, "Scout", any(GreenSlime,RedSlime,AngryYellowSlime), 2);
announce("##1The Monster "+.@ts$+" arrived! Watch out!", bc_all);
return;
}
// Spawn some monsters
// siege_cast ( map, NPCName, {, difficulty{, tpflag}} )
function script siege_cast {
// mz - map ; n - name ; d - difficulty ; tp - teleport
// a - ammount ; e - mobId
.@mz$=getarg(0);
.@n$=getarg(1);
.@d=getarg(2,0);
.@tp=getarg(3,0);
// Difficulty doesn't applies to Tulimshar
if (.@mz$ == "003-1")
.@d=0;
siege_selectmob(siege_calcdiff(.@mz$), .@d, .@tp);
// How many monsters? This value is multiplied by 3;
// 1 per player (= 3 mobs), 1 each 5 difficulty, 1 always present.
.@a=getmapusers(.@mz$);
.@a+=(.@d/5)+1;
.@e=any_of($@SIEGE_TMPMOBS);
array_remove($@SIEGE_TMPMOBS, .@e);
siege_spawn(.@mz$, .@e, .@a, "#SiegeCtrl::OnRespawn");
.@e=any_of($@SIEGE_TMPMOBS);
array_remove($@SIEGE_TMPMOBS, .@e);
siege_spawn(.@mz$, .@e, .@a, "#SiegeCtrl::OnRespawn");
.@e=any_of($@SIEGE_TMPMOBS);
array_remove($@SIEGE_TMPMOBS, .@e);
siege_spawn(.@mz$, .@e, .@a, "#SiegeCtrl::OnRespawn");
return;
}
////////////////////////////////////////////
// Utility Function
// do_siege ( town, outskirts, varcode, flag, npc, timer )
function script do_siege {
.@m$=getarg(0);
.@o$=getarg(1);
.@c$=getarg(2);
.@tp=getarg(3);
.@n$=getarg(4);
.@t=getarg(5);
// Dry run
if ($@SIEGE_ABORTED)
return;
// If no active player, KILL THE SCRIPT
.@c = getunits(BL_PC, .@players, MAX_CYCLE_PC);
.@idle = 0;
for (.@i = 0; .@i < .@c; .@i++) {
attachrid(.@players[.@i]);
if (checkidle() < 450)
.@idle++;
detachrid();
}
// No one is active, cancel the event
if (!.@idle) {
kamibroadcast(col(b("EVENT CANCELLED DUE TO PLAYER INACTIVITY"),1));
$@MK_AGGRO=$@MK_AGGRO/5; // Lower aggro bar to 20%
// Cleanup and Garbage Collection
siege_revert(.@m$);
siege_revert(.@o$);
enablenpc("Mana Stone");
setd("$@SIEGE_"+.@c$, 0);
setd("$@SIEGE_ABORTED", true);
setd("$@MK_SCENE", MK_NONE);
return;
}
// In past, we had a $@SIEGE_<town> to determine difficulty
// This behavior is now deprecated, we use a global $SIEGE_DIFFICULTY
// Which raises in 1 every victory and lowers in 1 every defeat (capped at 1)
// But this behavior can be overriden
if (!getd("$@SIEGE_"+.@c$)) {
.@difc=max(1, getd("$SIEGE_DIFFICULTY"));
// And then, we reset $@SIEGE_<town> so it can be manipulated
// And set .@difc to this value
setd("$@SIEGE_"+.@c$, .@difc);
}
// Set difficulty based on previous value
.@difc=getd("$@SIEGE_"+.@c$);
switch (.@t) {
// Warmup
case 70:
case 95:
siege_cast(.@m$, .@n$, .@difc, .@tp);
case 35:
siege_cast(.@o$, .@n$, .@difc, .@tp);
break;
// Setup and casts
case 0:
siege_setup(.@o$);
siege_cast(.@o$, .name$, 0, TP_HURNS);
break;
case 60:
siege_setup(.@m$);
siege_cast(.@o$, .@n$, 0, .@tp);
mapannounce(.@m$, "##2Message to all NPCs in town: Take shelter!", bc_map);
break;
case 90:
siege_cast(.@m$, .@n$, .@difc, .@tp);
siege_cast(.@o$, .@n$, .@difc, .@tp);
break;
// Boss stage
case 280:
siege_boss(.@m$, .@difc);
siege_cast(.@m$, .@n$, .@difc, .@tp);
break;
// Difficulty Raisers
case 310:
case 520:
case 640:
.@varsig=.@difc;
setd("$@SIEGE_"+.@c$, .@varsig+1);
// Regular flow
case 195:
case 220:
case 265:
//case 280: BOSS WAVE
//case 310: difficulty raiser
case 355:
case 400:
case 445:
case 490:
//case 520: difficulty raiser
case 535:
case 580:
case 625:
//case 640: difficulty raiser
case 670:
siege_cast(.@m$, .@n$, .@difc, .@tp);
break;
// Ending flow
// TODO: It would be better to make these values relative to MK_SIEGE_DURATION
case 700:
mapannounce(.@m$, "##1The Monster Army is planning to retreat soon!", bc_map);
siege_cast(.@m$, .@n$, .@difc, .@tp);
break;
case 760:
mapannounce(.@m$, "##1The Monster Army is withdrawing within 30 seconds!", bc_map);
$@MK_SCENE=MK_NONE;
$@MK_AGGRO=$@MK_AGGRO/20;
break;
// Check db/constants.conf to change this value!
case MK_SIEGE_DURATION:
siege_check(.@m$);
siege_revert(.@m$);
siege_revert(.@o$);
enablenpc("Mana Stone");
setd("$@SIEGE_"+.@c$, 0);
break;
}
return;
}
// Utility NPC
- script #SiegeCtrl NPC_HIDDEN,{
end;
OnRespawn:
if (playerattached()) {
getmapxy(.@m$,.@x,.@y,0);
if (rand(10000) <= $coinsrate)
makeitem StrangeCoin, 1, .@m$, .@x, .@y;
}
end;
// Boss Death Labels
OnSergeantDeath:
if ($GAME_STORYLINE == 2)
$MK_TEMPVAR+=1;
getitem StrangeCoin, rand2(1,5);
announce("##2The Monster Sergeant was defeated by "+strcharinfo(0)+"!", bc_all);
$@EXP_EVENT=rand2(1, 3);
$@EXP_EVENT_TIME=1;
donpcevent "@exprate::OnPlayerCall";
specialeffect(FX_FANFARE, AREA, getcharid(3));
end;
OnLieutenantDeath:
if ($GAME_STORYLINE == 2)
$MK_TEMPVAR+=3;
getitem StrangeCoin, rand2(5,10);
announce("##2The Monster Lieutenant was defeated by "+strcharinfo(0)+"!", bc_all);
$@EXP_EVENT=rand2(4, 6);
$@EXP_EVENT_TIME=1;
donpcevent "@exprate::OnPlayerCall";
specialeffect(FX_FANFARE, AREA, getcharid(3));
end;
OnCaptainDeath:
if ($GAME_STORYLINE == 2)
$MK_TEMPVAR+=5;
getitem StrangeCoin, rand2(10,15);
announce("##2The Monster Captain was defeated by "+strcharinfo(0)+"!", bc_all);
$@EXP_EVENT=rand2(7, 9);
$@EXP_EVENT_TIME=1;
donpcevent "@exprate::OnPlayerCall";
specialeffect(FX_FANFARE, AREA, getcharid(3));
end;
OnColonelDeath:
if ($GAME_STORYLINE == 2)
$MK_TEMPVAR+=7;
getitem StrangeCoin, rand2(15,20);
announce("##2The Monster Colonel was defeated by "+strcharinfo(0)+"!", bc_all);
$@EXP_EVENT=rand2(10, 12);
$@EXP_EVENT_TIME=1;
donpcevent "@exprate::OnPlayerCall";
specialeffect(FX_FANFARE, AREA, getcharid(3));
end;
OnGeneralDeath:
if ($GAME_STORYLINE == 2)
$MK_TEMPVAR+=9;
getitem StrangeCoin, rand2(20,25);
announce("##2The Monster General was defeated by "+strcharinfo(0)+"!", bc_all);
$@EXP_EVENT=rand2(13, 15);
$@EXP_EVENT_TIME=1;
donpcevent "@exprate::OnPlayerCall";
specialeffect(FX_FANFARE, AREA, getcharid(3));
end;
}