Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/s25main/FOWObjects.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class fowBuilding : public FOWObject
void Serialize(SerializedGameData& sgd) const override;
void Draw(DrawPoint drawPt) const override;
FoW_Type GetType() const override { return FoW_Type::Building; }
BuildingType GetBuildingType() const { return type; }
};

/// Baustelle
Expand Down
11 changes: 11 additions & 0 deletions libs/s25main/controls/ctrlBuildingIcon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "ctrlBuildingIcon.h"
#include "Loader.h"
#include "driver/MouseCoords.h"
#include "files.h"
#include "ogl/glArchivItem_Bitmap.h"
#include "gameTypes/BuildingType.h"
Expand All @@ -28,3 +29,13 @@ void ctrlBuildingIcon::Draw_()
}

void ctrlBuildingIcon::DrawContent() const {}

bool ctrlBuildingIcon::Msg_MouseMove(const MouseCoords& mc)
{
const bool wasHovered = (state == ButtonState::Hover);
const bool result = ctrlButton::Msg_MouseMove(mc);
const bool nowHovered = (state == ButtonState::Hover);
if(wasHovered != nowHovered && onHoverChanged_)
onHoverChanged_(nowHovered);
return result;
}
8 changes: 8 additions & 0 deletions libs/s25main/controls/ctrlBuildingIcon.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#pragma once

#include "ctrlButton.h"
#include <functional>
class Window;

class ctrlBuildingIcon : public ctrlButton
Expand All @@ -15,11 +16,18 @@ class ctrlBuildingIcon : public ctrlButton
/// liefert den GebäudeTyp des Icons.
BuildingType GetType() const { return type; }

/// Set callback for hover-state changes (called with true when hovered, false when not)
void SetOnHoverChanged(std::function<void(bool)> cb) { onHoverChanged_ = std::move(cb); }

protected:
/// zeichnet das Fenster.
void Draw_() override;
void DrawContent() const override;
bool Msg_MouseMove(const MouseCoords& mc) override;

const BuildingType type; /// der GebäudeType des Icons.
const Nation nation; /// Volk

private:
std::function<void(bool)> onHoverChanged_;
};
2 changes: 1 addition & 1 deletion libs/s25main/figures/nofCatapultMan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ void nofCatapultMan::HandleDerivedEvent(const unsigned /*id*/)
unsigned distance = world->CalcDistance(pos, building->GetPos());

// Entfernung nicht zu hoch?
if(distance < 14)
if(distance <= CATAPULT_MAX_TARGET_RANGE)
{
// Mit in die Liste aufnehmen
possibleTargets.push_back(PossibleTarget(building->GetPos(), distance));
Expand Down
3 changes: 3 additions & 0 deletions libs/s25main/figures/nofCatapultMan.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
class SerializedGameData;
class nobUsual;

/// Maximum distance to a target the catapult can attack (distance < 14 -> max 13)
constexpr unsigned CATAPULT_MAX_TARGET_RANGE = 13;

/// Arbeiter im Katapult
class nofCatapultMan : public nofBuildingWorker
{
Expand Down
6 changes: 2 additions & 4 deletions libs/s25main/figures/nofHunter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,14 @@ void nofHunter::HandleDerivedEvent(unsigned /*id*/)
void nofHunter::TryStartHunting()
{
// Find animals in a square around building (actually should be circle, but animals are moving anyway)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhm hunters look for animals in a SQUARE?

Should fix to be circle? Kinda out of scope but can include or do separate PR. Cause right now the range outline doesn't match what game uses.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created PR #1948 to make Hunters use circle instead of square.

const int SQUARE_SIZE = 19;

// Liste mit den gefundenen Tieren
std::vector<noAnimal*> available_animals;

// Durchgehen und nach Tieren suchen
Position curPos;
for(curPos.y = pos.y - SQUARE_SIZE; curPos.y <= pos.y + SQUARE_SIZE; ++curPos.y)
for(curPos.y = pos.y - HUNTER_SEARCH_HALFSIDE; curPos.y <= pos.y + HUNTER_SEARCH_HALFSIDE; ++curPos.y)
{
for(curPos.x = pos.x - SQUARE_SIZE; curPos.x <= pos.x + SQUARE_SIZE; ++curPos.x)
for(curPos.x = pos.x - HUNTER_SEARCH_HALFSIDE; curPos.x <= pos.x + HUNTER_SEARCH_HALFSIDE; ++curPos.x)
{
MapPoint curMapPos = world->MakeMapPoint(curPos);

Expand Down
3 changes: 3 additions & 0 deletions libs/s25main/figures/nofHunter.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
#include "nofBuildingWorker.h"
#include "gameTypes/Direction.h"

/// Half-side length of the square the hunter scans for animals (centered on the building)
constexpr int HUNTER_SEARCH_HALFSIDE = 19;

class noAnimal;
class SerializedGameData;
class nobUsual;
Expand Down
43 changes: 43 additions & 0 deletions libs/s25main/gameData/BuildingConsts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

#include "BuildingConsts.h"
#include "mygettext/mygettext.h"
#include "gameData/MilitaryConsts.h"
#include "gameData/GameConsts.h"
#include "figures/nofFarmhand.h"
#include "figures/nofCatapultMan.h"
#include "figures/nofHunter.h"
#include "gameTypes/BuildingTypes.h"
#include <type_traits>

const helpers::EnumArray<const char*, BuildingType> BUILDING_NAMES = {
Expand Down Expand Up @@ -409,3 +415,40 @@ const helpers::MultiEnumArray<DrawPoint, Nation, BuildingType> BUILDING_ARMOR_SI
babylonians[BuildingType::Fortress] = DrawPoint(20, -34);
return result;
}();

unsigned GetBuildingRadius(BuildingType bld)
{
switch(bld)
{
// Military buildings (territory influence radius) — from MilitaryConsts.h
case BuildingType::Barracks: return MILITARY_RADIUS[0];
case BuildingType::Guardhouse: return MILITARY_RADIUS[1];
case BuildingType::Watchtower: return MILITARY_RADIUS[2];
case BuildingType::Fortress: return MILITARY_RADIUS[3];
// Headquarters
case BuildingType::Headquarters: return HQ_RADIUS;
// Harbor building
case BuildingType::HarborBuilding: return HARBOR_RADIUS;
// Lookout tower — scouting visibility range
case BuildingType::LookoutTower: return VISUALRANGE_LOOKOUTTOWER;
// Catapult attack range
case BuildingType::Catapult: return CATAPULT_MAX_TARGET_RANGE;
// Hunter searches for animals in a square of this half-side length
case BuildingType::Hunter: return HUNTER_SEARCH_HALFSIDE;
// Mines — miner stays inside and extracts from adjacent tiles
case BuildingType::GraniteMine:
case BuildingType::CoalMine:
case BuildingType::IronMine:
case BuildingType::GoldMine: return MINER_RADIUS;
// For farmhand-based buildings, delegate to nofFarmhand::GetWorkRadius via the BLD_WORK_DESC
// job mapping. This is the single source of truth for worker reach radii, including
// addon-adjustable ranges (woodcutter, forester, stonemason).
default:
{
const auto job = BLD_WORK_DESC[bld].job;
if(job)
return nofFarmhand::GetWorkRadius(*job);
return 0;
}
}
}
3 changes: 3 additions & 0 deletions libs/s25main/gameData/BuildingConsts.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,6 @@ constexpr std::array<DrawPoint, 4> SUPPRESS_UNUSED NUBIAN_MINE_FIRE = {{

/// Hilfetexte für Gebäude
extern const helpers::EnumArray<const char*, BuildingType> BUILDING_HELP_STRINGS;

/// Get the radius in tiles for a building type (worker reach, territory influence, attack range, etc.)
unsigned GetBuildingRadius(BuildingType bld);
36 changes: 35 additions & 1 deletion libs/s25main/ingameWindows/iwAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "gameData/BuildingConsts.h"
#include "gameData/const_gui_ids.h"
#include <sstream>
#include <boost/format.hpp>

// Tab - Flags
enum TabID
Expand Down Expand Up @@ -175,6 +176,11 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP
std::stringstream tooltip;
tooltip << _(BUILDING_NAMES[bld]);

// Radius anzeigen falls vorhanden
const unsigned radius = GetBuildingRadius(bld);
if(radius > 0)
tooltip << boost::format(_("\nRange: %1% tiles")) % radius;

tooltip << _("\nCosts: ");
if(BUILDING_COSTS[bld].boards > 0)
tooltip << (int)BUILDING_COSTS[bld].boards << _(" boards");
Expand All @@ -186,9 +192,26 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP
}

DrawPoint iconPos((k % 5) * 36, (k / 5) * 36 + 45);
build_tab->GetGroup(static_cast<int>(bt))
ctrlBuildingIcon* icon = build_tab->GetGroup(static_cast<int>(bt))
->AddBuildingIcon(k, iconPos, bld, player.nation, 36, tooltip.str());

// Set hover callback to show radius preview on the game world
if(radius > 0)
{
icon->SetOnHoverChanged([this, icon, radius](bool hovered) noexcept {
if(hovered)
{
hoveredBldIcon_ = icon;
this->gwv.SetRadiusPreview(std::make_pair(this->selectedPt, radius));
} else if(hoveredBldIcon_ == icon)
{
// Only clear if no other icon took over hover
hoveredBldIcon_ = nullptr;
this->gwv.SetRadiusPreview(std::nullopt);
}
});
}

++k;
}

Expand Down Expand Up @@ -407,6 +430,9 @@ void iwAction::Close()
{
if(ShouldBeClosed())
return;
// Clear radius preview on the game world
hoveredBldIcon_ = nullptr;
gwv.SetRadiusPreview(std::nullopt);
IngameWindow::Close();
if(mousePosAtOpen_.isValid())
VIDEODRIVER.SetMousePos(mousePosAtOpen_);
Expand Down Expand Up @@ -469,6 +495,10 @@ void iwAction::Msg_Group_ButtonClick(const unsigned /*group_id*/, const unsigned

void iwAction::Msg_TabChange(const unsigned ctrl_id, const unsigned short tab_id)
{
// Clear radius preview when switching tabs — old icons won't trigger mouse-leave
hoveredBldIcon_ = nullptr;
gwv.SetRadiusPreview(std::nullopt);

switch(ctrl_id)
{
case 0: // Haupttabs
Expand Down Expand Up @@ -513,6 +543,10 @@ void iwAction::Msg_TabChange(const unsigned ctrl_id, const unsigned short tab_id

void iwAction::Msg_Group_TabChange(const unsigned /*group_id*/, const unsigned ctrl_id, const unsigned short tab_id)
{
// Clear radius preview when switching build subtabs, same reason as above
hoveredBldIcon_ = nullptr;
gwv.SetRadiusPreview(std::nullopt);

switch(ctrl_id)
{
case 1: // Gebäudetabs
Expand Down
4 changes: 4 additions & 0 deletions libs/s25main/ingameWindows/iwAction.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "IngameWindow.h"
#include "variant.h"
#include "gameTypes/MapCoordinates.h"
class ctrlBuildingIcon;
#include <array>

class GameInterface;
Expand Down Expand Up @@ -69,6 +70,9 @@ class iwAction : public IngameWindow
/// Die einzelnen Höhen für die einzelnen Tabs im Bautab
std::array<unsigned short, 4> building_tab_heights;

/// The building icon currently hovered in the build tabs (used to show radius preview)
const ctrlBuildingIcon* hoveredBldIcon_ = nullptr;

public:
iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapPoint selectedPt, const DrawPoint& mousePos,
Params params, bool military_buildings);
Expand Down
68 changes: 68 additions & 0 deletions libs/s25main/world/GameWorldView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
// SPDX-License-Identifier: GPL-2.0-or-later

#include "world/GameWorldView.h"
#include <optional>
#include "CatapultStone.h"
#include "Cheats.h"
#include "FOWObjects.h"
#include "ReturnMapPointWithRadius.h"
#include "Window.h"
#include "GameInterface.h"
#include "GamePlayer.h"
#include "GlobalGameSettings.h"
Expand Down Expand Up @@ -220,6 +223,36 @@ void GameWorldView::Draw(const RoadBuildState& rb, const MapPoint selected, bool
if(show_names || show_productivity)
DrawNameProductivityOverlay(terrainRenderer);

// Draw radius preview outline (if set via action window hover)
if(radiusPreview_)
DrawRadiusOutline(radiusPreview_->first, radiusPreview_->second);

// Draw radius outline for the building under the mouse cursor
if(!radiusPreview_ && mousePos.x >= 0 && mousePos.x < static_cast<int>(size_.x) && mousePos.y >= 0
Comment thread
Flamefire marked this conversation as resolved.
Outdated
&& mousePos.y < static_cast<int>(size_.y))
{
std::optional<BuildingType> bldType;
const Visibility vis = gwv.GetVisibility(selPt);
if(vis == Visibility::Visible)
{
const auto* bld = GetWorld().GetSpecObj<noBaseBuilding>(selPt);
if(bld)
bldType = bld->GetBuildingType();
} else if(vis == Visibility::FogOfWar)
{
const FOWObject* fow = gwv.GetYoungestFOWObject(selPt);
if(fow && fow->GetType() == FoW_Type::Building)
bldType = static_cast<const fowBuilding&>(*fow).GetBuildingType();
}

if(bldType)
{
const unsigned bldRadius = GetBuildingRadius(*bldType);
if(bldRadius > 0)
DrawRadiusOutline(selPt, bldRadius);
}
}

DrawGUI(rb, terrainRenderer, selected, drawMouse);

// Draw catapult stones
Expand Down Expand Up @@ -713,6 +746,41 @@ void GameWorldView::RemoveDrawNodeCallback(IDrawNodeCallback* callbackToRemove)
drawNodeCallbacks.erase(itPos);
}

void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius)
{
const auto& world = GetWorld();
// Get all border points at the exact radius
auto pts = world.GetPointsInRadius(center, radius, ReturnMapPointWithRadius{});

const MapExtent mapSize = world.GetSize();
constexpr unsigned BORDER_COLOR = 0xFFFF0000; // Red with full alpha

const int w = mapSize.x;
const int h = mapSize.y;

for(const auto& ptWithRadius : pts)
{
if(ptWithRadius.second != radius)
continue;

const MapPoint& basePt = ptWithRadius.first;

// Draw at all 9 toroidal copies (canonical ± 1 map dimension).
// Using all copies guarantees the ring is continuous across the seam
// regardless of viewport position — the renderer clips off-screen pixels.
for(int dw : {-w, 0, w})
{
for(int dh : {-h, 0, h})
{
const MapPoint copyPt = MakeMapPoint(Position(basePt.x + dw, basePt.y + dh), mapSize);
const auto alt = world.GetNode(copyPt).altitude;
const DrawPoint scr = GetNodePos(copyPt) - DrawPoint(0, HEIGHT_FACTOR * alt) - offset;
Window::DrawRectangle(Rect(scr - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR);
}
}

@morganchristiansson morganchristiansson Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noone commented on this yet.
The drawn radius was being clipped where map repeats and this is a workaround. Did not find any cleaner way advice appreciated.

Image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is indeed hard. You can use the GetNodePos overload taking the height as the base point. Then use const Position mapDrawSize = world->GetSize() * Position(TR_W, TR_H); as the offsets

I don't think it gets better than that: You have to draw possibly all copies when the zoom factor is large which it seems is also not yet taken into account. Possibly even that is not enough.
Maybe rather use a (flat_)set to store the radius points and move drawing to DrawGUI where you can check with contains whether to draw the mark

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that s25edit draws a brush under the cursor which is resizeable by numpad +/- and based the radius outline drawing on that!

}
}

void GameWorldView::CalcFxLx()
{
// Calc first and last point in map units (with 1 extra for incomplete triangles)
Expand Down
10 changes: 10 additions & 0 deletions libs/s25main/world/GameWorldView.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "DrawPoint.h"
#include "gameTypes/MapCoordinates.h"
#include "gameTypes/MapTypes.h"
#include <optional>
#include <boost/signals2.hpp>
#include <vector>

Expand Down Expand Up @@ -46,6 +47,9 @@ class GameWorldView
/// Show productivities
bool show_productivity;

/// Optional radius preview (center position, radius) drawn as outline on the map
std::optional<std::pair<MapPoint, unsigned>> radiusPreview_;

/// Offset from world origin in screen units (not map units): "scroll position"
DrawPoint offset;
/// Last scroll position (before jump)
Expand Down Expand Up @@ -117,6 +121,9 @@ class GameWorldView
void AddDrawNodeCallback(IDrawNodeCallback* newCallback);
void RemoveDrawNodeCallback(IDrawNodeCallback* callbackToRemove);

/// Set/show a radius preview outline on the map (or clear with std::nullopt)
void SetRadiusPreview(const std::optional<std::pair<MapPoint, unsigned>>& preview) { radiusPreview_ = preview; }

/// Gibt selektierten Punkt zurück
MapPoint GetSelectedPt() const { return selPt; }

Expand Down Expand Up @@ -146,6 +153,9 @@ class GameWorldView
void DrawGUI(const RoadBuildState& rb, const TerrainRenderer& terrainRenderer, const MapPoint& selectedPt,
bool drawMouse);

/// Draw a radius outline ring around a center point with the given radius
void DrawRadiusOutline(const MapPoint& center, unsigned radius);

void SaveIngameSettingsValues() const;
void updateEffectiveZoomFactor();
};