Downloads containing npcBunniesCore.asc

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

File preview

#pragma name "NPC Bunnies! (by Cranky)"
#pragma require "charactersMod.j2a"
#pragma require "charactersModAnimIndexes.asdat"
#pragma require "MenuAnimsMod.j2a"

namespace npcBunniesCore
{
// #region ENABLE/DISABLE NPC FEATURES
bool DISPLAY_WAYPOINTS_ON = false;
bool MAP_UPDATES_ON = false;
bool LOAD_ALL_MONSTER_ANIMATIONS = false; //If false, only already-loaded or explicitly-added monster/boss animsets are available for NPCs.
bool LOAD_BIG_NPC_ANIMATIONS = true; //If false, MenuAnimsMod.j2a is not loaded and dialogue uses small icons instead.
bool ENABLE_NPC_SYNC = true; //False: no runtime NPC packets; every machine simulates NPCs locally.
bool ENABLE_NPC_MONSTER_QUESTS = true; //False disables defeat monster/boss quest object scans and respawns.
bool ENABLE_NPC_POSITION_SYNC = true; //Only matters when ENABLE_NPC_SYNC is true.
bool CLIENT_SIMULATES_NPC_MOVEMENT = true; //Only matters when ENABLE_NPC_SYNC is true.
bool ENABLE_NPC_DENSE_DRAW_OPTIMIZATION = true;
bool ENABLE_NPC_MONSTER_DRAWING = true;
bool ENABLE_NPC_ACCESSORIES = true;
bool HUD_COUNTS_ON = true;
// #endregion

const string NPC_SYNC_SETTING_FILE = "npcSyncSetting.asdat";
bool NPC_SYNC_SETTING_HAS_OVERRIDE = false;

array<int> NPC_ADDITIONAL_MONSTER_ANIMATIONS;
bool NPC_ALL_MONSTER_ANIMS_PENDING_RESTART = false;
int TELEPORT_DISTANCE = 0; //legacy: kept for compatibility, use TOO_FAR_FOR_PATHFINDING for follow teleport distance.
int TOO_FAR_FOR_PATHFINDING = 20;
int NPC_TELEPORT_ANIMATION_TICKS = 8;
int NPC_JUMP_X_SPEED = 5;
int NPC_JUMP_Y_SPEED = 5;
int NPC_FOLLOW_LIMIT = 10; // NPC follow limit. -1 means no limit.
int NPC_FOLLOW_SPACING_TILES = 2;
int NPC_DRAW_MARGIN_PIXELS = 96;
int NPC_MAX_FULL_DRAWS_PER_FRAME = 24;
int NPC_MAX_NAME_DRAWS_PER_FRAME = 24;
int NPC_TRACE_MAX_FRAMES = 5;
int NPC_TRACE_EXPIRE_TICKS = 15; //Trace lifetime in ticks.
int NPC_POSITION_SYNC_INTERVAL = 70;
float NPC_POSITION_SYNC_MIN_DISTANCE = 16.0;
float NPC_FOLLOW_CATCHUP_DISTANCE_DIVISOR = 256.0;
float NPC_FOLLOW_CATCHUP_MAX_MULT = 1.5;
int NPC_MAX_FOLLOW_WAYPOINTS = 96;
int NPC_DEFEAT_TRACK_SCAN_INTERVAL = 35;
int NPC_FIGHT_COMPLEX_SCAN_INTERVAL = 140;
int NPC_FIGHT_FIRE_INTERVAL = 70;
int NPC_FIGHT_SIGHT_RANGE = 420;
int NPC_FIGHT_ANIMATION_TICKS = 18;
int NPC_FIGHT_HORIZONTAL_Y_TOLERANCE = 20;
int HUD_COUNTS_X = 12;
int HUD_COUNTS_Y = 12;
int NPC_QUEST_DIALOG_KEY = 0x51; // Q
const int NPC_TRACE_OFF = 0;
const int NPC_TRACE_NORMAL = 1;
const int NPC_TRACE_INVERTED = 2;
const int NPC_TRACE_CONST = 3;
array<int> NPC_TRACE_COLOR_VALUES = {39, 24, 40, 16, 32, 80, 88, 56, 15, 1};
array<int> NPC_TRACE_YELLOW_RAMP = {40, 41, 42, 59, 60, 61, 62, 63};
bool npcQuestDialogOpen = false;
bool npcQuestDialogCaptureKey = false;
bool npcQuestMouseWasDown = false;
bool npcQuestDialogDragging = false;
int npcQuestDialogDragOffsetX = 0;
int npcQuestDialogDragOffsetY = 0;
int npcQuestDialogX = -1;
int npcQuestDialogY = -1;
int npcQuestDialogInputTick = -9999;
int npcQuestDialogPage = 0;
const int DB_NPC_QUEST_PREV = 1001;
const int DB_NPC_QUEST_NEXT = 1002;
const int DB_NPC_QUEST_KEY = 1003;
const int DB_NPC_QUEST_SCROLL_UP = 1004;
const int DB_NPC_QUEST_SCROLL_DOWN = 1005;
const int DB_NPC_QUEST_INFO = 1006;
int npcQuestDialogScroll = 0;
bool npcQuestDialogShowInfo = false;
int npcTalkScrollLine = 0;
int npcTalkScrollTick = -9999;
DbWindow npcQuestDialogWindow;
/*
Jump: set - anim - frame where jump2 starts, number of frames for jump 2
1 - 20 - 9, 3 frames
2 - 20 - 9, 3 frames
3 - 16 - 9, 4 frames
11- 20 - 4, 4 frames

OBSERVATION:

Renamed variables:
Characters -> NpcAnim (to be similar to CharAnim from char.mut)
followingNumber -> followingTrainIndex (to be more suggestive)
-The methods too: getFollowingNumber, recalculateFollowingNumbers -> getFollowingTrainIndex, recalculateFollowingTrainIndexes
hasChoices -> choicesAnswerMethod

TODO:
Code cleaning:
-Analyze the new imported code. See if there's anything that needs more understanding.
-Some variables might need renaming. For example npc.type would maybe be more accruately be npc.animSet,
 dialogueSets[0].dialogue would maybe more accurately be dialogueSets[0].dialogueLine, since that is what the member actually is.
-Compartmentalize as much as the code as you can, for example the movement methods. Try to respect the SRP.
-Dialogue seems spread over many methods, it's in the npcBunnies class, in onDrawScore, etc. Try to see if you can
 reduce that somehow. It's confusing for something to be set in one method, then another method, etc.
-Analyze if there is any code left to import from npc-bunnies.mut. Keep in mind that you want to rewrite how
 the dialogue system works. But maybe you can keep how the choices were chosen and answers were typed.
 Some of that is kept already by making "T" not open chat when pressing T while talking to an NPC.
-Search for all the "TODO"s and "TO DO"s and resolve them.
Bird and animations:
x-Jazz big animations are behaving a bit weird. There is a red outline at the first chat but then no red at all
  although the headband should be red.
-Tails head has a pink outline.
x-Fix the bird animations, they are breaking when following the player and then you can't even stop following.
x-Add bird animation frames: find how you did the exporting without certain colors or just edit with JASC PSP8.
  Then add the bird frames and face.
(1) full, (2) without green, (3) without red, (4) without blue, (5) witout yellow (only pink and white for eyes and ears)
-If char.mut is active, take the char head in the talking animation representing the custom player character from characters.j2a.
 Of course, do not load characters.j2a again, just identify what the animation sets that these heads are loaded in are!
Movement:
-firse, of course, complete organizing and compartmentalizing the movement code. That way the jump code
 can also be a separate method.
-Bezier Curve for short distance jumps (if the player gets too far away, just teleport). See how it looks
 given the limited animations loaded in. CAUTION: there might be obstacles in the way so you need to find
 a path to jump through first so that the NPC doesn't look like it's jumping through walls.
New dialogue system:
-first, of course, complete organizing the code (detailed in "Code cleaning") and especially the dialogue sections.
-then think how of ALL the quests, rewards, choices, answers, all the *public* members of the dialogue class.
 What I mean by *public* is all the members that would be initialized with the *json* method. You don't need
 to worry about the *private* ones, the members that you add along the way when you code and realize that you
 need them in order to store something or to make some mechanism work. Of course, if you do realize that you
 need a *public* member that needs to be added to the *json* method, but try to think of all the things a
 level maker might need. Try to keep it as simple as possible.
-after that implement all the quests & rewards. Choices and answers are already implemented so you might just
 need to "dress" them up a little better.
*/

class Pair
{
	float x;
	float y;
}

//keep the order of the animations - it is the same as in charactersMin.j2a (charactersMod.j2a)
array<int> NpcAnimations =
{
	RABBIT::STAND,
	RABBIT::RUN1,
	RABBIT::RUN3,
	RABBIT::FALL,
	RABBIT::JUMPING1
};

enum NpcAnim
{
	HEAD = 0, //used for heads
	JAZZ = 1,
	SPAZ = 2,
	LORI = 3,
	FROG = 4,
	BIRD = 5, //leave empty folder in charactersMod.j2a if you don't need any additional animations
	DEVAN = 6, //leave empty folder in charactersMod.j2a if you don't need any additional animations
			   //however, I was thinking that if you want to add WITCH or other animations that are in anim.j2a
			   //you could use this as a sort of OTHER animation index instead of increasing the numbers on the rows belloe
			   //but you will see what is best when you get to it. Maybe you add these characters to the end instead (index 12).
			   //That might actually be better.
	MARIO = 7,
	SONIC = 8,
	TAILS = 9,
	JJ1 = 10,
	YOSHI = 11,
	EVA = 12,
};
//REMEMBER: when adding something in NpcAnim, add it in npcList as well
array<int> npcList = {NpcAnim::JAZZ, NpcAnim::SPAZ, NpcAnim::LORI, NpcAnim::FROG, NpcAnim::BIRD, NpcAnim::DEVAN,
	NpcAnim::MARIO, NpcAnim::SONIC, NpcAnim::TAILS, NpcAnim::JJ1, NpcAnim::YOSHI, NpcAnim::EVA};

bool loadTeleportAnims = true;

const string NPC_BUNNIES_ADMIN_PRIVILEGE = "npcBunnies.mut:*";
const string NPC_BUNNIES_ADMIN_PRIVILEGE_ROOT = "npcBunnies.mut";
const string NPC_BUNNIES_ADMIN_GROUP = "NpcBunniesAdmin";
const string NPC_BUNNIES_ADMIN_PRIVILEGE_LOWER = "npcbunnies.mut:*";
const string NPC_BUNNIES_ADMIN_PRIVILEGE_ROOT_LOWER = "npcbunnies.mut";
const string NPC_BUNNIES_ADMIN_PRIVILEGE_SHORT = "npcBunnies:*";
const string NPC_BUNNIES_ADMIN_PRIVILEGE_SHORT_ROOT = "npcBunnies";
const string NPC_BUNNIES_ADMIN_PRIVILEGE_SHORT_LOWER = "npcbunnies:*";
const string NPC_BUNNIES_ADMIN_PRIVILEGE_SHORT_ROOT_LOWER = "npcbunnies";
const string NPC_BUNNIES_ADMIN_GROUP_LOWER = "npcbunniesadmin";
bool npcBunniesAdminAccessGranted = false;
int npcBunniesAdminAccessRequestTick = -9999;
string npcDesignerSyncText = "";
bool npcDesignerSyncAllowed = false;

bool HasNpcBunniesAdminPrivilege(jjPLAYER@ play)
{
	if(play is null) return false;
	return play.hasPrivilege(NPC_BUNNIES_ADMIN_PRIVILEGE)
		|| play.hasPrivilege(NPC_BUNNIES_ADMIN_PRIVILEGE_ROOT)
		|| play.hasPrivilege(NPC_BUNNIES_ADMIN_GROUP)
		|| play.hasPrivilege(NPC_BUNNIES_ADMIN_PRIVILEGE_LOWER)
		|| play.hasPrivilege(NPC_BUNNIES_ADMIN_PRIVILEGE_ROOT_LOWER)
		|| play.hasPrivilege(NPC_BUNNIES_ADMIN_PRIVILEGE_SHORT)
		|| play.hasPrivilege(NPC_BUNNIES_ADMIN_PRIVILEGE_SHORT_ROOT)
		|| play.hasPrivilege(NPC_BUNNIES_ADMIN_PRIVILEGE_SHORT_LOWER)
		|| play.hasPrivilege(NPC_BUNNIES_ADMIN_PRIVILEGE_SHORT_ROOT_LOWER)
		|| play.hasPrivilege(NPC_BUNNIES_ADMIN_GROUP_LOWER);
}

bool HasNpcBunniesAdminAccess(jjPLAYER@ play)
{
	if(play is null) return false;
	if(HasNpcBunniesAdminPrivilege(play)) return true;
	return play.isLocal && npcBunniesAdminAccessGranted;
}

bool LocalPlayerHasNpcBunniesAdminAccess()
{
	for(int i = 0; i < jjLocalPlayerCount; i++)
	{
		jjPLAYER@ play = jjLocalPlayers[i];
		if(play.isInGame && HasNpcBunniesAdminAccess(play)) return true;
	}
	return false;
}
bool chatIsOpen = false;
int chatIsOpenTicks = -1;
void isChatOpen()
{
	if(jjKey[jjKeyChat])
	{
		chatIsOpen = true;
	}
	else if(chatIsOpen && (jjKey[13] || jjKey[0x1B]))
	{
		chatIsOpenTicks = 30;
	}
	if(chatIsOpenTicks > 0) chatIsOpenTicks--;
	else if(chatIsOpenTicks == 0){ chatIsOpen = false; chatIsOpenTicks = -1; }
}


string NpcQuestKeyName(int key)
{
	if(key >= 65 && key <= 90)
	{
		string letters = "abcdefghijklmnopqrstuvwxyz";
		return letters.substr(key - 65, 1);
	}
	if(key >= 48 && key <= 57)
	{
		string digits = "0123456789";
		return digits.substr(key - 48, 1);
	}
	if(key == 32) return "space";
	if(key == 13) return "enter";
	return "key " + key;
}

void LoadNpcBunniesPreferences()
{
	jjSTREAM load("npcBunniesPreferences.asdat");
	if(load.isEmpty()) return;
	string line;
	while(load.getLine(line))
	{
		array<string> parts = line.split(" ");
		if(parts.length >= 2 && (parts[0] == "QuestKey" || parts[0] == "QuestDialogKey")) NPC_QUEST_DIALOG_KEY = parseInt(parts[1]);
		else if(parts.length >= 2 && parts[0] == "QuestDialogX") npcQuestDialogX = parseInt(parts[1]);
		else if(parts.length >= 2 && parts[0] == "QuestDialogY") npcQuestDialogY = parseInt(parts[1]);
	}
}

string NpcSyncSettingText(bool enabled)
{
	return enabled ? "ON" : "OFF";
}

bool ParseNpcSyncSettingValue(const string &in value, bool &out enabled)
{
	if(value == "ON" || value == "on" || value == "On" || value == "1" || value == "true" || value == "TRUE" || value == "True")
	{
		enabled = true;
		return true;
	}
	if(value == "OFF" || value == "off" || value == "Off" || value == "0" || value == "false" || value == "FALSE" || value == "False")
	{
		enabled = false;
		return true;
	}
	return false;
}

void LoadNpcSyncSetting()
{
	NPC_SYNC_SETTING_HAS_OVERRIDE = false;
	jjSTREAM load(NPC_SYNC_SETTING_FILE);
	if(load.isEmpty()) return;
	string line;
	while(load.getLine(line))
	{
		array<string> parts = line.split(" ");
		if(parts.length < 2) continue;
		if(parts[0] != "NpcSync" && parts[0] != "ENABLE_NPC_SYNC") continue;
		bool enabled = ENABLE_NPC_SYNC;
		if(ParseNpcSyncSettingValue(parts[1], enabled))
		{
			ENABLE_NPC_SYNC = enabled;
			NPC_SYNC_SETTING_HAS_OVERRIDE = true;
			return;
		}
	}
}

void SaveNpcSyncSetting(bool enabled)
{
	jjSTREAM save;
	save.write("// NPC sync runtime setting. Edit the next line as NpcSync ON or NpcSync OFF.\n");
	save.write("// This is read by the server on level load. If this file is missing, ENABLE_NPC_SYNC from the script is used.\n");
	save.write("NpcSync " + NpcSyncSettingText(enabled) + "\n");
	save.save(NPC_SYNC_SETTING_FILE);
}

void SaveNpcBunniesPreferences()
{
	jjSTREAM save;
	save.write("QuestKey " + NPC_QUEST_DIALOG_KEY + "\n");
	save.write("QuestDialogX " + npcQuestDialogX + "\n");
	save.write("QuestDialogY " + npcQuestDialogY + "\n");
	save.save("npcBunniesPreferences.asdat");
}
class furClass
{
	uint c1;
	uint c2;
	uint c3;
	uint c4;
}

int GetPrimaryFurShift(int animIndex, uint color)
{
	if(animIndex == NpcAnim::SPAZ) return color == 16 ? 0 : int(color) - 16;
	if(animIndex == NpcAnim::LORI) return int(color) - (color == 16 ? 16 : 89);
	return int(color) - 16;
}

class GuideStep
{
	int x = 0;
	int y = 0;

	GuideStep(int x = 0, int y = 0)
	{
		this.x = x;
		this.y = y;
	}
}

class GuidePath
{
	bool isTeleport = false;
	string line = "";
	array<GuideStep@> steps;
	int teleportX = 0;
	int teleportY = 0;
	int teleportDirection = 1;
}
class npcBunny
{
	int id;
	int objectID;
	int type; //jazz, spaz, lori, frog, bird, etc. - can mark with ANIM::JAZZ / ANIM::CUSTOM[1] etc.
	string name;
	int playerFollowing;
	int followingTrainIndex; //the NPC number in the "train" line following the player
	int playerTalking;
	bool move;
	bool moveable;
	bool pushable = false;
	bool fightingAvailable;
	bool fightingEnabled;
	int fightingStyle;
	int fightingTargetObjectID;
	int fightingLastScanTick;
	int fightingLastFireTick;
	int fightingAnimUntilTick;
	int fightingDirection;
	int fightingLastPlayerFireTick;
	int hatFrame;
	int scarfFrame;
	int glassesFrame;
	furClass fur;

	bool xLimit = false;
	float fallToX;
	float fallToY;
	int tpAnim = -1;
	float tpTargetX = 0;
	float tpTargetY = 0;
	bool shouldFindPath = false;

	int action = 0; //0 = none, 1 = talking, 2 = follow, 3 = shop, etc. (TO DO: enum!!!)
	int convIndex = -1; //index for dialogueSet
	int dialogueIndex = -1;
	array<int> activeQuestSet;
	array<string> convLines;
	bool guideEnabled = false;
	array<GuidePath@> guidePaths;
	int guideNextPathIndex = 0;
	int guideActivePathIndex = -1;
	bool guideActive = false;
	string guideEndLine = "";
	string guideAlreadyAtDestinationLine = "";
	string guidePendingLine = "";
	int guidePendingAction = 0; //0 none, 1 start path, 2 end line, 3 already at destination
	int guidePendingPathIndex = -1;
	int guidePlayerId = -1;
	int traceMode = NPC_TRACE_OFF;
	int traceColor = 24;
	array<npcBunniesShop::NpcShopItem@> shopItems;
	int shopColumns = 1;
	int shopSelectedIndex = 0;
	bool shopConfirmOpen = false;
	bool shopConfirmYes = true;
	string shopMessage = "";

	float xTile;
	float yTile;
	int direction;
	int sizeMode = 1;
	float moveSpeed = 1.0;
	array<npcBunniesDialogue::DialogueSet@> dialogueSets;
	string dialogueOverrideLine = "";
	int dialogueOverrideNextSet = -2;
	bool dialogueOverrideFinalQuestCompleted = false;

	//TODO: methods here
	int getDialogueSetNumber()
	{
		if(npcBunnies[id].convIndex == -1) npcBunnies[id].convIndex++;
		//you should actually only check if conditions are fulfilled at the end of the dialogue set
		//otherwise you will not get to see the dialogue set and you might not even get the rewards
		if(npcBunnies[id].convIndex >= int(npcBunnies[id].dialogueSets.length)) npcBunnies[id].convIndex--;
		return npcBunnies[id].convIndex;
	}

	int getDialogueNumber()
	{
		if(uint(npcBunnies[id].dialogueIndex+1) >= npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].dialogue.length)
		{
			npcBunniesMovement::convOn = -1;
			return -1;
		}
		return npcBunnies[id].dialogueIndex+1;
	}

	bool isLastLineInDialogueSet()
	{
		if(npcBunnies[id].convIndex < 0) return false;
		return uint(npcBunnies[id].dialogueIndex) == (npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].dialogue.length - 1);
	}

	int checkConditions()
	{
		if(npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].conditionsExist || npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choicesAnswerMethod == DialogueAction::Quest)
		{
			return conditionsFulfilled() ? npcBunnies[id].convIndex + 1 : npcBunnies[id].convIndex;
		}
		return npcBunnies[id].convIndex;
	}

	bool conditionsFulfilled()
	{
		npcBunniesDialogue::DialogueSet@ set = npcBunnies[id].dialogueSets[npcBunnies[id].convIndex];
		return npcBunniesQuests::AreNpcQuestsComplete(set, jjPlayers[playerTalking], id, npcBunnies[id].convIndex);
	}

	void subtractQuestCosts()
	{
		npcBunniesDialogue::DialogueSet@ set = npcBunnies[id].dialogueSets[npcBunnies[id].convIndex];
		npcBunniesQuests::SpendQuestCosts(set, jjPlayers[playerTalking]);
	}

	void grantRewards()
	{
		npcBunniesDialogue::DialogueSet@ set = npcBunnies[id].dialogueSets[npcBunnies[id].convIndex];
		if(set.choicesAnswerMethod == DialogueAction::Quest && !npcBunniesQuests::AreNpcQuestsComplete(set, jjPlayers[playerTalking], id, npcBunnies[id].convIndex)) return;
		if(set.rewardsGranted && set.quests.length == 0) return;
		npcBunniesQuests::GrantNpcRewards(set, jjPlayers[playerTalking], id);
	}

	int resolveGoToSet(int nextSet)
	{
		if(nextSet == -2) return npcBunnies[id].convIndex;
		if(nextSet == -1)
		{
			int lastSet = int(npcBunnies[id].dialogueSets.length) - 1;
			int next = npcBunnies[id].convIndex + 1;
			return next > lastSet ? lastSet : next;
		}
		return nextSet;
	}

	int goToSet() //NEW
	{
		if(npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choicesAnswerMethod == DialogueAction::Choice) //TODO: enum::CHOICES
		{
			npcBunniesMovement::convOn = -1;
			dialogueIndex = 0;
			//jjAlert("goToSet: "+highlightChoice+" "+npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choices[highlightChoice].goToSet);
			//jjAlert("choices goTo: "+npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choices[0].goToSet+" "+npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choices[1].goToSet+" "+npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choices[2].goToSet);
			//jjAlert("go to: "+(npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choices[highlightChoice].goToSet));
			return resolveGoToSet(npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choices[npcBunniesDialogue::highlightChoice].goToSet);
		}
		else if(npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choicesAnswerMethod == 2) //TODO: enum::ANSWERS
		{
			npcBunniesMovement::convOn = -1;
			dialogueIndex = 0;
			npcBunniesDialogue::answerText += " ";
			for(uint i=0; i<npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choices.length; i++)
			{
				//TODO: answerText.Trim() and also choiceText.Trim() because level makers might add in unnecessary spaces to their answers
				if(npcBunniesDialogue::answerText[0] == 32 || npcBunniesDialogue::answerText[0] == 8 || npcBunniesDialogue::answerText[0] == 13) npcBunniesDialogue::answerText = npcBunniesDialogue::answerText.substr(1, npcBunniesDialogue::answerText.length);
				if(jjRegexMatch(npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choices[i].choiceText, npcBunniesDialogue::answerText.substr(0, npcBunniesDialogue::answerText.length-1), true))
				{
					npcBunniesDialogue::answerText = "";
					return resolveGoToSet(npcBunnies[id].dialogueSets[npcBunnies[id].convIndex].choices[i].goToSet);
				}
			}
			return npcBunnies[id].convIndex;
		}
		else
		{
			return getDialogueNumber();
		}
	}
}

array<npcBunny> npcBunnies;
dictionary npcObjectIds;
array<bool> localNpcControl(4, false);
array<string> activeQuestPlayerName(32, "");
array<bool> activeQuestPlayerKnown(32, false);
dictionary activeQuestOffersSeen;
dictionary guideEndLinePlayersSeen;

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

bool HasPlayerSeenGuideEndLine(const string &in playerName, int npcId)
{
	if(playerName == "" || npcId < 0) return false;
	bool seen = false;
	return guideEndLinePlayersSeen.get(MakeGuideEndLineSeenKey(playerName, npcId), seen) && seen;
}

void SetPlayerSeenGuideEndLine(const string &in playerName, int npcId)
{
	if(playerName == "" || npcId < 0) return;
	guideEndLinePlayersSeen.set(MakeGuideEndLineSeenKey(playerName, npcId), true);
}
bool IsValidNpcQuestPlayer(int playerId)
{
	return 0 <= playerId && playerId < 32;
}

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

bool HasPlayerSeenQuestOffer(const string &in playerName, int npcId, int setIndex)
{
	if(playerName == "" || npcId < 0 || setIndex < 0) return false;
	bool seen = false;
	return activeQuestOffersSeen.get(MakeActiveQuestOfferKey(playerName, npcId, setIndex), seen) && seen;
}

void SetPlayerSeenQuestOffer(const string &in playerName, int npcId, int setIndex)
{
	if(playerName == "" || npcId < 0 || setIndex < 0) return;
	activeQuestOffersSeen.set(MakeActiveQuestOfferKey(playerName, npcId, setIndex), true);
}

void ClearPlayerSeenQuestOffer(const string &in playerName, int npcId, int setIndex)
{
	if(playerName == "" || npcId < 0 || setIndex < 0) return;
	activeQuestOffersSeen.delete(MakeActiveQuestOfferKey(playerName, npcId, setIndex));
}

int FindSeenQuestOfferSet(const string &in playerName, int npcId)
{
	if(playerName == "" || npcId < 0 || npcId >= int(npcBunnies.length)) return -1;
	npcBunny npc = EnsureNpcQuestState(npcBunnies[npcId]);
	for(uint i = 0; i < npc.dialogueSets.length; i++)
	{
		if(npc.dialogueSets[i].choicesAnswerMethod == DialogueAction::Quest && HasPlayerSeenQuestOffer(playerName, npcId, int(i))) return int(i);
	}
	return -1;
}

npcBunny InitNpcQuestState(npcBunny npc)
{
	npc.activeQuestSet.resize(32);
	for(uint i = 0; i < npc.activeQuestSet.length; i++) npc.activeQuestSet[i] = -1;
	return npc;
}

npcBunny EnsureNpcQuestState(npcBunny npc)
{
	if(npc.activeQuestSet.length != 32) npc = InitNpcQuestState(npc);
	return npc;
}

int ResolveDialogueSetGoTo(npcBunny &in npc, int sourceSet, int goToSet)
{
	if(sourceSet < 0 || sourceSet >= int(npc.dialogueSets.length)) return sourceSet;
	if(goToSet == -2) return sourceSet;
	if(goToSet == -1)
	{
		int lastSet = int(npc.dialogueSets.length) - 1;
		int next = sourceSet + 1;
		return next > lastSet ? lastSet : next;
	}
	return goToSet;
}

bool IsFinalCompletedQuestSet(npcBunny &in npc, int setIndex)
{
	if(setIndex < 0 || setIndex >= int(npc.dialogueSets.length)) return false;
	npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[setIndex];
	if(set is null || set.choicesAnswerMethod != DialogueAction::Quest || !set.rewardsGranted) return false;
	int completedNext = set.questCompletedSet >= 0 ? set.questCompletedSet : ResolveDialogueSetGoTo(npc, setIndex, set.goToSet);
	return completedNext == setIndex;
}

void ShowFinalCompletedQuestLine(npcBunny &inout npc, int setIndex)
{
	npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[setIndex];
	npc.convIndex = setIndex;
	npc.dialogueIndex = -1;
	npc.dialogueOverrideLine = set.questCompletedLine != "" ? set.questCompletedLine : "Quest complete.";
	npc.dialogueOverrideNextSet = setIndex;
	npc.dialogueOverrideFinalQuestCompleted = true;
}

void RouteActiveQuestBeforeDialogue(int npcId, int playerId)
{
	if(npcId < 0 || npcId >= int(npcBunnies.length) || !IsValidNpcQuestPlayer(playerId)) return;
	string playerName = jjPlayers[playerId].nameUnformatted;
	npcBunny npc = EnsureNpcQuestState(npcBunnies[npcId]);
	int questSetIndex = FindSeenQuestOfferSet(playerName, npcId);
	if(questSetIndex < 0 || questSetIndex >= int(npc.dialogueSets.length))
	{
		if(IsFinalCompletedQuestSet(npc, npc.convIndex)) ShowFinalCompletedQuestLine(npc, npc.convIndex);
		npcBunnies[npcId] = npc;
		return;
	}
	npcBunniesDialogue::DialogueSet@ questSet = npc.dialogueSets[questSetIndex];
	if(questSet.choicesAnswerMethod != DialogueAction::Quest) { ClearPlayerSeenQuestOffer(playerName, npcId, questSetIndex); npcBunnies[npcId] = npc; return; }

	int questInProgressSet = questSet.questInProgressSet;
	int questCompletedSet = questSet.questCompletedSet;
	int questGoToSet = questSet.goToSet;
	npc.convIndex = questSetIndex;
	npc.dialogueIndex = -1;
	npcBunnies[npcId] = npc;
	if(npc.conditionsFulfilled())
	{
		npc.grantRewards();
		npc = EnsureNpcQuestState(npcBunnies[npcId]);
		npc.activeQuestSet[playerId] = -1;
		ClearPlayerSeenQuestOffer(playerName, npcId, questSetIndex);
		if(questSet.questCompletedLine != "")
		{
			npc.convIndex = questSetIndex;
			npc.dialogueOverrideLine = questSet.questCompletedLine;
			npc.dialogueOverrideNextSet = questCompletedSet >= 0 ? questCompletedSet : npc.resolveGoToSet(questGoToSet);
			npc.dialogueOverrideFinalQuestCompleted = false;
		}
		else npc.convIndex = questCompletedSet >= 0 ? questCompletedSet : npc.resolveGoToSet(questGoToSet);
	}
	else if(questSet.questInProgressLine != "")
	{
		npc.convIndex = questSetIndex;
		npc.dialogueOverrideLine = questSet.questInProgressLine;
		npc.dialogueOverrideNextSet = -2;
		npc.dialogueOverrideFinalQuestCompleted = false;
	}
	else if(questInProgressSet >= 0)
	{
		npc.convIndex = questInProgressSet;
	}
	npc.dialogueIndex = -1;
	npcBunnies[npcId] = npc;
}

void ClearQuestStateForPlayer(int playerId)
{
	for(uint npcId = 0; npcId < npcBunnies.length; npcId++)
	{
		npcBunny npc = EnsureNpcQuestState(npcBunnies[npcId]);
		npc.activeQuestSet[playerId] = -1;
		if(npc.playerTalking == playerId) npc.playerTalking = -1;
		if(npc.playerFollowing == playerId)
		{
			npcBunniesMovement::recalculateFollowingTrainIndexes(npc.followingTrainIndex, playerId);
			npc.playerFollowing = -1;
			npc.followingTrainIndex = -1;
			if(jjIsServer && ENABLE_NPC_SYNC) npcBunniesHooks::sendNpcSync(npc.id, npc.playerFollowing, npc.followingTrainIndex, 0);
		}
		npcBunnies[npcId] = npc;
	}
}

void ResetInactivePlayerQuestState()
{
	for(int playerId = 0; playerId < 32; playerId++)
	{
		if(jjPlayers[playerId].isInGame)
		{
			string currentPlayerName = jjPlayers[playerId].nameUnformatted;
			if(!activeQuestPlayerKnown[playerId] || activeQuestPlayerName[playerId] != currentPlayerName)
			{
				ClearQuestStateForPlayer(playerId);
				activeQuestPlayerName[playerId] = currentPlayerName;
				activeQuestPlayerKnown[playerId] = true;
			}
		}
		else if(activeQuestPlayerKnown[playerId])
		{
			ClearQuestStateForPlayer(playerId);
			activeQuestPlayerName[playerId] = "";
			activeQuestPlayerKnown[playerId] = false;
		}
	}
}

enum NpcAction
{
	Menu = 0,
	Talk = 1,
	Follow = 2,
	Shop = 3,
	Guide = 4,
	Fighting = 5,
	Return = 6
}

enum NpcFightingStyle
{
	Complex = 0,
	Simple = 1
}

float NpcSizeScale(int sizeMode)
{
	if(sizeMode <= 0) return 0.5;
	if(sizeMode >= 2) return 2.0;
	return 1.0;
}

float NpcSizeDrawYOffset(int sizeMode)
{
	if(sizeMode <= 0) return 11.0;
	if(sizeMode >= 2) return -23.5;
	return 0.0;
}

float NpcSizeNameYOffset(int sizeMode)
{
	if(sizeMode <= 0) return -5.0;
	if(sizeMode >= 2) return -75.0;
	return -34.0;
}

string NpcTraceModeName(int mode)
{
	if(mode == NPC_TRACE_NORMAL) return "ON";
	if(mode == NPC_TRACE_INVERTED) return "INVERTED";
	if(mode == NPC_TRACE_CONST) return "CONST";
	return "OFF";
}

int ClampNpcTraceMode(int mode)
{
	if(mode < NPC_TRACE_OFF) return NPC_TRACE_OFF;
	if(mode > NPC_TRACE_CONST) return NPC_TRACE_CONST;
	return mode;
}

int ClampNpcTraceColor(int color)
{
	if(color < 0) return 0;
	if(color > 255) return 255;
	return color;
}

bool IsNpcTraceRampBase(int color)
{
	return color == 0 || color == 8 || color == 16 || color == 24 || color == 32 || color == 40 || color == 48 || color == 56 || color == 80 || color == 88;
}

int NpcTraceRampColor(int color, int mode, int traceIndex, int traceCount)
{
	if(mode == NPC_TRACE_CONST) return ClampNpcTraceColor(color);
	if(!IsNpcTraceRampBase(color)) return ClampNpcTraceColor(color);
	int shade = traceCount <= 1 ? 0 : (traceIndex * 7) / (traceCount - 1);
	if(mode != NPC_TRACE_INVERTED)
		shade = 7 - shade;
	if(color == 40) return NPC_TRACE_YELLOW_RAMP[shade];
	return ClampNpcTraceColor(color + shade);
}

bool NpcHasStandingFightingAnimation(int npcType)
{
	return npcType == NpcAnim::JAZZ || npcType == NpcAnim::SPAZ || npcType == NpcAnim::LORI
		|| npcType == NpcAnim::TAILS || npcType == NpcAnim::JJ1 || npcType == NpcAnim::YOSHI
		|| npcType == NpcAnim::DEVAN;
}

bool NpcHasJumpFightingAnimation(int npcType)
{
	return npcType == NpcAnim::JAZZ || npcType == NpcAnim::SPAZ || npcType == NpcAnim::LORI || npcType == NpcAnim::YOSHI;
}

int GetNpcFightingAnimation(npcBunny npc, bool jumping, int fallbackAnimation)
{
	int npcType = npc.type == NpcAnim::FROG ? NpcAnim::FROG : int(npcBunniesAnimations::animTypes.find(npc.type));
	if(npcType == NpcAnim::FROG) return jumping ? fallbackAnimation : 3;
	if(jumping && NpcHasJumpFightingAnimation(npcType)) return 6;
	if(NpcHasStandingFightingAnimation(npcType)) return 5;
	return fallbackAnimation;
}
enum DialogueAction
{
	Normal = 0,
	Choice = 1,
	Answer = 2,
	Quest = 3
}

//(15, 30) - Mario, (5, 15) - Sonic, (10, 40) - Tails, (10, 45) - JJ1, (10, 40) - Yoshi, (5, 45) - Devan

}