
//war gamemode logic script

#define SERVER_ONLY

#include "WAR_Structs.as";
#include "RulesCore.as";
#include "RespawnSystem.as";				 
#include "WAR_PopulateSpawnList.as"
#include "WAR_HUDCommon.as";
#include "TradingCommon.as";
#include "Descriptions.as";

const f32 MOOK_SPAWN_DISTANCE = 600.0f;
const f32 DIFFICULTY_INCREASE_DISTANCE = 500.0f;

const int MAX_DIFFICULTY = 15;
const int START_DIFFICULTY = 0;

//simple config function - edit the variables in the config file
void Config(ChallengeCore@ this)
{
	string configstr = "Rules/Challenge/challenge_vars.cfg";
	if (getRules().exists("warconfig")) {
	   configstr = getRules().get_string("warconfig");
	}
	ConfigFile cfg = ConfigFile( configstr );

	//spawn after death time 
    this.spawnTime = sv_test ? 1 : cfg.read_u8("spawn_time", 5);

	this.buddies = cfg.read_s32("buddies", 3);
}


// without "shared" we cannot hot-swap this class :(
// with "shared" we needt o use other function that are "shared" too

shared class ChallengeSpawns : RespawnSystem
{
    ChallengeCore@ war_core;

    WarPlayerInfo@[] spawns;

	s32 nextSpawn;
	bool first;

	void SetCore(RulesCore@ _core)
	{
		RespawnSystem::SetCore(_core);
		@war_core = cast<ChallengeCore@>(core);	
		nextSpawn = 0;
		first = true;
	}

    void Update()
    {
		s32 time = getGameTime();
		if (time % 28 == 0 )
		{
			for (uint i = 0; i < spawns.length; i++)
			{
				updatePlayerSpawnTime(spawns[i]);
			}

			// calculate team sizes

			getRules().set_u8("team 0 count", getTeamSize(war_core.teams, 0));
			getRules().set_u8("team 1 count", getTeamSize(war_core.teams, 1));
		}

		if (time > nextSpawn) //each second
		{
			for (uint i = 0; i < spawns.length; i++)
			{
				WarPlayerInfo@ info = spawns[i];
				if (info.wave_delay > 0)
				{
					info.wave_delay--;
				}
			}
			int delta = -1;
			//we do erases in here, and unfortunately don't
			//have any other way to detect them than just looping until nothing more comes out.
			while(delta != 0)
			{
				uint len = spawns.length;
				for (uint i = 0; i < spawns.length; i++)
				{
					WarPlayerInfo@ info = spawns[i];
					DoSpawnPlayer( info ); //check if we should spawn them
				}
				delta = spawns.length-len;
			}

			nextSpawn += 30;
		}
    }
    
    void updatePlayerSpawnTime(WarPlayerInfo@ w_info)
    {
		WarTeamInfo@ team = cast<WarTeamInfo@>(core.getTeam(w_info.team));
		//sync player time to them directly
		string propname = "time to spawn "+w_info.username;
		s32 time = getSpawnTime( team, w_info );
		war_core.rules.set_s32( propname, time );
		war_core.rules.SyncToPlayer( propname, getPlayerByUsername(w_info.username) );
		propname = "needs respawn hud "+w_info.username;
		war_core.rules.set_bool( propname, (time > s32(getGameTime())) );
		war_core.rules.SyncToPlayer( propname, getPlayerByUsername(w_info.username) );	
	}

    void DoSpawnPlayer( PlayerInfo@ p_info )
    {
        WarPlayerInfo@ w_info = cast<WarPlayerInfo@>(p_info);
							
        if (canSpawnPlayer(p_info) || first)
        {
			p_info.team = 0;

			CPlayer@ player = getPlayerByUsername(p_info.username); // is still connected?
			if (player is null) {
				return;
			}
			RemovePlayerFromSpawn(player);
			first = false;

			Vec2f spawnPos;

			// force team
			p_info.team = 0;
					
			CBlob@ spawnBlob = getSpawnBlobs( p_info ); 
			if (spawnBlob !is null)
			{ 
				spawnPos = spawnBlob.getPosition();						 
			}
			else
			{		
				p_info.blob_name = "knight"; // force knight
				spawnPos = getSpawnLocation(p_info.team);
			}		  

			string propname = "needs respawn hud "+p_info.username;
			war_core.rules.set_bool( propname, false );
			war_core.rules.SyncToPlayer( propname, getPlayerByUsername(p_info.username) );
			p_info.spawnsCount++;

			SpawnPlayerIntoWorld( spawnPos, p_info );
        }
    }

    bool canSpawnPlayer(PlayerInfo@ p_info)
    {
		WarPlayerInfo@ w_info = cast<WarPlayerInfo@>(p_info);
		
		return w_info.wave_delay == 0;
    }

	s32 getSpawnTime( WarTeamInfo@ team, WarPlayerInfo@ w_info  )
	{
		return nextSpawn + w_info.wave_delay*30;
	}

    Vec2f getSpawnLocation(int team)
    {
		CMap@ map = getMap();
		f32 side = map.tilesize * 5.0f;
		f32 x = team == 0 ? side : (map.tilesize*map.tilemapwidth - side);
        f32 y = map.tilesize*map.tilemapheight;
        for (uint i = 0; i < map.tilemapheight; i++)
        {
			y -= map.tilesize;
			if ( !map.isTileSolid(map.getTile(Vec2f(x,y))) 
				&& !map.isTileSolid(map.getTile(Vec2f(x-map.tilesize,y)))
				&& !map.isTileSolid(map.getTile(Vec2f(x+map.tilesize,y)))
				&& !map.isTileSolid(map.getTile(Vec2f(x,y-map.tilesize)))
				&& !map.isTileSolid(map.getTile(Vec2f(x,y-2*map.tilesize)))
				&& !map.isTileSolid(map.getTile(Vec2f(x,y-3*map.tilesize)))
				)
				break;
		}
        y -= 32.0f;
        return Vec2f(x,y);
    }

    void RemovePlayerFromSpawn(CPlayer@ player)
    {
        WarPlayerInfo@ info = cast<WarPlayerInfo@>(core.getInfoFromPlayer(player));
        if (info is null) { warn("CHALLENGE LOGIC: Couldn't get player info ( in void RemovePlayerFromSpawn(CPlayer@ player) )"); return; }

        int pos = spawns.find(info);
        if (pos != -1) {
            spawns.erase(pos);
        }
    }

    void AddPlayerToSpawn( CPlayer@ player )
    {
		RemovePlayerFromSpawn(player);
		if (player.getTeamNum() == core.rules.getSpectatorTeamNum())
			return;
		
		WarPlayerInfo@ info = cast<WarPlayerInfo@>(core.getInfoFromPlayer(player));

        if (info is null) { warn("CHALLENGE LOGIC: Couldn't get player info  ( in void AddPlayerToSpawn(CPlayer@ player) )"); return; }

		//wave delay used as seconds
        info.wave_delay = 1+war_core.spawnTime;
		info.spawnpoint = player.getSpawnPoint();  
		
        spawns.push_back(info);
    }

	bool isSpawning( CPlayer@ player )
	{
		WarPlayerInfo@ info = cast<WarPlayerInfo@>(core.getInfoFromPlayer(player));
		int pos = spawns.find(info);
        return (pos != -1);
	}

	 CBlob@ getSpawnBlobs( PlayerInfo@ p_info, bool takeUnderRaid = false )
	 {
		 CBlob@[] available;
		 WarPlayerInfo@ w_info = cast<WarPlayerInfo@>(p_info);
		 if (w_info.deathDistanceToBase <= 0.0f) // first time
		 {
			 return null;
		 }

		 Vec2f deathPosition = w_info.deathPosition;

		 // get "respawn" bases
		 // only if close to them

		PopulateSpawnList( @available, w_info.team, takeUnderRaid );

		 if (available.length > 0) 
		 {
			 // pick closest to death position
			 while (available.size() > 0)
			 {
				 f32 closestDist = 999999.9f;
				 uint closestIndex = 999;  
				 for (uint i = 0; i < available.length; i++)
				 {
					 CBlob @b = available[i];
					 Vec2f bpos = b.getPosition();
					 const f32 dist = (bpos - w_info.deathPosition).getLength();
					 if (dist < closestDist)
					 {
						 closestDist = dist;
						 closestIndex = i;
					 }
				 } 
				 if (closestIndex >= 999) {
					 break;
				 }    
				 return available[closestIndex];
			 }				  
		 }

		 return null;
	 }

};

shared class ChallengeCore : RulesCore
{
    s32 spawnTime;
	s32 buddies;
	s32 difficulty;
	f32 lastDifficultyIncPos;
	int intervalSpawnerCount;
	f32 playerFarthestDistance;
	int winCounter;

    ChallengeSpawns@ war_spawns;

    ChallengeCore() {}

    ChallengeCore(CRules@ _rules, RespawnSystem@ _respawns )
    {
        super(_rules, _respawns );
    }

    void Setup(CRules@ _rules = null, RespawnSystem@ _respawns = null)
    {
        RulesCore::Setup(_rules, _respawns);
        @war_spawns = cast<ChallengeSpawns@>(_respawns);
        rules.SetCurrentState(GAME);
        server_CreateBlob( "Entities/Meta/WARMusic.cfg" );

		difficulty = START_DIFFICULTY;
		lastDifficultyIncPos = 0.0f;
		intervalSpawnerCount = -1;
		playerFarthestDistance = 150.0f;
		winCounter = -1;
		CMap@ map = getMap();					
		map.SetDayTime( playerFarthestDistance / float(map.tilemapwidth * map.tilesize) );
    }

    void Update()
    {
		const u32 time = getGameTime();
		//HUD
        if (time % 31 == 0)
		{
			updateHUD();

			// update day time - dawn - dusk based on players x position
			CBlob@ playerBlob = getLocalPlayerBlob();
			if (playerBlob !is null)
			{
				if (playerBlob.getPosition().x > playerFarthestDistance)
				{						
					playerFarthestDistance = playerBlob.getPosition().x;					
				}
			}
			CMap@ map = getMap();					
			map.SetDayTime( playerFarthestDistance / float(map.tilemapwidth * map.tilesize) );
		}		 
        if (rules.isGameOver()) { return; }

		if (time % 30 == 0)
		{
			UpdatePlayerCounts();
			UpdateMooks();
		}

		if (winCounter > 0)
		{
			winCounter--;
			if (winCounter == 0)
			{
				Sound::Play("/FanfareWin.ogg");
			}
		}

		RulesCore::Update(); //update respawns
    }
    
    void updateHUD()
    {
		CBitStream serialised_team_hud;
		serialised_team_hud.write_u16(0x54f3);

		WAR_HUD hud;

		WarTeamInfo@[] temp_teams;
		for (uint team_num = 0; team_num < teams.length; ++team_num )
		{
			WarTeamInfo@ team = cast<WarTeamInfo@>(teams[team_num]);

			if (team !is null) {
				temp_teams.push_back(team);
			}
		}
		
		CBlob@[] halls;
		getBlobsByName( "hall", @halls );
		
		hud.Generate(temp_teams, halls);
		
		hud.Serialise(serialised_team_hud);

		rules.set_CBitStream("WAR_serialised_team_hud",serialised_team_hud);
		rules.Sync("WAR_serialised_team_hud",true);
	}

    //team stuff

    void AddTeam(CTeam@ team)
    {
        WarTeamInfo t(teams.length, team.getName());
        teams.push_back(t);
    }

    void AddPlayer(CPlayer@ player, u8 team = 0, string default_config = "")
    {
        WarPlayerInfo p(player.getUsername(), 0, "knight");	// always team 0
        players.push_back(p);
        ChangeTeamPlayerCount( p.team, 1);
    }

	void onPlayerDie(CPlayer@ victim, CPlayer@ killer, u8 customData)
	{
		if (victim !is null )
		{
			CBlob@ blob = victim.getBlob();
			if (blob !is null)
			{
				f32 deathDistanceToBase = Maths::Abs( war_spawns.getSpawnLocation( blob.getTeamNum() ).x - blob.getPosition().x );
				NotifyDeathPosition( victim, blob.getPosition(), deathDistanceToBase );

				int coins = victim.getCoins();
				server_DropCoins( blob.getPosition(), coins*0.5f );
				victim.server_setCoins( coins*0.5f ); // lose half coins

				// check if necromancer nearby - evil laigh

				CBlob@[] blobs;
				if (getBlobsByName("necromancer", @blobs))
				{
					for (uint step = 0; step < blobs.length; ++step)
					{
						Vec2f npos = blobs[step].getPosition();
						if ((npos - blob.getPosition()).getLength() < 400.0f)
						{
							Sound::Play("EvilLaughShort", npos); 
							break;
						}
					}
				}
			}
		}
	}

	void UpdatePlayerCounts()
	{			   
		for (uint i = 0; i < teams.length; i++)
		{
			WarTeamInfo@ team = cast<WarTeamInfo@>( teams[i] );
			//"reset" with migrant count
			team.alive_count = team.migrantCount;
			team.under_raid = false;
		}

		for (uint step = 0; step < players.length; ++step)
		{
			CPlayer@ p = getPlayerByUsername(players[step].username);
			if (p is null) continue;
			CBlob@ player = p.getBlob();
			if (player is null) continue;
			//whew, actually got a blob now..
			if (!player.hasTag("dead"))
			{
				uint teamNum = uint(player.getTeamNum());
				if (teamNum >= 0 && teamNum < teams.length) {
					teams[teamNum].alive_count++;
				}
			}
		}
		
		CBlob@[] rooms;		
		getBlobsByName( "hall", @rooms );   
		for (uint i = 0; i < teams.length; i++)
		{
			WarTeamInfo@ team = cast<WarTeamInfo@>( teams[i] );

			for (uint roomStep=0; roomStep < rooms.length; roomStep++)
			{
				CBlob@ room = rooms[roomStep];
				const u8 teamNum = room.getTeamNum();
				if (teamNum == i)
				{
					if (room.hasTag("under raid")) {
						team.under_raid = true;
					}
				}
			}	
		}
	}
				 
	void NotifyDeathPosition( CPlayer@ player, Vec2f deathPosition, const f32 distance )
	{
		WarPlayerInfo@ info = cast<WarPlayerInfo@>(getInfoFromPlayer(player));
        if (info is null) { return; }
        info.deathDistanceToBase = distance;
		info.deathPosition = deathPosition;
	}

	bool getPlayerBlobs( CBlob@[]@ playerBlobs )
	{
		for (uint step = 0; step < players.length; ++step)
		{
			CPlayer@ p = getPlayerByUsername(players[step].username);
			if (p is null) continue;

			CBlob@ blob = p.getBlob();
			if (blob !is null) {  				
				playerBlobs.push_back( blob );
			}
		}
		return playerBlobs.size() > 0;
	}
		 

	void UpdateMooks()	// run every second
	{
		CBlob@[] playerBlobs;
		if (getPlayerBlobs( playerBlobs ))
		{
			CMap@ map = getMap(); 
			SpawnMooksOnSight( "knight", map, playerBlobs, true );
			SpawnMooksOnSight( "archer", map, playerBlobs, true );

			intervalSpawnerCount++;
			if (intervalSpawnerCount % 5 == 0) {
				IntervalSpawnMooksOnSight( map, playerBlobs );
			}
		}
	}

	void SpawnMooksOnSight( const string &in classname, CMap@ map, CBlob@[]@ playerBlobs, bool removeSpawn )
	{
		Vec2f[] knightsPos;
		if (map.getMarkers("mook "+classname, knightsPos )) 
		{
			for (uint i=0; i < knightsPos.length; i++)
			{
				Vec2f spos = knightsPos[i];
				for (uint pbi=0; pbi < playerBlobs.length; pbi++) 	{
					if ((playerBlobs[pbi].getPosition() - spos).getLength() < MOOK_SPAWN_DISTANCE)
					{
								//printf("Spawning " + classname + " mook");
						if (removeSpawn) {
							map.RemoveMarker("mook "+classname, i );
						}
						SpawnMook( spos, classname );
						return;
					}
				}
			}
		}
	}	

	void IntervalSpawnMooksOnSight( CMap@ map, CBlob@[]@ playerBlobs )
	{
		Vec2f[] knightsPos;
		if (map.getMarkers("mook spawner", knightsPos )) 
		{
			for (uint i=0; i < knightsPos.length; i++)
			{
				Vec2f spos = knightsPos[i];
				for (uint pbi=0; pbi < playerBlobs.length; pbi++) 	
				{
					const f32 dist = (playerBlobs[pbi].getPosition() - spos).getLength();
					if (dist < MOOK_SPAWN_DISTANCE
						&& (dist < MOOK_SPAWN_DISTANCE/2.0f || !map.rayCastSolid( playerBlobs[pbi].getPosition(), spos )))
					{
						map.RemoveMarker( "mook spawner", i );
						SpawnMook( spos, "knight" );
						return;
					}
				}
			}
		}
	}	

	CBlob@ SpawnMook( Vec2f pos, const string &in classname  )
	{
		CBlob@ blob = server_CreateBlobNoInit( classname );
		if (blob !is null) {
			//setup ready for init
			blob.setSexNum(XORRandom(2) == 0 ? XORRandom(2) : 0);
			blob.server_setTeamNum(1);
			blob.setPosition(pos + Vec2f(4.0f,0.0f));				
			blob.set_s32("difficulty", difficulty );
			SetMookHead( blob, classname );			
			blob.Init();						  
			blob.SetFacingLeft(XORRandom(2) == 0);
			blob.getBrain().server_SetActive( true );
			blob.server_SetTimeToDie(60*6);	  // delete after 6 minutes
			GiveAmmo( blob );
		}
		return blob;
	}

	void GiveAmmo( CBlob@ blob )
	{
		if (blob.getName() == "archer")
		{
			CBlob@ mat = server_CreateBlob( "mat_arrows" );
			if (mat !is null) {
				blob.server_PutInInventory(mat);
			}
		}
	}

	void SetMookHead( CBlob@ blob, const string &in classname )
	{
		const bool isKnight = classname == "knight";

		int head = 15;
		int selection = difficulty + XORRandom(3);
		if (selection > 15)
			selection = 15;

		if (isKnight)
		{
			switch (selection)
			{
			case 0:  head = 37; break;
			case 1:  head = 18; break;
			case 2:  head = 19; break;
			case 3:  head = 42; break;
			case 4:  head = 22; break;
			case 5:  head = 23; break;
			case 6:  head = 16; break;
			case 7:  head = 48; break;
			case 8:  head = 46; break;
			case 9:  head = 45; break;
			case 10: head = 47; break;
			case 11: head = 20; break;
			case 12: head = 21; break;
			case 13: head = 44; break;
			case 14: head = 43; break;
			case 15: head = 36; break;
			}
		}
		else
		{
			switch (selection)
			{
			case 0:  head = 35; break;
			case 1:  head = 51; break;
			case 2:  head = 52; break;
			case 3:  head = 26; break;
			case 4:  head = 22; break;
			case 5:  head = 27; break;
			case 6:  head = 24; break;
			case 7:  head = 49; break;
			case 8:  head = 17; break;
			case 9:  head = 17; break;
			case 10: head = 17; break;
			case 11: head = 33; break;
			case 12: head = 32; break;
			case 13: head = 34; break;
			case 14: head = 25; break;
			case 15: head = 36; break;
			}
		}

		blob.setHeadNum( head );
	}
};

//pass stuff to the core from each of the hooks

void onRestart( CRules@ this )
{
    printf("Restarting rules script: " + getCurrentScriptName() );
    ChallengeSpawns spawns();
    ChallengeCore core(this, spawns);
    Config(core);
    this.set("core", @core);

	this.set_bool("singleplayer", true);
}

void onBlobChangeTeam( CRules@ this, CBlob@ blob, const int oldTeam )
{
	if (blob.getName() == "hall" && oldTeam <= 2)
	{
		// check if any halls remain
		int teamHalls = 0;
		CBlob@[] rooms;		
		getBlobsByName( "hall", @rooms );
		for (uint roomStep=0; roomStep < rooms.length; roomStep++)
		{
			CBlob@ room = rooms[roomStep];
			const u8 teamNum = room.getTeamNum();
			if (teamNum == oldTeam)	{
				teamHalls++;
			}
		}	

		RulesCore@ core;
		this.get("core", @core);	 
		if (core !is null) 
		{
			ChallengeCore@ war_core = cast<ChallengeCore@>(core);
			war_core.teams[oldTeam].lost = teamHalls == 0;

			// delete any spawners nearb
			Vec2f hallPos = blob.getPosition();
			DeleteSpawnersAtPos( "mook spawner", hallPos, blob.getRadius()*2.0f );
		}
	}
}

void DeleteSpawnersAtPos( const string &in name, Vec2f pos, const f32 radius )
{
	Vec2f[] knightsPos;
	CMap@ map = getMap();
	if (map.getMarkers( name, knightsPos )) 
	{
		for (uint i=0; i < knightsPos.length; i++)
		{
			Vec2f spos = knightsPos[i];
			if ((pos - spos).getLength() < radius)
			{
				map.RemoveMarker( name, i );
				DeleteSpawnersAtPos( name, pos, radius );
				return;
			}
		}
	}
}

// TRADING

void onBlobCreated( CRules@ this, CBlob@ blob )
{		 
	const string name = blob.getName();
	if (name == "trader") {
		MakeTradeMenu( blob );
	}
	else if (name == "hall")
	{
		blob.RemoveScript("TunnelTravel");
		blob.RemoveScript("PickupIntoStorage");
	}	 
	else if (name == "catapult")
	{
		blob.RemoveScript("DecayInWater");
		blob.RemoveScript("DecayIfLowHealth");
		blob.RemoveScript("DecayIfFlipped");
		blob.RemoveScript("DecayIfLeftAlone");
	}
}

TradeItem@ addItemForCoin( CBlob@ this, const string &in name, int cost, const bool instantShipping, const string &in iconName, const string &in configFilename, const string &in description )
{
	TradeItem@ item = addTradeItem( this, name, 0, instantShipping, iconName, configFilename, description );
	if (item !is null && cost > 0) 
	{
		AddRequirement( item.reqs, "coin", "", "Coins", cost );
		item.buyIntoInventory = true;
	}
	return item;
}

void MakeTradeMenu( CBlob@ trader )
{		
	// build menu
	CreateTradeMenu( trader, Vec2f(4,5), "Buy weapons" );

	//
	addTradeSeparatorItem( trader, "$MENU_GENERIC$", Vec2f(3,1) );
	addTradeEmptyItem( trader );
	addItemForCoin( trader, "Bomb", 10, true, "$mat_bombs$", "mat_bombs", descriptions[1] );
	addItemForCoin( trader, "Water Bomb", 10, true, "$mat_waterbombs$", "mat_waterbombs", descriptions[50] );
	addItemForCoin( trader, "Satchel", 10, true, "$satchel$", "satchel", descriptions[21] );	 
	addItemForCoin( trader, "Keg", 50, true, "$keg$", "keg", descriptions[19] );	 
	addItemForCoin( trader, "Arrows", 5, true, "$mat_arrows$", "mat_arrows", descriptions[2] );	 
	addItemForCoin( trader, "Water Arrows", 10, true, "$mat_waterarrows$", "mat_waterarrows", descriptions[50] );	 
	addItemForCoin( trader, "Fire Arrows", 15, true, "$mat_firearrows$", "mat_firearrows", descriptions[32] );	 
	addItemForCoin( trader, "Bomb Arrow", 10, true, "$mat_bombarrows$", "mat_bombarrows", descriptions[51] );	 
	addItemForCoin( trader, "Mine", 25, true, "$mine$", "mine", descriptions[20] );	 
	addItemForCoin( trader, "Mounted Bow", 80, true, "$mounted_bow$", "mounted_bow", descriptions[31] );	 
	addItemForCoin( trader, "Drill", 30, true, "$drill$", "drill", descriptions[43] );	 
	addItemForCoin( trader, "Boulder", 5, true, "$boulder$", "boulder", descriptions[17] );	 
	addItemForCoin( trader, "Bread", 10, true, "$food$", "food", "Just food for healing. Don't think about this too much." );	 	
	//addItemForCoin( trader, "Balloon Bomber", 100, true, "$bomber$", "bomber", "An airship." );	
	//addItemForCoin( trader, "Catapult", 80, true, "$catapult$", "catapult", descriptions[5] );	 
	//addItemForCoin( trader, "Ballista", 80, true, "$ballista$", "ballista", descriptions[6] );	 
}

// add coins for red dead

// f32 onPlayerTakeDamage( CRules@ this, CPlayer@ victim, CPlayer@ attacker, f32 DamageScale )

void onBlobDie( CRules@ this, CBlob@ blob )
{				
	const string name = blob.getName();
	if (blob.getTeamNum() == 1 && (name == "archer" || name == "knight") && !blob.hasTag("dropped coins"))
	{
		RulesCore@ core;
		this.get("core", @core);	 
		if (core !is null) 
		{
			ChallengeCore@ ccore = cast<ChallengeCore@>(core);
			Vec2f pos = blob.getPosition();

			// drop coins

			server_DropCoins( pos, XORRandom(5)*((ccore.difficulty+1)/2.0f) );
			blob.Tag("dropped coins");

			// increase difficulty at each death

			if (pos.x - ccore.lastDifficultyIncPos > DIFFICULTY_INCREASE_DISTANCE)
			{
				ccore.difficulty++;
				if (ccore.difficulty > MAX_DIFFICULTY) {
					ccore.difficulty = MAX_DIFFICULTY;
				}

				ccore.lastDifficultyIncPos = pos.x;
				//if (g_debug > 0) 
				//{
				//	printf("debug: DIFFICULTY " + ccore.difficulty );
				//}
			}

			// make sure interval spawner spawns next second	
			ccore.intervalSpawnerCount = -1;
		}
	}

	// open all doors
	if (name == "necromancer")
	{ 
		CBlob@[] doors;
		if (getBlobsByTag( "door", @doors ))
		{
			for (uint i = 0; i < doors.length; i++)
			{
				CBlob@ door = doors[i];
				if (door.getTeamNum() != 0)
				{
					door.getSprite().Gib();
					door.server_Die();
				}
			}
		}

		CBlob@ playerBlob = getLocalPlayerBlob();
		if (playerBlob !is null)
		{				
			Sound::Play("/EvilLaugh.ogg", (playerBlob.getPosition() + blob.getPosition())/2.0f);
		}
		else {
			Sound::Play("/EvilLaugh.ogg");
		}

		RulesCore@ core;
		this.get("core", @core);	 
		if (core !is null) 
		{
			ChallengeCore@ ccore = cast<ChallengeCore@>(core);
			ccore.winCounter = 90;
		}
	}
}
