Divergent/mods/Hideout Furniture/gamedata/scripts/arm.script

462 lines
13 KiB
Plaintext
Raw Normal View History

2024-03-17 20:18:03 -04:00
---------------------
-- Helper functions
---------------------
function move_and_center_element(element, x, y)
local pos = vector2()
pos.x = x - element:GetWidth()/2
pos.y = y - element:GetHeight()/2
element:SetWndPos(pos)
end
---Scales UI elements to correct ratio after UI scaling
---Written by RavenAscendant, Yoinked and Commented by Aoldri
---@param ele CUIStatic
---@param adjust_x? boolean
---@param anchor? boolean
---@param anchor_point? `left`|`right`|`center`
---@param parent_width? number
function scale_ui(ele, adjust_x, anchor, anchor_point, parent_width)
p_width = parent_width or 1024
p_center = p_width/2
width = ele:GetWidth()
pos = ele:GetWndPos()
anchorpos = {}
anchorpos.left = pos.x
anchorpos.right = anchorpos.left + width
anchorpos.center = anchorpos.left + width/2
ratio = (device().height / device().width) / (768 / 1024)
xadjust = anchorpos.left
if adjust_x then
if anchor_point == "right" then
xadjust = p_width - (p_width - (anchor and anchorpos[anchor] or anchorpos.left))*ratio
elseif anchor_point == "center" then
xadjust = p_center - (p_center - (anchor and anchorpos[anchor] or anchorpos.left))*ratio
else
xadjust = ratio * (anchor and anchorpos[anchor] or anchorpos.left)
end
end
ele:SetWndSize(vector2():set(ele:GetWidth() * ratio, ele:GetHeight()))
ele:SetWndPos( vector2():set(xadjust , pos.y ) )
end
-----------
-- States
-----------
----Enum for option states
---@enum States
States = {
ENABLED = 1, -- Option is visible and accessible
DISABLED = 2, -- Option is visible, but not accessible
HIGHLIGHTED = 3, -- Option is 'selected' or 'active'
TOUCHED = 4, -- Mouse is hovering over option
}
-- Useful functions
State2Suffix = {
[States.ENABLED] = "_e",
[States.DISABLED] = "_d",
[States.HIGHLIGHTED] = "_h",
[States.TOUCHED] = "_t",
}
---Appends a texture name with a suffix corresponding to the state, like textures for CUI3tButton
---@param texture string
---@param state States
---@param fallback? string
---@return string
function get_stateful_texture(texture, state, fallback)
local suffix = State2Suffix[state]
if suffix then return texture .. suffix end
return fallback
end
State2Colour = {
[States.ENABLED] = GetARGB(70, 255, 255, 255),
[States.DISABLED] = GetARGB(70, 255, 255, 255),
[States.HIGHLIGHTED] = GetARGB(255, 255, 255, 255),
[States.TOUCHED] = GetARGB(255, 255, 255, 255),
}
---Gets a colour dependent on option state
---@param state States
---@return number
function get_stateful_colour(state)
return State2Colour[state]
end
------------------------------------------------------------------
-- UI
-------------------------------------------------------------------
class "OptionData"
---@param callback_id string
---@param texture string|fun(States): string
function OptionData:__init(callback_id, texture)
self.callback_id = callback_id
self:SetTexture(texture)
self.state = States.ENABLED
end
-- Texture
---@param texture string|fun(States): string
function OptionData:SetTexture(texture)
self.texture = texture
end
---@return string
function OptionData:GetTexture()
return self:GetAttribute("texture")
end
-- Text
---@param text {title: string, description: string}
function OptionData:SetText(text)
if not text.description then text.description = "" end
self.text = text
end
---@return {title: string, description: string}
function OptionData:GetText()
return self:GetAttribute("text") or {title="", description=""}
end
-- Colour
---@param colour number|fun(States): number
function OptionData:SetColour(colour)
self.colour = colour
end
---@return number
function OptionData:GetColour()
return self:GetAttribute("colour") or GetARGB(255, 255, 255, 255)
end
-- State
---@param state States
function OptionData:SetState(state)
self.state = state
end
---@return States
function OptionData:GetState()
return self.state
end
---@param state States
---@return boolean
function OptionData:IsState(state)
return self.state == state
end
---@param attribute string
---@return any
function OptionData:GetAttribute(attribute)
local value = nil
if type(self[attribute]) == "function" then
value = self[attribute](self.state)
else
value = self[attribute]
end
return value
end
---
class "UIRadialMenu" (CUIScriptWnd)
function UIRadialMenu:__init() super()
self:SetWndRect(Frect():set(0,0,1024,768))
self:AllowMovement(true)
self:SetAutoDelete(true)
self.xml = CScriptXmlInit()
self.xml:ParseFile("ui_radial_menu.xml")
self.callbacks = {}
---@type table<number, arm.OptionData>
self.option_data = {} -- Holds info about each option
---@type table<number, {icon: CUIStatic, hover_bg: CUIStatic, selected_bg: CUIStatic, angle: number}>
self.options = {} -- Holds UI elements for each option
-- Calculate center of screen
self.center = vector2():set(self:GetWidth()/2, self:GetHeight()/2)
-- Background
self.bg = self.xml:InitStatic("bg", self)
move_and_center_element(self.bg, self.center.x, self.center.y)
scale_ui(self.bg, true, true, "center")
-- Cursor
self.cursor = self.xml:InitStatic("cursor", self)
self.cursor:EnableHeading(true)
move_and_center_element(self.cursor, self.center.x, self.center.y-self.cursor:GetHeight()/2)
-- not sure why i don't have to re-scale this element
-- scale_ui(self.cursor, true, true, "center")
-- Central label
self.title = self.xml:InitTextWnd("title", self)
move_and_center_element(self.title, self.title:GetWndPos().x, self.title:GetWndPos().y)
-- Central description
self.description = self.xml:InitTextWnd("description", self)
move_and_center_element(self.description, self.description:GetWndPos().x, self.description:GetWndPos().y)
self.hovered_option_i = 1
self:DrawOptions()
end
function UIRadialMenu:__finalize()
end
---- Adds an option to the menu with specific callback id, texture, colour, and initial state.
---- Both `get_texture()` and `get_colour()` are functions with the option state passed as a parameter
---- Options are shown in order of when they are added, starting from the top and rotating clockwise
---@param option_data arm.OptionData
---@return boolean success
function UIRadialMenu:AddOption(option_data)
if not option_data then return false end
if not option_data:GetTexture() then return false end
table.insert(self.option_data, option_data)
return true
end
---Gets an option
---@param callback_id string
---@return arm.OptionData
function UIRadialMenu:GetOption(callback_id)
-- Look through each option and check for callback_id
for i, opt in ipairs(self.option_data) do
if opt.callback_id == callback_id then
return opt
end
end
-- returns nil if invalid callback_id
end
function UIRadialMenu:DrawOptions()
-- Remove old options in case new options were added or options were removed
self:ResetUI()
-- Create buttons
for i, btn_datum in ipairs(self.option_data) do
self.options[i] = {}
-- Create BGs
local hover_bg = self.xml:InitStatic("hover_bg", self)
local selected_bg = self.xml:InitStatic("selected_bg", self)
-- Create Icon
local icon = self.xml:InitStatic("icon", self)
icon:SetStretchTexture(true)
-- Calculate position (relative to centre)
local x = icon:GetWndPos().x
local y = icon:GetWndPos().y
local angle = (2*math.pi * ((i-1)/#self.option_data))
angle = angle --+ 2*math.pi*(1/12)
local new_x = x*math.cos(angle) - y*math.sin(angle)
local new_y = y*math.cos(angle) + x*math.sin(angle)
-- Offset position so that it centers around the middle
new_x = new_x + self.center.x
new_y = new_y + self.center.y
-- printf("new_x: %s, new_y: %s", new_x, new_y)
-- Move to point and center on it
move_and_center_element(icon, new_x, new_y)
scale_ui(icon, true, true, "center")
move_and_center_element(hover_bg, new_x, new_y)
scale_ui(hover_bg, true, true, "center")
hover_bg:Show(false)
move_and_center_element(selected_bg, new_x, new_y)
scale_ui(selected_bg, true, true, "center")
selected_bg:Show(false)
self.options[i].icon = icon
self.options[i].hover_bg = hover_bg
self.options[i].selected_bg = selected_bg
self.options[i].angle = angle
-- Update Icon
self:UpdateIcon(i)
end
end
function UIRadialMenu:OnButtonClick(i)
if self.option_data[i]:IsState(States.DISABLED) then return end
---@alias callback_flags {close_gui: boolean}
local flags = {
close_gui = true,
}
self:SendCallback(self.option_data[i].callback_id, flags)
if flags.close_gui then
self:Close()
end
end
-- TODO: Rewrite logic to look better maybe
function UIRadialMenu:OnButtonHover(i)
if self.hovered_option_i == i then return end
-- New option
self.options[i].hover_bg:Show(true)
self.title:SetText(self.option_data[i]:GetText().title)
self.title:AdjustHeightToText()
self.description:SetText(self.option_data[i]:GetText().description)
self.description:SetWndPos(vector2():set(self.description:GetWndPos().x, self.title:GetWndPos().y + self.title:GetHeight() + 8))
if self.option_data[i]:IsState(States.ENABLED) then
self.option_data[i]:SetState(States.TOUCHED)
end
-- Previous option
self.options[self.hovered_option_i].hover_bg:Show(false)
if self.option_data[self.hovered_option_i]:IsState(States.TOUCHED) then
self.option_data[self.hovered_option_i]:SetState(States.ENABLED)
end
self.hovered_option_i = i
end
function UIRadialMenu:SendCallback(callback_id, ...)
local funcs = self.callbacks[callback_id]
if not funcs then return end
for i, func in ipairs(funcs) do
func(...)
end
end
---Register a function to be called when an option with the corresponding callback_id is selected.
---Each function is executed in order of when they were registered with this function.
---
---This function should take in one parameter: `flags`
---
--- - flags.close_gui will close the menu if `true` (default: `true`)
---@param callback_id string
---@param callback_function fun(flags: callback_flags)
function UIRadialMenu:RegisterCallback(callback_id, callback_function)
self.callbacks[callback_id] = self.callbacks[callback_id] or {}
table.insert(self.callbacks[callback_id], callback_function)
end
function UIRadialMenu:ResetUI()
-- Remove pre-existing options
if self.options ~= nil then
for i, opt in ipairs(self.options) do
self:DetachChild(opt.icon)
self:DetachChild(opt.hover_bg)
self:DetachChild(opt.selected_bg)
end
end
self.options = {}
end
function UIRadialMenu:Reset()
self:ResetUI()
self.option_data = {}
self.callbacks = {}
end
function UIRadialMenu:UpdateIcon(i)
local icon = self.options[i].icon
local data = self.option_data[i]
-- Set Texture
icon:InitTexture(data:GetTexture())
-- Set Colour
icon:SetTextureColor(data:GetColour())
if data:IsState(States.HIGHLIGHTED) then
self.options[i].selected_bg:Show(true)
else
self.options[i].selected_bg:Show(false)
end
end
function UIRadialMenu:Update()
CUIScriptWnd.Update(self)
-- Update Cursor pos and rot
local cur_pos = vector2():set(0, -self.cursor:GetHeight()/2)
local mouse_pos = GetCursorPosition()
local x_scale = (device().height / device().width) / (768 / 1024)
-- Get angle from center of screen to mouse cursor
-- Account for scaling from 1024x768 so that the cursor rotates towards cursor properly
local angle = math.atan2(-(mouse_pos.x-self.center.x)/x_scale, -(mouse_pos.y-self.center.y))
angle = (angle < 0) and 2*math.pi+angle or angle
angle = 2*math.pi - angle
local new_x = cur_pos.x*math.cos(angle) - cur_pos.y*math.sin(angle)
local new_y = cur_pos.y*math.cos(angle) + cur_pos.x*math.sin(angle)
-- Scale Correction
new_x = new_x*x_scale
-- Move to orbit around center of screen
new_x = new_x + self.center.x
new_y = new_y + self.center.y
move_and_center_element(self.cursor, new_x, new_y)
self.cursor:SetHeading(2*math.pi - angle)
-- Get option from angle
local best_i = nil
local best_similarity = nil
for i, option in ipairs(self.options) do
-- Ignore disabled options
-- if not self.option_data[i]:IsState(States.DISABLED) then
local similarity = math.pi - math.abs(math.fmod(math.abs(angle - option.angle), 2*math.pi) - math.pi)
if best_similarity == nil or similarity < best_similarity then
best_similarity = similarity
best_i = i
end
-- end
end
if best_i then self:OnButtonHover(best_i) end
-- Update option textures
for i, option in ipairs(self.options) do
self:UpdateIcon(i)
end
end
function UIRadialMenu:OnAccept()
end
function UIRadialMenu:OnKeyboard(dik, keyboard_action)
local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action)
if (res == false) then
if keyboard_action == ui_events.WINDOW_KEY_PRESSED then
if dik == DIK_keys.DIK_ESCAPE then
self:Close()
end
if dik == DIK_keys.MOUSE_1 then
self:OnButtonClick(self.hovered_option_i)
end
end
end
return res
end
function UIRadialMenu:Close()
self:HideDialog()
Unregister_UI("UIRadialMenu")
end