File size: 14,774 Bytes
b6a38d7 |
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 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 |
DefineClass.XFloatingText = {
__parents = { "XText" },
properties = {
{ category = "Floating Text", id = "expire_time", name = "Expire time", editor = "number", default = 800 },
{ category = "Floating Text", id = "life_time", name = "Life time", editor = "number", default = 0, scale = "ms",
help = "The life time of a floating text. If it is not shown in that period of time, it will never be shown. Set this to 0 for an endless life time.", },
{ category = "Floating Text", id = "fade_start", name = "Fade start time", editor = "number", default = 400 },
{ category = "Floating Text", id = "transparency_start", name = "Transparency start", editor = "number", default = 255, min = 0, max = 255 },
{ category = "Floating Text", id = "transparency_end", name = "Transparency end", editor = "number", default = 0, min = 0, max = 255 },
{ category = "Floating Text", id = "offset_start_time", name = "Offset start time", editor = "number", default = 0 },
{ category = "Floating Text", id = "offset_amount", name = "Offset amount", editor = "number", default = 100 },
{ category = "Floating Text", id = "z_offset", name = "Z offset", editor = "number", default = 30, scale = "m" },
{ category = "Floating Text", id = "randomize_x", name = "Randomize X", editor = "bool", default = true },
{ category = "Floating Text", id = "exclusive", name = "Exclusive", editor = "bool", default = false,
help = "Only this floating text can be active on the target. Removes all previously spawned." },
{ category = "Floating Text", id = "same_exclusive", name = "Same Exclusive", editor = "choice", items = {"False", "Same Time", "All"}, default = "False",
help = "The same floating texts cannot coexist. Removes the new one. Same Time - removes same floating texts if added at the same time, All - removes same floating texts." },
{ category = "Floating Text", id = "exclusive_discard", name = "Exclusive Discard", editor = "bool", default = false,
help = "Like exclusive, but in reverse - prevents other texts from spawning, instead destroying them." },
{ category = "Floating Text", id = "exclusive_by_type", name = "Exclusive By Type", editor = "bool", default = false,
help = "Like exclusive, but only removes previously spawned texts of the same type." },
{ category = "Floating Text", id = "interpolate_opacity", name = "Interpolate opacity", editor = "bool", default = false },
{ category = "Floating Text", id = "interpolate_pos", name = "Interpolate position", editor = "bool", default = true },
{ category = "Floating Text", id = "always_show_on_distance", name = "doe snot hide on distance", editor = "bool", default = false },
},
TextStyle = "EditorText",
target = false,
HandleMouse = false,
ChildrenHandleMouse = false,
UseClipBox = false,
Clip = false,
Translate = true,
default_spot = false,
prevent_overlap = true,
stagger_spawn = true, -- Prevent texts from overlapping by staggering texts on the same target.
spawn_stagger = false,
dbg_removed_by = false
}
function XFloatingText:OnDelete()
Msg(self)
end
MapVar("FloatingTexts", {}, weak_keys_meta)
PersistableGlobals.FloatingTexts = false
local function lFindTextPos(target, z_offset)
local prev = FloatingTexts and FloatingTexts[target]
prev = prev and prev[1]
local tx, ty, tz = target:GetVisualPosXYZ()
local z = tz + z_offset*guic
return tx, ty, z
end
local function lIntersectionDepth(box1, box2)
local halfWidthA = box1:sizex() / 2
local halfHeightA = box1:sizey() / 2
local halfWidthB = box2:sizex() / 2
local halfHeightB = box2:sizey() / 2
local centerA = box1:Center()
local centerB = box2:Center()
-- calculate current and minimum-non-intersecting distances between centers
local distanceX = centerA:x() - centerB:x()
local distanceY = centerA:y() - centerB:y()
local minDistanceX = halfWidthA + halfWidthB
local minDistanceY = halfHeightA + halfHeightB
-- if we are not intersecting at all, return (0, 0)
if abs(distanceX) >= minDistanceX or abs(distanceY) >= minDistanceY then return 0, 0 end
local depthX
if distanceX == 0 then
depthX = 0
elseif distanceX > 0 then
depthX = minDistanceX - distanceX
else
depthX = -minDistanceX - distanceX
end
local depthY
if distanceY == 0 then
depthY = 0
elseif distanceY > 0 then
depthY = minDistanceY - distanceY
else
depthY = -minDistanceY - distanceY
end
return depthX, depthY
end
local ShowFloatingTextDist = const.Camera and const.Camera.ShowFloatingTextDist or 500 * guim
local IsPoint = IsPoint
local IsValid = IsValid
local GameToScreenXY = GameToScreenXY
local IsCloser2D = IsCloser2D
function ShouldShowFloatingText(target, text, always_show_on_distance)
if not text or text == "" or not target or not config.FloatingTextEnabled then return end
-- invalid pos
if not IsValidPos(target) then
return
end
-- do not show at game start
if GameInitAfterLoading or GameTime() == 0 then return end
if always_show_on_distance then
return true
end
-- show only texts visible on-screen
local front, sx, sy = GameToScreenXY(target)
if not front or terminal.desktop.box:Dist2D2(sx, sy) > 200*200 then
return
end
-- show only texts close to the camera
local cam_pos = camera.GetPos()
return IsValidPos(cam_pos) and IsCloser2D(cam_pos, target, ShowFloatingTextDist)
end
function CreateCustomFloatingText(ftext, target, text, style, spot, stagger_spawn, params, game_time)
if not ShouldShowFloatingText(target, text, ftext and ftext.always_show_on_distance) then
if ftext then
ftext:delete()
end
return
end
if not IsT(text) then
text = Untranslated(text)
end
local timeNow = game_time and GameTime() or GetPreciseTicks()
local target_key = IsPoint(target) and xxhash(target) or target
local list = FloatingTexts and FloatingTexts[target_key]
local prev = list and list[#list]
if prev and prev.window_state ~= "destroying" then
if prev.exclusive_discard then
if ftext then
ftext.dbg_removed_by = "exclusive discard ftext on target"
ftext:delete()
end
return
end
local ttext = _InternalTranslate(text)
for _, previ in ipairs(list) do
if (previ.same_exclusive == "Same Time" and previ.timeNow == timeNow or previ.same_exclusive == "All") and previ.text == ttext then
if ftext then
ftext.dbg_removed_by = "exclusive same ftext on target"
ftext:delete()
end
return
end
end
end
-- don't show the same texts on nearby targets
if config.RemoveSameFTextNearby then
local ttext = _InternalTranslate(text)
local _, sx, sy = GameToScreenXY(target)
for ftarget, ftext_list in pairs(FloatingTexts) do
if target.show_same_floating_texts_nearby or target == ftarget or not IsValid(ftarget) then goto skip end
local _, t_sx, t_sy = GameToScreenXY(ftarget)
if IsCloser2D(sx, sy , t_sx, t_sy, 30) then
for _, prev_ftext in ipairs(ftext_list) do
if prev_ftext.text == ttext then
if ftext then
ftext.dbg_removed_by = "exclusive same ftext on targets nearby"
ftext:delete()
end
return
end
end
end
::skip::
end
end
if not ftext then
ftext = XTemplateSpawn(config.FloatingTextClass or "XFloatingText", EnsureDialog("FloatingTextDialog"), false)
table.overwrite(ftext, params or empty_table)
end
if ftext.window_state == "new" then ftext:Open() end
spot = spot or ftext.default_spot
stagger_spawn = stagger_spawn == nil and ftext.stagger_spawn or stagger_spawn
if list then
-- Close any previously open floating texts on this target.
-- We don't want them overlapping in some cases.
if (ftext.exclusive or ftext.exclusive_by_type) and prev then
local byType = ftext.exclusive_by_type
local myType = ftext.class
for i, t in ipairs(list) do
if t.window_state == "open" and (not byType or IsKindOf(t, myType)) then
t.dbg_removed_by = "exclusive ftext, byType:" .. tostring(byType) .. " and of type " .. tostring(myType)
t:Close()
end
end
prev = false
end
list[#list + 1] = ftext
else
list = { ftext }
FloatingTexts[target_key] = list
end
ftext.timeNow = timeNow
ftext.target = target_key
ftext:SetText(text)
if type(style) == "number" then
ftext:SetTextColor(style)
elseif type(style) == "string" then
ftext:SetTextStyle(style)
end
local backupPosition
if IsValid(target) then
local x, y, z = target:GetVisualPosXYZ()
backupPosition = point(x, y, z + ftext.z_offset*guic)
end
CreateMapRealTimeThread(WaitStartFloatingText, ftext, prev, stagger_spawn, spot, target, backupPosition, game_time)
return ftext
end
local function RemoveFloatingTextReason(ftext, reason)
table.remove_entry(FloatingTexts[ftext.target], ftext)
if #(FloatingTexts[ftext.target] or "") == 0 then
FloatingTexts[ftext.target] = nil
end
if ftext.window_state == "open" then
ftext.dbg_removed_by = reason
ftext:Close()
end
end
function WaitStartFloatingText(ftext, prev, stagger_spawn, spot, target, backupPosition, game_time)
-- Center the text above the spot. To do this get the text size...
local width, height = ftext:Measure(ftext.MaxWidth, ftext.MaxHeight)
local minx, miny, maxx, maxy = ftext:GetEffectiveMargins()
local xLoc = -(width / 2) + minx
local yLoc = -height + miny
-- If the height of the window is smaller than the offset up, it will be considered off screen (pos is 0,0) and clipped
height = height + -yLoc
if ftext.OffsetBox then
xLoc, yLoc = ftext:OffsetBox(xLoc, yLoc)
end
if prev and ftext.prevent_overlap and prev.expire_time then
if ftext.randomize_x then
xLoc = xLoc + AsyncRand(-100, 50) -- Make it more interesting
end
if stagger_spawn then
-- Stagger this text a bit to let the previous one pass
local timeNow = game_time and GameTime() or RealTime()
if prev.spawn_stagger and (prev.spawn_stagger - timeNow > 0) then
ftext.spawn_stagger = prev.spawn_stagger
else
ftext.spawn_stagger = timeNow
end
ftext:SetVisible(false)
ftext.spawn_stagger = ftext.spawn_stagger + prev.offset_start_time + prev.expire_time / 3
while prev.window_state ~= "destroying" do
local sleep_time = ftext.spawn_stagger - (game_time and GameTime() or RealTime())
if game_time then
sleep_time = MulDivRound(sleep_time, 1000, Max(GetTimeFactor(), 1000))
end
if sleep_time <= 1 then
break
end
local textDeleted = WaitMsg(prev, sleep_time)
if textDeleted then
break
end
end
if ftext.window_state == "destroying" then return end -- Was deleted while waiting.
-- Check if this ftext's lifetime is over and if so - remove it
if ftext.life_time ~= 0 then
timeNow = game_time and GameTime() or RealTime()
if ftext.timeNow + ftext.life_time - timeNow < 0 then
RemoveFloatingTextReason(ftext, "lifetime")
return
end
end
ftext:SetVisible(true)
end
end
-- Set box directly outside of the layout system
local x1, y1, x2, y2 = ScaleXY(ftext.scale, ftext.Padding:xyxy())
ftext:SetBox(xLoc - x1, yLoc - y1, width + maxx + x1 + x2, height + maxy + y1 + y2, false)
ftext.Dock = "ignore"
local max_cam_dist_m = config.FloatingTextMaxDist_m
local targetIsPoint = IsPoint(target)
if not targetIsPoint and not (IsValid(target) and target:IsValidPos()) then
target = backupPosition
targetIsPoint = true
end
if spot and not targetIsPoint then
ftext:AddDynamicPosModifier{
id = "attached_ui",
target = target,
spot_type = EntitySpots[spot],
max_cam_dist_m = max_cam_dist_m,
}
else
if targetIsPoint then
ftext:AddDynamicPosModifier{
id = "attached_ui",
target = ValidateZ(target),
max_cam_dist_m = max_cam_dist_m,
}
else
-- If a target, but without a specified spot, randomize a position around.
local posx, posy, posz = lFindTextPos(target, ftext.z_offset)
ftext:AddDynamicPosModifier{
id = "attached_ui",
target = point(posx, posy, posz),
max_cam_dist_m = max_cam_dist_m,
}
end
end
if ftext.expire_time then
ftext:CreateThread("floating_text_interp", function()
ftext:StartInterpolation(game_time)
Sleep(ftext.expire_time)
RemoveFloatingTextReason(ftext, "expired")
end)
end
end
--- Create floating text at the specified pos.
-- @param target point || CObject The point or object to place the text above.
-- @param text T || string The text to display.
-- @param style string The text style to use, this will override the one specified in the template.
-- @param spot string Optional, the target's spot to attach to.
function CreateFloatingText(target, text, style, spot, stagger_spawn, params, game_time)
return CreateCustomFloatingText(nil, target, text, style, spot, stagger_spawn, params, game_time)
end
local b = box(0, 0, 1, 1)
function XFloatingText:StartInterpolation(game_time)
if self.interpolate_opacity then
local transInter = {
id = "transparency",
type = const.intAlpha,
startValue = self.transparency_start,
endValue = self.transparency_end,
start = (game_time and GameTime() or GetPreciseTicks()) + self.fade_start,
duration = self.expire_time - self.fade_start,
flags = game_time and const.intfGameTime or nil,
}
self:AddInterpolation(transInter)
end
if self.interpolate_pos then
local _, offset_y = ScaleXY(self.scale, 0, self.offset_amount)
local moveInterp = {
id = "movement",
type = const.intRect,
originalRect = b,
targetRect = box(b:minx(), b:miny() - offset_y, b:maxx(), b:maxy() - offset_y),
start = (game_time and GameTime() or GetPreciseTicks()) + self.offset_start_time,
duration = self.expire_time - self.offset_start_time,
flags = game_time and const.intfGameTime or nil,
}
self:AddInterpolation(moveInterp)
end
end
DefineClass.FloatingTextDialog = {
__parents = { "XDialog" },
UseClipBox = false,
ZOrder = 0,
FocusOnOpen = false,
}
function FloatingTextDialog:Measure(max_width, max_height)
return 0, 0 -- prevent calls to measure all children
end
function FloatingTextDialog:UpdateLayout()
self.layout_update = false -- do nothing, all floating text set their boxes manually
end
function ShowFloatingTextNoExpire(actor, text, style)
return CreateFloatingText(actor, text, style, nil, nil, { expire_time = false })
end
function OnMsg.DoneMap()
for _, texts in pairs(FloatingTexts or empty_table) do
for _, text in ipairs(texts) do
if text.window_state ~= "destroying" then
text:delete()
end
end
end
end
function RemoveFloatingTextsFrom(obj, except)
local list = FloatingTexts[obj]
if not list then return end
for i = #list, 1, -1 do
local text = list[i]
if (not except or not IsKindOf(text, except)) and text.window_state ~= "destroying" then
text:delete()
text.dbg_removed_by = "call to RemoveFloatingTextsFrom"
table.remove(list, i)
end
end
end |