-- Bundled by luabundle {"version":"1.6.0"}
local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)
	local loadingPlaceholder = {[{}] = true}

	local register
	local modules = {}

	local require
	local loaded = {}

	register = function(name, body)
		if not modules[name] then
			modules[name] = body
		end
	end

	require = function(name)
		local loadedModule = loaded[name]

		if loadedModule then
			if loadedModule == loadingPlaceholder then
				return nil
			end
		else
			if not modules[name] then
				if not superRequire then
					local identifier = type(name) == 'string' and '\"' .. name .. '\"' or tostring(name)
					error('Tried to require ' .. identifier .. ', but no such module has been registered')
				else
					return superRequire(name)
				end
			end

			loaded[name] = loadingPlaceholder
			loadedModule = modules[name](require, loaded, register, modules)
			loaded[name] = loadedModule
		end

		return loadedModule
	end

	return require, loaded, register, modules
end)(nil)
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
---------------------------------------------------------
-- specific setup (different for each playmat)
---------------------------------------------------------

PLAYER_COLOR          = "Green"
PLAY_ZONE_POSITION    = { x = -26.5, y = 4, z = 26.5 }
PLAY_ZONE_SCALE       = { x = 32, y = 5, z = 12 }
TRASHCAN_GUID         = "5f896a"
STAT_TRACKER_GUID     = "af7ed7"
RESOURCE_COUNTER_GUID = "cd15ac"
CLUE_COUNTER_GUID     = "032300"
CLUE_CLICKER_GUID     = "891403"

require("playermat/Playmat")
end)
__bundle_register("playermat/Playmat", function(require, _LOADED, __bundle_register, __bundle_modules)
local tokenManager = require("core/token/TokenManager")
local tokenChecker = require("core/token/TokenChecker")

-- set true to enable debug logging and show Physics.cast()
local DEBUG = false

-- we use this to turn off collision handling until onLoad() is complete
local COLLISION_ENABLED = false

-- position offsets relative to mat [x, y, z]
local DRAWN_ENCOUNTER_CARD_OFFSET = {1.365, 0.5, -0.635}
local DRAWN_CHAOS_TOKEN_OFFSET = {-1.55, 0.5, -0.58}
local DISCARD_BUTTON_OFFSETS = {
  {-1.38, 0.1, -0.94},
  {-0.92, 0.1, -0.94},
  {-0.46, 0.1, -0.94},
  {0.00, 0.1, -0.94},
  {0.46, 0.1, -0.94},
  {0.92, 0.1, -0.94}
}

local MAIN_PLAY_AREA = {
  upperLeft = {
    x = 1.98,
    z = 0.736,
  },
  lowerRight = {
    x = -0.79,
    z = -0.39,
  }
}
local INVESTIGATOR_AREA = {
  upperLeft = {
    x = -1.084,
    z = 0.06517
  },
  lowerRight = {
    x = -1.258,
    z = -0.0805,
  }
}
local THREAT_AREA = {
  upperLeft = {
    x = 1.53,
    z = -0.34
  },
  lowerRight = {
    x = -1.13,
    z = -0.92,
  }
}

local DRAW_DECK_POSITION    = { x = -1.82, y = 1, z = 0 }
local DISCARD_PILE_POSITION = { x = -1.82, y = 1.5, z = 0.61 }
local PLAY_ZONE_ROTATION = self.getRotation()

local TRASHCAN
local STAT_TRACKER
local RESOURCE_COUNTER

-- global variable so it can be reset by the Clean Up Helper
activeInvestigatorId = "00000"
local isDrawButtonVisible = false

function onSave()
  return JSON.encode({
    zoneID = zoneID,
    playerColor = PLAYER_COLOR,
    activeInvestigatorId = activeInvestigatorId,
    isDrawButtonVisible = isDrawButtonVisible
  })
end

function onLoad(save_state)
  self.interactable = DEBUG

  TRASHCAN = getObjectFromGUID(TRASHCAN_GUID)
  STAT_TRACKER = getObjectFromGUID(STAT_TRACKER_GUID)
  RESOURCE_COUNTER = getObjectFromGUID(RESOURCE_COUNTER_GUID)

  for i = 1, 6 do
    makeDiscardButton(DISCARD_BUTTON_OFFSETS[i], {-3.85, 3, 10.38}, i)
  end

  self.createButton({
    click_function = "drawEncountercard",
    function_owner = self,
    position = {-1.84, 0, -0.65},
    rotation = {0, 80, 0},
    width = 265,
    height = 190
  })

  self.createButton({
    click_function = "drawChaostokenButton",
    function_owner = self,
    position = {1.85, 0, -0.74},
    rotation = {0, -45, 0},
    width = 135,
    height = 135
  })

  self.createButton({
    label = "Upkeep",
    click_function = "doUpkeep",
    function_owner = self,
    position = {1.84, 0.1, -0.44},
    scale = {0.12, 0.12, 0.12},
    width = 800,
    height = 280,
    font_size = 180
  })

  local state = JSON.decode(save_state)
  if state ~= nil then
    zoneID = state.zoneID
    PLAYER_COLOR = state.playerColor
    activeInvestigatorId = state.activeInvestigatorId
    isDrawButtonVisible = state.isDrawButtonVisible
  end

  showDrawButton(isDrawButtonVisible)

  if getObjectFromGUID(zoneID) == nil then spawnDeckZone() end
  COLLISION_ENABLED = true
end

---------------------------------------------------------
-- utility functions
---------------------------------------------------------

function log(message)
  if DEBUG then print(message) end
end

-- send messages to player who clicked button if no seated player found
function setMessageColor(color)
  messageColor = Player[PLAYER_COLOR].seated and PLAYER_COLOR or color
end

function spawnDeckZone()
  spawnObject({
    position = self.positionToWorld({-1.4, 0, 0.3 }),
    scale = {3, 5, 8 },
    type = 'ScriptingTrigger',
    callback = function (zone) zoneID = zone.getGUID() end,
    callback_owner = self,
    rotation = self.getRotation()
  })
end

function searchArea(origin, size)
  return Physics.cast({
    origin       = origin,
    direction    = {0, 1, 0},
    orientation  = PLAY_ZONE_ROTATION,
    type         = 3,
    size         = size,
    max_distance = 1,
    debug        = DEBUG
  })
end

function doNotReady(card) return card.getVar("do_not_ready") or false end

---------------------------------------------------------
-- Discard buttons
---------------------------------------------------------

-- builds a function that discards things in searchPosition to discardPosition
-- stuff on the card/deck will be put into the local trashcan
function makeDiscardHandlerFor(searchPosition, discardPosition)
  return function ()
    for _, hitObj in ipairs(findObjectsAtPosition(searchPosition)) do
      local obj = hitObj.hit_object
      if obj.tag == "Deck" or obj.tag == "Card" then
        if obj.hasTag("PlayerCard") then
          obj.setPositionSmooth(self.positionToWorld(DISCARD_PILE_POSITION), false, true)
          obj.setRotation(PLAY_ZONE_ROTATION)
        else
          obj.setPositionSmooth(discardPosition, false, true)
          obj.setRotation({0, -90, 0})
        end
      -- put chaos tokens back into bag (e.g. Unrelenting)
      elseif tokenChecker.isChaosToken(obj) then
        local chaosBag = Global.call("findChaosBag")
        chaosBag.putObject(obj)
      -- don't touch the table or this playmat itself
      elseif obj.guid ~= "4ee1f2" and obj ~= self then
        TRASHCAN.putObject(obj)
      end
    end
  end
end

-- build a discard button to discard from searchPosition to discardPosition (number must be unique)
function makeDiscardButton(position, discardPosition, number)
  local searchPosition = {-position[1], position[2], position[3] + 0.32}
  local handler = makeDiscardHandlerFor(searchPosition, discardPosition)
  local handlerName = 'handler' .. number
  self.setVar(handlerName, handler)
  self.createButton({
    label = "Discard",
    click_function = handlerName,
    function_owner = self,
    position = {position[1], position[2], position[3] + 0.6},
    scale = {0.12, 0.12, 0.12},
    width = 900,
    height = 350,
    font_size = 220
  })
end

function findObjectsAtPosition(localPos)
  return Physics.cast({
    origin       = self.positionToWorld(localPos),
    direction    = {0, 1, 0},
    orientation  = {0, PLAY_ZONE_ROTATION.y + 90, 0},
    type         = 3,
    size         = {3.2, 1, 2},
    max_distance = 0,
    debug        = DEBUG
  })
end

---------------------------------------------------------
-- Upkeep button
---------------------------------------------------------

function doUpkeep(_, color, alt_click)
  setMessageColor(color)

  -- right-click binds to new player color
  if alt_click then
    PLAYER_COLOR = color
    printToColor("Upkeep button bound to " .. color, color)
    return
  end

  local forcedLearning = false

  -- unexhaust cards in play zone, flip action tokens and find forcedLearning
  for _, v in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
    local obj = v.hit_object
    if obj.getDescription() == "Action Token" and obj.is_face_down then
      obj.flip()
    elseif obj.tag == "Card" and not obj.is_face_down and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then
      local cardMetadata = JSON.decode(obj.getGMNotes()) or {}
      if not doNotReady(obj) then
        obj.setRotation(PLAY_ZONE_ROTATION)
      end
      if cardMetadata.id == "08031" then
        forcedLearning = true
      end
      if cardMetadata.uses ~= nil then
        -- check for cards with 'replenish' in their metadata
        local count
        local replenish
        -- Uses structure underwent a breaking change in 2.4.0, have to check to see if this is
        -- a single entry or an array.  TODO: Clean this up when 2.4.0 has been out long
        -- enough that saved decks don't have old data
        if cardMetadata.uses.count ~= nil then
          count = cardMetadata.uses.count
          replenish = cardMetadata.uses.replenish
        else
          count = cardMetadata.uses[1].count
          replenish = cardMetadata.uses[1].replenish
        end
        if count and replenish then
          replenishTokens(obj, count, replenish)
        end
      end
    end
  end

  -- flip investigator mini-card and summoned servitor mini-card
  -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)
  if activeInvestigatorId ~= nil then
    local miniId = string.match(activeInvestigatorId, ".....") .. "-m"
    for _, obj in ipairs(getObjects()) do
      if obj.tag == "Card" and obj.is_face_down then
        local notes = JSON.decode(obj.getGMNotes())
        if notes ~= nil and notes.type == "Minicard" and (notes.id == miniId or notes.id == "09080-m") then
          obj.flip()
        end
      end
    end
  end

  -- gain a resource
  RESOURCE_COUNTER.call("addOrSubtract")

  -- gain an additional resource for Jenny Barnes
  if string.match(activeInvestigatorId, "%d%d%d%d%d") == "02003" then
    RESOURCE_COUNTER.call("addOrSubtract")
    printToColor("Gaining 2 resources (Jenny)", messageColor)
  end

  -- draw a card (with handling for Patrice and Forced Learning)
  if activeInvestigatorId == "06005" then
    if forcedLearning then
      printToColor("Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'? Choose which draw replacement effect takes priority and draw cards accordingly.", messageColor)
    else
      local handSize = #Player[PLAYER_COLOR].getHandObjects()
      if handSize < 5 then
        local cardsToDraw = 5 - handSize
        printToColor("Drawing " .. cardsToDraw .. " cards (Patrice)", messageColor)
        drawCardsWithReshuffle(cardsToDraw)
      end
    end
  elseif forcedLearning then
    printToColor("Drawing 2 cards, discard 1 (Forced Learning)", messageColor)
    drawCardsWithReshuffle(2)
  else
    drawCardsWithReshuffle(1)
  end
end

-- function for "draw 1 button" (that can be added via option panel)
function doDrawOne(_, color)
  setMessageColor(color)
  drawCardsWithReshuffle(1)
end

-- draw X cards (shuffle discards if necessary)
function drawCardsWithReshuffle(numCards)
  getDrawDiscardDecks()

  -- Norman Withers handling
  if string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" then
    local harbinger = false
    if topCard ~= nil and topCard.getName() == "The Harbinger" then harbinger = true
    elseif drawDeck ~= nil and not drawDeck.is_face_down then
      local cards = drawDeck.getObjects()
      if cards[#cards].name == "The Harbinger" then harbinger = true end
    end

    if harbinger then
      printToColor("The Harbinger is on top of your deck, not drawing cards", messageColor)
      return
    end

    if topCard ~= nil then
      topCard.deal(numCards, PLAYER_COLOR)
      numCards = numCards - 1
      if numCards == 0 then return end
    end
  end

  local deckSize = 1
  if drawDeck == nil then
    deckSize = 0
  elseif drawDeck.tag == "Deck" then
    deckSize = #drawDeck.getObjects()
  end

  if deckSize >= numCards then
    drawCards(numCards)
    return
  end

  drawCards(deckSize)
  if discardPile ~= nil then
    shuffleDiscardIntoDeck()
    Wait.time(|| drawCards(numCards - deckSize), 1)
  end
  printToColor("Take 1 horror (drawing card from empty deck)", messageColor)
end

-- get the draw deck and discard pile objects
function getDrawDiscardDecks()
  drawDeck = nil
  discardPile = nil
  topCard = nil

  local zone = getObjectFromGUID(zoneID)
  if zone == nil then return end

  for _, object in ipairs(zone.getObjects()) do
    if object.tag == "Deck" or object.tag == "Card" then
      if self.positionToLocal(object.getPosition()).z > 0.5 then
        discardPile = object
      -- Norman Withers handling
      elseif string.match(activeInvestigatorId, "%d%d%d%d%d") == "08004" and object.tag == "Card" and not object.is_face_down then
        topCard = object
      else
        drawDeck = object
      end
    end
  end
end

function drawCards(numCards)
  if drawDeck == nil then return end
  drawDeck.deal(numCards, PLAYER_COLOR)
end

function shuffleDiscardIntoDeck()
  if not discardPile.is_face_down then discardPile.flip() end
  discardPile.shuffle()
  discardPile.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)
  drawDeck = discardPile
  discardPile = nil
end

---------------------------------------------------------
-- playmat token spawning
---------------------------------------------------------

-- replenish Tokens for specific cards (like 'Physical Training (4)')
function replenishTokens(card, count, replenish)
  local cardPos = card.getPosition()

  -- don't continue for cards on your deck (Norman) or in your discard pile
  if self.positionToLocal(cardPos).x < -1 then return end

  -- get current amount of resource tokens on the card
  local search = searchArea(cardPos, { 2.5, 0.5, 3.5 })
  local clickableResourceCounter = nil
  local foundTokens = 0

  for _, obj in ipairs(search) do
    local obj = obj.hit_object
    if obj.getCustomObject().image == "http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/" then
      foundTokens = foundTokens + math.abs(obj.getQuantity())
      obj.destruct()
    elseif obj.getMemo() == "resourceCounter" then
      foundTokens = obj.getVar("val")
      clickableResourceCounter = obj
      break
    end
  end

  -- handling Runic Axe upgrade sheet for additional replenish
  if card.getName() == "Runic Axe" then
    for _, v in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
      local obj = v.hit_object
      if obj.tag == "Card" then
        local notes = JSON.decode(obj.getGMNotes()) or {}
        if notes ~= nil and notes.id == "09022-c" then
          if obj.getVar("markedBoxes")[7] == 3 then replenish = 2 end
          break
        end
      end
    end
  end

  local newCount = foundTokens + replenish
  if newCount > count then newCount = count end

  if clickableResourceCounter then
    clickableResourceCounter.call("updateVal", newCount)
  else
    tokenManager.spawnTokenGroup(card, "resource", newCount)
  end
end

function syncCustomizableMetadata(card)
  local cardMetadata = JSON.decode(card.getGMNotes()) or { }
  if cardMetadata ~= nil and cardMetadata.customizations ~= nil then
    for _, collision in ipairs(searchArea(PLAY_ZONE_POSITION, PLAY_ZONE_SCALE)) do
      local obj = collision.hit_object
      if obj.name == "Card" or obj.name == "CardCustom" then
        local notes = JSON.decode(obj.getGMNotes()) or { }
        if notes.id == (cardMetadata.id .. "-c") then
          for i, customization in ipairs(cardMetadata.customizations) do
            if obj.getVar("markedBoxes")[i] == customization.xp
                and customization.replaces ~= nil
                and customization.replaces.uses ~= nil then
              cardMetadata.uses = customization.replaces.uses
              card.setGMNotes(JSON.encode(cardMetadata))
            end
          end
        end
      end
    end
  end
end

function spawnTokensFor(object)
  local extraUses = { }
  if activeInvestigatorId == "03004" then
    extraUses["Charge"] = 1
  end

  tokenManager.spawnForCard(object, extraUses)
end

function onCollisionEnter(collision_info)
  if not COLLISION_ENABLED then return end
  local object = collision_info.collision_object

  -- only continue for cards
  if object.name ~= "Card" and object.name ~= "CardCustom" then return end

  maybeUpdateActiveInvestigator(object)
  syncCustomizableMetadata(object)

  if isInDeckZone(object) then
    tokenManager.resetTokensSpawned(object)
  elseif shouldSpawnTokens(object) then
    spawnTokensFor(object)
  end
end

function shouldSpawnTokens(card)
  if card.is_face_down then
    return false
  end
  local localCardPos = self.positionToLocal(card.getPosition())
  local metadata = JSON.decode(card.getGMNotes())
  -- If no metadata we don't know the type, so only spawn in the main area
  if metadata == nil then
    return inArea(localCardPos, MAIN_PLAY_AREA)
  end
  -- Spawn tokens for assets and events on the main area, and all encounter types in the threat area
  if inArea(localCardPos, MAIN_PLAY_AREA)
      and (metadata.type == "Asset"
      or metadata.type == "Event") then
    return true
  end
  if inArea(localCardPos, THREAT_AREA)
      and (metadata.type == "Treachery"
      or metadata.type == "Enemy"
      or metadata.weakness) then
    return true
  end
  return false
end

function onObjectEnterContainer(container, object)
  Wait.frames(function() resetTokensIfInDeckZone(container, object) end, 1)
end

function resetTokensIfInDeckZone(container, object)
  if isInDeckZone(container) then
    tokenManager.resetTokensSpawned(object)
  end
end

function isInDeckZone(checkObject)
  local deckZone = getObjectFromGUID(zoneID)
  if deckZone == nil then
    return false
  end
  for _, obj in ipairs(deckZone.getObjects()) do
    if obj == checkObject then
      return true
    end
  end

  return false
end

---------------------------------------------------------
-- investigator ID grabbing and skill tracker
---------------------------------------------------------

function maybeUpdateActiveInvestigator(card)
  if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end

  local notes = JSON.decode(card.getGMNotes())
  local class

  if notes ~= nil and notes.type == "Investigator" and notes.id ~= nil then
    if notes.id == activeInvestigatorId then return end
    class = notes.class
    activeInvestigatorId = notes.id
    STAT_TRACKER.call("updateStats", {notes.willpowerIcons, notes.intellectIcons, notes.combatIcons, notes.agilityIcons})
  elseif activeInvestigatorId ~= "00000" then
    class = "Neutral"
    activeInvestigatorId = "00000"
    STAT_TRACKER.call("updateStats", {1, 1, 1, 1})
  else
    return
  end

  -- change state of action tokens
  local search = searchArea(self.positionToWorld({-1.25, 0.05, -1.11}), {4, 1, 1})
  local smallToken = nil
  local STATE_TABLE = {
    ["Guardian"] = 1,
    ["Seeker"]   = 2,
    ["Rogue"]    = 3,
    ["Mystic"]   = 4,
    ["Survivor"] = 5,
    ["Neutral"]  = 6
  }

  for _, obj in ipairs(search) do
    local obj = obj.hit_object
    if obj.getDescription() == "Action Token" and obj.getStateId() > 0 then
      if obj.getScale().x < 0.4 then
        smallToken = obj
      else
        setObjectState(obj, STATE_TABLE[class])
      end
    end
  end

  -- update the small token with special action for certain investigators
  local SPECIAL_ACTIONS = {
    ["04002"]    = 8,  -- Ursula Downs
    ["01002"]    = 9,  -- Daisy Walker
    ["01502"]    = 9,  -- Daisy Walker
    ["01002-pb"] = 9,  -- Daisy Walker
    ["06003"]    = 10, -- Tony Morgan
    ["04003"]    = 11, -- Finn Edwards
    ["08016"]    = 14  -- Bob Jenkins
  }

  if smallToken ~= nil then
    setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class])
  end
end

function setObjectState(obj, stateId)
  if obj.getStateId() ~= stateId then obj.setState(stateId) end
end

---------------------------------------------------------
-- calls to 'Global' / functions for calls from outside
---------------------------------------------------------

function drawChaostokenButton(_, _, isRightClick)
  Global.call("drawChaostoken", {self, DRAWN_CHAOS_TOKEN_OFFSET, isRightClick})
end

function drawEncountercard(object, player, isRightClick)
  Global.call("drawEncountercard", PLAYER_COLOR)
end

function returnGlobalDiscardPosition()
  return self.positionToWorld(DISCARD_PILE_POSITION)
end

-- Sets this playermat's draw 1 button to visible
---@param visible Boolean.  Whether the draw 1 button should be visible
function showDrawButton(visible)
  isDrawButtonVisible = visible

  -- create the "Draw 1" button
  if isDrawButtonVisible then
    self.createButton({
        label = "Draw 1",
        click_function = "doDrawOne",
        function_owner = self,
        position = { 1.84, 0.1, -0.36 },
        scale = { 0.12, 0.12, 0.12 },
        width = 800,
        height = 280,
        font_size = 180
    })

  -- remove the "Draw 1" button
  else
    local buttons = self.getButtons()
    for i = 1, #buttons do
      if buttons[i].label == "Draw 1" then
        self.removeButton(buttons[i].index)
      end
    end
  end
end

-- Spawns / destroys a clickable clue counter for this playmat with the correct amount of clues
---@param showCounter Boolean Whether the clickable clue counter should be present
function clickableClues(showCounter)
  local CLUE_COUNTER = getObjectFromGUID(CLUE_COUNTER_GUID)
  local CLUE_CLICKER = getObjectFromGUID(CLUE_CLICKER_GUID)
  local clickerPos = CLUE_CLICKER.getPosition()
  local clueCount = 0

  if showCounter then
    -- current clue count
    clueCount = CLUE_COUNTER.getVar("exposedValue")

    -- remove clues
    CLUE_COUNTER.call("removeAllClues")

    -- set value for clue clickers
    CLUE_CLICKER.call("updateVal", clueCount)

    -- move clue counters up
    clickerPos.y = 1.52
    CLUE_CLICKER.setPosition(clickerPos)
  else
    -- current clue count
    clueCount = CLUE_CLICKER.getVar("val")

    -- move clue counters down
    clickerPos.y = 1.3
    CLUE_CLICKER.setPosition(clickerPos)

    -- spawn clues
    local pos = self.positionToWorld({x = -1.12, y = 0.05, z = 0.7})
    for i = 1, clueCount do
      pos.y = pos.y + 0.045 * i
      tokenManager.spawnToken(pos, "clue", PLAY_ZONE_ROTATION)
    end
  end
end

-- removes all clues (moving tokens to the trash and setting counters to 0)
function removeClues()
  local CLUE_COUNTER = getObjectFromGUID(CLUE_COUNTER_GUID)
  local CLUE_CLICKER = getObjectFromGUID(CLUE_CLICKER_GUID)

  CLUE_COUNTER.call("removeAllClues")
  CLUE_CLICKER.call("updateVal", 0)
end

-- reports the clue count
---@param useClickableCounters Boolean Controls which type of counter is getting checked
function getClueCount(useClickableCounters)
  local count = 0

  if useClickableCounters then
    local CLUE_CLICKER = getObjectFromGUID(CLUE_CLICKER_GUID)
    count = tonumber(CLUE_CLICKER.getVar("val"))
  else
    local CLUE_COUNTER = getObjectFromGUID(CLUE_COUNTER_GUID)
    count = tonumber(CLUE_COUNTER.getVar("exposedValue"))
  end
  return count
end

-- Sets this playermat's snap points to limit snapping to matching card types or not.  If matchTypes
-- is true, the main card slot snap points will only snap assets, while the investigator area point
-- will only snap Investigators.  If matchTypes is false, snap points will be reset to snap all
-- cards.
---@param matchTypes Boolean.  Whether snap points should only snap for the matching card types.
function setLimitSnapsByType(matchTypes)
  local snaps = self.getSnapPoints()
  for i, snap in ipairs(snaps) do
    local snapPos = snap.position
    if inArea(snapPos, MAIN_PLAY_AREA) then
      local snapTags = snaps[i].tags
      if matchTypes then
        if snapTags == nil then
          snaps[i].tags = { "Asset" }
        else
          table.insert(snaps[i].tags, "Asset")
        end
      else
        snaps[i].tags = nil
      end
    end
    if inArea(snapPos, INVESTIGATOR_AREA) then
      local snapTags = snaps[i].tags
      if matchTypes then
        if snapTags == nil then
          snaps[i].tags = { "Investigator" }
        else
          table.insert(snaps[i].tags, "Investigator")
        end
      else
        snaps[i].tags = nil
      end
    end
  end
  self.setSnapPoints(snaps)
end

-- Simple method to check if the given point is in a specified area.  Local use only,
---@param point Vector.  Point to check, only x and z values are relevant
---@param bounds Table.  Defined area to see if the point is within.  See MAIN_PLAY_AREA for sample
--     bounds definition.
---@return Boolean.  True if the point is in the area defined by bounds
function inArea(point, bounds)
  return (point.x < bounds.upperLeft.x
      and point.x > bounds.lowerRight.x
      and point.z < bounds.upperLeft.z
      and point.z > bounds.lowerRight.z)
end

-- called by custom data helpers to add player card data
---@param args table Contains only one entry, the GUID of the custom data helper
function updatePlayerCards(args)
  local customDataHelper = getObjectFromGUID(args[1])
  local playerCardData = customDataHelper.getTable("PLAYER_CARD_DATA")
  tokenManager.addPlayerCardData(playerCardData)
end
end)
__bundle_register("core/token/TokenChecker", function(require, _LOADED, __bundle_register, __bundle_modules)
do
  local CHAOS_TOKEN_NAMES = {
    ["Elder Sign"] = true,
    ["+1"] = true,
    ["0"] = true,
    ["-1"] = true,
    ["-2"] = true,
    ["-3"] = true,
    ["-4"] = true,
    ["-5"] = true,
    ["-6"] = true,
    ["-7"] = true,
    ["-8"] = true,
    ["Skull"] = true,
    ["Cultist"] = true,
    ["Tablet"] = true,
    ["Elder Thing"] = true,
    ["Auto-fail"] = true,
    ["Bless"] = true,
    ["Curse"] = true,
    ["Frost"] = true
  }

  local TokenChecker = {}

  -- returns true if the passed object is a chaos token (by name)
  TokenChecker.isChaosToken = function(obj)
    if CHAOS_TOKEN_NAMES[obj.getName()] then
      return true
    else
      return false
    end
  end

  return TokenChecker
end
end)
__bundle_register("core/token/TokenManager", function(require, _LOADED, __bundle_register, __bundle_modules)
do
  local tokenSpawnTracker = require("core/token/TokenSpawnTrackerApi")
  local playArea = require("core/PlayAreaApi")

  local PLAYER_CARD_TOKEN_OFFSETS = {
    [1] = {
      Vector(0, 3, -0.2)
    },
    [2] = {
      Vector(0.4, 3, -0.2),
      Vector(-0.4, 3, -0.2)
    },
    [3] = {
      Vector(0, 3, -0.9),
      Vector(0.4, 3, -0.2),
      Vector(-0.4, 3, -0.2)
    },
    [4] = {
      Vector(0.4, 3, -0.9),
      Vector(-0.4, 3, -0.9),
      Vector(0.4, 3, -0.2),
      Vector(-0.4, 3, -0.2)
    },
    [5] = {
      Vector(0.7, 3, -0.9),
      Vector(0, 3, -0.9),
      Vector(-0.7, 3, -0.9),
      Vector(0.4, 3, -0.2),
      Vector(-0.4, 3, -0.2)
    },
    [6] = {
      Vector(0.7, 3, -0.9),
      Vector(0, 3, -0.9),
      Vector(-0.7, 3, -0.9),
      Vector(0.7, 3, -0.2),
      Vector(0, 3, -0.2),
      Vector(-0.7, 3, -0.2)
    },
    [7] = {
      Vector(0.7, 3, -0.9),
      Vector(0, 3, -0.9),
      Vector(-0.7, 3, -0.9),
      Vector(0.7, 3, -0.2),
      Vector(0, 3, -0.2),
      Vector(-0.7, 3, -0.2),
      Vector(0, 3, 0.5)
    },
    [8] = {
      Vector(0.7, 3, -0.9),
      Vector(0, 3, -0.9),
      Vector(-0.7, 3, -0.9),
      Vector(0.7, 3, -0.2),
      Vector(0, 3, -0.2),
      Vector(-0.7, 3, -0.2),
      Vector(-0.35, 3, 0.5),
      Vector(0.35, 3, 0.5)
    },
    [9] = {
      Vector(0.7, 3, -0.9),
      Vector(0, 3, -0.9),
      Vector(-0.7, 3, -0.9),
      Vector(0.7, 3, -0.2),
      Vector(0, 3, -0.2),
      Vector(-0.7, 3, -0.2),
      Vector(0.7, 3, 0.5),
      Vector(0, 3, 0.5),
      Vector(-0.7, 3, 0.5)
    },
    [10] = {
      Vector(0.7, 3, -0.9),
      Vector(0, 3, -0.9),
      Vector(-0.7, 3, -0.9),
      Vector(0.7, 3, -0.2),
      Vector(0, 3, -0.2),
      Vector(-0.7, 3, -0.2),
      Vector(0.7, 3, 0.5),
      Vector(0, 3, 0.5),
      Vector(-0.7, 3, 0.5),
      Vector(0, 3, 1.2)
    },
    [11] = {
      Vector(0.7, 3, -0.9),
      Vector(0, 3, -0.9),
      Vector(-0.7, 3, -0.9),
      Vector(0.7, 3, -0.2),
      Vector(0, 3, -0.2),
      Vector(-0.7, 3, -0.2),
      Vector(0.7, 3, 0.5),
      Vector(0, 3, 0.5),
      Vector(-0.7, 3, 0.5),
      Vector(-0.35, 3, 1.2),
      Vector(0.35, 3, 1.2)
    },
    [12] = {
      Vector(0.7, 3, -0.9),
      Vector(0, 3, -0.9),
      Vector(-0.7, 3, -0.9),
      Vector(0.7, 3, -0.2),
      Vector(0, 3, -0.2),
      Vector(-0.7, 3, -0.2),
      Vector(0.7, 3, 0.5),
      Vector(0, 3, 0.5),
      Vector(-0.7, 3, 0.5),
      Vector(0.7, 3, 1.2),
      Vector(0, 3, 1.2),
      Vector(-0.7, 3, 1.2)
    }
  }

  -- Source for tokens
  local TOKEN_SOURCE_GUID = "124381"

  -- Table of data extracted from the token source bag, keyed by the Memo on each token which
  -- should match the token type keys ("resource", "clue", etc)
  local tokenTemplates

  local DATA_HELPER_GUID = "708279"

  local playerCardData
  local locationData

  local TokenManager = { }
  local internal = { }

  -- Spawns tokens for the card.  This function is built to just throw a card at it and let it do
  -- the work once a card has hit an area where it might spawn tokens.  It will check to see if
  -- the card has already spawned, find appropriate data from either the uses metadata or the Data
  -- Helper, and spawn the tokens.
  ---@param card Object Card to maybe spawn tokens for
  ---@param extraUses Table A table of <use type>=<count> which will modify the number of tokens
  ---    spawned for that type.  e.g. Akachi's playmat should pass "Charge"=1
  TokenManager.spawnForCard = function(card, extraUses)
    if tokenSpawnTracker.hasSpawnedTokens(card.getGUID()) then
      return
    end
    local metadata = JSON.decode(card.getGMNotes())
    if metadata ~= nil then
      internal.spawnTokensFromUses(card, extraUses)
    else
      internal.spawnTokensFromDataHelper(card)
    end
  end

  -- Spawns a set of tokens on the given card.
  ---@param card Object Card to spawn tokens on
  ---@param tokenType String type of token to spawn, valid values are "damage", "horror",
  --     "resource", "doom", or "clue"
  ---@param tokenCount Number How many tokens to spawn.  For damage or horror this value will be set to the
  --     spawned state object rather than spawning multiple tokens
  ---@param shiftDown Number An offset for the z-value of this group of tokens
  TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown)
    local optionPanel = Global.getTable("optionPanel")

    if tokenType == "damage" or tokenType == "horror" then
      TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)
    elseif tokenType == "resource" and optionPanel["useResourceCounters"] then
      TokenManager.spawnResourceCounterToken(card, tokenCount)
    else
      TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown)
    end
  end

  -- Spawns a single counter token and sets the value to tokenValue.  Used for damage and horror
  -- tokens.
  ---@param card Object Card to spawn tokens on
  ---@param tokenType String type of token to spawn, valid values are "damage" and "horror".  Other
  --     types should use spawnMultipleTokens()
  ---@param tokenValue Number Value to set the damage/horror to
  TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)
    if tokenValue < 1 or tokenValue > 50 then return end

    local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))
    local rot = card.getRotation()
    TokenManager.spawnToken(pos, tokenType, rot, function(spawned) spawned.setState(tokenValue) end)
  end

  TokenManager.spawnResourceCounterToken = function(card, tokenCount)
    local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))
    local rot = card.getRotation()
    TokenManager.spawnToken(pos, "resourceCounter", rot, function(spawned)
      spawned.call("updateVal", tokenCount)
    end)
  end

  -- Spawns a number of tokens.
  ---@param tokenType String type of token to spawn, valid values are resource", "doom", or "clue".
  --     Other types should use spawnCounterToken()
  ---@param tokenCount Number How many tokens to spawn
  ---@param shiftDown Number An offset for the z-value of this group of tokens
  TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown)
    if tokenCount < 1 or tokenCount > 12 then
      return
    end

    local offsets = {}
    if tokenType == "clue" then
      offsets = internal.buildClueOffsets(card, tokenCount)
    else
      for i = 1, tokenCount do
        offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])
        -- Fix the y-position for the spawn, since positionToWorld considers rotation which can
        -- have bad results for face up/down differences
        offsets[i].y = card.getPosition().y + 0.15
      end
    end

    if shiftDown ~= nil then
      -- Copy the offsets to make sure we don't change the static values
      local baseOffsets = offsets
      offsets = { }
      for i, baseOffset in ipairs(baseOffsets) do
        offsets[i] = baseOffset
        offsets[i][3] = offsets[i][3] + shiftDown
      end
    end

    if offsets == nil then
      error("couldn't find offsets for " .. tokenCount .. ' tokens')
      return
    end

    for i = 1, tokenCount do
      TokenManager.spawnToken(offsets[i], tokenType, card.getRotation())
    end
  end

  -- Spawns a single token at the given global position by copying it from the template bag.
  ---@param position Global position to spawn the token
  ---@param tokenType String type of token to spawn, valid values are "damage", "horror",
  --     "resource", "doom", or "clue"
  ---@param rotation Vector Rotation to be used for the new token.  Only the y-value will be used,
  --     x and z will use the default rotation from the source bag
  ---@param callback function A callback function triggered after the new token is spawned
  TokenManager.spawnToken = function(position, tokenType, rotation, callback)
    internal.initTokenTemplates()
    local loadTokenType = tokenType
    if tokenType == "clue" or tokenType == "doom" then
      loadTokenType = "clueDoom"
    end
    if tokenTemplates[loadTokenType] == nil then
      error("Unknown token type '" .. tokenType .. "'")
      return
    end
    local tokenTemplate = tokenTemplates[loadTokenType]

    -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag
    local rot = Vector(tokenTemplate.Transform.rotX,
        270,
        tokenTemplate.Transform.rotZ)
    if rotation ~= nil then
      rot.y = rotation.y
    end
    if tokenType == "doom" then
      rot.z = 180
    end

    tokenTemplate.Nickname = ""
    return spawnObjectData({
      data = tokenTemplate,
      position = position,
      rotation = rot,
      callback_function = callback
    })
  end

  -- Delegate function to the token spawn tracker.  Exists to avoid circular dependencies in some
  -- callers.
  ---@param card Object Card object to reset the tokens for
  TokenManager.resetTokensSpawned = function(card)
    tokenSpawnTracker.resetTokensSpawned(card.getGUID())
  end

  -- Pushes new player card data into the local copy of the Data Helper player data.
  ---@param dataTable Table Key/Value pairs following the DataHelper style
  TokenManager.addPlayerCardData = function(dataTable)
    internal.initDataHelperData()
    for k, v in pairs(dataTable) do
      playerCardData[k] = v
    end
  end

  -- Pushes new location data into the local copy of the Data Helper location data.
  ---@param dataTable Table Key/Value pairs following the DataHelper style
  TokenManager.addLocationData = function(dataTable)
    internal.initDataHelperData()
    for k, v in pairs(dataTable) do
      locationData[k] = v
    end
  end

  -- Checks to see if the given card has location data in the DataHelper
  ---@param card Object Card to check for data
  ---@return Boolean True if this card has data in the helper, false otherwise
  TokenManager.hasLocationData = function(card)
    internal.initDataHelperData()
    return internal.getLocationData(card) ~= nil
  end

  internal.initTokenTemplates = function()
    if tokenTemplates ~= nil then
      return
    end
    tokenTemplates = { }
    local tokenSource = getObjectFromGUID(TOKEN_SOURCE_GUID)
    for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do
      local tokenName = tokenTemplate.Memo
      tokenTemplates[tokenName] = tokenTemplate
    end
  end

  -- Copies the data from the DataHelper.  Will only happen once.
  internal.initDataHelperData = function()
    if playerCardData ~= nil then
      return
    end
    local dataHelper = getObjectFromGUID(DATA_HELPER_GUID)
    playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')
    locationData = dataHelper.getTable('LOCATIONS_DATA')
  end

  -- Spawn tokens for a card based on the uses metadata.  This will consider the face up/down state
  -- of the card for both locations and standard cards.
  ---@param card Object Card to maybe spawn tokens for
  ---@param extraUses Table A table of <use type>=<count> which will modify the number of tokens
  ---    spawned for that type.  e.g. Akachi's playmat should pass "Charge"=1
  internal.spawnTokensFromUses = function(card, extraUses)
    local uses = internal.getUses(card)
    if uses == nil then
      return
    end
    local type = nil
    local token = nil
    local tokenCount = 0
    -- Uses structure underwent a breaking change in 2.4.0, have to check to see if this is
    -- a single entry or an array.  This is ugly and duplicated, but impossible to replicate the
    -- multi-spawn vs. single spawn otherwise.  TODO: Clean this up when 2.4.0 has been out long
    -- enough that saved decks don't have old data
    if uses.count != nil then
      type = cardMetadata.uses.type
      token = cardMetadata.uses.token
      tokenCount = cardMetadata.uses.count
      if extraUses ~= nil and extraUses[type] ~= nil then
        tokenCount = tokenCount + extraUses[type]
      end
      log("Spawning single use tokens for "..card.getName()..'['..card.getDescription()..']: '..tokenCount.."x "..token)
      TokenManager.spawnTokenGroup(card, token, tokenCount)
    else
      for i, useInfo in ipairs(uses) do
        type = useInfo.type
        token = useInfo.token
        tokenCount = (useInfo.count or 0)
            + (useInfo.countPerInvestigator or 0) * playArea.getInvestigatorCount()
        if extraUses ~= nil and extraUses[type] ~= nil then
          tokenCount = tokenCount + extraUses[type]
        end
        log("Spawning use array tokens for "..card.getName()..'['..card.getDescription()..']: '..tokenCount.."x "..token)
        -- Shift each spawned group after the first down so they don't pile on each other
        TokenManager.spawnTokenGroup(card, token, tokenCount, (i - 1) * 0.8)
      end
    end
    tokenSpawnTracker.markTokensSpawned(card.getGUID())
  end

  -- Spawn tokens for a card based on the data helper data.  This will consider the face up/down state
  -- of the card for both locations and standard cards.
  ---@param card Object Card to maybe spawn tokens for
  internal.spawnTokensFromDataHelper = function(card)
    internal.initDataHelperData()
    local playerData = internal.getPlayerCardData(card)
    if playerData ~= nil then
      internal.spawnPlayerCardTokensFromDataHelper(card, playerData)
    end
    local locationData = internal.getLocationData(card)
    if locationData ~= nil then
      internal.spawnLocationTokensFromDataHelper(card, locationData)
    end
  end

  -- Spawn tokens for a player card using data retrieved from the Data Helper.
  ---@param card Object Card to maybe spawn tokens for
  ---@param playerData Table Player card data structure retrieved from the DataHelper.  Should be
  --     the right data for this card.
  internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)
    token = playerData.tokenType
    tokenCount = playerData.tokenCount
    log("Spawning data helper tokens for "..card.getName()..'['..card.getDescription()..']: '..tokenCount.."x "..token)
    TokenManager.spawnTokenGroup(card, token, tokenCount)
    tokenSpawnTracker.markTokensSpawned(card.getGUID())
  end

  -- Spawn tokens for a location using data retrieved from the Data Helper.
  ---@param card Object Card to maybe spawn tokens for
  ---@param playerData Table Location data structure retrieved from the DataHelper.  Should be
  --     the right data for this card.
  internal.spawnLocationTokensFromDataHelper = function(card, locationData)
    local clueCount = internal.getClueCountFromData(card, locationData)
    if clueCount > 0 then
      TokenManager.spawnTokenGroup(card, "clue", clueCount)
      tokenSpawnTracker.markTokensSpawned(card.getGUID())
    end
  end

  internal.getPlayerCardData = function(card)
    return playerCardData[card.getName() .. ':' .. card.getDescription()]
        or playerCardData[card.getName()]
  end

  internal.getLocationData = function(card)
    return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]
  end

  internal.getClueCountFromData = function(card, locationData)
    -- Return the number of clues to spawn on this location
    if locationData == nil then
      error('attempted to get clue for unexpected object: ' .. card.getName())
      return 0
    end

    log(card.getName() .. ' : ' .. locationData.type .. ' : ' .. locationData.value .. ' : ' .. locationData.clueSide)
    if ((card.is_face_down and locationData.clueSide == 'back')
        or (not card.is_face_down and locationData.clueSide == 'front')) then
      if locationData.type == 'fixed' then
        return locationData.value
      elseif locationData.type == 'perPlayer' then
        return locationData.value * playArea.getInvestigatorCount()
      end
      error('unexpected location type: ' .. locationData.type)
    end
    return 0
  end

  -- Gets the right uses structure for this card, based on metadata and face up/down state
  ---@param card Object Card to pull the uses from
  internal.getUses = function(card)
    local metadata = JSON.decode(card.getGMNotes()) or { }
    if metadata.type == "Location" then
      if card.is_face_down and metadata.locationBack ~= nil then
        return metadata.locationBack.uses
      elseif not card.is_face_down and metadata.locationFront ~= nil then
        return metadata.locationFront.uses
      end
    elseif not card.is_face_down then
      return metadata.uses
    end

    return nil
  end

  -- Dynamically create positions for clues on a card.
  ---@param card Object Card the clues will be placed on
  ---@param count Integer How many clues?
  ---@return Table Array of global positions to spawn the clues at
  internal.buildClueOffsets = function(card, count)
    local pos = card.getPosition()
    local cluePositions = { }
    for i = 1, count do
      local row = math.floor(1 + (i - 1) / 4)
      local column = (i - 1) % 4
      table.insert(cluePositions, Vector(pos.x + 1.5 - 0.55 * row, pos.y + 0.15, pos.z - 0.825 + 0.55 * column))
    end

    return cluePositions
  end

  return TokenManager

end
end)
__bundle_register("core/PlayAreaApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
  local PlayAreaApi = { }

  local PLAY_AREA_GUID = "721ba2"

  -- Returns the current value of the investigator counter from the playmat
  ---@return Integer.  Number of investigators currently set on the counter
  PlayAreaApi.getInvestigatorCount = function()
    return getObjectFromGUID(PLAY_AREA_GUID).call("getInvestigatorCount")
  end

  -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction.  Certain
  -- fixed objects will be ignored, as will anything the player has tagged with
  -- 'displacement_excluded'
  ---@param playerColor Color of the player requesting the shift.  Used solely to send an error
  ---    message in the unlikely case that the scripting zone has been deleted
  PlayAreaApi.shiftContentsUp = function(playerColor)
    return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsUp", playerColor)
  end

  PlayAreaApi.shiftContentsDown = function(playerColor)
    return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsDown", playerColor)
  end

  PlayAreaApi.shiftContentsLeft = function(playerColor)
    return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsLeft", playerColor)
  end

  PlayAreaApi.shiftContentsRight = function(playerColor)
    return getObjectFromGUID(PLAY_AREA_GUID).call("shiftContentsRight", playerColor)
  end

  -- Reset the play area's tracking of which cards have had tokens spawned.
  PlayAreaApi.resetSpawnedCards = function()
    return getObjectFromGUID(PLAY_AREA_GUID).call("resetSpawnedCards")
  end

  -- Event to be called when the current scenario has changed.
  ---@param scenarioName Name of the new scenario
  PlayAreaApi.onScenarioChanged = function(scenarioName)
    getObjectFromGUID(PLAY_AREA_GUID).call("onScenarioChanged", scenarioName)
  end

  -- Sets this playmat's snap points to limit snapping to locations or not.
  -- If matchTypes is false, snap points will be reset to snap all cards.
  ---@param matchTypes Boolean  Whether snap points should only snap for the matching card types.
  PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)
    getObjectFromGUID(PLAY_AREA_GUID).call("setLimitSnapsByType", matchCardTypes)
  end

  -- Receiver for the Global tryObjectEnterContainer event.  Used to clear vector lines from dragged
  -- cards before they're destroyed by entering the container
  PlayAreaApi.tryObjectEnterContainer = function(container, object)
    getObjectFromGUID(PLAY_AREA_GUID).call("tryObjectEnterContainer",
        { container = container, object = object })
  end

  return PlayAreaApi
end
end)
__bundle_register("core/token/TokenSpawnTrackerApi", function(require, _LOADED, __bundle_register, __bundle_modules)
do
  local TokenSpawnTracker = { }

  local SPAWN_TRACKER_GUID = "e3ffc9"

  TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)
    return getObjectFromGUID(SPAWN_TRACKER_GUID).call("hasSpawnedTokens", cardGuid)
  end

  TokenSpawnTracker.markTokensSpawned = function(cardGuid)
    return getObjectFromGUID(SPAWN_TRACKER_GUID).call("markTokensSpawned", cardGuid)
  end

  TokenSpawnTracker.resetTokensSpawned = function(cardGuid)
    return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetTokensSpawned", cardGuid)
  end

  TokenSpawnTracker.resetAllAssetAndEvents = function()
    return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAllAssetAndEvents")
  end

  TokenSpawnTracker.resetAllLocations = function()
    return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAllLocations")
  end

  TokenSpawnTracker.resetAll = function()
    return getObjectFromGUID(SPAWN_TRACKER_GUID).call("resetAll")
  end

  return TokenSpawnTracker
end
end)
return __bundle_require("__root")
