NextGoal.luau
Luau, 92 lines from UI overview
--!strict
-- NextGoal: config-driven "Next goal" pill. An ordered list of rules; the first rule whose goal is
-- not yet met wins. Each rule returns what the pill shows and what a tap does.
-- Put this in ReplicatedStorage next to the shared formula/config modules, so the pill is computed
-- from the same numbers the server enforces.
export type Goal = {
id: string,
text: string, -- "Get 700 Iron for Tier 3"
have: number?, -- progress, e.g. 420
need: number?, -- e.g. 700 (pill shows "420/700" + bar when both are set)
ready: boolean?, -- green READY style
endsAt: number?, -- countdown (os.time() seconds) for "Researching Tier 3 2:32"
action: string, -- what a tap does: "openResearch", "openWar", "openSell", "pulseProducer"
arg: any?, -- e.g. the research node id or the producer kind
}
export type Rule = (state: any, config: any) -> Goal?
local NextGoal = {}
-- Example rules for a tier-gated game. Each returns a Goal when it is the current blocker, else nil.
-- Order matters: it is the order a player should solve things in.
NextGoal.rules = {
function(s, cfg): Goal?
if s.research.active and s.research.active.isTierGate then
return { id = "researching", text = "Researching Tier " .. s.tier + 1,
endsAt = s.research.active.endsAt, action = "openResearch" }
end
return nil
end,
function(s, cfg): Goal?
local gate = cfg.tiers[s.tier + 1]
if not gate then return nil end
local left = gate.conquests - s.conquests
if left > 0 then
return { id = "conquer", text = string.format("Conquer %d more", left),
have = s.conquests, need = gate.conquests, action = "openWar" }
end
return nil
end,
function(s, cfg): Goal?
local gate = cfg.tiers[s.tier + 1]
if not gate then return nil end
for res, amount in gate.resources do
local have = s.resources[res] or 0
if have < amount then
return { id = "res_" .. res,
text = string.format("Get %d %s for Tier %d", amount, cfg.names[res], s.tier + 1),
have = have, need = amount, action = "pulseProducer", arg = res }
end
end
if s.cash < gate.cash then
return { id = "cash", text = string.format("Save $%d for Tier %d", gate.cash, s.tier + 1),
have = s.cash, need = gate.cash, action = "openSell" }
end
if s.research.active then
return { id = "labBusy", text = "Lab busy: finishes first",
endsAt = s.research.active.endsAt, action = "openResearch" }
end
return { id = "ready", text = "Research Tier " .. s.tier + 1, ready = true,
action = "openResearch", arg = gate.researchNode }
end,
}
function NextGoal.resolve(state: any, config: any): Goal?
if state.tutorialActive then
return nil -- the tutorial card is the goal while it runs
end
for _, rule in NextGoal.rules do
local g = rule(state, config)
if g then return g end
end
return nil
end
-- Wire-up sketch (client):
-- local last
-- local function refresh()
-- local g = NextGoal.resolve(State.get(), Config)
-- local key = g and (g.id .. tostring(g.have) .. tostring(g.need)) or ""
-- if key == last then return end -- update in place only when something changed
-- last = key
-- UI.setGoal(g) -- hides the pill when g is nil
-- end
-- State.changed:Connect(refresh)
-- Pill.Activated:Connect(function() local g = NextGoal.resolve(State.get(), Config)
-- if g then Actions[g.action](g.arg) end end)
-- (a separate 1 s loop only re-renders the countdown text when g.endsAt is set)
return NextGoal