Download CommonLua/UI/FloatingText.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 14.8 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/UI/FloatingText.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/UI/FloatingText.lua
-
curl -L -o FloatingText.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/UI/FloatingText.lua
14.8 kB
| 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 |