HUB | Project UAI
Key Required

HUB | Project UAI

[🎉 UPD 7] Anime DiceUltDEVUltDEV 3.1K views 8.2K executesUpdated Sep 24, 2026
5.0/ 5(1 votes)
10100% Positive
Lua Script•1296 lines (45221 chars)
--[[
	ANIME DICE HUB  —  place 113290951185459  ("[UPD 5] Anime Dice")
	Built with Project UAI  ·  model deepseek-v4-flash
	Built from the decompiled server source (all Framework.Features services are
	replicated to the client, so every remote, arg shape and server check is known).

	Everything here is automation of the game's own API. The server is authoritative:
	it re-rolls every rarity/grade/trait server-side, recomputes prices and holds the
	profile. Nothing here can forge a reward — the hub just drives the normal calls
	faster than a human can and never lets the inventory back up.

	Toggle UI: RightShift.  Panic stop: the UI "STOP ALL" button, or
	getgenv().__ANIME_DICE_HUB.stop()

	API: getgenv().__ANIME_DICE_HUB
	  .cfg            config table (enabled flags + tunables)
	  .stats          live counters
	  .set(k, v)      set a config value
	  .toggle(k)      flip an enabled flag
	  .sellNow() .collectNow() .equipBest() .claimNow() .redeemAll()
	  .stop()         stop everything and disconnect
]]

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local RS = game:GetService("ReplicatedStorage")
local plr = Players.LocalPlayer

-- one instance only ---------------------------------------------------------
if getgenv().__ANIME_DICE_HUB then
	pcall(function() getgenv().__ANIME_DICE_HUB.stop() end)
end
local HUB_VER = "1.0"

local function waitFor(parent, name, t)
	return parent and parent:WaitForChild(name, t or 15)
end

local Net = waitFor(RS, "Network")
if not Net then
	return warn("[ADH] no Network folder - wrong game?")
end

local function rem(folder, class, name)
	local f = waitFor(Net, folder)
	local c = f and waitFor(f, class)
	local r = c and waitFor(c, name)
	if not r then warn("[ADH] missing remote: " .. folder .. "." .. class .. "." .. name) end
	return r
end

-- exact remote map (verified against the decompiled services) ----------------
local R = {
	Roll           = rem("RollService", "RF", "RollDice"),
	SetAutoRoll    = rem("RollService", "RE", "SetAutoRoll"),
	SellInventory  = rem("SellService", "RF", "SellInventory"),
	SellEquipped   = rem("SellService", "RF", "SellEquipped"),
	UpdateAutoSell = rem("SellService", "RE", "UpdateAutoSell"),
	SetSetting     = rem("SettingsService", "RE", "SetSetting"),
	CollectBalance = rem("PlotService", "RE", "CollectBalance"),
	EquipBest      = rem("PlotService", "RE", "EquipBest"),
	LevelUpSlot    = rem("PlotService", "RE", "LevelUpSlot"),
	QuestClaim     = rem("QuestService", "RE", "Claim"),
	QuestBuy       = rem("QuestService", "RE", "Buy"),
	DailyClaim     = rem("DailyRewardService", "RE", "Claim"),
	GroupClaim     = rem("GroupRewardService", "RE", "Claim"),
	OfflineClaim   = rem("OfflineEarningsService", "RE", "Claim"),
	Rebirth        = rem("RebirthService", "RE", "Rebirth"),
	BuyDice        = rem("DiceShopService", "RE", "BuyDice"),
	EquipDice      = rem("DiceShopService", "RE", "EquipDice"),
	GradeRoll      = rem("GradeService", "RE", "Roll"),
	GradeProtect   = rem("GradeService", "RE", "SetGradeProtected"),
	TraitRoll      = rem("TraitService", "RE", "Roll"),
	TraitProtect   = rem("TraitService", "RE", "SetTraitProtected"),
	Equip          = rem("UnitService", "RF", "Equip"),
	Unequip        = rem("UnitService", "RF", "Unequip"),
	SetLocked      = rem("UnitService", "RE", "SetLocked"),
	BoostUse       = rem("BoostService", "RE", "Use"),
	SpinUse        = rem("SpinService", "RE", "Use"),
	Fuse           = rem("FusingService", "RE", "Fuse"),
	TowerPlay      = rem("Towers", "RF", "PlayTower"),
	TowerFloor     = rem("Towers", "RF", "CompleteTowerFloor"),
	TowerCancel    = rem("Towers", "RF", "CancelTower"),
	TowerEquipBest = rem("Towers", "RE", "EquipBestTowerTeam"),
	FuseResult     = rem("FusingService", "RE", "Result"),
	Redeem         = rem("MonetizationService", "RE", "RedeemCode"),
}
R.BuyUpgrade = waitFor(Net, "RE") and waitFor(Net.RE, "BuyUpgrade")

-- game config tables (all replicated to the client) --------------------------
local function mod(path)
	local node = RS
	for seg in string.gmatch(path, "[^%.]+") do
		node = node and node:FindFirstChild(seg)
	end
	if not node then
		warn("[ADH] missing module " .. path)
		return nil
	end
	local ok, m = pcall(require, node)
	if not ok then
		warn("[ADH] require failed " .. path .. ": " .. tostring(m))
		return nil
	end
	return m
end

local ER        = mod("Framework.Features.Inventory.EntryRegistry")
local Rar       = mod("Framework.Other.Rarities")
local DiceCfg   = mod("Framework.Features.Rolling.Dice")
local Grades    = mod("Framework.Features.Grades.Grades")
local Traits    = mod("Framework.Features.Traits.Traits")
local Rebirths  = mod("Framework.Features.Rebirth.Rebirths")
local Upgrades  = mod("Framework.Features.Upgrades.Upgrades")
local Tree      = mod("Framework.Features.Upgrades.TreeStructure")
local QuestCfg  = mod("Framework.Features.Quests.QuestConfig")
local PlotCfg   = mod("Framework.Features.Plot.PlotConfig")
local MonetCfg  = mod("Framework.Features.Monetization.MonetizationConfig")
local TowersCfg = mod("Framework.Features.Towers.Towers")
local FusingCfg = mod("Framework.Features.Fusing.FusingConfig")
local UIRefs    = mod("Framework.Features.UI.UIReferences")
local D         = mod("Framework.Features.Data.DataController")
local BuffC     = mod("Framework.Features.Buffs.BuffController")

if not (ER and D) then
	return warn("[ADH] core modules unavailable, aborting")
end

-- config ---------------------------------------------------------------------
local Cfg = {
	enabled = {
		autoRoll       = true,
		autoCollect    = true,
		autoSell       = true,
		autoEquipBest  = true,
		autoUpgrades   = true,
		autoQuests     = true,
		autoQuestShop  = true,
		autoClaims     = true,
		autoGrade      = true,
		autoTrait      = true,
		autoDice       = true,
		autoBoostSpins = true,
		autoRedeem     = true,
		autoRebirth    = true,
		autoTower      = false,
		autoFuse       = true,
		killShowcase   = true,
		killAutoRoll   = false,
		serverAutoSell = false,
	},
	rollInterval    = 2.9,
	collectInterval = 5,
	sellInterval    = 8,
	upgradeInterval = 3,
	diceInterval    = 30,
	claimInterval   = 45,
	questInterval   = 15,
	gradeInterval   = 0.45,
	fuseInterval    = 5,
	fuseMaxChance   = 50000,
	fuseFloorUnits  = 25,
	fuseMoneyMultiple = 5,
	-- selling
	sellChance      = 1000,
	keepRarity      = 4,
	keepTop         = 8,
	keepMutated     = false,
	maxUnits        = 70,
	reserveUnits    = 20,
	sellBatch       = 60,
	-- targets
	gradeTarget     = 3,
	traitTarget     = 2,
	moneyReserve    = 0,
	diceSpendFrac   = 0.5,
	questShopPriority = { "Roll Speed", "More Cash", "Luck", "Ultra Luck", "Trait Reroll", "Gems", "Lucky Spin" },
	priorityUpgrades = { "Roll Speed", "Unit Storage" },
	questShopPerTick = 2,
	rareLogChance   = 1000,
	fuseMinMoney    = 1e6,
	towerName       = nil,
	serverAutoSellValue = 0,
	uiKey           = Enum.KeyCode.RightShift,
}

-- runtime state --------------------------------------------------------------
local State = {
	connections = {},
	tasks = {},
	running = true,
	fuseActive = false,
	towerActive = false,
	towerStalls = 0,
}

local Stats = {
	rolls = 0, soldUnits = 0, soldMoney = 0,
	money = 0, moneyPerMin = 0, rollsPerMin = 0,
	startClock = os.clock(), lastRollClock = os.clock(),
	rares = {}, log = {}, earned = 0,
}

local function log(msg, colour)
	table.insert(Stats.log, 1, {
		text = os.date("[%H:%M:%S] ") .. msg,
		colour = colour or Color3.fromRGB(220, 220, 220),
	})
	while #Stats.log > 40 do
		table.remove(Stats.log)
	end
end
-- data helpers ---------------------------------------------------------------
local function pv(fn, ...)
	local ok, v = pcall(fn, ...)
	if ok then return v end
	return nil
end

local function money()
	local v = pv(function() return D.Money() end)
	return typeof(v) == "number" and v or 0
end

local function inventory()
	local v = pv(function() return D.Inventory() end)
	return typeof(v) == "table" and v or {}
end

local function slots()
	local v = pv(function() return D.Slots() end)
	return typeof(v) == "table" and v or {}
end

local function amountOf(name)
	local total = 0
	for _, e in pairs(inventory()) do
		if e.name == name then total = total + (tonumber(e.amount) or 0) end
	end
	return total
end

local function buff(name)
	if not BuffC then return nil end
	local v = pv(BuffC.GetBuff, name)
	return tonumber(v)
end

local function inventoryCap()
	return buff("Unit Storage") or 100
end

local function unitEntries()
	local out = {}
	for key, e in pairs(inventory()) do
		local cfg = ER.getEntryConfig(e.name)
		if cfg and cfg.kind == "Unit" then
			local attrs = e.attributes or {}
			local inc = tonumber(pv(cfg.income, attrs)) or 0
			local ch = tonumber(pv(cfg.chance, attrs)) or 0
			table.insert(out, {
				key = key, name = e.name, attrs = attrs, cfg = cfg,
				income = inc, chance = ch, amount = tonumber(e.amount) or 1,
			})
		end
	end
	return out
end

local function rarityOrder(name)
	if Rar and Rar.Get and name then
		local r = pv(Rar.Get, name)
		if typeof(r) == "table" and tonumber(r.sortOrder) then return tonumber(r.sortOrder) end
	end
	return 1
end

local function fmt(n)
	n = tonumber(n) or 0
	if n >= 1e15 then return string.format("%.2fQa", n / 1e15) end
	if n >= 1e12 then return string.format("%.2fT", n / 1e12) end
	if n >= 1e9 then return string.format("%.2fB", n / 1e9) end
	if n >= 1e6 then return string.format("%.2fM", n / 1e6) end
	if n >= 1e3 then return string.format("%.1fK", n / 1e3) end
	return string.format("%d", n)
end

local function chanceOf(cfg, attrs)
	return tonumber(pv(cfg.chance, attrs)) or 0
end

local function plottedKeys()
	local set = {}
	for _, s in pairs(slots()) do
		if typeof(s) == "table" and s.unitId then set[s.unitId] = true end
	end
	return set
end
-- actions --------------------------------------------------------------------
local A = {}

function A.roll()
	if not R.Roll then return false end
	if os.clock() < (State.nextRollAt or 0) then return false end
	local res = R.Roll:InvokeServer()
	if typeof(res) ~= "table" then
		State.nextRollAt = os.clock() + 0.4
		local cap = inventoryCap()
		local n = #unitEntries()
		if n >= cap and os.clock() - (Stats.lastFullWarn or 0) > 15 then
			Stats.lastFullWarn = os.clock()
			log("roll blocked: " .. n .. "/" .. cap .. " units - freeing space", Color3.fromRGB(255, 200, 120))
		end
		if n >= cap - 5 and os.clock() - (State.lastEmergencySell or 0) > 4 then
			State.lastEmergencySell = os.clock()
			task.spawn(A.sell)
		end
		return false
	end
	State.nextRollAt = os.clock() + Cfg.rollInterval
	Stats.rolls = Stats.rolls + 1
	Stats.rolls = Stats.rolls + 1
	Stats.lastRollClock = os.clock()
	for _, r in ipairs(res) do
		if typeof(r) == "table" and r.result then
			local cfg = ER.getEntryConfig(r.result)
			local ch = cfg and chanceOf(cfg, { mutation = r.mutation }) or 0
			if ch >= Cfg.rareLogChance then
				local label = tostring(r.result)
				if r.mutation then label = tostring(r.mutation) .. " " .. label end
				table.insert(Stats.rares, 1, label .. " (1/" .. fmt(ch) .. ")")
				while #Stats.rares > 8 do table.remove(Stats.rares) end
				log("ROLLED " .. label .. "  1 in " .. fmt(ch), Color3.fromRGB(255, 105, 220))
			end
		end
	end
	return true
end

function A.sellList()
	local plotted = plottedKeys()
	local units = unitEntries()
	table.sort(units, function(a, b) return a.income > b.income end)
	local seen, list = {}, {}
	local function add(u)
		if not seen[u.key] then
			seen[u.key] = true
			table.insert(list, u.key)
		end
	end

	-- tier 1: junk pass - anything commoner than the sell threshold
	for i, u in ipairs(units) do
		if i > Cfg.keepTop and u.chance < Cfg.sellChance
			and not u.attrs.locked and not plotted[u.key]
			and not (Cfg.keepMutated and u.attrs.mutation) then
			add(u)
		end
	end

	local cap = inventoryCap()
	local remaining = #units - #list

	-- tier 2: compaction - hold the inventory down to the unit target
	if Cfg.maxUnits > 0 and remaining > Cfg.maxUnits then
		for i = #units, 1, -1 do
			if remaining <= Cfg.maxUnits then break end
			local u = units[i]
			if i > Cfg.keepTop and not u.attrs.locked and not plotted[u.key] and not seen[u.key] then
				add(u)
				remaining = remaining - 1
			end
		end
	end

	-- tier 3: keep free space (hard protection: locked, plotted, top-keep)
	if remaining > cap - Cfg.reserveUnits then
		for i = #units, 1, -1 do
			if remaining <= cap - Cfg.reserveUnits then break end
			local u = units[i]
			if i > Cfg.keepTop and not u.attrs.locked and not plotted[u.key]
				and rarityOrder(u.cfg.rarity) < Cfg.keepRarity and not seen[u.key] then
				add(u)
				remaining = remaining - 1
			end
		end
	end

	-- tier 4: emergency - the server is about to refuse every roll
	if remaining > cap - 5 then
		for i = #units, 1, -1 do
			if remaining <= cap - 10 then break end
			local u = units[i]
			if i > 3 and not u.attrs.locked and not plotted[u.key] and not seen[u.key] then
				add(u)
				remaining = remaining - 1
			end
		end
	end

	return list, #units, cap
end

function A.sell()
	if not R.SellInventory then return 0 end
	local list, unitCount, cap = A.sellList()
	if #list == 0 then
		if unitCount >= cap - 5 then
			log("nothing sellable but " .. unitCount .. "/" .. cap .. " units", Color3.fromRGB(255, 160, 120))
		end
		return 0
	end
	local sold, gained, i = 0, 0, 1
	while i <= #list do
		local batch = {}
		for j = i, math.min(i + Cfg.sellBatch - 1, #list) do
			table.insert(batch, list[j])
		end
		local moneyGained, unitsSold = R.SellInventory:InvokeServer(batch)
		if typeof(unitsSold) == "number" then sold = sold + unitsSold end
		if typeof(moneyGained) == "number" then gained = gained + moneyGained end
		i = i + Cfg.sellBatch
		if i <= #list then task.wait(0.15) end
	end
	if sold > 0 and (sold >= 3 or unitCount - sold > Cfg.maxUnits + 10) then
		Stats.soldUnits = Stats.soldUnits + sold
		Stats.soldMoney = Stats.soldMoney + gained
		log(string.format("sold %d units for %s  (%d/%d left)", sold, fmt(gained), unitCount - sold, cap), Color3.fromRGB(130, 255, 160))
	end
	return sold
end

function A.collect()
	if not R.CollectBalance then return 0 end
	local n = 0
	for i, s in pairs(slots()) do
		local idx = tonumber(i)
		if idx and typeof(s) == "table" and (tonumber(s.balance) or 0) > 0 then
			R.CollectBalance:FireServer(idx)
			n = n + 1
		end
	end
	return n
end

function A.equipBest()
	if R.EquipBest then R.EquipBest:FireServer() end
	if R.TowerEquipBest then R.TowerEquipBest:FireServer() end
end

local function isPriorityUpgrade(name)
	for _, pat in ipairs(Cfg.priorityUpgrades) do
		if string.find(name, pat, 1, true) then return true end
	end
	return false
end

function A.upgrades()
	if not (Upgrades and R.BuyUpgrade) then return 0 end
	local owned = pv(function() return D.Upgrades() end) or {}
	local cash = money()
	local cands = {}
	for name, u in pairs(Upgrades) do
		local price = tonumber(u.price)
		if not owned[name] and price then
			local parent = Tree and Tree.GetParent and pv(Tree.GetParent, name)
			if parent == nil or parent == "Start" or owned[parent] == true then
				table.insert(cands, { name = name, price = price })
			end
		end
	end
	table.sort(cands, function(a, b)
		local pa, pb = isPriorityUpgrade(a.name), isPriorityUpgrade(b.name)
		if pa ~= pb then return pa end
		return a.price < b.price
	end)
	local bought = 0
	for _, c in ipairs(cands) do
		if cash - c.price >= Cfg.moneyReserve then
			R.BuyUpgrade:FireServer(c.name)
			cash = cash - c.price
			bought = bought + 1
			if bought >= 3 then break end
			task.wait(0.05)
		else
			break
		end
	end
	if bought > 0 then log("bought " .. bought .. " upgrade(s)") end
	return bought
end
function A.quests()
	if not (QuestCfg and R.QuestClaim) then return 0 end
	local claimed = 0
	local state = pv(function() return D.Quests end)
	for pname, pcfg in pairs(QuestCfg.Periods or {}) do
		local snap
		if state and state[pname] then snap = pv(state[pname]) end
		if typeof(snap) == "table" then
			for _, quest in ipairs(pcfg.quests or {}) do
				local prog = (snap.progress or {})[quest.id] or 0
				if prog >= quest.target and not (snap.claimed or {})[quest.id] then
					R.QuestClaim:FireServer(pname, quest.id, snap.expiresAt)
					claimed = claimed + 1
					log("quest claimed: " .. pname .. " / " .. quest.id .. " (+" .. tostring(quest.tickets) .. " tickets)",
						Color3.fromRGB(150, 220, 255))
					task.wait(0.3)
				end
			end
		end
	end
	return claimed
end

function A.questShop()
	if not (QuestCfg and R.QuestBuy) then return 0 end
	local priority = {}
	for i, n in ipairs(Cfg.questShopPriority) do priority[n] = i end
	local items = {}
	for _, item in ipairs(QuestCfg.Shop or {}) do
		if not item.gamepass and priority[item.name] then
			table.insert(items, item)
		end
	end
	table.sort(items, function(a, b) return priority[a.name] < priority[b.name] end)
	local tickets = amountOf("Tickets")
	local bought, names = 0, {}
	for i = 1, Cfg.questShopPerTick do
		local did = false
		for _, item in ipairs(items) do
			if tickets >= item.tickets then
				R.QuestBuy:FireServer(item.name)
				tickets = tickets - item.tickets
				bought = bought + 1
				table.insert(names, item.name)
				did = true
				task.wait(0.3)
				break
			end
		end
		if not did then break end
	end
	if bought > 0 then log("quest shop: " .. table.concat(names, ", "), Color3.fromRGB(255, 220, 140)) end
	return bought
end

function A.claims()
	if R.DailyClaim then
		local last = tonumber(pv(function() return D.LastDailyRewardClaim() end)) or 0
		if os.time() - last >= 82800 then
			R.DailyClaim:FireServer()
			log("daily reward claimed")
		end
	end
	if R.GroupClaim and not pv(function() return D.ClaimedGroupReward() end) then
		R.GroupClaim:FireServer()
	end
	if R.OfflineClaim and (tonumber(pv(function() return D.PendingOfflineEarnings() end)) or 0) > 0 then
		R.OfflineClaim:FireServer()
		log("offline earnings claimed")
	end
end

function A.gradeTrait()
	local gems = amountOf("Gems")
	local rerolls = amountOf("Trait Reroll")
	if (not Cfg.enabled.autoGrade or gems < 1) and (not Cfg.enabled.autoTrait or rerolls < 1) then return false end
	local units = unitEntries()
	table.sort(units, function(a, b) return a.income > b.income end)
	for _, u in ipairs(units) do
		if Cfg.enabled.autoGrade and gems >= 1 then
			local g = Grades and u.attrs.grade and Grades[u.attrs.grade]
			local mult = (g and tonumber(g.incomeMultiplier)) or 1
			if mult < Cfg.gradeTarget then
				R.GradeRoll:FireServer(u.key, false)
				gems = gems - 1
				return true
			end
		end
		if Cfg.enabled.autoTrait and rerolls >= 1 then
			local t = Traits and u.attrs.trait and Traits[u.attrs.trait]
			local mult = (t and tonumber(t.incomeMultiplier)) or 0
			if mult < Cfg.traitTarget then
				R.TraitRoll:FireServer(u.key, false)
				rerolls = rerolls - 1
				return true
			end
		end
	end
	return false
end

function A.dice()
	if not (DiceCfg and R.BuyDice) then return end
	local all = pv(DiceCfg.GetAll) or {}
	local owned = pv(function() return D.OwnedDice() end) or {}
	local cash = money()
	local bestOwned, bestLuck = nil, -1
	local bestBuy, bestBuyLuck = nil, -1
	for name, cfg in pairs(all) do
		local luck = tonumber(cfg.luck) or 0
		if owned[name] then
			if luck > bestLuck then bestLuck, bestOwned = luck, name end
		else
			local price = tonumber(cfg.price)
			if price and price <= cash * Cfg.diceSpendFrac and luck > bestBuyLuck then
				bestBuyLuck, bestBuy = luck, name
			end
		end
	end
	if bestBuy and bestBuyLuck > math.max(bestLuck, 0) * 1.3 then
		R.BuyDice:FireServer(bestBuy)
		log("bought dice " .. bestBuy .. " (luck x" .. tostring(bestBuyLuck) .. ")")
		task.wait(0.5)
		return
	end
	local equipped = pv(function() return D.Dice() end)
	if bestOwned and equipped ~= bestOwned and R.EquipDice then
		R.EquipDice:FireServer(bestOwned)
		log("equipped dice " .. bestOwned .. " (luck x" .. tostring(bestLuck) .. ")")
	end
end

function A.boostSpins()
	local done = 0
	for key, e in pairs(inventory()) do
		local cfg = ER.getEntryConfig(e.name)
		if cfg and cfg.kind == "Spin" and R.SpinUse then
			R.SpinUse:FireServer(key)
			done = done + 1
		elseif cfg and cfg.kind == "Boost" and R.BoostUse then
			R.BoostUse:FireServer(key)
			done = done + 1
		end
		if done >= 3 then break end
		task.wait(0.05)
	end
	if done > 0 then log("used " .. done .. " boost/spin") end
	return done
end

function A.rebirth()
	if not (Rebirths and R.Rebirth) then return end
	local cur = tonumber(pv(function() return D.Rebirth() end)) or 0
	local next_ = pv(Rebirths.GetNext, cur)
	if next_ and money() >= (tonumber(next_.cost) or math.huge) then
		R.Rebirth:FireServer()
		log("REBIRTH -> " .. tostring(cur + 1) .. "  (luck x" .. tostring(next_.luckMultiplier) .. ")", Color3.fromRGB(255, 205, 90))
	end
end

function A.fuse()
	if State.fuseActive or not R.Fuse then return end
	local plotted = plottedKeys()
	local units = unitEntries()
	if #units < Cfg.fuseFloorUnits then return end
	table.sort(units, function(a, b) return a.income > b.income end)
	local pool = {}
	for i, u in ipairs(units) do
		if i > Cfg.keepTop and u.amount == 1 and not u.cfg.limited and not u.attrs.locked
			and not plotted[u.key] and u.chance <= Cfg.fuseMaxChance then
			table.insert(pool, u)
		end
	end
	if #pool < 3 then return end
	table.sort(pool, function(a, b) return a.income < b.income end)
	local a1, a2, a3 = pool[1], pool[2], pool[3]
	local cost = (FusingCfg and FusingCfg.GetCost(a1.chance + a2.chance + a3.chance)) or math.huge
	if money() < math.max(Cfg.fuseMinMoney, cost * Cfg.fuseMoneyMultiple) then return end
	State.fuseActive = true
	State.lastFuse = { a1.name, a2.name, a3.name }
	R.Fuse:FireServer(a1.key, a2.key, a3.key)
	task.delay(5, function() State.fuseActive = false end)
end

if R.FuseResult then
	State.fuseConn = R.FuseResult.OnClientEvent:Connect(function(unit, err)
		if err ~= nil then
			local msg = tostring(err)
			local colour = Color3.fromRGB(255, 120, 120)
			if string.find(msg, "Please wait", 1, true) then
				colour = Color3.fromRGB(150, 150, 160)
			end
			log("fuse: " .. msg, colour)
			return
		end
		if typeof(unit) ~= "table" then return end
		local cfg = ER.getEntryConfig(unit.name)
		local ch = cfg and chanceOf(cfg, unit.attributes) or 0
		local label = tostring(unit.name)
		if unit.attributes and unit.attributes.mutation then
			label = tostring(unit.attributes.mutation) .. " " .. label
		end
		local consumed = State.lastFuse and table.concat(State.lastFuse, " + ") or "3 units"
		log(string.format("FUSED %s -> %s  (1 in %s)", consumed, label, fmt(ch)), Color3.fromRGB(255, 200, 120))
	end)
end
function A.tower()
	if not (TowersCfg and R.TowerPlay) then return end
	if State.towerActive then
		local res = R.TowerFloor:InvokeServer()
		if res == nil then
			State.towerStalls = State.towerStalls + 1
			if State.towerStalls >= 5 then
				R.TowerCancel:InvokeServer()
				State.towerActive = false
				State.towerStalls = 0
				State.towerCooldown = os.clock() + 8
			end
		else
			State.towerStalls = 0
		end
		return
	end
	if (State.towerCooldown or 0) > os.clock() then return end
	local team = pv(function() return D.TowerTeam() end)
	local hasTeam = false
	if typeof(team) == "table" then
		for _, v in pairs(team) do
			if v then hasTeam = true break end
		end
	end
	if not hasTeam then
		if R.TowerEquipBest then R.TowerEquipBest:FireServer() end
		State.towerCooldown = os.clock() + 3
		return
	end
	local name = Cfg.towerName
	if not name then
		local all = pv(TowersCfg.GetAll)
		local best, order = nil, -1
		if typeof(all) == "table" then
			for tname, tcfg in pairs(all) do
				local o = tonumber(tcfg.order) or 0
				if o > order then order, best = o, tname end
			end
		end
		name = best
	end
	if not name then return end
	if R.TowerPlay:InvokeServer(name) then
		State.towerActive = true
		State.towerStalls = 0
		log("tower started: " .. name, Color3.fromRGB(255, 180, 120))
	end
end

function A.redeem(silent)
	if not (MonetCfg and MonetCfg.Codes and R.Redeem) then return 0 end
	local done = pv(function() return D.RedeemedCodes() end) or {}
	local n = 0
	for code in pairs(MonetCfg.Codes) do
		if not done[code] then
			R.Redeem:FireServer(code)
			n = n + 1
			task.wait(0.6)
		end
	end
	if n > 0 and not silent then
		log("redeemed " .. n .. " code(s)", Color3.fromRGB(180, 255, 140))
	end
	return n
end
-- scheduler (Heartbeat driven: every action is a cheap state check, real work
-- runs in its own thread so a blocking InvokeServer never stalls the frame) ---
local Tasks = {}
local function register(name, interval, fn, gate)
	Tasks[name] = { interval = interval, fn = fn, gate = gate, last = 0, running = false }
end

local moneySamples, rollSamples = {}, {}
local function updateStats()
	Stats.money = money()
	local now = os.clock()
	local prev = State.lastMoney
	if prev and Stats.money > prev then
		Stats.earned = Stats.earned + (Stats.money - prev)
	end
	State.lastMoney = Stats.money
	table.insert(moneySamples, { t = now, m = Stats.earned })
	while #moneySamples > 2 and now - moneySamples[1].t > 60 do table.remove(moneySamples, 1) end
	local a, b = moneySamples[1], moneySamples[#moneySamples]
	if b.t - a.t > 5 then Stats.moneyPerMin = (b.m - a.m) / (b.t - a.t) * 60 end

	table.insert(rollSamples, { t = now, n = Stats.rolls })
	while #rollSamples > 2 and now - rollSamples[1].t > 60 do table.remove(rollSamples, 1) end
	local ra, rb = rollSamples[1], rollSamples[#rollSamples]
	if rb.t - ra.t > 5 then Stats.rollsPerMin = (rb.n - ra.n) / (rb.t - ra.t) * 60 end
end

register("roll",      function() return 0.4 end, function() A.roll() end, "autoRoll")
register("collect",   function() return Cfg.collectInterval end, A.collect, "autoCollect")
register("sell",      function() return (#unitEntries() >= inventoryCap() - Cfg.reserveUnits) and 2 or Cfg.sellInterval end, A.sell, "autoSell")
register("equipBest", 20, A.equipBest, "autoEquipBest")
register("upgrades",  function() return Cfg.upgradeInterval end, A.upgrades, "autoUpgrades")
register("quests",    function() return Cfg.questInterval end, A.quests, "autoQuests")
register("questShop", function() return Cfg.questInterval end, A.questShop, "autoQuestShop")
register("claims",    function() return Cfg.claimInterval end, A.claims, "autoClaims")
register("gradeTrait",function() return Cfg.gradeInterval end, A.gradeTrait, function() return Cfg.enabled.autoGrade or Cfg.enabled.autoTrait end)
register("dice",      function() return Cfg.diceInterval end, A.dice, "autoDice")
register("boostSpins",30, A.boostSpins, "autoBoostSpins")
register("redeem",    5, A.redeem, "autoRedeem")
register("rebirth",   10, A.rebirth, "autoRebirth")
register("fuse",      function() return Cfg.fuseInterval end, A.fuse, "autoFuse")
register("tower",     function() return Cfg.towerInterval end, A.tower, "autoTower")
register("killAutoRoll", 10, function()
	if pv(function() return D.AutoRoll() end) then
		R.SetAutoRoll:FireServer(false)
		log("game AutoRoll switched off", Color3.fromRGB(160, 200, 255))
	end
end, "killAutoRoll")
register("stats",     1, updateStats, nil)

local function runTask(name, t)
	local iv = typeof(t.interval) == "function" and t.interval() or t.interval
	if os.clock() - t.last < iv then return end
	t.last = os.clock()
	t.running = true
	task.spawn(function()
		local ok, err = pcall(t.fn)
		t.running = false
		if not ok then
			log(name .. ": " .. tostring(err), Color3.fromRGB(255, 120, 120))
		end
	end)
end

local function schedulerTick()
	for name, t in pairs(Tasks) do
		local gate = t.gate
		local on
		if gate == nil then
			on = true
		elseif typeof(gate) == "function" then
			on = gate()
		else
			on = Cfg.enabled[gate]
		end
		if on and not t.running then
			runTask(name, t)
		end
	end
end

State.scheduler = RunService.Heartbeat:Connect(function()
	if not State.running then return end
	local ok, err = pcall(schedulerTick)
	if not ok then
		warn("[ADH] scheduler: " .. tostring(err))
	end
end)
-- UI -------------------------------------------------------------------------
local Theme = {
	bg     = Color3.fromRGB(16, 16, 20),
	panel  = Color3.fromRGB(26, 26, 34),
	row    = Color3.fromRGB(36, 36, 46),
	accent = Color3.fromRGB(255, 92, 172),
	text   = Color3.fromRGB(236, 236, 242),
	dim    = Color3.fromRGB(152, 152, 166),
	good   = Color3.fromRGB(122, 255, 152),
	bad    = Color3.fromRGB(255, 118, 118),
}

local function new(class, props, parent)
	local o = Instance.new(class)
	for k, v in pairs(props) do
		o[k] = v
	end
	o.Parent = parent
	return o
end

local playerGui = plr:WaitForChild("PlayerGui")
local stale = playerGui:FindFirstChild("AnimeDiceHub")
if stale then stale:Destroy() end

local gui = new("ScreenGui", {
	Name = "AnimeDiceHub", ResetOnSpawn = false,
	ZIndexBehavior = Enum.ZIndexBehavior.Sibling, DisplayOrder = 999,
}, playerGui)

local main = new("Frame", {
	Name = "Main", Size = UDim2.new(0, 334, 0, 476),
	Position = UDim2.new(0, 18, 0.5, -238), BackgroundColor3 = Theme.bg,
	BorderSizePixel = 0, Active = true,
}, gui)
new("UICorner", { CornerRadius = UDim.new(0, 10) }, main)
new("UIStroke", { Color = Theme.accent, Transparency = 0.55, Thickness = 1 }, main)

local header = new("Frame", {
	Size = UDim2.new(1, 0, 0, 34), BackgroundColor3 = Theme.panel, BorderSizePixel = 0,
}, main)
new("UICorner", { CornerRadius = UDim.new(0, 10) }, header)
new("TextLabel", {
	Size = UDim2.new(1, -180, 1, 0), Position = UDim2.new(0, 10, 0, 0),
	BackgroundTransparency = 1, Text = "ANIME DICE HUB  v" .. HUB_VER,
	Font = Enum.Font.GothamBold, TextSize = 14, TextColor3 = Theme.accent,
	TextXAlignment = Enum.TextXAlignment.Left,
}, header)
local closeBtn = new("TextButton", {
	Size = UDim2.new(0, 24, 0, 22), Position = UDim2.new(1, -30, 0, 6),
	BackgroundColor3 = Theme.bad, Text = "X", Font = Enum.Font.GothamBold,
	TextSize = 12, TextColor3 = Color3.new(0, 0, 0), BorderSizePixel = 0,
}, header)
new("UICorner", { CornerRadius = UDim.new(0, 6) }, closeBtn)
local stopBtn = new("TextButton", {
	Size = UDim2.new(0, 66, 0, 22), Position = UDim2.new(1, -102, 0, 6),
	BackgroundColor3 = Theme.accent, Text = "STOP ALL", Font = Enum.Font.GothamBold,
	TextSize = 10, TextColor3 = Color3.new(0, 0, 0), BorderSizePixel = 0,
}, header)
new("UICorner", { CornerRadius = UDim.new(0, 6) }, stopBtn)

local statsLabel = new("TextLabel", {
	Size = UDim2.new(1, -16, 0, 78), Position = UDim2.new(0, 8, 0, 40),
	BackgroundColor3 = Theme.panel, BorderSizePixel = 0, Text = "", Font = Enum.Font.Code,
	TextSize = 12, TextColor3 = Theme.text, TextXAlignment = Enum.TextXAlignment.Left,
	TextYAlignment = Enum.TextYAlignment.Top,
}, main)
new("UICorner", { CornerRadius = UDim.new(0, 8) }, statsLabel)
new("UIPadding", { PaddingLeft = UDim.new(0, 8), PaddingTop = UDim.new(0, 5) }, statsLabel)

local scroll = new("ScrollingFrame", {
	Size = UDim2.new(1, -16, 1, -246), Position = UDim2.new(0, 8, 0, 124),
	BackgroundTransparency = 1, BorderSizePixel = 0, CanvasSize = UDim2.new(0, 0, 0, 0),
	AutomaticCanvasSize = Enum.AutomaticSize.Y, ScrollingDirection = Enum.ScrollingDirection.Y,
	ScrollBarThickness = 5, ScrollBarImageColor3 = Theme.accent,
}, main)
new("UIListLayout", { Padding = UDim.new(0, 4), SortOrder = Enum.SortOrder.LayoutOrder }, scroll)
new("UIPadding", { PaddingRight = UDim.new(0, 6), PaddingBottom = UDim.new(0, 6) }, scroll)

local logLabel = new("TextLabel", {
	Size = UDim2.new(1, -16, 0, 100), Position = UDim2.new(0, 8, 1, -118),
	BackgroundColor3 = Theme.panel, BorderSizePixel = 0, Text = "", Font = Enum.Font.Code,
	TextSize = 11, TextColor3 = Theme.dim, TextXAlignment = Enum.TextXAlignment.Left,
	TextYAlignment = Enum.TextYAlignment.Top, RichText = true,
}, main)
new("UICorner", { CornerRadius = UDim.new(0, 8) }, logLabel)
new("UIPadding", { PaddingLeft = UDim.new(0, 7), PaddingTop = UDim.new(0, 4) }, logLabel)

new("TextLabel", {
	Size = UDim2.new(1, -16, 0, 12), Position = UDim2.new(0, 8, 1, -16),
	BackgroundTransparency = 1, Text = "made with Project UAI  ·  model deepseek-v4-flash",
	Font = Enum.Font.Gotham, TextSize = 9, TextColor3 = Theme.dim,
	TextXAlignment = Enum.TextXAlignment.Center,
}, main)

local order = 0
local function nextOrder()
	order = order + 1
	return order
end

local refreshers = {}

local function toggleRow(key, label)
	local row = new("TextButton", {
		Size = UDim2.new(1, 0, 0, 24), BackgroundColor3 = Theme.row, Text = "",
		AutoButtonColor = true, BorderSizePixel = 0, LayoutOrder = nextOrder(),
	}, scroll)
	new("UICorner", { CornerRadius = UDim.new(0, 6) }, row)
	new("TextLabel", {
		Size = UDim2.new(1, -70, 1, 0), Position = UDim2.new(0, 8, 0, 0), BackgroundTransparency = 1,
		Text = label, Font = Enum.Font.Gotham, TextSize = 13, TextColor3 = Theme.text,
		TextXAlignment = Enum.TextXAlignment.Left,
	}, row)
	local state = new("TextLabel", {
		Size = UDim2.new(0, 54, 1, 0), Position = UDim2.new(1, -58, 0, 0), BackgroundTransparency = 1,
		Text = "", Font = Enum.Font.GothamBold, TextSize = 12,
		TextXAlignment = Enum.TextXAlignment.Right,
	}, row)
	local function refresh()
		local on = Cfg.enabled[key]
		state.Text = on and "ON" or "OFF"
		state.TextColor3 = on and Theme.good or Theme.bad
		row.BackgroundColor3 = on and Color3.fromRGB(30, 48, 36) or Theme.row
	end
	row.MouseButton1Click:Connect(function()
		Cfg.enabled[key] = not Cfg.enabled[key]
		refresh()
		if key == "serverAutoSell" and R.SetSetting then
			R.SetSetting:FireServer("AutoSell", Cfg.enabled[key] and Cfg.serverAutoSellValue or 0)
			log("server AutoSell = " .. tostring(Cfg.enabled[key] and Cfg.serverAutoSellValue or 0))
		end
	end)
	refresh()
	refreshers[key] = refresh
	return row
end

local function numberRow(cfgKey, label, suffix)
	local row = new("Frame", {
		Size = UDim2.new(1, 0, 0, 24), BackgroundColor3 = Theme.row,
		BorderSizePixel = 0, LayoutOrder = nextOrder(),
	}, scroll)
	new("UICorner", { CornerRadius = UDim.new(0, 6) }, row)
	new("TextLabel", {
		Size = UDim2.new(1, -100, 1, 0), Position = UDim2.new(0, 8, 0, 0), BackgroundTransparency = 1,
		Text = label .. (suffix and ("  (" .. suffix .. ")") or ""), Font = Enum.Font.Gotham,
		TextSize = 12, TextColor3 = Theme.dim, TextXAlignment = Enum.TextXAlignment.Left,
	}, row)
	local box = new("TextBox", {
		Size = UDim2.new(0, 84, 0, 18), Position = UDim2.new(1, -92, 0, 3),
		BackgroundColor3 = Theme.panel, Text = tostring(Cfg[cfgKey]), Font = Enum.Font.Code,
		TextSize = 12, TextColor3 = Theme.text, ClearTextOnFocus = false,
		TextXAlignment = Enum.TextXAlignment.Center, BorderSizePixel = 0,
	}, row)
	new("UICorner", { CornerRadius = UDim.new(0, 5) }, box)
	box.FocusLost:Connect(function()
		local n = tonumber(box.Text)
		if n then
			Cfg[cfgKey] = n
			box.Text = tostring(n)
			if cfgKey == "serverAutoSellValue" and Cfg.enabled.serverAutoSell and R.SetSetting then
				R.SetSetting:FireServer("AutoSell", n)
			end
			log(cfgKey .. " = " .. tostring(n))
		else
			box.Text = tostring(Cfg[cfgKey])
		end
	end)
	return row
end

local function buttonRow(label, colour, fn)
	local row = new("TextButton", {
		Size = UDim2.new(1, 0, 0, 26), BackgroundColor3 = colour, Text = label,
		Font = Enum.Font.GothamBold, TextSize = 12, TextColor3 = Color3.new(0, 0, 0),
		BorderSizePixel = 0, LayoutOrder = nextOrder(),
	}, scroll)
	new("UICorner", { CornerRadius = UDim.new(0, 6) }, row)
	row.MouseButton1Click:Connect(function()
		task.spawn(function()
			local ok, err = pcall(fn)
			if not ok then log("action failed: " .. tostring(err), Theme.bad) end
		end)
	end)
	return row
end
-- roll-showcase suppressor --------------------------------------------------
-- The rare-roll showcase is client-only (RollCutscene): it parents a ScreenGui
-- named "CutsceneBlackFade", clones Assets/.../CameraModel + lines into Workspace,
-- anchors your character, sets the camera to Scriptable and drives it from
-- CamPart, then reveals a "1 in X" counter behind black bars for ~5s.
-- This kills that, and only that: the dice roll animation / roll UI are untouched,
-- and nothing here talks to the server.
local function killShowcase()
	for _, g in ipairs(playerGui:GetChildren()) do
		if g:IsA("ScreenGui") and g.Name ~= "AnimeDiceHub" and string.find(g.Name, "Cutscene", 1, true) then
			pcall(function() g:Destroy() end)
		end
	end

	local cut = UIRefs and UIRefs.Root and UIRefs.Root:FindFirstChild("Cutscene")
	if cut then
		for _, c in ipairs(cut:GetChildren()) do
			if c:IsA("GuiObject") then c.Visible = false end
		end
	end

	for _, m in ipairs(workspace:GetChildren()) do
		if m.Name == "CameraModel" or m.Name == "lines" then
			for _, d in ipairs(m:GetDescendants()) do
				if d.Name == "CamPart" then pcall(function() d:Destroy() end) end
			end
			if m.Name == "lines" then pcall(function() m:Destroy() end) end
		end
	end

	local cam = workspace.CurrentCamera
	local char = plr.Character
	if char then
		local hrp = char:FindFirstChild("HumanoidRootPart")
		if hrp then hrp.Anchored = false end
		for _, d in ipairs(char:GetDescendants()) do
			if d:IsA("BasePart") then d.LocalTransparencyModifier = 0 end
		end
	end
	if cam then
		local hum = char and char:FindFirstChildOfClass("Humanoid")
		if cam.CameraType == Enum.CameraType.Scriptable then
			cam.CameraType = Enum.CameraType.Custom
		end
		if hum then cam.CameraSubject = hum end
	end
end

State.cutsceneConn = playerGui.DescendantAdded:Connect(function(inst)
	if not Cfg.enabled.killShowcase then return end
	if not inst:IsA("ScreenGui") then return end
	local isCutscene = inst.Name == "CutsceneBlackFade"
	if isCutscene then
		State.suppressUntil = os.clock() + 6
	end
	if isCutscene or os.clock() < (State.suppressUntil or 0) then
		pcall(function() inst:Destroy() end)
		if isCutscene then
			-- the showcase sets Scriptable ~0.5s in, so sweep the whole window
			task.spawn(killShowcase)
			for _, d in ipairs({ 0.35, 0.7, 1.2, 2.5, 4.2 }) do
				task.delay(d, killShowcase)
			end
			if os.clock() - (State.lastShowcaseLog or 0) > 5 then
				State.lastShowcaseLog = os.clock()
				log("roll showcase suppressed", Color3.fromRGB(160, 200, 255))
			end
		end
	end
end)

-- clear one that is already mid-flight when the hub loads
task.spawn(function()
	if Cfg.enabled.killShowcase then pcall(killShowcase) end
end)
toggleRow("autoRoll", "Auto Roll")
toggleRow("autoSell", "Auto Sell Junk")
toggleRow("autoCollect", "Auto Collect Plot")
toggleRow("autoEquipBest", "Auto Equip Best")
toggleRow("autoUpgrades", "Auto Buy Upgrades")
toggleRow("autoDice", "Auto Dice (buy / equip best)")
toggleRow("autoQuests", "Auto Claim Quests")
toggleRow("autoQuestShop", "Auto Spend Tickets")
toggleRow("autoClaims", "Auto Daily / Group / Offline")
toggleRow("autoGrade", "Auto Grade  (1 Gem)")
toggleRow("autoTrait", "Auto Trait  (1 Reroll)")
toggleRow("autoBoostSpins", "Auto Use Boosts / Spins")
toggleRow("autoRedeem", "Auto Redeem Codes")
toggleRow("autoRebirth", "Auto Rebirth")
toggleRow("autoTower", "Auto Tower")
toggleRow("autoFuse", "Auto Fuse Junk -> Rare")
toggleRow("killShowcase", "Kill Roll Showcase")
toggleRow("killAutoRoll", "Kill In-Game AutoRoll")
toggleRow("serverAutoSell", "Server AutoSell (profile)")

numberRow("rollInterval", "Roll interval", "s")
numberRow("collectInterval", "Collect interval", "s")
numberRow("sellInterval", "Sell interval", "s")
numberRow("sellChance", "Sell below 1-in-X", "chance")
numberRow("keepTop", "Keep best units", "count")
numberRow("maxUnits", "Sell above N units", "count")
numberRow("reserveUnits", "Free space reserve", "count")
numberRow("fuseMaxChance", "Fuse below 1-in-X", "chance")
numberRow("gradeTarget", "Grade target", "income x")
numberRow("traitTarget", "Trait target", "income x")
numberRow("serverAutoSellValue", "Server AutoSell value", "1-in-X")

buttonRow("SELL JUNK NOW", Theme.accent, function() A.sell() end)
buttonRow("COLLECT NOW", Color3.fromRGB(150, 220, 255), function()
	local n = A.collect()
	log("collected " .. tostring(n) .. " slot(s)", Color3.fromRGB(150, 220, 255))
end)
buttonRow("EQUIP BEST + TOWER TEAM", Color3.fromRGB(180, 255, 160), function() A.equipBest() end)
buttonRow("CLAIM QUESTS + REWARDS", Color3.fromRGB(255, 220, 140), function()
	A.quests()
	A.claims()
end)
buttonRow("REDEEM ALL CODES", Color3.fromRGB(200, 180, 255), function() A.redeem() end)
buttonRow("USE BOOSTS / SPINS", Color3.fromRGB(255, 170, 200), function() A.boostSpins() end)
buttonRow("FUSE 3 COMMONS", Color3.fromRGB(255, 200, 120), function() A.fuse() end)

-- live UI refresh ------------------------------------------------------------
local function hex(c)
	return string.format("#%02X%02X%02X", math.floor(c.R * 255), math.floor(c.G * 255), math.floor(c.B * 255))
end

local uiAcc = 0
State.uiConn = RunService.Heartbeat:Connect(function(dt)
	if not State.running then return end
	local ok, err = pcall(function()
	uiAcc = uiAcc + dt
	if uiAcc < 0.5 then return end
	uiAcc = 0

	local units, gems, rerolls, tickets, invCount = 0, 0, 0, 0, 0
	for _, e in pairs(inventory()) do
		invCount = invCount + 1
		if e.name == "Gems" then gems = gems + (tonumber(e.amount) or 0)
		elseif e.name == "Trait Reroll" then rerolls = rerolls + (tonumber(e.amount) or 0)
		elseif e.name == "Tickets" then tickets = tickets + (tonumber(e.amount) or 0)
		else
			local cfg = ER.getEntryConfig(e.name)
			if cfg and cfg.kind == "Unit" then units = units + 1 end
		end
	end

	statsLabel.Text = table.concat({
		string.format("Money    %-9s  +%s/min", fmt(Stats.money), fmt(Stats.moneyPerMin)),
		string.format("Rolls    %-9d  %.0f/min   sold %d (%s)", Stats.rolls, Stats.rollsPerMin, Stats.soldUnits, fmt(Stats.soldMoney)),
		string.format("Units    %d/%d   rebirth %s   dice %s", units, inventoryCap(),
			tostring(pv(function() return D.Rebirth() end)), tostring(pv(function() return D.Dice() end))),
		string.format("Buffs    Luck x%s  Money x%s  Roll %ss", tostring(buff("Luck")), tostring(buff("Money Multiplier")), tostring(buff("Roll Duration"))),
		string.format("Gems %-6d Rerolls %-6d Tickets %-6d", gems, rerolls, tickets),
		string.format("Rares: %s", #Stats.rares > 0 and Stats.rares[1] or "-"),
	}, "\n")

	local lines = {}
	for i = 1, math.min(7, #Stats.log) do
		local e = Stats.log[i]
		table.insert(lines, "<font color='" .. hex(e.colour) .. "'>" .. e.text .. "</font>")
	end
	logLabel.Text = table.concat(lines, "\n")
	end)
	if not ok then warn("[ADH] ui: " .. tostring(err)) end
end)

-- dragging -------------------------------------------------------------------
local dragging, dragStart, startPos = false, nil, nil
header.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
		dragging = true
		dragStart = input.Position
		startPos = main.Position
		local conn
		conn = input.Changed:Connect(function()
			if input.UserInputState == Enum.UserInputState.End then
				dragging = false
				conn:Disconnect()
			end
		end)
	end
end)
State.dragConn = UserInputService.InputChanged:Connect(function(input)
	if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
		local d = input.Position - dragStart
		main.Position = UDim2.new(startPos.X.Scale, startPos.X.Offset + d.X, startPos.Y.Scale, startPos.Y.Offset + d.Y)
	end
end)
State.keyConn = UserInputService.InputBegan:Connect(function(input, gpe)
	if gpe then return end
	if input.KeyCode == Cfg.uiKey then
		main.Visible = not main.Visible
	end
end)
closeBtn.MouseButton1Click:Connect(function()
	main.Visible = false
end)

-- API ------------------------------------------------------------------------
local API = {}
API.version = HUB_VER
API.cfg = Cfg
API.stats = Stats
API.actions = A
API.tasks = Tasks

function API.set(k, v)
	Cfg[k] = v
	if refreshers[k] then refreshers[k]() end
end

function API.toggle(k)
	Cfg.enabled[k] = not Cfg.enabled[k]
	if refreshers[k] then refreshers[k]() end
	if k == "serverAutoSell" and R.SetSetting then
		R.SetSetting:FireServer("AutoSell", Cfg.enabled[k] and Cfg.serverAutoSellValue or 0)
	end
	return Cfg.enabled[k]
end

API.sellNow = A.sell
API.collectNow = A.collect
API.equipBest = A.equipBest
API.claimNow = A.claims
API.redeemAll = A.redeem
API.log = log

function API.stop()
	getgenv().__ADH_STOP = debug.traceback("[ADH] stop called", 2)
	State.running = false
	for _, key in ipairs({ "scheduler", "uiConn", "dragConn", "keyConn", "fuseConn", "cutsceneConn" }) do
		local c = State[key]
		if typeof(c) == "RBXScriptConnection" then pcall(function() c:Disconnect() end) end
		State[key] = nil
	end
	if gui then gui:Destroy() end
	getgenv().__ANIME_DICE_HUB = nil
	warn("[ADH] stopped")
end

stopBtn.MouseButton1Click:Connect(function() API.stop() end)
getgenv().__ANIME_DICE_HUB = API

-- startup --------------------------------------------------------------------
task.spawn(function()
	log("ANIME DICE HUB v" .. HUB_VER .. " online  ·  Project UAI / deepseek-v4-flash")
	print(string.format("[ADH] hub loaded: %d tasks, %d remotes", (function() local n = 0 for _ in pairs(Tasks) do n = n + 1 end return n end)(), 33))
	pcall(A.claims)
	pcall(A.collect)
	pcall(A.equipBest)
	pcall(A.quests)
	pcall(A.redeem)
	pcall(A.boostSpins)
	warn("[ADH] ready - RightShift toggles the UI")
end)

return API

Features & Information

--[[ ANIME DICE HUB — place 113290951185459 ("[UPD 5] Anime Dice") Built with Project UAI · model deepseek-v4-flash Built from the decompiled server source (all Framework.Features services are replicated to the client, so every remote, arg shape and server check is known). Everything here is automation of the game's own API. The server is authoritative: it re-rolls every rarity/grade/trait server-side, recomputes prices and holds the profile. Nothing here can forge a reward — the hub just drives the normal calls faster than a human can and never lets the inventory back up. Toggle UI: RightShift. Panic stop: the UI "STOP ALL" button, or getgenv().__ANIME_DICE_HUB.stop() API: getgenv().__ANIME_DICE_HUB .cfg config table (enabled flags + tunables) .stats live counters .set(k, v) set a config value .toggle(k) flip an enabled flag .sellNow() .collectNow() .equipBest() .claimNow() .redeemAll() .stop() stop everything and disconnect ]]
#autofarm#ai

Community Comments (0)

No comments yet for this script.

More Scripts for [🎉 UPD 7] Anime Dice