-- above all hex levels ZO_SCRYING_FRAME_LEVEL = 10 ----------------- -- Scrying Hex -- ----------------- --[[ Each scrying hex manages the state of each hexagonal tile in the game board. ]]-- ZO_ScryingHex = ZO_InitializingObject:Subclass() do function ZO_ScryingHex:Initialize(board, control, hexIndex) self.board = board self.hexIndex = hexIndex self.row, self.column = GetScryingHexCoordinatesFromIndex(hexIndex) self.control = control self.control.hexObject = self self:InitializeControls() self.refreshTypeCallback = function() self:RefreshType() end self.idleLoop = SCRYING_HEX_ANIMATION_PROVIDER:AcquireIdleAnimation(self) self:ResetToCanonicalState() end ZO_SCRYING_HEX_DARKNESS_SURFACE = 1 ZO_SCRYING_HEX_TYPE_SURFACE = 2 ZO_SCRYING_HEX_FLASH_SURFACE = 3 ZO_SCRYING_HEX_FOREGROUND_SURFACE = 4 ZO_SCRYING_HEX_BORDER_SURFACE = 5 ZO_SCRYING_HEX_TEXTURE_WIDTH = 210 ZO_SCRYING_HEX_TEXTURE_HEIGHT = 182 ZO_SCRYING_SURFACE_TEXTURE_WIDTH = 256 ZO_SCRYING_SURFACE_TEXTURE_HEIGHT = 256 function ZO_ScryingHex:InitializeControls() local boardControl = self.board:GetControl() local hexWidth, hexHeight = self.board:GetHexDimensions() local offsetX, offsetY = self.board:ConvertHexCoordinateToOffset(self.row, self.column) self.control:SetAnchor(CENTER, boardControl, BOTTOM, offsetX, offsetY) self.control:SetDimensions(hexWidth, hexHeight) local surfaceWidth = hexWidth * (ZO_SCRYING_SURFACE_TEXTURE_WIDTH / ZO_SCRYING_HEX_TEXTURE_WIDTH) local surfaceHeight = hexHeight * (ZO_SCRYING_SURFACE_TEXTURE_HEIGHT / ZO_SCRYING_HEX_TEXTURE_HEIGHT) local backgroundTexture = self.control:GetNamedChild("Background") backgroundTexture:SetDimensions(surfaceWidth, surfaceHeight) self.textureComposite = self.control:GetNamedChild("Textures") self.textureComposite:SetDimensions(surfaceWidth, surfaceHeight) self.polygon = self.control:GetNamedChild("Polygon") local radiusNumHexes = 8 -- pre-computing multiple things: self.lightRadiusPixelsCoefficient = 1 / (radiusNumHexes * hexHeight) -- reciprocal to avoid divide in hot path self.lightRadiusPixelsCoefficient = self.lightRadiusPixelsCoefficient * self.lightRadiusPixelsCoefficient -- squaring to avoid sqrt in hot path end function ZO_ScryingHex:GetControl() return self.control end function ZO_ScryingHex:GetParentBoard() return self.board end function ZO_ScryingHex:GetCoordinates() return self.row, self.column end function ZO_ScryingHex:GetHexType() return self.hexType end function ZO_ScryingHex:GetGridRect() local hexWidth, hexHeight = self.board:GetHexDimensions() local halfWidth, halfHeight = hexWidth * 0.5, hexHeight * 0.5 local offsetX, offsetY = self.board:ConvertHexCoordinateToOffset(self.row, self.column) local left = offsetX - halfWidth local right = offsetX + halfWidth local top = offsetY - halfHeight local bottom = offsetY + halfHeight return left, right, top, bottom end -- In other systems this would be a refresh function; however, since we -- want to intentionally desync game logic and visual presentation to support -- animations, this should be used sparingly function ZO_ScryingHex:ResetToCanonicalState() self.hexType = GetScryingHexType(self.hexIndex) self.state = GetScryingHexState(self.hexIndex) self.isGoal, self.hasGoalBeenPreviouslyAchieved = IsScryingHexGoal(self.hexIndex) self:RefreshType() self:SetBorderType(ZO_SCRYING_HEX_BORDER_NONE) self:RefreshGoal() end function ZO_ScryingHex:SetScale(scale) self.control:SetScale(scale) end function ZO_ScryingHex:SetAlpha(alpha) self.control:SetAlpha(alpha) end function ZO_ScryingHex:PlayChangeAnimation() local newHexType = GetScryingHexType(self.hexIndex) if self.hexType ~= newHexType then self.hexType = newHexType local epicenterHex = SCRYING:GetCurrentSkill() == SCRYING_ACTIVE_SKILL_LINE and self.board:GetLineStartHex() or self.board:GetTargetHex() -- delay animation based on distance from epicenter: this causes a nice "chain" effect local epicenterX, epicenterY = epicenterHex:GetControl():GetCenter() local currentX, currentY = self:GetControl():GetCenter() local delay = zo_distance(epicenterX, epicenterY, currentX, currentY) / self:GetControl():GetHeight() SCRYING_HEX_ANIMATION_PROVIDER:PlayFlashAnimation(self, delay * 50) local ANIMATION_PLAYED = true return ANIMATION_PLAYED end local ANIMATION_NOT_PLAYED = false return ANIMATION_NOT_PLAYED end local HEX_TYPE_TEXTURES = { [SCRYING_HEX_TYPE_ONE] = { left = 0.25, top = 0, right = 0.5, bottom = 0.25 }, [SCRYING_HEX_TYPE_TWO] = { left = 0.5, top = 0, right = 0.75, bottom = 0.25 }, [SCRYING_HEX_TYPE_THREE] = { left = 0.75, top = 0, right = 1, bottom = 0.25 }, [SCRYING_HEX_TYPE_FOUR] = { left = 0, top = 0.25, right = 0.25, bottom = 0.5 }, [SCRYING_HEX_TYPE_FIVE] = { left = 0.25, top = 0.25, right = 0.5, bottom = 0.5 }, [SCRYING_HEX_TYPE_SIX] = { left = 0.5, top = 0.25, right = 0.75, bottom = 0.5 }, [SCRYING_HEX_TYPE_SEVEN] = { left = 0.75, top = 0.25, right = 1, bottom = 0.5 }, [SCRYING_HEX_TYPE_EIGHT] = { left = 0, top = 0.5, right = 0.25, bottom = 0.75 }, [SCRYING_HEX_TYPE_OWNED] = { left = 0, top = 0, right = 0.25, bottom = 0.25 }, [SCRYING_HEX_TYPE_GOAL] = false, -- no symbol } function ZO_ScryingHex:RefreshType() local coords = HEX_TYPE_TEXTURES[self.hexType] if coords then self.textureComposite:SetSurfaceHidden(ZO_SCRYING_HEX_TYPE_SURFACE, false) self.textureComposite:SetTextureCoords(ZO_SCRYING_HEX_TYPE_SURFACE, coords.left, coords.right, coords.top, coords.bottom) else self.textureComposite:SetSurfaceHidden(ZO_SCRYING_HEX_TYPE_SURFACE, true) end -- Update idle animation SCRYING_HEX_ANIMATION_PROVIDER:UpdateIdleAnimationParams(self.idleLoop, self) if self.goalObject and self.hexType == SCRYING_HEX_TYPE_OWNED then -- Update goal state self.goalObject:SetState(ZO_SCRYING_GOAL_STATE_OWNING) end end function ZO_ScryingHex:GetRefreshTypeCallback() return self.refreshTypeCallback end ZO_SCRYING_HEX_BORDER_NONE = 0 ZO_SCRYING_HEX_BORDER_TARGETED = 1 ZO_SCRYING_HEX_BORDER_AFFECTED = 2 local SELECTED_COLOR = ZO_WHITE local NOT_SELECTED_COLOR = ZO_ColorDef:New("FF00EFFF") function ZO_ScryingHex:SetBorderType(borderType) if borderType ~= self.borderType then self.borderType = borderType if borderType == ZO_SCRYING_HEX_BORDER_AFFECTED then self.polygon:SetBorderColor(SELECTED_COLOR:UnpackRGBA()) self.polygon:SetBorderThickness(ZO_AFFECTED_HEX_BORDER_PIXELS, ZO_AFFECTED_HEX_BORDER_PIXELS, 0) elseif borderType == ZO_SCRYING_HEX_BORDER_TARGETED then self.polygon:SetBorderColor(SELECTED_COLOR:UnpackRGBA()) self.polygon:SetBorderThickness(ZO_TARGETED_HEX_BORDER_PIXELS, ZO_TARGETED_HEX_BORDER_PIXELS, 0) else self.polygon:SetBorderColor(NOT_SELECTED_COLOR:UnpackRGBA()) self.polygon:SetBorderThickness(ZO_NOT_SELECTED_HEX_BORDER_PIXELS, ZO_NOT_SELECTED_HEX_BORDER_PIXELS, 0) end end end function ZO_ScryingHex:GetBorderType() return self.borderType end function ZO_ScryingHex:RefreshGoal() if self.isGoal and not self.goalObject then self.goalObject, self.goalKey = SCRYING_HEX_ANIMATION_PROVIDER:AcquireGoal() self.goalObject:SetHex(self) if self.hexType == SCRYING_HEX_TYPE_OWNED then self.goalObject:SetState(ZO_SCRYING_GOAL_STATE_OWNED) elseif self.hasGoalBeenPreviouslyAchieved then self.goalObject:SetState(ZO_SCRYING_GOAL_STATE_PREVIOUSLY_OWNED) else self.goalObject:SetState(ZO_SCRYING_GOAL_STATE_UNOWNED) end elseif not self.isGoal and self.goalObject then self.board:ReleaseGoal(self.goalKey) self.goalObject, self.goalKey = nil, nil end end local Clamp = zo_clamp -- localizing for performance function ZO_ScryingHex:RefreshDistanceFromCursor(epicenterX, epicenterY) local alpha = 0 if not self.isGoal and self.borderType == ZO_SCRYING_HEX_BORDER_NONE and self.hexType ~= SCRYING_HEX_TYPE_OWNED then local currentX, currentY = self.control:GetCenter() local dx, dy = epicenterX - currentX, epicenterY - currentY local distanceFromHexSquared = dx * dx + dy * dy alpha = Clamp(distanceFromHexSquared * self.lightRadiusPixelsCoefficient, 0, 1) -- EaseOutQuartic alpha = 1 - alpha alpha = 1 - (alpha * alpha * alpha * alpha) end self.textureComposite:SetSurfaceAlpha(ZO_SCRYING_HEX_DARKNESS_SURFACE, alpha) end function ZO_ScryingHex:DisableDarknessEffect() self.textureComposite:SetSurfaceAlpha(ZO_SCRYING_HEX_DARKNESS_SURFACE, 0) end function ZO_ScryingHex:OnMouseEnter() if self.board:IsMouseEnabled() then self.board:ChangeTargetHex(self) end end function ZO_ScryingHex:OnMouseExit() if self.board:IsMouseEnabled() then self.board:ChangeTargetHex(nil) end end function ZO_ScryingHex:OnMouseClicked() self.board:PerformActionOnHex(self) end function ZO_ScryingHex:OnMouseRightClicked() self.board:CancelLineAction() end function ZO_ScryingHex:OnDragStart() if SCRYING:GetCurrentSkill() == SCRYING_ACTIVE_SKILL_LINE then -- record line start target self.board:PerformActionOnHex(self) end end function ZO_ScryingHex:OnReceiveDrag() if SCRYING:GetCurrentSkill() == SCRYING_ACTIVE_SKILL_LINE and self.board:GetLineStartHex() ~= nil then -- record line end target self.board:PerformActionOnHex(self) end end function ZO_ScryingHex:OnEffectivelyShown() self.idleLoop:PlayFromStart() end function ZO_ScryingHex:OnEffectivelyHidden() self.idleLoop:Stop() end end --[[ Each action button represents the state of a scrying active skill, and the keybind you use to activate it ]]-- ZO_ScryingActionButton = ZO_InitializingObject:Subclass() function ZO_ScryingActionButton:Initialize(control, skill, actionName) self.control = control self.abilitySlot = control:GetNamedChild("AbilitySlot") self.icon = self.abilitySlot:GetNamedChild("Icon") self.activeSkill = skill self.actionName = actionName self.keybindLabel = control:GetNamedChild("Keybind") ZO_Keybindings_RegisterLabelForBindingUpdate(self.keybindLabel, actionName) self.abilitySlot:SetHandler("OnMouseEnter", function() InitializeTooltip(AbilityTooltip, self.icon, TOPLEFT, 5, -5, TOPRIGHT) AbilityTooltip:SetAbilityId(self.abilityId) end) self.abilitySlot:SetHandler("OnMouseExit", function() ClearTooltip(AbilityTooltip) end) self.abilitySlot:SetHandler("OnMouseUp", function(_, button, upInside) if button == MOUSE_BUTTON_INDEX_LEFT and upInside then SCRYING:TrySetCurrentSkill(self.activeSkill) end end) self:UpdatePlatformStyle() end do local SCRYING_ACTION_ACTIVE_COLOR = ZO_DEFAULT_ENABLED_COLOR local SCRYING_ACTION_INACTIVE_COLOR = ZO_ColorDef:New(.7, .7, .7) local SCRYING_ACTION_DISABLED_COLOR = ZO_DEFAULT_DISABLED_COLOR function ZO_ScryingActionButton:Refresh() if not IsScryingActiveSkillUnlocked(self.activeSkill) then self.control:SetHidden(true) return else self.control:SetHidden(false) end self.abilityId = GetScryingActiveSkillAbilityId(self.activeSkill) self.icon:SetTexture(GetAbilityIcon(self.abilityId)) if SCRYING:GetCurrentSkill() == self.activeSkill then self.icon:SetColor(SCRYING_ACTION_ACTIVE_COLOR:UnpackRGBA()) elseif IsScryingActiveSkillUsable(self.activeSkill) == SCRYING_ACTIVE_SKILL_USE_RESULT_SUCCESS then self.icon:SetColor(SCRYING_ACTION_INACTIVE_COLOR:UnpackRGBA()) else self.icon:SetColor(SCRYING_ACTION_DISABLED_COLOR:UnpackRGBA()) end end end do local KEYBOARD_BINDING_FONT = "ZoFontWinH3" local GAMEPAD_BINDING_FONT = "ZoFontGamepad18" function ZO_ScryingActionButton:UpdatePlatformStyle() self.keybindLabel:SetFont(IsInGamepadPreferredMode() and GAMEPAD_BINDING_FONT or KEYBOARD_BINDING_FONT) end end ---------------------------------- -- Scrying Normal Actions Meter -- ---------------------------------- --[[ This object replicates the StatusBarControl API for the normal actions meter, so we can reuse the existing status bar animations. ]]-- ZO_ScryingNormalActionsMeter = ZO_InitializingObject:Subclass() function ZO_ScryingNormalActionsMeter:Initialize(control) self.control = control self.activatedMask = control:GetNamedChild("ActivatedMask") self.edgeTexture = control:GetNamedChild("Edge") self.valueText = control:GetNamedChild("EdgeText") self.crossfadeAnim = ANIMATION_MANAGER:CreateTimelineFromVirtual("ZO_ScryingNormalActionMeter_Crossfade", self.control) self.idleLoop = ANIMATION_MANAGER:CreateTimelineFromVirtual("ZO_ScryingNormalActionMeter_Idle", self.control) self.value = 0 self.min = 0 self.max = 1 self.isTextVisible = false self.maskBaseHeight = self.control:GetHeight() self.edgeBaseWidth = self.edgeTexture:GetWidth() self.edgeBaseHeight = self.edgeTexture:GetHeight() self:SetValue(0) self:UpdatePlatformStyle() self.idleLoop:PlayFromStart() end function ZO_ScryingNormalActionsMeter:SetValue(value) self.value = value local min, max = self.min, self.max local progress = (value - min) / (max - min) -- set % masked based on value self.activatedMask:SetHeight(progress * self.maskBaseHeight) -- taper off edge height near top/bottom local distanceFromEdge = progress > 0.5 and 1 - progress or progress -- linear ramp from 0 -> .5 -> 0 local taperHeight = math.min(0.1, distanceFromEdge) * 10 self.edgeTexture:SetHeight(taperHeight * self.edgeBaseHeight) -- taper off edge width near top local taperWidth = math.min(1 / 3, progress) * 3 self.edgeTexture:SetWidth(taperWidth * self.edgeBaseWidth) end function ZO_ScryingNormalActionsMeter:GetValue() return self.value end function ZO_ScryingNormalActionsMeter:SetMinMax(min, max) self.min = min self.max = max end do local TURNS_UNTIL_SHOW_TEXT = 3 function ZO_ScryingNormalActionsMeter:SmoothTransition(value, max, forceInit) ZO_StatusBar_SmoothTransition(self, value, max, forceInit) local shouldTextBeVisible = value <= TURNS_UNTIL_SHOW_TEXT if shouldTextBeVisible ~= self.isTextVisible then self.isTextVisible = shouldTextBeVisible if shouldTextBeVisible then if forceInit then self.crossfadeAnim:PlayInstantlyToEnd() else self.crossfadeAnim:PlayFromStart() end else if forceInit then self.crossfadeAnim:PlayInstantlyToStart() else self.crossfadeAnim:PlayFromEnd() end end end if shouldTextBeVisible then self.valueText:SetText(tostring(value)) end end end do local KEYBOARD_VALUE_FONT = "ZoFontKeyboard18ThickOutline" local GAMEPAD_VALUE_FONT = "ZoFontGamepad36ThickOutlineLatin" function ZO_ScryingNormalActionsMeter:UpdatePlatformStyle() self.valueText:SetFont(IsInGamepadPreferredMode() and GAMEPAD_VALUE_FONT or KEYBOARD_VALUE_FONT) end end function ZO_ScryingNormalActionsMeter:OnShowing() self.control:SetAlpha(1) end function ZO_ScryingNormalActionsMeter:OnEndOfGameTimelineUpdate(animation, progress) local alpha = zo_lerp(1, 0, progress) self.control:SetAlpha(alpha) end ---------------------------------- -- Scrying Special Actions Meter -- ---------------------------------- --[[ Unlike the normal actions meter, which is fairly statusbar-like, the special actions meter is special in that the valid number of steps is fixed. The special actions meter crossfades between two well-defined textures for each step. ]]-- ZO_ScryingSpecialActionsMeter = ZO_InitializingObject:Subclass() function ZO_ScryingSpecialActionsMeter:Initialize(control) self.control = control self.activeTexture = self.control:GetNamedChild("ActiveTexture") self.lastTexture = self.control:GetNamedChild("LastTexture") self.crossfadeAnim = ANIMATION_MANAGER:CreateTimelineFromVirtual("ZO_ScryingSpecialActionMeter_Crossfade", self.control) local INSTANT = true self:SmoothTransition(0, INSTANT) end do local FULL_METER_TEXTURES = { [0] = { left = 0, top = 0, right = 0, bottom = 0 }, -- A single transparent pixel in the top-left [1] = { left = 0.5, top = 0, right = 1, bottom = 0.25 }, [2] = { left = 0.5, top = 0.25, right = 1, bottom = 0.5 }, [3] = { left = 0.5, top = 0.5, right = 1, bottom = 0.75 }, } local PARTIAL_METER_TEXTURES = { [0] = { left = 0, top = 0, right = 0.5, bottom = 0.25 }, [1] = { left = 0, top = 0.25, right = 0.5, bottom = 0.5 }, [2] = { left = 0, top = 0.5, right = 0.5, bottom = 0.75 }, } function ZO_ScryingSpecialActionsMeter:SmoothTransition(numGems, shouldFillPartialGem, shouldAnimateInstantly) local coords if shouldFillPartialGem then coords = PARTIAL_METER_TEXTURES[numGems] else coords = FULL_METER_TEXTURES[numGems] end self.lastTexture:SetTextureCoords(self.activeTexture:GetTextureCoords()) self.activeTexture:SetTextureCoords(coords.left, coords.right, coords.top, coords.bottom) if shouldAnimateInstantly then self.crossfadeAnim:PlayInstantlyToEnd() else self.crossfadeAnim:PlayFromStart() end end end -------------------------- -- Scrying Modal Cursor -- -------------------------- --[[ The Scrying modal cursor handles a few cursor-like things: * for gamepad, it helps the players know which hex they currently have targeted * on both platforms, it will add extra context to each special ability ]]-- ZO_ScryingModalCursor = ZO_InitializingObject:Subclass() function ZO_ScryingModalCursor:Initialize(board, control) self.board = board self.control = control self.gamepadCursorEnabled = false local function GetStickMagnitude(direction) --When using a movement controller on the slightest input in a direction it will move in that direction and then start accumulating to do the following move. This is important for responsiveness. --However it means that it's super touchy when using both X and Y. Pressing the stick at 89 degress will cause a movement to the right. So we apply these thresholds to get rid of those weird moves. if direction == MOVEMENT_CONTROLLER_DIRECTION_VERTICAL then return zo_abs(self.gamepadY) > 0.4 and self.gamepadY or 0 end return zo_abs(self.gamepadX) > 0.4 and -self.gamepadX or 0 end self.verticalMovementController = ZO_MovementController:New(MOVEMENT_CONTROLLER_DIRECTION_VERTICAL, 8, GetStickMagnitude) self.horizontalMovementController = ZO_MovementController:New(MOVEMENT_CONTROLLER_DIRECTION_HORIZONTAL, 8, GetStickMagnitude) self.gamepadTargetControl = self.control:GetNamedChild("GamepadTarget") self.lineStartControl = self.control:GetNamedChild("LineStart") self.lineEndControl = self.control:GetNamedChild("LineEnd") self.coOptControl = self.control:GetNamedChild("CoOpt") self.coOptControl.translationAnimation:SetTranslateOffsets(0, -10, 0, 0) self.bombControl = self.control:GetNamedChild("Bomb") self:CreateBombCursorPetals() self.gamepadTargetIdleLoop = ANIMATION_MANAGER:CreateTimelineFromVirtual("ZO_ScryingGamepadTargetCursor_Idle", self.gamepadTargetControl) self.gamepadTargetIdleLoop:PlayFromStart() end function ZO_ScryingModalCursor:CreateBombCursorPetals() self.bombCursorPetalControls = {} for direction = SCRYING_GRID_DIRECTION_UP, SCRYING_GRID_DIRECTION_UP_LEFT do local petal = CreateControlFromVirtual("$(parent)Petal" .. direction, self.bombControl, "ZO_ScryingBombCursor_Petal") local angle = GetAngleFromUpForScryingGridDirection(direction) petal:SetTextureRotation(angle) local DELAY_PER_PETAL_MS = 30 petal.fadeInTimeline:SetAllAnimationOffsets((direction - SCRYING_GRID_DIRECTION_UP) * DELAY_PER_PETAL_MS) local INITIAL_DISTANCE = 30 local startX, startY = ZO_Rotate2D(-angle, 0, INITIAL_DISTANCE) petal.translationAnimation:SetTranslateOffsets(startX, startY, 0, 0) table.insert(self.bombCursorPetalControls, petal) end end ZO_SCRYING_GAMEPAD_CURSOR_HEIGHT = 64 ZO_SCRYING_GAMEPAD_CURSOR_WIDTH = 64 ZO_SCRYING_LINE_CURSOR_WIDTH = 256 * 1.5 ZO_SCRYING_LINE_CURSOR_HEIGHT = 256 * 1.5 ZO_SCRYING_CO_OPT_CURSOR_WIDTH = 128 * 1.75 ZO_SCRYING_CO_OPT_CURSOR_HEIGHT = 128 * 1.75 ZO_SCRYING_BOMB_PETAL_WIDTH = 256 * 1.5 ZO_SCRYING_BOMB_PETAL_HEIGHT = 256 * 1.5 function ZO_ScryingModalCursor:ResizeToMatchHexSize(hexWidth, hexHeight) local hexScaleX = hexWidth / ZO_SCRYING_HEX_TEXTURE_WIDTH local hexScaleY = hexWidth / ZO_SCRYING_HEX_TEXTURE_HEIGHT self.gamepadTargetControl:SetDimensions(ZO_SCRYING_GAMEPAD_CURSOR_WIDTH * hexScaleX, ZO_SCRYING_GAMEPAD_CURSOR_HEIGHT * hexScaleY) self.lineStartControl:SetDimensions(ZO_SCRYING_LINE_CURSOR_WIDTH * hexScaleX, ZO_SCRYING_LINE_CURSOR_HEIGHT * hexScaleY) self.lineEndControl:SetDimensions(ZO_SCRYING_LINE_CURSOR_WIDTH * hexScaleX, ZO_SCRYING_LINE_CURSOR_HEIGHT * hexScaleY) self.coOptControl:SetDimensions(ZO_SCRYING_CO_OPT_CURSOR_WIDTH * hexScaleX, ZO_SCRYING_CO_OPT_CURSOR_HEIGHT * hexScaleY) for _, petal in ipairs(self.bombCursorPetalControls) do petal:SetDimensions(ZO_SCRYING_BOMB_PETAL_WIDTH * hexScaleX, ZO_SCRYING_BOMB_PETAL_HEIGHT * hexScaleY) end end do local function AnchorToHex(control, hex) control:ClearAnchors() control:SetAnchor(CENTER, hex:GetControl(), CENTER) end function ZO_ScryingModalCursor:UpdateModalAction() local targetHex = self.board:GetTargetHex() if not targetHex then self.control:SetHidden(true) return end self.control:SetHidden(false) local currentActiveSkill = SCRYING:GetCurrentSkill() self.lineStartControl:SetHidden(currentActiveSkill ~= SCRYING_ACTIVE_SKILL_LINE) self.lineEndControl:SetHidden(currentActiveSkill ~= SCRYING_ACTIVE_SKILL_LINE) self.coOptControl:SetHidden(currentActiveSkill ~= SCRYING_ACTIVE_SKILL_CO_OPT) self.bombControl:SetHidden(currentActiveSkill ~= SCRYING_ACTIVE_SKILL_BOMB) -- every cursor except line start points at the target hex, so this will recursively hit all of them AnchorToHex(self.control, targetHex) if currentActiveSkill == SCRYING_ACTIVE_SKILL_LINE then local lineStartHex = self.board:GetLineStartHex() if lineStartHex and lineStartHex ~= targetHex then local direction, endHexIndex = GetScryingLastAffectedLineActionInfo() local angle = GetAngleFromUpForScryingGridDirection(direction) local lineEndHex = endHexIndex and self.board:GetHexByIndex(endHexIndex) AnchorToHex(self.lineStartControl, lineStartHex) self.lineStartControl:SetTextureRotation(angle + ZO_PI) if lineEndHex then self.lineEndControl:SetHidden(false) AnchorToHex(self.lineEndControl, lineEndHex) self.lineEndControl:SetTextureRotation(angle) else self.lineEndControl:SetHidden(true) end else AnchorToHex(self.lineStartControl, targetHex) self.lineStartControl:SetTextureRotation(ZO_PI) self.lineEndControl:SetHidden(false) AnchorToHex(self.lineEndControl, targetHex) self.lineEndControl:SetTextureRotation(0) end end end end function ZO_ScryingModalCursor:SetGamepadVirtualCursorEnabled(enabled) if self.gamepadCursorEnabled ~= enabled then self.gamepadCursorEnabled = enabled self.gamepadTargetControl:SetHidden(not enabled) if enabled then DIRECTIONAL_INPUT:Activate(self, self.control) else DIRECTIONAL_INPUT:Deactivate(self) end end end function ZO_ScryingModalCursor:IsGamepadVirtualCursorEnabled() return self.gamepadCursorEnabled end function ZO_ScryingModalCursor:UpdateDirectionalInput() self.gamepadX, self.gamepadY = DIRECTIONAL_INPUT:GetXY(ZO_DI_LEFT_STICK, ZO_DI_DPAD) local dx, dy = self.gamepadX, self.gamepadY local targetHex = self.board:GetTargetHex() if internalassert(targetHex, "targetHex is nil") then local row, column = targetHex:GetCoordinates() local newRow, newColumn = row, column local verticalMove, horizontalMove if self.board:GetLineStartHex() then -- line drawing mode local lineStartHex = self.board:GetLineStartHex() dy = -dy -- flip such that +1 is down local DEADZONE = 0.4 if math.sqrt(dx * dx + dy * dy) > DEADZONE then local angleTurns = math.atan2(dy, dx) / ZO_TWO_PI -- recenter such that the top sixth of of the circle is centered around 0 and the bottom sixth around 3 local angleTurnsNormalized = (angleTurns + 5 / 16) % 1 local angleDirection = math.floor(angleTurnsNormalized * 6) local rowOffset, columnOffset = GetHexOffsetsForScryingGridDirection(angleDirection) local rowStart, columnStart = lineStartHex:GetCoordinates() newRow = rowStart + rowOffset newColumn = columnStart + columnOffset end else -- vertical movement verticalMove = self.verticalMovementController:CheckMovement() if verticalMove == MOVEMENT_CONTROLLER_MOVE_NEXT then newRow = row + 2 elseif verticalMove == MOVEMENT_CONTROLLER_MOVE_PREVIOUS then newRow = row - 2 end -- horizontal movement horizontalMove = self.horizontalMovementController:CheckMovement() if horizontalMove == MOVEMENT_CONTROLLER_MOVE_NEXT then newColumn = column + 1 newRow = (newColumn % 2 == 0) and (row - 1) or (row + 1) elseif horizontalMove == MOVEMENT_CONTROLLER_MOVE_PREVIOUS then newColumn = column - 1 newRow = (newColumn % 2 == 0) and (row - 1) or (row + 1) end end -- apply movement local newTargetHex = self.board:GetHex(newRow, newColumn) if not newTargetHex then -- on the very last row, the usual up/down pattern we apply to -- navigate between rows may not exist. to handle this let's try again -- by only shifting columns, not rows if horizontalMove == MOVEMENT_CONTROLLER_MOVE_NEXT then newColumn = column + 2 newRow = row newTargetHex = self.board:GetHex(newRow, newColumn) elseif horizontalMove == MOVEMENT_CONTROLLER_MOVE_PREVIOUS then newColumn = column - 2 newRow = row newTargetHex = self.board:GetHex(newRow, newColumn) end end if newTargetHex and self.targetHex ~= newTargetHex then self.board:ChangeTargetHex(newTargetHex) end end end ------------------- -- Scrying Board -- ------------------- --[[ The scrying board maintains the state of each hex, and any extra data about the game such as turns left ]]-- ZO_ScryingBoard = ZO_InitializingObject:Subclass() function ZO_ScryingBoard:Initialize(gameControl) self.gameControl = gameControl self.boardControl = self.gameControl:GetNamedChild("SafeArea") self.boardMask = self.gameControl:GetNamedChild("EyeBackgroundMask") self.hexList = {} self.lastBorderVisibleHexes = {} self.borderVisibleHexes = {} self.targetHex = nil self.hexControlPool = ZO_ControlPool:New("ZO_ScryingHex", self.boardControl, "Hex") local function ResetHexControl(hexControl) hexControl.hexObject = nil hexControl:SetScale(1) hexControl:SetAlpha(1) end self.hexControlPool:SetCustomResetBehavior(ResetHexControl) local function HexAcquire(hexControl, hexIndex) ZO_ScryingHex:New(self, hexControl, hexIndex) end self.hexControlPool:SetCustomAcquireBehavior(HexAcquire) self.fakeHexPool = ZO_ControlPool:New("ZO_ScryingFakeHex", self.boardControl, "FakeHex") self.goalControlPool = ZO_ControlPool:New("ZO_ScryingGoal", self.boardControl, "Goal") self.modalCursor = ZO_ScryingModalCursor:New(self, self.boardControl:GetNamedChild("ModalCursor")) end function ZO_ScryingBoard:OnShowing() ZO_ClearNumericallyIndexedTable(self.hexList) self.hexControlPool:ReleaseAllObjects() SCRYING_HEX_ANIMATION_PROVIDER:ReleaseAllAnimations() self.targetHex = nil self.rootHexRow, self.rootHexColumn = GetScryingHexCoordinatesFromIndex(GetRootScryingHexIndex()) self:RecalculateHexSize() for hexIndex = 1, GetNumTotalScryingHexes() do local hexControl = self.hexControlPool:AcquireObject(hexIndex) self.hexList[hexIndex] = hexControl.hexObject end self:SetupFakeHexBackground() EVENT_MANAGER:RegisterForUpdate("ZO_ScryingBoard", 0, function() self:OnUpdate() end) end function ZO_ScryingBoard:OnHidden() EVENT_MANAGER:UnregisterForUpdate("ZO_ScryingBoard") end function ZO_ScryingBoard:RecalculateHexSize() local targetBoardWidth, targetBoardHeight = self.boardControl:GetDimensions() local numRows, numColumns = GetScryingBoardSize() numRows = numRows - 1 -- exclude root hex, we want it to overlap local maxRow, maxColumn = numRows + 1, numColumns + (1/3) -- add 1 (or 1/3) to calculate the bottom-right point of the bottom-right hex, and not the top-left local targetHexWidthHorizontal = targetBoardWidth / (maxColumn * 0.75) -- This is the ConvertHexCoordinateToOffset math, but reversed local targetHexHeightHorizontal = targetHexWidthHorizontal * ZO_SCRYING_HEX_TEXTURE_HEIGHT / ZO_SCRYING_HEX_TEXTURE_WIDTH -- match ratio of source texture -- repeat, but treat the vertical axis as the constraining axis local targetHexHeightVertical = targetBoardHeight / (maxRow * 0.5) local targetHexWidthVertical = targetHexHeightVertical * ZO_SCRYING_HEX_TEXTURE_WIDTH / ZO_SCRYING_HEX_TEXTURE_HEIGHT -- pick the axis that results in smaller overall hexes, the other axis would go outside of the safe area if targetHexWidthHorizontal < targetHexWidthVertical then self.hexWidth, self.hexHeight = targetHexWidthHorizontal, targetHexHeightHorizontal else self.hexWidth, self.hexHeight = targetHexWidthVertical, targetHexHeightVertical end self.modalCursor:ResizeToMatchHexSize(self.hexWidth, self.hexHeight) end do local BOARD_CENTER_OFFSET_Y = 10 local BORDER_COLOR = ZO_ColorDef:New("AA00EFFF") local MAX_HEX_SCALE = 0.75 local MIN_HEX_SCALE = 0.35 local MAX_HEX_RADIUS_X = 520 local MAX_HEX_RADIUS_Y = 400 function ZO_ScryingBoard:CreateFakeHex(row, column, distance) if (row + column) % 2 == 0 and not self:GetHex(row, column) then local offsetX, offsetY = self:ConvertHexCoordinateToOffset(row, column) local boardBottomY = self.boardControl:GetBottom() local boardCenterX, boardCenterY = self.boardControl:GetCenter() local expectedCenterX, expectedCenterY = boardCenterX + offsetX, boardBottomY + offsetY -- Calculate hexagon's normalized elliptical distance from the board's origin -- and derive an eased alpha for the control. local centerOffsetX, centerOffsetY = expectedCenterX - boardCenterX, expectedCenterY - (boardCenterY + BOARD_CENTER_OFFSET_Y) local angleRadians = math.atan2(centerOffsetX, centerOffsetY) local a = math.abs(math.sin(angleRadians) * (centerOffsetX / MAX_HEX_RADIUS_X)) local b = math.abs(math.cos(angleRadians) * (centerOffsetY / MAX_HEX_RADIUS_Y)) local normalizedDistance = 1 - (a + b) if normalizedDistance <= 0 then return end local easedDistance = normalizedDistance > 0.25 and 1 or ZO_EaseOutCubic(zo_lerp(0, 4, normalizedDistance)) -- Before creating the object, test if it will overlap anything we don't want it to local scale = zo_lerp(MIN_HEX_SCALE, MAX_HEX_SCALE, easedDistance) local surfaceWidth = self.hexWidth * scale local surfaceHeight = self.hexHeight * scale -- testing top and bottom to have more precise testing against lattice work -- we don't need this precision on the X axis local expectedTopY = expectedCenterY - (surfaceWidth * 0.5) local expectedBottomY = expectedCenterY + (surfaceWidth * 0.5) if not self.boardMask:IsPointInside(expectedCenterX, expectedTopY) or not self.boardMask:IsPointInside(expectedCenterX, expectedBottomY) then return end local fakeControl, fakeControlKey = self.fakeHexPool:AcquireObject() fakeControl.centerOffsetAngleRadians = angleRadians fakeControl:ClearAnchors() fakeControl:SetAnchor(CENTER, self.boardControl, BOTTOM, offsetX, offsetY) fakeControl:SetDimensions(surfaceWidth, surfaceHeight) -- Remove margin around center texture local halfTextureRatioX = ZO_SCRYING_HEX_TEXTURE_WIDTH / ZO_SCRYING_SURFACE_TEXTURE_WIDTH / 2 local halfTextureRatioY = ZO_SCRYING_HEX_TEXTURE_HEIGHT / ZO_SCRYING_SURFACE_TEXTURE_HEIGHT / 2 fakeControl:SetCenterTextureCoords(1 - halfTextureRatioX, halfTextureRatioX, 1 - halfTextureRatioY, halfTextureRatioY) local r, g, b = BORDER_COLOR:UnpackRGB() local a = easedDistance fakeControl:SetBorderColor(r, g, b, a) end end end local function ExclusiveRangeIterator(stopValue, iterValue) iterValue = iterValue + 1 if iterValue < stopValue then return iterValue end end -- this matches `for(i = startValue; i < stopValue; ++i) {}` local function ExclusiveRange(startValue, stopValue) return ExclusiveRangeIterator, stopValue, startValue - 1 end function ZO_ScryingBoard:SetupFakeHexBackground() self.fakeHexPool:ReleaseAllObjects() local numRows, numColumns = GetScryingBoardSize() -- left local COLUMNS_LEFT = 10 for row in ExclusiveRange(0, numRows - 1) do for column in ExclusiveRange(1 - COLUMNS_LEFT, 1) do local distance = 1 - column self:CreateFakeHex(row, column, distance) end end -- right local COLUMNS_RIGHT = 10 for row in ExclusiveRange(0, numRows - 1) do for column in ExclusiveRange(numColumns + 1, numColumns + COLUMNS_RIGHT) do local distance = column - numColumns self:CreateFakeHex(row, column, distance) end end -- down local ROWS_DOWN = 10 for column in ExclusiveRange(1 - COLUMNS_LEFT, numColumns + COLUMNS_RIGHT) do for row in ExclusiveRange(numRows - 1, numRows + ROWS_DOWN) do -- also generate background hexes for the last row, which has only the root hex in it local downDistance = zo_floor((3 + row - numRows) * 0.5) -- halving then flooring to treat every two rows as the same row: this creates a nice visual band of hexes local leftDistance = 1 - column local rightDistance = column - numColumns local distance = zo_max(downDistance, leftDistance, rightDistance) -- Pick between the distance functions for down, left, and right to create a rectangular gradient self:CreateFakeHex(row, column, distance) end end -- up local ROWS_UP = 10 for column in ExclusiveRange(1 - COLUMNS_LEFT, numColumns + COLUMNS_RIGHT) do for row in ExclusiveRange(-ROWS_UP, 0) do local upDistance = zo_floor((1 - row) * 0.5) -- halving then flooring to treat every two rows as the same row: this creates a nice visual band of hexes local leftDistance = 1 - column local rightDistance = column - numColumns local distance = zo_max(upDistance, leftDistance, rightDistance) -- Pick between the distance functions for up, left, and right to create a rectangular gradient self:CreateFakeHex(row, column, distance) end end end function ZO_ScryingBoard:GetHexDimensions() return self.hexWidth, self.hexHeight end -- All hex math assuming each hexagon is oriented to be flat on the top and bottom, with doubled coordinates function ZO_ScryingBoard:ConvertHexCoordinateToOffset(row, column) -- Reminder, with doubled coordinates: -- the hexagon directly underneath the current hexagon is row+2, col -- the hexagon underneath and to the right of the current hexagon is row+1, col+1 -- the hexagon to the right of the current hexagon is row, col+2 -- The root hex should always have an offset of 0, 0 column = column - self.rootHexColumn row = row - self.rootHexRow -- The leftmost point of the hexagon one column to the right should be flush with the bottom-right point of the current hexagon local offsetX = column * self.hexWidth * 0.75 -- The center of the hexagon one row down should be flush with the bottom of the current hexagon local offsetY = row * self.hexHeight * 0.5 return offsetX, offsetY end function ZO_ScryingBoard:GetBoardDimensions() return self.boardControl:GetDimensions() end function ZO_ScryingBoard:EnableVirtualCursor() self.modalCursor:SetGamepadVirtualCursorEnabled(true) if not self:GetTargetHex() then local rootHex = self:GetRootHex() local NO_SOUND = true self:ChangeTargetHex(rootHex, NO_SOUND) end end function ZO_ScryingBoard:DisableVirtualCursor() self.modalCursor:SetGamepadVirtualCursorEnabled(false) end function ZO_ScryingBoard:IsVirtualCursorEnabled() return self.modalCursor:IsGamepadVirtualCursorEnabled() end function ZO_ScryingBoard:IsMouseEnabled() return SCRYING:IsPlayerInputEnabled() and not self.modalCursor:IsGamepadVirtualCursorEnabled() end function ZO_ScryingBoard:GetHex(row, column) local hexIndex = GetScryingHexIndexFromCoordinates(row, column) return self.hexList[hexIndex] end function ZO_ScryingBoard:GetHexByIndex(hexIndex) return self.hexList[hexIndex] end function ZO_ScryingBoard:GetRootHex() return self.hexList[GetRootScryingHexIndex()] end function ZO_ScryingBoard:ResetAllHexes() for _, hex in ipairs(self.hexList) do hex:ResetToCanonicalState() end end function ZO_ScryingBoard:PlayHexChangeAnimations() local numChangedHexes = 0 for _, hex in ipairs(self.hexList) do if hex:PlayChangeAnimation() then numChangedHexes = numChangedHexes + 1 end end local activeSkill = SCRYING:GetCurrentSkill() if activeSkill == SCRYING_ACTIVE_SKILL_NORMAL then if numChangedHexes <= 3 then PlaySound(SOUNDS.SCRYING_CAPTURE_HEX_SMALL) elseif numChangedHexes <= 6 then PlaySound(SOUNDS.SCRYING_CAPTURE_HEX_MEDIUM) else PlaySound(SOUNDS.SCRYING_CAPTURE_HEX_LARGE) end elseif activeSkill == SCRYING_ACTIVE_SKILL_CO_OPT then if numChangedHexes <= 3 then PlaySound(SOUNDS.SCRYING_CO_OPT_HEX_SMALL) elseif numChangedHexes <= 6 then PlaySound(SOUNDS.SCRYING_CO_OPT_HEX_MEDIUM) else PlaySound(SOUNDS.SCRYING_CO_OPT_HEX_LARGE) end elseif activeSkill == SCRYING_ACTIVE_SKILL_BOMB then PlaySound(SOUNDS.SCRYING_ACTIVATE_BOMB) elseif activeSkill == SCRYING_ACTIVE_SKILL_LINE then PlaySound(SOUNDS.SCRYING_ACTIVATE_LINE) end end function ZO_ScryingBoard:GetTargetHex() return self.targetHex end function ZO_ScryingBoard:GetScryingActionParameters() local currentSkill = SCRYING:GetCurrentSkill() if not self.targetHex then return SCRYING_ACTIVE_SKILL_INVALID -- all skills need a target hex elseif self.lineStartHex then local lineStartRow, lineStartColumn = self.lineStartHex:GetCoordinates() local lineEndRow, lineEndColumn = self.targetHex:GetCoordinates() return currentSkill, lineStartRow, lineStartColumn, lineEndRow, lineEndColumn else local targetRow, targetColumn = self.targetHex:GetCoordinates() return currentSkill, targetRow, targetColumn end end do local function SortByHexIndex(leftHex, rightHex) return leftHex.hexIndex < rightHex.hexIndex end local function HaveBorderVisibleHexesChanged(lastBorderVisibleHexes, borderVisibleHexes) if #lastBorderVisibleHexes ~= #borderVisibleHexes then return true end -- assumes hex list is sorted, which it should be for index, hex in ipairs(lastBorderVisibleHexes) do if hex ~= borderVisibleHexes[index] then return true end end return false end function ZO_ScryingBoard:DirtySimulatedActionState(shouldPlaySound) -- simulated action state is a once per frame dirty flag. this is so we -- can process multiple changed target hexes per frame without doubling up -- on sfx or breaking the logic that detects if we've left an affected hex island or not self.simulatedActionStateDirty = true if not self.playActionStateSound then self.playActionStateSound = shouldPlaySound == true -- coerce to bool end end local HEX_NEIGHBORS = { {-2, 0}, {-1, 1}, {1, 1}, {2, 0}, {1, -1}, {-1, -1}, } local function FindAdjacentIslandForHex(board, hexToIslandMap, hex) local row, column = hex:GetCoordinates() for _, neighborOffset in ipairs(HEX_NEIGHBORS) do local rowOffset, columnOffset = unpack(neighborOffset) local neighborRow, neighborColumn = row + rowOffset, column + columnOffset local neighborHex = board:GetHex(neighborRow, neighborColumn) if hexToIslandMap[neighborHex] then return hexToIslandMap[neighborHex] end end end function ZO_ScryingBoard:RefreshSimulatedAction() local lastBorderVisibleHexes = self.borderVisibleHexes local borderVisibleHexes = self.lastBorderVisibleHexes -- reusing to avoid garbage ZO_ClearNumericallyIndexedTable(borderVisibleHexes) local canPreviewAction = CanPreviewScryingAction(self:GetScryingActionParameters()) == SCRYING_ACTIVE_SKILL_USE_RESULT_SUCCESS for _, hex in ipairs(lastBorderVisibleHexes) do hex:SetBorderType(ZO_SCRYING_HEX_BORDER_NONE) end SCRYING_HEX_ANIMATION_PROVIDER:ReleaseBorderHexIslands() if self.targetHex then self.targetHex:SetBorderType(ZO_SCRYING_HEX_BORDER_NONE) if canPreviewAction then -- Highlight affected hexes for index = 1, GetNumAffectedScryingHexes(self:GetScryingActionParameters()) do local hex = self.hexList[GetAffectedScryingHex(index, self:GetScryingActionParameters())] hex:SetBorderType(ZO_SCRYING_HEX_BORDER_AFFECTED) table.insert(borderVisibleHexes, hex) end else -- only highlight target hex self.targetHex:SetBorderType(ZO_SCRYING_HEX_BORDER_TARGETED) table.insert(borderVisibleHexes, self.targetHex) end table.sort(borderVisibleHexes, SortByHexIndex) -- find islands local islands = {} local hexToIslandMap = {} for _, hex in ipairs(borderVisibleHexes) do local island = FindAdjacentIslandForHex(self, hexToIslandMap, hex) if not island then island = { firstHex = hex } table.insert(islands, island) end hexToIslandMap[hex] = island end -- Add outlines to all affected hexes for _, island in ipairs(islands) do local hex = island.firstHex SCRYING_HEX_ANIMATION_PROVIDER:TraceBorderHexIsland(hex) end -- play sound to suggest that the previewed action has changed if self.playActionStateSound and self.targetHex:GetHexType() ~= SCRYING_HEX_TYPE_OWNED then if not canPreviewAction then PlaySound(SOUNDS.SCRYING_TARGET_UNAFFECTED_HEX) elseif HaveBorderVisibleHexesChanged(lastBorderVisibleHexes, borderVisibleHexes) then if SCRYING:GetCurrentSkill() == SCRYING_ACTIVE_SKILL_BOMB then PlaySound(SOUNDS.SCRYING_TARGET_HEX_WITH_BOMB) elseif SCRYING:GetCurrentSkill() == SCRYING_ACTIVE_SKILL_LINE then PlaySound(SOUNDS.SCRYING_TARGET_HEX_WITH_LINE) else PlaySound(SOUNDS.SCRYING_TARGET_AFFECTED_HEX) end end end end self.modalCursor:UpdateModalAction() self.simulatedActionStateDirty = false self.playActionStateSound = false self.lastBorderVisibleHexes = lastBorderVisibleHexes self.borderVisibleHexes = borderVisibleHexes end end function ZO_ScryingBoard:OnUpdate() -- update hex distances if not IsInGamepadPreferredMode() then local cursorX, cursorY = GetUIMousePosition() for _, hex in ipairs(self.hexList) do hex:RefreshDistanceFromCursor(cursorX, cursorY) end self.darknessDisabled = false elseif not self.darknessDisabled then for _, hex in ipairs(self.hexList) do hex:DisableDarknessEffect() end self.darknessDisabled = true end -- update simulated action if self.simulatedActionStateDirty then self:RefreshSimulatedAction() end end function ZO_ScryingBoard:ChangeTargetHex(newTargetHex, shouldSupressSound) if self.targetHex ~= newTargetHex then self.targetHex = newTargetHex local shouldPlaySound = not shouldSupressSound self:DirtySimulatedActionState(shouldPlaySound) end end function ZO_ScryingBoard:PerformActionOnHex(hex) self.targetHex = hex -- skip refreshing cells/audio self:PerformActionOnTargetHex() end function ZO_ScryingBoard:PerformActionOnTargetHex() local targetHex = self.targetHex if not targetHex then return end local currentSkill = SCRYING:GetCurrentSkill() if currentSkill == SCRYING_ACTIVE_SKILL_LINE and self.lineStartHex == nil then local result = CanPreviewScryingAction(self:GetScryingActionParameters()) if result ~= SCRYING_ACTIVE_SKILL_USE_RESULT_SUCCESS then DisplayScryingActiveSkillUseResult(result) return end self.lineStartHex = targetHex PlaySound(SOUNDS.SCRYING_START_LINE) elseif currentSkill == SCRYING_ACTIVE_SKILL_LINE and self.lineStartHex == targetHex then self:CancelLineAction() return else local result = CanPerformScryingAction(self:GetScryingActionParameters()) if result ~= SCRYING_ACTIVE_SKILL_USE_RESULT_SUCCESS then DisplayScryingActiveSkillUseResult(result) return end HandleScryingAction(self:GetScryingActionParameters()) self:PlayHexChangeAnimations() local NO_SOUND = true SCRYING:TrySetCurrentSkill(SCRYING_ACTIVE_SKILL_NORMAL, NO_SOUND) end self:DirtySimulatedActionState() SCRYING:RefreshNormalActionMeter() SCRYING:RefreshSpecialActionMeter() SCRYING:RefreshActionButtons() SCRYING:RefreshEyeAnimations() if GetNumScryingGoalsAchieved() > 0 then TUTORIAL_MANAGER:ShowTutorial(TUTORIAL_TRIGGER_ANTIQUITY_SCRYING_GOAL_CAPTURED) end end do local MIN_SCALE = 0.75 local MIN_ALPHA = 0.4 function ZO_ScryingBoard:OnEndOfGameTimelineUpdate(animation, progress) local scale = zo_lerp(1, MIN_SCALE, progress) local alpha = zo_lerp(1, MIN_ALPHA, progress) for _, hex in ipairs(self.hexList) do if hex:GetHexType() ~= SCRYING_HEX_TYPE_OWNED then hex:SetScale(scale) hex:SetAlpha(alpha) end end end end function ZO_ScryingBoard:GetLineStartHex() return self.lineStartHex end function ZO_ScryingBoard:HasInProgressLineAction() return self.lineStartHex ~= nil end function ZO_ScryingBoard:CancelLineAction() if self:HasInProgressLineAction() then self.lineStartHex = nil self:DirtySimulatedActionState() end end function ZO_ScryingBoard:OnChangeCurrentSkill() self:CancelLineAction() self:DirtySimulatedActionState() end function ZO_ScryingBoard:GetControl() return self.boardControl end ---------------------- -- Scrying Minigame -- ---------------------- ZO_Scrying = ZO_InitializingObject:Subclass() function ZO_Scrying:Initialize(control) self.control = control self.board = ZO_ScryingBoard:New(control:GetNamedChild("Game")) self.isScryingReady = false self.waitingForScryingResult = false self.waitingToCompleteScrying = false self.lastScryingResult = nil local frameElements = control:GetNamedChild("GameFrame") self.normalActionMeter = ZO_ScryingNormalActionsMeter:New(frameElements:GetNamedChild("NormalActionMeter")) self.specialActionMeter = ZO_ScryingSpecialActionsMeter:New(frameElements:GetNamedChild("SpecialActionMeter")) self.actionButtons = { ZO_ScryingActionButton:New(frameElements:GetNamedChild("NormalAction"), SCRYING_ACTIVE_SKILL_NORMAL, "SCRYING_TOGGLE_NORMAL_ACTION"), ZO_ScryingActionButton:New(frameElements:GetNamedChild("SpecialAction1"), SCRYING_ACTIVE_SKILL_BOMB, "SCRYING_TOGGLE_SPECIAL_ACTION_1"), ZO_ScryingActionButton:New(frameElements:GetNamedChild("SpecialAction2"), SCRYING_ACTIVE_SKILL_CO_OPT, "SCRYING_TOGGLE_SPECIAL_ACTION_2"), ZO_ScryingActionButton:New(frameElements:GetNamedChild("SpecialAction3"), SCRYING_ACTIVE_SKILL_LINE, "SCRYING_TOGGLE_SPECIAL_ACTION_3"), } self.moreInfoButton = frameElements:GetNamedChild("MoreInfoKeybindButton") self.moreInfoButton:SetText(GetString(SI_SCRYING_MORE_INFO)) local DEFAULT_SHOW_UNBOUND = nil local DEFAULT_GAMEPAD_PREFERRED_KEYBIND = nil local DEFAULT_ALWAYS_PREFER_GAMEPAD_MODE = nil local SHOW_AS_HOLD = true self.moreInfoButton:SetKeybind("SCRYING_MORE_INFO", DEFAULT_SHOW_UNBOUND, DEFAULT_GAMEPAD_PREFERRED_KEYBIND, DEFAULT_ALWAYS_PREFER_GAMEPAD_MODE, SHOW_AS_HOLD) ApplyTemplateToControl(self.moreInfoButton, "ZO_KeybindButton_Gamepad_Template") SCRYING_SCENE = ZO_RemoteScene:New("Scrying", SCENE_MANAGER) SCRYING_SCENE:RegisterCallback("StateChange", function(oldState, newState) local NO_SOUND = true local ANIMATE_INSTANTLY = true if newState == SCENE_SHOWING then self.isScryingReady = false self.startedOutro = false self.startedEndOfGame = false self:TrySetCurrentSkill(SCRYING_ACTIVE_SKILL_NORMAL, NO_SOUND) self.board:OnShowing() self.normalActionMeter:OnShowing() self:RefreshNormalActionMeter(ANIMATE_INSTANTLY) self:RefreshSpecialActionMeter(ANIMATE_INSTANTLY) self:RefreshActionButtons() self:RefreshEyeAnimations() self:RefreshMoreInfoButton() self:RefreshInputState() PlaySound(SOUNDS.SCRYING_START_INTRO) elseif newState == SCENE_HIDING then --clear the current tutorial when hiding so we don't push an extra action layer self:RefreshInputState() ZO_Dialogs_ReleaseAllDialogsOfName("CONFIRM_EXIT_SCRYING") elseif newState == SCENE_HIDDEN then self.board:OnHidden() -- on the off chance that we are asked to hide while the outro is still in progress, clear out all that state self:ResetScryingPostGameElements() end end) control:RegisterForEvent(EVENT_SCRYING_INTRO_COMPLETE, function() self.isScryingReady = true self:TryTriggerInitialTutorials() end) control:RegisterForEvent(EVENT_SCRYING_OUTRO_COMPLETE, function() self.waitingForOutro = false self:TryCompleteScrying() end) control:RegisterForEvent(EVENT_GAMEPAD_PREFERRED_MODE_CHANGED, function() if SCRYING_SCENE:IsShowing() then self:RefreshInputState() self:RefreshActionButtons() self:RefreshMoreInfoButton() end end) control:RegisterForEvent(EVENT_START_SCRYING, function() SCENE_MANAGER:Show("Scrying") end) control:RegisterForEvent(EVENT_REQUEST_SCRYING_EXIT, function() self:HideMoreInfo() ZO_Dialogs_ShowPlatformDialog("CONFIRM_EXIT_SCRYING") end) control:RegisterForEvent(EVENT_SCRYING_GAME_OVER, function() self.waitingForScryingResult = true self.waitingToCompleteScrying = true self:TryCompleteScrying() end) control:RegisterForEvent(EVENT_ANTIQUITY_SCRYING_RESULT, function(_, result) self.lastScryingResult = result if self.waitingForScryingResult then self.waitingForScryingResult = false self:TryCompleteScrying() end end) local function RefreshInputState() self:RefreshInputState() end ZO_HELP_OVERLAY_SYNC_OBJECT:SetHandler("OnShown", RefreshInputState, "Scrying") ZO_HELP_OVERLAY_SYNC_OBJECT:SetHandler("OnHidden", RefreshInputState, "Scrying") ZO_DIALOG_SYNC_OBJECT:SetHandler("OnShown", RefreshInputState, "Scrying") ZO_DIALOG_SYNC_OBJECT:SetHandler("OnHidden", function() self:RefreshInputState() if self.waitingOnDialogToEndGame then self:TryCompleteScrying() self.waitingOnDialogToEndGame = false end end, "Scrying") SCRYING_HEX_ANIMATION_PROVIDER:RegisterCallback("BlockingAnimationsCompleted", function() self:TryCompleteScrying() end) self.endOfGameTimeline = ANIMATION_MANAGER:CreateTimelineFromVirtual("ZO_ScryingHex_EndOfGame") self.endOfGameTimeline:GetFirstAnimation():SetUpdateFunction(function(...) self.board:OnEndOfGameTimelineUpdate(...) self.normalActionMeter:OnEndOfGameTimelineUpdate(...) end) self.platformStyle = ZO_PlatformStyle:New(function(style) self:ApplyPlatformStyle(style) end) end function ZO_Scrying:CanFireActions() return self.isScryingReady end function ZO_Scrying:RefreshInputState() local allowPlayerInput = SCRYING_SCENE:IsShowing() and not ZO_HELP_OVERLAY_SYNC_OBJECT:IsShown() and not ZO_DIALOG_SYNC_OBJECT:IsShown() if self.isPlayerInputEnabled ~= allowPlayerInput then if allowPlayerInput then PushActionLayerByName("ScryingActions") self.isPlayerInputEnabled = true else RemoveActionLayerByName("ScryingActions") self.isPlayerInputEnabled = false end end local useVirtualCursor = allowPlayerInput and IsInGamepadPreferredMode() if self.board:IsVirtualCursorEnabled() ~= useVirtualCursor then if useVirtualCursor then self.board:EnableVirtualCursor() else -- with input mode switching we can get into a situation where the hide more info release callback never gets triggered -- manually hide here to deal with that self:HideMoreInfo() self.board:DisableVirtualCursor() end end end function ZO_Scrying:RefreshMoreInfoButton() self.moreInfoButton:SetHidden(not IsInGamepadPreferredMode()) end function ZO_Scrying:ApplyPlatformStyle(style) self.normalActionMeter:UpdatePlatformStyle() for _, actionButton in ipairs(self.actionButtons) do actionButton:UpdatePlatformStyle() end end function ZO_Scrying:IsPlayerInputEnabled() return self.isPlayerInputEnabled end do local ABILITY_TUTORIALS = { [SCRYING_ACTIVE_SKILL_BOMB] = TUTORIAL_TRIGGER_ANTIQUITY_SCRYING_UNITE_UNLOCKED, [SCRYING_ACTIVE_SKILL_CO_OPT] = TUTORIAL_TRIGGER_ANTIQUITY_SCRYING_CO_OPT_UNLOCKED, [SCRYING_ACTIVE_SKILL_LINE] = TUTORIAL_TRIGGER_ANTIQUITY_SCRYING_EXTEND_UNLOCKED, } local ABILITY_UPGRADED_TUTORIALS = { [SCRYING_ACTIVE_SKILL_BOMB] = TUTORIAL_TRIGGER_ANTIQUITY_SCRYING_UNITE_UPGRADED, [SCRYING_ACTIVE_SKILL_CO_OPT] = TUTORIAL_TRIGGER_ANTIQUITY_SCRYING_CO_OPT_UPGRADED, [SCRYING_ACTIVE_SKILL_LINE] = TUTORIAL_TRIGGER_ANTIQUITY_SCRYING_EXTEND_UPGRADED, } function ZO_Scrying:TryTriggerInitialTutorials() TUTORIAL_MANAGER:ShowTutorial(TUTORIAL_TRIGGER_ANTIQUITY_SCRYING_OPENED) for skill = SCRYING_ACTIVE_SKILL_ITERATION_BEGIN, SCRYING_ACTIVE_SKILL_ITERATION_END do if IsScryingActiveSkillUnlocked(skill) then local abilityTutorial = ABILITY_TUTORIALS[skill] if abilityTutorial then TUTORIAL_MANAGER:ShowTutorial(abilityTutorial) end if IsScryingActiveSkillUpgraded(skill) then local upgradedAbilityTutorial = ABILITY_UPGRADED_TUTORIALS[skill] if upgradedAbilityTutorial then TUTORIAL_MANAGER:ShowTutorial(upgradedAbilityTutorial) end end end end end end function ZO_Scrying:ResetScryingPostGameElements() self.waitingToCompleteScrying = false self.waitingForScryingResult = false self:CancelEndOfGame() self.startedOutro = false self.waitingForOutro = false end function ZO_Scrying:TryCompleteScrying() if not self.waitingToCompleteScrying then return end if self.waitingForScryingResult then -- wait until server provides a scrying result return end if SCRYING_HEX_ANIMATION_PROVIDER:AreBlockingAnimationsPlaying() then -- wait until existing animations are complete return end if ZO_DIALOG_SYNC_OBJECT:IsShown() then -- Wait until the user closes the tutorial self.waitingOnDialogToEndGame = true return end if not self.startedEndOfGame then self:StartEndOfGame() end if self.waitingForEndOfGame then return end if not self.startedOutro then self.startedOutro = true StartScryingOutro() self.waitingForOutro = true end if self.waitingForOutro then return end self.waitingToCompleteScrying = false SCENE_MANAGER:Hide("Scrying") if self.lastScryingResult == ANTIQUITY_SCRYING_RESULT_SUCCESS then RequestRevealAntiquityDigSitesOnMap(GetScryingCurrentAntiquityId()) end end function ZO_Scrying:GetScryingBoard() return self.board end function ZO_Scrying:TrySetCurrentSkill(scryingActiveSkill, shouldSupressSound) if self.currentActiveSkill ~= scryingActiveSkill then local useResult = IsScryingActiveSkillUsable(scryingActiveSkill) if useResult ~= SCRYING_ACTIVE_SKILL_USE_RESULT_SUCCESS then DisplayScryingActiveSkillUseResult(useResult) return end self.currentActiveSkill = scryingActiveSkill if not shouldSupressSound then PlaySound(SOUNDS.SCRYING_CHOOSE_SKILL) end self.board:OnChangeCurrentSkill() self:RefreshActionButtons() end end function ZO_Scrying:TryToggleCurrentSkill(scryingActiveSkill, shouldSupressSound) if not self:CanFireActions() then return end if self.currentActiveSkill == scryingActiveSkill then -- toggle back to normal mode. if we were going to normal mode anwyays, this has no effect scryingActiveSkill = SCRYING_ACTIVE_SKILL_NORMAL end self:TrySetCurrentSkill(scryingActiveSkill, shouldSupressSound) end function ZO_Scrying:GetCurrentSkill() return self.currentActiveSkill end function ZO_Scrying:PerformAction() if not self:CanFireActions() then return end self.board:PerformActionOnTargetHex() end function ZO_Scrying:RefreshNormalActionMeter(shouldAnimateInstantly) self.normalActionMeter:SmoothTransition(GetNumScryingNormalActions(), GetNumScryingNormalActionsLimit(), shouldAnimateInstantly) end function ZO_Scrying:RefreshSpecialActionMeter(shouldAnimateInstantly) local specialActions, partialSpecialActions = GetNumScryingSpecialActions() local shouldFillPartialGem = partialSpecialActions > 0 self.specialActionMeter:SmoothTransition(specialActions, shouldFillPartialGem, shouldAnimateInstantly) end function ZO_Scrying:RefreshActionButtons() for _, actionButton in ipairs(self.actionButtons) do actionButton:Refresh() end end function ZO_Scrying:RefreshEyeAnimations() if not self.eyeBackdropLoop then local backgroundElements = self.control:GetNamedChild("GameEyeBackground") self.eyeBackdropLoop = ANIMATION_MANAGER:CreateTimelineFromVirtual("ZO_ScryingBackdrop_TextureScroll", backgroundElements:GetNamedChild("Scroll")) self.eyeBackdropLoop:PlayFromStart() self.eyeBackdropLeftSpinLoop = ANIMATION_MANAGER:CreateTimelineFromVirtual("ZO_ScryingBackdrop_SpinCW", backgroundElements:GetNamedChild("LeftSpin")) self.eyeBackdropLeftSpinLoop:PlayFromStart() self.eyeBackdropRightSpinLoop = ANIMATION_MANAGER:CreateTimelineFromVirtual("ZO_ScryingBackdrop_SpinCW", backgroundElements:GetNamedChild("RightSpin")) self.eyeBackdropRightSpinLoop:PlayFromEnd() -- reverse for counterclockwise rotation local frameElements = self.control:GetNamedChild("GameFrame") self.eyeFrameGlowLoop = ANIMATION_MANAGER:CreateTimelineFromVirtual("ZO_ScryingFrame_GlowLoop", frameElements:GetNamedChild("Glow")) self.eyeFrameGlowLoop:PlayFromStart() end end function ZO_Scrying:OnUnhandledMouseUp() self.board:CancelLineAction() end function ZO_Scrying:OnUnhandledReceiveDrag() self.board:CancelLineAction() end function ZO_Scrying:TryCancel() if not self:CanFireActions() then return end if self.board:HasInProgressLineAction() then self.board:CancelLineAction() else SCENE_MANAGER:Hide("Scrying") end end function ZO_Scrying:StartEndOfGame() self.startedEndOfGame = true self.completedEndOfGameCallLaterId = zo_callLater(function() self.completedEndOfGameCallLaterId = nil self.waitingForEndOfGame = false self:TryCompleteScrying() end, 2000) self.endOfGameTimeline:PlayFromStart() PlaySound(SOUNDS.SCRYING_START_END_OF_GAME) self.waitingForEndOfGame = true end function ZO_Scrying:CancelEndOfGame() if self.completedEndOfGameCallLaterId then zo_removeCallLater(self.completedEndOfGameCallLaterId) self.completedEndOfGameCallLaterId = nil self.endOfGameTimeline:Stop() end end function ZO_Scrying:ShowMoreInfo() if not self:CanFireActions() then return end local abilityId = GetScryingActiveSkillAbilityId(self:GetCurrentSkill()) GAMEPAD_TOOLTIPS:LayoutSimpleAbility(GAMEPAD_RIGHT_TOOLTIP, abilityId) end function ZO_Scrying:HideMoreInfo() GAMEPAD_TOOLTIPS:ClearTooltip(GAMEPAD_RIGHT_TOOLTIP) end do local ACCEPT = true local REJECT = false ZO_Dialogs_RegisterCustomDialog("CONFIRM_EXIT_SCRYING", { mustChoose = true, gamepadInfo = { dialogType = GAMEPAD_DIALOGS.BASIC, }, title = { text = SI_SCRYING_CONFIRM_EXIT_DIALOG_TITLE }, mainText = { text = SI_SCRYING_CONFIRM_EXIT_DIALOG_DESCRIPTION }, setup = function() SCRYING:RefreshInputState() end, finishedCallback = function() SCRYING:RefreshInputState() end, buttons = { { text = SI_DIALOG_ACCEPT, callback = function() RequestRespondToScryingExit(ACCEPT) end }, { text = SI_DIALOG_DECLINE, callback = function() RequestRespondToScryingExit(REJECT) end }, } }) end -- XML function ZO_Scrying_OnInitialized(control) SCRYING = ZO_Scrying:New(control) end do local UNIT_HEX_RADIUS_X = 1 -- the rightmost point of the hexagon is already at 1 local UNIT_HEX_RADIUS_Y = 2 / math.sqrt(3) -- AKA 1 / sin(60deg), this will take the top/bottom of the hexagon at sin(60deg) and stretch it up to 1 function ZO_Scrying_CalculateHexPoint(angleDegrees) local angleRadians = math.rad(angleDegrees) -- Step 1. multiply by the unit hex radius. This will create a "circle" with value range [-1, 1] local scaledX = math.cos(angleRadians) * UNIT_HEX_RADIUS_X local scaledY = math.sin(angleRadians) * UNIT_HEX_RADIUS_Y -- Step 2. convert [-1, 1] values from unit circle to [0, 1] local normalizedX = (scaledX + 1) * 0.5 local normalizedY = (scaledY + 1) * 0.5 internalassert(normalizedX >= 0 and normalizedX <= 1) internalassert(normalizedY >= 0 and normalizedY <= 1) return normalizedX, normalizedY end function ZO_ScryingHexPolygon_OnInitialized(polygon) -- Create hexagon from points for angleDegrees = 0, 300, 60 do polygon:AddPoint(ZO_Scrying_CalculateHexPoint(angleDegrees)) end end function ZO_FramePolygon_OnInitialized(polygon) -- This is a shape that outlines the "window" into the antiquarian's eye. It traces the edge of the window fairly precisely. polygon:AddPoint(0.819490, 0.501118) polygon:AddPoint(0.777049, 0.633015) polygon:AddPoint(0.729892, 0.735502) polygon:AddPoint(0.676250, 0.820165) polygon:AddPoint(0.623788, 0.870963) polygon:AddPoint(0.572504, 0.872745) polygon:AddPoint(0.548336, 0.861160) polygon:AddPoint(0.532421, 0.882548) polygon:AddPoint(0.524758, 0.869180) polygon:AddPoint(0.524168, 0.809471) polygon:AddPoint(0.511200, 0.765802) polygon:AddPoint(0.500000, 0.745305) polygon:AddPoint(0.481727, 0.785408) polygon:AddPoint(0.475832, 0.814818) polygon:AddPoint(0.476421, 0.871854) polygon:AddPoint(0.468758, 0.883440) polygon:AddPoint(0.451074, 0.860269) polygon:AddPoint(0.429854, 0.872745) polygon:AddPoint(0.377391, 0.875419) polygon:AddPoint(0.320802, 0.815709) polygon:AddPoint(0.269519, 0.739958) polygon:AddPoint(0.228256, 0.650838) polygon:AddPoint(0.201730, 0.577761) polygon:AddPoint(0.179920, 0.502901) polygon:AddPoint(0.212930, 0.386154) polygon:AddPoint(0.244172, 0.312185) polygon:AddPoint(0.306066, 0.200786) polygon:AddPoint(0.372675, 0.125926) polygon:AddPoint(0.477011, 0.125035) polygon:AddPoint(0.500589, 0.160683) polygon:AddPoint(0.521221, 0.127709) polygon:AddPoint(0.624967, 0.127709) polygon:AddPoint(0.676250, 0.181180) polygon:AddPoint(0.731071, 0.268517) polygon:AddPoint(0.782943, 0.379916) end function ZO_HexMaskPolygon_OnInitialized(polygon) -- defines a smaller mask that avoids the extra lattice work at the top and bottom of the window polygon:AddPoint(0.785301, 0.413781) polygon:AddPoint(0.792375, 0.506465) polygon:AddPoint(0.768796, 0.623211) polygon:AddPoint(0.723997, 0.732828) polygon:AddPoint(0.675661, 0.789864) polygon:AddPoint(0.617893, 0.816600) polygon:AddPoint(0.558357, 0.791647) polygon:AddPoint(0.540673, 0.805906) polygon:AddPoint(0.521221, 0.786300) polygon:AddPoint(0.501768, 0.742631) polygon:AddPoint(0.477600, 0.786300) polygon:AddPoint(0.458737, 0.803232) polygon:AddPoint(0.439285, 0.789864) polygon:AddPoint(0.380338, 0.811253) polygon:AddPoint(0.317266, 0.784517) polygon:AddPoint(0.278950, 0.736393) polygon:AddPoint(0.234151, 0.639253) polygon:AddPoint(0.220593, 0.577761) polygon:AddPoint(0.212341, 0.450320) polygon:AddPoint(0.222362, 0.388828) polygon:AddPoint(0.247119, 0.316641) polygon:AddPoint(0.297224, 0.224176) polygon:AddPoint(0.329055, 0.223285) polygon:AddPoint(0.415117, 0.216155) polygon:AddPoint(0.434569, 0.206352) polygon:AddPoint(0.456380, 0.231306) polygon:AddPoint(0.476421, 0.216155) polygon:AddPoint(0.500000, 0.250021) polygon:AddPoint(0.522400, 0.215264) polygon:AddPoint(0.544799, 0.232197) polygon:AddPoint(0.565431, 0.207243) polygon:AddPoint(0.600799, 0.219720) polygon:AddPoint(0.695703, 0.221502) polygon:AddPoint(0.732839, 0.284558) end end function ZO_Scrying_FadeInCursor_OnInitialized(cursor) cursor.fadeInTimeline = ANIMATION_MANAGER:CreateTimelineFromVirtual("ZO_ScryingModalCursor_FadeIn", cursor) cursor.translationAnimation = cursor.fadeInTimeline:GetFirstAnimation() -- every control that inherits from this should set the translationAnimation's offsets end function ZO_Scrying_FadeInCursor_OnEffectivelyShown(cursor) cursor:SetAlpha(0) cursor.fadeInTimeline:PlayFromStart() end