| Name | Author | Game Mode | Rating | |||||
|---|---|---|---|---|---|---|---|---|
| NpcBunnies | Cranky | Mutator | N/A | |||||
namespace npcBunniesAccessories
{
namespace NpcAccessories
{
const int CATEGORY_HAT = 0;
const int CATEGORY_SCARF = 1;
const int CATEGORY_GLASSES = 2;
const int CATEGORY_COUNT = 3;
const bool NPC_DEBUG_HAT_POSITION_ON = false;
bool ENABLE_HAT_ONLY_FOR_NPC_CHARACTERS = true; // False to enable hats for bosses, monsters, static as well
int framesPerCharacter = 1;
int characterSlotCount = 1;
const int NO_ACCESSORY = -1;
const int RUN_SHOOT_RABBIT_ANIM = RABBIT::DIVEFIRERIGHT;
const int NPC_HAT_ACTION_CATEGORY = 9101;
const int NPC_HAT_ACTION_TYPE = 9102;
const int NPC_HAT_ACTION_STATE = 9103;
const int NPC_HAT_ACTION_FRAME = 9104;
const int NPC_HAT_ACTION_POS_X = 9105;
const int NPC_HAT_ACTION_POS_Y = 9106;
const int NPC_HAT_ACTION_ROTATION = 9107;
const int NPC_HAT_ACTION_SCALE_X = 9108;
const int NPC_HAT_ACTION_SCALE_Y = 9109;
const int NPC_HAT_ACTION_INCREMENT = 9110;
const int NPC_HAT_ACTION_EDIT_FIELD = 9111;
const int NPC_HAT_ACTION_MOUSE = 9112;
array<string> filenames = {"npcHats.asdat", "npcScarfs.asdat", "npcGlasses.asdat"};
array<string> categoryNames = {"Hat", "Scarf", "Glasses"};
array<string> stateNames = {"Idle", "Walk", "Run", "Fall", "Jump", "Shoot", "RunShoot"};
array<int> stateNpcAnims = {0, 1, 2, 3, 4, 5, 6};
array<int> stateRabbitAnims = {RABBIT::IDLE1, RABBIT::RUN1, RABBIT::RUN3, RABBIT::FALL, RABBIT::JUMPING1, RABBIT::FIRE, RUN_SHOOT_RABBIT_ANIM};
array<array<int>> adjustX;
array<array<int>> adjustY;
array<array<int>> rotation;
array<array<float>> scaleX;
array<array<float>> scaleY;
array<array<bool>> edited;
array<array<int>> nearestEditedIndex;
array<int> lazyAccessoryLookupGeneration;
array<int> lazyAccessoryNpcType;
array<int> lazyAccessoryCategoryFrame;
array<int> lazyAccessoryIndex;
array<int> lazyAccessoryCoordinateFrameIndex;
array<int> lazyAccessoryStoredFrameIndex;
array<int> lazyAccessoryPhysicalFrameIndex;
array<int> lazyStateFrameCountGeneration;
array<int> lazyStateFrameCountType;
array<int> lazyStateFrameCountValue;
array<int> cachedNpcAccessoryType;
array<int> cachedNpcAccessoryHatFrame;
array<int> cachedNpcAccessoryScarfFrame;
array<int> cachedNpcAccessoryGlassesFrame;
array<bool> cachedNpcHasDrawableAccessory;
array<array<int>> minAnimStart;
array<array<int>> minAnimCount;
int accessorySet = -1;
bool accessoryLookupDirty = true;
int accessoryLookupGeneration = 0;
bool loaded = false;
bool editorOpen = false;
bool editorMouseMode = false;
bool editorMouseWasDown = false;
bool editorPressedKey = false;
bool editorPressedFire = false;
int editorCategory = 0;
int editorTypeIndex = 0;
int editorState = 0;
int editorFrame = 0;
int editorX = 0;
int editorY = 0;
int editorRotation = 0;
float editorScaleX = 1.0;
float editorScaleY = 1.0;
int editorIncrement = 1;
int editorField = -1;
string editorFieldValue = "";
bool editorFieldFresh = false;
int editorLastKey = -1;
DbWindow editorWindow;
DbResult editorResult;
int PositionArrayLength()
{
return characterSlotCount * framesPerCharacter + 1;
}
void ResizePositionArrays(int newCharacterSlotCount, int newFramesPerCharacter = -1)
{
if(newCharacterSlotCount < 1) newCharacterSlotCount = 1;
if(newFramesPerCharacter < 1) newFramesPerCharacter = framesPerCharacter;
if(newFramesPerCharacter < 1) newFramesPerCharacter = 1;
if(newCharacterSlotCount == characterSlotCount
&& newFramesPerCharacter == framesPerCharacter
&& int(adjustX.length) == CATEGORY_COUNT
&& int(adjustX[0].length) == characterSlotCount * framesPerCharacter + 1) return;
int oldCharacterSlotCount = characterSlotCount;
int oldFramesPerCharacter = framesPerCharacter;
int oldPositionLength = oldCharacterSlotCount * oldFramesPerCharacter + 1;
array<array<int>> oldAdjustX = adjustX;
array<array<int>> oldAdjustY = adjustY;
array<array<int>> oldRotation = rotation;
array<array<float>> oldScaleX = scaleX;
array<array<float>> oldScaleY = scaleY;
array<array<bool>> oldEdited = edited;
characterSlotCount = newCharacterSlotCount;
framesPerCharacter = newFramesPerCharacter;
int newPositionLength = PositionArrayLength();
adjustX.resize(CATEGORY_COUNT);
adjustY.resize(CATEGORY_COUNT);
rotation.resize(CATEGORY_COUNT);
scaleX.resize(CATEGORY_COUNT);
scaleY.resize(CATEGORY_COUNT);
edited.resize(CATEGORY_COUNT);
nearestEditedIndex.resize(CATEGORY_COUNT);
for(int category = 0; category < CATEGORY_COUNT; category++)
{
adjustX[category].resize(newPositionLength);
adjustY[category].resize(newPositionLength);
rotation[category].resize(newPositionLength);
scaleX[category].resize(newPositionLength);
scaleY[category].resize(newPositionLength);
edited[category].resize(newPositionLength);
nearestEditedIndex[category].resize(newPositionLength);
for(int i = 0; i < newPositionLength; i++)
{
adjustX[category][i] = 0;
adjustY[category][i] = 0;
rotation[category][i] = 0;
scaleX[category][i] = 1.0;
scaleY[category][i] = 1.0;
edited[category][i] = false;
nearestEditedIndex[category][i] = -1;
}
}
int copySlots = oldCharacterSlotCount < newCharacterSlotCount ? oldCharacterSlotCount : newCharacterSlotCount;
int copyFrames = oldFramesPerCharacter < newFramesPerCharacter ? oldFramesPerCharacter : newFramesPerCharacter;
for(int category = 0; category < CATEGORY_COUNT; category++)
{
for(int slot = 0; slot < copySlots; slot++)
{
for(int frame = 0; frame < copyFrames; frame++)
{
int oldIndex = slot * oldFramesPerCharacter + frame;
int newIndex = slot * newFramesPerCharacter + frame;
if(category >= int(oldEdited.length)) continue;
if(oldIndex < 0 || oldIndex >= int(oldEdited[category].length) || newIndex < 0 || newIndex >= newPositionLength) continue;
adjustX[category][newIndex] = oldAdjustX[category][oldIndex];
adjustY[category][newIndex] = oldAdjustY[category][oldIndex];
rotation[category][newIndex] = oldRotation[category][oldIndex];
scaleX[category][newIndex] = oldScaleX[category][oldIndex];
scaleY[category][newIndex] = oldScaleY[category][oldIndex];
edited[category][newIndex] = oldEdited[category][oldIndex];
}
}
}
accessoryLookupDirty = true;
}
void EnsurePositionIndexFits(int charSlot, int frameIndex)
{
if(charSlot < 0) charSlot = 0;
if(frameIndex < 0) frameIndex = 0;
int neededSlots = characterSlotCount;
int neededFrames = framesPerCharacter;
if(charSlot >= neededSlots) neededSlots = charSlot + 1;
if(frameIndex >= neededFrames) neededFrames = frameIndex + 1;
if(int(adjustX.length) != CATEGORY_COUNT || neededSlots != characterSlotCount || neededFrames != framesPerCharacter)
ResizePositionArrays(neededSlots, neededFrames);
}
int Index(int category, int charSlot, int frameIndex)
{
if(category < 0) category = 0;
if(category >= CATEGORY_COUNT) category = CATEGORY_COUNT - 1;
if(charSlot < 0) charSlot = 0;
if(frameIndex < 0) frameIndex = 0;
EnsurePositionIndexFits(charSlot, frameIndex);
return charSlot * framesPerCharacter + frameIndex;
}
int ExistingIndex(int category, int charSlot, int frameIndex)
{
if(category < 0 || category >= CATEGORY_COUNT) return -1;
if(charSlot < 0 || frameIndex < 0) return -1;
if(charSlot >= characterSlotCount || frameIndex >= framesPerCharacter) return -1;
int index = charSlot * framesPerCharacter + frameIndex;
if(category >= int(adjustX.length) || index < 0 || index >= int(adjustX[category].length)) return -1;
return index;
}
void RebuildNearestEditedIndexes()
{
int positionLength = PositionArrayLength();
nearestEditedIndex.resize(CATEGORY_COUNT);
for(int category = 0; category < CATEGORY_COUNT; category++)
{
nearestEditedIndex[category].resize(positionLength);
for(int i = 0; i < positionLength; i++) nearestEditedIndex[category][i] = -1;
for(int slot = 0; slot < characterSlotCount; slot++)
{
int lastEdited = -1;
for(int frame = 0; frame < framesPerCharacter; frame++)
{
int index = slot * framesPerCharacter + frame;
if(index >= 0 && index < int(edited[category].length) && edited[category][index]) lastEdited = index;
nearestEditedIndex[category][index] = lastEdited;
}
int nextEdited = -1;
for(int frame = framesPerCharacter - 1; frame >= 0; frame--)
{
int index = slot * framesPerCharacter + frame;
if(index >= 0 && index < int(edited[category].length) && edited[category][index]) nextEdited = index;
if(nextEdited < 0) continue;
int current = nearestEditedIndex[category][index];
if(current < 0)
{
nearestEditedIndex[category][index] = nextEdited;
}
else
{
int leftFrame = current % framesPerCharacter;
int rightFrame = nextEdited % framesPerCharacter;
if(rightFrame - frame < frame - leftFrame) nearestEditedIndex[category][index] = nextEdited;
}
}
}
}
accessoryLookupDirty = false;
accessoryLookupGeneration++;
}
void EnsureAccessoryLookupCache()
{
if(accessoryLookupDirty) RebuildNearestEditedIndexes();
}
void EnsureLazyAccessoryCacheIndex(int index)
{
if(index < 0 || int(lazyAccessoryIndex.length) > index) return;
int needed = index + 1;
int oldLength = int(lazyAccessoryIndex.length);
lazyAccessoryLookupGeneration.resize(needed);
lazyAccessoryNpcType.resize(needed);
lazyAccessoryCategoryFrame.resize(needed);
lazyAccessoryIndex.resize(needed);
lazyAccessoryCoordinateFrameIndex.resize(needed);
lazyAccessoryStoredFrameIndex.resize(needed);
lazyAccessoryPhysicalFrameIndex.resize(needed);
for(int i = oldLength; i < needed; i++)
{
lazyAccessoryLookupGeneration[i] = -1;
lazyAccessoryNpcType[i] = 0;
lazyAccessoryCategoryFrame[i] = -9999;
lazyAccessoryIndex[i] = -1;
lazyAccessoryCoordinateFrameIndex[i] = -1;
lazyAccessoryStoredFrameIndex[i] = -1;
lazyAccessoryPhysicalFrameIndex[i] = -1;
}
}
int LazyAccessoryCacheIndex(int npcId, int state, int frame, int category)
{
if(npcId < 0 || state < 0 || state >= int(stateNpcAnims.length) || frame < 0 || frame >= framesPerCharacter || category < 0 || category >= CATEGORY_COUNT) return -1;
int index = ((npcId * int(stateNpcAnims.length) + state) * framesPerCharacter + frame) * CATEGORY_COUNT + category;
EnsureLazyAccessoryCacheIndex(index);
return index;
}
void EnsureLazyStateFrameCountCacheIndex(int index)
{
if(index < 0 || int(lazyStateFrameCountValue.length) > index) return;
int needed = index + 1;
int oldLength = int(lazyStateFrameCountValue.length);
lazyStateFrameCountGeneration.resize(needed);
lazyStateFrameCountType.resize(needed);
lazyStateFrameCountValue.resize(needed);
for(int i = oldLength; i < needed; i++)
{
lazyStateFrameCountGeneration[i] = -1;
lazyStateFrameCountType[i] = 0;
lazyStateFrameCountValue[i] = 1;
}
}
int LazyStateFrameCountCacheIndex(int npcId, int state)
{
if(npcId < 0 || state < 0 || state >= int(stateNpcAnims.length)) return -1;
int index = npcId * int(stateNpcAnims.length) + state;
EnsureLazyStateFrameCountCacheIndex(index);
return index;
}
void EnsureNpcAccessoryStateCache(int npcId)
{
if(npcId < 0) return;
int needed = npcId + 1;
if(int(cachedNpcHasDrawableAccessory.length) >= needed) return;
int oldLength = int(cachedNpcHasDrawableAccessory.length);
cachedNpcAccessoryType.resize(needed);
cachedNpcAccessoryHatFrame.resize(needed);
cachedNpcAccessoryScarfFrame.resize(needed);
cachedNpcAccessoryGlassesFrame.resize(needed);
cachedNpcHasDrawableAccessory.resize(needed);
for(int i = oldLength; i < needed; i++)
{
cachedNpcAccessoryType[i] = 0;
cachedNpcAccessoryHatFrame[i] = -9999;
cachedNpcAccessoryScarfFrame[i] = -9999;
cachedNpcAccessoryGlassesFrame[i] = -9999;
cachedNpcHasDrawableAccessory[i] = false;
}
}
void RefreshNpcAccessoryStateCache(npcBunniesCore::npcBunny &in npc)
{
if(npc.id < 0) return;
EnsureNpcAccessoryStateCache(npc.id);
if(cachedNpcAccessoryType[npc.id] == npc.type
&& cachedNpcAccessoryHatFrame[npc.id] == npc.hatFrame
&& cachedNpcAccessoryScarfFrame[npc.id] == npc.scarfFrame
&& cachedNpcAccessoryGlassesFrame[npc.id] == npc.glassesFrame)
return;
cachedNpcAccessoryType[npc.id] = npc.type;
cachedNpcAccessoryHatFrame[npc.id] = npc.hatFrame;
cachedNpcAccessoryScarfFrame[npc.id] = npc.scarfFrame;
cachedNpcAccessoryGlassesFrame[npc.id] = npc.glassesFrame;
cachedNpcHasDrawableAccessory[npc.id] = (npc.scarfFrame >= 0 || npc.glassesFrame >= 0 || (npc.hatFrame >= 0 && CanNpcUseHat(npc.type)));
}
bool NpcHasDrawableAccessory(npcBunniesCore::npcBunny &in npc)
{
if(npc.id < 0) return HasDrawableAccessory(npc);
RefreshNpcAccessoryStateCache(npc);
return cachedNpcHasDrawableAccessory[npc.id];
}
int StateFrameCountForNpcType(int npcType, int state)
{
int stateAnim = state >= 0 && state < int(stateNpcAnims.length) ? stateNpcAnims[state] : 0;
int count = SpecialEditorPreviewFrameCount(npcType, state);
if(count <= 0)
{
int sourceSet = CharactersModSourceSetForNpcType(npcType, stateAnim);
if(sourceSet >= 0) count = npcBunniesAnimations::charactersModAnimCount[sourceSet][stateAnim];
}
if(count <= 0) count = framesPerCharacter;
if(count <= 0) count = 1;
if(count > framesPerCharacter) count = framesPerCharacter;
return count;
}
int StateFrameCountForNpc(npcBunniesCore::npcBunny &in npc, int state)
{
int cacheIndex = LazyStateFrameCountCacheIndex(npc.id, state);
if(cacheIndex >= 0
&& lazyStateFrameCountGeneration[cacheIndex] == accessoryLookupGeneration
&& lazyStateFrameCountType[cacheIndex] == npc.type)
return lazyStateFrameCountValue[cacheIndex];
int count = StateFrameCountForNpcType(npc.type, state);
if(cacheIndex >= 0)
{
lazyStateFrameCountGeneration[cacheIndex] = accessoryLookupGeneration;
lazyStateFrameCountType[cacheIndex] = npc.type;
lazyStateFrameCountValue[cacheIndex] = count;
}
return count;
}
int GetAnimationDivisionForCharacterSlot(int charSlot)
{
// Use the resolved character slot, not the raw npc.type value.
// Some NPCs use getTypeFromTypeName(...) values instead of NpcAnim::*
// constants, so checking only npc.type missed Jazz/Spaz/Lori aliases
// and made runtime color-layer frames look for unsaved positions.
if(charSlot == 1) return 5; // Jazz: full + 4 color-removed versions
if(charSlot == 2) return 5; // Spaz: full + 4 color-removed versions
if(charSlot == 3) return 4; // Lori: full + 3 color-removed versions
if(charSlot == 5) return 4; // Bird: full + 3 color-removed versions
if(charSlot == 8) return 5; // Mario: full + 4 color-removed versions
if(charSlot == 9) return 5; // Sonic: full + 4 color-removed versions
if(charSlot == 10) return 5; // Tails: full + 4 color-removed versions
if(charSlot == 11) return 5; // JJ1: full + 4 color-removed versions
if(charSlot == 12) return 5; // Devan: full + 4 color-removed versions
if(charSlot == 13) return 5; // Yoshi: full + 4 color-removed versions
return 1;
}
int GetAnimationDivisionForNpcType(int type)
{
return GetAnimationDivisionForCharacterSlot(CharacterSlotForNpcType(type));
}
int GetMergedAccessoryFrame(int frameIndex, int division, int layer = 0)
{
if(division <= 1) return frameIndex;
if(layer < 0) layer = 0;
if(layer >= division) layer = division - 1;
int layerFrameCount = framesPerCharacter / division;
if(layerFrameCount <= 0) return frameIndex;
int frameInLayer = frameIndex % layerFrameCount;
if(frameInLayer < 0) frameInLayer += layerFrameCount;
return layer * layerFrameCount + frameInLayer;
}
void EnsureLoaded()
{
if(loaded) return;
loaded = true;
accessorySet = 0;
while(jjAnimSets[ANIM::CUSTOM[accessorySet]].firstAnim != 0) accessorySet++;
jjAnimSets[ANIM::CUSTOM[accessorySet]].load(0, "xmasHats.j2a");
ResizePositionArrays(InitialCharacterSlotCount(), 1);
LoadCharactersMinIndexes();
for(int category = 0; category < CATEGORY_COUNT; category++) ReadCategory(category);
RebuildNearestEditedIndexes();
}
int InitialCharacterSlotCount()
{
int count = 1;
for(int i = 0; i < int(npcBunniesCore::npcList.length); i++)
{
int slot = StorageSlotForCharacterSlot(CharacterSlotForNpcType(npcBunniesCore::npcList[i]));
if(slot + 1 > count) count = slot + 1;
}
return count;
}
void EnsureMinAnimIndexFits(int sourceSet, int anim)
{
if(sourceSet < 0 || anim < 0) return;
if(sourceSet >= int(minAnimStart.length))
{
minAnimStart.resize(sourceSet + 1);
minAnimCount.resize(sourceSet + 1);
}
int oldLength = int(minAnimStart[sourceSet].length);
if(anim >= oldLength)
{
minAnimStart[sourceSet].resize(anim + 1);
minAnimCount[sourceSet].resize(anim + 1);
for(int i = oldLength; i <= anim; i++)
{
minAnimStart[sourceSet][i] = -1;
minAnimCount[sourceSet][i] = 0;
}
}
}
void LoadCharactersMinIndexes()
{
minAnimStart.resize(0);
minAnimCount.resize(0);
jjSTREAM file("charactersMinAnimIndexes.asdat");
if(file.isEmpty()) return;
string line = "";
int currentSet = -1;
while(file.getLine(line))
{
if(line == "" || line.substr(0, 1) == "/") continue;
array<string> parts = line.split(" ");
if(int(parts.length) == 0) continue;
if(parts[0] == "set")
{
currentSet = int(parts.length) >= 2 ? parseInt(parts[1]) : -1;
continue;
}
if(parts[0] == "end") { currentSet = -1; continue; }
if(currentSet < 0 || int(parts.length) < 4) continue;
int anim = parseInt(parts[0]);
if(anim < 0) continue;
EnsureMinAnimIndexFits(currentSet, anim);
minAnimStart[currentSet][anim] = parseInt(parts[2]);
minAnimCount[currentSet][anim] = parseInt(parts[3]);
}
}
void ReadCategory(int category)
{
if(category < 0 || category >= CATEGORY_COUNT) return;
jjSTREAM file(filenames[category]);
if(file.isEmpty()) return;
array<int> entryFrame;
array<int> entrySlot;
array<int> entryX;
array<int> entryY;
array<int> entryRotation;
array<float> entryScaleX;
array<float> entryScaleY;
int maxSlot = characterSlotCount - 1;
int maxFrame = framesPerCharacter - 1;
string line = "";
while(file.getLine(line))
{
array<string> parts = line.split(" ");
if(int(parts.length) < 6) continue;
int frameIndex = parseInt(parts[0]);
int charSlot = parseInt(parts[5]) - 1;
if(frameIndex < 0 || charSlot < 0) continue;
entryFrame.insertLast(frameIndex);
entrySlot.insertLast(charSlot);
entryX.insertLast(parseInt(parts[1]));
entryY.insertLast(parseInt(parts[2]));
entryRotation.insertLast(parseInt(parts[3]));
entryScaleX.insertLast(int(parts.length) >= 8 ? parseFloat(parts[6]) : 1.0);
entryScaleY.insertLast(int(parts.length) >= 8 ? parseFloat(parts[7]) : 1.0);
if(charSlot > maxSlot) maxSlot = charSlot;
if(frameIndex > maxFrame) maxFrame = frameIndex;
}
if(maxSlot + 1 > characterSlotCount || maxFrame + 1 > framesPerCharacter)
ResizePositionArrays(maxSlot + 1, maxFrame + 1);
for(int i = 0; i < int(entryFrame.length); i++)
{
int index = Index(category, entrySlot[i], entryFrame[i]);
adjustX[category][index] = entryX[i];
adjustY[category][index] = entryY[i];
rotation[category][index] = entryRotation[i];
scaleX[category][index] = entryScaleX[i];
scaleY[category][index] = entryScaleY[i];
edited[category][index] = true;
}
accessoryLookupDirty = true;
}
void SaveCategory(int category)
{
if(category < 0 || category >= CATEGORY_COUNT) return;
jjSTREAM file;
for(int charSlot = 0; charSlot < characterSlotCount; charSlot++)
{
for(int frameIndex = 0; frameIndex < framesPerCharacter; frameIndex++)
{
int index = ExistingIndex(category, charSlot, frameIndex);
if(index < 0 || !edited[category][index]) continue;
file.write(frameIndex + " " + adjustX[category][index] + " " + adjustY[category][index] + " " + rotation[category][index] + " 0 " + (charSlot + 1) + " " + scaleX[category][index] + " " + scaleY[category][index] + " \n");
}
}
file.save(filenames[category]);
}
int StorageSlotForCharacterSlot(int charSlot)
{
return charSlot <= 0 ? 0 : charSlot - 1;
}
int FrameCount(int category)
{
EnsureLoaded();
if(accessorySet < 0 || category < 0 || category >= CATEGORY_COUNT) return 0;
int firstAnim = jjAnimSets[ANIM::CUSTOM[accessorySet]].firstAnim;
if(firstAnim == 0) return 0;
int count = jjAnimations[firstAnim + category].frameCount;
return count > 0 ? count : 0;
}
int ClampFrame(int category, int frame)
{
if(frame < 0) return NO_ACCESSORY;
int count = FrameCount(category);
if(count <= 0) return NO_ACCESSORY;
if(frame >= count) return count - 1;
return frame;
}
int CharacterSlotForNpcType(int type)
{
if(type == npcBunniesCore::NpcAnim::JAZZ) return 1;
if(type == npcBunniesCore::NpcAnim::SPAZ) return 2;
if(type == npcBunniesCore::NpcAnim::LORI) return 3;
if(type == npcBunniesCore::NpcAnim::FROG) return 4;
if(type == npcBunniesCore::NpcAnim::BIRD) return 5;
if(type == npcBunniesCore::NpcAnim::DEVAN) return 12;
if(type == npcBunniesCore::NpcAnim::MARIO) return 8;
if(type == npcBunniesCore::NpcAnim::SONIC) return 9;
if(type == npcBunniesCore::NpcAnim::TAILS) return 10;
if(type == npcBunniesCore::NpcAnim::JJ1) return 11;
if(type == npcBunniesCore::NpcAnim::YOSHI) return 13;
if(type == npcBunniesCore::NpcAnim::EVA) return 27;
if(type == npcBunniesAnimations::getTypeFromTypeName("Jazz")) return 1;
if(type == npcBunniesAnimations::getTypeFromTypeName("Spaz")) return 2;
if(type == npcBunniesAnimations::getTypeFromTypeName("Lori")) return 3;
if(type == npcBunniesCore::NpcAnim::FROG) return 4;
if(type == npcBunniesCore::NpcAnim::BIRD) return 5;
if(type == ANIM::DEVILDEVAN) return 12;
if(type == npcBunniesAnimations::getTypeFromTypeName("Mario")) return 8;
if(type == npcBunniesAnimations::getTypeFromTypeName("Sonic")) return 9;
if(type == npcBunniesAnimations::getTypeFromTypeName("Tails")) return 10;
if(type == npcBunniesAnimations::getTypeFromTypeName("JJ1")) return 11;
if(type == npcBunniesAnimations::getTypeFromTypeName("Yoshi")) return 13;
if(type == ANIM::EVA) return 27;
return 1;
}
bool IsNpcCharacterType(int type)
{
for(int i = 0; i < int(npcBunniesCore::npcList.length); i++)
{
if(type == npcBunniesCore::npcList[i]) return true;
}
if(type == npcBunniesAnimations::getTypeFromTypeName("Jazz")) return true;
if(type == npcBunniesAnimations::getTypeFromTypeName("Spaz")) return true;
if(type == npcBunniesAnimations::getTypeFromTypeName("Lori")) return true;
if(type == npcBunniesAnimations::getTypeFromTypeName("Frog")) return true;
if(type == npcBunniesAnimations::getTypeFromTypeName("Bird")) return true;
if(type == npcBunniesAnimations::getTypeFromTypeName("Devan")) return true;
if(type == npcBunniesAnimations::getTypeFromTypeName("Mario")) return true;
if(type == npcBunniesAnimations::getTypeFromTypeName("Sonic")) return true;
if(type == npcBunniesAnimations::getTypeFromTypeName("Tails")) return true;
if(type == npcBunniesAnimations::getTypeFromTypeName("JJ1")) return true;
if(type == npcBunniesAnimations::getTypeFromTypeName("Yoshi")) return true;
if(type == npcBunniesAnimations::getTypeFromTypeName("Eva")) return true;
return false;
}
bool CanNpcUseHat(int type)
{
return !ENABLE_HAT_ONLY_FOR_NPC_CHARACTERS || IsNpcCharacterType(type);
}
int EditorNpcType()
{
if(editorTypeIndex < 0) editorTypeIndex = int(npcBunniesCore::npcList.length) - 1;
if(editorTypeIndex >= int(npcBunniesCore::npcList.length)) editorTypeIndex = 0;
return npcBunniesCore::npcList[editorTypeIndex];
}
string EditorNpcTypeName()
{
int type = EditorNpcType();
if(type == npcBunniesCore::NpcAnim::JAZZ) return "Jazz";
if(type == npcBunniesCore::NpcAnim::SPAZ) return "Spaz";
if(type == npcBunniesCore::NpcAnim::LORI) return "Lori";
if(type == npcBunniesCore::NpcAnim::FROG) return "Frog";
if(type == npcBunniesCore::NpcAnim::BIRD) return "Bird";
if(type == npcBunniesCore::NpcAnim::DEVAN) return "Devan";
if(type == npcBunniesCore::NpcAnim::MARIO) return "Mario";
if(type == npcBunniesCore::NpcAnim::SONIC) return "Sonic";
if(type == npcBunniesCore::NpcAnim::TAILS) return "Tails";
if(type == npcBunniesCore::NpcAnim::JJ1) return "JJ1";
if(type == npcBunniesCore::NpcAnim::YOSHI) return "Yoshi";
if(type == npcBunniesCore::NpcAnim::EVA) return "Eva";
return "NPC";
}
int SourceSetForCharacterSlot(int charSlot)
{
if(charSlot == 1) return npcBunniesCore::NpcAnim::JAZZ;
if(charSlot == 2) return npcBunniesCore::NpcAnim::SPAZ;
if(charSlot == 3) return npcBunniesCore::NpcAnim::LORI;
if(charSlot == 4) return npcBunniesCore::NpcAnim::FROG;
if(charSlot == 5) return npcBunniesCore::NpcAnim::BIRD;
if(charSlot == 8) return npcBunniesCore::NpcAnim::MARIO;
if(charSlot == 9) return npcBunniesCore::NpcAnim::SONIC;
if(charSlot == 10) return npcBunniesCore::NpcAnim::TAILS;
if(charSlot == 11) return npcBunniesCore::NpcAnim::JJ1;
if(charSlot == 12) return 7;
if(charSlot == 13) return npcBunniesCore::NpcAnim::YOSHI;
return -1;
}
int BaseAnimSetForCharacterSlot(int charSlot)
{
if(charSlot == 1) return ANIM::JAZZ;
if(charSlot == 2) return ANIM::SPAZ;
if(charSlot == 3) return ANIM::LORI;
if(charSlot == 4) return ANIM::FROG;
if(charSlot == 5) return ANIM::BIRD;
return -1;
}
int FrameIndexForAnim(int animSet, int anim, int frame)
{
if(animSet < 0 || jjAnimSets[animSet].firstAnim == 0) return 0;
int firstAnim = jjAnimSets[animSet].firstAnim;
int animIndex = firstAnim + anim;
if(animIndex < 0) return 0;
int count = jjAnimations[animIndex].frameCount;
int remappedFrame = count > 0 ? frame % count : 0;
if(remappedFrame < 0) remappedFrame += count;
int firstFrame = jjAnimations[firstAnim].firstFrame;
int currentFrame = jjAnimations[animIndex].firstFrame + remappedFrame;
int index = currentFrame - firstFrame;
if(index < 0) index = 0;
return index;
}
int LocalFrameForAnim(int animSet, int anim, int frame)
{
if(animSet < 0 || jjAnimSets[animSet].firstAnim == 0) return frame < 0 ? 0 : frame;
int animIndex = jjAnimSets[animSet].firstAnim + anim;
int count = jjAnimations[animIndex].frameCount;
if(count <= 0) return frame < 0 ? 0 : frame;
int remappedFrame = frame % count;
if(remappedFrame < 0) remappedFrame += count;
return remappedFrame;
}
int FrameIndexForRabbitAnim(int charSlot, int rabbitAnim, int frame)
{
int sourceSet = SourceSetForCharacterSlot(charSlot);
if(sourceSet >= 0 && sourceSet < int(minAnimStart.length))
{
if(rabbitAnim < 0 || rabbitAnim >= int(minAnimStart[sourceSet].length)) return -1;
int start = minAnimStart[sourceSet][rabbitAnim];
int count = minAnimCount[sourceSet][rabbitAnim];
if(start < 0 || count <= 0) return -1;
int remappedFrame = frame % count;
if(remappedFrame < 0) remappedFrame += count;
return start + remappedFrame;
}
int baseAnimSet = BaseAnimSetForCharacterSlot(charSlot);
if(baseAnimSet < 0 || jjAnimSets[baseAnimSet].firstAnim == 0 || rabbitAnim < 0) return -1;
int firstAnim = jjAnimSets[baseAnimSet].firstAnim;
int animIndex = firstAnim + rabbitAnim;
int count = jjAnimations[animIndex].frameCount;
int remappedFrame = count > 0 ? frame % count : 0;
if(remappedFrame < 0) remappedFrame += count;
return jjAnimations[animIndex].firstFrame + remappedFrame - jjAnimations[firstAnim].firstFrame;
}
bool HasCharactersModAnim(int sourceSet, int anim)
{
return sourceSet >= 0
&& sourceSet < int(npcBunniesAnimations::charactersModAnimStart.length)
&& anim >= 0
&& anim < int(npcBunniesAnimations::charactersModAnimStart[sourceSet].length)
&& anim < int(npcBunniesAnimations::charactersModAnimCount[sourceSet].length)
&& npcBunniesAnimations::charactersModAnimStart[sourceSet][anim] >= 0
&& npcBunniesAnimations::charactersModAnimCount[sourceSet][anim] > 0;
}
int CharactersModSourceSetForNpcType(int npcType, int anim)
{
if(HasCharactersModAnim(npcType, anim)) return npcType;
int charSlot = CharacterSlotForNpcType(npcType);
int sourceSet = SourceSetForCharacterSlot(charSlot);
if(HasCharactersModAnim(sourceSet, anim)) return sourceSet;
return -1;
}
int NpcFrameIndexForState(int npcType, int state, int frame)
{
if(state < 0) state = 0;
if(state >= int(stateNpcAnims.length)) state = int(stateNpcAnims.length) - 1;
if(npcType == npcBunniesCore::NpcAnim::EVA || npcType == ANIM::EVA)
{
if(state == 3 || state == 4) return LocalFrameForAnim(ANIM::FLAG, 6, frame);
return frame < 0 ? 0 : frame;
}
if((npcType == npcBunniesCore::NpcAnim::DEVAN || npcType == ANIM::DEVILDEVAN) && state == 5)
{
if(jjAnimSets[ANIM::DEVILDEVAN].firstAnim == 0) jjAnimSets[ANIM::DEVILDEVAN].load();
return FrameIndexForAnim(ANIM::DEVILDEVAN, 14, frame);
}
if(npcType == npcBunniesCore::NpcAnim::DEVAN || npcType == ANIM::DEVILDEVAN)
{
int actualAnim = ActualNpcAnimForEditorState(npcType, state);
if(actualAnim < 0) actualAnim = 0;
int devSourceSet = CharactersModSourceSetForNpcType(npcType, actualAnim);
if(devSourceSet >= 0)
{
int devStart = npcBunniesAnimations::charactersModAnimStart[devSourceSet][actualAnim];
int devCount = SpecialEditorPreviewFrameCount(npcType, state);
if(devCount <= 0) devCount = npcBunniesAnimations::charactersModAnimCount[devSourceSet][actualAnim];
int devRemappedFrame = devCount > 0 ? frame % devCount : 0;
if(devRemappedFrame < 0) devRemappedFrame += devCount;
return devStart + devRemappedFrame;
}
}
int npcAnim = ActualNpcAnimForEditorState(npcType, state);
int sourceSet = CharactersModSourceSetForNpcType(npcType, npcAnim);
if(sourceSet >= 0)
{
int start = npcBunniesAnimations::charactersModAnimStart[sourceSet][npcAnim];
int count = npcBunniesAnimations::charactersModAnimCount[sourceSet][npcAnim];
int division = GetAnimationDivisionForNpcType(npcType);
if(division < 1) division = 1;
int editableFrameCount = count / division;
if(editableFrameCount <= 0) editableFrameCount = count;
int remappedFrame = frame % editableFrameCount;
if(remappedFrame < 0) remappedFrame += editableFrameCount;
return start + remappedFrame;
}
int charSlot = CharacterSlotForNpcType(npcType);
return FrameIndexForRabbitAnim(charSlot, stateRabbitAnims[state], frame);
}
int FirstNpcFrameIndexForCoordinateLookup(int npcType)
{
// Do NOT use the minimum start across all states here. Some NPC tables can
// contain unrelated/unused states at lower frame indexes, which made runtime
// keep using packed frames like 170-199 for hat coordinates. The coordinate
// .asdat uses frames relative to the character's first editable animation,
// normally Idle: idle 0, walk 5, walk2 6, etc.
int idleAnim = stateNpcAnims[0];
int sourceSet = CharactersModSourceSetForNpcType(npcType, idleAnim);
if(sourceSet >= 0)
{
int idleStart = npcBunniesAnimations::charactersModAnimStart[sourceSet][idleAnim];
int idleCount = npcBunniesAnimations::charactersModAnimCount[sourceSet][idleAnim];
if(idleStart >= 0 && idleCount > 0) return idleStart;
}
for(int i = 0; i < int(stateNpcAnims.length); i++)
{
int npcAnim = stateNpcAnims[i];
sourceSet = CharactersModSourceSetForNpcType(npcType, npcAnim);
if(sourceSet < 0) continue;
int start = npcBunniesAnimations::charactersModAnimStart[sourceSet][npcAnim];
int count = npcBunniesAnimations::charactersModAnimCount[sourceSet][npcAnim];
if(start >= 0 && count > 0) return start;
}
return -1;
}
int CoordinateFrameIndexForNpcState(int npcType, int state, int frame)
{
// Coordinates in npcHats.asdat are character-relative frame numbers:
// Jazz idle starts at 0, walk starts at 5, etc.
// Custom NPC body frames can be packed much later in the global frame list
// (for example 170+), so subtract that NPC's first packed frame before
// using the value as a coordinate key.
if(state < 0) state = 0;
if(state >= int(stateRabbitAnims.length)) state = int(stateRabbitAnims.length) - 1;
int physical = NpcFrameIndexForState(npcType, state, frame);
int first = FirstNpcFrameIndexForCoordinateLookup(npcType);
if(first >= 0 && physical >= first) return physical - first;
int charSlot = CharacterSlotForNpcType(npcType);
int index = FrameIndexForRabbitAnim(charSlot, stateRabbitAnims[state], frame);
if(index >= 0) return index;
int fallbackSlot = DefaultCharacterSlotForCustom(charSlot);
if(fallbackSlot != charSlot)
{
index = FrameIndexForRabbitAnim(fallbackSlot, stateRabbitAnims[state], frame);
if(index >= 0) return index;
}
// Last resort: use the local animation frame supplied by the caller. Never
// return the packed 170+ body frame as an accessory coordinate key.
return frame < 0 ? 0 : frame;
}
int CompactFrameIndexForBaseCharacterAnim(int npcType, int animSet, int anim, int physicalFrame)
{
int charSlot = CharacterSlotForNpcType(npcType);
if(animSet != BaseAnimSetForCharacterSlot(charSlot)) return -1;
if(animSet < 0 || jjAnimSets[animSet].firstAnim == 0 || anim < 0) return -1;
int sourceSet = CharactersModSourceSetForNpcType(npcType, anim);
if(sourceSet < 0) return -1;
int compactStart = npcBunniesAnimations::charactersModAnimStart[sourceSet][anim];
int compactCount = npcBunniesAnimations::charactersModAnimCount[sourceSet][anim];
if(compactStart < 0 || compactCount <= 0) return -1;
int firstAnim = jjAnimSets[animSet].firstAnim;
int animIndex = firstAnim + anim;
int baseStart = jjAnimations[animIndex].firstFrame - jjAnimations[firstAnim].firstFrame;
int baseCount = jjAnimations[animIndex].frameCount;
if(baseCount <= 0) return compactStart;
int rel = physicalFrame - baseStart;
rel = rel % baseCount;
if(rel < 0) rel += baseCount;
int division = GetAnimationDivisionForCharacterSlot(charSlot);
if(division < 1) division = 1;
int editableFrameCount = compactCount / division;
if(editableFrameCount <= 0) editableFrameCount = compactCount;
rel = rel % editableFrameCount;
if(rel < 0) rel += editableFrameCount;
return compactStart + rel;
}
int CoordinateFrameIndexForNpcAnimDraw(int npcType, int animSet, int anim, int frame)
{
// Same coordinate-key fix as the editor path, but for the older Draw(...)
// entry point. Some NPC draw calls pass a packed/custom frame here too.
int physical = FrameIndexForAnim(animSet, anim, frame);
int compact = CompactFrameIndexForBaseCharacterAnim(npcType, animSet, anim, physical);
if(compact >= 0) return compact;
int first = FirstNpcFrameIndexForCoordinateLookup(npcType);
if(first >= 0 && physical >= first) return physical - first;
return physical >= 0 && physical < framesPerCharacter ? physical : (frame < 0 ? 0 : frame);
}
int CurrentEditorFrameIndex()
{
return NpcFrameIndexForState(EditorNpcType(), editorState, editorFrame);
}
int StoredAdjustmentFrameFromSegmentForSlot(int charSlot, int start, int count, int physicalFrame)
{
if(start < 0 || count <= 0) return physicalFrame;
if(physicalFrame < start || physicalFrame >= start + count) return physicalFrame;
int division = GetAnimationDivisionForCharacterSlot(charSlot);
if(division <= 1) return physicalFrame;
// Saved .asdat frame numbers stay physical/full-color indexes:
// Jazz idle uses 0, Jazz walk starts at 5, etc. Runtime color-layer
// frames collapse back to that saved full-color frame inside the same
// animation segment.
int editableFrameCount = (count + division - 1) / division;
if(editableFrameCount <= 0) return physicalFrame;
int rel = physicalFrame - start;
int baseRel = rel % editableFrameCount;
if(baseRel < 0) baseRel += editableFrameCount;
return start + baseRel;
}
int StoredAdjustmentFrameFromSegment(int npcType, int start, int count, int physicalFrame)
{
return StoredAdjustmentFrameFromSegmentForSlot(CharacterSlotForNpcType(npcType), start, count, physicalFrame);
}
int StoredAdjustmentFrameForNpcState(int npcType, int state, int physicalFrame)
{
if(state < 0) state = 0;
if(state >= int(stateNpcAnims.length)) state = int(stateNpcAnims.length) - 1;
if(npcType == npcBunniesCore::NpcAnim::EVA || npcType == ANIM::EVA)
return physicalFrame;
if(npcType == npcBunniesCore::NpcAnim::DEVAN || npcType == ANIM::DEVILDEVAN)
return physicalFrame;
int npcAnim = ActualNpcAnimForEditorState(npcType, state);
int sourceSet = CharactersModSourceSetForNpcType(npcType, npcAnim);
if(sourceSet >= 0)
{
int start = npcBunniesAnimations::charactersModAnimStart[sourceSet][npcAnim];
int count = npcBunniesAnimations::charactersModAnimCount[sourceSet][npcAnim];
return StoredAdjustmentFrameFromSegment(npcType, start, count, physicalFrame);
}
int charSlot = CharacterSlotForNpcType(npcType);
int rabbitAnim = stateRabbitAnims[state];
int fallbackSourceSet = SourceSetForCharacterSlot(charSlot);
if(fallbackSourceSet >= 0 && fallbackSourceSet < int(minAnimStart.length)
&& rabbitAnim >= 0 && rabbitAnim < int(minAnimStart[fallbackSourceSet].length))
{
int start = minAnimStart[fallbackSourceSet][rabbitAnim];
int count = minAnimCount[fallbackSourceSet][rabbitAnim];
if(start >= 0 && count > 0)
return StoredAdjustmentFrameFromSegment(npcType, start, count, physicalFrame);
}
int baseAnimSet = BaseAnimSetForCharacterSlot(charSlot);
if(baseAnimSet >= 0 && jjAnimSets[baseAnimSet].firstAnim > 0 && rabbitAnim >= 0)
{
int firstAnim = jjAnimSets[baseAnimSet].firstAnim;
int animIndex = firstAnim + rabbitAnim;
int start = jjAnimations[animIndex].firstFrame - jjAnimations[firstAnim].firstFrame;
int count = jjAnimations[animIndex].frameCount;
return StoredAdjustmentFrameFromSegment(npcType, start, count, physicalFrame);
}
return physicalFrame;
}
int StoredAdjustmentFrameForAnim(int npcType, int animSet, int anim, int physicalFrame)
{
if(animSet < 0 || jjAnimSets[animSet].firstAnim == 0 || anim < 0) return physicalFrame;
int compact = CompactFrameIndexForBaseCharacterAnim(npcType, animSet, anim, physicalFrame);
if(compact >= 0) return compact;
int firstAnim = jjAnimSets[animSet].firstAnim;
int animIndex = firstAnim + anim;
int start = jjAnimations[animIndex].firstFrame - jjAnimations[firstAnim].firstFrame;
int count = jjAnimations[animIndex].frameCount;
return StoredAdjustmentFrameFromSegment(npcType, start, count, physicalFrame);
}
int StoredAdjustmentFrameForCharacterSlotNpcState(int charSlot, int state, int physicalFrame)
{
if(state < 0) state = 0;
if(state >= int(stateRabbitAnims.length)) state = int(stateRabbitAnims.length) - 1;
int rabbitAnim = stateRabbitAnims[state];
int sourceSet = SourceSetForCharacterSlot(charSlot);
if(sourceSet >= 0 && sourceSet < int(minAnimStart.length)
&& rabbitAnim >= 0 && rabbitAnim < int(minAnimStart[sourceSet].length))
{
int start = minAnimStart[sourceSet][rabbitAnim];
int count = minAnimCount[sourceSet][rabbitAnim];
if(start >= 0 && count > 0)
return StoredAdjustmentFrameFromSegmentForSlot(charSlot, start, count, physicalFrame);
}
int baseAnimSet = BaseAnimSetForCharacterSlot(charSlot);
if(baseAnimSet >= 0 && jjAnimSets[baseAnimSet].firstAnim > 0 && rabbitAnim >= 0)
{
int firstAnim = jjAnimSets[baseAnimSet].firstAnim;
int animIndex = firstAnim + rabbitAnim;
int start = jjAnimations[animIndex].firstFrame - jjAnimations[firstAnim].firstFrame;
int count = jjAnimations[animIndex].frameCount;
return StoredAdjustmentFrameFromSegmentForSlot(charSlot, start, count, physicalFrame);
}
return physicalFrame;
}
int StoredAdjustmentFrameForCharacterSlotAnim(int charSlot, int animSet, int anim, int physicalFrame)
{
if(animSet < 0 || jjAnimSets[animSet].firstAnim == 0 || anim < 0) return physicalFrame;
int firstAnim = jjAnimSets[animSet].firstAnim;
int animIndex = firstAnim + anim;
int start = jjAnimations[animIndex].firstFrame - jjAnimations[firstAnim].firstFrame;
int count = jjAnimations[animIndex].frameCount;
return StoredAdjustmentFrameFromSegmentForSlot(charSlot, start, count, physicalFrame);
}
int RabbitAnimForNpcState(int movementAnim, bool shooting, bool jumping, bool moving)
{
if(shooting)
{
if(jumping || movementAnim == RABBIT::JUMPING1 || movementAnim == RABBIT::FALL) return RABBIT::JUMPFIRERIGHT;
if(movementAnim == RABBIT::RUN1 || movementAnim == RABBIT::RUN3 || moving) return RUN_SHOOT_RABBIT_ANIM;
return RABBIT::FIRE;
}
if(movementAnim == RABBIT::RUN1) return RABBIT::RUN1;
if(movementAnim == RABBIT::RUN3) return RABBIT::RUN3;
if(movementAnim == RABBIT::FALL) return RABBIT::FALL;
if(movementAnim == RABBIT::JUMPING1) return RABBIT::JUMPING1;
return RABBIT::IDLE1;
}
int NpcStateForMovement(int movementAnim, bool shooting, bool jumping, bool moving)
{
if(shooting)
{
if(jumping || movementAnim == RABBIT::JUMPING1 || movementAnim == RABBIT::FALL) return 5;
if(movementAnim == RABBIT::RUN1 || movementAnim == RABBIT::RUN3 || moving) return 6;
return 5;
}
if(movementAnim == RABBIT::RUN1) return 1;
if(movementAnim == RABBIT::RUN3) return 2;
if(movementAnim == RABBIT::FALL) return 3;
if(movementAnim == RABBIT::JUMPING1) return 4;
return 0;
}
int NpcStateForActualAnim(int anim, int fallbackState)
{
for(int i = 0; i < int(stateNpcAnims.length); i++)
{
if(stateNpcAnims[i] == anim) return i;
}
return fallbackState;
}
int EditorNpcTypeIndex(int npcType)
{
if(npcType >= npcBunniesCore::NpcAnim::JAZZ && npcType <= npcBunniesCore::NpcAnim::EVA) return npcType;
return int(npcBunniesAnimations::animTypes.find(npcType));
}
int ActualNpcAnimForEditorState(int npcType, int state)
{
if(state < 0) state = 0;
if(state >= int(stateNpcAnims.length)) state = int(stateNpcAnims.length) - 1;
int npcTypeIndex = EditorNpcTypeIndex(npcType);
if(npcTypeIndex == npcBunniesCore::NpcAnim::FROG && state == 3) return 4;
if(state == 5)
{
if(npcTypeIndex == npcBunniesCore::NpcAnim::FROG) return 3;
if(npcBunniesCore::NpcHasStandingFightingAnimation(npcTypeIndex)) return 5;
return 0;
}
if(state == 6)
{
if(npcBunniesCore::NpcHasJumpFightingAnimation(npcTypeIndex)) return 6;
if(npcBunniesCore::NpcHasStandingFightingAnimation(npcTypeIndex)) return 5;
return 1;
}
return stateNpcAnims[state];
}
int DefaultCharacterSlotForCustom(int charSlot)
{
if(charSlot == 1 || charSlot == 11) return 1;
if(charSlot == 3) return 3;
if(charSlot == 4) return 4;
if(charSlot == 5 || charSlot == 6) return charSlot;
return 2;
}
int EditedIndexOrMissing(int category, int charSlot, int frameIndex)
{
int index = ExistingIndex(category, charSlot, frameIndex);
if(index >= 0 && edited[category][index]) return index;
return -1;
}
int ClosestEditedIndexOrMissing(int category, int charSlot, int frameIndex)
{
if(category < 0 || category >= CATEGORY_COUNT) return -1;
if(charSlot < 0 || charSlot >= characterSlotCount) return -1;
if(framesPerCharacter <= 0) return -1;
if(frameIndex < 0) frameIndex = 0;
if(frameIndex >= framesPerCharacter) frameIndex = framesPerCharacter - 1;
EnsureAccessoryLookupCache();
int index = charSlot * framesPerCharacter + frameIndex;
if(category >= int(nearestEditedIndex.length) || index < 0 || index >= int(nearestEditedIndex[category].length)) return -1;
return nearestEditedIndex[category][index];
}
int DrawAdjustmentIndex(int category, int storageSlot, int frameIndex, int storedFrameIndex, int fallbackStorageSlot = -1, int fallbackFrameIndex = -1, int fallbackStoredFrameIndex = -1)
{
// First use the saved full-color frame for this animation segment.
// Runtime draws the NPC body in color layers, but accessories are saved
// once on the full frame: 5, not 13/21/29/37 for a 5-layer walk segment.
int index = EditedIndexOrMissing(category, storageSlot, storedFrameIndex);
if(index >= 0) return index;
// Then use coordinates saved for this exact NPC slot/frame if it is a
// real non-layered frame or intentionally has its own adjustment.
if(storedFrameIndex != frameIndex)
{
index = EditedIndexOrMissing(category, storageSlot, frameIndex);
if(index >= 0) return index;
}
// If the exact frame math is wrong for a custom NPC, do not hide the
// accessory. Use the nearest coordinate that was actually saved for the
// same character slot. This keeps real .asdat coordinates and avoids
// the bad fake 0,0 fallback.
index = ClosestEditedIndexOrMissing(category, storageSlot, storedFrameIndex);
if(index >= 0) return index;
// Custom/NPC slots need the base character's coordinate frame, not the
// custom sprite frame. This is the xmasAccessories-style fallback, but
// still requires a real saved coordinate.
if(fallbackStorageSlot >= 0 && fallbackStorageSlot != storageSlot)
{
if(fallbackFrameIndex < 0) fallbackFrameIndex = frameIndex;
if(fallbackStoredFrameIndex < 0) fallbackStoredFrameIndex = storedFrameIndex;
index = EditedIndexOrMissing(category, fallbackStorageSlot, fallbackStoredFrameIndex);
if(index >= 0) return index;
if(fallbackStoredFrameIndex != fallbackFrameIndex)
{
index = EditedIndexOrMissing(category, fallbackStorageSlot, fallbackFrameIndex);
if(index >= 0) return index;
}
index = ClosestEditedIndexOrMissing(category, fallbackStorageSlot, fallbackStoredFrameIndex);
if(index >= 0) return index;
}
return -1;
}
void DrawAccessorySprite(int category, float x, float y, int direction, int grav, float baseScale, int accessoryFrame, int index, int coordinateFrameIndex, int storedFrameIndex, int physicalFrameIndex = -1, int state = -1)
{
float drawX = x + adjustX[category][index] * direction * baseScale;
float drawY = y + adjustY[category][index] * grav * baseScale;
jjDrawRotatedSprite(
drawX,
drawY,
ANIM::CUSTOM[accessorySet],
category,
accessoryFrame,
rotation[category][index] * direction * grav,
scaleX[category][index] * direction * baseScale,
scaleY[category][index] * grav * baseScale,
SPRITE::NORMAL,
0, 4, 4, -1
);
if(category == CATEGORY_HAT && NPC_DEBUG_HAT_POSITION_ON)
{
jjDrawString(
int(drawX),
int(drawY) - 50,
//"anim=" + ANIM::CUSTOM[accessorySet]
//+ " category=" + category +
"index="+index +
" coord=" + coordinateFrameIndex +
" stored=" + storedFrameIndex +
" phys=" + physicalFrameIndex +
" state=" + state +
" frame=" + accessoryFrame
+ " pos=" + adjustX[category][index] + ","
+ adjustY[category][index]
+ " rot=" + rotation[category][index],
STRING::SMALL,
STRING::NORMAL,
0, 4, 4, -1
);
}
}
int NpcAccessoryFrame(npcBunniesCore::npcBunny &in npc, int category)
{
if(category == CATEGORY_HAT) return npc.hatFrame;
if(category == CATEGORY_SCARF) return npc.scarfFrame;
return npc.glassesFrame;
}
bool HasDrawableAccessory(npcBunniesCore::npcBunny &in npc)
{
if(npc.scarfFrame >= 0 || npc.glassesFrame >= 0) return true;
if(npc.hatFrame < 0) return false;
return CanNpcUseHat(npc.type);
}
void Draw(npcBunniesCore::npcBunny &in npc, float x, float y, int animSet, int anim, int frame, int direction, int grav = 1, float baseScale = 1.0)
{
if(!npcBunniesCore::ENABLE_NPC_ACCESSORIES) return;
if(!NpcHasDrawableAccessory(npc)) return;
EnsureLoaded();
EnsureAccessoryLookupCache();
if(accessorySet < 0 || jjAnimSets[ANIM::CUSTOM[accessorySet]].firstAnim == 0) return;
int charSlot = CharacterSlotForNpcType(npc.type);
int storageSlot = StorageSlotForCharacterSlot(charSlot);
int physicalFrameIndex = FrameIndexForAnim(animSet, anim, frame);
int frameIndex = CoordinateFrameIndexForNpcAnimDraw(npc.type, animSet, anim, frame);
int storedFrameIndex = StoredAdjustmentFrameForAnim(npc.type, animSet, anim, physicalFrameIndex);
int fallbackFrameIndex = frameIndex;
int fallbackStoredFrameIndex = storedFrameIndex;
int fallbackSlot = DefaultCharacterSlotForCustom(charSlot);
int fallbackStorageSlot = StorageSlotForCharacterSlot(fallbackSlot);
if(fallbackSlot != charSlot)
{
int fallbackAnimSet = BaseAnimSetForCharacterSlot(fallbackSlot);
if(fallbackAnimSet >= 0 && jjAnimSets[fallbackAnimSet].firstAnim > 0)
fallbackFrameIndex = FrameIndexForAnim(fallbackAnimSet, anim, frame);
fallbackStoredFrameIndex = StoredAdjustmentFrameForCharacterSlotAnim(fallbackSlot, fallbackAnimSet, anim, fallbackFrameIndex);
}
for(int category = 0; category < CATEGORY_COUNT; category++)
{
if(category == CATEGORY_HAT && !CanNpcUseHat(npc.type)) continue;
int accessoryFrame = NpcAccessoryFrame(npc, category);
if(accessoryFrame < 0) continue;
accessoryFrame = ClampFrame(category, accessoryFrame);
if(accessoryFrame < 0) continue;
int index = DrawAdjustmentIndex(category, storageSlot, frameIndex, storedFrameIndex, fallbackStorageSlot, fallbackFrameIndex, fallbackStoredFrameIndex);
if(index < 0) continue;
DrawAccessorySprite(category, x, y, direction, grav, baseScale, accessoryFrame, index, frameIndex, storedFrameIndex, physicalFrameIndex, -1);
}
}
void DrawForNpcState(npcBunniesCore::npcBunny &in npc, float x, float y, int fallbackAnimSet, int fallbackAnim, int frame, int movementAnim, bool shooting, bool jumping, bool moving, int direction, int grav = 1, float baseScale = 1.0)
{
if(!npcBunniesCore::ENABLE_NPC_ACCESSORIES) return;
if(!NpcHasDrawableAccessory(npc)) return;
EnsureLoaded();
EnsureAccessoryLookupCache();
if(accessorySet < 0 || jjAnimSets[ANIM::CUSTOM[accessorySet]].firstAnim == 0) return;
int charSlot = CharacterSlotForNpcType(npc.type);
int movementState = NpcStateForMovement(movementAnim, shooting, jumping, moving);
int state = (npc.type == npcBunniesCore::NpcAnim::FROG || npc.type == npcBunniesCore::NpcAnim::DEVAN || npc.type == ANIM::DEVILDEVAN) ? movementState : NpcStateForActualAnim(fallbackAnim, movementState);
if((npc.type == npcBunniesCore::NpcAnim::EVA || npc.type == ANIM::EVA) && (movementState == 3 || movementState == 4)) state = movementState;
int stateFrameCount = StateFrameCountForNpc(npc, state);
if(stateFrameCount > 0)
{
frame = frame % stateFrameCount;
if(frame < 0) frame += stateFrameCount;
}
if(frame < 0) frame = 0;
if(frame >= framesPerCharacter) frame = framesPerCharacter - 1;
for(int category = 0; category < CATEGORY_COUNT; category++)
{
if(category == CATEGORY_HAT && !CanNpcUseHat(npc.type)) continue;
int accessoryFrame = NpcAccessoryFrame(npc, category);
if(accessoryFrame < 0) continue;
accessoryFrame = ClampFrame(category, accessoryFrame);
if(accessoryFrame < 0) continue;
int cacheIndex = LazyAccessoryCacheIndex(npc.id, state, frame, category);
if(cacheIndex >= 0
&& lazyAccessoryLookupGeneration[cacheIndex] == accessoryLookupGeneration
&& lazyAccessoryNpcType[cacheIndex] == npc.type
&& lazyAccessoryCategoryFrame[cacheIndex] == accessoryFrame)
{
if(lazyAccessoryIndex[cacheIndex] >= 0)
DrawAccessorySprite(category, x, y, direction, grav, baseScale, accessoryFrame, lazyAccessoryIndex[cacheIndex], lazyAccessoryCoordinateFrameIndex[cacheIndex], lazyAccessoryStoredFrameIndex[cacheIndex], lazyAccessoryPhysicalFrameIndex[cacheIndex], state);
continue;
}
int storageSlot = StorageSlotForCharacterSlot(charSlot);
int physicalFrameIndex = NpcFrameIndexForState(npc.type, state, frame);
if(physicalFrameIndex < 0) physicalFrameIndex = FrameIndexForAnim(fallbackAnimSet, fallbackAnim, frame);
int frameIndex = CoordinateFrameIndexForNpcState(npc.type, state, frame);
if(frameIndex < 0) frameIndex = physicalFrameIndex;
int storedFrameIndex = StoredAdjustmentFrameForNpcState(npc.type, state, physicalFrameIndex);
int fallbackFrameIndex = frameIndex;
int fallbackStoredFrameIndex = storedFrameIndex;
int fallbackSlot = DefaultCharacterSlotForCustom(charSlot);
int fallbackStorageSlot = StorageSlotForCharacterSlot(fallbackSlot);
if(fallbackSlot != charSlot)
{
fallbackFrameIndex = FrameIndexForRabbitAnim(fallbackSlot, stateRabbitAnims[state], frame);
if(fallbackFrameIndex < 0) fallbackFrameIndex = frameIndex;
fallbackStoredFrameIndex = StoredAdjustmentFrameForCharacterSlotNpcState(fallbackSlot, state, fallbackFrameIndex);
}
int index = DrawAdjustmentIndex(category, storageSlot, frameIndex, storedFrameIndex, fallbackStorageSlot, fallbackFrameIndex, fallbackStoredFrameIndex);
if(cacheIndex >= 0)
{
lazyAccessoryLookupGeneration[cacheIndex] = accessoryLookupGeneration;
lazyAccessoryNpcType[cacheIndex] = npc.type;
lazyAccessoryCategoryFrame[cacheIndex] = accessoryFrame;
lazyAccessoryIndex[cacheIndex] = index;
lazyAccessoryCoordinateFrameIndex[cacheIndex] = frameIndex;
lazyAccessoryStoredFrameIndex[cacheIndex] = storedFrameIndex;
lazyAccessoryPhysicalFrameIndex[cacheIndex] = physicalFrameIndex;
}
if(index < 0) continue;
DrawAccessorySprite(category, x, y, direction, grav, baseScale, accessoryFrame, index, frameIndex, storedFrameIndex, physicalFrameIndex, state);
}
}
void Load()
{
if(!npcBunniesCore::ENABLE_NPC_ACCESSORIES) return;
EnsureLoaded();
}
int GetBaseFrameForStoredAdjustment(int npcType, int state, int physicalFrame)
{
// Convert physical frame back to base frame (layer 0) for lookup
int npcAnim = ActualNpcAnimForEditorState(npcType, state);
int division = GetAnimationDivisionForNpcType(npcType);
int count = 1;
int sourceSet = CharactersModSourceSetForNpcType(npcType, npcAnim);
if(sourceSet >= 0)
{
count = npcBunniesAnimations::charactersModAnimCount[sourceSet][npcAnim];
}
int layerFrameCount = count / division;
if(layerFrameCount <= 0) return physicalFrame;
return physicalFrame % layerFrameCount;
}
void LoadEditorAdjustment()
{
EnsureLoaded();
int npcType = EditorNpcType();
int physicalFrameIndex = NpcFrameIndexForState(npcType, editorState, editorFrame);
int storedFrameIndex = StoredAdjustmentFrameForNpcState(npcType, editorState, physicalFrameIndex);
int index = ExistingIndex(editorCategory, StorageSlotForCharacterSlot(CharacterSlotForNpcType(npcType)), storedFrameIndex);
if(index < 0 || !edited[editorCategory][index])
{
editorX = 0;
editorY = 0;
editorRotation = 0;
editorScaleX = 1.0;
editorScaleY = 1.0;
return;
}
editorX = adjustX[editorCategory][index];
editorY = adjustY[editorCategory][index];
editorRotation = rotation[editorCategory][index];
editorScaleX = scaleX[editorCategory][index];
editorScaleY = scaleY[editorCategory][index];
}
void SaveEditorAdjustment()
{
EnsureLoaded();
int npcType = EditorNpcType();
int physicalFrameIndex = NpcFrameIndexForState(npcType, editorState, editorFrame);
int storedFrameIndex = StoredAdjustmentFrameForNpcState(npcType, editorState, physicalFrameIndex);
int index = Index(editorCategory, StorageSlotForCharacterSlot(CharacterSlotForNpcType(npcType)), storedFrameIndex);
adjustX[editorCategory][index] = editorX;
adjustY[editorCategory][index] = editorY;
rotation[editorCategory][index] = editorRotation;
scaleX[editorCategory][index] = editorScaleX;
scaleY[editorCategory][index] = editorScaleY;
edited[editorCategory][index] = true;
accessoryLookupDirty = true;
SaveCategory(editorCategory);
}
int PreviousEditorPhysicalFrameIndex(int npcType)
{
if(editorFrame > 0)
{
int previousPhysicalFrame = NpcFrameIndexForState(npcType, editorState, editorFrame - 1);
return StoredAdjustmentFrameForNpcState(npcType, editorState, previousPhysicalFrame);
}
if(editorState <= 0) return -1;
int previousState = editorState - 1;
int previousStateFrameCount = LogicalFrameCountForState(npcType, previousState);
if(previousStateFrameCount <= 0) return -1;
int previousPhysicalFrame = NpcFrameIndexForState(npcType, previousState, previousStateFrameCount - 1);
return StoredAdjustmentFrameForNpcState(npcType, previousState, previousPhysicalFrame);
}
void CopyPreviousEditorFrameAdjustment()
{
EnsureLoaded();
ClampEditor();
int npcType = EditorNpcType();
int previousFrameIndex = PreviousEditorPhysicalFrameIndex(npcType);
if(previousFrameIndex < 0) return;
int previousIndex = ExistingIndex(editorCategory, StorageSlotForCharacterSlot(CharacterSlotForNpcType(npcType)), previousFrameIndex);
if(previousIndex < 0 || !edited[editorCategory][previousIndex]) return;
editorX = adjustX[editorCategory][previousIndex];
editorY = adjustY[editorCategory][previousIndex];
editorRotation = rotation[editorCategory][previousIndex];
editorScaleX = scaleX[editorCategory][previousIndex];
editorScaleY = scaleY[editorCategory][previousIndex];
SaveEditorAdjustment();
}
int LogicalFrameCountForState(int npcType, int state)
{
if(state < 0) state = 0;
if(state >= int(stateNpcAnims.length)) state = int(stateNpcAnims.length) - 1;
int npcAnim = ActualNpcAnimForEditorState(npcType, state);
int division = GetAnimationDivisionForNpcType(npcType);
if(division < 1) division = 1;
int sourceSet = CharactersModSourceSetForNpcType(npcType, npcAnim);
if(sourceSet >= 0)
{
int count = npcBunniesAnimations::charactersModAnimCount[sourceSet][npcAnim];
if(count > 0) return (count + division - 1) / division;
}
return 1;
}
int CurrentEditorFrameCount()
{
if(EditorNpcType() == npcBunniesCore::NpcAnim::FROG && ActualNpcAnimForEditorState(EditorNpcType(), editorState) == 3)
return LogicalFrameCountForState(EditorNpcType(), editorState);
int specialCount = SpecialEditorPreviewFrameCount(EditorNpcType(), editorState);
if(specialCount > 0) return specialCount;
return LogicalFrameCountForState(EditorNpcType(), editorState);
}
int EditorFrameCountForState(int state)
{
int oldState = editorState;
editorState = state;
int count = CurrentEditorFrameCount();
editorState = oldState;
return count;
}
void MoveEditorFrame(int delta)
{
if(delta == 0) return;
if(editorField >= 0) CommitFieldEdit();
ClampEditor();
if(delta > 0)
{
int count = CurrentEditorFrameCount();
if(editorFrame + 1 < count) editorFrame++;
else
{
editorState++;
if(editorState >= int(stateNames.length)) editorState = 0;
editorFrame = 0;
}
}
else
{
if(editorFrame > 0) editorFrame--;
else
{
editorState--;
if(editorState < 0) editorState = int(stateNames.length) - 1;
int count = CurrentEditorFrameCount();
editorFrame = count > 0 ? count - 1 : 0;
}
}
ClampEditor();
LoadEditorAdjustment();
}
bool SpecialEditorPreviewAnim(int npcType, int state, int &out animSet, int &out anim, int &out yOffset, float &out sizeX, SPRITE::Mode &out spriteMode)
{
int actualAnim = ActualNpcAnimForEditorState(npcType, state);
yOffset = 0;
sizeX = 1;
spriteMode = SPRITE::NORMAL;
if(npcType == npcBunniesCore::NpcAnim::DEVAN || npcType == ANIM::DEVILDEVAN)
{
array<int> devanIndexes = {8, 18, 18, 2, 2, 11, 11};
if(actualAnim < 0) actualAnim = 0;
if(actualAnim >= int(devanIndexes.length)) actualAnim = 0;
animSet = ANIM::DEVILDEVAN;
anim = actualAnim == 5 ? 14 : devanIndexes[actualAnim];
yOffset = -16;
return true;
}
if(npcType == npcBunniesCore::NpcAnim::EVA || npcType == ANIM::EVA)
{
if(actualAnim == 3 || actualAnim == 4)
{
animSet = ANIM::FLAG;
anim = 6;
sizeX = 1;
}
else
{
animSet = ANIM::EVA;
anim = 0;
sizeX = -1;
}
yOffset = -4;
return true;
}
if(npcType == npcBunniesCore::NpcAnim::FROG)
{
if(actualAnim == 5) actualAnim = 3;
array<int> frogIndexes = {2, 12, 12, 9, 0, 9, 0};
if(actualAnim < 0) actualAnim = 0;
if(actualAnim >= int(frogIndexes.length)) actualAnim = 0;
animSet = ANIM::FROG;
anim = frogIndexes[actualAnim];
spriteMode = SPRITE::PALSHIFT;
return true;
}
if(npcType == npcBunniesCore::NpcAnim::BIRD)
{
animSet = ANIM::BIRD;
anim = 9;
spriteMode = SPRITE::PALSHIFT;
return true;
}
return false;
}
int SpecialEditorPreviewFrameCount(int npcType, int state)
{
int animSet = 0, anim = 0, yOffset = 0;
float sizeX = 1;
SPRITE::Mode spriteMode = SPRITE::NORMAL;
if(!SpecialEditorPreviewAnim(npcType, state, animSet, anim, yOffset, sizeX, spriteMode)) return -1;
if(jjAnimSets[animSet].firstAnim == 0) jjAnimSets[animSet].load();
if(jjAnimSets[animSet].firstAnim == 0) return 1;
int count = jjAnimations[jjAnimSets[animSet].firstAnim + anim].frameCount;
return count > 0 ? count : 1;
}
void ClampEditor()
{
if(editorCategory < 0) editorCategory = CATEGORY_COUNT - 1;
if(editorCategory >= CATEGORY_COUNT) editorCategory = 0;
if(editorTypeIndex < 0) editorTypeIndex = int(npcBunniesCore::npcList.length) - 1;
if(editorTypeIndex >= int(npcBunniesCore::npcList.length)) editorTypeIndex = 0;
if(editorState < 0) editorState = int(stateNames.length) - 1;
if(editorState >= int(stateNames.length)) editorState = 0;
int count = CurrentEditorFrameCount();
if(editorFrame < 0) editorFrame = count - 1;
if(editorFrame >= count) editorFrame = 0;
}
string FormatScale(float value)
{
bool negative = value < 0;
float absolute = negative ? -value : value;
int tenths = int(absolute * 10 + 0.5);
return (negative ? "-" : "") + (tenths / 10) + "." + (tenths % 10);
}
bool IsEditorFloatField(int action)
{
return action == NPC_HAT_ACTION_SCALE_X || action == NPC_HAT_ACTION_SCALE_Y;
}
bool IsEditorNumericField(int action)
{
return action == NPC_HAT_ACTION_FRAME || action == NPC_HAT_ACTION_POS_X || action == NPC_HAT_ACTION_POS_Y
|| action == NPC_HAT_ACTION_ROTATION || action == NPC_HAT_ACTION_SCALE_X || action == NPC_HAT_ACTION_SCALE_Y;
}
string EditorFieldValue(int action)
{
if(action == NPC_HAT_ACTION_FRAME) return "" + editorFrame;
if(action == NPC_HAT_ACTION_POS_X) return "" + editorX;
if(action == NPC_HAT_ACTION_POS_Y) return "" + editorY;
if(action == NPC_HAT_ACTION_ROTATION) return "" + editorRotation;
if(action == NPC_HAT_ACTION_SCALE_X) return FormatScale(editorScaleX);
if(action == NPC_HAT_ACTION_SCALE_Y) return FormatScale(editorScaleY);
return "";
}
void SetEditorFieldValue(int action, const string &in value)
{
if(value == "" || value == "-" || value == "." || value == "-.") return;
if(action == NPC_HAT_ACTION_FRAME)
{
editorFrame = parseInt(value);
ClampEditor();
LoadEditorAdjustment();
return;
}
if(action == NPC_HAT_ACTION_POS_X) editorX = parseInt(value);
else if(action == NPC_HAT_ACTION_POS_Y) editorY = parseInt(value);
else if(action == NPC_HAT_ACTION_ROTATION) editorRotation = parseInt(value);
else if(action == NPC_HAT_ACTION_SCALE_X) editorScaleX = parseFloat(value);
else if(action == NPC_HAT_ACTION_SCALE_Y) editorScaleY = parseFloat(value);
else return;
SaveEditorAdjustment();
}
void BeginFieldEdit(int action)
{
if(!IsEditorNumericField(action)) return;
editorField = action;
editorFieldValue = EditorFieldValue(action);
editorFieldFresh = true;
}
void CommitFieldEdit()
{
if(editorField < 0) return;
SetEditorFieldValue(editorField, editorFieldValue);
editorField = -1;
editorFieldValue = "";
editorFieldFresh = false;
}
void AppendEditChar(const string &in value)
{
if(editorFieldFresh)
{
editorFieldValue = "";
editorFieldFresh = false;
}
if(value == "-")
{
if(int(editorFieldValue.length) > 0 && editorFieldValue.substr(0, 1) == "-") editorFieldValue = editorFieldValue.substr(1);
else editorFieldValue = "-" + editorFieldValue;
return;
}
if(value == "." && (!IsEditorFloatField(editorField) || editorFieldValue.findFirst(".") >= 0)) return;
editorFieldValue += value;
}
bool ProcessEditorTextInput()
{
if(editorField < 0) return false;
int key = -1;
for(int i = 0x30; i <= 0x39 && key < 0; i++) if(jjKey[i]) key = i;
for(int i = 0x60; i <= 0x69 && key < 0; i++) if(jjKey[i]) key = i;
if(key < 0 && (jjKey[0xBD] || jjKey[0x6D])) key = 0xBD;
if(key < 0 && (jjKey[0xBE] || jjKey[0x6E])) key = 0xBE;
if(key < 0 && jjKey[0x08]) key = 0x08;
if(key < 0 && jjKey[0x0D]) key = 0x0D;
if(key < 0 && jjKey[0x1B]) key = 0x1B;
if(key < 0) { editorLastKey = -1; return true; }
if(key == editorLastKey) return true;
editorLastKey = key;
if(key >= 0x30 && key <= 0x39) AppendEditChar("" + (key - 0x30));
else if(key >= 0x60 && key <= 0x69) AppendEditChar("" + (key - 0x60));
else if(key == 0xBD) AppendEditChar("-");
else if(key == 0xBE) AppendEditChar(".");
else if(key == 0x08)
{
if(editorFieldFresh) { editorFieldValue = ""; editorFieldFresh = false; }
else if(int(editorFieldValue.length) > 0) editorFieldValue = editorFieldValue.substr(0, int(editorFieldValue.length) - 1);
}
else if(key == 0x0D) CommitFieldEdit();
else if(key == 0x1B) editorField = -1;
return true;
}
string StepperText(int action, const string &in text)
{
if(editorField != action) return text;
int colon = int(text.findFirst(":"));
if(colon >= 0) return text.substr(0, colon + 2) + editorFieldValue;
return editorFieldValue;
}
int EditorSmallTextWidth(const string &in text)
{
// Keep this deliberately conservative. The earlier variable-width guess
// underestimated JJ2's small font and pushed the > button into the text.
// Eight pixels per character overestimates narrow glyphs a little, but it
// keeps every current value readable while still placing > much closer than
// the old fixed x + 70 + w position.
return int(text.length) * 8;
}
void DrawStepper(jjCANVAS@ canvas, int x, int y, int w, int action, int delta, const string &in text)
{
string shown = StepperText(action, text);
int shownWidth = EditorSmallTextWidth(shown);
int rightButtonX = x + 22 + shownWidth + 32; // +10px safety gap after measured text
if(rightButtonX < x + 42) rightButtonX = x + 42;
int fieldW = rightButtonX - (x + 20) - 2;
if(fieldW < 16) fieldW = 16;
DbDrawButton(canvas, @editorWindow, x, y, 16, 16, action, 0, -delta, "<");
if(IsEditorNumericField(action))
{
canvas.drawRectangle(x + 20, y - 7, fieldW, 16, editorField == action ? 80 : 0, SPRITE::TRANSLUCENT);
DbAddControl(@editorWindow, x + 20, y, fieldW, 16, NPC_HAT_ACTION_EDIT_FIELD, 0, action, shown);
}
else DbAddControl(@editorWindow, x + 20, y, fieldW, 16, action, 0, delta, shown);
canvas.drawString(x + 22, y, shown, STRING::SMALL, editorField == action ? STRING::PALSHIFT : STRING::NORMAL, editorField == action ? 80 : 0);
DbDrawButton(canvas, @editorWindow, rightButtonX, y, 16, 16, action, 0, delta, ">");
}
void ApplyEditorAction(int action, int delta)
{
if(editorField >= 0 && action != NPC_HAT_ACTION_EDIT_FIELD) CommitFieldEdit();
if(action == NPC_HAT_ACTION_EDIT_FIELD) { BeginFieldEdit(delta); return; }
if(action == NPC_HAT_ACTION_CATEGORY) editorCategory += delta;
else if(action == NPC_HAT_ACTION_TYPE) editorTypeIndex += delta;
else if(action == NPC_HAT_ACTION_STATE) editorState += delta;
else if(action == NPC_HAT_ACTION_FRAME) editorFrame += delta;
else if(action == NPC_HAT_ACTION_INCREMENT) editorIncrement = editorIncrement == 1 ? 10 : 1;
else if(action == NPC_HAT_ACTION_POS_X) { editorX += delta * editorIncrement; SaveEditorAdjustment(); return; }
else if(action == NPC_HAT_ACTION_POS_Y) { editorY += delta * editorIncrement; SaveEditorAdjustment(); return; }
else if(action == NPC_HAT_ACTION_ROTATION) { editorRotation += delta * editorIncrement; SaveEditorAdjustment(); return; }
else if(action == NPC_HAT_ACTION_SCALE_X) { editorScaleX += delta * (editorIncrement == 1 ? 0.1 : 1); SaveEditorAdjustment(); return; }
else if(action == NPC_HAT_ACTION_SCALE_Y) { editorScaleY += delta * (editorIncrement == 1 ? 0.1 : 1); SaveEditorAdjustment(); return; }
else if(action == NPC_HAT_ACTION_MOUSE) { editorMouseMode = !editorMouseMode; return; }
ClampEditor();
LoadEditorAdjustment();
}
void HandleEditorFireKey(jjPLAYER@ play)
{
if(play.keyFire && !editorPressedFire)
{
if(editorField >= 0) CommitFieldEdit();
CopyPreviousEditorFrameAdjustment();
editorPressedFire = true;
}
else if(!play.keyFire) editorPressedFire = false;
}
void DrawNpcPreviewFrame(jjCANVAS@ canvas, int x, int y, int npcType, int state, int frame)
{
int previewFrameCount = EditorFrameCountForState(state);
int previewFrame = previewFrameCount > 0 ? frame % previewFrameCount : 0;
if(previewFrame < 0) previewFrame += previewFrameCount;
int previewAnimSet = 0, previewAnim = 0, previewYOffset = 0;
float previewSizeX = 1;
SPRITE::Mode previewSpriteMode = SPRITE::NORMAL;
if(SpecialEditorPreviewAnim(npcType, state, previewAnimSet, previewAnim, previewYOffset, previewSizeX, previewSpriteMode))
{
if(jjAnimSets[previewAnimSet].firstAnim == 0) jjAnimSets[previewAnimSet].load();
if(jjAnimSets[previewAnimSet].firstAnim > 0)
canvas.drawResizedSprite(x, y + previewYOffset, previewAnimSet, previewAnim, previewFrame, previewSizeX, 1, previewSpriteMode, 0);
}
else if(npcType >= 0 && npcType < int(npcBunniesAnimations::emptyCharSet.length))
{
int frameIndex = NpcFrameIndexForState(npcType, state, previewFrame);
int animSet = ANIM::CUSTOM[npcBunniesAnimations::emptyCharSet[npcType]];
if(jjAnimSets[animSet].firstAnim > 0)
canvas.drawSpriteFromCurFrame(x, y, jjAnimations[jjAnimSets[animSet].firstAnim].firstFrame + frameIndex, 0, SPRITE::NORMAL, 0);
}
}
void DrawEditorPreviewAccessory(jjCANVAS@ canvas, int x, int y, int npcType, int state, int frame, int accessoryFrame, bool useCurrentAdjustment)
{
int frameCount = FrameCount(editorCategory);
if(frameCount <= 0) return;
if(useCurrentAdjustment)
{
canvas.drawRotatedSprite(x + editorX, y + editorY, ANIM::CUSTOM[accessorySet], editorCategory, accessoryFrame, editorRotation, editorScaleX, editorScaleY);
return;
}
int previewFrameCount = EditorFrameCountForState(state);
int previewFrame = previewFrameCount > 0 ? frame % previewFrameCount : 0;
if(previewFrame < 0) previewFrame += previewFrameCount;
int charSlot = CharacterSlotForNpcType(npcType);
int storageSlot = StorageSlotForCharacterSlot(charSlot);
int physicalFrameIndex = NpcFrameIndexForState(npcType, state, previewFrame);
int frameIndex = CoordinateFrameIndexForNpcState(npcType, state, previewFrame);
if(frameIndex < 0) frameIndex = physicalFrameIndex;
int storedFrameIndex = StoredAdjustmentFrameForNpcState(npcType, state, physicalFrameIndex);
int fallbackSlot = DefaultCharacterSlotForCustom(charSlot);
int fallbackStorageSlot = StorageSlotForCharacterSlot(fallbackSlot);
int fallbackFrameIndex = frameIndex;
int fallbackStoredFrameIndex = storedFrameIndex;
if(fallbackSlot != charSlot)
{
fallbackFrameIndex = FrameIndexForRabbitAnim(fallbackSlot, stateRabbitAnims[state], previewFrame);
if(fallbackFrameIndex < 0) fallbackFrameIndex = frameIndex;
fallbackStoredFrameIndex = StoredAdjustmentFrameForCharacterSlotNpcState(fallbackSlot, state, fallbackFrameIndex);
}
int index = DrawAdjustmentIndex(editorCategory, storageSlot, frameIndex, storedFrameIndex, fallbackStorageSlot, fallbackFrameIndex, fallbackStoredFrameIndex);
if(index < 0) return;
canvas.drawRotatedSprite(
x + adjustX[editorCategory][index],
y + adjustY[editorCategory][index],
ANIM::CUSTOM[accessorySet],
editorCategory,
accessoryFrame,
rotation[editorCategory][index],
scaleX[editorCategory][index],
scaleY[editorCategory][index]
);
}
void DrawNpcPreview(jjPLAYER@ play, jjCANVAS@ canvas)
{
// Draw the editor preview directly on the score canvas. Calling jjDraw*
// from OnDrawScore queues world-layer sprites after the world has already
// been drawn, which is what made the selected bunny/accessory frame blink.
// This keeps the original transparent look: no preview rectangle/background.
int x = int(play.xPos - play.cameraX + 96);
int y = int(play.yPos - play.cameraY);
int npcType = EditorNpcType();
const int previewRowOffset = 32 * 3;
// Upper row: accessory index 1, three tiles above the normal preview row.
DrawNpcPreviewFrame(canvas, x, y - previewRowOffset, npcType, editorState, editorFrame);
DrawEditorPreviewAccessory(canvas, x, y - previewRowOffset, npcType, editorState, editorFrame, 1, true);
DrawNpcPreviewFrame(canvas, x + 64, y - previewRowOffset, npcType, editorState, jjGameTicks / 8);
DrawEditorPreviewAccessory(canvas, x + 64, y - previewRowOffset, npcType, editorState, jjGameTicks / 8, 1, false);
// Existing lower row: accessory index 0.
DrawNpcPreviewFrame(canvas, x, y, npcType, editorState, editorFrame);
DrawEditorPreviewAccessory(canvas, x, y, npcType, editorState, editorFrame, 0, true);
DrawNpcPreviewFrame(canvas, x + 64, y, npcType, editorState, jjGameTicks / 8);
DrawEditorPreviewAccessory(canvas, x + 64, y, npcType, editorState, jjGameTicks / 8, 0, false);
}
void HandleEditorMouse(jjPLAYER@ play)
{
if(!editorMouseMode) { editorMouseWasDown = false; return; }
bool down = jjKey[0x01];
bool over = DbPointIn(jjMouseX, jjMouseY + 8, editorWindow.x, editorWindow.y, editorWindow.w, editorWindow.h);
if(down && !editorMouseWasDown && !over)
{
float previewX = play.xPos + 96;
float previewY = play.yPos;
editorX = int(play.cameraX + jjMouseX - previewX);
editorY = int(play.cameraY + jjMouseY - previewY);
SaveEditorAdjustment();
}
editorMouseWasDown = down;
}
void OnMain()
{
if(!npcBunniesCore::ENABLE_NPC_ACCESSORIES) return;
if(!editorOpen) return;
if(ProcessEditorTextInput()) return;
if(!editorPressedKey)
{
if(jjKey[0x26]) { MoveEditorFrame(1); }
else if(jjKey[0x28]) { MoveEditorFrame(-1); }
else if(jjKey[0x57]) { editorY -= editorIncrement; SaveEditorAdjustment(); }
else if(jjKey[0x53]) { editorY += editorIncrement; SaveEditorAdjustment(); }
else if(jjKey[0x41]) { editorX -= editorIncrement; SaveEditorAdjustment(); }
else if(jjKey[0x44]) { editorX += editorIncrement; SaveEditorAdjustment(); }
else if(jjKey[0x51]) { editorRotation += editorIncrement; SaveEditorAdjustment(); }
else if(jjKey[0x45]) { editorRotation -= editorIncrement; SaveEditorAdjustment(); }
editorPressedKey = true;
}
if(!jjKey[0x26] && !jjKey[0x28] && !jjKey[0x57] && !jjKey[0x53] && !jjKey[0x41] && !jjKey[0x44] && !jjKey[0x51] && !jjKey[0x45]) editorPressedKey = false;
}
void OnDrawScore(jjPLAYER@ play, jjCANVAS@ canvas)
{
if(!npcBunniesCore::ENABLE_NPC_ACCESSORIES) return;
if(!editorOpen) return;
EnsureLoaded();
ClampEditor();
DbBegin(@editorWindow, "npcHatEditor", "NPC Hat Editor", jjResolutionWidth - 400, 72, 390, 230);
editorWindow.draggable = true;
editorWindow.closeButton = true;
editorWindow.titleBar = true;
editorWindow.backgroundColor = 32;
editorWindow.focusColor = 15;
DbDrawFrame(canvas, @editorWindow);
int x = editorWindow.x + 10;
int y = editorWindow.y + 32;
DrawStepper(canvas, x, y, 140, NPC_HAT_ACTION_CATEGORY, 1, "Category: " + categoryNames[editorCategory]); y += 22;
DrawStepper(canvas, x, y, 120, NPC_HAT_ACTION_TYPE, 1, "NPC: " + EditorNpcTypeName()); y += 22;
DrawStepper(canvas, x, y, 120, NPC_HAT_ACTION_STATE, 1, "State: " + stateNames[editorState]); y += 22;
DrawStepper(canvas, x, y, 82, NPC_HAT_ACTION_FRAME, 1, "Frame: " + editorFrame);
DrawStepper(canvas, x + 160, y, 90, NPC_HAT_ACTION_INCREMENT, 1, "Inc: " + editorIncrement); y += 24;
DrawStepper(canvas, x, y, 70, NPC_HAT_ACTION_POS_X, 1, "X: " + editorX);
DrawStepper(canvas, x + 140, y, 70, NPC_HAT_ACTION_POS_Y, 1, "Y: " + editorY); y += 22;
DrawStepper(canvas, x, y, 80, NPC_HAT_ACTION_ROTATION, 1, "Rot: " + editorRotation); y += 22;
DrawStepper(canvas, x, y, 82, NPC_HAT_ACTION_SCALE_X, 1, "SX: " + FormatScale(editorScaleX));
DrawStepper(canvas, x + 150, y, 82, NPC_HAT_ACTION_SCALE_Y, 1, "SY: " + FormatScale(editorScaleY)); y += 24;
DbDrawButton(canvas, @editorWindow, x, y, 110, 16, NPC_HAT_ACTION_MOUSE, 0, 0, "Mouse: " + (editorMouseMode ? "ON" : "OFF"), true, editorMouseMode);
DbResetResult(@editorResult);
DbProcessMouse(@editorWindow, @editorResult);
if(editorResult.closeRequested) { editorOpen = false; editorPressedFire = false; }
else if(editorResult.activated) ApplyEditorAction(editorResult.action, editorResult.b);
HandleEditorMouse(play);
HandleEditorFireKey(play);
DrawNpcPreview(play, canvas);
}
bool OnLocalChat(string &in text)
{
if(!jjRegexMatch(text, "^!npcHatEdit(?:\\s+(?:ON|OFF))?$", true) && !jjRegexMatch(text, "^!npcHatEditor(?:\\s+(?:ON|OFF))?$", true)) return false;
if(!npcBunniesCore::ENABLE_NPC_ACCESSORIES) return true;
if(!npcBunniesCore::LocalPlayerHasNpcBunniesAdminAccess()) return true;
int space = int(text.findFirst(" "));
if(space >= 0)
{
int textLength = int(text.length);
string arg = text.substr(space + 1, textLength - space - 1);
if(jjRegexMatch(arg, "^OFF$", true)) { editorOpen = false; editorPressedFire = false; }
else if(jjRegexMatch(arg, "^ON$", true)) editorOpen = true;
else editorOpen = !editorOpen;
}
else editorOpen = !editorOpen;
ClampEditor();
LoadEditorAdjustment();
return true;
}
}
}
Jazz2Online © 1999-INFINITY (Site Credits). We have a Privacy Policy. Jazz Jackrabbit, Jazz Jackrabbit 2, Jazz Jackrabbit Advance and all related trademarks and media are ™ and © Epic Games. Lori Jackrabbit is © Dean Dodrill. J2O development powered by Loops of Fury and Chemical Beats.
Eat your lima beans, Johnny.