Downloads containing npcBunnies_Release.asc

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

File preview

// Generated by npcBuildScript.bat. Edit the separate source files, not this release file.

#pragma require "charactersMod.j2a"
#pragma require "charactersMinAnimIndexes.asdat"
#pragma require "MenuAnimsMod.j2a"
#pragma require "xmasHats.j2a"
#pragma require "npcHats.asdat"
#pragma require "npcScarfs.asdat"
#pragma require "npcGlasses.asdat"

// ===== DialogueBox.asc =====
const string DB_PREFERENCES_FILE = "DialogueBoxPreferences.asdat";
const int DB_ACTION_NONE = 0;
const int DB_ACTION_CLOSE = -1;
const int DB_ACTION_TAB = -2;
const int DB_ACTION_SCROLL_UP = -3;
const int DB_ACTION_SCROLL_DOWN = -4;
const int DB_FOCUS_OFFSET_Y = -10;

class DbResult
{
	bool activated = false;
	bool closeRequested = false;
	bool tabChanged = false;
	bool positionChanged = false;
	int action = DB_ACTION_NONE;
	int a = 0;
	int b = 0;
	int tab = 0;
}

class DbControl
{
	int x;
	int y;
	int w;
	int h;
	int action;
	int a;
	int b;
	string text;
	bool enabled = true;
}

class DbWindow
{
	string id;
	string title;
	string preferencesFile = "DialogueBoxPreferences.asdat";
	int x;
	int y;
	int w;
	int h;
	int pad = 10;
	int titleHeight = 18;
	int focusIndex = 0;
	int activeTab = 0;
	int lastClickTick = -9999;
	int lastKeyTick = -9999;
	bool mouseWasDown = false;
	bool dragging = false;
	int dragOffsetX = 0;
	int dragOffsetY = 0;
	bool closeButton = true;
	bool draggable = false;
	bool titleBar = true;
	int backgroundColor = 32;
	int titleColor = 72;
	int focusColor = 15;
	array<DbControl> controls;
}

array<string> dbPreferenceIds;
array<int> dbPreferenceX;
array<int> dbPreferenceY;
bool dbPreferencesLoaded = false;
string dbPreferencesLoadedFile = "";

int DbPreferenceIndex(const string &in id)
{
	for(uint i = 0; i < dbPreferenceIds.length; i++)
		if(dbPreferenceIds[i] == id) return int(i);
	return -1;
}

void DbLoadPreferences(const string &in filename)
{
	if(dbPreferencesLoaded && dbPreferencesLoadedFile == filename) return;
	dbPreferencesLoaded = true;
	dbPreferencesLoadedFile = filename;
	dbPreferenceIds.resize(0);
	dbPreferenceX.resize(0);
	dbPreferenceY.resize(0);
	jjSTREAM load(filename);
	string line;
	while(load.getLine(line))
	{
		array<string> parts = line.split(" ");
		if(parts.length >= 3)
		{
			dbPreferenceIds.insertLast(parts[0]);
			dbPreferenceX.insertLast(parseInt(parts[1]));
			dbPreferenceY.insertLast(parseInt(parts[2]));
		}
	}
}

void DbSavePreferences(const string &in filename)
{
	jjSTREAM save;
	for(uint i = 0; i < dbPreferenceIds.length; i++)
		save.write(dbPreferenceIds[i] + " " + dbPreferenceX[i] + " " + dbPreferenceY[i] + "\n");
	save.save(filename);
}

void DbApplySavedPosition(DbWindow@ window)
{
	DbLoadPreferences(window.preferencesFile);
	int index = DbPreferenceIndex(window.id);
	if(index >= 0)
	{
		window.x = dbPreferenceX[index];
		window.y = dbPreferenceY[index];
	}
	DbClampToScreen(window);
}

void DbRememberPosition(DbWindow@ window)
{
	DbLoadPreferences(window.preferencesFile);
	int index = DbPreferenceIndex(window.id);
	if(index < 0)
	{
		dbPreferenceIds.insertLast(window.id);
		dbPreferenceX.insertLast(window.x);
		dbPreferenceY.insertLast(window.y);
	}
	else
	{
		dbPreferenceX[index] = window.x;
		dbPreferenceY[index] = window.y;
	}
	DbSavePreferences(window.preferencesFile);
}

void DbClampToScreen(DbWindow@ window)
{
	if(window.x + window.w > jjResolutionWidth) window.x = jjResolutionWidth - window.w;
	if(window.y + window.h > jjResolutionHeight) window.y = jjResolutionHeight - window.h;
	if(window.x < 0) window.x = 0;
	if(window.y < 0) window.y = 0;
}

void DbBegin(DbWindow@ window, const string &in id, const string &in title, int x, int y, int w, int h)
{
	if(window.preferencesFile == "") window.preferencesFile = DB_PREFERENCES_FILE;
	bool sameWindow = window.id == id;
	window.id = id;
	window.title = title;
	window.w = w;
	window.h = h;
	window.controls.resize(0);
	if(sameWindow)
	{
		DbClampToScreen(window);
	}
	else
	{
		window.x = x;
		window.y = y;
		DbApplySavedPosition(window);
	}
}

void DbClearControls(DbWindow@ window)
{
	window.controls.resize(0);
}

void DbAddControl(DbWindow@ window, int x, int y, int w, int h, int action, int a, int b, const string &in text, bool enabled = true)
{
	DbControl control;
	control.x = x;
	control.y = y;
	control.w = w;
	control.h = h;
	control.action = action;
	control.a = a;
	control.b = b;
	control.text = text;
	control.enabled = enabled;
	window.controls.insertLast(control);
}

array<string> DbWrappedSmallLines(const string &in text, int maxPixels)
{
	array<string> lines;
	if(maxPixels <= 0) return lines;
	array<string> paragraphs = text.split("\n");
	for(uint p = 0; p < paragraphs.length; p++)
	{
		if(paragraphs[p] == "")
		{
			lines.insertLast("");
			continue;
		}
		array<string> words = paragraphs[p].split(" ");
		string line = "";
		for(uint i = 0; i < words.length; i++)
		{
			string remaining = words[i];
			while(remaining != "" && jjGetStringWidth(remaining, STRING::SMALL, STRING::NORMAL) > maxPixels)
			{
				int take = int(remaining.length());
				while(take > 1 && jjGetStringWidth(remaining.substr(0, take), STRING::SMALL, STRING::NORMAL) > maxPixels)
					take--;
				if(line != "")
				{
					lines.insertLast(line);
					line = "";
				}
				lines.insertLast(remaining.substr(0, take));
				remaining = remaining.substr(take);
			}
			if(remaining == "") continue;
			string candidate = line == "" ? remaining : line + " " + remaining;
			if(jjGetStringWidth(candidate, STRING::SMALL, STRING::NORMAL) <= maxPixels) line = candidate;
			else
			{
				if(line != "") lines.insertLast(line);
				line = remaining;
			}
		}
		if(line != "") lines.insertLast(line);
	}
	if(lines.length == 0) lines.insertLast("");
	return lines;
}

int DbWrappedSmallLineCount(const string &in text, int maxPixels)
{
	array<string> lines = DbWrappedSmallLines(text, maxPixels);
	return int(lines.length);
}

int DbVisibleSmallLines(int pixelHeight, int lineHeight = 14)
{
	int lines = pixelHeight / lineHeight;
	return lines < 1 ? 1 : lines;
}

int DbClampSmallScroll(const string &in text, int maxPixels, int pixelHeight, int scrollLine, int lineHeight = 14)
{
	int maxScroll = DbWrappedSmallLineCount(text, maxPixels) - DbVisibleSmallLines(pixelHeight, lineHeight);
	if(maxScroll < 0) maxScroll = 0;
	if(scrollLine < 0) return 0;
	if(scrollLine > maxScroll) return maxScroll;
	return scrollLine;
}

int DbDrawWrappedSmallScrolled(jjCANVAS@ canvas, int x, int y, const string &in text, int maxPixels, int maxY, int scrollLine, int lineHeight = 14)
{
	array<string> lines = DbWrappedSmallLines(text, maxPixels);
	for(uint i = 0; i < lines.length; i++)
	{
		if(int(i) < scrollLine) continue;
		if(y + lineHeight > maxY) break;
		canvas.drawString(x, y, lines[i], STRING::SMALL, STRING::NORMAL);
		y += lineHeight;
	}
	return y;
}

int DbDrawWrappedSmall(jjCANVAS@ canvas, int x, int y, const string &in text, int maxPixels, int maxY, int lineHeight = 14)
{
	return DbDrawWrappedSmallScrolled(canvas, x, y, text, maxPixels, maxY, 0, lineHeight);
}

int DbDrawWrappedSmallInBox(jjCANVAS@ canvas, int x, int y, const string &in text, int maxPixels, int pixelHeight, int lineHeight = 14)
{
	return DbDrawWrappedSmall(canvas, x, y, text, maxPixels, y + pixelHeight, lineHeight);
}

int DbDrawScrollableSmallText(jjCANVAS@ canvas, DbWindow@ window, int x, int y, int w, int maxY, const string &in text, int scrollLine, bool scrollButtons = true, int scrollUpAction = DB_ACTION_SCROLL_UP, int scrollDownAction = DB_ACTION_SCROLL_DOWN, int lineHeight = 14)
{
	int textW = scrollButtons ? w - 62 : w;
	if(textW < 20) textW = w;
	int visibleHeight = maxY - y;
	if(visibleHeight < lineHeight) visibleHeight = lineHeight;
	int maxScroll = DbWrappedSmallLineCount(text, textW) - DbVisibleSmallLines(visibleHeight, lineHeight);
	if(maxScroll < 0) maxScroll = 0;
	if(scrollLine < 0) scrollLine = 0;
	if(scrollLine > maxScroll) scrollLine = maxScroll;
	if(scrollButtons && maxScroll > 0)
	{
		if(scrollLine > 0) DbDrawButton(canvas, window, x + w - 46, y, 42, 14, scrollUpAction, 0, -1, "UP");
		if(scrollLine < maxScroll) DbDrawButton(canvas, window, x + w - 58, maxY - 12, 54, 14, scrollDownAction, 0, 1, "DOWN");
	}
	DbDrawWrappedSmallScrolled(canvas, x, y, text, textW, maxY, scrollLine, lineHeight);
	return scrollLine;
}

int DbHandleScrollResult(DbResult@ result, int scrollLine, int scrollUpAction = DB_ACTION_SCROLL_UP, int scrollDownAction = DB_ACTION_SCROLL_DOWN)
{
	if(result.action == scrollUpAction)
	{
		scrollLine--;
		return scrollLine;
	}
	if(result.action == scrollDownAction)
	{
		scrollLine++;
		return scrollLine;
	}
	return scrollLine;
}

bool DbIsScrollResult(DbResult@ result, int scrollUpAction = DB_ACTION_SCROLL_UP, int scrollDownAction = DB_ACTION_SCROLL_DOWN)
{
	return result.action == scrollUpAction || result.action == scrollDownAction;
}

// EDITABLE boxes

int DbClampTextCursor(const string &in text, int cursor)
{
	if(cursor < 0) return 0;
	if(cursor > int(text.length)) return int(text.length);
	return cursor;
}

string DbInsertTextAtCursor(const string &in text, int cursor, const string &in insert)
{
	cursor = DbClampTextCursor(text, cursor);
	return text.substr(0, cursor) + insert + text.substr(cursor);
}

string DbBackspaceTextAtCursor(const string &in text, int cursor)
{
	cursor = DbClampTextCursor(text, cursor);
	if(cursor <= 0) return text;
	return text.substr(0, cursor - 1) + text.substr(cursor);
}

int DbMoveTextCursor(const string &in text, int cursor, int delta)
{
	return DbClampTextCursor(text, cursor + delta);
}

int DbEditableCursorLine(const string &in text, int maxPixels, int cursor)
{
	array<string> lines = DbWrappedSmallLines(text, maxPixels);
	cursor = DbClampTextCursor(text, cursor);
	int remaining = cursor;
	for(uint i = 0; i < lines.length; i++)
	{
		int lineLen = int(lines[i].length);
		if(remaining <= lineLen) return int(i);
		remaining -= lineLen + 1;
	}
	return int(lines.length) - 1;
}

int DbClosestCursorInLine(const string &in line, int targetPixels)
{
	int best = 0;
	int bestDistance = 99999;
	for(int i = 0; i <= int(line.length); i++)
	{
		int width = jjGetStringWidth(line.substr(0, i), STRING::SMALL, STRING::NORMAL);
		int distance = width > targetPixels ? width - targetPixels : targetPixels - width;
		if(distance < bestDistance)
		{
			bestDistance = distance;
			best = i;
		}
	}
	return best;
}

int DbMoveTextCursorVertical(const string &in text, int maxPixels, int cursor, int delta)
{
	array<string> lines = DbWrappedSmallLines(text, maxPixels);
	if(lines.length == 0) return 0;
	cursor = DbClampTextCursor(text, cursor);
	int remaining = cursor;
	int currentLine = 0;
	int currentColumn = 0;
	for(uint i = 0; i < lines.length; i++)
	{
		int lineLen = int(lines[i].length);
		if(remaining <= lineLen || i + 1 == lines.length)
		{
			currentLine = int(i);
			currentColumn = remaining < lineLen ? remaining : lineLen;
			break;
		}
		remaining -= lineLen + 1;
	}
	int targetLine = currentLine + delta;
	if(targetLine < 0) targetLine = 0;
	if(targetLine >= int(lines.length)) targetLine = int(lines.length) - 1;
	int targetPixels = jjGetStringWidth(lines[currentLine].substr(0, currentColumn), STRING::SMALL, STRING::NORMAL);
	int targetColumn = DbClosestCursorInLine(lines[targetLine], targetPixels);
	int targetCursor = 0;
	for(int i = 0; i < targetLine; i++)
		targetCursor += int(lines[i].length) + 1;
	targetCursor += targetColumn;
	return DbClampTextCursor(text, targetCursor);
}

int DbClampEditableScrollToCursor(const string &in text, int maxPixels, int pixelHeight, int scrollLine, int cursor, int lineHeight = 14)
{
	int cursorLine = DbEditableCursorLine(text, maxPixels, cursor);
	int visibleLines = DbVisibleSmallLines(pixelHeight, lineHeight);
	if(cursorLine < scrollLine) scrollLine = cursorLine;
	if(cursorLine >= scrollLine + visibleLines) scrollLine = cursorLine - visibleLines + 1;
	return DbClampSmallScroll(text, maxPixels, pixelHeight, scrollLine, lineHeight);
}

int DbSmallSpaceWidth()
{
	int width = jjGetStringWidth("x x", STRING::SMALL, STRING::NORMAL) - jjGetStringWidth("xx", STRING::SMALL, STRING::NORMAL);
	return width > 0 ? width : 4;
}

int DbSmallTextWidthPreserveTrailingSpaces(const string &in text)
{
	int trailingSpaces = 0;
	for(int i = int(text.length) - 1; i >= 0 && text[i] == 32; i--)
		trailingSpaces++;
	if(trailingSpaces <= 0) return jjGetStringWidth(text, STRING::SMALL, STRING::NORMAL);
	return jjGetStringWidth(text.substr(0, int(text.length) - trailingSpaces), STRING::SMALL, STRING::NORMAL) + trailingSpaces * DbSmallSpaceWidth();
}

void DbDrawEditableSmallCursor(jjCANVAS@ canvas, int x, int y, const string &in text, int maxPixels, int maxY, int cursor, int scrollLine, int lineHeight = 14, int color = 80)
{
	array<string> lines = DbWrappedSmallLines(text, maxPixels);
	cursor = DbClampTextCursor(text, cursor);
	int remaining = cursor;
	for(uint i = 0; i < lines.length; i++)
	{
		int lineLen = int(lines[i].length);
		if(remaining <= lineLen || i + 1 == lines.length)
		{
			if(int(i) < scrollLine) return;
			int cursorY = y + (int(i) - scrollLine) * lineHeight - 5;
			if(cursorY + lineHeight > maxY) return;
			int cursorChars = remaining < lineLen ? remaining : lineLen;
			int extraSpaces = remaining > lineLen ? remaining - lineLen : 0;
			int cursorX = x + DbSmallTextWidthPreserveTrailingSpaces(lines[i].substr(0, cursorChars)) + extraSpaces * DbSmallSpaceWidth();
			canvas.drawRectangle(cursorX, cursorY - 1, 2, lineHeight, color, SPRITE::NORMAL);
			return;
		}
		remaining -= lineLen + 1;
		if(remaining < 0) remaining = 0;
	}
	canvas.drawRectangle(x, y, 2, lineHeight, color, SPRITE::NORMAL);
}

string DbClipSmall(const string &in text, int maxPixels)
{
	return text;
}

void DbDrawFrame(jjCANVAS@ canvas, DbWindow@ window)
{
	canvas.drawRectangle(window.x, window.y, window.w, window.h, 0, SPRITE::SHADOW);
	canvas.drawRectangle(window.x + 2, window.y + 2, window.w - 4, window.h - 4, window.backgroundColor, SPRITE::TRANSLUCENT);
	if(window.titleBar)
	{
		canvas.drawRectangle(window.x + 2, window.y + 2, window.w - 4, window.titleHeight - 3, window.titleColor, SPRITE::NORMAL);
		canvas.drawString(window.x + window.pad, window.y + 10, window.title, STRING::SMALL, STRING::NORMAL);
	}
	else canvas.drawString(window.x + window.pad, window.y + window.pad, window.title, STRING::MEDIUM, STRING::NORMAL);
	if(window.closeButton)
		DbDrawButton(canvas, window, window.x + window.w - 24, window.y + (window.titleBar ? 8 : window.pad), 18, 15, DB_ACTION_CLOSE, 0, 0, "X");
}

void DbDrawFocus(jjCANVAS@ canvas, DbWindow@ window, int focusOffsetY = DB_FOCUS_OFFSET_Y, int focusHeight = 0)
{
	DbNormalizeFocus(window);
	if(window.focusIndex < 0 || window.focusIndex >= int(window.controls.length)) return;
	DbControl control = window.controls[window.focusIndex];
	if(!control.enabled) return;
	int h = focusHeight > 0 ? focusHeight : control.h;
	canvas.drawRectangle(control.x - 3, control.y + focusOffsetY, control.w + 6, h, window.focusColor, SPRITE::TRANSLUCENT);
}

void DbDrawButton(jjCANVAS@ canvas, DbWindow@ window, int x, int y, int w, int h, int action, int a, int b, const string &in text, bool enabled = true, bool selected = false)
{
	string shown = enabled ? text : "||" + text;
	int drawH = h;
	int wrappedH = DbWrappedSmallLineCount(shown, w) * 14;
	if(wrappedH > drawH) drawH = wrappedH;
	DbAddControl(window, x, y, w, drawH, action, a, b, text, enabled);
	if(selected)
		canvas.drawRectangle(x - 3, y + DB_FOCUS_OFFSET_Y, w + 6, drawH, 24, SPRITE::TRANSLUCENT);
	DbDrawWrappedSmall(canvas, x, y, shown, w, y + drawH);
}

void DbDrawTab(jjCANVAS@ canvas, DbWindow@ window, int x, int y, int w, int tab, const string &in text, bool enabled = true)
{
	string shown = window.activeTab == tab ? "|||" + text : (enabled ? "||||" + text : "||" + text);
	DbDrawButton(canvas, window, x, y, w, 16, DB_ACTION_TAB, tab, 0, shown, enabled, window.activeTab == tab);
}

void DbDrawStepper(jjCANVAS@ canvas, DbWindow@ window, int x, int y, int textW, int action, int a, int prevB, int nextB, const string &in text)
{
	DbDrawButton(canvas, window, x, y, 16, 16, action, a, prevB, "<");
	DbDrawWrappedSmall(canvas, x + 22, y, text, textW, y + DbWrappedSmallLineCount(text, textW) * 14);
	DbDrawButton(canvas, window, x + 26 + textW, y, 16, 16, action, a, nextB, ">");
}

void DbContentRect(DbWindow@ window, int &out x, int &out y, int &out w, int &out h)
{
	x = window.x + window.pad;
	y = window.y + (window.titleBar ? window.titleHeight + window.pad : window.pad + 20);
	w = window.w - window.pad * 2;
	h = window.h - (y - window.y) - window.pad;
}

bool DbPointIn(int tx, int ty, int x, int y, int w, int h)
{
	return tx >= x && tx <= x + w && ty >= y && ty <= y + h;
}

int DbHitControl(DbWindow@ window, int tx, int ty)
{
	for(int i = int(window.controls.length) - 1; i >= 0; i--)
	{
		DbControl control = window.controls[i];
		if(control.enabled && DbPointIn(tx, ty, control.x, control.y, control.w, control.h)) return i;
	}
	return -1;
}

void DbActivateControl(DbWindow@ window, int index, DbResult@ result)
{
	if(index < 0 || index >= int(window.controls.length)) return;
	DbControl control = window.controls[index];
	if(!control.enabled) return;
	result.activated = true;
	result.action = control.action;
	result.a = control.a;
	result.b = control.b;
	if(control.action == DB_ACTION_CLOSE) result.closeRequested = true;
	else if(control.action == DB_ACTION_TAB)
	{
		window.activeTab = control.a;
		result.tabChanged = true;
		result.tab = control.a;
	}
}

bool DbProcessMouse(DbWindow@ window, DbResult@ result)
{
	bool mouseDown = jjKey[0x01];
	int tx = jjMouseX;
	int ty = jjMouseY + 8;
	bool clicked = mouseDown && !window.mouseWasDown && jjGameTicks > window.lastClickTick + 10;
	if(!mouseDown)
	{
		if(window.dragging)
		{
			result.positionChanged = true;
			DbRememberPosition(window);
		}
		window.mouseWasDown = false;
		window.dragging = false;
	}
	else window.mouseWasDown = true;
	if(window.dragging)
	{
		window.x = tx - window.dragOffsetX;
		window.y = ty - window.dragOffsetY;
		DbClampToScreen(window);
		result.positionChanged = true;
		return true;
	}
	if(!clicked) return false;
	int controlIndex = DbHitControl(window, tx, ty);
	if(controlIndex >= 0)
	{
		window.focusIndex = controlIndex;
		window.lastClickTick = jjGameTicks;
		DbActivateControl(window, controlIndex, result);
		return true;
	}
	if(window.draggable && window.titleBar && DbPointIn(tx, ty, window.x + 2, window.y + 2, window.w - 4, window.titleHeight - 3))
	{
		window.dragging = true;
		window.dragOffsetX = tx - window.x;
		window.dragOffsetY = ty - window.y;
		window.lastClickTick = jjGameTicks;
		return true;
	}
	return false;
}

void DbNormalizeFocus(DbWindow@ window)
{
	int count = int(window.controls.length);
	if(count <= 0) { window.focusIndex = 0; return; }
	while(window.focusIndex < 0) window.focusIndex += count;
	while(window.focusIndex >= count) window.focusIndex -= count;
	for(int attempts = 0; attempts < count && !window.controls[window.focusIndex].enabled; attempts++)
	{
		window.focusIndex++;
		if(window.focusIndex >= count) window.focusIndex = 0;
	}
}

void DbMoveFocus(DbWindow@ window, int delta)
{
	if(window.controls.length == 0) return;
	window.focusIndex += delta;
	DbNormalizeFocus(window);
}

bool DbProcessKeyboard(DbWindow@ window, jjPLAYER@ play, DbResult@ result, int repeatTicks = 10)
{
	if(jjGameTicks - window.lastKeyTick < repeatTicks) return false;
	if(play.keyFire || play.keySelect || jjKey[0x0D])
	{
		window.lastKeyTick = jjGameTicks;
		DbNormalizeFocus(window);
		DbActivateControl(window, window.focusIndex, result);
		return true;
	}
	if(play.keyUp || play.keyLeft || jjKey[0x26] || jjKey[0x25])
	{
		window.lastKeyTick = jjGameTicks;
		DbMoveFocus(window, -1);
		return true;
	}
	if(play.keyDown || play.keyRight || jjKey[0x28] || jjKey[0x27])
	{
		window.lastKeyTick = jjGameTicks;
		DbMoveFocus(window, 1);
		return true;
	}
	return false;
}

void DbResetResult(DbResult@ result)
{
	result.activated = false;
	result.closeRequested = false;
	result.tabChanged = false;
	result.positionChanged = false;
	result.action = DB_ACTION_NONE;
	result.a = 0;
	result.b = 0;
	result.tab = 0;
}



// ===== CharactersModAnim.asc =====
// Generated from charactersMod.j2a animation-index data.
// Each entry maps (source set, original animation, frame) to the
// merged animation ID and relative frame offset after jjANIMSET::load().
namespace CharactersModAnim {
	array<int> SetOffsets = {
		0, 18, 25, 32, 39, 43, -1, 44,
		49, 54, 60, 66
	};

	array<int> AnimCounts = {
		18, 7, 7, 7, 4, 1, 0, 5,
		5, 6, 6, 7
	};

	array<int> MergedAnims = {
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
		0
	};

	array<int> StartFrames = {
		0, 10, 47, 84, 121, 158, 195, 232,
		269, 306, 343, 355, 367, 368, 369, 406,
		443, 444, 0, 5, 45, 65, 80, 140,
		150, 0, 25, 65, 85, 100, 160, 170,
		0, 32, 64, 96, 108, 160, 168, 0,
		2, 10, 18, 0, 0, 25, 65, 85,
		100, 0, 25, 65, 85, 100, 0, 50,
		110, 135, 145, 155, 0, 5, 35, 55,
		60, 70, 0, 20, 60, 100, 105, 145,
		170
	};

	array<int> FrameCounts = {
		10, 37, 37, 37, 37, 37, 37, 37,
		37, 37, 12, 12, 1, 1, 37, 37,
		1, 1, 5, 40, 20, 15, 60, 10,
		10, 25, 40, 20, 15, 60, 10, 10,
		32, 32, 32, 12, 52, 8, 8, 2,
		8, 8, 14, 32, 25, 40, 20, 15,
		15, 25, 40, 20, 15, 15, 50, 60,
		25, 10, 10, 30, 5, 30, 20, 5,
		10, 10, 20, 40, 40, 5, 40, 25,
		40
	};

	int Index(int sourceSet, int originalAnim) {
		if(sourceSet < 0 || sourceSet >= int(SetOffsets.length))
			return -1;
		if(originalAnim < 0 || originalAnim >= AnimCounts[sourceSet])
			return -1;
		return SetOffsets[sourceSet] + originalAnim;
	}

	int AnimCount(int sourceSet) {
		if(sourceSet < 0 || sourceSet >= int(AnimCounts.length))
			return 0;
		return AnimCounts[sourceSet];
	}

	int MergedAnim(int sourceSet, int originalAnim) {
		int index = Index(sourceSet, originalAnim);
		return index < 0 ? 0 : MergedAnims[index];
	}

	int StartFrame(int sourceSet, int originalAnim) {
		int index = Index(sourceSet, originalAnim);
		return index < 0 ? 0 : StartFrames[index];
	}

	int FrameCount(int sourceSet, int originalAnim) {
		int index = Index(sourceSet, originalAnim);
		return index < 0 ? 1 : FrameCounts[index];
	}

	int Frame(int sourceSet, int originalAnim, int frameID) {
		int index = Index(sourceSet, originalAnim);
		if(index < 0)
			return frameID;
		int frameCount = FrameCounts[index];
		if(frameCount <= 0)
			return StartFrames[index];
		return StartFrames[index] + frameID % frameCount;
	}
}


// ===== npcBunniesCore.asc =====
#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

}


// ===== npcBunniesAnimations.asc =====

namespace npcBunniesAnimations
{
array<uint> emptyCharSet;
array<int> animTypes;
void FurSetup_AdditionalCharacters(int type)
{
	if(type != 0) emptyCharSet[type] = emptyCharSet[type-1] + 1;
	while (jjAnimSets[ANIM::CUSTOM[emptyCharSet[type]]].firstAnim != 0)
	{
		++emptyCharSet[type];
	}

	if(type == npcBunniesCore::NpcAnim::DEVAN)
	{
		jjAnimSets[ANIM::DEVILDEVAN].load(); //load Devan
		animTypes[type] = ANIM::DEVILDEVAN;
	}
	else if(type == npcBunniesCore::NpcAnim::EVA)
	{
		jjAnimSets[ANIM::EVA].load();
		jjAnimSets[ANIM::FLAG].load();
		animTypes[type] = ANIM::EVA;
	}
	else
	{
		jjANIMSET@ emptyCustomAnim = jjAnimSets[ANIM::CUSTOM[emptyCharSet[type]]];
		emptyCustomAnim.load(type, "charactersMod.j2a");

		animTypes[type] = ANIM::CUSTOM[emptyCharSet[type]];
		RegisterCharactersModAnimSet(animTypes[type], type);
	}
}

//to use as index for customMenu[] characters: the big characters in MenuAnimsMod.j2a
array<int> charFind = { 0, CHAR::JAZZ, CHAR::SPAZ, CHAR::LORI, CHAR::BIRD, CHAR::FROG, CHAR::BIRD2 };
//the ANIM::FACES order in JazzSD.exe (index 39 in 1.23 and 40 in 1.24). For 1.23, this is initialized without LORI in onLevelLoad()
array<int> charFaces = { CHAR::BIRD, CHAR::BIRD2, CHAR::FROG, CHAR::JAZZ, CHAR::LORI, CHAR::SPAZ };

array<int> npcFaces = {0, npcBunniesCore::NpcAnim::LORI, npcBunniesCore::NpcAnim::LORI, npcBunniesCore::NpcAnim::MARIO, npcBunniesCore::NpcAnim::FROG, npcBunniesCore::NpcAnim::BIRD, npcBunniesCore::NpcAnim::BIRD, npcBunniesCore::NpcAnim::BIRD,
	npcBunniesCore::NpcAnim::SONIC, npcBunniesCore::NpcAnim::SONIC, npcBunniesCore::NpcAnim::TAILS, npcBunniesCore::NpcAnim::TAILS, npcBunniesCore::NpcAnim::JJ1, npcBunniesCore::NpcAnim::JJ1, npcBunniesCore::NpcAnim::DEVAN, npcBunniesCore::NpcAnim::DEVAN,
	npcBunniesCore::NpcAnim::YOSHI, npcBunniesCore::NpcAnim::YOSHI
};
array<int> npcFacesDefault = {npcBunniesCore::NpcAnim::BIRD, npcBunniesCore::NpcAnim::BIRD, npcBunniesCore::NpcAnim::FROG, npcBunniesCore::NpcAnim::JAZZ, npcBunniesCore::NpcAnim::LORI, npcBunniesCore::NpcAnim::SPAZ };
array<int> customMenu(5,0); //index 0 will have nothing. Starts at 1 with Jazz
void loadMenuAnims()
{
	if(!jjIsTSF)
	{
		charFaces = { CHAR::BIRD, CHAR::BIRD2, CHAR::FROG, CHAR::JAZZ, CHAR::SPAZ };
		npcFacesDefault = {npcBunniesCore::NpcAnim::BIRD, npcBunniesCore::NpcAnim::BIRD, npcBunniesCore::NpcAnim::FROG, npcBunniesCore::NpcAnim::JAZZ, npcBunniesCore::NpcAnim::SPAZ };
	}

	for(uint i=0; i<3; i++)
	{
		if(i!=0) customMenu[i+1] = customMenu[i] + 1;
		while (jjAnimSets[ANIM::CUSTOM[customMenu[i+1]]].firstAnim != 0) //find an unused slot in ANIM::CUSTOM
			++customMenu[i+1];
		jjANIMSET@ animMenu = jjAnimSets[ANIM::CUSTOM[customMenu[i+1]]];
		animMenu.load(i, "MenuAnimsMod.j2a");
	}
}

void LoadNpcUiIconAnimSets()
{
	jjAnimSets[ANIM::BAT].load();
	jjAnimSets[ANIM::BUMBEE].load();
	jjAnimSets[ANIM::BEEBOY].load();
	jjAnimSets[ANIM::BEES].load();
	jjAnimSets[ANIM::BUTTERFLY].load();
	jjAnimSets[ANIM::DEMON].load();
	jjAnimSets[ANIM::DOG].load();
	jjAnimSets[ANIM::DRAGON].load();
	jjAnimSets[ANIM::DRAGFLY].load();
	jjAnimSets[ANIM::FATCHK].load();
	jjAnimSets[ANIM::FENCER].load();
	jjAnimSets[ANIM::FISH].load();
	jjAnimSets[ANIM::HATTER].load();
	jjAnimSets[ANIM::HELMUT].load();
	jjAnimSets[ANIM::LABRAT].load();
	jjAnimSets[ANIM::LIZARD].load();
	jjAnimSets[ANIM::XLIZARD].load();
	jjAnimSets[ANIM::MONKEY].load();
	jjAnimSets[ANIM::TURTLE].load();
	jjAnimSets[ANIM::XTURTLE].load();
	jjAnimSets[ANIM::TUBETURT].load();
	jjAnimSets[ANIM::TUFTUR].load();
	jjAnimSets[ANIM::RAPIER].load();
	jjAnimSets[ANIM::RAVEN].load();
	jjAnimSets[ANIM::SKELETON].load();
	jjAnimSets[ANIM::SPARK].load();
	jjAnimSets[ANIM::SUCKER].load();
	jjAnimSets[ANIM::CAT].load();
	jjAnimSets[ANIM::CAT2].load();
	jjAnimSets[ANIM::MOTH].load();
	jjAnimSets[ANIM::WITCH].load();
	jjAnimSets[ANIM::BILSBOSS].load();
	jjAnimSets[ANIM::XBILSY].load();
	jjAnimSets[ANIM::BOSS].load();
	jjAnimSets[ANIM::BUBBA].load();
	jjAnimSets[ANIM::DEVAN].load();
	jjAnimSets[ANIM::DEVILDEVAN].load();
	jjAnimSets[ANIM::EVA].load();
	jjAnimSets[ANIM::QUEEN].load();
	jjAnimSets[ANIM::ROBOT].load();
	jjAnimSets[ANIM::ROCKTURT].load();
	jjAnimSets[ANIM::TUFBOSS].load();
	jjAnimSets[ANIM::TWEEDLE].load();
	jjAnimSets[ANIM::UTERUS].load();
}

class DrawParams
{
	float xPos;
	float yPos;
	int animSet;
	int anim;
	int frame = 0;
	float sizeX = 1;
	float sizeY = 1;
	SPRITE::Mode spriteMode;
	int param;
	int grav;
	int animation;

	void add(float xPos, float yPos, int animSet, int anim, int frame, float sizeX, float sizeY, SPRITE::Mode spriteMode, int param, int grav, int animation = -1)
	{
		this.xPos = xPos;
		this.yPos = yPos;
		this.animSet = animSet;
		this.anim = anim;
		this.frame = frame;
		this.sizeX = sizeX;
		this.sizeY = sizeY;
		this.spriteMode = spriteMode;
		this.param = param;
		this.grav = grav;
		this.animation = animation;
	}
}
bool charactersModAvailable = false;
array<array<int>> charactersModAnimMerged;
array<array<int>> charactersModAnimStart;
array<array<int>> charactersModAnimCount;
array<int> charactersModSetByAnimSet(4096, -1);

void InitializeCharactersModTables()
{
	charactersModAnimMerged.resize(16);
	charactersModAnimStart.resize(16);
	charactersModAnimCount.resize(16);
	for(uint i = 0; i < charactersModAnimStart.length; i++)
	{
		charactersModAnimMerged[i].resize(128);
		charactersModAnimStart[i].resize(128);
		charactersModAnimCount[i].resize(128);
		for(uint j = 0; j < 128; j++)
		{
			charactersModAnimMerged[i][j] = -1;
			charactersModAnimStart[i][j] = -1;
			charactersModAnimCount[i][j] = 0;
		}
	}
}

bool LoadCharactersModAnimIndexes()
{
	InitializeCharactersModTables();

	// CharactersModAnim.asc replaces the text table. Keep these local arrays
	// because the rest of this file already uses them for drawing.
	for(int sourceSet = 0; sourceSet < int(charactersModAnimStart.length); sourceSet++)
	{
		int animCount = CharactersModAnim::AnimCount(sourceSet);
		if(animCount > int(charactersModAnimStart[sourceSet].length))
			animCount = int(charactersModAnimStart[sourceSet].length);

		for(int anim = 0; anim < animCount; anim++)
		{
			charactersModAnimMerged[sourceSet][anim] = CharactersModAnim::MergedAnim(sourceSet, anim);
			charactersModAnimStart[sourceSet][anim] = CharactersModAnim::StartFrame(sourceSet, anim);
			charactersModAnimCount[sourceSet][anim] = CharactersModAnim::FrameCount(sourceSet, anim);
		}
	}

	return true;
}

void LoadCharactersModAnimationIndexes()
{
	charactersModAvailable = LoadCharactersModAnimIndexes();
}

void RegisterCharactersModAnimSet(int animSet, int sourceSet)
{
	if(animSet < 0 || sourceSet < 0) return;
	if(animSet >= int(charactersModSetByAnimSet.length))
	{
		uint oldLength = charactersModSetByAnimSet.length;
		charactersModSetByAnimSet.resize(animSet + 1);
		for(uint i = oldLength; i < charactersModSetByAnimSet.length; i++) charactersModSetByAnimSet[i] = -1;
	}
	charactersModSetByAnimSet[animSet] = sourceSet;
}

int CharactersModSourceSetForAnimSet(int animSet)
{
	if(animSet >= 0 && animSet < int(charactersModSetByAnimSet.length)) return charactersModSetByAnimSet[animSet];
	return -1;
}

int CharactersModSourceSetForDraw(DrawParams &in draw)
{
	if(draw.animation == -2) return -1;
	if(draw.animation >= 0) return draw.animation;
	return CharactersModSourceSetForAnimSet(draw.animSet);
}

int CharactersModCurFrame(int animSet, int anim, int frame)
{
	return jjAnimations[jjAnimSets[animSet].firstAnim + anim].firstFrame + frame;
}

bool ApplyCharactersModFrame(DrawParams &inout draw)
{
	if(!charactersModAvailable) return true;
	int sourceSet = CharactersModSourceSetForDraw(draw);
	if(sourceSet < 0) return true;
	if(sourceSet >= int(charactersModAnimStart.length)) return false;
	if(draw.anim < 0 || draw.anim >= int(charactersModAnimStart[sourceSet].length)) return false;
	int frameCount = charactersModAnimCount[sourceSet][draw.anim];
	if(frameCount <= 0) return false;
	int mergedAnim = charactersModAnimMerged[sourceSet][draw.anim];
	if(mergedAnim < 0) return false;
	int remappedFrame = draw.frame % frameCount;
	if(remappedFrame < 0) remappedFrame += frameCount;
	draw.frame = charactersModAnimStart[sourceSet][draw.anim] + remappedFrame;
	draw.anim = mergedAnim;
	draw.animation = -2;
	return true;
}

bool ApplyMergedCharacterLayer(DrawParams &inout draw, int division, int layer)
{
	int sourceSet = charactersModAvailable ? CharactersModSourceSetForDraw(draw) : -1;
	if(sourceSet < 0)
	{
		draw.frame = GetMergedAnimationFrame(draw.animSet, draw.anim, draw.frame, division, layer);
		return true;
	}
	if(sourceSet >= int(charactersModAnimStart.length)) return false;
	if(draw.anim < 0 || draw.anim >= int(charactersModAnimStart[sourceSet].length)) return false;
	int frameCount = charactersModAnimCount[sourceSet][draw.anim];
	if(frameCount <= 0) return false;
	int mergedAnim = charactersModAnimMerged[sourceSet][draw.anim];
	if(mergedAnim < 0) return false;
	if(division <= 1) division = 1;
	if(layer < 0) layer = 0;
	if(layer >= division) layer = division - 1;
	int layerFrameCount = frameCount / division;
	if(layerFrameCount <= 0) return false;
	int frameInLayer = draw.frame % layerFrameCount;
	if(frameInLayer < 0) frameInLayer += layerFrameCount;
	draw.frame = charactersModAnimStart[sourceSet][draw.anim] + layer * layerFrameCount + frameInLayer;
	draw.anim = mergedAnim;
	draw.animation = -2;
	return true;
}
int GetMergedAnimationFrame(int animSet, int anim, int frame, int division, int layer)
{
	if(division <= 1) return frame;
	if(layer < 0) layer = 0;
	if(layer >= division) layer = division - 1;

	int animationIndex = jjAnimSets[animSet].firstAnim + anim;
	int frameCount = jjAnimations[animationIndex].frameCount;
	int layerFrameCount = frameCount / division;
	if(layerFrameCount <= 0) return frame;

	int frameInLayer = frame % layerFrameCount;
	if(frameInLayer < 0) frameInLayer += layerFrameCount;
	return layer * layerFrameCount + frameInLayer;
}

void DrawMergedCharacterLayer(DrawParams draw, int division, int layer)
{
	bool useCurFrame = charactersModAvailable && CharactersModSourceSetForDraw(draw) >= 0;
	if(!ApplyMergedCharacterLayer(draw, division, layer)) return;
	if(useCurFrame)
		jjDrawResizedSpriteFromCurFrame(draw.xPos, draw.yPos, CharactersModCurFrame(draw.animSet, draw.anim, draw.frame), draw.sizeX, draw.sizeY, draw.spriteMode, draw.param, 4, 4, -1);
	else
		DrawCharacter(draw);
}

void DrawMergedCharacterLayerOnCanvas(jjCANVAS@ canvas, DrawParams draw, int division, int layer)
{
	if(!ApplyMergedCharacterLayer(draw, division, layer)) return;
	DrawCharacterOnCanvas(canvas, draw);
}

void DrawCharacter(DrawParams draw)
{
	bool useCurFrame = charactersModAvailable && CharactersModSourceSetForDraw(draw) >= 0;
	if(!ApplyCharactersModFrame(draw)) return;
	if(useCurFrame)
		jjDrawResizedSpriteFromCurFrame(draw.xPos, draw.yPos, CharactersModCurFrame(draw.animSet, draw.anim, draw.frame), draw.sizeX, draw.sizeY, draw.spriteMode, draw.param, 4, 4, -1);
	else
		jjDrawResizedSprite(draw.xPos, draw.yPos, draw.animSet, draw.anim, draw.frame, draw.sizeX, draw.sizeY, draw.spriteMode, draw.param, 4, 4, -1);
}

void DrawCharacterOnCanvas(jjCANVAS@ canvas, DrawParams draw)
{
	bool useCurFrame = charactersModAvailable && CharactersModSourceSetForDraw(draw) >= 0;
	if(!ApplyCharactersModFrame(draw)) return;
	if(useCurFrame)
		canvas.drawResizedSpriteFromCurFrame(int(draw.xPos), int(draw.yPos), CharactersModCurFrame(draw.animSet, draw.anim, draw.frame), draw.sizeX, draw.sizeY, draw.spriteMode, draw.param);
	else
		canvas.drawResizedSprite(int(draw.xPos), int(draw.yPos), draw.animSet, draw.anim, draw.frame, draw.sizeX, draw.sizeY, draw.spriteMode, draw.param);
}

int getTypeFromTypeName(string typeName)
{
	bool explicitBossType = int(typeName.findFirst("Boss:")) >= 0 || int(typeName.findFirst("npcBunniesAnimations::NpcBoss::")) >= 0;
	int namespaceSep = int(typeName.findFirst("::"));
	if(namespaceSep >= 0 && namespaceSep + 2 < int(typeName.length))
	{
		typeName = typeName.substr(namespaceSep + 2, typeName.length - namespaceSep - 2);
	}
	else
	{
		int colon = int(typeName.findFirst(":"));
		if(colon >= 0 && colon + 1 < int(typeName.length)) typeName = typeName.substr(colon + 1, typeName.length - colon - 1);
	}
	while(typeName.length > 0 && typeName[0] == 32) typeName = typeName.substr(1, typeName.length - 1);
	if(explicitBossType)
	{
		if(jjRegexMatch(typeName, "XmasBilsy", true)) return npcBunniesAnimations::NpcBoss::XmasBilsy;
		if(jjRegexMatch(typeName, "Bilsy", true)) return npcBunniesAnimations::NpcBoss::Bilsy;
		if(jjRegexMatch(typeName, "Bolly", true)) return npcBunniesAnimations::NpcBoss::Bolly;
		if(jjRegexMatch(typeName, "Bubba", true)) return npcBunniesAnimations::NpcBoss::Bubba;
		if(jjRegexMatch(typeName, "DevilDevan", true)) return npcBunniesAnimations::NpcBoss::DevilDevan;
		if(jjRegexMatch(typeName, "Queen", true)) return npcBunniesAnimations::NpcBoss::Queen;
		if(jjRegexMatch(typeName, "Robot", true)) return npcBunniesAnimations::NpcBoss::Robot;
		if(jjRegexMatch(typeName, "RocketTurtle", true)) return npcBunniesAnimations::NpcBoss::RocketTurtle;
		if(jjRegexMatch(typeName, "TufBoss", true)) return npcBunniesAnimations::NpcBoss::TufBoss;
		if(jjRegexMatch(typeName, "Tweedle", true)) return npcBunniesAnimations::NpcBoss::Tweedle;
		if(jjRegexMatch(typeName, "Uterus", true)) return npcBunniesAnimations::NpcBoss::Uterus;
	}
		 if(parseInt(typeName) == npcBunniesCore::NpcAnim::JAZZ  || jjRegexMatch(typeName, "Jazz", true)) 	return ANIM::CUSTOM[emptyCharSet[npcBunniesCore::NpcAnim::JAZZ]];
	else if(parseInt(typeName) == npcBunniesCore::NpcAnim::SPAZ  || jjRegexMatch(typeName, "Spaz", true)) 	return ANIM::CUSTOM[emptyCharSet[npcBunniesCore::NpcAnim::SPAZ]];
	else if(parseInt(typeName) == npcBunniesCore::NpcAnim::LORI  || jjRegexMatch(typeName, "Lori", true)) 	return ANIM::CUSTOM[emptyCharSet[npcBunniesCore::NpcAnim::LORI]];
	else if(parseInt(typeName) == npcBunniesCore::NpcAnim::FROG  || jjRegexMatch(typeName, "Frog", true)) 	return npcBunniesCore::NpcAnim::FROG;
	else if(parseInt(typeName) == npcBunniesCore::NpcAnim::BIRD  || jjRegexMatch(typeName, "Bird", true)) 	return npcBunniesCore::NpcAnim::BIRD;
	else if(parseInt(typeName) == npcBunniesCore::NpcAnim::DEVAN || jjRegexMatch(typeName, "Devan", true)) 	return ANIM::DEVILDEVAN;
	else if(parseInt(typeName) == npcBunniesCore::NpcAnim::MARIO || jjRegexMatch(typeName, "Mario", true)) 	return ANIM::CUSTOM[emptyCharSet[npcBunniesCore::NpcAnim::MARIO]];
	else if(parseInt(typeName) == npcBunniesCore::NpcAnim::SONIC || jjRegexMatch(typeName, "Sonic", true)) 	return ANIM::CUSTOM[emptyCharSet[npcBunniesCore::NpcAnim::SONIC]];
	else if(parseInt(typeName) == npcBunniesCore::NpcAnim::TAILS || jjRegexMatch(typeName, "Tails", true)) 	return ANIM::CUSTOM[emptyCharSet[npcBunniesCore::NpcAnim::TAILS]];
	else if(parseInt(typeName) == npcBunniesCore::NpcAnim::JJ1   || jjRegexMatch(typeName, "JJ1", true)) 	return ANIM::CUSTOM[emptyCharSet[npcBunniesCore::NpcAnim::JJ1]];
	else if(parseInt(typeName) == npcBunniesCore::NpcAnim::YOSHI || jjRegexMatch(typeName, "Yoshi", true)) 	return ANIM::CUSTOM[emptyCharSet[npcBunniesCore::NpcAnim::YOSHI]];
	else if(parseInt(typeName) == npcBunniesCore::NpcAnim::EVA   || jjRegexMatch(typeName, "Eva", true)) 	return ANIM::EVA;
		else if(jjRegexMatch(typeName, "Static", true)) return npcBunniesAnimations::NpcStatic::Static;
	else if(jjRegexMatch(typeName, "Bat", true)) return npcBunniesAnimations::NpcMonster::Bat;
	else if(jjRegexMatch(typeName, "BeeBoy", true)) return npcBunniesAnimations::NpcMonster::BeeBoy;
	else if(jjRegexMatch(typeName, "Bees", true)) return npcBunniesAnimations::NpcMonster::Bees;
	else if(jjRegexMatch(typeName, "Bumbee", true) || jjRegexMatch(typeName, "Bee", true)) return npcBunniesAnimations::NpcMonster::Bee;
	else if(jjRegexMatch(typeName, "Butterfly", true)) return npcBunniesAnimations::NpcMonster::Butterfly;
	else if(jjRegexMatch(typeName, "Crab", true)) return npcBunniesAnimations::NpcMonster::Crab;
	else if(jjRegexMatch(typeName, "Demon", true)) return npcBunniesAnimations::NpcMonster::Demon;
	else if(jjRegexMatch(typeName, "DoggyDogg", true) || jjRegexMatch(typeName, "Dog", true)) return npcBunniesAnimations::NpcMonster::DoggyDogg;
	else if(jjRegexMatch(typeName, "Dragfly", true) || jjRegexMatch(typeName, "Dragonfly", true)) return npcBunniesAnimations::NpcMonster::Dragonfly;
	else if(jjRegexMatch(typeName, "Dragon", true)) return npcBunniesAnimations::NpcMonster::Dragon;
	else if(jjRegexMatch(typeName, "Fatchk", true) || jjRegexMatch(typeName, "FatChick", true)) return npcBunniesAnimations::NpcMonster::FatChick;
	else if(jjRegexMatch(typeName, "Fencer", true)) return npcBunniesAnimations::NpcMonster::Fencer;
	else if(jjRegexMatch(typeName, "Fish", true)) return npcBunniesAnimations::NpcMonster::Fish;
	else if(jjRegexMatch(typeName, "Hatter", true)) return npcBunniesAnimations::NpcMonster::Hatter;
	else if(jjRegexMatch(typeName, "Helmut", true)) return npcBunniesAnimations::NpcMonster::Helmut;
	else if(jjRegexMatch(typeName, "LabRat", true)) return npcBunniesAnimations::NpcMonster::LabRat;
	else if(jjRegexMatch(typeName, "XmasFloatLizard", true) || jjRegexMatch(typeName, "XmasFloatLz", true) || jjRegexMatch(typeName, "XmasFloat", true) || jjRegexMatch(typeName, "XmasFlyingLizard", true)) return npcBunniesAnimations::NpcMonster::XmasFloatLizard;
	else if(jjRegexMatch(typeName, "FloatLizard", true) || jjRegexMatch(typeName, "FloatLz", true)) return npcBunniesAnimations::NpcMonster::FloatLizard;
	else if(jjRegexMatch(typeName, "XLizard", true) || jjRegexMatch(typeName, "XmasLizard", true) || jjRegexMatch(typeName, "XmasLz", true)) return npcBunniesAnimations::NpcMonster::XmasLizard;
	else if(jjRegexMatch(typeName, "Lizard", true)) return npcBunniesAnimations::NpcMonster::Lizard;
	else if(jjRegexMatch(typeName, "StandMonkey", true)) return npcBunniesAnimations::NpcMonster::StandMonkey;
	else if(jjRegexMatch(typeName, "Monkey", true)) return npcBunniesAnimations::NpcMonster::Monkey;
	else if(jjRegexMatch(typeName, "XmasNormalTurtle", true) || jjRegexMatch(typeName, "XmasTurt", true)) return npcBunniesAnimations::NpcMonster::XmasNormalTurtle;
	else if(jjRegexMatch(typeName, "TubeTurt", true) || jjRegexMatch(typeName, "TubeTurtle", true)) return npcBunniesAnimations::NpcMonster::TubeTurtle;
	else if(jjRegexMatch(typeName, "TufTurt", true) || jjRegexMatch(typeName, "TufTurtle", true)) return npcBunniesAnimations::NpcMonster::TufTurtle;
	else if(jjRegexMatch(typeName, "NormalTurtle", true) || jjRegexMatch(typeName, "Turtle", true)) return npcBunniesAnimations::NpcMonster::NormalTurtle;
	else if(jjRegexMatch(typeName, "Rapier", true)) return npcBunniesAnimations::NpcMonster::Rapier;
	else if(jjRegexMatch(typeName, "Raven", true)) return npcBunniesAnimations::NpcMonster::Raven;
	else if(jjRegexMatch(typeName, "Skeleton", true)) return npcBunniesAnimations::NpcMonster::Skeleton;
	else if(jjRegexMatch(typeName, "Spark", true)) return npcBunniesAnimations::NpcMonster::Spark;
	else if(jjRegexMatch(typeName, "FloatSucker", true) || jjRegexMatch(typeName, "FloatSuck", true)) return npcBunniesAnimations::NpcMonster::FloatSucker;
	else if(jjRegexMatch(typeName, "Sucker", true)) return npcBunniesAnimations::NpcMonster::Sucker;
	else if(jjRegexMatch(typeName, "Cat2", true) || jjRegexMatch(typeName, "PacmanGhost", true) || jjRegexMatch(typeName, "PacGhost", true)) return npcBunniesAnimations::NpcMonster::PacmanGhost;
	else if(jjRegexMatch(typeName, "Cat", true)) return npcBunniesAnimations::NpcMonster::Cat;
	else if(jjRegexMatch(typeName, "Moth", true)) return npcBunniesAnimations::NpcMonster::Moth;
	else if(jjRegexMatch(typeName, "Witch", true)) return npcBunniesAnimations::NpcMonster::Witch;
	else if(jjRegexMatch(typeName, "XBilsy", true) || jjRegexMatch(typeName, "XmasBilsy", true)) return npcBunniesAnimations::NpcBoss::XmasBilsy;
	else if(jjRegexMatch(typeName, "BilsBoss", true) || jjRegexMatch(typeName, "Bilsy", true)) return npcBunniesAnimations::NpcBoss::Bilsy;
	else if(jjRegexMatch(typeName, "Bolly", true)) return npcBunniesAnimations::NpcBoss::Bolly;
	else if(jjRegexMatch(typeName, "Bubba", true)) return npcBunniesAnimations::NpcBoss::Bubba;
	else if(jjRegexMatch(typeName, "DevilDevan", true)) return npcBunniesAnimations::NpcBoss::DevilDevan;
	else if(jjRegexMatch(typeName, "Queen", true)) return npcBunniesAnimations::NpcBoss::Queen;
	else if(jjRegexMatch(typeName, "Robot", true)) return npcBunniesAnimations::NpcBoss::Robot;
	else if(jjRegexMatch(typeName, "RockTurt", true) || jjRegexMatch(typeName, "RocketTurtle", true)) return npcBunniesAnimations::NpcBoss::RocketTurtle;
	else if(jjRegexMatch(typeName, "TufBoss", true)) return npcBunniesAnimations::NpcBoss::TufBoss;
	else if(jjRegexMatch(typeName, "Tweedle", true)) return npcBunniesAnimations::NpcBoss::Tweedle;
	else if(jjRegexMatch(typeName, "Uterus", true)) return npcBunniesAnimations::NpcBoss::Uterus;
	else if(jjRegexMatch(typeName, "WaterShield", true) || jjRegexMatch(typeName, "BubbleShield", true) || jjRegexMatch(typeName, "BubbleSh", true)) return npcBunniesAnimations::NpcObject::WaterShield;
	else if(jjRegexMatch(typeName, "FireShield", true) || jjRegexMatch(typeName, "FireSh", true)) return npcBunniesAnimations::NpcObject::FireShield;
	else if(jjRegexMatch(typeName, "LightningShield", true) || jjRegexMatch(typeName, "ElectricShield", true) || jjRegexMatch(typeName, "ElectroShield", true) || jjRegexMatch(typeName, "ElectroSh", true)) return npcBunniesAnimations::NpcObject::LightningShield;
	else if(jjRegexMatch(typeName, "MorphMonitor", true) || jjRegexMatch(typeName, "Morph", true)) return npcBunniesAnimations::NpcObject::MorphMonitor;
	else if(jjRegexMatch(typeName, "BirdMonitor", true) || jjRegexMatch(typeName, "BirdMorph", true)) return npcBunniesAnimations::NpcObject::BirdMonitor;
	else if(jjRegexMatch(typeName, "BlasterPowerup", true) || jjRegexMatch(typeName, "Blaster", true)) return npcBunniesAnimations::NpcObject::BlasterPowerup;
	else if(jjRegexMatch(typeName, "BouncerPowerup", true) || jjRegexMatch(typeName, "Bouncer", true)) return npcBunniesAnimations::NpcObject::BouncerPowerup;
	else if(jjRegexMatch(typeName, "IcePowerup", true) || jjRegexMatch(typeName, "Freezer", true) || jjRegexMatch(typeName, "Ice", true)) return npcBunniesAnimations::NpcObject::IcePowerup;
	else if(jjRegexMatch(typeName, "SeekerPowerup", true) || jjRegexMatch(typeName, "Seeker", true)) return npcBunniesAnimations::NpcObject::SeekerPowerup;
	else if(jjRegexMatch(typeName, "RFPowerup", true) || jjRegexMatch(typeName, "RF", true)) return npcBunniesAnimations::NpcObject::RFPowerup;
	else if(jjRegexMatch(typeName, "ToasterPowerup", true) || jjRegexMatch(typeName, "Toaster", true)) return npcBunniesAnimations::NpcObject::ToasterPowerup;
	else if(jjRegexMatch(typeName, "Gun8Powerup", true) || jjRegexMatch(typeName, "Pepper", true) || jjRegexMatch(typeName, "Gun8", true)) return npcBunniesAnimations::NpcObject::Gun8Powerup;
	else if(jjRegexMatch(typeName, "Gun9Powerup", true) || jjRegexMatch(typeName, "Electro", true) || jjRegexMatch(typeName, "Gun9", true)) return npcBunniesAnimations::NpcObject::Gun9Powerup;
	return ANIM::CUSTOM[emptyCharSet[npcBunniesCore::NpcAnim::JAZZ]]; //default to Jazz
}
/// JUST FOR TESTING /// These will be removed when you will load NPCs from the _npc file
/// Setup_NPC(); insertFurs(); are just for testing
void Setup_NPC()
{
	npcBunniesCore::furClass defaultFur;
	defaultFur.c1 = 16;
	defaultFur.c2 = 24;
	defaultFur.c3 = 32;
	defaultFur.c4 = 40;
	array<string> npc_names = {"Jazz", "Spaz", "Lori", "Frog", "Bird", "Devan", "Mario", "Sonic", "Tails", "JJ1", "Yoshi", "Eva"};
	array<int> x_array = {64, 66, 68, 78, 83, 88, 93, 98, 103, 108, 113, 117};
	for(int i=0; i<12; i++)
	{
		npcBunniesCore::npcBunny npc;
		//type
		npc.id = i;
		npc.name = npc_names[i];
		npc.type = getTypeFromTypeName(npc_names[i]); //was NpcList[i];

		//location
		npc.xTile = x_array[i];
		npc.yTile = 253;
		//direction
		npc.direction = -1;

		//fur
		npc.fur.c1 = 40;
		npc.fur.c2 = 32;
		npc.fur.c3 = 80;
		npc.fur.c4 = 64;

		//moveable
		npc.moveable = true;
		npc.move = false; //it is moveable but it is not moving
		npc.playerFollowing = -1;
		npc = npcBunniesCore::InitNpcQuestState(npc);

		//the object itself
		int objId = jjAddObject(OBJECT::CHERRY, npc.xTile*32, npc.yTile*32);
		jjOBJ@ obj = jjObjects[objId];
		obj.behavior = npcBunniesMovement::Npc();
		obj.direction = npc.direction;
		obj.scriptedCollisions = true;
		obj.playerHandling = HANDLING::SPECIAL;
		obj.var[0] = npc.id + 1;
		npc.objectID = obj.objectID;

		//point to stand on
		npc.fallToX = obj.xPos;
		npc.fallToY = obj.yPos;

		//adding the NPC in npcObjectIds dictionary and npcBunnies array
		npcBunniesCore::npcObjectIds.set(obj.objectID+'', npc.id);
		npcBunniesCore::npcBunnies.insertLast(npc);

		//--- Add dialogue to Jazz NPC ---
		npcBunniesCore::npcBunnies[i].dialogueSets =
		{
			@npcBunniesDialogue::DialogueSet
			(
				dialogue:
				{
					@npcBunniesDialogue::DialogueLine
					(
						personId: 0,
						isNpc: 1,
						text: "Test Line 1"
					),
					@npcBunniesDialogue::DialogueLine
					(
						personId: 1,
						isNpc: 0,
						text: "Test Line 2"
					)
				},
				choices:
				{
					@npcBunniesDialogue::Choice
					(
						choiceText: "Choice A"
					),
					@npcBunniesDialogue::Choice
					(
						choiceText: "Choice B"
					),
					@npcBunniesDialogue::Choice
					(
						choiceText: "Choice C"
					),
					@npcBunniesDialogue::Choice
					(
						choiceText: "Choice D",
						goToSet: 1
					)
				},
				choicesAnswerMethod: 1
			),
			@npcBunniesDialogue::DialogueSet
			(
				dialogue:
				{
					@npcBunniesDialogue::DialogueLine
					(
						personId: 0,
						isNpc: 1,
						text: "Test Line 3"
					),
					@npcBunniesDialogue::DialogueLine
					(
						personId: 0,
						isNpc: 0,
						text: "Test Line 4"
					)
				},
				choices:
				{
					@npcBunniesDialogue::Choice
					(
						choiceText: "Choice A"
					),
					@npcBunniesDialogue::Choice
					(
						choiceText: "Choice B"
					),
					@npcBunniesDialogue::Choice
					(
						choiceText: "Choice C"
					),
					@npcBunniesDialogue::Choice
					(
						choiceText: "Choice D",
						goToSet: 0
					)
				},
				choicesAnswerMethod: 2
			)
		};
	}
	//It works! These alerts prove it:
	//jjAlert("Dialogue 1: "+npcBunnies[0].dialogueSets[0].dialogue[0].text);
	//jjAlert("Dialogue 2: "+npcBunnies[0].dialogueSets[0].dialogue[1].text);
	//jjAlert("Dialogue 3: "+npcBunnies[0].dialogueSets[1].dialogue[0].text);
	//jjAlert("Dialogue 4: "+npcBunnies[0].dialogueSets[1].dialogue[1].text);
}

npcBunniesDialogue::DialogueSet@ CreateDialogueSet(array<npcBunniesDialogue::DialogueLine@> dialogue, array<npcBunniesDialogue::Choice@> choices, int choicesAnswerMethod)
{
    return npcBunniesDialogue::DialogueSet(dialogue, choices, choicesAnswerMethod);
}

//----------------------------------------------------------------------------------------------------

//these arrays go with charIndex: Jazz, Spaz, Lori, Frog, Bird, Devan, Mario, Sonic, Tails, JJ1, Yoshi

array<int>   adjustFaceX = {0, 0,0,0,  5, 0,  5, 15,  5, 10, 10, 10};
array<int>   adjustFaceY = {0, 0,0,0, 15, 0, 45, 30, 15, 40, 45, 40};
array<float> adjustSizeX = {0, 1,1,1,  1, 1,  1,  1,  1,1.3,1.3,1.5};
array<float> adjustSizeY = {0, 1,1,1,  1, 1,  1,  1,  1,1.3,1.3,1.3};
array<int>  facePalshift = {0, 1,1,1,  9, 0,  1,  2,  3,  4,  1,  1};
int AnimatedNpcUiFrame(const NpcUiAnimation &in icon)
{
	if(icon.frameCount <= 1) return icon.firstFrame;
	int speed = icon.ticksPerFrame <= 0 ? 6 : icon.ticksPerFrame;
	return icon.firstFrame + ((jjGameTicks / speed) % icon.frameCount);
}

void DrawNpcUiIcon(jjCANVAS@ canvas, int x, int y, const NpcUiAnimation &in icon, float size = 0.75)
{
	if(icon.hidden) return;
	canvas.drawResizedSprite(x + icon.offsetX, y + icon.offsetY - 8, icon.animSet, icon.anim, AnimatedNpcUiFrame(icon), size, size, icon.spriteMode, icon.param);
}

void DrawNpcItemAmountText(jjCANVAS@ canvas, int x, int y, const string &in amountText, int amountTextWidth, const NpcUiAnimation &in icon)
{
	if(icon.hidden) return;
	canvas.drawString(x, y, amountText, STRING::SMALL, STRING::NORMAL);
	DrawNpcUiIcon(canvas, x + amountTextWidth + 8, y + 8, icon);
}

void DrawNpcItemAmount(jjCANVAS@ canvas, int x, int y, int amount, const NpcUiAnimation &in icon)
{
	if(icon.hidden) return;
	string amountText = amount + " x ";
	DrawNpcItemAmountText(canvas, x, y, amountText, jjGetStringWidth(amountText, STRING::SMALL, STRING::NORMAL), icon);
}

int GetNpcItemAmountWidth(int amount)
{
	return jjGetStringWidth(amount + " x ", STRING::SMALL, STRING::NORMAL) + 30;
}

int cachedHudCoinAmount = -999999;
int cachedHudGemAmount = -999999;
string cachedHudCoinText = "";
string cachedHudGemText = "";
int cachedHudCoinTextWidth = 0;
int cachedHudGemTextWidth = 0;

void UpdateNpcHudCoinCache(int amount)
{
	if(cachedHudCoinAmount == amount) return;
	cachedHudCoinAmount = amount;
	cachedHudCoinText = amount + " x ";
	cachedHudCoinTextWidth = jjGetStringWidth(cachedHudCoinText, STRING::SMALL, STRING::NORMAL);
}

void UpdateNpcHudGemCache(int amount)
{
	if(cachedHudGemAmount == amount) return;
	cachedHudGemAmount = amount;
	cachedHudGemText = amount + " x ";
	cachedHudGemTextWidth = jjGetStringWidth(cachedHudGemText, STRING::SMALL, STRING::NORMAL);
}

void DrawNpcCountsHud(jjPLAYER@ play, jjCANVAS@ canvas)
{
	if(!npcBunniesCore::HUD_COUNTS_ON || play is null) return;
	int x = npcBunniesCore::HUD_COUNTS_X;
	int y = npcBunniesCore::HUD_COUNTS_Y;
	int gemAmount = play.gems[GEM::RED] + 5 * play.gems[GEM::GREEN] + 10 * play.gems[GEM::BLUE] + 1 * play.gems[GEM::PURPLE];
	UpdateNpcHudCoinCache(play.coins);
	UpdateNpcHudGemCache(gemAmount);
	NpcUiAnimation gemIcon;
	GetGemUiIcon(GEM::RED, gemIcon);
	NpcUiAnimation coinIcon;
	GetNpcUiIcon(NpcUiIcon::Coin, coinIcon);
	DrawNpcItemAmountText(canvas, x + 150 + 92, y, cachedHudGemText, cachedHudGemTextWidth, gemIcon);
	DrawNpcItemAmountText(canvas, x + 150, y, cachedHudCoinText, cachedHudCoinTextWidth, coinIcon);
}

int GetMonsterQuestIconId(int eventID)
{
	if(eventID == OBJECT::BAT) return NpcUiIcon::MonsterBat;
	if(eventID == OBJECT::BEE) return NpcUiIcon::MonsterBee;
	if(eventID == OBJECT::BEEBOY) return NpcUiIcon::MonsterBeeBoy;
	if(eventID == OBJECT::BEES) return NpcUiIcon::MonsterBees;
	if(eventID == OBJECT::BUTTERFLY) return NpcUiIcon::MonsterButterfly;
	if(eventID == OBJECT::CRAB) return NpcUiIcon::MonsterCrab;
	if(eventID == OBJECT::DEMON) return NpcUiIcon::MonsterDemon;
	if(eventID == OBJECT::DOGGYDOGG) return NpcUiIcon::MonsterDoggyDogg;
	if(eventID == OBJECT::DRAGON) return NpcUiIcon::MonsterDragon;
	if(eventID == OBJECT::DRAGONFLY) return NpcUiIcon::MonsterDragonfly;
	if(eventID == OBJECT::FATCHICK) return NpcUiIcon::MonsterFatChick;
	if(eventID == OBJECT::FENCER) return NpcUiIcon::MonsterFencer;
	if(eventID == OBJECT::FISH) return NpcUiIcon::MonsterFish;
	if(eventID == OBJECT::HATTER) return NpcUiIcon::MonsterHatter;
	if(eventID == OBJECT::HELMUT) return NpcUiIcon::MonsterHelmut;
	if(eventID == OBJECT::LABRAT) return NpcUiIcon::MonsterLabRat;
	if(eventID == OBJECT::LIZARD) return NpcUiIcon::MonsterLizard;
	if(eventID == OBJECT::FLOATLIZARD) return NpcUiIcon::MonsterFloatLizard;
	if(eventID == OBJECT::XMASLIZARD) return NpcUiIcon::MonsterXmasLizard;
	if(eventID == OBJECT::MONKEY) return NpcUiIcon::MonsterMonkey;
	if(eventID == OBJECT::STANDMONKEY) return NpcUiIcon::MonsterStandMonkey;
	if(eventID == OBJECT::NORMTURTLE) return NpcUiIcon::MonsterNormalTurtle;
	if(eventID == OBJECT::XMASNORMTURTLE) return NpcUiIcon::MonsterXmasNormalTurtle;
	if(eventID == OBJECT::TUBETURTLE) return NpcUiIcon::MonsterTubeTurtle;
	if(eventID == OBJECT::TUFTURT) return NpcUiIcon::MonsterTufTurtle;
	if(eventID == OBJECT::RAPIER) return NpcUiIcon::MonsterRapier;
	if(eventID == OBJECT::RAVEN) return NpcUiIcon::MonsterRaven;
	if(eventID == OBJECT::SKELETON) return NpcUiIcon::MonsterSkeleton;
	if(eventID == OBJECT::SPARK) return NpcUiIcon::MonsterSpark;
	if(eventID == OBJECT::SUCKER) return NpcUiIcon::MonsterSucker;
	if(eventID == OBJECT::FLOATSUCKER) return NpcUiIcon::MonsterFloatSucker;
	if(eventID == OBJECT::CAT) return NpcUiIcon::MonsterCat;
	if(eventID == OBJECT::PACMANGHOST) return NpcUiIcon::MonsterPacmanGhost;
	if(eventID == OBJECT::MOTH) return NpcUiIcon::MonsterMoth;
	if(eventID == OBJECT::WITCH) return NpcUiIcon::MonsterWitch;
	return NpcUiIcon::MonsterDefault;
}

int GetBossQuestIconId(int eventID)
{
	if(eventID == OBJECT::ROBOT) return NpcUiIcon::BossRobot;
	if(eventID == OBJECT::BILSY) return NpcUiIcon::BossBilsy;
	if(eventID == OBJECT::XMASBILSY) return NpcUiIcon::BossXmasBilsy;
	if(eventID == OBJECT::BOLLY) return NpcUiIcon::BossBolly;
	if(eventID == OBJECT::BUBBA) return NpcUiIcon::BossBubba;
	if(eventID == OBJECT::DEVANROBOT) return NpcUiIcon::BossDevan;
	if(eventID == OBJECT::DEVILDEVAN) return NpcUiIcon::BossDevilDevan;
	if(eventID == OBJECT::QUEEN) return NpcUiIcon::BossQueen;
	if(eventID == OBJECT::ROCKETTURTLE) return NpcUiIcon::BossRocketTurtle;
	if(eventID == OBJECT::TUFBOSS) return NpcUiIcon::BossTufBoss;
	if(eventID == OBJECT::TWEEDLEBOSS) return NpcUiIcon::BossTweedle;
	if(eventID == OBJECT::UTERUS) return NpcUiIcon::BossUterus;
	return NpcUiIcon::BossDefault;
}
int GetQuestIconId(npcBunniesQuests::NpcQuest@ quest)
{
	if(quest.questType == npcBunniesQuests::QuestType::Coins) return NpcUiIcon::Coin;
	if(quest.questType == npcBunniesQuests::QuestType::Gems) return NpcUiIcon::Gem;
	if(quest.questType == npcBunniesQuests::QuestType::DefeatBoss) return GetBossQuestIconId(quest.objectType);
	if(quest.questType == npcBunniesQuests::QuestType::DefeatMonster) return GetMonsterQuestIconId(quest.objectType);
	return NpcUiIcon::Unknown;
}

void CopyNpcUiAnimation(const NpcUiAnimation &in source, NpcUiAnimation &inout result)
{
	SetNpcUiAnimation(result, source.animSet, source.anim, source.firstFrame, source.frameCount, source.ticksPerFrame, source.spriteMode, source.param, source.offsetX, source.offsetY, source.hidden);
}
void GetQuestUiAnimation(npcBunniesQuests::NpcQuest@ quest, NpcUiAnimation &inout result)
{
	if(quest is null) { SetNpcUiAnimation(result, 0, 0, 0, 1, 6); result.hidden = true; return; }
	if(quest.iconHidden) { CopyNpcUiAnimation(quest.icon, result); result.hidden = true; return; }
	if(quest.iconSpecified) { CopyNpcUiAnimation(quest.icon, result); return; }
	if(quest.questType == npcBunniesQuests::QuestType::Gems) { GetGemUiIcon(quest.gemType, result); return; }
	if(quest.questType == npcBunniesQuests::QuestType::DefeatMonster || quest.questType == npcBunniesQuests::QuestType::DefeatBoss)
	{
		int simpleType = GetQuestObjectNpcSimpleType(quest.objectType);
		if(simpleType != 0)
		{
			LoadNpcQuestIconAnimSet(simpleType);
			GetNpcSimpleAnimation(simpleType, result);
			result.firstFrame = 0;
			result.ticksPerFrame = 8;
			return;
		}
		GetNpcUiIcon(GetQuestIconId(quest), result);
		return;
	}
	GetNpcUiIcon(GetQuestIconId(quest), result);
}

void LoadNpcQuestIconAnimSet(int type)
{
	int animSet = GetNpcSimpleAnimSet(type);
	if(animSet >= 0 && jjAnimSets[animSet].firstAnim == 0) jjAnimSets[animSet].load();
}

int GetQuestObjectNpcSimpleType(int eventID)
{
	if(eventID == OBJECT::BAT) return npcBunniesAnimations::NpcMonster::Bat;
	if(eventID == OBJECT::BEE) return npcBunniesAnimations::NpcMonster::Bee;
	if(eventID == OBJECT::BEEBOY) return npcBunniesAnimations::NpcMonster::BeeBoy;
	if(eventID == OBJECT::BEES) return npcBunniesAnimations::NpcMonster::Bees;
	if(eventID == OBJECT::BUTTERFLY) return npcBunniesAnimations::NpcMonster::Butterfly;
	if(eventID == OBJECT::CRAB) return npcBunniesAnimations::NpcMonster::Crab;
	if(eventID == OBJECT::DEMON) return npcBunniesAnimations::NpcMonster::Demon;
	if(eventID == OBJECT::DOGGYDOGG) return npcBunniesAnimations::NpcMonster::DoggyDogg;
	if(eventID == OBJECT::DRAGON) return npcBunniesAnimations::NpcMonster::Dragon;
	if(eventID == OBJECT::DRAGONFLY) return npcBunniesAnimations::NpcMonster::Dragonfly;
	if(eventID == OBJECT::FATCHICK) return npcBunniesAnimations::NpcMonster::FatChick;
	if(eventID == OBJECT::FENCER) return npcBunniesAnimations::NpcMonster::Fencer;
	if(eventID == OBJECT::FISH) return npcBunniesAnimations::NpcMonster::Fish;
	if(eventID == OBJECT::HATTER) return npcBunniesAnimations::NpcMonster::Hatter;
	if(eventID == OBJECT::HELMUT) return npcBunniesAnimations::NpcMonster::Helmut;
	if(eventID == OBJECT::LABRAT) return npcBunniesAnimations::NpcMonster::LabRat;
	if(eventID == OBJECT::LIZARD) return npcBunniesAnimations::NpcMonster::Lizard;
	if(eventID == OBJECT::FLOATLIZARD) return npcBunniesAnimations::NpcMonster::FloatLizard;
	if(eventID == OBJECT::XMASLIZARD) return npcBunniesAnimations::NpcMonster::XmasLizard;
	if(eventID == OBJECT::XMASFLOATLIZARD) return npcBunniesAnimations::NpcMonster::XmasFloatLizard;
	if(eventID == OBJECT::MONKEY) return npcBunniesAnimations::NpcMonster::Monkey;
	if(eventID == OBJECT::STANDMONKEY) return npcBunniesAnimations::NpcMonster::StandMonkey;
	if(eventID == OBJECT::NORMTURTLE) return npcBunniesAnimations::NpcMonster::NormalTurtle;
	if(eventID == OBJECT::XMASNORMTURTLE) return npcBunniesAnimations::NpcMonster::XmasNormalTurtle;
	if(eventID == OBJECT::TUBETURTLE) return npcBunniesAnimations::NpcMonster::TubeTurtle;
	if(eventID == OBJECT::TUFTURT) return npcBunniesAnimations::NpcMonster::TufTurtle;
	if(eventID == OBJECT::RAPIER) return npcBunniesAnimations::NpcMonster::Rapier;
	if(eventID == OBJECT::RAVEN) return npcBunniesAnimations::NpcMonster::Raven;
	if(eventID == OBJECT::SKELETON) return npcBunniesAnimations::NpcMonster::Skeleton;
	if(eventID == OBJECT::SPARK) return npcBunniesAnimations::NpcMonster::Spark;
	if(eventID == OBJECT::SUCKER) return npcBunniesAnimations::NpcMonster::Sucker;
	if(eventID == OBJECT::FLOATSUCKER) return npcBunniesAnimations::NpcMonster::FloatSucker;
	if(eventID == OBJECT::CAT) return npcBunniesAnimations::NpcMonster::Cat;
	if(eventID == OBJECT::PACMANGHOST) return npcBunniesAnimations::NpcMonster::PacmanGhost;
	if(eventID == OBJECT::MOTH) return npcBunniesAnimations::NpcMonster::Moth;
	if(eventID == OBJECT::WITCH) return npcBunniesAnimations::NpcMonster::Witch;
	if(eventID == OBJECT::BILSY) return npcBunniesAnimations::NpcBoss::Bilsy;
	if(eventID == OBJECT::XMASBILSY) return npcBunniesAnimations::NpcBoss::XmasBilsy;
	if(eventID == OBJECT::BOLLY) return npcBunniesAnimations::NpcBoss::Bolly;
	if(eventID == OBJECT::BUBBA) return npcBunniesAnimations::NpcBoss::Bubba;
	if(eventID == OBJECT::DEVILDEVAN) return npcBunniesAnimations::NpcBoss::DevilDevan;
	if(eventID == OBJECT::QUEEN) return npcBunniesAnimations::NpcBoss::Queen;
	if(eventID == OBJECT::ROBOT) return npcBunniesAnimations::NpcBoss::Robot;
	if(eventID == OBJECT::ROCKETTURTLE) return npcBunniesAnimations::NpcBoss::RocketTurtle;
	if(eventID == OBJECT::TUFBOSS) return npcBunniesAnimations::NpcBoss::TufBoss;
	if(eventID == OBJECT::TWEEDLEBOSS) return npcBunniesAnimations::NpcBoss::Tweedle;
	if(eventID == OBJECT::UTERUS) return npcBunniesAnimations::NpcBoss::Uterus;
	return 0;
}

int GetRewardIconId(npcBunniesQuests::NpcReward@ reward)
{
	if(reward is null) return NpcUiIcon::Unknown;
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardCoins) return NpcUiIcon::Coin;
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardGems) return NpcUiIcon::Gem;
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardAmmo) return GetAmmoRewardIconId(reward.weaponType);
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardPowerup) return GetPowerupRewardIconId(reward.weaponType);
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardTeleport) return NpcUiIcon::Teleport;
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardTrigger) return NpcUiIcon::Trigger;
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardFastFire) return NpcUiIcon::FastFire;
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardMoveable) return NpcUiIcon::Moveable;
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardGuidePath) return NpcUiIcon::Teleport;
	return NpcUiIcon::Unknown;
}

int GetAmmoRewardIconId(int weaponType)
{
	if(weaponType == WEAPON::BOUNCER) return NpcUiIcon::AmmoBouncer3;
	if(weaponType == WEAPON::ICE) return NpcUiIcon::AmmoIce3;
	if(weaponType == WEAPON::SEEKER) return NpcUiIcon::AmmoSeeker3;
	if(weaponType == WEAPON::RF) return NpcUiIcon::AmmoRF3;
	if(weaponType == WEAPON::TOASTER) return NpcUiIcon::AmmoToaster3;
	if(weaponType == WEAPON::TNT) return NpcUiIcon::AmmoTNT3;
	if(weaponType == WEAPON::GUN8) return NpcUiIcon::AmmoGun8_3;
	if(weaponType == WEAPON::GUN9) return NpcUiIcon::AmmoGun9_3;
	return NpcUiIcon::AmmoDefault;
}

int GetPowerupRewardIconId(int weaponType)
{
	if(weaponType == WEAPON::BLASTER) return NpcUiIcon::PowerupBlaster;
	if(weaponType == WEAPON::BOUNCER) return NpcUiIcon::PowerupBouncer;
	if(weaponType == WEAPON::ICE) return NpcUiIcon::PowerupIce;
	if(weaponType == WEAPON::SEEKER) return NpcUiIcon::PowerupSeeker;
	if(weaponType == WEAPON::RF) return NpcUiIcon::PowerupRF;
	if(weaponType == WEAPON::TOASTER) return NpcUiIcon::PowerupToaster;
	if(weaponType == WEAPON::GUN8) return NpcUiIcon::PowerupGun8;
	if(weaponType == WEAPON::GUN9) return NpcUiIcon::PowerupGun9;
	return NpcUiIcon::PowerupDefault;
}

void GetRewardUiAnimation(npcBunniesQuests::NpcReward@ reward, NpcUiAnimation &inout result)
{
	if(reward is null) { SetNpcUiAnimation(result, 0, 0, 0, 1, 6); result.hidden = true; return; }
	if(reward.iconHidden) { CopyNpcUiAnimation(reward.icon, result); result.hidden = true; return; }
	if(reward.iconSpecified) { CopyNpcUiAnimation(reward.icon, result); return; }
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardGems) { GetGemUiIcon(reward.gemType, result); return; }
	GetNpcUiIcon(GetRewardIconId(reward), result);
}

int GetRewardDisplayAmount(npcBunniesQuests::NpcReward@ reward)
{
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardPowerup || reward.rewardType == npcBunniesQuests::RewardType::RewardTeleport || reward.rewardType == npcBunniesQuests::RewardType::RewardTrigger || reward.rewardType == npcBunniesQuests::RewardType::RewardMoveable || reward.rewardType == npcBunniesQuests::RewardType::RewardGuidePath) return 1;
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardFastFire) return reward.amount > 0 ? reward.amount : 5;
	return reward.amount > 0 ? reward.amount : 1;
}

void DrawNpcQuestRewardStrip(jjCANVAS@ canvas, npcBunniesDialogue::DialogueSet@ set)
{
	if(set is null) return;
	int totalWidth = 0;
	for(uint i = 0; i < set.quests.length; i++)
		if(set.quests[i] !is null && !set.quests[i].iconHidden) totalWidth += GetNpcItemAmountWidth(set.quests[i].amount) + 18;
	for(uint i = 0; i < set.rewards.length; i++)
		if(set.rewards[i] !is null && !set.rewards[i].iconHidden) totalWidth += GetNpcItemAmountWidth(GetRewardDisplayAmount(set.rewards[i])) + 18;
	if(totalWidth <= 0) return;
	int x = (jjResolutionWidth - totalWidth) / 2;
	int y = 340;
	for(uint i = 0; i < set.quests.length; i++)
	{
		npcBunniesQuests::NpcQuest@ quest = set.quests[i];
		if(quest is null || quest.iconHidden) continue;
		NpcUiAnimation icon;
		GetQuestUiAnimation(quest, icon);
		DrawNpcItemAmount(canvas, x, y, quest.amount, icon);
		x += GetNpcItemAmountWidth(quest.amount) + 18;
	}
	for(uint i = 0; i < set.rewards.length; i++)
	{
		npcBunniesQuests::NpcReward@ reward = set.rewards[i];
		if(reward is null || reward.iconHidden) continue;
		int amount = GetRewardDisplayAmount(reward);
		NpcUiAnimation icon;
		GetRewardUiAnimation(reward, icon);
		DrawNpcItemAmount(canvas, x, y, amount, icon);
		x += GetNpcItemAmountWidth(amount) + 18;
	}
}


enum NpcUiIcon
{
	Coin = 0,
	Gem = 1,

[preview ends here]