Compare commits

...
Author SHA1 Message Date
Mike Klaas 3d586168bd single shot mode 2026-07-17 21:49:41 -07:00
Mike Klaas 178ef908e0 cleanup 2026-07-16 22:10:26 -07:00
Mike Klaas 4e22593026 HRP style + burst mode 2026-07-16 22:02:00 -07:00
Harley Folgado eeffaed8a3 Add optional alternate ammo metre 2026-07-16 21:05:19 -07:00
7 changed files with 234 additions and 11 deletions
+2
View File
@@ -153,6 +153,8 @@ In time this stuff will receive in-game interface, right now you have to do it m
* Integrated "HELP" menu
* 44.1 kHz stereo sound/music supported, in .ogg and .wav format as well as legacy .acm
* Last used save slot is remembered
* You can cancel elevator floor selection using ESC
* Spiffy ammo meter with burst-mode segmentation
* Item/Corpse/Container/Critter highlighting (configure using [mods/sfall-mods.ini](https://github.com/sfall-team/sfall/blob/master/artifacts/config_files/sfall-mods.ini))
## Contributing
+2
View File
@@ -110,6 +110,8 @@ iface_bar_mode=0
iface_bar_side_art=2
iface_bar_sides_ori=0
iface_bar_width=800
;Set to 1 to use the HRP-style alternate ammo meter with burst segments, or 2 to also segment single-shot firearms with up to 6 shots.
alternate_ammo_meter=1
;Whether to load EDG files (HRP format) when loading maps. If loaded, they override default edge clipping and scroll blocking behavior.
edg_support=1
ignore_map_edges=0
+1
View File
@@ -28,6 +28,7 @@ namespace fallout {
#define GAME_CONFIG_IFACE_BAR_WIDTH_KEY "iface_bar_width"
#define GAME_CONFIG_IFACE_BAR_SIDE_ART_KEY "iface_bar_side_art"
#define GAME_CONFIG_IFACE_BAR_SIDES_ORI_KEY "iface_bar_sides_ori"
#define GAME_CONFIG_ALTERNATE_AMMO_METER_KEY "alternate_ammo_meter"
#define GAME_CONFIG_IGNORE_MAP_EDGES_KEY "ignore_map_edges"
#define GAME_CONFIG_SPLASH_SCREEN_SIZE_KEY "splash_screen_size"
#define GAME_CONFIG_MOVIE_ASPECT_FIT_KEY "movie_aspect_fit"
+1
View File
@@ -38,6 +38,7 @@ namespace {
{ "IFACE", "IFACE_BAR_WIDTH", GAME_CONFIG_UI_KEY, GAME_CONFIG_IFACE_BAR_WIDTH_KEY },
{ "IFACE", "IFACE_BAR_SIDE_ART", GAME_CONFIG_UI_KEY, GAME_CONFIG_IFACE_BAR_SIDE_ART_KEY },
{ "IFACE", "IFACE_BAR_SIDES_ORI", GAME_CONFIG_UI_KEY, GAME_CONFIG_IFACE_BAR_SIDES_ORI_KEY },
{ "IFACE", "ALTERNATE_AMMO_METRE", GAME_CONFIG_UI_KEY, GAME_CONFIG_ALTERNATE_AMMO_METER_KEY },
{ "MAPS", "IGNORE_MAP_EDGES", GAME_CONFIG_UI_KEY, GAME_CONFIG_IGNORE_MAP_EDGES_KEY },
{ "STATIC_SCREENS", "SPLASH_SCRN_SIZE", GAME_CONFIG_UI_KEY, GAME_CONFIG_SPLASH_SCREEN_SIZE_KEY },
{ "MOVIES", "MOVIE_SIZE", GAME_CONFIG_UI_KEY, GAME_CONFIG_MOVIE_ASPECT_FIT_KEY },
+224 -11
View File
@@ -111,6 +111,26 @@ constexpr int kCustomIndicatorMaxCount = kCustomIndicatorMaxTag - kCustomIndicat
constexpr int kCustomIndicatorDefaultCount = 5;
constexpr int kCustomIndicatorTextLength = 19;
constexpr int kCustomIndicatorTextBufferSize = kCustomIndicatorTextLength + 1;
constexpr int kAmmoBarLeft = 463;
constexpr int kAmmoBarTop = 26;
constexpr int kAmmoBarMaxRatio = 70;
constexpr int kAmmoAlternateMeterWidth = 4;
constexpr int kAmmoAlternateMeterTopBorder = kAmmoBarTop - 1;
constexpr int kAmmoAlternateMeterMaxSegmentCount = 12;
constexpr int kAmmoAlternateMeterMaxSingleShotSegmentCount = 6;
constexpr unsigned char kAmmoAlternateMeterLeftBorderColor = 11;
constexpr unsigned char kAmmoAlternateMeterTopBorderColor = 15;
constexpr unsigned char kAmmoAlternateMeterBottomBorderColor = 10;
constexpr unsigned char kAmmoAlternateMeterEmptyDarkColor = 13;
constexpr unsigned char kAmmoAlternateMeterEmptyLightColor = 15;
constexpr unsigned char kAmmoAlternateMeterFillColor = 196;
constexpr unsigned char kAmmoAlternateMeterFillShadeColor = 75;
enum class AmmoMeterDividerStyle {
None,
Dark,
Light,
};
struct CustomIndicatorDescription {
bool isActive;
@@ -130,7 +150,14 @@ static int endTurnButtonInit();
static int endTurnButtonFree();
static int endCombatButtonInit();
static int endCombatButtonFree();
static void interfaceUpdateAmmoBar(int x, int ratio);
static void interfaceUpdateAmmoBar(int x, int ratio, int ammoCapacity, int ammoPerShot);
static int interfaceGetActiveWeaponAmmoPerShot(const InterfaceItemState* itemState, int ammoCapacity);
static int interfaceGetWeaponAmmoPerShot(Object* weapon, int hitMode, int ammoCapacity);
static void interfaceUpdateAlternateAmmoMeter(int x, int ratio, int ammoCapacity, int ammoPerShot);
static void interfaceRestoreAlternateAmmoMeterBackground(int x);
static void interfaceUpdateAlternateAmmoMeterRow(unsigned char* dest, bool filled, bool lowerHalf);
static AmmoMeterDividerStyle interfaceAlternateAmmoMeterGetDividerStyle(int row, int ammoCapacity, int ammoPerShot);
static void interfaceUpdateAlternateAmmoMeterDividerRow(unsigned char* dest, AmmoMeterDividerStyle style);
static int _intface_item_reload();
static void interfaceDrawActionButtonOverlay(unsigned char* data, int width, int height, int pitch, int upX, int upY, int darkenColor);
static void interfaceRenderCounterAnimationStep(unsigned char* src, unsigned char* dest, int delayMs, Rect* numbersRect, bool refreshMouse);
@@ -1427,24 +1454,27 @@ int _intface_update_ammo_lights()
InterfaceItemState* p = &(gInterfaceItemStates[gInterfaceCurrentHand]);
int ratio = 0;
int maximum = 0;
int ammoPerShot = 0;
if (p->isWeapon != 0) {
int maximum = ammoGetCapacity(p->item);
maximum = ammoGetCapacity(p->item);
if (maximum > 0) {
int current = ammoGetQuantity(p->item);
ratio = (int)((double)current / (double)maximum * 70.0);
ratio = std::clamp(static_cast<int>(static_cast<long long>(current) * kAmmoBarMaxRatio / maximum), 0, kAmmoBarMaxRatio);
ammoPerShot = settings.ui.alternate_ammo_meter != 0 ? interfaceGetActiveWeaponAmmoPerShot(p, maximum) : 0;
}
} else {
if (itemGetType(p->item) == ITEM_TYPE_MISC) {
int maximum = miscItemGetMaxCharges(p->item);
maximum = miscItemGetMaxCharges(p->item);
if (maximum > 0) {
int current = miscItemGetCharges(p->item);
ratio = (int)((double)current / (double)maximum * 70.0);
ratio = std::clamp(static_cast<int>(static_cast<long long>(current) * kAmmoBarMaxRatio / maximum), 0, kAmmoBarMaxRatio);
}
}
}
interfaceUpdateAmmoBar(463 + gInterfaceBarContentOffset, ratio);
interfaceUpdateAmmoBar(kAmmoBarLeft + gInterfaceBarContentOffset, ratio, maximum, ammoPerShot);
return 0;
}
@@ -2051,15 +2081,20 @@ static int endCombatButtonFree()
}
// 0x460AA0 intface_draw_ammo_lights
static void interfaceUpdateAmmoBar(int x, int ratio)
static void interfaceUpdateAmmoBar(int x, int ratio, int ammoCapacity, int ammoPerShot)
{
if (settings.ui.alternate_ammo_meter != 0) {
interfaceUpdateAlternateAmmoMeter(x, ratio, ammoCapacity, ammoPerShot);
return;
}
if ((ratio & 1) != 0) {
ratio -= 1;
}
unsigned char* dest = gInterfaceWindowBuffer + gInterfaceBarWidth * 26 + x;
unsigned char* dest = gInterfaceWindowBuffer + gInterfaceBarWidth * kAmmoBarTop + x;
for (int index = 70; index > ratio; index--) {
for (int index = kAmmoBarMaxRatio; index > ratio; index--) {
*dest = 14;
dest += gInterfaceBarWidth;
}
@@ -2077,13 +2112,191 @@ static void interfaceUpdateAmmoBar(int x, int ratio)
if (!gInterfaceBarInitialized) {
Rect rect;
rect.left = x;
rect.top = 26;
rect.top = kAmmoBarTop;
rect.right = x + 1;
rect.bottom = 26 + 70;
rect.bottom = kAmmoBarTop + kAmmoBarMaxRatio;
windowRefreshRect(gInterfaceBarWindow, &rect);
}
}
static int interfaceGetActiveWeaponAmmoPerShot(const InterfaceItemState* itemState, int ammoCapacity)
{
int hitMode = -1;
switch (itemState->action) {
case INTERFACE_ITEM_ACTION_PRIMARY_AIMING:
case INTERFACE_ITEM_ACTION_PRIMARY:
hitMode = itemState->primaryHitMode;
break;
case INTERFACE_ITEM_ACTION_SECONDARY_AIMING:
case INTERFACE_ITEM_ACTION_SECONDARY:
hitMode = itemState->secondaryHitMode;
break;
default:
hitMode = itemState->primaryHitMode;
break;
}
return interfaceGetWeaponAmmoPerShot(itemState->item, hitMode, ammoCapacity);
}
static int interfaceGetWeaponAmmoPerShot(Object* weapon, int hitMode, int ammoCapacity)
{
if (weapon == nullptr || hitMode == -1) {
return 0;
}
int anim = weaponGetAnimationForHitMode(weapon, hitMode);
if (anim == ANIM_FIRE_BURST || anim == ANIM_FIRE_CONTINUOUS) {
return std::max(weaponGetBurstRounds(weapon), 1);
}
if (settings.ui.alternate_ammo_meter == 2 && anim == ANIM_FIRE_SINGLE) {
int ammoPerShot = 1;
if (weaponComputeAmmoCost(weapon, &ammoPerShot) == -1) {
ammoPerShot = 1;
}
ammoPerShot = std::max(ammoPerShot, 1);
return (ammoCapacity + ammoPerShot - 1) / ammoPerShot <= kAmmoAlternateMeterMaxSingleShotSegmentCount ? ammoPerShot : 0;
}
return 0;
}
static void interfaceUpdateAlternateAmmoMeter(int x, int ratio, int ammoCapacity, int ammoPerShot)
{
interfaceRestoreAlternateAmmoMeterBackground(x);
if (ammoCapacity > 0) {
if ((ratio & 1) != 0) {
ratio -= 1;
}
unsigned char* dest = gInterfaceWindowBuffer + gInterfaceBarWidth * kAmmoAlternateMeterTopBorder + x;
std::fill_n(dest, kAmmoAlternateMeterWidth, kAmmoAlternateMeterTopBorderColor);
dest += gInterfaceBarWidth;
int firstActiveRow = kAmmoBarMaxRatio - ratio;
for (int row = 0; row < kAmmoBarMaxRatio; row++) {
bool filled = row >= firstActiveRow;
AmmoMeterDividerStyle dividerStyle = interfaceAlternateAmmoMeterGetDividerStyle(row, ammoCapacity, ammoPerShot);
if (dividerStyle != AmmoMeterDividerStyle::None) {
interfaceUpdateAlternateAmmoMeterDividerRow(dest, dividerStyle);
} else {
bool lowerHalf = filled
? ((row - firstActiveRow) & 1) != 0
: (row & 1) != 0;
interfaceUpdateAlternateAmmoMeterRow(dest, filled, lowerHalf);
}
dest += gInterfaceBarWidth;
}
std::fill_n(dest, kAmmoAlternateMeterWidth, kAmmoAlternateMeterBottomBorderColor);
}
if (!gInterfaceBarInitialized) {
Rect rect;
rect.left = x;
rect.top = kAmmoAlternateMeterTopBorder;
rect.right = x + kAmmoAlternateMeterWidth - 1;
rect.bottom = kAmmoBarTop + kAmmoBarMaxRatio;
windowRefreshRect(gInterfaceBarWindow, &rect);
}
}
static void interfaceRestoreAlternateAmmoMeterBackground(int x)
{
unsigned char* src = nullptr;
FrmImage backgroundFrmImage;
if (gInterfaceBarIsCustom) {
src = customInterfaceBarGetBackgroundImageData();
} else {
int fid = buildFid(OBJ_TYPE_INTERFACE, 16, 0, 0, 0);
if (!backgroundFrmImage.lock(fid)) {
return;
}
src = backgroundFrmImage.getData();
}
int height = kAmmoBarMaxRatio + 2;
blitBufferToBuffer(src + gInterfaceBarWidth * kAmmoAlternateMeterTopBorder + x,
kAmmoAlternateMeterWidth,
height,
gInterfaceBarWidth,
gInterfaceWindowBuffer + gInterfaceBarWidth * kAmmoAlternateMeterTopBorder + x,
gInterfaceBarWidth);
}
static void interfaceUpdateAlternateAmmoMeterRow(unsigned char* dest, bool filled, bool lowerHalf)
{
dest[0] = kAmmoAlternateMeterLeftBorderColor;
if (filled) {
unsigned char color = lowerHalf ? kAmmoAlternateMeterFillShadeColor : kAmmoAlternateMeterFillColor;
dest[1] = color;
dest[2] = color;
dest[3] = kAmmoAlternateMeterFillShadeColor;
} else {
unsigned char color = lowerHalf ? kAmmoAlternateMeterEmptyLightColor : kAmmoAlternateMeterEmptyDarkColor;
dest[1] = color;
dest[2] = color;
dest[3] = kAmmoAlternateMeterEmptyLightColor;
}
}
static AmmoMeterDividerStyle interfaceAlternateAmmoMeterGetDividerStyle(int row, int ammoCapacity, int ammoPerShot)
{
if (ammoCapacity <= 0 || ammoPerShot <= 0 || ammoPerShot >= ammoCapacity) {
return AmmoMeterDividerStyle::None;
}
int segmentCount = (ammoCapacity + ammoPerShot - 1) / ammoPerShot;
if (segmentCount <= 1 || segmentCount > kAmmoAlternateMeterMaxSegmentCount) {
return AmmoMeterDividerStyle::None;
}
constexpr int dividerHeight = 2;
int segmentRows = kAmmoBarMaxRatio - dividerHeight * (segmentCount - 1);
if (segmentRows < segmentCount) {
return AmmoMeterDividerStyle::None;
}
int baseSegmentHeight = segmentRows / segmentCount;
int extraRows = segmentRows % segmentCount;
int dividerRow = 0;
for (int segmentIndex = 0; segmentIndex < segmentCount - 1; segmentIndex++) {
dividerRow += baseSegmentHeight + (segmentIndex < extraRows ? 1 : 0);
if (row == dividerRow) {
return AmmoMeterDividerStyle::Dark;
}
if (row == dividerRow + 1) {
return AmmoMeterDividerStyle::Light;
}
dividerRow += dividerHeight;
}
return AmmoMeterDividerStyle::None;
}
static void interfaceUpdateAlternateAmmoMeterDividerRow(unsigned char* dest, AmmoMeterDividerStyle style)
{
dest[0] = kAmmoAlternateMeterLeftBorderColor;
if (style == AmmoMeterDividerStyle::Dark) {
dest[1] = kAmmoAlternateMeterLeftBorderColor;
dest[2] = kAmmoAlternateMeterEmptyDarkColor;
dest[3] = kAmmoAlternateMeterEmptyLightColor;
} else {
dest[1] = kAmmoAlternateMeterEmptyLightColor;
dest[2] = kAmmoAlternateMeterEmptyLightColor;
dest[3] = kAmmoAlternateMeterEmptyLightColor;
}
}
// 0x460B20 intface_item_reload
static int _intface_item_reload()
{
+1
View File
@@ -156,6 +156,7 @@ void initSettingsRegistry(bool isMapper)
SETTING_P(iface_bar_width, clamp(640, 4320));
SETTING_P(iface_bar_side_art, clamp(0, 999));
SETTING(iface_bar_sides_ori);
SETTING_P(alternate_ammo_meter, clamp(0, 2));
SETTING_P(splash_screen_size, clamp(0, 2));
SETTING(movie_aspect_fit);
SETTING(edg_support);
+3
View File
@@ -59,6 +59,9 @@ struct UISettings {
// Iface-bar side graphics extend from the Screen edges to the Iface-Bar if true (otherwise from bar to edges).
bool iface_bar_sides_ori = false;
// 0 - vanilla ammo lights, 1 - alternate ammo meter with burst segments, 2 - also segment low-capacity single-shot weapons.
int alternate_ammo_meter = 0;
// Extends AP bar to 16 dots instead of 10.
bool extend_ap_bar = false;