Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create capture cost reticle #5948

Merged
merged 18 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all 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 changelog/snippets/features.5948.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- (#5948) When in capture mode, display energy cost and time to capture hovered-over units.
42 changes: 42 additions & 0 deletions lua/shared/captureCost.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
--******************************************************************************************************
--** Copyright (c) 2024 lL1l1
--**
--** Permission is hereby granted, free of charge, to any person obtaining a copy
--** of this software and associated documentation files (the "Software"), to deal
--** in the Software without restriction, including without limitation the rights
--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--** copies of the Software, and to permit persons to whom the Software is
--** furnished to do so, subject to the following conditions:
--**
--** The above copyright notice and this permission notice shall be included in all
--** copies or substantial portions of the Software.
--**
--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
--** SOFTWARE.
--******************************************************************************************************

-- All functions in this file (inside /lua/shared) should be:
-- - pure: they should only use the arguments provided, do not touch any global state.
-- - sim / ui proof: they should work for both sim code and ui code.

--- Formula to compute the energy and time cost of capturing
---@param blueprint UnitBlueprint
---@param number buildRate
---@return number time
---@return number energy
GetBlueprintCaptureCost = function(blueprint, buildRate)
local blueprintEconomy = blueprint.Economy

local time = ((blueprintEconomy.BuildTime or 10) / buildRate) / 2
local energy = blueprintEconomy.BuildCostEnergy or 100
if time < 0 then
time = 0.1
end

return time, energy
end
3 changes: 2 additions & 1 deletion lua/shared/observable.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ local ObservableMeta = {}
ObservableMeta.__index = ObservableMeta

--- Adds an observer that is updated when the value is subject is set.
-- @param callback A function that receives the value as its first argument.
---@param callback function A function that receives the value as its first argument.
---@param name? string Optional name to be able to reference the callback later on
function ObservableMeta:AddObserver(callback, name)
if name then
self.Listeners[name] = callback
Expand Down
8 changes: 5 additions & 3 deletions lua/sim/Unit.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ local Weapon = import("/lua/sim/weapon.lua").Weapon
local IntelComponent = import('/lua/defaultcomponents.lua').IntelComponent
local VeterancyComponent = import('/lua/defaultcomponents.lua').VeterancyComponent

local GetBlueprintCaptureCost = import('/lua/shared/captureCost.lua').GetBlueprintCaptureCost

local TrashBag = TrashBag
local TrashAdd = TrashBag.Add
local TrashDestroy = TrashBag.Destroy
Expand Down Expand Up @@ -3969,7 +3971,7 @@ Unit = ClassUnit(moho.unit_methods, IntelComponent, VeterancyComponent) {
return 0, 0, 0
end,

--- Multiplies the time it takes to capture a unit, defaults to 1.0. Often
--- Multiplies the time the unit takes to capture others, defaults to 1.0. Often
--- used by campaign events.
---@param self Unit
---@param captureTimeMultiplier number
Expand Down Expand Up @@ -3998,8 +4000,8 @@ Unit = ClassUnit(moho.unit_methods, IntelComponent, VeterancyComponent) {

-- compute capture costs
local targetBlueprintEconomy = target.Blueprint.Economy
local time = ((targetBlueprintEconomy.BuildTime or 10) / self:GetBuildRate()) / 2
local energy = targetBlueprintEconomy.BuildCostEnergy or 100
local time, energy = GetBlueprintCaptureCost(target.Blueprint, self:GetBuildRate())

time = time * (self.CaptureTimeMultiplier or 1)
if time < 0 then
time = 1
Expand Down
145 changes: 145 additions & 0 deletions lua/ui/controls/reticles/capture.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
--******************************************************************************************************
--** Copyright (c) 2024 lL1l1
--**
--** Permission is hereby granted, free of charge, to any person obtaining a copy
--** of this software and associated documentation files (the "Software"), to deal
--** in the Software without restriction, including without limitation the rights
--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
--** copies of the Software, and to permit persons to whom the Software is
--** furnished to do so, subject to the following conditions:
--**
--** The above copyright notice and this permission notice shall be included in all
--** copies or substantial portions of the Software.
--**
--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
--** SOFTWARE.
--******************************************************************************************************

local Bitmap = import("/lua/maui/bitmap.lua").Bitmap

local UIUtil = import("/lua/ui/uiutil.lua")
local LayoutHelpers = import("/lua/maui/layouthelpers.lua")
local Layouter = LayoutHelpers.ReusedLayoutFor

local Reticle = import('/lua/ui/controls/reticle.lua').Reticle

local GetBlueprintCaptureCost = import('/lua/shared/captureCost.lua').GetBlueprintCaptureCost

local ObserveSelection = import("/lua/ui/game/gamemain.lua").ObserveSelection

-- Local upvalues for performance
local GetRolloverInfo = GetRolloverInfo
local GetSelectedUnits = GetSelectedUnits
local MathFloor = math.floor
local EntityCategoryFilterDown = EntityCategoryFilterDown
local tableEmpty = table.empty

---@param units UserUnit[]
---@return number totalBuildRate
local getBuildRateOfCapturerUnits = function(units)
local capturerUnits = EntityCategoryFilterDown(categories.CAPTURE, units)
if not tableEmpty(capturerUnits) then
local totalBuildRate = 0
for _, unit in capturerUnits do
totalBuildRate = totalBuildRate + unit:GetBuildRate()
end
return totalBuildRate
end
return 0
end

---@type number
local selectionBuildRate
---@param cachedSelection { oldSelection: UserUnit[], newSelection: UserUnit[], added: UserUnit[], removed: UserUnit[] }
local OnSelectionChanged = function(cachedSelection)
selectionBuildRate = selectionBuildRate + getBuildRateOfCapturerUnits(cachedSelection.added) - getBuildRateOfCapturerUnits(cachedSelection.removed)
end


--- Reticle that displays the capture cost and rate of hovered-over units based on the current selection
---@class CaptureReticle : UIReticle
---@field BuildTimeIcon Bitmap
---@field EnergyCostIcon Bitmap
---@field eText Text
---@field tText Text
---@field focusArmy Army
CaptureReticle = ClassUI(Reticle) {

---@param self CaptureReticle
---@param parent Control
---@param data any
__init = function(self, parent, data)
Reticle.__init(self, parent, data)

self.focusArmy = GetFocusArmy()

selectionBuildRate = getBuildRateOfCapturerUnits(GetSelectedUnits())
ObserveSelection:AddObserver(OnSelectionChanged, "CaptureReticleSelectionObserver")
end,

---@param self CaptureReticle
OnDestroy = function(self)
ObserveSelection:AddObserver(nil, "CaptureReticleSelectionObserver")
end,

---@param self CaptureReticle
SetLayout = function(self)
self.EnergyCostIcon = Layouter(Bitmap(self)):Texture(UIUtil.UIFile('/game/unit_view_icons/energy.dds')):Width(19):Height(19):RightOf(self, 4):End()
self.BuildTimeIcon = Layouter(Bitmap(self)):Texture(UIUtil.UIFile('/game/unit_view_icons/time.dds')):Width(19):Height(19):Below(self.EnergyCostIcon, 4):End()

-- eText color is from economy_mini.lua, same color as the energy stored/storage text
self.eText = Layouter(UIUtil.CreateText(self, "eCost", 16, UIUtil.bodyFont, true)):RightOf(self.EnergyCostIcon, 4):Color('fff7c70f'):End()
self.tText = Layouter(UIUtil.CreateText(self, "tCost", 16, UIUtil.bodyFont, true)):RightOf(self.BuildTimeIcon, 4):End()
end,

---@param self CaptureReticle
UpdateDisplay = function(self)
local rolloverInfo = GetRolloverInfo()
local isNotAlly, targetBp
local isCapturable = true
if rolloverInfo then
isNotAlly = not IsAlly(self.focusArmy, rolloverInfo.armyIndex + 1)
targetBp = __blueprints[rolloverInfo.blueprintId]
-- `Unit:IsCapturable()` is sim-side so we will use what we have in the bp.
-- May not work with units from mods or changed by script.
if targetBp.Display.Abilities then
for _, ability in targetBp.Display.Abilities do
if ability == "<LOC ability_notcap>Not Capturable" then
isCapturable = false
break
end
end
end
end
if isNotAlly and isCapturable then
if self:IsHidden() then
self:SetHidden(false)
end

local time, energy = GetBlueprintCaptureCost(targetBp, selectionBuildRate)

local rate
if time == 0 then
-- pretend to capture in 1 tick
rate = energy * 10
else
rate = energy/time
end

self.eText:SetText(string.format('%.0f (%.0f)', energy, -rate))
local minutes = MathFloor(time/60)
local seconds = time - 60 * minutes
self.tText:SetText(string.format('%02.0f:%02.0f', minutes, seconds))
else
if not self:IsHidden() then
self:Hide()
end
end
end,

}
10 changes: 5 additions & 5 deletions lua/ui/controls/reticles/teleport.lua
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ local Reticle = import('/lua/ui/controls/reticle.lua').Reticle
local GetSelectedUnits = GetSelectedUnits

--- Reticle for teleport cost info
---@class UITeleportReticle : UIReticle
---@field ePrefix Text
---@field tPrefix Text
---@class TeleportReticle : UIReticle
---@field BuildTimeIcon Bitmap
---@field EnergyCostIcon Bitmap
---@field eText Text
---@field tText Text
TeleportReticle = ClassUI(Reticle) {

---@param self UITeleportReticle
---@param self TeleportReticle
SetLayout = function(self)
self.BuildTimeIcon = Bitmap(self)
self.BuildTimeIcon:SetTexture(UIUtil.UIFile('/game/unit_view_icons/time.dds'))
Expand All @@ -57,7 +57,7 @@ TeleportReticle = ClassUI(Reticle) {
self.eText:SetColor('fff7c70f') -- from economy_mini.lua, same color as the energy stored/storage text
end,

---@param self UITeleportReticle
---@param self TeleportReticle
---@param mouseWorldPos Vector
UpdateDisplay = function(self, mouseWorldPos)
if self.onMap then
Expand Down
2 changes: 2 additions & 0 deletions lua/ui/controls/worldview.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ local OverchargeCanKill = import("/lua/ui/game/unitview.lua").OverchargeCanKill
local CommandMode = import("/lua/ui/game/commandmode.lua")

local TeleportReticle = import("/lua/ui/controls/reticles/teleport.lua").TeleportReticle
local CaptureReticle = import("/lua/ui/controls/reticles/capture.lua").CaptureReticle

WorldViewParams = {
ui_SelectTolerance = 7.0,
Expand Down Expand Up @@ -799,6 +800,7 @@ WorldView = ClassUI(moho.UIWorldView, Control) {
local cursor = self.Cursor
cursor[1], cursor[2], cursor[3], cursor[4], cursor[5] = UIUtil.GetCursor(identifier)
self:ApplyCursor()
CaptureReticle(self)
end
end
end,
Expand Down
12 changes: 6 additions & 6 deletions lua/ui/game/gamemain.lua
Original file line number Diff line number Diff line change
Expand Up @@ -586,14 +586,14 @@ local cachedSelection = {
--- Observable to allow mods to do something with a new selection
ObserveSelection = import("/lua/shared/observable.lua").Create()

-- This function is called whenever the set of currently selected units changes
-- See /lua/unit.lua for more information on the lua unit object
-- @param oldSelection: What the selection was before
-- @param newSelection: What the selection is now
-- @param added: Which units were added to the old selection
-- @param removed: Which units where removed from the old selection
local hotkeyLabelsOnSelectionChanged = false
local upgradeTab = false

---This function is called whenever the set of currently selected units changes
---@param oldSelection UserUnit[] What the selection was before
---@param newSelection UserUnit[] What the selection is now
---@param added UserUnit[] Which units were added to the old selection
---@param removed UserUnit[] Which units where removed from the old selection
function OnSelectionChanged(oldSelection, newSelection, added, removed)

if ignoreSelection then
Expand Down