Downloads containing npcBunniesQuests.asc

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: NpcBunnies Cranky Mutator N/A Download file

File preview

namespace npcBunniesQuests
{
enum QuestType
{
	Coins = 0,
	Gems = 1,
	DefeatMonster = 2,
	DefeatBoss = 3
}

class NpcQuest
{
	int questType = QuestType::Coins;
	int amount = 0;
	GEM::Color gemType = GEM::RED;
	int objectType = 0;
	bool countAfterQuestIsStarted = true;
	bool completeQuestTogether = false;
	bool respawn = false;
	int goToSet = -1;
	bool iconSpecified = false;
	bool iconHidden = false;
	npcBunniesAnimations::NpcUiAnimation icon;
}

enum RewardType
{
	RewardCoins = 0,
	RewardGems = 1,
	RewardAmmo = 2,
	RewardPowerup = 3,
	RewardTeleport = 4,
	RewardMoveable = 5,
	RewardTrigger = 6,
	RewardFastFire = 7,
	RewardGuidePath = 8
}

class NpcReward
{
	int rewardType = RewardType::RewardCoins;
	int amount = 0;
	GEM::Color gemType = GEM::RED;
	int weaponType = WEAPON::BOUNCER;
	float x = 0;
	float y = 0;
	bool moveable = true;
	int npcIndex = -1;
	int guidePathIndex = -1;
	int triggerId = -1;
	bool triggerState = true;
	bool iconSpecified = false;
	bool iconHidden = false;
	npcBunniesAnimations::NpcUiAnimation icon;
}


enum DefeatMonsterEvent
{
	MonsterUnknown = 0,

	Bat = OBJECT::BAT,
	BeeBoy = OBJECT::BEEBOY,
	Bees = OBJECT::BEES,
	Bee = OBJECT::BEE,
	Butterfly = OBJECT::BUTTERFLY,
	Crab = OBJECT::CRAB,
	Demon = OBJECT::DEMON,
	DoggyDogg = OBJECT::DOGGYDOGG,
	Dragonfly = OBJECT::DRAGONFLY,
	Dragon = OBJECT::DRAGON,
	FatChick = OBJECT::FATCHICK,
	Fencer = OBJECT::FENCER,
	Fish = OBJECT::FISH,
	Hatter = OBJECT::HATTER,
	Helmut = OBJECT::HELMUT,
	LabRat = OBJECT::LABRAT,
	FloatLizard = OBJECT::FLOATLIZARD,
	XmasLizard = OBJECT::XMASLIZARD,
	Lizard = OBJECT::LIZARD,
	StandMonkey = OBJECT::STANDMONKEY,
	Monkey = OBJECT::MONKEY,
	XmasNormalTurtle = OBJECT::XMASNORMTURTLE,
	TubeTurtle = OBJECT::TUBETURTLE,
	TufTurtle = OBJECT::TUFTURT,
	NormalTurtle = OBJECT::NORMTURTLE,
	Rapier = OBJECT::RAPIER,
	Raven = OBJECT::RAVEN,
	Skeleton = OBJECT::SKELETON,
	Spark = OBJECT::SPARK,
	FloatSucker = OBJECT::FLOATSUCKER,
	Sucker = OBJECT::SUCKER,
	Cat = OBJECT::CAT,
	PacmanGhost = OBJECT::PACMANGHOST,
	Moth = OBJECT::MOTH,
	Witch = OBJECT::WITCH
}

enum DefeatBossEvent
{
	BossUnknown = 0,

	XmasBilsy = OBJECT::XMASBILSY,
	Bilsy = OBJECT::BILSY,
	Bolly = OBJECT::BOLLY,
	Bubba = OBJECT::BUBBA,
	Devan = OBJECT::DEVANROBOT,
	DevilDevan = OBJECT::DEVILDEVAN,
	Queen = OBJECT::QUEEN,
	Robot = OBJECT::ROBOT,
	RocketTurtle = OBJECT::ROCKETTURTLE,
	TufBoss = OBJECT::TUFBOSS,
	Tweedle = OBJECT::TWEEDLEBOSS,
	Uterus = OBJECT::UTERUS
}

dictionary defeatedObjectCounts;
dictionary trackedDefeatEventIds;
dictionary questStartDefeatedObjectCounts;
dictionary activeQuestDefeatProgress;
dictionary unlockedGuidePaths;
array<string> defeatedObjectPlayerNames;
array<bool> trackedDefeatObjectWasActive;
array<int> trackedDefeatObjectEventID;
array<string> trackedDefeatObjectLastHitPlayer;
array<bool> trackedDefeatObjectCounted;
int lastTrackedDefeatObjectScanTick = -9999;
bool trackedDefeatNeedsGlobalScan = false;
bool forceTrackedDefeatObjectScan = false;

string MakeActiveQuestDefeatProgressKey(const string &in playerName, int npcId, int setIndex, int eventID)
{
	return playerName + "|" + npcId + "|" + setIndex + "|" + eventID;
}

int GetActiveQuestDefeatProgressForName(const string &in playerName, int npcId, int setIndex, int eventID)
{
	int count = 0;
	activeQuestDefeatProgress.get(MakeActiveQuestDefeatProgressKey(playerName, npcId, setIndex, eventID), count);
	return count;
}

void SetActiveQuestDefeatProgressForName(const string &in playerName, int npcId, int setIndex, int eventID, int count)
{
	if(playerName == "" || npcId < 0 || setIndex < 0 || eventID <= 0) return;
	activeQuestDefeatProgress.set(MakeActiveQuestDefeatProgressKey(playerName, npcId, setIndex, eventID), count);
}

string MakeUnlockedGuidePathKey(const string &in playerName, int npcId, int pathIndex)
{
	return playerName + "|" + npcId + "|" + pathIndex;
}

bool IsGuidePathUnlocked(const string &in playerName, int npcId, int pathIndex)
{
	if(playerName == "" || npcId < 0 || pathIndex < 0) return false;
	bool unlocked = false;
	return unlockedGuidePaths.get(MakeUnlockedGuidePathKey(playerName, npcId, pathIndex), unlocked) && unlocked;
}

void UnlockGuidePathForPlayer(const string &in playerName, int npcId, int pathIndex)
{
	if(playerName == "" || npcId < 0 || pathIndex < 0) return;
	unlockedGuidePaths.set(MakeUnlockedGuidePathKey(playerName, npcId, pathIndex), true);
}

bool FindGuidePathReward(npcBunniesCore::npcBunny &in npc, int pathIndex, int &out setIndex, int &out rewardIndex)
{
	setIndex = -1;
	rewardIndex = -1;
	for(uint s = 0; s < npc.dialogueSets.length; s++)
	{
		npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[s];
		for(uint r = 0; r < set.rewards.length; r++)
		{
			NpcReward@ reward = set.rewards[r];
			if(reward !is null && reward.rewardType == RewardType::RewardGuidePath && reward.guidePathIndex == pathIndex)
			{
				setIndex = int(s);
				rewardIndex = int(r);
				return true;
			}
		}
	}
	return false;
}

bool IsGuidePathRewardLockedForPlayer(npcBunniesCore::npcBunny &in npc, const string &in playerName, int pathIndex)
{
	int setIndex, rewardIndex;
	if(!FindGuidePathReward(npc, pathIndex, setIndex, rewardIndex)) return false;
	return !IsGuidePathUnlocked(playerName, npc.id, pathIndex);
}

int GetAvailableGuidePathCountForPlayer(npcBunniesCore::npcBunny &in npc, const string &in playerName)
{
	for(uint i = 0; i < npc.guidePaths.length; i++)
		if(IsGuidePathRewardLockedForPlayer(npc, playerName, int(i))) return int(i);
	return int(npc.guidePaths.length);
}

void IncrementActiveQuestDefeatProgressForName(const string &in playerName, int eventID)
{
	if(playerName == "" || eventID <= 0) return;
	for(uint npcId = 0; npcId < npcBunniesCore::npcBunnies.length; npcId++)
	{
		npcBunniesCore::npcBunny npc = npcBunniesCore::EnsureNpcQuestState(npcBunniesCore::npcBunnies[npcId]);
		for(uint setIndex = 0; setIndex < npc.dialogueSets.length; setIndex++)
		{
			if(!npcBunniesCore::HasPlayerSeenQuestOffer(playerName, int(npcId), int(setIndex))) continue;
			npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[setIndex];
			if(set.choicesAnswerMethod != npcBunniesCore::DialogueAction::Quest) continue;
			for(uint q = 0; q < set.quests.length; q++)
			{
				NpcQuest@ quest = set.quests[q];
				if(quest is null || (quest.questType != QuestType::DefeatMonster && quest.questType != QuestType::DefeatBoss) || quest.objectType != eventID) continue;
				int count = GetActiveQuestDefeatProgressForName(playerName, int(npcId), int(setIndex), eventID) + 1;
				SetActiveQuestDefeatProgressForName(playerName, int(npcId), int(setIndex), eventID, count);
			}
		}
		npcBunniesCore::npcBunnies[npcId] = npc;
	}
}
string MakeDefeatedObjectKey(const string &in playerName, int eventID)
{
	return playerName + "|" + eventID;
}

string MakeQuestStartDefeatedObjectKey(const string &in playerName, int npcId, int setIndex, int eventID)
{
	return playerName + "|" + npcId + "|" + setIndex + "|" + eventID;
}

void AddUniqueDefeatedObjectPlayerName(const string &in playerName)
{
	if(playerName == "") return;
	for(uint i = 0; i < defeatedObjectPlayerNames.length; i++) if(defeatedObjectPlayerNames[i] == playerName) return;
	defeatedObjectPlayerNames.insertLast(playerName);
}

void TrackDefeatEventID(int eventID, bool needsGlobalScan = false)
{
	if(eventID > 0) trackedDefeatEventIds.set(eventID + "", true);
	if(needsGlobalScan) trackedDefeatNeedsGlobalScan = true;
}
bool IsTrackedDefeatEventID(int eventID)
{
	if(eventID <= 0) return false;
	bool tracked = false;
	return trackedDefeatEventIds.get(eventID + "", tracked) && tracked;
}

int GetDefeatedObjectCountForName(const string &in playerName, int eventID)
{
	int count = 0;
	defeatedObjectCounts.get(MakeDefeatedObjectKey(playerName, eventID), count);
	return count;
}

int GetDefeatedObjectCount(jjPLAYER@ player, int eventID)
{
	if(player is null) return 0;
	return GetDefeatedObjectCountForName(player.nameUnformatted, eventID);
}

int GetDefeatedObjectCountTogether(int eventID)
{
	int count = 0;
	for(uint i = 0; i < defeatedObjectPlayerNames.length; i++) count += GetDefeatedObjectCountForName(defeatedObjectPlayerNames[i], eventID);
	return count;
}

int GetQuestDefeatedObjectBaseline(jjPLAYER@ player, int npcId, int setIndex, int eventID, bool completeTogether)
{
	if(player is null || npcId < 0 || setIndex < 0) return 0;
	int count = 0;
	questStartDefeatedObjectCounts.get(MakeQuestStartDefeatedObjectKey(player.nameUnformatted, npcId, setIndex, eventID), count);
	return count;
}

int GetQuestDefeatedObjectProgress(NpcQuest@ quest, jjPLAYER@ player, int npcId, int setIndex)
{
	if(!npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) return 0;
	if(quest is null || player is null) return 0;
	if(quest.countAfterQuestIsStarted)
	{
		if(npcId < 0 || setIndex < 0 || !npcBunniesCore::HasPlayerSeenQuestOffer(player.nameUnformatted, npcId, setIndex)) return 0;
		return GetActiveQuestDefeatProgressForName(player.nameUnformatted, npcId, setIndex, quest.objectType);
	}
	int count = quest.completeQuestTogether ? GetDefeatedObjectCountTogether(quest.objectType) : GetDefeatedObjectCount(player, quest.objectType);
	return count > 0 ? count : 0;
}

bool HasActiveObjectAtTile(int eventID, int xTile, int yTile)
{
	if(!npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) return false;
	for(int i = 1; i < jjObjectCount; i++)
	{
		jjOBJ@ obj = jjObjects[i];
		if(obj.isActive && int(obj.eventID) == eventID && int(obj.xPos) / 32 == xTile && int(obj.yPos) / 32 == yTile) return true;
	}
	return false;
}

void RespawnQuestObjects(int eventID)
{
	if(!npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) return;
	if(eventID <= 0) return;
	for(int x = 0; x < jjLayerWidth[4]; x++)
	{
		for(int y = 0; y < jjLayerHeight[4]; y++)
		{
			if(int(jjEventGet(x, y)) == eventID && !HasActiveObjectAtTile(eventID, x, y)) jjAddObject(eventID, x * 32, y * 32);
		}
	}
}


bool RespawnNpcFightingTargetAt(int eventID, int xTile, int yTile)
{
	if(!npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) return false;
	if(eventID <= 0 || !npcBunniesFighting::IsNpcFightingTargetEventID(eventID)) return false;
	if(HasActiveObjectAtTile(eventID, xTile, yTile)) return false;
	jjAddObject(eventID, xTile * 32, yTile * 32);
	return true;
}

void SendNpcRespawnMonsterToClients(int eventID, int xTile, int yTile)
{
	if(!jjIsServer || !npcBunniesCore::ENABLE_NPC_SYNC) return;
	jjSTREAM packet;
	packet.push("npcRespawnMonsterAt " + eventID + " " + xTile + " " + yTile);
	jjSendPacket(packet);
}

int RespawnNpcFightingTargets(bool syncClients = false)
{
	if(!npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) return 0;
	int respawned = 0;
	for(int x = 0; x < jjLayerWidth[4]; x++)
	{
		for(int y = 0; y < jjLayerHeight[4]; y++)
		{
			int eventID = int(jjEventGet(x, y));
			if(eventID <= 0 || !npcBunniesFighting::IsNpcFightingTargetEventID(eventID)) continue;
			if(RespawnNpcFightingTargetAt(eventID, x, y)) respawned++;
			if(syncClients) SendNpcRespawnMonsterToClients(eventID, x, y);
		}
	}
	return respawned;
}
void RequestRespawnNpcFightingTargets()
{
	if(!npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) return;
	if(!npcBunniesCore::ENABLE_NPC_SYNC) { RespawnNpcFightingTargets(); return; }
	jjSTREAM packet;
	packet.push("npcRespawnMonsters");
	jjSendPacket(packet);
}
void StartQuestForPlayer(int npcId, int setIndex, jjPLAYER@ player)
{
	if(!npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) return;
	if(player is null || npcId < 0 || npcId >= int(npcBunniesCore::npcBunnies.length)) return;
	npcBunniesCore::npcBunny npc = npcBunniesCore::EnsureNpcQuestState(npcBunniesCore::npcBunnies[npcId]);
	if(setIndex < 0 || setIndex >= int(npc.dialogueSets.length)) return;
	npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[setIndex];
	for(uint i = 0; i < set.quests.length; i++)
	{
		NpcQuest@ quest = set.quests[i];
		if(quest is null || (quest.questType != QuestType::DefeatMonster && quest.questType != QuestType::DefeatBoss)) continue;
		if(set.respawnMonsters || quest.respawn) RespawnQuestObjects(quest.objectType);
		if(quest.countAfterQuestIsStarted) { forceTrackedDefeatObjectScan = true; lastTrackedDefeatObjectScanTick = -9999; }
		if(quest.countAfterQuestIsStarted)
		{
			int count = quest.completeQuestTogether ? GetDefeatedObjectCountTogether(quest.objectType) : GetDefeatedObjectCount(player, quest.objectType);
			questStartDefeatedObjectCounts.set(MakeQuestStartDefeatedObjectKey(player.nameUnformatted, npcId, setIndex, quest.objectType), count);
		}
		SetActiveQuestDefeatProgressForName(player.nameUnformatted, npcId, setIndex, quest.objectType, 0);
	}
}

void EnsureTrackedDefeatObjectArrays()
{
	int length = jjObjectCount + 1;
	if(length < 1) length = 1;
	if(int(trackedDefeatObjectWasActive.length) >= length) return;
	uint oldLength = trackedDefeatObjectWasActive.length;
	trackedDefeatObjectWasActive.resize(length);
	trackedDefeatObjectEventID.resize(length);
	trackedDefeatObjectLastHitPlayer.resize(length);
	trackedDefeatObjectCounted.resize(length);
	for(uint i = oldLength; i < trackedDefeatObjectWasActive.length; i++)
	{
		trackedDefeatObjectWasActive[i] = false;
		trackedDefeatObjectEventID[i] = 0;
		trackedDefeatObjectLastHitPlayer[i] = "";
		trackedDefeatObjectCounted[i] = false;
	}
}

void IncrementDefeatedObjectCountForName(const string &in playerName, int eventID)
{
	if(playerName == "" || eventID <= 0) return;
	int count = GetDefeatedObjectCountForName(playerName, eventID) + 1;
	defeatedObjectCounts.set(MakeDefeatedObjectKey(playerName, eventID), count);
	AddUniqueDefeatedObjectPlayerName(playerName);
}

void IncrementDefeatForActiveQuestPlayers(int eventID)
{
	if(!npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) return;
	if(eventID <= 0) return;
	for(uint npcId = 0; npcId < npcBunniesCore::npcBunnies.length; npcId++)
	{
		npcBunniesCore::npcBunny npc = npcBunniesCore::EnsureNpcQuestState(npcBunniesCore::npcBunnies[npcId]);
		for(int playerId = 0; playerId < 32; playerId++)
		{
			int setIndex = npc.activeQuestSet[playerId];
			if(setIndex < 0 || setIndex >= int(npc.dialogueSets.length) || !jjPlayers[playerId].isInGame) continue;
			npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[setIndex];
			bool needsEvent = false;
			for(uint q = 0; q < set.quests.length; q++)
			{
				NpcQuest@ quest = set.quests[q];
				if(quest !is null && (quest.questType == QuestType::DefeatMonster || quest.questType == QuestType::DefeatBoss) && quest.objectType == eventID) needsEvent = true;
			}
			if(needsEvent)
			{
				string playerName = jjPlayers[playerId].nameUnformatted;
				int count = GetActiveQuestDefeatProgressForName(playerName, int(npcId), setIndex, eventID) + 1;
				SetActiveQuestDefeatProgressForName(playerName, int(npcId), setIndex, eventID, count);
			}
		}
		npcBunniesCore::npcBunnies[npcId] = npc;
	}
}
bool HasActiveCountAfterStartDefeatQuest()
{
	for(uint npcId = 0; npcId < npcBunniesCore::npcBunnies.length; npcId++)
	{
		npcBunniesCore::npcBunny npc = npcBunniesCore::EnsureNpcQuestState(npcBunniesCore::npcBunnies[npcId]);
		for(int playerId = 0; playerId < 32; playerId++)
		{
			if(!jjPlayers[playerId].isInGame || playerId >= int(npc.activeQuestSet.length)) continue;
			int setIndex = npc.activeQuestSet[playerId];
			if(setIndex < 0 || setIndex >= int(npc.dialogueSets.length)) continue;
			npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[setIndex];
			for(uint q = 0; q < set.quests.length; q++)
			{
				NpcQuest@ quest = set.quests[q];
				if(quest !is null && quest.countAfterQuestIsStarted && (quest.questType == QuestType::DefeatMonster || quest.questType == QuestType::DefeatBoss)) return true;
			}
		}
	}
	return false;
}
void UpdateTrackedDefeatObjects()
{
	if(!npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) return;
	if(trackedDefeatEventIds.getSize() == 0) return;
	if(lastTrackedDefeatObjectScanTick + npcBunniesCore::NPC_DEFEAT_TRACK_SCAN_INTERVAL > jjGameTicks) return;
	lastTrackedDefeatObjectScanTick = jjGameTicks;
	bool forceScan = forceTrackedDefeatObjectScan;
	forceTrackedDefeatObjectScan = false;
	if(!trackedDefeatNeedsGlobalScan && !forceScan && !HasActiveCountAfterStartDefeatQuest()) return;
	EnsureTrackedDefeatObjectArrays();
	for(int i = 1; i < int(trackedDefeatObjectWasActive.length); i++)
	{
		bool activeNow = i < jjObjectCount && jjObjects[i].isActive && IsTrackedDefeatEventID(jjObjects[i].eventID);
		if(activeNow)
		{
			if(!trackedDefeatObjectWasActive[i] || trackedDefeatObjectEventID[i] != int(jjObjects[i].eventID))
			{
				trackedDefeatObjectLastHitPlayer[i] = "";
				trackedDefeatObjectCounted[i] = false;
			}
			trackedDefeatObjectWasActive[i] = true;
			trackedDefeatObjectEventID[i] = jjObjects[i].eventID;
		}
		else if(trackedDefeatObjectWasActive[i])
		{
			if(!trackedDefeatObjectCounted[i])
			{
				if(trackedDefeatObjectLastHitPlayer[i] != "") { IncrementDefeatedObjectCountForName(trackedDefeatObjectLastHitPlayer[i], trackedDefeatObjectEventID[i]); IncrementActiveQuestDefeatProgressForName(trackedDefeatObjectLastHitPlayer[i], trackedDefeatObjectEventID[i]); }
				else IncrementDefeatForActiveQuestPlayers(trackedDefeatObjectEventID[i]);
			}
			trackedDefeatObjectWasActive[i] = false;
			trackedDefeatObjectEventID[i] = 0;
			trackedDefeatObjectLastHitPlayer[i] = "";
			trackedDefeatObjectCounted[i] = false;
		}
	}
}

bool AreNpcQuestsComplete(npcBunniesDialogue::DialogueSet@ set, jjPLAYER@ player, int npcId = -1, int setIndex = -1)
{
	if(set is null) return true;
	for(uint i = 0; i < set.quests.length; i++)
	{
		if(!IsQuestComplete(set.quests[i], player, npcId, setIndex)) return false;
	}
	return true;
}

bool IsQuestComplete(NpcQuest@ quest, jjPLAYER@ player, int npcId = -1, int setIndex = -1)
{
	if(quest is null) return true;
	if(player is null) return false;
	if(quest.amount <= 0) return true;
	if(quest.questType == QuestType::Coins) return player.coins >= quest.amount;
	if(quest.questType == QuestType::Gems) return player.gems[quest.gemType] >= quest.amount;
	if(quest.questType == QuestType::DefeatMonster || quest.questType == QuestType::DefeatBoss)
	{
		if(!npcBunniesCore::ENABLE_NPC_MONSTER_QUESTS) return false;
		if(quest.objectType <= 0) return false;
		return GetQuestDefeatedObjectProgress(quest, player, npcId, setIndex) >= quest.amount;
	}
	return false;
}

void SpendQuestCosts(npcBunniesDialogue::DialogueSet@ set, jjPLAYER@ player)
{
	if(set is null) return;
	for(uint i = 0; i < set.quests.length; i++) SpendQuestCost(set.quests[i], player);
}

void SpendQuestCost(NpcQuest@ quest, jjPLAYER@ player)
{
	if(quest is null || quest.amount <= 0) return;
	if(quest.questType == QuestType::Coins)
		player.coins = player.coins >= quest.amount ? player.coins - quest.amount : 0;
	else if(quest.questType == QuestType::Gems)
		player.gems[quest.gemType] = player.gems[quest.gemType] >= quest.amount ? player.gems[quest.gemType] - quest.amount : 0;
}

int ParseDefeatEventID(const string &in value)
{
	int eventID = parseInt(value);
	if (value == "0" || eventID != 0)
		return eventID;

	// =========================
	// Bosses
	// =========================
	if (jjRegexMatch(value, "Bilsy", true))
		return OBJECT::BILSY;

	if (jjRegexMatch(value, "XmasBilsy", true) || jjRegexMatch(value, "ChristmasBilsy", true))
		return OBJECT::XMASBILSY;

	if (jjRegexMatch(value, "Bolly", true))
		return OBJECT::BOLLY;

	if (jjRegexMatch(value, "Bubba", true))
		return OBJECT::BUBBA;

	if (jjRegexMatch(value, "DevilDevan", true))
		return OBJECT::DEVILDEVAN;

	if (jjRegexMatch(value, "Queen", true))
		return OBJECT::QUEEN;

	if (jjRegexMatch(value, "Robot", true))
		return OBJECT::ROBOT;

	if (jjRegexMatch(value, "RocketTurtle", true))
		return OBJECT::ROCKETTURTLE;

	if (jjRegexMatch(value, "SchwarzenGuard", true) || jjRegexMatch(value, "TurtleBoss", true) || jjRegexMatch(value, "TufBoss", true))
		return OBJECT::TUFBOSS;

	if (jjRegexMatch(value, "Tweedle", true))
		return OBJECT::TWEEDLEBOSS;

	if (jjRegexMatch(value, "Uterus", true))
		return OBJECT::UTERUS;

	// =========================
	// Enemies / Monsters
	// =========================

	if (jjRegexMatch(value, "Bat", true))
		return OBJECT::BAT;

	if (jjRegexMatch(value, "Bee", true))
		return OBJECT::BEE;

	if (jjRegexMatch(value, "BeeBoy", true))
		return OBJECT::BEEBOY;

	if (jjRegexMatch(value, "Bees", true))
		return OBJECT::BEES;

	if (jjRegexMatch(value, "Butterfly", true) || jjRegexMatch(value, "EvilButterfly", true))
		return OBJECT::BUTTERFLY;

	if (jjRegexMatch(value, "Crab", true))
		return OBJECT::CRAB;

	if (jjRegexMatch(value, "Demon", true))
		return OBJECT::DEMON;

	if (jjRegexMatch(value, "Dog", true) || jjRegexMatch(value, "Doggy", true))
		return OBJECT::DOGGYDOGG;

	if (jjRegexMatch(value, "Dragon", true))
		return OBJECT::DRAGON;

	if (jjRegexMatch(value, "Dragonfly", true))
		return OBJECT::DRAGONFLY;

	if (jjRegexMatch(value, "FatChick", true))
		return OBJECT::FATCHICK;

	if (jjRegexMatch(value, "Fencer", true))
		return OBJECT::FENCER;

	if (jjRegexMatch(value, "Fish", true))
		return OBJECT::FISH;

	if (jjRegexMatch(value, "Hatter", true))
		return OBJECT::HATTER;

	if (jjRegexMatch(value, "Helmut", true))
		return OBJECT::HELMUT;

	if (jjRegexMatch(value, "LabRat", true) || jjRegexMatch(value, "Rat", true))
		return OBJECT::LABRAT;

	if (jjRegexMatch(value, "Lizard", true))
		return OBJECT::LIZARD;

	if (jjRegexMatch(value, "FloatLizard", true))
		return OBJECT::FLOATLIZARD;

	if (jjRegexMatch(value, "XmasLizard", true))
		return OBJECT::XMASLIZARD;

	if (jjRegexMatch(value, "Monkey", true))
		return OBJECT::MONKEY;

	if (jjRegexMatch(value, "StandMonkey", true))
		return OBJECT::STANDMONKEY;

	if (jjRegexMatch(value, "XmasTurtle", true) || jjRegexMatch(value, "XmasNormalTurtle", true)
	 || jjRegexMatch(value, "ChristmasTurtle", true) || jjRegexMatch(value, "ChristmasNormalTurtle", true))
		return OBJECT::XMASNORMTURTLE;

	if (jjRegexMatch(value, "TubeTurtle", true))
		return OBJECT::TUBETURTLE;

	if (jjRegexMatch(value, "TufTurtle", true))
		return OBJECT::TUFTURT;

	if (jjRegexMatch(value, "Turtle", true) || jjRegexMatch(value, "NormalTurtle", true))
		return OBJECT::NORMTURTLE;

	if (jjRegexMatch(value, "Rapier", true) || jjRegexMatch(value, "Ghost", true))
		return OBJECT::RAPIER;

	if (jjRegexMatch(value, "Raven", true))
		return OBJECT::RAVEN;

	if (jjRegexMatch(value, "Skeleton", true))
		return OBJECT::SKELETON;

	if (jjRegexMatch(value, "Spark", true))
		return OBJECT::SPARK;

	if (jjRegexMatch(value, "Sucker", true))
		return OBJECT::SUCKER;

	if (jjRegexMatch(value, "FloatSucker", true))
		return OBJECT::FLOATSUCKER;

	if (jjRegexMatch(value, "Cat", true))
		return OBJECT::CAT;

	if (jjRegexMatch(value, "PacmanGhost", true))
		return OBJECT::PACMANGHOST;

	if (jjRegexMatch(value, "Moth", true))
		return OBJECT::MOTH;

	if (jjRegexMatch(value, "Witch", true))
		return OBJECT::WITCH;

	return 0;
}

int ParseQuestType(const string &in value)
{
	if(jjRegexMatch(value, "Gem", true) || jjRegexMatch(value, "Gems", true)) return QuestType::Gems;
	if(jjRegexMatch(value, "DefeatBoss", true) || jjRegexMatch(value, "Boss", true)) return QuestType::DefeatBoss;
	if(jjRegexMatch(value, "DefeatMonster", true) || jjRegexMatch(value, "Monster", true)) return QuestType::DefeatMonster;
	return QuestType::Coins;
}

//----------------------------------------------------------------------------------------------------
// NPC REWARD REGION
// Summary: Rewards are applied after all quests in a dialogue set pass. Currency rewards add to the player,
// teleport changes player position, trigger rewards set jjTriggers, and moveable rewards update an NPC flag.

void GrantNpcRewards(npcBunniesDialogue::DialogueSet@ set, jjPLAYER@ player, int npcId)
{
	if(set is null) return;
	SpendQuestCosts(set, player);
	for(uint i = 0; i < set.rewards.length; i++) GrantNpcReward(set.rewards[i], player, npcId);
	set.rewardsGranted = true;
}

void GrantNpcAmmoReward(jjPLAYER@ player, int weaponType, int amount)
{
	if(player is null || amount <= 0 || weaponType <= 0 || weaponType >= 10) return;
	int targetAmmo = player.ammo[weaponType] + amount;
	int originalMaximum = jjWeapons[weaponType].maximum;
	if(targetAmmo > jjWeapons[weaponType].maximum) jjWeapons[weaponType].maximum = targetAmmo;
	player.ammo[weaponType] = targetAmmo;
	jjWeapons[weaponType].maximum = originalMaximum;
}

void GrantNpcReward(NpcReward@ reward, jjPLAYER@ player, int npcId)
{
	if(reward is null) return;
	if(reward.rewardType == RewardType::RewardCoins) player.coins += reward.amount;
	else if(reward.rewardType == RewardType::RewardGems) player.gems[reward.gemType] = player.gems[reward.gemType] + reward.amount;
	else if(reward.rewardType == RewardType::RewardAmmo) GrantNpcAmmoReward(player, reward.weaponType, reward.amount);
	else if(reward.rewardType == RewardType::RewardPowerup) player.powerup[reward.weaponType] = true;
	else if(reward.rewardType == RewardType::RewardTeleport)
	{
		player.xPos = reward.x * 32;
		player.yPos = reward.y * 32;
	}
	else if(reward.rewardType == RewardType::RewardTrigger)
	{
		if(0 <= reward.triggerId && reward.triggerId < 32) jjTriggers[reward.triggerId] = reward.triggerState;
	}
	else if(reward.rewardType == RewardType::RewardMoveable)
	{
		int target = reward.npcIndex >= 0 ? reward.npcIndex : npcId;
		if(0 <= target && target < int(npcBunniesCore::npcBunnies.length)) npcBunniesCore::npcBunnies[target].moveable = reward.moveable;
	}
	else if(reward.rewardType == RewardType::RewardFastFire)
	{
		int amount = reward.amount > 0 ? reward.amount : 5;
		player.fastfire = player.fastfire - amount >= 6 ? player.fastfire - amount : 6;
	}
	else if(reward.rewardType == RewardType::RewardGuidePath)
	{
		if(player !is null && 0 <= npcId && npcId < int(npcBunniesCore::npcBunnies.length) && 0 <= reward.guidePathIndex && reward.guidePathIndex < int(npcBunniesCore::npcBunnies[npcId].guidePaths.length))
			UnlockGuidePathForPlayer(player.nameUnformatted, npcId, reward.guidePathIndex);
	}
}

int ParseRewardType(const string &in value)
{
	if(jjRegexMatch(value, "Gem", true) || jjRegexMatch(value, "Gems", true)) return RewardType::RewardGems;
	if(jjRegexMatch(value, "Ammo", true)) return RewardType::RewardAmmo;
	if(jjRegexMatch(value, "Powerup", true)) return RewardType::RewardPowerup;
	if(jjRegexMatch(value, "Teleport", true)) return RewardType::RewardTeleport;
	if(jjRegexMatch(value, "Trigger", true)) return RewardType::RewardTrigger;
	if(jjRegexMatch(value, "FastFire", true) || jjRegexMatch(value, "Fast Fire", true)) return RewardType::RewardFastFire;
	if(jjRegexMatch(value, "GuidePath", true) || jjRegexMatch(value, "Guide Path", true)) return RewardType::RewardGuidePath;
	if(jjRegexMatch(value, "Moveable", true) || jjRegexMatch(value, "Movable", true)) return RewardType::RewardMoveable;
	return RewardType::RewardCoins;
}

GEM::Color ParseGemType(const string &in value)
{
	if(jjRegexMatch(value, "Green", true)) return GEM::GREEN;
	if(jjRegexMatch(value, "Blue", true)) return GEM::BLUE;
	if(jjRegexMatch(value, "Purple", true)) return GEM::PURPLE;
	return GEM::RED;
}

int ParseWeaponType(const string &in value)
{
	if(value == "2" || jjRegexMatch(value, "Bouncer", true)) return WEAPON::BOUNCER;
	if(value == "3" || jjRegexMatch(value, "Ice", true) || jjRegexMatch(value, "Freezer", true)) return WEAPON::ICE;
	if(value == "4" || jjRegexMatch(value, "Seeker", true)) return WEAPON::SEEKER;
	if(value == "5" || jjRegexMatch(value, "RF", true)) return WEAPON::RF;
	if(value == "6" || jjRegexMatch(value, "Toaster", true)) return WEAPON::TOASTER;
	if(value == "7" || jjRegexMatch(value, "TNT", true)) return WEAPON::TNT;
	if(value == "8" || jjRegexMatch(value, "Pepper", true)) return WEAPON::GUN8;
	if(value == "9" || jjRegexMatch(value, "Electro", true)) return WEAPON::GUN9;
	return WEAPON::BOUNCER;
}
//----------------------------------------------------------------------------------------------------
// NPC JSON PARSING REGION
// Summary: LoadNpcDialogueFile builds <level>_npc.asdat from jjLevelFileName, falls back to <level>_npc.asdat,
// removes // and /* */ comments outside quoted strings, then parses the JSON-like NPC/dialogue structure.

}