From e90c8af4526f5d90d16b31dbe764c94f5aa32976 Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Wed, 12 Jul 2023 21:55:24 -0400 Subject: [PATCH] Enhancement: Enemy Health Bar (#3035) * add enemy health bar * add more cosmetic editor options to health bar * add tooltip * fix enemy health texture when no magic bar --- soh/include/z64actor.h | 3 + .../cosmetics/CosmeticsEditor.cpp | 60 +++++- .../Enhancements/cosmetics/cosmeticsTypes.h | 8 +- soh/soh/SohMenuBar.cpp | 2 + soh/src/code/z_actor.c | 5 + soh/src/code/z_parameter.c | 174 ++++++++++++++++++ 6 files changed, 243 insertions(+), 9 deletions(-) diff --git a/soh/include/z64actor.h b/soh/include/z64actor.h index 09e6a64ed..82c8611d1 100644 --- a/soh/include/z64actor.h +++ b/soh/include/z64actor.h @@ -178,6 +178,9 @@ typedef struct Actor { /* 0x134 */ ActorFunc draw; // Draw Routine. Called by `Actor_Draw` /* 0x138 */ ActorResetFunc reset; /* 0x13C */ char dbgPad[0x10]; // Padding that only exists in the debug rom + // #region SOH [General] + /* */ u8 maximumHealth; // Max health value for use with health bars, set on actor init + // #endregion } Actor; // size = 0x14C typedef enum { diff --git a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp index b678a253c..b5f3ec8be 100644 --- a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp +++ b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp @@ -257,6 +257,8 @@ static std::map cosmeticOptions = { COSMETIC_OPTION("Hud_Minimap", "Minimap", GROUP_HUD, ImVec4( 0, 255, 255, 255), false, true, false), COSMETIC_OPTION("Hud_MinimapPosition", "Minimap Position", GROUP_HUD, ImVec4(200, 255, 0, 255), false, true, true), COSMETIC_OPTION("Hud_MinimapEntrance", "Minimap Entrance", GROUP_HUD, ImVec4(200, 0, 0, 255), false, true, true), + COSMETIC_OPTION("Hud_EnemyHealthBar", "Enemy Health Bar", GROUP_HUD, ImVec4(255, 0, 0, 255), true, true, false), + COSMETIC_OPTION("Hud_EnemyHealthBorder", "Enemy Health Border", GROUP_HUD, ImVec4(255, 255, 255, 255), true, false, true), COSMETIC_OPTION("Title_FileChoose", "File Choose", GROUP_TITLE, ImVec4(100, 150, 255, 255), false, true, false), COSMETIC_OPTION("Title_NintendoLogo", "Nintendo Logo", GROUP_TITLE, ImVec4( 0, 0, 255, 255), false, true, true), @@ -400,6 +402,10 @@ void CosmeticsUpdateTick() { newColor.g = sin(frequency * (hue + index) + (2 * M_PI / 3)) * 127 + 128; newColor.b = sin(frequency * (hue + index) + (4 * M_PI / 3)) * 127 + 128; newColor.a = 255; + // For alpha supported options, retain the last set alpha instead of overwriting + if (cosmeticOption.supportsAlpha) { + newColor.a = cosmeticOption.currentColor.w * 255; + } cosmeticOption.currentColor.x = newColor.r / 255.0; cosmeticOption.currentColor.y = newColor.g / 255.0; @@ -1425,6 +1431,32 @@ void Draw_Placements(){ ImGui::EndTable(); } } + if (ImGui::CollapsingHeader("Enemy Health Bar position")) { + if (ImGui::BeginTable("enemyhealthbar", 1, FlagsTable)) { + ImGui::TableSetupColumn("Enemy Health Bar settings", FlagsCell, TablesCellsWidth); + Table_InitHeader(false); + std::string posTypeCVar = "gCosmetics.Hud_EnemyHealthBarPosType"; + UIWidgets::EnhancementRadioButton("Anchor to Enemy", posTypeCVar.c_str(), ENEMYHEALTH_ANCHOR_ACTOR); + UIWidgets::Tooltip("This will use enemy on screen position"); + UIWidgets::EnhancementRadioButton("Anchor to the top", posTypeCVar.c_str(), ENEMYHEALTH_ANCHOR_TOP); + UIWidgets::Tooltip("This will make your elements follow the top edge of your game window"); + UIWidgets::EnhancementRadioButton("Anchor to the bottom", posTypeCVar.c_str(), ENEMYHEALTH_ANCHOR_BOTTOM); + UIWidgets::Tooltip("This will make your elements follow the bottom edge of your game window"); + DrawPositionSlider("gCosmetics.Hud_EnemyHealthBar", -SCREEN_HEIGHT, SCREEN_HEIGHT, -ImGui::GetWindowViewport()->Size.x / 2, ImGui::GetWindowViewport()->Size.x / 2); + if (UIWidgets::EnhancementSliderInt("Health Bar Width: %d", "##EnemyHealthBarWidth", "gCosmetics.Hud_EnemyHealthBarWidth.Value", 32, 128, "", 64)) { + CVarSetInteger("gCosmetics.Hud_EnemyHealthBarWidth.Changed", 1); + } + UIWidgets::Tooltip("This will change the width of the health bar"); + ImGui::SameLine(); + if (ImGui::Button("Reset##EnemyHealthBarWidth")) { + CVarClear("gCosmetics.Hud_EnemyHealthBarWidth.Value"); + CVarClear("gCosmetics.Hud_EnemyHealthBarWidth.Changed"); + LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + ImGui::NewLine(); + ImGui::EndTable(); + } + } } void DrawSillyTab() { @@ -1539,6 +1571,10 @@ void RandomizeColor(CosmeticOption& cosmeticOption) { newColor.g = Random(0, 255); newColor.b = Random(0, 255); newColor.a = 255; + // For alpha supported options, retain the last set alpha instead of overwriting + if (cosmeticOption.supportsAlpha) { + newColor.a = cosmeticOption.currentColor.w * 255; + } cosmeticOption.currentColor.x = newColor.r / 255.0; cosmeticOption.currentColor.y = newColor.g / 255.0; @@ -1607,7 +1643,13 @@ void ResetColor(CosmeticOption& cosmeticOption) { } void DrawCosmeticRow(CosmeticOption& cosmeticOption) { - if (ImGui::ColorEdit3(cosmeticOption.label.c_str(), (float*)&cosmeticOption.currentColor, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) { + bool colorChanged; + if (cosmeticOption.supportsAlpha) { + colorChanged = ImGui::ColorEdit4(cosmeticOption.label.c_str(), (float*)&cosmeticOption.currentColor, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel); + } else { + colorChanged = ImGui::ColorEdit3(cosmeticOption.label.c_str(), (float*)&cosmeticOption.currentColor, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel); + } + if (colorChanged) { Color_RGBA8 color; color.r = cosmeticOption.currentColor.x * 255.0; color.g = cosmeticOption.currentColor.y * 255.0; @@ -1628,13 +1670,15 @@ void DrawCosmeticRow(CosmeticOption& cosmeticOption) { ApplyOrResetCustomGfxPatches(); LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); } - ImGui::SameLine(); - bool isRainbow = (bool)CVarGetInteger((cosmeticOption.rainbowCvar), 0); - if (ImGui::Checkbox(("Rainbow##" + cosmeticOption.label).c_str(), &isRainbow)) { - CVarSetInteger((cosmeticOption.rainbowCvar), isRainbow); - CVarSetInteger((cosmeticOption.changedCvar), 1); - ApplyOrResetCustomGfxPatches(); - LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + if (cosmeticOption.supportsRainbow) { + ImGui::SameLine(); + bool isRainbow = (bool)CVarGetInteger((cosmeticOption.rainbowCvar), 0); + if (ImGui::Checkbox(("Rainbow##" + cosmeticOption.label).c_str(), &isRainbow)) { + CVarSetInteger((cosmeticOption.rainbowCvar), isRainbow); + CVarSetInteger((cosmeticOption.changedCvar), 1); + ApplyOrResetCustomGfxPatches(); + LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } } ImGui::SameLine(); bool isLocked = (bool)CVarGetInteger((cosmeticOption.lockedCvar), 0); diff --git a/soh/soh/Enhancements/cosmetics/cosmeticsTypes.h b/soh/soh/Enhancements/cosmetics/cosmeticsTypes.h index 5da453d48..14f06552d 100644 --- a/soh/soh/Enhancements/cosmetics/cosmeticsTypes.h +++ b/soh/soh/Enhancements/cosmetics/cosmeticsTypes.h @@ -1,4 +1,10 @@ typedef enum { COLORSCHEME_N64, COLORSCHEME_GAMECUBE -} DefaultColorScheme; \ No newline at end of file +} DefaultColorScheme; + +typedef enum { + ENEMYHEALTH_ANCHOR_ACTOR, + ENEMYHEALTH_ANCHOR_TOP, + ENEMYHEALTH_ANCHOR_BOTTOM, +} EnemyHealthBarAnchorType; diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index 337a848e0..9330feec4 100644 --- a/soh/soh/SohMenuBar.cpp +++ b/soh/soh/SohMenuBar.cpp @@ -865,6 +865,8 @@ void DrawEnhancementsMenu() { UIWidgets::PaddedEnhancementCheckbox("Disable Crit wiggle", "gDisableCritWiggle", true, false); UIWidgets::Tooltip("Disable random camera wiggle at low health"); + UIWidgets::PaddedEnhancementCheckbox("Enemy Health Bars", "gEnemyHealthBar", true, false); + UIWidgets::Tooltip("Renders a health bar for enemies when Z-Targeted"); ImGui::EndMenu(); } diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index 3b3b4a970..fd44a3abd 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -1212,6 +1212,11 @@ void Actor_Init(Actor* actor, PlayState* play) { //Actor_SetObjectDependency(play, actor); actor->init(actor, play); actor->init = NULL; + + // For enemy health bar we need to know the max health during init + if (actor->category == ACTORCAT_ENEMY) { + actor->maximumHealth = actor->colChkInfo.health; + } } } diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index 3895cb3a9..89b3f73d8 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -3643,6 +3643,175 @@ void Interface_DrawMagicBar(PlayState* play) { CLOSE_DISPS(play->state.gfxCtx); } +static Vtx sEnemyHealthVtx[12]; + +// Build vertex coordinates for a quad command +// In order of top left, top right, bottom left, then bottom right +// Supports flipping the texture horizontally +void Interface_CreateQuadVertexGroup(Vtx* vtxList, s32 xStart, s32 yStart, s32 width, s32 height, u8 flippedH) { + vtxList[0].v.ob[0] = xStart; + vtxList[0].v.ob[1] = yStart; + vtxList[0].v.tc[0] = (flippedH ? width : 0) << 5; + vtxList[0].v.tc[1] = 0 << 5; + + vtxList[1].v.ob[0] = xStart + width; + vtxList[1].v.ob[1] = yStart; + vtxList[1].v.tc[0] = (flippedH ? width * 2 : width) << 5; + vtxList[1].v.tc[1] = 0 << 5; + + vtxList[2].v.ob[0] = xStart; + vtxList[2].v.ob[1] = yStart + height; + vtxList[2].v.tc[0] = (flippedH ? width : 0) << 5; + vtxList[2].v.tc[1] = height << 5; + + vtxList[3].v.ob[0] = xStart + width; + vtxList[3].v.ob[1] = yStart + height; + vtxList[3].v.tc[0] = (flippedH ? width * 2 : width) << 5; + vtxList[3].v.tc[1] = height << 5; +} + +// Draws an enemy health bar using the magic bar textures and positions it in a similar way to Z-Targeting +void Interface_DrawEnemyHealthBar(TargetContext* targetCtx, PlayState* play) { + InterfaceContext* interfaceCtx = &play->interfaceCtx; + Player* player = GET_PLAYER(play); + Actor* actor = targetCtx->targetedActor; + + Vec3f projTargetCenter; + f32 projTargetCappedInvW; + + Color_RGBA8 healthbar_red = { 255, 0, 0, 255 }; + Color_RGBA8 healthbar_border = { 255, 255, 255, 255 }; + s16 healthbar_fillWidth = 64; + s16 healthbar_actorOffset = 40; + s32 healthbar_offsetX = CVarGetInteger("gCosmetics.Hud_EnemyHealthBarPosX", 0); + s32 healthbar_offsetY = CVarGetInteger("gCosmetics.Hud_EnemyHealthBarPosY", 0); + s8 anchorType = CVarGetInteger("gCosmetics.Hud_EnemyHealthBarPosType", ENEMYHEALTH_ANCHOR_ACTOR); + + if (CVarGetInteger("gCosmetics.Hud_EnemyHealthBar.Changed", 0)) { + healthbar_red = CVarGetColor("gCosmetics.Hud_EnemyHealthBar.Value", healthbar_red); + } + if (CVarGetInteger("gCosmetics.Hud_EnemyHealthBorder.Changed", 0)) { + healthbar_border = CVarGetColor("gCosmetics.Hud_EnemyHealthBorder.Value", healthbar_border); + } + if (CVarGetInteger("gCosmetics.Hud_EnemyHealthBarWidth.Changed", 0)) { + healthbar_fillWidth = CVarGetInteger("gCosmetics.Hud_EnemyHealthBarWidth.Value", healthbar_fillWidth); + } + + OPEN_DISPS(play->state.gfxCtx); + + if (targetCtx->unk_48 != 0 && actor != NULL && actor->category == ACTORCAT_ENEMY) { + s16 texHeight = 16; + s16 endTexWidth = 8; + f32 scaleY = -0.75f; + f32 scaledHeight = -texHeight * scaleY; + f32 halfBarWidth = endTexWidth + (healthbar_fillWidth / 2); + s16 healthBarFill = ((f32)actor->colChkInfo.health / actor->maximumHealth) * healthbar_fillWidth; + + if (anchorType == ENEMYHEALTH_ANCHOR_ACTOR) { + // Get actor projected position + func_8002BE04(play, &targetCtx->targetCenterPos, &projTargetCenter, &projTargetCappedInvW); + + projTargetCenter.x = (SCREEN_WIDTH / 2) * (projTargetCenter.x * projTargetCappedInvW); + projTargetCenter.x = projTargetCenter.x * (CVarGetInteger("gMirroredWorld", 0) ? -1 : 1); + projTargetCenter.x = CLAMP(projTargetCenter.x, (-SCREEN_WIDTH / 2) + halfBarWidth, + (SCREEN_WIDTH / 2) - halfBarWidth); + + projTargetCenter.y = (SCREEN_HEIGHT / 2) * (projTargetCenter.y * projTargetCappedInvW); + projTargetCenter.y = projTargetCenter.y - healthbar_offsetY + healthbar_actorOffset; + projTargetCenter.y = CLAMP(projTargetCenter.y, (-SCREEN_HEIGHT / 2) + (scaledHeight / 2), + (SCREEN_HEIGHT / 2) - (scaledHeight / 2)); + } else if (anchorType == ENEMYHEALTH_ANCHOR_TOP) { + projTargetCenter.x = healthbar_offsetX; + projTargetCenter.y = (SCREEN_HEIGHT / 2) - (scaledHeight / 2) - healthbar_offsetY; + } else if (anchorType == ENEMYHEALTH_ANCHOR_BOTTOM) { + projTargetCenter.x = healthbar_offsetX; + projTargetCenter.y = (-SCREEN_HEIGHT / 2) + (scaledHeight / 2) - healthbar_offsetY; + } + + // Health bar border end left + Interface_CreateQuadVertexGroup(&sEnemyHealthVtx[0], -halfBarWidth, -texHeight / 2, endTexWidth, texHeight, 0); + // Health bar border middle + Interface_CreateQuadVertexGroup(&sEnemyHealthVtx[4], -halfBarWidth + endTexWidth, -texHeight / 2, + healthbar_fillWidth, texHeight, 0); + // Health bar border end right + Interface_CreateQuadVertexGroup(&sEnemyHealthVtx[8], halfBarWidth - endTexWidth, -texHeight / 2, endTexWidth, + texHeight, 1); + // Health bar fill + Interface_CreateQuadVertexGroup(&sEnemyHealthVtx[12], -halfBarWidth + endTexWidth, (-texHeight / 2) + 3, + healthBarFill, 7, 0); + + if (((!(player->stateFlags1 & 0x40)) || (actor != player->unk_664)) && targetCtx->unk_44 < 500.0f) { + f32 slideInOffsetY = 0; + + // Slide in the health bar from edge of the screen (mimic the Z-Target triangles fly in) + if (anchorType == ENEMYHEALTH_ANCHOR_ACTOR && targetCtx->unk_44 > 120.0f) { + slideInOffsetY = (targetCtx->unk_44 - 120.0f) / 2; + // Slide in from the top if the bar is placed on the top half of the screen + if (healthbar_offsetY - healthbar_actorOffset <= 0) { + slideInOffsetY *= -1; + } + } + + // Setup DL for overlay disp + Gfx_SetupDL_39Overlay(play->state.gfxCtx); + + Matrix_Translate(projTargetCenter.x, projTargetCenter.y - slideInOffsetY, 0, MTXMODE_NEW); + Matrix_Scale(1.0f, scaleY, 1.0f, MTXMODE_APPLY); + gSPMatrix(OVERLAY_DISP++, MATRIX_NEWMTX(play->state.gfxCtx), G_MTX_MODELVIEW | G_MTX_LOAD); + + // Health bar border + gDPPipeSync(OVERLAY_DISP++); + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, healthbar_border.r, healthbar_border.g, healthbar_border.b, + healthbar_border.a); + gDPSetEnvColor(OVERLAY_DISP++, 100, 50, 50, 255); + + gSPVertex(OVERLAY_DISP++, sEnemyHealthVtx, 16, 0); + + gDPLoadTextureBlock(OVERLAY_DISP++, gMagicMeterEndTex, G_IM_FMT_IA, G_IM_SIZ_8b, endTexWidth, texHeight, 0, + G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, + G_TX_NOLOD, G_TX_NOLOD); + + gSP1Quadrangle(OVERLAY_DISP++, 0, 2, 3, 1, 0); + + gDPLoadTextureBlock(OVERLAY_DISP++, gMagicMeterMidTex, G_IM_FMT_IA, G_IM_SIZ_8b, 24, texHeight, 0, + G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, + G_TX_NOLOD, G_TX_NOLOD); + + gSP1Quadrangle(OVERLAY_DISP++, 4, 6, 7, 5, 0); + + gDPLoadTextureBlock(OVERLAY_DISP++, gMagicMeterEndTex, G_IM_FMT_IA, G_IM_SIZ_8b, endTexWidth, texHeight, 0, + G_TX_MIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, 3, G_TX_NOMASK, G_TX_NOLOD, + G_TX_NOLOD); + + gSP1Quadrangle(OVERLAY_DISP++, 8, 10, 11, 9, 0); + + // Health bar fill + Matrix_Push(); + Matrix_Translate(-0.375f, -0.5f, 0, MTXMODE_APPLY); + gSPMatrix(OVERLAY_DISP++, MATRIX_NEWMTX(play->state.gfxCtx), G_MTX_MODELVIEW | G_MTX_LOAD); + + gDPPipeSync(OVERLAY_DISP++); + gDPSetCombineLERP(OVERLAY_DISP++, PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, 0, 0, 0, PRIMITIVE, + PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, 0, 0, 0, PRIMITIVE); + gDPSetEnvColor(OVERLAY_DISP++, 0, 0, 0, 255); + + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, healthbar_red.r, healthbar_red.g, healthbar_red.b, healthbar_red.a); + + gDPLoadMultiBlock_4b(OVERLAY_DISP++, gMagicMeterFillTex, 0, G_TX_RENDERTILE, G_IM_FMT_I, 16, texHeight, 0, + G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, + G_TX_NOLOD, G_TX_NOLOD); + + gSPVertex(OVERLAY_DISP++, &sEnemyHealthVtx[12], 4, 0); + + gSP1Quadrangle(OVERLAY_DISP++, 0, 2, 3, 1, 0); + + Matrix_Pop(); + } + } + + CLOSE_DISPS(play->state.gfxCtx); +} + void func_80088AA0(s16 arg0) { gSaveContext.timerX[1] = 140; gSaveContext.timerY[1] = 80; @@ -5096,6 +5265,11 @@ void Interface_Draw(PlayState* play) { if (CVarGetInteger("gMirroredWorld", 0)) { gSPMatrix(OVERLAY_DISP++, interfaceCtx->view.projectionPtr, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_PROJECTION); } + + // Render enemy health bar after Z-target to leverage set variables + if (CVarGetInteger("gEnemyHealthBar", 0)) { + Interface_DrawEnemyHealthBar(&play->actorCtx.targetCtx, play); + } } Gfx_SetupDL_39Overlay(play->state.gfxCtx);