1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
|
#include "2s2h/ObjectExtension/ObjectExtension.h"
#include "2s2h/GameInteractor/GameInteractor.h"
#include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h"
#include "2s2h/ShipInit.hpp"
#include "2s2h/ShipUtils.h"
#include <libultraship/bridge/consolevariablebridge.h>
#include <cassert>
extern "C" {
#include "assets/interface/parameter_static/parameter_static.h"
#include "overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope.h"
#include "overlays/actors/ovl_Boss_02/z_boss_02.h"
#include "overlays/actors/ovl_Boss_07/z_boss_07.h"
#include "functions.h"
#include "macros.h"
#include "variables.h"
}
#define CVAR_NAME "gEnhancements.Graphics.EnemyHealthBars"
#define CVAR CVarGetInteger(CVAR_NAME, 0)
typedef enum {
ENEMYHEALTH_ANCHOR_ACTOR,
ENEMYHEALTH_ANCHOR_TOP,
ENEMYHEALTH_ANCHOR_BOTTOM,
} EnemyHealthBarAnchorType;
struct ActorMaximumHealth {
u8 maximumHealth = 0;
};
static ObjectExtension::Register<ActorMaximumHealth> ActorMaximumHealthRegister;
static Vtx sEnemyHealthVtx[16];
static Mtx sEnemyHealthMtx[2];
u8 GetActorMaxHealth(Actor* actor) {
const ActorMaximumHealth* maxHealth = ObjectExtension::GetInstance().Get<ActorMaximumHealth>(actor);
return maxHealth != nullptr ? maxHealth->maximumHealth : ActorMaximumHealth{}.maximumHealth;
}
void SetActorMaximumHealth(const Actor* actor, u8 maximumHealth) {
ObjectExtension::GetInstance().Set<ActorMaximumHealth>(actor, ActorMaximumHealth{ maximumHealth });
}
// Draws an enemy health bar using the magic bar textures and positions it in a similar way to Z-Targeting
void Interface_DrawEnemyHealthBar(Attention* attention, PlayState* play) {
InterfaceContext* interfaceCtx = &play->interfaceCtx;
PauseContext* pauseCtx = &play->pauseCtx;
Player* player = GET_PLAYER(play);
Actor* actor = attention->reticleActor;
Actor* healthActor = actor;
if (actor == NULL || (actor->category != ACTORCAT_ENEMY && actor->category != ACTORCAT_BOSS) ||
attention->reticleFadeAlphaControl == 0 || ((s32)pauseCtx->state > PAUSE_STATE_OPENING_2)) {
return;
}
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 = 0;
s32 healthBar_offsetY = 0;
EnemyHealthBarAnchorType anchorType = ENEMYHEALTH_ANCHOR_ACTOR;
// Twinmold's tail actor is what is targettable, but the health values are on the parent actor (the body)
if (actor->id == ACTOR_BOSS_02 && actor->params == TWINMOLD_TYPE_TAIL && actor->parent != NULL &&
(actor->parent->params == TWINMOLD_TYPE_RED || actor->parent->params == TWINMOLD_TYPE_BLUE)) {
healthActor = actor->parent;
}
OPEN_DISPS(play->state.gfxCtx);
s16 texHeight = 16;
s16 endTexWidth = 8;
f32 halfBarWidth = endTexWidth + ((f32)healthBar_fillWidth / 2);
// Convert health to signed value and clamp to 0 (some actors overflow the health when subtracting damage)
s8 curHealth = CLAMP_MIN((s8)healthActor->colChkInfo.health, 0);
u8 maxHealth = GetActorMaxHealth(healthActor);
// If an enhancement like BossHealthMultiplier increases boss health above the original limit the UI knew about
// we update the maximum health to recalculate the percentage and keep the red bar inside the box.
if (curHealth > maxHealth) {
maxHealth = curHealth;
SetActorMaximumHealth(healthActor, maxHealth);
}
s16 healthBarFill = ((f32)curHealth / maxHealth) * healthBar_fillWidth;
if (anchorType == ENEMYHEALTH_ANCHOR_ACTOR) {
// Get actor projected position
Actor_GetProjectedPos(play, &actor->focus.pos, &projTargetCenter, &projTargetCappedInvW);
projTargetCenter.x = (SCREEN_WIDTH / 2) * (projTargetCenter.x * projTargetCappedInvW);
projTargetCenter.x = projTargetCenter.x * (CVarGetInteger("gModes.MirroredWorld.State", 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) + (texHeight / 2), (SCREEN_HEIGHT / 2) - (texHeight / 2));
} else if (anchorType == ENEMYHEALTH_ANCHOR_TOP) {
projTargetCenter.x = healthBar_offsetX;
projTargetCenter.y = (SCREEN_HEIGHT / 2) - (texHeight / 2) - healthBar_offsetY;
} else if (anchorType == ENEMYHEALTH_ANCHOR_BOTTOM) {
projTargetCenter.x = healthBar_offsetX;
projTargetCenter.y = (-SCREEN_HEIGHT / 2) + (texHeight / 2) - healthBar_offsetY;
}
// Health bar border end left
Ship_CreateQuadVertexGroup(&sEnemyHealthVtx[0], -floorf(halfBarWidth), -texHeight / 2, endTexWidth, texHeight,
false);
// Health bar border middle
Ship_CreateQuadVertexGroup(&sEnemyHealthVtx[4], -floorf(halfBarWidth) + endTexWidth, -texHeight / 2,
healthBar_fillWidth, texHeight, false);
// Health bar border end right
Ship_CreateQuadVertexGroup(&sEnemyHealthVtx[8], ceilf(halfBarWidth) - endTexWidth, -texHeight / 2, endTexWidth,
texHeight, true);
// Health bar fill
Ship_CreateQuadVertexGroup(&sEnemyHealthVtx[12], -floorf(halfBarWidth) + endTexWidth, (-texHeight / 2) + 3,
healthBarFill, 7, false);
if (((!(player->stateFlags1 & PLAYER_STATE1_TALKING)) || (actor != player->focusActor)) &&
attention->reticleRadius < 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 && attention->reticleRadius > 120.0f) {
slideInOffsetY = (attention->reticleRadius - 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_SetupDL39_Overlay(play->state.gfxCtx);
Matrix_Translate(projTargetCenter.x, projTargetCenter.y - slideInOffsetY, 0, MTXMODE_NEW);
Matrix_Scale(0.65f, 0.65f * -1.0f, 1.0f, MTXMODE_APPLY);
Matrix_ToMtx(&sEnemyHealthMtx[0]);
gSPMatrix(OVERLAY_DISP++, &sEnemyHealthMtx[0], 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++, (uintptr_t)sEnemyHealthVtx, 16, 0);
gDPLoadTextureBlock(OVERLAY_DISP++, (uintptr_t)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++, (uintptr_t)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++, (uintptr_t)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_Translate(-0.375f, -0.5f, 0, MTXMODE_APPLY);
Matrix_ToMtx(&sEnemyHealthMtx[1]);
gSPMatrix(OVERLAY_DISP++, &sEnemyHealthMtx[1], 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++, (uintptr_t)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);
gSP1Quadrangle(OVERLAY_DISP++, 12, 14, 15, 13, 0);
}
CLOSE_DISPS(play->state.gfxCtx);
}
static RegisterShipInitFunc initFunc(
[]() {
// Register actor extension health data and actor init hook once
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorInit>([](Actor* actor) {
u8 maxHealth = actor->colChkInfo.health;
// The remains in the Majora fight get their health set after init for the first time fighting,
// so we set the expected value now
if (actor->id == ACTOR_BOSS_07 && actor->params >= MAJORA_TYPE_REMAINS) {
maxHealth = 5;
}
SetActorMaximumHealth(actor, maxHealth);
});
COND_HOOK(OnInterfaceDrawStart, CVAR, []() {
if (gPlayState != NULL) {
Interface_DrawEnemyHealthBar(&gPlayState->actorCtx.attention, gPlayState);
}
});
},
{ CVAR_NAME });
|