Downloads containing npcBunniesEditorActions.asc

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

File preview

namespace npcBunniesEditorActions
{
void NdeProcessClick(jjPLAYER@ play)
{
	bool mouseDown = jjKey[0x01];
	if(!mouseDown) { npcBunniesEditorCore::NpcDesignerMouseWasDown = false; return; }
	if(npcBunniesEditorCore::NpcDesignerMouseWasDown) return;
	npcBunniesEditorCore::NpcDesignerMouseWasDown = true;
	if(jjGameTicks < npcBunniesEditorCore::NpcDesignerIgnoreInputUntilTick) return;
	if(jjGameTicks - npcBunniesEditorCore::NpcDesignerLastClick < 20) return;
	npcBunniesEditorCore::NpcDesignerLastClick = jjGameTicks;
	int tx = jjMouseX, ty = jjMouseY + 8;
	int hit = DbHitControl(@npcBunniesEditorCore::NpcDesignerWindow, tx, ty);
	if(hit >= 0 && hit < int(npcBunniesEditorCore::NpcDesignerButtons.length))
	{
		npcBunniesEditorCore::NpcDesignerFocus = hit;
		npcBunniesEditorCore::NpcDesignerWindow.focusIndex = hit;
		NdeActivateButton(npcBunniesEditorCore::NpcDesignerButtons[hit]);
		return;
	}
	if(npcBunniesEditorCore::NpcDesignerView == npcBunniesEditorCore::NDE_CREATE && npcBunniesEditorCore::NpcDesignerSelectedType >= 0 && !npcBunniesEditorCore::NdeInsidePanel(tx, ty))
		NdePlaceNewNpcAtMouse(play, jjMouseX, jjMouseY);
	else if(npcBunniesEditorCore::NpcDesignerMovingNpc && NdeValidNpc() && !npcBunniesEditorCore::NdeInsidePanel(tx, ty))
		NdeMoveSelectedNpcToMouse(play, jjMouseX, jjMouseY);
	else if(npcBunniesEditorCore::NpcDesignerEditingTeleport && NdeValidNpc() && !npcBunniesEditorCore::NdeInsidePanel(tx, ty))
		NdeSetTeleportEditFromMouse(play, jjMouseX, jjMouseY);
}

/// <summary>
/// Keeps the keyboard focus index inside the current button list.
/// </summary>
void NdeNormalizeFocus()
{
	if(npcBunniesEditorCore::NpcDesignerButtons.length == 0) { npcBunniesEditorCore::NpcDesignerFocus = 0; return; }
	npcBunniesEditorCore::NpcDesignerWindow.focusIndex = npcBunniesEditorCore::NpcDesignerFocus;
	DbNormalizeFocus(@npcBunniesEditorCore::NpcDesignerWindow);
	npcBunniesEditorCore::NpcDesignerFocus = npcBunniesEditorCore::NpcDesignerWindow.focusIndex;
}

/// <summary>
/// Activates the currently focused editor button.
/// </summary>
void NdeActivateFocus()
{
	if(npcBunniesEditorCore::NpcDesignerButtons.length == 0) return;
	NdeNormalizeFocus();
	NdeActivateButton(npcBunniesEditorCore::NpcDesignerButtons[npcBunniesEditorCore::NpcDesignerFocus]);
}

/// <summary>
/// Global editor state for focused button action.
/// </summary>
int NdeFocusedButtonAction()
{
	if(npcBunniesEditorCore::NpcDesignerFocus < 0 || npcBunniesEditorCore::NpcDesignerFocus >= int(npcBunniesEditorCore::NpcDesignerButtons.length)) return -1;
	return npcBunniesEditorCore::NpcDesignerButtons[npcBunniesEditorCore::NpcDesignerFocus].action;
}

/// <summary>
/// Dispatches a clicked or focused editor button to its behavior.
/// </summary>
void NdeActivateButton(npcBunniesEditorCore::NpcDesignerButton@ b)
{
	if(b is null) return;
	if(b.action != npcBunniesEditorCore::NDE_ACT_FIELD) NdeEndTextEdit();
	if(b.action == npcBunniesEditorCore::NDE_ACT_TOGGLE_PLAYER_MOVEMENT) { npcBunniesEditorCore::NpcDesignerAllowPlayerMovement = !npcBunniesEditorCore::NpcDesignerAllowPlayerMovement; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_HELP) { npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_HELP; npcBunniesEditorCore::NpcDesignerScroll = 0; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_TOGGLE_BIG_ANIMS) { npcBunniesCore::LOAD_BIG_NPC_ANIMATIONS = !npcBunniesCore::LOAD_BIG_NPC_ANIMATIONS; if(npcBunniesCore::LOAD_BIG_NPC_ANIMATIONS) npcBunniesAnimations::loadMenuAnims(); npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_TOGGLE_ALL_MONSTER_ANIMS) { npcBunniesCore::LOAD_ALL_MONSTER_ANIMATIONS = !npcBunniesCore::LOAD_ALL_MONSTER_ANIMATIONS; npcBunniesCore::NPC_ALL_MONSTER_ANIMS_PENDING_RESTART = npcBunniesCore::LOAD_ALL_MONSTER_ANIMATIONS; if(npcBunniesCore::LOAD_ALL_MONSTER_ANIMATIONS) jjAlert("NPC Designer: all monster/boss animations will fully apply after level restart.", false); npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_LOAD_ANIMS) { npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_LOAD_ANIMS; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_LOAD_ANIM_PREV || b.action == npcBunniesEditorCore::NDE_ACT_LOAD_ANIM_NEXT) { npcBunniesEditorPreview::NdeCycleLoadAnimSelection(b.a, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_LOAD_ANIM_LOAD) { npcBunniesEditorPreview::NdeSetAdditionalSimpleAnim(npcBunniesEditorPreview::NdeSelectedLoadAnimType(b.a), true); npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_LOAD_ANIM_UNLOAD) { npcBunniesEditorPreview::NdeSetAdditionalSimpleAnim(npcBunniesEditorPreview::NdeSelectedLoadAnimType(b.a), false); npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_EXIT) { NdeFlushTextEdit(); NdeCancelGuideEntryEdit(); npcBunniesEditorCore::NpcDesignerOpen = false; npcBunniesEditorCore::NpcDesignerHelpOnly = false; npcBunniesEditorCore::NpcDesignerMovingNpc = false; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_BACK_MAIN)
	{
		if(npcBunniesEditorCore::NpcDesignerHelpOnly) { NdeFlushTextEdit(); NdeCancelGuideEntryEdit(); npcBunniesEditorCore::NpcDesignerOpen = false; npcBunniesEditorCore::NpcDesignerHelpOnly = false; npcBunniesEditorCore::NpcDesignerMovingNpc = false; return; }
		if(b.a == 1) npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_EDIT;
		else if(b.a == 2) { NdeCancelGuideEntryEdit(); npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_GUIDE; npcBunniesEditorSerialization::NdeSaveNpcFile(); }
		else if(b.a == 3) npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_DIALOGUE;
		else npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_MAIN;
		return;
	}
	if(b.action == npcBunniesEditorCore::NDE_ACT_MAIN_CREATE) { npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_CREATE; npcBunniesEditorCore::NpcDesignerScroll = 0; npcBunniesEditorCore::NpcDesignerSelectedType = -1; npcBunniesEditorCore::NpcDesignerDeletePick = false; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_MAIN_EDIT) { npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_EDIT_PICK; npcBunniesEditorCore::NpcDesignerScroll = 0; npcBunniesEditorCore::NpcDesignerDeletePick = false; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_MAIN_DELETE) { npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_EDIT_PICK; npcBunniesEditorCore::NpcDesignerScroll = 0; npcBunniesEditorCore::NpcDesignerDeletePick = true; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_MAIN_UNDO_DELETE) { NdeUndoDelete(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_SCROLL_UP) { npcBunniesEditorCore::NpcDesignerScroll--; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_SCROLL_DOWN) { npcBunniesEditorCore::NpcDesignerScroll++; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_CREATE_TYPE)
	{
		if(npcBunniesEditorCore::NpcDesignerSelectedType == b.a) return;
		npcBunniesEditorCore::NpcDesignerSelectedType = b.a;
		return;
	}
	if(b.action == npcBunniesEditorCore::NDE_ACT_PICK_EDIT_NPC)
	{
		npcBunniesEditorCore::NpcDesignerSelectedNpc = b.a;
		if(npcBunniesEditorCore::NpcDesignerDeletePick)
		{
			NdeDeleteNpc(b.a);
			npcBunniesEditorCore::NpcDesignerDeletePick = false;
			npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_MAIN;
			return;
		}
		if(npcBunniesEditorCore::NpcDesignerView == npcBunniesEditorCore::NDE_EDIT_PICK) npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_EDIT;
		return;
	}
	if(b.action == npcBunniesEditorCore::NDE_ACT_FIELD)
	{
		if(npcBunniesEditorCore::NpcDesignerEditingField != b.a) NdeFlushTextEdit();
		npcBunniesEditorCore::NpcDesignerEditingField = -1;
		npcBunniesEditorCore::NpcDesignerEditingValueField = -1;
		npcBunniesEditorCore::NpcDesignerTextScrollField = b.a;
		npcBunniesEditorCore::NpcDesignerTextScrollLine = 0;
		npcBunniesEditorCore::NpcDesignerTextScrollManual = false;
		if(NdeValidNpc())
		{
			npcBunniesCore::npcBunny editNpc = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc];
			npcBunniesEditorCore::NpcDesignerEditingValue = NdeGetFieldValue(editNpc, b.a);
			npcBunniesEditorCore::NpcDesignerEditingValueField = b.a;
			npcBunniesEditorCore::NpcDesignerEditingCursor = int(npcBunniesEditorCore::NpcDesignerEditingValue.length);
		}
		npcBunniesEditorCore::NpcDesignerEditingField = b.a;
		return;
	}
	if(!NdeValidNpc()) return;
	npcBunniesCore::npcBunny npc = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc];
	if(b.action == npcBunniesEditorCore::NDE_ACT_TOGGLE_POSITION) { npcBunniesEditorCore::NpcDesignerMovingNpc = !npcBunniesEditorCore::NpcDesignerMovingNpc; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_CHAR_CATEGORY) { npcBunniesEditorPreview::NdeCycleNpcTypeCategory(npc); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_CHAR_PREV || b.action == npcBunniesEditorCore::NDE_ACT_CHAR_NEXT) { npcBunniesEditorPreview::NdeCycleNpcCharacter(npc, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_NPC_ACCESSORY) { NdeCycleNpcAccessory(npc, b.a, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_NPC_SIZE) { NdeCycleNpcSize(npc, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_NPC_SPEED) { NdeCycleNpcSpeed(npc, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_TOGGLE_PUSHABLE) { npc.pushable = !npc.pushable; npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_TELEPORT_DIRECTION && npcBunniesEditorCore::NpcDesignerPathIndex >= 0 && npcBunniesEditorCore::NpcDesignerPathIndex < int(npc.guidePaths.length))
	{
		npc.guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex].teleportDirection = npc.guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex].teleportDirection < 0 ? 1 : -1;
		npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
		npcBunniesEditorSerialization::NdeSaveNpcFile();
		return;
	}
	if(b.action == npcBunniesEditorCore::NDE_ACT_DIRECTION)
	{
		npc.direction = npc.direction < 0 ? 1 : -1;
		if(npc.objectID >= 0) jjObjects[npc.objectID].direction = npc.direction;
		npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return;
	}
	if(b.action == npcBunniesEditorCore::NDE_ACT_TOGGLE_FOLLOW) { npc.moveable = !npc.moveable; npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_TOGGLE_FIGHTING) { npc.fightingAvailable = !npc.fightingAvailable; npc.fightingEnabled = npc.fightingAvailable; npc.fightingTargetObjectID = -1; npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_FIGHTING_STYLE) { npc.fightingStyle = npc.fightingStyle == npcBunniesCore::NpcFightingStyle::Complex ? npcBunniesCore::NpcFightingStyle::Simple : npcBunniesCore::NpcFightingStyle::Complex; npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_EDIT_DIALOGUE) { npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_DIALOGUE; npcBunniesEditorCore::NpcDesignerDialogueSet = 0; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_EDIT_SHOP) { npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_SHOP; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_TOGGLE_GUIDE) { npc.guideEnabled = !npc.guideEnabled; npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_EDIT_GUIDE) { npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_GUIDE; npcBunniesEditorCore::NpcDesignerScroll = 0; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_NPC_TRACE_MODE)
	{
		npc.traceMode++;
		if(npc.traceMode > npcBunniesCore::NPC_TRACE_CONST) npc.traceMode = npcBunniesCore::NPC_TRACE_OFF;
		npc.traceMode = npcBunniesCore::ClampNpcTraceMode(npc.traceMode);
		if(npc.traceMode == npcBunniesCore::NPC_TRACE_OFF) npcBunniesMovement::ClearNpcTraceFrames(npc.id);
		npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
		npcBunniesEditorSerialization::NdeSaveNpcFile();
		return;
	}
	if(b.action == npcBunniesEditorCore::NDE_ACT_NPC_TRACE_COLOR)
	{
		int index = npcBunniesCore::NPC_TRACE_COLOR_VALUES.find(npc.traceColor);
		if(index < 0) index = 0;
		else index = (index + 1) % int(npcBunniesCore::NPC_TRACE_COLOR_VALUES.length);
		npc.traceColor = npcBunniesCore::NPC_TRACE_COLOR_VALUES[index];
		npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
		npcBunniesEditorSerialization::NdeSaveNpcFile();
		return;
	}
	if(b.action == npcBunniesEditorCore::NDE_ACT_PATH_EDIT_START)
	{
		NdeEndTextEdit();
		if(NdeValidNpc() && npcBunniesEditorCore::NpcDesignerPathIndex >= 0 && npcBunniesEditorCore::NpcDesignerPathIndex < int(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths.length) && npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex].isTeleport)
			NdeStartTeleportEdit(false);
		else
		{
			npcBunniesEditorCore::NpcDesignerRecordingPath = true;
			int tx, ty;
			NdeGuidePathEnd(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc], npcBunniesEditorCore::NpcDesignerPathIndex, tx, ty);
			NdeCenterEditorCameraOnTile(tx, ty);
		}
		return;
	}
	if(b.action == npcBunniesEditorCore::NDE_ACT_PATH_EDIT_STOP) { NdeCancelGuideEntryEdit(); npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	NdeActivateDialogueShopGuide(b, npc);
}

/// <summary>
/// Dispatches button actions shared by dialogue, shop, guide, quest, and reward views.
/// </summary>
void NdeActivateDialogueShopGuide(npcBunniesEditorCore::NpcDesignerButton@ b, npcBunniesCore::npcBunny npc)
{
	if(b.action == npcBunniesEditorCore::NDE_ACT_DIALOGUE_PREV) { npcBunniesEditorCore::NpcDesignerDialogueSet--; if(npcBunniesEditorCore::NpcDesignerDialogueSet < 0) npcBunniesEditorCore::NpcDesignerDialogueSet = int(npc.dialogueSets.length) - 1; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_DIALOGUE_NEXT) { npcBunniesEditorCore::NpcDesignerDialogueSet++; if(npcBunniesEditorCore::NpcDesignerDialogueSet >= int(npc.dialogueSets.length)) npcBunniesEditorCore::NpcDesignerDialogueSet = 0; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_DIALOGUE_ADD)
	{
		npc.dialogueSets.insertLast(@npcBunniesDialogue::DialogueSet(array<npcBunniesDialogue::DialogueLine@>(), array<npcBunniesDialogue::Choice@>(), npcBunniesCore::DialogueAction::Normal));
		npcBunniesEditorCore::NpcDesignerDialogueSet = int(npc.dialogueSets.length) - 1;
		npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return;
	}
	if(b.action == npcBunniesEditorCore::NDE_ACT_DIALOGUE_DELETE && npc.dialogueSets.length > 1 && npcBunniesEditorCore::NpcDesignerDialogueSet >= 0 && npcBunniesEditorCore::NpcDesignerDialogueSet < int(npc.dialogueSets.length))
	{
		npc.dialogueSets.removeAt(npcBunniesEditorCore::NpcDesignerDialogueSet);
		if(npcBunniesEditorCore::NpcDesignerDialogueSet >= int(npc.dialogueSets.length)) npcBunniesEditorCore::NpcDesignerDialogueSet = int(npc.dialogueSets.length) - 1;
		npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return;
	}
	if(npcBunniesEditorCore::NpcDesignerDialogueSet >= 0 && npcBunniesEditorCore::NpcDesignerDialogueSet < int(npc.dialogueSets.length))
	{
		npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet];
		if(b.action == npcBunniesEditorCore::NDE_ACT_DIALOGUE_TYPE)
		{
			set.choicesAnswerMethod++;
			if(set.choicesAnswerMethod > npcBunniesCore::DialogueAction::Quest) set.choicesAnswerMethod = npcBunniesCore::DialogueAction::Normal;
			NdeEnsureDialogueShape(set); npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return;
		}
		if(b.action == npcBunniesEditorCore::NDE_ACT_DIALOGUE_SPEAKER)
		{
			NdeCycleDialogueSpeaker(set, b.b);
			npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return;
		}
		if(b.action == npcBunniesEditorCore::NDE_ACT_DIALOGUE_AUTO_CONTINUE)
		{
			NdeCycleDialogueAutoContinue(npc, set, b.b);
			npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return;
		}
		if(b.action == npcBunniesEditorCore::NDE_ACT_DIALOGUE_GOTO)
		{
			NdeCycleDialogueGoTo(npc, set, b.b);
			npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return;
		}
		if(b.action == npcBunniesEditorCore::NDE_ACT_QUEST_RESPAWN_MONSTERS)
		{
			set.respawnMonsters = !set.respawnMonsters;
			npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return;
		}
		if(b.action == npcBunniesEditorCore::NDE_ACT_DIALOGUE_FRAME_PREV || b.action == npcBunniesEditorCore::NDE_ACT_DIALOGUE_FRAME_NEXT)
		{
			npcBunniesCore::npcBunny speakerNpc = NdeDialogueSpeakerNpc(npc, set);
			npcBunniesEditorPreview::NdeCycleDialogueBigFrame(speakerNpc, set, b.b);
			npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return;
		}
		if(b.action == npcBunniesEditorCore::NDE_ACT_CHOICE_GOTO)
		{
			NdeEnsureChoice(set, b.a);
			set.choices[b.a].goToSet++;
			if(set.choices[b.a].goToSet >= int(npc.dialogueSets.length)) set.choices[b.a].goToSet = -1;
			npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return;
		}
		if(b.action == npcBunniesEditorCore::NDE_ACT_EDIT_QUEST) { npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_QUEST; return; }
		if(b.action == npcBunniesEditorCore::NDE_ACT_EDIT_REWARD) { npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_REWARD; return; }
	}
	if(b.action == npcBunniesEditorCore::NDE_ACT_SHOP_ADD) { npc.shopItems.insertLast(NdeNewShopItem()); npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_SHOP_REWARD_PREV || b.action == npcBunniesEditorCore::NDE_ACT_SHOP_REWARD_NEXT) { NdeCycleShopReward(npc, b.a, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_SHOP_AMOUNT) { NdeChangeShopAmount(npc, b.a, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_SHOP_COINS) { NdeChangeShopCost(npc, b.a, true, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_SHOP_GEMS) { NdeChangeShopCost(npc, b.a, false, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_SHOP_STOCK) { NdeChangeShopStock(npc, b.a, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_SHOP_DELETE && b.a >= 0 && b.a < int(npc.shopItems.length)) { npc.shopItems.removeAt(b.a); npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_GUIDE_ADD) { npcBunniesCore::GuidePath@ path = @npcBunniesCore::GuidePath(); npc.guidePaths.insertLast(path); npc.guideEnabled = true; npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_GUIDE_ADD_TELEPORT) { NdeAddGuideTeleport(npc); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_GUIDE_EDIT && b.a >= 0 && b.a < int(npc.guidePaths.length)) { npcBunniesEditorCore::NpcDesignerPathIndex = b.a; npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_PATH_RECORD; npcBunniesEditorCore::NpcDesignerRecordingPath = false; npcBunniesEditorCore::NpcDesignerEditingTeleport = false; return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_GUIDE_DELETE && b.a >= 0 && b.a < int(npc.guidePaths.length)) { NdeRemoveGuidePathRewardReferences(npc, b.a); npc.guidePaths.removeAt(b.a); npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc; npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_EDIT_PATH_REWARD && b.a >= 0 && b.a < int(npc.dialogueSets.length)) { NdeCancelGuideEntryEdit(); npcBunniesEditorCore::NpcDesignerDialogueSet = b.a; npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_REWARD; npcBunniesEditorCore::NpcDesignerScroll = npcBunniesEditorCore::NdeMax(0, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_QUEST_ADD) { NdeCurrentSet().quests.insertLast(@npcBunniesQuests::NpcQuest()); npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_REWARD_ADD) { NdeCurrentSet().rewards.insertLast(@npcBunniesQuests::NpcReward()); npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_QUEST_TYPE) { NdeCycleQuest(b.a, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_REWARD_TYPE) { NdeCycleReward(b.a, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_QUEST_AMOUNT && b.a >= 0 && b.a < int(NdeCurrentSet().quests.length)) { NdeCurrentSet().quests[b.a].amount = npcBunniesEditorCore::NdeMax(0, NdeCurrentSet().quests[b.a].amount + (b.b < 0 ? -1 : 1)); npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_REWARD_AMOUNT) { NdeChangeRewardAmount(b.a, b.b); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_QUEST_DELETE && b.a >= 0 && b.a < int(NdeCurrentSet().quests.length)) { NdeCurrentSet().quests.removeAt(b.a); npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
	if(b.action == npcBunniesEditorCore::NDE_ACT_REWARD_DELETE && b.a >= 0 && b.a < int(NdeCurrentSet().rewards.length)) { NdeCurrentSet().rewards.removeAt(b.a); npcBunniesEditorSerialization::NdeSaveNpcFile(); return; }
}

/// <summary>
/// Global editor state for valid npc.
/// </summary>
bool NdeValidNpc()
{
	return npcBunniesEditorCore::NpcDesignerSelectedNpc >= 0 && npcBunniesEditorCore::NpcDesignerSelectedNpc < int(npcBunniesCore::npcBunnies.length);
}

/// <summary>
/// Returns the currently selected dialogue set.
/// </summary>
npcBunniesDialogue::DialogueSet@ NdeCurrentSet()
{
	return npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet];
}

/// <summary>
/// Ensures required nested dialogue data exists before editing it.
/// </summary>
void NdeEnsureDialogueShape(npcBunniesDialogue::DialogueSet@ set)
{
	if(set.dialogue.length == 0) set.dialogue.insertLast(@npcBunniesDialogue::DialogueLine(0, 0, ""));
	if(set.choicesAnswerMethod == npcBunniesCore::DialogueAction::Answer) NdeEnsureChoice(set, 0);
	if(set.choicesAnswerMethod == npcBunniesCore::DialogueAction::Choice) for(int i = 0; i < 4; i++) NdeEnsureChoice(set, i);
}

/// <summary>
/// Ensures required nested dialogue data exists before editing it.
/// </summary>
void NdeEnsureChoice(npcBunniesDialogue::DialogueSet@ set, int index)
{
	while(int(set.choices.length) <= index) set.choices.insertLast(@npcBunniesDialogue::Choice("", -1));
}

/// <summary>
/// Applies keyboard input to the active editor text field.
/// </summary>
bool NdeEditingFieldIsNumeric()
{
	return (npcBunniesEditorCore::NpcDesignerEditingField >= 1 && npcBunniesEditorCore::NpcDesignerEditingField <= 6)
		|| NdeAccessoryCategoryFromField(npcBunniesEditorCore::NpcDesignerEditingField) >= 0;
}

bool NdeEditingFieldIsDecimal()
{
	return npcBunniesEditorCore::NpcDesignerEditingField == 5 || npcBunniesEditorCore::NpcDesignerEditingField == 6;
}

bool NdeEditingFieldAllowsNewline()
{
	return npcBunniesEditorCore::NpcDesignerEditingField == 10
		|| npcBunniesEditorCore::NpcDesignerEditingField == 20
		|| (npcBunniesEditorCore::NpcDesignerEditingField >= 30 && npcBunniesEditorCore::NpcDesignerEditingField < 40)
		|| (npcBunniesEditorCore::NpcDesignerEditingField >= npcBunniesEditorCore::NDE_FIELD_GUIDE_PATH_BASE && npcBunniesEditorCore::NpcDesignerEditingField < npcBunniesEditorCore::NDE_FIELD_GUIDE_END_LINE)
		|| npcBunniesEditorCore::NpcDesignerEditingField == npcBunniesEditorCore::NDE_FIELD_GUIDE_END_LINE
		|| npcBunniesEditorCore::NpcDesignerEditingField == npcBunniesEditorCore::NDE_FIELD_GUIDE_ALREADY_LINE
		|| npcBunniesEditorCore::NpcDesignerEditingField == npcBunniesEditorCore::NDE_FIELD_QUEST_IN_PROGRESS_LINE
		|| npcBunniesEditorCore::NpcDesignerEditingField == npcBunniesEditorCore::NDE_FIELD_QUEST_FINISHED_LINE;
}

bool NdeShiftKeyDown()
{
	return jjKey[16] || jjKey[160] || jjKey[161];
}

void NdeSaveActiveFieldValue(npcBunniesCore::npcBunny npc, const string &in value)
{
	npcBunniesEditorCore::NpcDesignerEditingValue = value;
	npcBunniesEditorCore::NpcDesignerEditingValueField = npcBunniesEditorCore::NpcDesignerEditingField;
	npcBunniesEditorCore::NpcDesignerEditingCursor = DbClampTextCursor(value, npcBunniesEditorCore::NpcDesignerEditingCursor);
	npcBunniesEditorViews::NdeKeepActiveTextFieldCursorViewport(value, npcBunniesEditorViews::NdeActiveTextFieldInnerWidth(npcBunniesEditorCore::NpcDesignerEditingField), npcBunniesEditorViews::NdeActiveTextFieldInnerHeight(npcBunniesEditorCore::NpcDesignerEditingField));
	if(NdeEditingFieldIsDecimal() && value.length > 0 && value[value.length - 1] == 46) return;
	NdeSetFieldValue(npc, npcBunniesEditorCore::NpcDesignerEditingField, value);
	npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
	npcBunniesEditorCore::NpcDesignerTextDirty = true;
	npcBunniesEditorCore::NpcDesignerTextDirtyTick = jjGameTicks;
}

void NdeFlushTextEdit()
{
	if(!npcBunniesEditorCore::NpcDesignerTextDirty) return;
	npcBunniesEditorCore::NpcDesignerTextDirty = false;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

void NdeEndTextEdit()
{
	NdeFlushTextEdit();
	npcBunniesEditorCore::NpcDesignerEditingField = -1;
	npcBunniesEditorCore::NpcDesignerEditingValueField = -1;
	npcBunniesEditorCore::NpcDesignerEditingCursor = 0;
}

void NdeInsertIntoActiveField(const string &in insert)
{
	if(!NdeValidNpc() || npcBunniesEditorCore::NpcDesignerEditingField < 0 || insert == "") return;
	if(insert == "\n" && (NdeEditingFieldIsNumeric() || !NdeEditingFieldAllowsNewline())) return;
	npcBunniesCore::npcBunny npc = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc];
	string value = NdeGetFieldValue(npc, npcBunniesEditorCore::NpcDesignerEditingField);
	npcBunniesEditorCore::NpcDesignerEditingCursor = DbClampTextCursor(value, npcBunniesEditorCore::NpcDesignerEditingCursor);
	value = DbInsertTextAtCursor(value, npcBunniesEditorCore::NpcDesignerEditingCursor, insert);
	npcBunniesEditorCore::NpcDesignerEditingCursor += int(insert.length);
	NdeSaveActiveFieldValue(npc, value);
}

void NdeMoveEditingCursor(int delta)
{
	if(!NdeValidNpc() || npcBunniesEditorCore::NpcDesignerEditingField < 0) return;
	npcBunniesCore::npcBunny npc = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc];
	string value = NdeGetFieldValue(npc, npcBunniesEditorCore::NpcDesignerEditingField);
	npcBunniesEditorCore::NpcDesignerEditingCursor = DbMoveTextCursor(value, npcBunniesEditorCore::NpcDesignerEditingCursor, delta);
	npcBunniesEditorViews::NdeKeepActiveTextFieldCursorViewport(value, npcBunniesEditorViews::NdeActiveTextFieldInnerWidth(npcBunniesEditorCore::NpcDesignerEditingField), npcBunniesEditorViews::NdeActiveTextFieldInnerHeight(npcBunniesEditorCore::NpcDesignerEditingField));
}

void NdeMoveEditingCursorVertical(int delta)
{
	if(!NdeValidNpc() || npcBunniesEditorCore::NpcDesignerEditingField < 0) return;
	npcBunniesCore::npcBunny npc = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc];
	string value = NdeGetFieldValue(npc, npcBunniesEditorCore::NpcDesignerEditingField);
	npcBunniesEditorCore::NpcDesignerEditingCursor = DbMoveTextCursorVertical(value, npcBunniesEditorViews::NdeActiveTextFieldInnerWidth(npcBunniesEditorCore::NpcDesignerEditingField), npcBunniesEditorCore::NpcDesignerEditingCursor, delta);
	npcBunniesEditorViews::NdeKeepActiveTextFieldCursorViewport(value, npcBunniesEditorViews::NdeActiveTextFieldInnerWidth(npcBunniesEditorCore::NpcDesignerEditingField), npcBunniesEditorViews::NdeActiveTextFieldInnerHeight(npcBunniesEditorCore::NpcDesignerEditingField));
}

void NdeTypeIntoField()
{
	if(!NdeValidNpc()) return;
	if(jjKey[20])
	{
		if(!npcBunniesEditorCore::NpcDesignerCapsKeyDown) npcBunniesEditorCore::NpcDesignerCapsLock = !npcBunniesEditorCore::NpcDesignerCapsLock;
		npcBunniesEditorCore::NpcDesignerCapsKeyDown = true;
	}
	else npcBunniesEditorCore::NpcDesignerCapsKeyDown = false;
	int key = -1;
	if((npcBunniesEditorCore::NpcDesignerEditingField == 5 || npcBunniesEditorCore::NpcDesignerEditingField == 6) && (jjKey[0xBE])) key = 46;
	if(key < 0 && jjKey[32]) key = 32;
	for(int i = 48; key < 0 && i <= 57; i++)
		if(jjKey[i]) key = i;
	for(int i = 65; key < 0 && i <= 90; i++)
		if(jjKey[i]) key = i;
	if(NdeFocusedButtonAction() != npcBunniesEditorCore::NDE_ACT_FIELD && key == 32) key = -1;
	if(key < 0)
	{
		if(jjKey[186]) key = 186;
		else if(jjKey[187]) key = 187;
		else if(jjKey[189]) key = 189;
		else if(jjKey[192]) key = 192;
		else if(jjKey[219]) key = 219;
		else if(jjKey[220]) key = 220;
		else if(jjKey[221]) key = 221;
		else if(jjKey[222]) key = 222;
		else if(jjKey[191]) key = 191;
		else if(jjKey[188]) key = 188;
		else if(jjKey[190]) key = 190;
		else if(jjKey[110]) key = 110;
	}
	if(key < 0 && jjKey[8]) key = 8;
	if(key < 0)
	{
		npcBunniesEditorCore::NpcDesignerLastTextKey = -1;
		return;
	}
	if(key == npcBunniesEditorCore::NpcDesignerLastTextKey && (key != 8 || jjGameTicks - npcBunniesEditorCore::NpcDesignerLastTextTick < 18)) return;
	if(jjGameTicks - npcBunniesEditorCore::NpcDesignerLastTextTick < 8) return;
	npcBunniesEditorCore::NpcDesignerLastTextKey = key;
	npcBunniesEditorCore::NpcDesignerLastTextTick = jjGameTicks;
	npcBunniesCore::npcBunny npc = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc];
	string value = NdeGetFieldValue(npc, npcBunniesEditorCore::NpcDesignerEditingField);
	npcBunniesEditorCore::NpcDesignerEditingCursor = DbClampTextCursor(value, npcBunniesEditorCore::NpcDesignerEditingCursor);
	if(key == 8)
	{
		if(npcBunniesEditorCore::NpcDesignerEditingCursor > 0)
		{
			value = DbBackspaceTextAtCursor(value, npcBunniesEditorCore::NpcDesignerEditingCursor);
			npcBunniesEditorCore::NpcDesignerEditingCursor--;
		}
	}
	else
	{
		bool numeric = NdeEditingFieldIsNumeric();
		bool decimalField = NdeEditingFieldIsDecimal();
		string special = NdeSpecialCharacterFromKey(key);
		bool decimalPoint = decimalField && value.findFirst(".") < 0 && (special == "." || key == 46 || key == 58 || key == 110 || key == 190);
		int dotAt = value.findFirst(".");
		bool decimalLimitReached = decimalField && dotAt >= 0 && key >= 48 && key <= 57 && npcBunniesEditorCore::NpcDesignerEditingCursor > dotAt && int(value.length) - dotAt - 1 >= 3;
		if(!decimalLimitReached && (!numeric || (key >= 48 && key <= 57) || decimalPoint))
		{
			string insert = " ";
			if(!numeric && special != "") insert[0] = special[0];
			else if(key >= 48 && key <= 57) insert[0] += key - 32;
			else if(decimalPoint) insert[0] = 46;
			else if(key >= 65 && key <= 90)
			{
				bool uppercase = npcBunniesEditorCore::NpcDesignerCapsLock || NdeShiftKeyDown();
				insert[0] += uppercase ? key - 32 : key;
			}
			else insert[0] = 32;
			value = DbInsertTextAtCursor(value, npcBunniesEditorCore::NpcDesignerEditingCursor, insert);
			npcBunniesEditorCore::NpcDesignerEditingCursor += int(insert.length);
		}
	}
	NdeSaveActiveFieldValue(npc, value);
}

/// <summary>
/// Maps supported keyboard keys to allowed special characters for text fields.
/// </summary>
string NdeSpecialCharacterFromKey(int key)
{
	string value = "";
	bool shifted = NdeShiftKeyDown();
	if(key == 49 && shifted) value = "!";
	else if(key == 50 && shifted) value = "@";
	else if(key == 51 && shifted) value = "#";
	else if(key == 52 && shifted) value = "$";
	else if(key == 53 && shifted) value = "%";
	else if(key == 54 && shifted) value = "^";
	else if(key == 55 && shifted) value = "&";
	else if(key == 56 && shifted) value = "*";
	else if(key == 57 && shifted) value = "(";
	else if(key == 48 && shifted) value = ")";
	else if(key == 186) value = shifted ? ":" : ";";
	else if(key == 187) value = shifted ? "+" : "=";
	else if(key == 189) value = shifted ? "_" : "-";
	else if(key == 191) value = shifted ? "?" : "/";
	else if(key == 188) value = shifted ? "<" : ",";
	else if(key == 190 || key == 110 || key == 58 || key == 46) value = shifted && key == 190 ? ">" : ".";
	else if(key == 219) value = shifted ? "{" : "[";
	else if(key == 221) value = shifted ? "}" : "]";
	else if(key == 220) value = shifted ? "|" : "\\";
	else if(key == 222) value = shifted ? "\"" : "'";
	else if(key == 192) value = shifted ? "~" : "`";
	if(value != "" && npcBunniesEditorCore::ALLOWED_SPECIAL_CHARACTERS.findFirst(value) >= 0) return value;
	return "";
}

/// <summary>
/// Reads the current value for an editor text field identifier.
/// </summary>
string NdeGetFieldValue(npcBunniesCore::npcBunny &inout npc, int field)
{
	if(field == npcBunniesEditorCore::NpcDesignerEditingField && field == npcBunniesEditorCore::NpcDesignerEditingValueField) return npcBunniesEditorCore::NpcDesignerEditingValue;
	if(field == 0) return npc.name;
	if(field == 1) return "" + npc.fur.c1;
	if(field == 2) return "" + npc.fur.c2;
	if(field == 3) return "" + npc.fur.c3;
	if(field == 4) return "" + npc.fur.c4;
	if(field == 5) return "" + npc.xTile;
	if(field == 6) return "" + npc.yTile;
	if(field == 10 && npcBunniesEditorCore::NpcDesignerDialogueSet < int(npc.dialogueSets.length))
	{
		NdeEnsureDialogueShape(npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet]);
		return npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet].dialogue[0].text;
	}
	if(field >= 20 && field < 40 && npcBunniesEditorCore::NpcDesignerDialogueSet < int(npc.dialogueSets.length))
	{
		int index = field == 20 ? 0 : field - 30;
		NdeEnsureChoice(npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet], index);
		return npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet].choices[index].choiceText;
	}
	if(field >= npcBunniesEditorCore::NDE_FIELD_GUIDE_PATH_BASE && field < npcBunniesEditorCore::NDE_FIELD_GUIDE_END_LINE)
	{
		int index = field - npcBunniesEditorCore::NDE_FIELD_GUIDE_PATH_BASE;
		if(index >= 0 && index < int(npc.guidePaths.length)) return npc.guidePaths[index].line;
	}
	if(field == npcBunniesEditorCore::NDE_FIELD_GUIDE_END_LINE) return npc.guideEndLine;
	if(field == npcBunniesEditorCore::NDE_FIELD_GUIDE_ALREADY_LINE) return npc.guideAlreadyAtDestinationLine;
	if(field == npcBunniesEditorCore::NDE_FIELD_QUEST_IN_PROGRESS_LINE && npcBunniesEditorCore::NpcDesignerDialogueSet < int(npc.dialogueSets.length)) return npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet].questInProgressLine;
	if(field == npcBunniesEditorCore::NDE_FIELD_QUEST_FINISHED_LINE && npcBunniesEditorCore::NpcDesignerDialogueSet < int(npc.dialogueSets.length)) return npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet].questCompletedLine;
	if(field == npcBunniesEditorCore::NDE_FIELD_NPC_TRACE_COLOR) return "" + npc.traceColor;
	int accessoryCategory = NdeAccessoryCategoryFromField(field);
	if(accessoryCategory >= 0)
	{
		int frame = NdeNpcAccessoryFrame(npc, accessoryCategory);
		return frame < 0 ? "" : "" + frame;
	}
	return "";
}

/// <summary>
/// Writes a new value for an editor text field identifier.
/// </summary>
void NdeSetFieldValue(npcBunniesCore::npcBunny &inout npc, int field, const string &in value)
{
	if(field == 0) npc.name = value;
	else if(field == 1) npc.fur.c1 = parseInt(value);
	else if(field == 2) npc.fur.c2 = parseInt(value);
	else if(field == 3) npc.fur.c3 = parseInt(value);
	else if(field == 4) npc.fur.c4 = parseInt(value);
	else if(field == 5) { npc.xTile = npcBunniesEditorCore::NdeRoundTileFloat(npcBunniesEditorCore::NdeClampFloat(parseFloat(value), 0, jjLayerWidth[4] - 1)); NdeApplyNpcTilePosition(npc); }
	else if(field == 6) { npc.yTile = npcBunniesEditorCore::NdeRoundTileFloat(npcBunniesEditorCore::NdeClampFloat(parseFloat(value), 0, jjLayerHeight[4] - 1)); NdeApplyNpcTilePosition(npc); }
	else if(field == 10 && npcBunniesEditorCore::NpcDesignerDialogueSet < int(npc.dialogueSets.length))
	{
		NdeEnsureDialogueShape(npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet]);
		npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet].dialogue[0].text = value;
	}
	else if(field >= 20 && field < 40 && npcBunniesEditorCore::NpcDesignerDialogueSet < int(npc.dialogueSets.length))
	{
		int index = field == 20 ? 0 : field - 30;
		NdeEnsureChoice(npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet], index);
		npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet].choices[index].choiceText = value;
	}
	else if(field >= npcBunniesEditorCore::NDE_FIELD_GUIDE_PATH_BASE && field < npcBunniesEditorCore::NDE_FIELD_GUIDE_END_LINE)
	{
		int index = field - npcBunniesEditorCore::NDE_FIELD_GUIDE_PATH_BASE;
		if(index >= 0 && index < int(npc.guidePaths.length)) npc.guidePaths[index].line = value;
	}
	else if(field == npcBunniesEditorCore::NDE_FIELD_GUIDE_END_LINE) npc.guideEndLine = value;
	else if(field == npcBunniesEditorCore::NDE_FIELD_GUIDE_ALREADY_LINE) npc.guideAlreadyAtDestinationLine = value;
	else if(field == npcBunniesEditorCore::NDE_FIELD_QUEST_IN_PROGRESS_LINE && npcBunniesEditorCore::NpcDesignerDialogueSet < int(npc.dialogueSets.length)) npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet].questInProgressLine = value;
	else if(field == npcBunniesEditorCore::NDE_FIELD_QUEST_FINISHED_LINE && npcBunniesEditorCore::NpcDesignerDialogueSet < int(npc.dialogueSets.length)) npc.dialogueSets[npcBunniesEditorCore::NpcDesignerDialogueSet].questCompletedLine = value;
	else if(field == npcBunniesEditorCore::NDE_FIELD_NPC_TRACE_COLOR) npc.traceColor = npcBunniesCore::ClampNpcTraceColor(parseInt(value));
	else
	{
		int accessoryCategory = NdeAccessoryCategoryFromField(field);
		if(accessoryCategory >= 0) NdeSetNpcAccessoryFrame(npc, accessoryCategory, value == "" ? -1 : parseInt(value));
	}
}

int NdeAccessoryCategoryFromField(int field)
{
	if(field == npcBunniesEditorCore::NDE_FIELD_NPC_HAT) return npcBunniesAccessories::NpcAccessories::CATEGORY_HAT;
	if(field == npcBunniesEditorCore::NDE_FIELD_NPC_SCARF) return npcBunniesAccessories::NpcAccessories::CATEGORY_SCARF;
	if(field == npcBunniesEditorCore::NDE_FIELD_NPC_GLASSES) return npcBunniesAccessories::NpcAccessories::CATEGORY_GLASSES;
	return -1;
}

int NdeNpcAccessoryFrame(npcBunniesCore::npcBunny &in npc, int category)
{
	if(category == npcBunniesAccessories::NpcAccessories::CATEGORY_HAT) return npc.hatFrame;
	if(category == npcBunniesAccessories::NpcAccessories::CATEGORY_SCARF) return npc.scarfFrame;
	if(category == npcBunniesAccessories::NpcAccessories::CATEGORY_GLASSES) return npc.glassesFrame;
	return -1;
}

void NdeSetNpcAccessoryFrame(npcBunniesCore::npcBunny &inout npc, int category, int frame)
{
	if(category == npcBunniesAccessories::NpcAccessories::CATEGORY_HAT && !npcBunniesAccessories::NpcAccessories::CanNpcUseHat(npc.type))
	{
		npc.hatFrame = -1;
		return;
	}
	int count = npcBunniesAccessories::NpcAccessories::FrameCount(category);
	if(count <= 0) frame = -1;
	else if(frame < 0) frame = -1;
	else if(frame >= count) frame = count - 1;
	if(category == npcBunniesAccessories::NpcAccessories::CATEGORY_HAT) npc.hatFrame = frame;
	else if(category == npcBunniesAccessories::NpcAccessories::CATEGORY_SCARF) npc.scarfFrame = frame;
	else if(category == npcBunniesAccessories::NpcAccessories::CATEGORY_GLASSES) npc.glassesFrame = frame;
}

void NdeCycleNpcAccessory(npcBunniesCore::npcBunny npc, int category, int delta)
{
	if(category == npcBunniesAccessories::NpcAccessories::CATEGORY_HAT && !npcBunniesAccessories::NpcAccessories::CanNpcUseHat(npc.type)) return;
	int count = npcBunniesAccessories::NpcAccessories::FrameCount(category);
	if(count <= 0) return;
	int frame = NdeNpcAccessoryFrame(npc, category) + delta;
	if(frame < -1) frame = count - 1;
	if(frame >= count) frame = -1;
	NdeSetNpcAccessoryFrame(npc, category, frame);
	npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

string NdeNpcSizeName(int sizeMode)
{
	if(sizeMode <= 0) return "Tiny";
	if(sizeMode >= 2) return "Giant";
	return "Normal";
}

void NdeCycleNpcSize(npcBunniesCore::npcBunny npc, int delta)
{
	npc.sizeMode += delta;
	if(npc.sizeMode < 0) npc.sizeMode = 2;
	if(npc.sizeMode > 2) npc.sizeMode = 0;
	npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

float NdeClampNpcSpeed(float speed)
{
	if(speed < 0.1) return 0.1;
	if(speed > 5.0) return 5.0;
	return speed;
}

string NdeNpcSpeedName(float speed)
{
	int tenths = int(NdeClampNpcSpeed(speed) * 10 + 0.5);
	return "" + (tenths / 10) + "." + (tenths % 10);
}

void NdeCycleNpcSpeed(npcBunniesCore::npcBunny npc, int delta)
{
	int tenths = int(NdeClampNpcSpeed(npc.moveSpeed) * 10 + 0.5) + delta;
	if(tenths < 1) tenths = 1;
	if(tenths > 50) tenths = 50;
	npc.moveSpeed = float(tenths) / 10.0;
	npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

npcBunniesCore::npcBunny NdeDialogueSpeakerNpc(npcBunniesCore::npcBunny &in fallbackNpc, npcBunniesDialogue::DialogueSet@ set)
{
	if(set !is null && set.speakerNpcIndex >= 0 && set.speakerNpcIndex < int(npcBunniesCore::npcBunnies.length)) return npcBunniesCore::npcBunnies[set.speakerNpcIndex];
	return fallbackNpc;
}

string NdeDialogueSpeakerName(npcBunniesCore::npcBunny &in fallbackNpc, npcBunniesDialogue::DialogueSet@ set)
{
	if(set is null || set.speakerNpcIndex < 0) return "default";
	if(set.speakerNpcIndex >= int(npcBunniesCore::npcBunnies.length)) return "default";
	string name = npcBunniesCore::npcBunnies[set.speakerNpcIndex].name;
	if(name == "") name = "NPC " + (set.speakerNpcIndex + 1);
	return name;
}

void NdeCycleDialogueSpeaker(npcBunniesDialogue::DialogueSet@ set, int delta)
{
	if(set is null || npcBunniesCore::npcBunnies.length == 0) return;
	array<int> choices;
	choices.insertLast(-1);
	for(uint i = 0; i < npcBunniesCore::npcBunnies.length; i++)
		if(int(i) != npcBunniesEditorCore::NpcDesignerSelectedNpc) choices.insertLast(int(i));
	int current = choices.find(set.speakerNpcIndex);
	if(current < 0) current = 0;
	current += delta < 0 ? -1 : 1;
	if(current < 0) current = int(choices.length) - 1;
	if(current >= int(choices.length)) current = 0;
	set.speakerNpcIndex = choices[current];
	npcBunniesEditorPreview::NdeClampDialogueBigFrame(NdeDialogueSpeakerNpc(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc], set), set);
}

string NdeDialogueAutoContinueName(npcBunniesCore::npcBunny &in npc, npcBunniesDialogue::DialogueSet@ set)
{
	if(set is null || set.autoContinueSet == -2) return "-";
	if(set.autoContinueSet == -1) return "next set";
	if(set.autoContinueSet >= 0 && set.autoContinueSet < int(npc.dialogueSets.length)) return "set " + (set.autoContinueSet + 1);
	return "-";
}

string NdeDialogueGoToName(npcBunniesCore::npcBunny &in npc, npcBunniesDialogue::DialogueSet@ set)
{
	if(set is null || set.goToSet == -1) return "next";
	if(set.goToSet == -2) return "stay";
	if(set.goToSet >= 0 && set.goToSet < int(npc.dialogueSets.length)) return "set " + (set.goToSet + 1);
	return "next";
}

void NdeBuildDialogueGoToChoices(npcBunniesCore::npcBunny &in npc, array<int> &out choices)
{
	choices.resize(0);
	choices.insertLast(-1);
	choices.insertLast(-2);
	for(uint i = 0; i < npc.dialogueSets.length; i++) choices.insertLast(int(i));
}

void NdeCycleDialogueGoTo(npcBunniesCore::npcBunny &in npc, npcBunniesDialogue::DialogueSet@ set, int delta)
{
	if(set is null) return;
	array<int> choices;
	NdeBuildDialogueGoToChoices(npc, choices);
	int current = choices.find(set.goToSet);
	if(current < 0) current = 0;
	current += delta < 0 ? -1 : 1;
	if(current < 0) current = int(choices.length) - 1;
	if(current >= int(choices.length)) current = 0;
	set.goToSet = choices[current];
}

void NdeBuildDialogueAutoContinueChoices(npcBunniesCore::npcBunny &in npc, array<int> &out choices)
{
	choices.resize(0);
	choices.insertLast(-2);
	if(!NdeDialogueAutoContinueWouldLoop(npc, npcBunniesEditorCore::NpcDesignerDialogueSet, -1)) choices.insertLast(-1);
	for(uint i = 0; i < npc.dialogueSets.length; i++)
	{
		if(int(i) == npcBunniesEditorCore::NpcDesignerDialogueSet) continue;
		if(NdeDialogueAutoContinueWouldLoop(npc, npcBunniesEditorCore::NpcDesignerDialogueSet, int(i))) continue;
		choices.insertLast(int(i));
	}
}

int NdeDialogueAutoContinueTarget(npcBunniesCore::npcBunny &in npc, int sourceSet, int autoContinueSet)
{
	if(autoContinueSet == -2) return -1;
	if(autoContinueSet == -1) return sourceSet + 1;
	return autoContinueSet;
}

bool NdeDialogueAutoContinueWouldLoop(npcBunniesCore::npcBunny &in npc, int sourceSet, int candidate)
{
	int target = NdeDialogueAutoContinueTarget(npc, sourceSet, candidate);
	for(int guard = 0; guard < int(npc.dialogueSets.length) + 1; guard++)
	{
		if(target < 0 || target >= int(npc.dialogueSets.length)) return false;
		if(target == sourceSet) return true;
		npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[target];
		if(set is null || set.choicesAnswerMethod != npcBunniesCore::DialogueAction::Normal || set.autoContinueSet == -2) return false;
		target = NdeDialogueAutoContinueTarget(npc, target, set.autoContinueSet);
	}
	return true;
}

void NdeCycleDialogueAutoContinue(npcBunniesCore::npcBunny &in npc, npcBunniesDialogue::DialogueSet@ set, int delta)
{
	if(set is null) return;
	array<int> choices;
	NdeBuildDialogueAutoContinueChoices(npc, choices);
	int current = choices.find(set.autoContinueSet);
	if(current < 0) current = 0;
	current += delta < 0 ? -1 : 1;
	if(current < 0) current = int(choices.length) - 1;
	if(current >= int(choices.length)) current = 0;
	set.autoContinueSet = choices[current];
}

string NdeNpcAccessoryDisplay(npcBunniesCore::npcBunny &in npc, int category)
{
	int frame = NdeNpcAccessoryFrame(npc, category);
	if(frame < 0)
	{
		if(category == npcBunniesAccessories::NpcAccessories::CATEGORY_HAT) return "No Hat";
		if(category == npcBunniesAccessories::NpcAccessories::CATEGORY_SCARF) return "No scarf";
		return "No glasses";
	}
	return "" + frame;
}

/// <summary>
/// Creates the selected NPC type at the mouse position in the level.
/// </summary>
void NdePlaceNewNpcAtMouse(jjPLAYER@ play, int mouseX, int mouseY)
{
	npcBunniesCore::npcBunny npc; npcBunniesParser::SetNpcDefaults(npc);
	npc.name = npcBunniesEditorCore::NpcDesignerNpcNames[npcBunniesEditorCore::NpcDesignerSelectedType];
	npc.type = npcBunniesAnimations::getTypeFromTypeName(npc.name);
	NdeSetNpcTileFromMouse(npc, play, mouseX, mouseY);
	npcBunniesParser::AddParsedNpc(npc);
	npcBunniesEditorCore::NpcDesignerSelectedNpc = int(npcBunniesCore::npcBunnies.length) - 1;
	npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_EDIT;
	npcBunniesEditorCore::NpcDesignerSelectedType = -1;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

/// <summary>
/// Moves the selected NPC to the mouse position in the level.
/// </summary>
void NdeMoveSelectedNpcToMouse(jjPLAYER@ play, int mouseX, int mouseY)
{
	npcBunniesCore::npcBunny npc = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc];
	NdeSetNpcTileFromMouse(npc, play, mouseX, mouseY);
	NdeApplyNpcTilePosition(npc);
	npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
	npcBunniesEditorCore::NpcDesignerMovingNpc = false;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

/// <summary>
/// Converts the mouse screen position to tile coordinates and stores them on an NPC.
/// </summary>
void NdeSetNpcTileFromMouse(npcBunniesCore::npcBunny &inout npc, jjPLAYER@ play, int mouseX, int mouseY)
{
	float worldX = play.cameraX + mouseX;
	float worldY = play.cameraY + mouseY;
	npc.xTile = npcBunniesEditorCore::NdeClamp(int(worldX / 32.0 + npcBunniesEditorCore::NDE_MOUSE_PLACE_TILE_OFFSET_X), 0, jjLayerWidth[4] - 1);
	npc.yTile = npcBunniesEditorCore::NdeClamp(int(worldY / 32.0 + npcBunniesEditorCore::NDE_MOUSE_PLACE_TILE_OFFSET_Y), 0, jjLayerHeight[4] - 1);
}

void NdeSetTeleportEditFromMouse(jjPLAYER@ play, int mouseX, int mouseY)
{
	npcBunniesEditorCore::NpcDesignerTeleportEditX = int((play.cameraX + mouseX) / 32);
	npcBunniesEditorCore::NpcDesignerTeleportEditY = int((play.cameraY + mouseY) / 32);
	NdeCenterEditorCameraOnTile(npcBunniesEditorCore::NpcDesignerTeleportEditX, npcBunniesEditorCore::NpcDesignerTeleportEditY);
}

/// <summary>
/// Applies an NPC's tile coordinates to its live object and follow/fall target positions.
/// </summary>
void NdeApplyNpcTilePosition(npcBunniesCore::npcBunny &inout npc)
{
	if(npc.objectID >= 0)
	{
		jjObjects[npc.objectID].xPos = npc.xTile * 32;
		jjObjects[npc.objectID].yPos = npc.yTile * 32;
		jjObjects[npc.objectID].direction = npc.direction;
	}
	npc.fallToX = npc.xTile * 32;
	npc.fallToY = npc.yTile * 32;
}

/// <summary>
/// Restores the most recently deleted NPC.
/// </summary>
void NdeUndoDelete()
{
	if(!npcBunniesEditorCore::NpcDesignerHasDeletedNpc) return;
	npcBunniesParser::AddParsedNpc(npcBunniesEditorCore::NpcDesignerDeletedNpc);
	npcBunniesEditorCore::NpcDesignerHasDeletedNpc = false;
	npcBunniesEditorCore::NpcDesignerSelectedNpc = int(npcBunniesCore::npcBunnies.length) - 1;
	npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_EDIT;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

/// <summary>
/// Deletes an NPC and stores it for undo.
/// </summary>
void NdeDeleteNpc(int index)
{
	if(index < 0 || index >= int(npcBunniesCore::npcBunnies.length)) return;
	npcBunniesEditorCore::NpcDesignerDeletedNpc = npcBunniesCore::npcBunnies[index];
	npcBunniesEditorCore::NpcDesignerHasDeletedNpc = true;
	if(npcBunniesCore::npcBunnies[index].objectID >= 0) jjObjects[npcBunniesCore::npcBunnies[index].objectID].delete();
	npcBunniesCore::npcBunnies.removeAt(index);
	for(uint i = 0; i < npcBunniesCore::npcBunnies.length; i++) npcBunniesCore::npcBunnies[i].id = int(i);
	npcBunniesEditorCore::NpcDesignerSelectedNpc = -1;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

/// <summary>
/// Creates a default shop item.
/// </summary>
npcBunniesShop::NpcShopItem@ NdeNewShopItem()
{
	npcBunniesShop::NpcShopItem@ item = @npcBunniesShop::NpcShopItem();
	item.priceCoins = 0;
	item.priceGems = 0;
	item.stock = -1;
	NdeApplyShopReward(item.item, 0);
	item.name = NdeShopRewardName(item.item);
	return item;
}

/// <summary>
/// Cycles the selected shop reward value.
/// </summary>
void NdeCycleShopReward(npcBunniesCore::npcBunny npc, int index, int delta)
{
	if(index < 0 || index >= int(npc.shopItems.length)) return;
	npcBunniesQuests::NpcReward@ reward = npc.shopItems[index].item;
	int itemIndex = NdeFindShopRewardIndex(reward);
	itemIndex += delta;
	while(itemIndex < 0) itemIndex += int(npcBunniesEditorCore::NpcDesignerShopRewardTypes.length);
	while(itemIndex >= int(npcBunniesEditorCore::NpcDesignerShopRewardTypes.length)) itemIndex -= int(npcBunniesEditorCore::NpcDesignerShopRewardTypes.length);
	NdeApplyShopReward(reward, itemIndex);
	npc.shopItems[index].name = NdeShopRewardName(reward);
	npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

/// <summary>
/// Global editor state for find shop reward index.
/// </summary>
int NdeFindShopRewardIndex(npcBunniesQuests::NpcReward@ reward)
{
	if(reward is null) return 0;
	for(uint i = 0; i < npcBunniesEditorCore::NpcDesignerShopRewardTypes.length; i++)
	{
		if(reward.rewardType == npcBunniesEditorCore::NpcDesignerShopRewardTypes[i] && (reward.rewardType == npcBunniesQuests::RewardType::RewardFastFire || reward.weaponType == npcBunniesEditorCore::NpcDesignerShopWeaponTypes[i]))
			return int(i);
	}
	return 0;
}

/// <summary>
/// Applies the selected editor list value to shop reward data.
/// </summary>
void NdeApplyShopReward(npcBunniesQuests::NpcReward@ reward, int index)
{
	if(reward is null) return;
	index = npcBunniesEditorCore::NdeClamp(index, 0, int(npcBunniesEditorCore::NpcDesignerShopRewardTypes.length) - 1);
	reward.rewardType = npcBunniesEditorCore::NpcDesignerShopRewardTypes[index];
	reward.weaponType = npcBunniesEditorCore::NpcDesignerShopWeaponTypes[index];
	reward.amount = reward.rewardType == npcBunniesQuests::RewardType::RewardFastFire ? 5 : (reward.rewardType == npcBunniesQuests::RewardType::RewardAmmo ? 5 : 1);
}

/// <summary>
/// Returns the display name for a shop reward item.
/// </summary>
string NdeShopRewardName(npcBunniesQuests::NpcReward@ reward)
{
	if(reward is null) return "Item";
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardAmmo) return npcBunniesShop::GetWeaponDisplayName(reward.weaponType) + " ammo";
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardPowerup) return npcBunniesShop::GetWeaponDisplayName(reward.weaponType) + " powerup";
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardFastFire) return "Fastfire";
	return "Item";
}

/// <summary>
/// Changes the selected shop cost value and saves the NPC file.
/// </summary>
void NdeChangeShopCost(npcBunniesCore::npcBunny npc, int index, bool coins, int delta)
{
	if(index < 0 || index >= int(npc.shopItems.length)) return;
	if(coins) npc.shopItems[index].priceCoins = npcBunniesEditorCore::NdeMax(0, npc.shopItems[index].priceCoins + delta);
	else npc.shopItems[index].priceGems = npcBunniesEditorCore::NdeMax(0, npc.shopItems[index].priceGems + delta);
	npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

/// <summary>
/// Changes the selected shop amount value and saves the NPC file.
/// </summary>
void NdeChangeShopAmount(npcBunniesCore::npcBunny npc, int index, int delta)
{
	if(index < 0 || index >= int(npc.shopItems.length)) return;
	npc.shopItems[index].item.amount = npcBunniesEditorCore::NdeMax(1, npc.shopItems[index].item.amount + delta);
	npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

/// <summary>
/// Changes the selected shop stock value and saves the NPC file.
/// </summary>
void NdeChangeShopStock(npcBunniesCore::npcBunny npc, int index, int delta)
{
	if(index < 0 || index >= int(npc.shopItems.length)) return;
	if(npc.shopItems[index].stock < 0) npc.shopItems[index].stock = delta > 0 ? 1 : -1;
	else
	{
		npc.shopItems[index].stock += delta;
		if(npc.shopItems[index].stock < 1) npc.shopItems[index].stock = -1;
	}
	npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

/// <summary>
/// Cycles the selected quest value.
/// </summary>
void NdeCycleQuest(int index, int delta)
{
	npcBunniesDialogue::DialogueSet@ set = NdeCurrentSet();
	if(index < 0 || index >= int(set.quests.length)) return;
	int itemIndex = NdeFindQuestIndex(set.quests[index]) + delta;
	while(itemIndex < 0) itemIndex += int(npcBunniesEditorCore::NpcDesignerQuestTypes.length);
	while(itemIndex >= int(npcBunniesEditorCore::NpcDesignerQuestTypes.length)) itemIndex -= int(npcBunniesEditorCore::NpcDesignerQuestTypes.length);
	NdeApplyQuestItem(set.quests[index], itemIndex);
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

/// <summary>
/// Global editor state for find quest index.
/// </summary>
int NdeFindQuestIndex(npcBunniesQuests::NpcQuest@ quest)
{
	if(quest is null) return 0;
	for(uint i = 0; i < npcBunniesEditorCore::NpcDesignerQuestTypes.length; i++)
	{
		if(quest.questType != npcBunniesEditorCore::NpcDesignerQuestTypes[i]) continue;
		if(quest.questType == npcBunniesQuests::QuestType::Gems && quest.gemType != npcBunniesEditorCore::NpcDesignerQuestSubTypes[i]) continue;
		if((quest.questType == npcBunniesQuests::QuestType::DefeatMonster || quest.questType == npcBunniesQuests::QuestType::DefeatBoss) && quest.objectType != npcBunniesEditorCore::NpcDesignerQuestSubTypes[i]) continue;
		return int(i);
	}
	return 0;
}

/// <summary>
/// Applies the selected editor list value to quest item data.
/// </summary>
void NdeApplyQuestItem(npcBunniesQuests::NpcQuest@ quest, int index)
{
	if(quest is null) return;
	index = npcBunniesEditorCore::NdeClamp(index, 0, int(npcBunniesEditorCore::NpcDesignerQuestTypes.length) - 1);
	quest.questType = npcBunniesEditorCore::NpcDesignerQuestTypes[index];
	if(quest.questType == npcBunniesQuests::QuestType::Gems) quest.gemType = npcBunniesEditorPreview::NdeGemFromInt(npcBunniesEditorCore::NpcDesignerQuestSubTypes[index]);
	else if(quest.questType == npcBunniesQuests::QuestType::DefeatMonster || quest.questType == npcBunniesQuests::QuestType::DefeatBoss) quest.objectType = npcBunniesEditorCore::NpcDesignerQuestSubTypes[index];
}

/// <summary>
/// Cycles the selected reward value.
/// </summary>
void NdeCycleReward(int index, int delta)
{
	npcBunniesDialogue::DialogueSet@ set = NdeCurrentSet();
	if(index < 0 || index >= int(set.rewards.length)) return;
	array<int> rewardTypes;
	array<int> rewardSubTypes;
	NdeBuildRewardChoices(npcBunniesEditorCore::NpcDesignerDialogueSet, index, rewardTypes, rewardSubTypes);
	if(rewardTypes.length == 0) return;
	int itemIndex = NdeFindRewardIndex(set.rewards[index], rewardTypes, rewardSubTypes) + delta;
	while(itemIndex < 0) itemIndex += int(rewardTypes.length);
	while(itemIndex >= int(rewardTypes.length)) itemIndex -= int(rewardTypes.length);
	NdeApplyRewardItem(set.rewards[index], itemIndex, rewardTypes, rewardSubTypes);
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

/// <summary>
/// Builds the reward type list, including guide paths that are not already claimed by another reward.
/// </summary>
void NdeBuildRewardChoices(int currentSetIndex, int currentRewardIndex, array<int> &out rewardTypes, array<int> &out rewardSubTypes)
{
	rewardTypes.resize(0);
	rewardSubTypes.resize(0);
	for(uint i = 0; i < npcBunniesEditorCore::NpcDesignerRewardTypes.length; i++)
	{
		rewardTypes.insertLast(npcBunniesEditorCore::NpcDesignerRewardTypes[i]);
		rewardSubTypes.insertLast(npcBunniesEditorCore::NpcDesignerRewardSubTypes[i]);
	}
	if(!NdeValidNpc()) return;
	npcBunniesCore::npcBunny npc = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc];
	if(!npc.guideEnabled || npc.guidePaths.length == 0) return;
	for(uint path = 0; path < npc.guidePaths.length; path++)
	{
		if(!NdeGuidePathRewardUsedByOther(npc, int(path), currentSetIndex, currentRewardIndex))
		{
			rewardTypes.insertLast(npcBunniesQuests::RewardType::RewardGuidePath);
			rewardSubTypes.insertLast(int(path));
		}
	}
}

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

string NdeGuideEntryRewardName(int index)
{
	if(!NdeValidNpc() || index < 0 || index >= int(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths.length)) return "Guide entry";
	return NdeGuideEntryDisplayName(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc], index);
}

string NdeGuideEntryDisplayName(npcBunniesCore::npcBunny &in npc, int index)
{
	if(index < 0 || index >= int(npc.guidePaths.length)) return "Guide entry";
	bool teleport = npc.guidePaths[index].isTeleport;
	int number = 0;
	for(int i = 0; i <= index && i < int(npc.guidePaths.length); i++)
	{
		if(npc.guidePaths[i].isTeleport == teleport) number++;
	}
	return (teleport ? "Teleport " : "Path ") + number;
}

void NdeRemoveGuidePathRewardReferences(npcBunniesCore::npcBunny &inout npc, int removedPathIndex)
{
	for(uint s = 0; s < npc.dialogueSets.length; s++)
	{
		npcBunniesDialogue::DialogueSet@ set = npc.dialogueSets[s];
		for(uint r = 0; r < set.rewards.length; r++)
		{
			npcBunniesQuests::NpcReward@ reward = set.rewards[r];
			if(reward is null || reward.rewardType != npcBunniesQuests::RewardType::RewardGuidePath) continue;
			if(reward.guidePathIndex == removedPathIndex)
			{
				reward.rewardType = npcBunniesQuests::RewardType::RewardCoins;
				reward.amount = 1;
				reward.guidePathIndex = -1;
			}
			else if(reward.guidePathIndex > removedPathIndex)
				reward.guidePathIndex--;
		}
	}
}

/// <summary>
/// Global editor state for find reward index.
/// </summary>
int NdeFindRewardIndex(npcBunniesQuests::NpcReward@ reward, array<int> &in rewardTypes, array<int> &in rewardSubTypes)
{
	if(reward is null) return 0;
	for(uint i = 0; i < rewardTypes.length; i++)
	{
		if(reward.rewardType != rewardTypes[i]) continue;
		if(reward.rewardType == npcBunniesQuests::RewardType::RewardGems && reward.gemType != rewardSubTypes[i]) continue;
		if((reward.rewardType == npcBunniesQuests::RewardType::RewardAmmo || reward.rewardType == npcBunniesQuests::RewardType::RewardPowerup) && reward.weaponType != rewardSubTypes[i]) continue;
		if(reward.rewardType == npcBunniesQuests::RewardType::RewardGuidePath && reward.guidePathIndex != rewardSubTypes[i]) continue;
		return int(i);
	}
	return 0;
}

/// <summary>
/// Applies the selected editor list value to reward item data.
/// </summary>
void NdeApplyRewardItem(npcBunniesQuests::NpcReward@ reward, int index, array<int> &in rewardTypes, array<int> &in rewardSubTypes)
{
	if(reward is null) return;
	index = npcBunniesEditorCore::NdeClamp(index, 0, int(rewardTypes.length) - 1);
	reward.rewardType = rewardTypes[index];
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardGems) reward.gemType = npcBunniesEditorPreview::NdeGemFromInt(rewardSubTypes[index]);
	else if(reward.rewardType == npcBunniesQuests::RewardType::RewardAmmo || reward.rewardType == npcBunniesQuests::RewardType::RewardPowerup) reward.weaponType = rewardSubTypes[index];
	else if(reward.rewardType == npcBunniesQuests::RewardType::RewardGuidePath) reward.guidePathIndex = rewardSubTypes[index];
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardTeleport) { reward.amount = 1; reward.x = int(reward.x); reward.y = int(reward.y); }
	else if(reward.rewardType == npcBunniesQuests::RewardType::RewardTrigger) { reward.amount = 1; if(reward.triggerId < 0) reward.triggerId = 0; }
	else if(reward.rewardType == npcBunniesQuests::RewardType::RewardPowerup || reward.rewardType == npcBunniesQuests::RewardType::RewardMoveable || reward.rewardType == npcBunniesQuests::RewardType::RewardGuidePath) reward.amount = 1;
	else if(reward.amount <= 0) reward.amount = reward.rewardType == npcBunniesQuests::RewardType::RewardFastFire ? 5 : 1;
}

/// <summary>
/// Changes the selected reward amount value and saves the NPC file.
/// </summary>
void NdeChangeRewardAmount(int index, int encodedDelta)
{
	npcBunniesDialogue::DialogueSet@ set = NdeCurrentSet();
	if(index < 0 || index >= int(set.rewards.length)) return;
	npcBunniesQuests::NpcReward@ reward = set.rewards[index];
	int delta = encodedDelta < 0 ? -1 : 1;
	int part = npcBunniesEditorCore::NdeAbs(encodedDelta) / 10;
	if(reward.rewardType == npcBunniesQuests::RewardType::RewardTeleport)
	{
		if(part == 1) reward.x = npcBunniesEditorCore::NdeMax(0, int(reward.x) + delta);
		else if(part == 2) reward.y = npcBunniesEditorCore::NdeMax(0, int(reward.y) + delta);
		else reward.amount = npcBunniesEditorCore::NdeMax(0, reward.amount + delta);
	}
	else if(reward.rewardType == npcBunniesQuests::RewardType::RewardTrigger)
	{
		reward.triggerId = npcBunniesEditorCore::NdeMax(0, (reward.triggerId < 0 ? 0 : reward.triggerId) + delta);
		reward.amount = 1;
	}
	else if(reward.rewardType == npcBunniesQuests::RewardType::RewardGuidePath)
	{
		reward.amount = 1;
	}
	else reward.amount = npcBunniesEditorCore::NdeMax(0, reward.amount + delta);
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

/// <summary>
/// Records arrow-key guide path steps while path editing is active.
/// </summary>
void NdeRecordPathKeys()
{
	if(!NdeValidNpc() || npcBunniesEditorCore::NpcDesignerPathIndex < 0 || npcBunniesEditorCore::NpcDesignerPathIndex >= int(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths.length)) return;
	if(jjGameTicks - npcBunniesEditorCore::NpcDesignerLastKey < 8) return;
	if(npcBunniesEditorCore::NpcDesignerEditingTeleport)
	{
		NdeRecordTeleportKeys();
		return;
	}
	if(jjKey[0x0D] || jjKey[0x1B])
	{
		npcBunniesEditorCore::NpcDesignerLastKey = jjGameTicks;
		npcBunniesEditorCore::NpcDesignerRecordingPath = false;
		NdeUnfreezeEditorCamera();
		npcBunniesEditorSerialization::NdeSaveNpcFile();
		return;
	}
	int dx = 0, dy = 0;
	if(jjKey[0x25]) dx = -1;
	else if(jjKey[0x27]) dx = 1;
	else if(jjKey[0x26]) dy = -1;
	else if(jjKey[0x28]) dy = 1;
	if(dx == 0 && dy == 0) return;
	npcBunniesEditorCore::NpcDesignerLastKey = jjGameTicks;
	npcBunniesCore::GuidePath@ path = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex];
	int startX, startY;
	NdeGuidePathStart(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc], npcBunniesEditorCore::NpcDesignerPathIndex, startX, startY);
	int currentX = startX, currentY = startY;
	for(uint i = 0; i < path.steps.length; i++)
	{
		currentX += path.steps[i].x;
		currentY += path.steps[i].y;
	}
	int nextX = currentX + dx, nextY = currentY + dy;
	if(nextX == startX && nextY == startY)
	{
		path.steps.resize(0);
		NdeCenterEditorCameraOnTile(startX, startY);
		npcBunniesEditorSerialization::NdeSaveNpcFile();
		return;
	}
	int testX = startX, testY = startY;
	for(uint i = 0; i < path.steps.length; i++)
	{
		testX += path.steps[i].x;
		testY += path.steps[i].y;
		if(nextX == testX && nextY == testY)
		{
			path.steps.resize(i + 1);
			NdeCenterEditorCameraOnTile(nextX, nextY);
			npcBunniesEditorSerialization::NdeSaveNpcFile();
			return;
		}
	}
	path.steps.insertLast(@npcBunniesCore::GuideStep(dx, dy));
	NdeGuidePathEnd(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc], npcBunniesEditorCore::NpcDesignerPathIndex, nextX, nextY);
	NdeCenterEditorCameraOnTile(nextX, nextY);
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

void NdeRecordTeleportKeys()
{
	if(jjKey[0x1B])
	{
		npcBunniesEditorCore::NpcDesignerLastKey = jjGameTicks;
		NdeCancelGuideEntryEdit();
		npcBunniesEditorSerialization::NdeSaveNpcFile();
		return;
	}
	if(jjKey[0x0D] || npcBunniesEditorCore::NpcDesignerTeleportFirePressed || (jjLocalPlayerCount > 0 && jjLocalPlayers[0].keyFire))
	{
		npcBunniesEditorCore::NpcDesignerLastKey = jjGameTicks;
		npcBunniesEditorCore::NpcDesignerTeleportFirePressed = false;
		NdeCommitTeleportEdit();
		return;
	}
	int dx = 0, dy = 0;
	if(jjKey[0x25]) dx = -1;
	else if(jjKey[0x27]) dx = 1;
	else if(jjKey[0x26]) dy = -1;
	else if(jjKey[0x28]) dy = 1;
	if(dx == 0 && dy == 0) return;
	npcBunniesEditorCore::NpcDesignerLastKey = jjGameTicks;
	npcBunniesEditorCore::NpcDesignerTeleportEditX += dx;
	npcBunniesEditorCore::NpcDesignerTeleportEditY += dy;
	NdeCenterEditorCameraOnTile(npcBunniesEditorCore::NpcDesignerTeleportEditX, npcBunniesEditorCore::NpcDesignerTeleportEditY);
}

/// <summary>
/// Draws the recorded path portion of the NPC Designer UI.
/// </summary>
void NdeDrawRecordedPath(jjCANVAS@ canvas)
{
	if(npcBunniesEditorCore::NpcDesignerView != npcBunniesEditorCore::NDE_PATH_RECORD || !NdeValidNpc() || npcBunniesEditorCore::NpcDesignerPathIndex < 0 || npcBunniesEditorCore::NpcDesignerPathIndex >= int(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths.length)) return;
	npcBunniesCore::npcBunny npc = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc];
	if(npc.guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex].isTeleport)
	{
		int tx = npcBunniesEditorCore::NpcDesignerEditingTeleport ? npcBunniesEditorCore::NpcDesignerTeleportEditX : npc.guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex].teleportX;
		int ty = npcBunniesEditorCore::NpcDesignerEditingTeleport ? npcBunniesEditorCore::NpcDesignerTeleportEditY : npc.guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex].teleportY;
		int screenX, screenY;
		NdeWorldToScreen(tx * 32, ty * 32, screenX, screenY);
		canvas.drawRectangle(screenX, screenY, 32, 32, 80, SPRITE::TRANSLUCENT);
		return;
	}
	int tx, ty;
	NdeGuidePathStart(npc, npcBunniesEditorCore::NpcDesignerPathIndex, tx, ty);
	for(uint i = 0; i < npc.guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex].steps.length; i++)
	{
		tx += npc.guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex].steps[i].x;
		ty += npc.guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex].steps[i].y;
		int screenX, screenY;
		NdeWorldToScreen(tx * 32 + 12, ty * 32 + 12, screenX, screenY);
		canvas.drawRectangle(screenX, screenY, 8, 8, 24, SPRITE::NORMAL);
	}
}

/// <summary>
/// Calculates the tile where a guide path starts.
/// </summary>
void NdeGuidePathStart(npcBunniesCore::npcBunny &in npc, int pathIndex, int &out tx, int &out ty)
{
	tx = int(npc.xTile);
	ty = int(npc.yTile);
	for(int path = 0; path < pathIndex && path < int(npc.guidePaths.length); path++)
	{
		if(npc.guidePaths[path].isTeleport)
		{
			tx = npc.guidePaths[path].teleportX;
			ty = npc.guidePaths[path].teleportY;
			continue;
		}
		for(uint step = 0; step < npc.guidePaths[path].steps.length; step++)
		{
			tx += npc.guidePaths[path].steps[step].x;
			ty += npc.guidePaths[path].steps[step].y;
		}
	}
}

void NdeGuidePathEnd(npcBunniesCore::npcBunny &in npc, int pathIndex, int &out tx, int &out ty)
{
	NdeGuidePathStart(npc, pathIndex, tx, ty);
	if(pathIndex < 0 || pathIndex >= int(npc.guidePaths.length)) return;
	if(npc.guidePaths[pathIndex].isTeleport)
	{
		tx = npc.guidePaths[pathIndex].teleportX;
		ty = npc.guidePaths[pathIndex].teleportY;
		return;
	}
	for(uint step = 0; step < npc.guidePaths[pathIndex].steps.length; step++)
	{
		tx += npc.guidePaths[pathIndex].steps[step].x;
		ty += npc.guidePaths[pathIndex].steps[step].y;
	}
}

void NdeAddGuideTeleport(npcBunniesCore::npcBunny &inout npc)
{
	npcBunniesCore::GuidePath@ teleport = @npcBunniesCore::GuidePath();
	teleport.isTeleport = true;
	int tx, ty;
	NdeGuidePathEnd(npc, int(npc.guidePaths.length) - 1, tx, ty);
	teleport.teleportX = tx;
	teleport.teleportY = ty;
	npc.guidePaths.insertLast(teleport);
	npc.guideEnabled = true;
	npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
	npcBunniesEditorCore::NpcDesignerPathIndex = int(npc.guidePaths.length) - 1;
	npcBunniesEditorCore::NpcDesignerView = npcBunniesEditorCore::NDE_PATH_RECORD;
	NdeStartTeleportEdit(true);
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

void NdeStartTeleportEdit(bool isNew)
{
	if(!NdeValidNpc() || npcBunniesEditorCore::NpcDesignerPathIndex < 0 || npcBunniesEditorCore::NpcDesignerPathIndex >= int(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths.length)) return;
	npcBunniesCore::GuidePath@ entry = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex];
	if(entry is null || !entry.isTeleport) return;
	npcBunniesEditorCore::NpcDesignerRecordingPath = false;
	npcBunniesEditorCore::NpcDesignerEditingTeleport = true;
	npcBunniesEditorCore::NpcDesignerTeleportIsNew = isNew;
	npcBunniesEditorCore::NpcDesignerTeleportFirePressed = false;
	npcBunniesEditorCore::NpcDesignerTeleportEditX = entry.teleportX;
	npcBunniesEditorCore::NpcDesignerTeleportEditY = entry.teleportY;
	npcBunniesEditorCore::NpcDesignerTeleportOriginalX = entry.teleportX;
	npcBunniesEditorCore::NpcDesignerTeleportOriginalY = entry.teleportY;
	NdeCenterEditorCameraOnTile(npcBunniesEditorCore::NpcDesignerTeleportEditX, npcBunniesEditorCore::NpcDesignerTeleportEditY);
}

void NdeCommitTeleportEdit()
{
	if(!NdeValidNpc() || npcBunniesEditorCore::NpcDesignerPathIndex < 0 || npcBunniesEditorCore::NpcDesignerPathIndex >= int(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths.length)) return;
	npcBunniesCore::GuidePath@ entry = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex];
	if(entry is null || !entry.isTeleport) return;
	entry.teleportX = npcBunniesEditorCore::NpcDesignerTeleportEditX;
	entry.teleportY = npcBunniesEditorCore::NpcDesignerTeleportEditY;
	npcBunniesEditorCore::NpcDesignerEditingTeleport = false;
	npcBunniesEditorCore::NpcDesignerTeleportIsNew = false;
	npcBunniesEditorCore::NpcDesignerTeleportFirePressed = false;
	NdeUnfreezeEditorCamera();
	npcBunniesEditorSerialization::NdeSaveNpcFile();
}

void NdeCancelGuideEntryEdit()
{
	npcBunniesEditorCore::NpcDesignerRecordingPath = false;
	if(npcBunniesEditorCore::NpcDesignerEditingTeleport)
	{
		if(npcBunniesEditorCore::NpcDesignerTeleportIsNew && NdeValidNpc() && npcBunniesEditorCore::NpcDesignerPathIndex >= 0 && npcBunniesEditorCore::NpcDesignerPathIndex < int(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths.length))
		{
			npcBunniesCore::npcBunny npc = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc];
			NdeRemoveGuidePathRewardReferences(npc, npcBunniesEditorCore::NpcDesignerPathIndex);
			npc.guidePaths.removeAt(npcBunniesEditorCore::NpcDesignerPathIndex);
			npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc] = npc;
			npcBunniesEditorCore::NpcDesignerPathIndex = -1;
		}
		else if(NdeValidNpc() && npcBunniesEditorCore::NpcDesignerPathIndex >= 0 && npcBunniesEditorCore::NpcDesignerPathIndex < int(npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths.length))
		{
			npcBunniesCore::GuidePath@ entry = npcBunniesCore::npcBunnies[npcBunniesEditorCore::NpcDesignerSelectedNpc].guidePaths[npcBunniesEditorCore::NpcDesignerPathIndex];
			if(entry !is null && entry.isTeleport)
			{
				entry.teleportX = npcBunniesEditorCore::NpcDesignerTeleportOriginalX;
				entry.teleportY = npcBunniesEditorCore::NpcDesignerTeleportOriginalY;
			}
		}
	}
	npcBunniesEditorCore::NpcDesignerEditingTeleport = false;
	npcBunniesEditorCore::NpcDesignerTeleportIsNew = false;
	npcBunniesEditorCore::NpcDesignerTeleportFirePressed = false;
	NdeUnfreezeEditorCamera();
}

void NdeCenterEditorCameraOnTile(int tx, int ty)
{
	if(jjLocalPlayerCount <= 0) return;
	jjLocalPlayers[0].cameraFreeze(tx * 32 + 16, ty * 32 + 16, true, true);
}

void NdeUnfreezeEditorCamera()
{
	if(jjLocalPlayerCount <= 0) return;
	jjLocalPlayers[0].cameraUnfreeze();
}

/// <summary>
/// Converts world pixel coordinates to local screen coordinates.
/// </summary>
void NdeWorldToScreen(int worldX, int worldY, int &out screenX, int &out screenY)
{
	jjPLAYER@ play = jjLocalPlayers[0];
	screenX = int(worldX - play.cameraX);
	screenY = int(worldY - play.cameraY);
}

/// <summary>
/// Draws the npc preview portion of the NPC Designer UI.
/// </summary>

}