46 TypewriterStartDelay = 2.5, -- Delay in seconds before typewriter effect starts (increased
for more contemplative pacing)
48 TypewriterBaseSpeed = 8, -- Scaling multiplier
for how much the conditions will impact revealing text (increased
for slower base
speed)
62-- Get normalized direction vector
for a given IsoDirection (with caching)
67 return cached.
x, cached.
y
75 dirX,
dirY = 0.7071, -0.7071 -- Northeast: up-right (pre-normalized)
79 dirX,
dirY = 0.7071, 0.7071 -- Southeast: down-right (pre-normalized)
83 dirX,
dirY = -0.7071, 0.7071 -- Southwest: down-left (pre-normalized)
87 dirX,
dirY = -0.7071, -0.7071 -- Northwest: up-left (pre-normalized)
98 -- Fast
angle calculation
using pre-computed lookup
105-- Pre-computed
angle constants
for performance optimization
107 PI_6 = 0.5236, -- math.pi / 6 (30 degrees)
108 PI_4 = 0.7854, -- math.pi / 4 (45 degrees)
109 PI_3 = 1.0472, -- math.pi / 3 (60 degrees)
110 PI_2 = 1.5708, -- math.pi / 2 (90 degrees)
111 PI_1_5 = 2.0944 -- math.pi / 1.5 (120 degrees)
114-- Utility functions
for common operations to reduce code duplication
121 -- Calculate cache
key for screen positions
123 return playerId ..
"_" .. math.floor(
x * 10) ..
"_" .. math.floor(
y * 10) ..
"_" .. z
126 -- Get adjacent square based on direction
128 return cell:getGridSquare(baseX +
dx, baseY +
dy, z)
131 -- Calculate squared distance between two points
138 -- Find best
candidate from candidates array (optimized loop)
142 for
i = 1, #candidates do
154 for
i = 1, #candidates do
156 return true, candidates[
i].score
169 -- Method 1: Check
properties for HasWindow or similar
173 -- Check for common property names that might indicate
a window using Is() method
182-- Centralized function to check if
a wall/door/window combination allows passage
183-- This eliminates massive code duplication across directional checks
185 -- Check for door opening first (prioritize openings)
186 if door and door:IsOpen() then
187 return true -- Open door allows passage
188 elseif door and not door:IsOpen() then
189 -- Closed door - but check if it has
a window we can see through
190 if
self:DoorHasWindow(door) then
191 -- Door has
a built-in window - check if there
's also a separate window object for curtains
193 -- Both door with window AND separate window object - check curtain state
195 local curtain = window:HasCurtains()
196 if curtain and not curtain:IsOpen() then
197 return false -- Door with window but closed curtains blocks
199 return true -- Door with window and open/no curtains allows passage
202 return true -- In permissive mode, door windows don't block regardless of curtains
205 -- Door has window but no separate window
object (probably no curtains)
206 return true -- Door with uncurtained window allows passage
209 -- Closed solid door but separate window - check
curtain state
213 return false -- Separate window with closed curtains blocks
215 return true -- Separate window with open/no curtains allows passage
218 return true -- In permissive mode, windows don
't block regardless of curtains
221 return false -- Closed solid door without window blocks
224 -- Window without door - check for window opening
226 local curtain = window:HasCurtains()
227 if curtain and not curtain:IsOpen() then
228 return false -- Window with closed curtains blocks in restrictive mode
230 return true -- Window with open/no curtains allows passage in restrictive mode
233 return true -- In permissive mode, windows don't block regardless of curtains
236 return false -- Wall always blocks
238 return true -- No obstruction at all
242-- Calculate
angle between
player's facing direction and target position using dot product (optimized)
243function FrameworkZ.Tooltips:CalculatePlayerTargetAngle(player, targetX, targetY)
245 return math.pi -- Return max angle if no player
248local playerDir = player:getDir()
249local playerX = player:getX()
250local playerY = player:getY()
252-- Get normalized direction vector
253local dirX, dirY = self:GetDirectionVector(playerDir)
255 return math.pi -- Unknown direction, return max angle
258-- Vector from player to target
259local toTargetX = targetX - playerX
260local toTargetY = targetY - playerY
261local toTargetLengthSqr = toTargetX * toTargetX + toTargetY * toTargetY
263if toTargetLengthSqr == 0 then
264 return 0 -- Same position, no angle
267-- Normalize target vector using fast inverse square root approximation for better performance
268local toTargetLength = math.sqrt(toTargetLengthSqr)
269toTargetX = toTargetX / toTargetLength
270toTargetY = toTargetY / toTargetLength
272-- Calculate dot product to get cosine of angle between vectors
273local dotProduct = dirX * toTargetX + dirY * toTargetY
275-- Calculate angle from dot product with clamping
276local angle = math.acos(math.max(-1, math.min(1, dotProduct)))
281-- Check if player is facing towards a target within field of view (optimized)
282function FrameworkZ.Tooltips:IsPlayerFacingTarget(player, targetX, targetY, fieldOfViewAngle)
287 -- Calculate angle between player facing direction and target
288 local angle = self:CalculatePlayerTargetAngle(player, targetX, targetY)
290 if angle == math.pi then
291 return false -- Invalid calculation or unknown direction
294 -- Check if within field of view using pre-computed constant
295 return angle <= (fieldOfViewAngle or ANGLE_CONSTANTS.PI_1_5) -- 120° field of view
298-- Check for obstructions in a square based on movement direction (centralized function)
299function FrameworkZ.Tooltips:CheckSquareObstructions(square, stepX, stepY, isTargetSquare, restrictive)
301 return false -- No square means no obstruction
304 local hasOpening = true -- Start by assuming there's an opening, then check if blocked
307 -- Project Zomboid wall system:
308 -- getWall(true) = North walls, getWall(false) = West walls
309 -- South walls are on the square to the south (Y+1), East walls are on the square to the east (X+1)
311 -- Check for north-side obstructions when moving south (
stepY > 0)
313 local northDoor = square:getDoor(true)
314 local northWindow = square:getWindow(true)
315 local northWall = square:getWall(true)
316 hasOpening =
self:CheckWallPassage(northDoor, northWindow, northWall, restrictive)
319 -- Check for west-side obstructions when moving east (
stepX > 0)
320 if
stepX > 0 and hasOpening then
321 local westDoor = square:getDoor(false)
322 local westWindow = square:getWindow(false)
323 local westWall = square:getWall(false)
324 hasOpening =
self:CheckWallPassage(westDoor, westWindow, westWall, restrictive)
327 -- Check for south-side obstructions when moving north (
stepY < 0)
328 -- South walls are stored as north walls of the square below
329 if
stepY < 0 and hasOpening then
330 local southSquare = cell:getGridSquare(square:getX(), square:getY() + 1, square:getZ())
332 local southDoor = southSquare:getDoor(true)
333 local southWindow = southSquare:getWindow(true)
334 local southWall = southSquare:getWall(true)
335 hasOpening =
self:CheckWallPassage(southDoor, southWindow, southWall, restrictive)
339 -- Check for east-side obstructions when moving west (
stepX < 0)
340 -- East walls are stored as west walls of the square to the right
341 if
stepX < 0 and hasOpening then
342 local eastSquare = cell:getGridSquare(square:getX() + 1, square:getY(), square:getZ())
344 local eastDoor = eastSquare:getDoor(false)
345 local eastWindow = eastSquare:getWindow(false)
346 local eastWall = eastSquare:getWall(false)
347 hasOpening =
self:CheckWallPassage(eastDoor, eastWindow, eastWall, restrictive)
351 return not hasOpening -- Return true if blocked (no opening found)
354-- Check for valid openings between adjacent players (centralized function)
355-- Check for valid openings between adjacent players (optimized with centralized logic)
359 if
dx == 1 and
dy == 0 then
360 -- Moving east: check west walls between squares
361 -- Check west wall of target square
363 local door = targetSquare:getDoor(false)
364 local window = targetSquare:getWindow(false)
365 local wall = targetSquare:getWall(false)
367 if
self:CheckWallPassage(door, window, wall, restrictive) then
372 -- Also check east wall of local square (west wall of square to the right)
374 local localX, localY = localSquare:getX(), localSquare:getY()
375 local eastSquare = cell:getGridSquare(localX + 1, localY, localSquare:getZ())
377 local door = eastSquare:getDoor(false)
378 local window = eastSquare:getWindow(false)
379 local wall = eastSquare:getWall(false)
381 return
self:CheckWallPassage(door, window, wall, restrictive)
385 elseif
dx == -1 and
dy == 0 then
386 -- Moving west: check west wall of local square
388 local door = localSquare:getDoor(false)
389 local window = localSquare:getWindow(false)
390 local wall = localSquare:getWall(false)
392 return
self:CheckWallPassage(door, window, wall, restrictive)
395 elseif
dx == 0 and
dy == 1 then
396 -- Moving south: check north walls between squares
397 -- Check north wall of target square
399 local door = targetSquare:getDoor(true)
400 local window = targetSquare:getWindow(true)
401 local wall = targetSquare:getWall(true)
403 if
self:CheckWallPassage(door, window, wall, restrictive) then
408 -- Also check south wall of local square (north wall of square below)
410 local localX, localY = localSquare:getX(), localSquare:getY()
411 local southSquare = cell:getGridSquare(localX, localY + 1, localSquare:getZ())
413 local door = southSquare:getDoor(true)
414 local window = southSquare:getWindow(true)
415 local wall = southSquare:getWall(true)
417 return
self:CheckWallPassage(door, window, wall, restrictive)
421 elseif
dx == 0 and
dy == -1 then
422 -- Moving north: check north wall of local square
424 local door = localSquare:getDoor(true)
425 local window = localSquare:getWindow(true)
426 local wall = localSquare:getWall(true)
428 return
self:CheckWallPassage(door, window, wall, restrictive)
437 local lines, line, len = {},
"", 0
438 for word in
string.gmatch(desc,
"%S+")
do
440 if len + wlen + 1 <= 30 then
448 table.insert(lines, line)
449 line, len =
word, wlen
452 table.insert(lines, line)
456-- calculate character selection score
for weighted tracking (optimized)
457function
FrameworkZ.
Tooltips:CalculateCharacterScore(localPlayer, targetPlayer, mouseX, mouseY)
458 if not localPlayer or not targetPlayer then
463 local targetId = targetPlayer:getPlayerNum()
465 local px, py, pz = targetPlayer:getX(), targetPlayer:getY(), targetPlayer:getZ()
472 sx, sy = cached.
x, cached.
y
477 --
Cache the result with shorter lifetime for moving targets
479 self.HoveredCharacterData._lastScreenCacheTime = currentTime
482 -- Distance from mouse cursor to character on screen (closer = higher score)
483 local screenDx = sx - mouseX
484 local screenDy = sy - mouseY
485 local screenDistanceSqr = screenDx * screenDx + screenDy * screenDy
486 local screenScore = math.max(0, 100 - math.sqrt(screenDistanceSqr) * 0.5)
488 -- World distance factor (closer = higher score)
using utility function
489 local worldDistanceSqr =
Utils.getDistanceSquared(px, py, localPlayer:getX(), localPlayer:getY())
490 local worldScore = math.max(0, 50 - math.sqrt(worldDistanceSqr) * 10)
492 -- Line of sight bonus (visible = higher score) - use centralized function
493 local hasLineOfSight =
self:HasLineOfSight(localPlayer, targetPlayer)
494 local losScore = hasLineOfSight and 25 or 0
496 -- Apply stickiness bonus if this is the currently tracked
player
497 local stickinessBonus = 0
502 return screenScore + worldScore + losScore + stickinessBonus
505-- check line of sight between two players (optimized but more permissive)
506-- Centralized line of sight calculation with configurable strictness
507function
FrameworkZ.
Tooltips:CalculateLineOfSight(localPlayer, targetPlayer, restrictive, adjacentRange)
508 if not localPlayer or not targetPlayer then
512 -- Check if both players are on the same Z level
513 if localPlayer:getZ() ~= targetPlayer:getZ() then
517 -- Get starting and ending grid coordinates using utility function
521 -- If players are close, handle adjacent checking
524 if distanceSqr <= (adjacentRange or 1) then
525 -- Within adjacent range - use different logic
526 if distanceSqr <= 1 and restrictive then
527 -- Adjacent players with restrictive checking
529 local localSquare = cell:getGridSquare(x1, y1, z)
530 local targetSquare = cell:getGridSquare(x2, y2, z)
532 -- Use centralized function to check for valid openings
535 local hasOpening =
self:CheckAdjacentOpening(localSquare, targetSquare,
dx,
dy, restrictive)
539 -- Close range, permissive or within range - assume line of sight
544 -- Use Bresenham-like algorithm for longer distances
547 local absDx = math.abs(
dx)
548 local absDy = math.abs(
dy)
549 local
stepX = x1 < x2 and 1 or -1
550 local
stepY = y1 < y2 and 1 or -1
551 local err = absDx - absDy
558 -- Check
current square for obstructions using centralized function
559 local checkSquare = cell:getGridSquare(checkX, checkY, z)
560 local isTargetSquare = (checkX == x2 and checkY == y2)
562 if
self:CheckSquareObstructions(checkSquare,
stepX,
stepY, isTargetSquare, restrictive) then
563 return false -- Blocked by obstruction
566 -- Check if we
've reached the target
567 if checkX == x2 and checkY == y2 then
571 -- Move to next square using Bresenham algorithm
573 if err2 > -absDy then
575 checkX = checkX + stepX
579 checkY = checkY + stepY
586-- Strict line of sight check for tooltip visibility
587function FrameworkZ.Tooltips:HasLineOfSight(localPlayer, targetPlayer)
588 return self:CalculateLineOfSight(localPlayer, targetPlayer, true, 1)
591-- More permissive line of sight check specifically for typewriter speed calculation
592function FrameworkZ.Tooltips:HasLineOfSightForTypewriter(localPlayer, targetPlayer)
593 return self:CalculateLineOfSight(localPlayer, targetPlayer, false, 9) -- Within 3 tiles, very generous
596-- calculate typewriter speed based on distance, line of sight, and facing direction (slowed and more variable for immersive roleplay)
597function FrameworkZ.Tooltips:CalculateTypewriterSpeed(localPlayer, targetPlayer)
598 if not localPlayer or not targetPlayer then
599 return self.HoveredCharacterData.TypewriterBaseSpeed * 2.5 -- Conservative fallback for measured pacing
602 -- Use utility function for distance calculation
603 local distanceSqr = Utils.getDistanceSquared(localPlayer:getX(), localPlayer:getY(), targetPlayer:getX(), targetPlayer:getY())
605 -- Start with slower, more contemplative base speed
606 local finalSpeed = self.HoveredCharacterData.TypewriterBaseSpeed * 1.0
608 -- Distance factor (much more variable and generally slower)
609 local distanceMultiplier = 1.0
610 if distanceSqr < 1.0 then -- < 1 tile - intimate range
611 distanceMultiplier = 0.7 -- Still fairly quick for close interaction, but not instant
612 elseif distanceSqr < 4.0 then -- < 2 tiles - conversation range
613 distanceMultiplier = 1.0 -- Normal speed for conversation distance
614 elseif distanceSqr < 9.0 then -- < 3 tiles - observation range
615 distanceMultiplier = 1.4 -- Slower for distant observation
616 elseif distanceSqr < 16.0 then -- < 4 tiles - recognition range
617 distanceMultiplier = 1.8 -- Much slower for distant recognition
619 distanceMultiplier = 2.5 -- Very slow for far targets
621 finalSpeed = finalSpeed * distanceMultiplier
623 -- Line of sight check (significant impact for realism)
624 local hasLineOfSight = self:HasLineOfSightForTypewriter(localPlayer, targetPlayer)
625 if not hasLineOfSight then
626 finalSpeed = finalSpeed * 3.5 -- Much slower without clear line of sight
628 finalSpeed = finalSpeed * 1.0 -- Normal speed with clear line of sight
631 -- Local player facing direction check (significant variability for immersion)
632 local localAngleDiff = self:CalculatePlayerTargetAngle(localPlayer, targetPlayer:getX(), targetPlayer:getY())
634 -- Local player facing factor (more dramatic differences for engagement)
635 if localAngleDiff < ANGLE_CONSTANTS.PI_6 then
636 finalSpeed = finalSpeed * 0.6 -- Faster when staring directly, but not instant
637 elseif localAngleDiff < ANGLE_CONSTANTS.PI_4 then
638 finalSpeed = finalSpeed * 0.8 -- Moderately fast when looking at target
639 elseif localAngleDiff < ANGLE_CONSTANTS.PI_3 then
640 finalSpeed = finalSpeed * 1.0 -- Normal speed when generally facing
641 elseif localAngleDiff < ANGLE_CONSTANTS.PI_2 then
642 finalSpeed = finalSpeed * 1.3 -- Slower when partially facing
644 finalSpeed = finalSpeed * 2.2 -- Much slower when not facing
647 -- Target player facing direction check (moderate but noticeable effect)
648 local targetAngleDiff = self:CalculatePlayerTargetAngle(targetPlayer, localPlayer:getX(), localPlayer:getY())
650 -- Target facing factor (creates natural interaction rhythms)
651 if targetAngleDiff < ANGLE_CONSTANTS.PI_6 then
652 finalSpeed = finalSpeed * 0.7 -- Faster when target stares back (mutual attention)
653 elseif targetAngleDiff < ANGLE_CONSTANTS.PI_4 then
654 finalSpeed = finalSpeed * 0.9 -- Slightly faster when target looks back
655 elseif targetAngleDiff < ANGLE_CONSTANTS.PI_3 then
656 finalSpeed = finalSpeed * 1.1 -- Slightly slower when target is somewhat attentive
657 elseif targetAngleDiff < ANGLE_CONSTANTS.PI_2 then
658 finalSpeed = finalSpeed * 1.3 -- Slower when target is partially facing
660 finalSpeed = finalSpeed * 1.7 -- Much slower when target isn't paying attention
663 -- Mutual attention bonus (rewarding but not overpowering)
665 finalSpeed = finalSpeed * 0.5 -- Good bonus for mutual eye contact/attention
667 finalSpeed = finalSpeed * 0.7 -- Moderate bonus for good mutual attention
669 finalSpeed = finalSpeed * 0.9 -- Small bonus for casual mutual awareness
675-- update typewriter progress (optimized)
679 -- Check if we
're still in the delay period
680 if (currentTime - self.HoveredCharacterData.TypewriterDelayStartTime) < (self.HoveredCharacterData.TypewriterStartDelay * 1000) then
681 return -- Don't start typewriter effect yet
684 -- If this is the first update after delay, reset the timer
687 return -- Skip this frame to avoid large deltaTime
692 -- Recalculate
speed less frequently for better performance
695 -- Only recalculate every few frames (more responsive: 100ms instead of 200ms)
703 local avgFPS = math.max(1,
getAverageFPS()) -- prevent division by zero
704 local fpsNormalizer = 60 / avgFPS
706 -- Calculate frame-rate independent character reveal
speed
709 local charactersToReveal = deltaTime * adjustedCharsPerSecond * fpsNormalizer
711 if charactersToReveal >= 1.0 then
712 local charsToAdd = math.floor(charactersToReveal)
714 -- Update
description progress first (observing appearance)
715 local allDescriptionComplete = true
717 for
i = 1, #descLines do
718 if charsToAdd <= 0 then break end
720 local line = descLines[
i]
721 local lineLen = #line
730 if currentProgress < lineLen then
731 local lineCharsToAdd = math.min(charsToAdd, lineLen - currentProgress)
733 charsToAdd = charsToAdd - lineCharsToAdd
734 allDescriptionComplete = false
737 allDescriptionComplete = false
742 if allDescriptionComplete then
743 for
i = 1, #descLines do
745 if progress < #descLines[
i] then
746 allDescriptionComplete = false
753 if allDescriptionComplete and charsToAdd > 0 then
765-- reset typewriter state for new character (optimized)
769 self.HoveredCharacterData.TypewriterDelayStartTime =
getTimestampMs() -- Start the delay timer
771 -- Clear caches when resetting state
772 self.HoveredCharacterData._cachedScreenPositions = {}
773 self.HoveredCharacterData._lastScreenCacheTime = 0
774 self.HoveredCharacterData._lastTypewriterSpeedTime = 0
777-- draw the selector + text each
UI frame (optimized)
781 if not tooltipData.TooltipPlayer or not tooltipData.UI then
return end
784 local
player = tooltipData.TooltipPlayer
785 local pidx =
player:getPlayerNum()
790 -- Always draw selector ring immediately
791 local texture = tooltipData.
Texture
792 local w, h = texture:getWidth(), texture:getHeight()
794 local sw, sh = w * scale, h * scale
795 tooltipData.
UI:drawTextureScaled(
803 -- Check if delay period has passed before showing text
806 return -- Don't show text yet, only selector
809 -- update typewriter progress (only after delay)
814 local
font = UIFont.Dialogue
815 local lineH = tm:getFontFromEnum(
font):getLineHeight()
816 local ty = sy + (sh * 0.5) + 6
818 --
name at top (with typewriter effect) - recognition after observation
821 tm:DrawStringCentre(
font, sx, ty, visibleName, nameColor.r, nameColor.g, nameColor.b, nameColor.
a)
824 --
description below
name (with typewriter effect) - observing appearance
826 for
i = 1,
#descLines do
827 local line = descLines[
i]
828 local visibleChars = tooltipData.TypewriterDescriptionProgress[
i] or 0
829 local visibleLine =
string.sub(line, 1, visibleChars)
830 tm:DrawStringCentre(
font, sx, ty, visibleLine, 1, 1, 1, 1)
852 -- Reset character
data state when disabling tooltip
860--
Cache cleanup function to prevent memory leaks
864 -- Clean up screen
position cache if it's getting too old (every 10 seconds)
867 self.HoveredCharacterData._lastCacheCleanupTime = currentTime
870 -- Limit cache size to prevent memory bloat (increased from 50 to 100)
872 for _ in pairs(
self.HoveredCharacterData._cachedScreenPositions) do
873 cacheCount = cacheCount + 1
876 if cacheCount > 100 then -- Limit to 100 cached positions
877 -- Clear oldest entries by recreating cache (simple but effective)
880 for key,
value in pairs(
self.HoveredCharacterData._cachedScreenPositions) do
881 if count < 50 then -- Keep newest 50
892-- Main tick function (heavily optimized)
894 -- Cleanup caches periodically
904 local pidx = mp:getPlayerNum()
915 -- scan
a 3×3 grid for all IsoPlayers and score them (optimized)
916 local candidates = {}
918 local sqX, sqY = sq:getX(), sq:getY()
920 for ix = sqX - 1, sqX + 1 do
921 for iy = sqY - 1, sqY + 1 do
922 local sq2 = cell:getGridSquare(ix, iy, wz)
924 local movingObjects = sq2:getMovingObjects()
925 local objCount = movingObjects:size()
926 for
i = 0, objCount - 1 do
927 local o = movingObjects:get(
i)
930 if score > 0 then -- Only add candidates with positive scores
931 candidates[
#candidates + 1] = {player = o, score = score}
939 -- Find the best
candidate using utility function
942 -- Determine
if we should
switch to
a new character
943 local shouldSwitch =
false
944 local currentPlayerStillValid =
false
945 local currentScore = 0
948 -- Check
if current player is still in candidates list
using utility function
949 if tooltipData.TooltipPlayer then
950 currentPlayerStillValid, currentScore =
Utils.checkPlayerValidity(candidates, tooltipData.TooltipPlayer)
953 if not tooltipData.TooltipPlayer then
957 -- No candidates found, disable tooltip
960 elseif not currentPlayerStillValid then
964 -- Check
if the best
candidate is different and significantly better
966 -- Only
switch if the
new candidate is significantly better
968 shouldSwitch = scoreDifference > tooltipData.TooltipSwitchThreshold
974 -- Check line of sight before showing tooltip -
if no line of sight, don
't show tooltip at all
975 if not FrameworkZ.Tooltips:HasLineOfSight(mp, bestCandidate) then
976 FrameworkZ.Tooltips:DisableTooltip()
980 -- Check if player is facing the target - if not, don't show tooltip at all
986 -- Update to new character
990 -- Reset character
data state only if switching to
a new character
998 -- Show loading state immediately
1001 tooltipData.TooltipCharacterDescriptionLines = {}
1003 -- Request character
data from server - only send
if we haven
't already sent for this specific player
1004 if not tooltipData.TooltipRequestSent then
1005 FrameworkZ.Foundation:SendFire(mp, "FrameworkZ.Tooltips.RequestCharacterData", function(data, responseData)
1006 -- Handle response from server
1007 FrameworkZ.Tooltips:OnReceiveCharacterData(responseData)
1008 end, mp:getUsername(), targetUsername)
1009 tooltipData.TooltipRequestSent = true
1010 elseif tooltipData.TooltipDataLoaded then
1011 -- If data is already loaded, show it immediately
1012 local character = FrameworkZ.Characters:GetCharacterByID(mp:getUsername())
1013 if not character then return end
1014 local targetCharacter = FrameworkZ.Characters:GetCharacterByID(targetUsername)
1015 if not targetCharacter then return end
1017 tooltipData.TooltipCharacterName = character:GetRecognition(targetCharacter)
1018 tooltipData.TooltipCharacterNameColor = FrameworkZ.Factions:GetFactionByID(tooltipData.TooltipCharacterFaction):GetColor() or {r = 1, g = 1, b = 1, a = 1}
1019 tooltipData.TooltipCharacterDescriptionLines = FrameworkZ.Tooltips:GetDescriptionLines(tooltipData.TooltipCharacterDescription)
1021 FrameworkZ.Tooltips:ResetTypewriterState()
1024 -- Calculate initial typewriter speed and reset state
1025 tooltipData.TypewriterCurrentSpeed = FrameworkZ.Tooltips:CalculateTypewriterSpeed(mp, bestCandidate)
1026 FrameworkZ.Tooltips:ResetTypewriterState()
1028 FrameworkZ.Tooltips:EnableTooltip()
1030 -- No valid candidate, disable tooltip
1031 tooltipData.TooltipPlayer = nil
1032 FrameworkZ.Tooltips:DisableTooltip()
1034 elseif tooltipData.TooltipPlayer and currentPlayerStillValid then
1035 -- Check line of sight for current player before keeping tooltip
1036 if not FrameworkZ.Tooltips:HasLineOfSight(mp, tooltipData.TooltipPlayer) then
1037 -- Lost line of sight, disable tooltip
1038 FrameworkZ.Tooltips:DisableTooltip()
1042 -- Also check if still facing the current target
1043 if not FrameworkZ.Tooltips:IsPlayerFacingTarget(mp, tooltipData.TooltipPlayer:getX(), tooltipData.TooltipPlayer:getY(), ANGLE_CONSTANTS.PI_1_5) then
1044 FrameworkZ.Tooltips:DisableTooltip()
1048 -- Keep showing tooltip for current player
1049 FrameworkZ.Tooltips:EnableTooltip()
1051 -- No valid player to show
1052 FrameworkZ.Tooltips:DisableTooltip()
1055Events.OnTick.Add(FrameworkZ.Tooltips.OnTick)
1057function FrameworkZ.Tooltips:OnReceiveCharacterData(responseData)
1058 if not responseData then
1062 -- Only process if this response is for our current tooltip target
1063 if self.HoveredCharacterData.TooltipPlayer and self.HoveredCharacterData.TooltipPlayer:getUsername() == responseData.targetUsername then
1064 -- Store the character data
1065 self.HoveredCharacterData.TooltipDataLoaded = true
1067 -- Update tooltip data with recognition logic
1068 self.HoveredCharacterData.TooltipCharacterName = responseData.targetData.name or "[Unknown]"
1070 -- Get faction color
1071 local targetFaction = responseData.targetData.faction
1072 self.HoveredCharacterData.TooltipCharacterNameColor = targetFaction and FrameworkZ.Factions:GetFactionByID(targetFaction):GetColor() or {r = 1, g = 1, b = 1, a = 1}
1075 local descStr = responseData.targetData.description or ""
1076 self.HoveredCharacterData.TooltipCharacterDescriptionLines = FrameworkZ.Tooltips:GetDescriptionLines(descStr)
1078 -- Reset typewriter state with new data
1079 FrameworkZ.Tooltips:ResetTypewriterState()
1084 function FrameworkZ.Tooltips.RequestCharacterData(data, localUsername, targetUsername, requestingPlayer)
1085 -- Get both characters from server-side data
1086 local localCharacter = FrameworkZ.Characters:GetCharacterByID(localUsername)
1087 local targetCharacter = FrameworkZ.Characters:GetCharacterByID(targetUsername)
1089 if localCharacter and targetCharacter then
1090 local responseData = {
1091 localUsername = localUsername,
1092 targetUsername = targetUsername,
1093 localRecognizes = localCharacter:GetRecognition(targetCharacter) or "[Unknown]",
1095 name = localCharacter:GetRecognition(targetCharacter) or "[Unknown]",
1096 description = targetCharacter:GetDescription() or "[No Description]",
1097 faction = targetCharacter:GetFaction() or "[No Faction]",
1098 uid = targetCharacter:GetUID() or "[No UID]"
1102 -- Return response data to client via callback
1106 -- Return empty response if something went wrong
1108 localUsername = localUsername or "unknown",
1109 targetUsername = targetUsername or "unknown",
1110 localRecognizes = {},
1113 description = "[Error retrieving data]",
1119 FrameworkZ.Foundation:Subscribe("FrameworkZ.Tooltips.RequestCharacterData", FrameworkZ.Tooltips.RequestCharacterData)
void self createCharacterButton font()
void self FrameworkZ UI self nil
void processingNotification backgroundColor a()
Foundation for FrameworkZ.
Contains all of the User Interfaces for FrameworkZ.