Director
The referee of GREASEPAINT. One of these sits on the scene's `Director` anchor and owns `Core.state()` — it is the only thing that writes the phase, the deck, the hand, Nerve, Breath, Block, Laughter and the clown. Everything else reads that state, listens on the Core bus, and as…
Director
The referee of GREASEPAINT. One of these sits on the scene's Director
anchor and owns Core.state() — it is the only thing that writes the phase,
the deck, the hand, Nerve, Breath, Block, Laughter and the clown. Everything
else reads that state, listens on the Core bus, and asks for changes through
Core.cmd.
It needs no sibling components. It reaches the ring through
entity.find(Core.ANCHORS.clown) and the figures through
game.clowns.Roster, both looked up when the act starts, so a night still
runs when either is still being built.
Public fields
autoStart: boolean— start a night the moment play begins instead of waiting on the title card. A testing convenience; false in the game.beatScale: number— multiplier on every staged pause the Director holds: the opening, the pause after a death, the breath between turns.1is the played tempo.0collapses them to a single frame, which is howselfplay.luauplays a whole night without waiting.
The night
title → howto? → intro → four acts of (battle → reward) → won
or lost, with pause reachable from battle, reward and the opening.
Acts I–III are the three clowns of Core.RUN.pool in a shuffled order; act
IV is Mr. Marrow. Nerve, the Laughter meter and the card library carry
across acts; Block, the piles and the turn count are rebuilt for each one.
A turn is: draw five, spend three Breath, End Turn, the clown acts on the intent it telegraphed. Ada's Block clears at the start of her next turn and a clown's clears the moment it acts again, so neither can be stacked across turns. Sawdust halves the clown's next blow and Mirror sends it back — both stay armed until a blow actually lands on them.
End Turn is taken once. From the press until the next hand is dealt the
Director is busy: a second endTurn, and any playCard, is refused, and the
beat that resolves the clown is the only thing that gives the turn back.
Every Grin still in the hand when the turn ends costs a Nerve — one
ada.hit { source = "grin" } carrying the total — before the clown acts.
When Laughter reaches 100 the tent laughs: one card in the library is
painted into a Grin, in the working piles as well so it is felt this act.
The first laugh of a night empties the meter; every laugh after leaves
RULES.LAUGHTER_CARRY standing. Each laugh also warms the house a step, to
RULES.HEAT_MAX, emitting tent.heat { level } on the rise and speaking
Core.BARKER.heat[level]; the house never cools before morning. At every
step, laughter the clowns raise is worth RULES.HEAT_LAUGHTER more —
laughter Ada takes back off the meter is worth what it says, because the heat
is the tent's advantage and not a tax on her answer to it.
Grins are unplayable and each one sitting in the hand raises Laughter every
turn. Match burns one out of the deck for good — and while the deck holds a
Grin and nothing that burns one, every reward offer includes a Match, because
a deck the player cannot clean is a dead end rather than a difficulty. A card
with exhaust set — Match, Curtsey — leaves the act when it is played
instead of going to the discard.
The interval
reward.offer carries next, the clown of the coming act, and mode:
"take"— three cards out ofRULES.REWARD_POOL, with the Match guarantee above. Picking adds one to the library."leave"— atRULES.LEAVE_ACT, three cards out of Ada's OWN library. Picking removes the first entry equal to it, Grins included.
pickReward { index = 0 } declines in either mode — TAKE NOTHING, KEEP THEM
ALL — and emits reward.picked { card = nil, mode }. Either way the next act
begins.
Commands it answers
startRun, howto, back, playCard {index?}, focusCard {index | delta},
endTurn, pickReward {index?}, pause, resume, restart,
quitToTitle.
Each is refused from a phase that does not expect it, so a stray command
cannot park a night: howto opens only from the title and the pause card,
pause only from an act, a reward or the opening, and no card is played
while the game is on a card of its own.
playCard and pickReward fall back to the focused index when given none,
so one control serves a click on a card, a tap on it, and a pad's south
button on the focused one. focusCard takes an absolute index from a
mouse or a touch and a relative delta from a stick or the arrow keys,
wrapping at both ends.
Events it emits
Every name in Core.EVENTS. phase.changed on each transition;
run.start / run.won / run.lost; act.start / act.won; turn.start /
turn.end; card.drawn / card.played / card.painted / card.burned;
clown.intent / clown.hit / clown.blocked / clown.guard /
clown.attack / clown.laugh / clown.die; ada.hit / ada.heal;
laughter.changed / tent.laughs / tent.heat; reward.offer /
reward.picked; focus.changed.
ada.hit carries source — "clown" for a blow, "grin" for the bite.
clown.laugh's delta is what the meter TOOK, which in a warm house is more
than the clown asked for.
A card is resolved in state the instant it is played and its events go out
immediately after — nothing here waits on an animation. voice.line is the
one event it shares: the sound slice speaks the line and emits it with the
clip's real duration, and if nothing has answered within a beat the Director
emits the line itself, so the words always reach the subtitle strip even
when a generation failed.
Lifecycle
awake()— subscribes to every command, tagged, so a hot reload replaces its handlers instead of doubling them.start()— starts a night ifautoStartis set.update(dt)— counts down the staged beats. They do not run while the game is on the pause card.onDestroy()— unsubscribes, drops pending beats, despawns the clown.
Usage
local Core = require("game.Core")
Core.cmd("startRun")
Core.on("act.start", function(p) print("act", p.act, p.clown) end)
selfplay.luau, beside this component, plays whole nights through those
commands alone and reports what fired. It carries two harnesses:
execute { file = ".../selfplay.luau" } -- three scripted nights
execute { file = ".../selfplay.luau", args = { mode = "greedy", nights = 12 } }
The scripted run is the regression: it walks the console cold, plays a night
to a win, one sat out to a loss and one played for laughs until a Grin burns,
checks that a restart leaves a clean night, and reports missing: [] when
every name in Core.EVENTS fired. The greedy run is the exchange rate's
measuring stick — a player who reads only the numbers, N nights over, with
the win count, the paints a night, the heat and the highest guard any clown
ever stood behind. It drops and replays a night the shared engine interfered
with rather than averaging it in. Both restore beatScale and leave the game
on the title card.
Scoped to this part · feeds back into the world's score.