Writing it well
The examples below come from one boss health bar.
1. A fact is a cell
Section titled “1. A fact is a cell”If you would ever read a value back off the screen, keep it in a cell and draw the prop from it:
export const bossHudPinned = sessionState(0);<sprite x={{ right: 8 }} y={5} w={16} h={16} sprite={Marks.PINNED} hide={pinned === 0} transparency={fade} />Decide from hp, not from bar.width.
2. Derive, don’t store
Section titled “2. Derive, don’t store”A value that follows from other values is arithmetic where it is used:
<rect x={1} y={1} w={(TRACK_W - 2) * hp / Math.max(1, max)} h={{ fill: 2 }} color={hp * 4 < max ? Colors.RED : 0x2fa84f} fill transparency={fade} />When the value comes from a function that reads many server values, follow
just its result with useDerived:
const threat = useDerived(() => threatOf(which));3. A handler calls a setter
Section titled “3. A handler calls a setter”A handler changes state, and the markup draws from it:
<layer w="fill" h="fill" rebuildOn={fight} onClick={() => { setPinned(1 - pinned); }}>onClick={() => { lock.hide = false; }} // undone when the region is rebuiltA component that was handed a ref, like a scrollbar given its pane, writes through it. See Refs.
4. Read where it branches
Section titled “4. Read where it branches”Read a value in the component that uses it, not in a parent that passes it down:
<Window title="Boss"> <layer x={10} y={42} w={{ fill: 20 }} h={{ fill: 52 }}> <BossHud /> </layer></Window>5. An animation is a tick over a cell
Section titled “5. An animation is a tick over a cell”One cell holds how far the animation has got, one interval advances it, and the render draws from it.
const [fade, setFade] = useState(0);const host = useRef();useInterval(() => { setFade(Math.min(255, fade + 16)); }, { everyMs: 20 }, host);A timer on a hidden host does not run, so the host’s hide is the idle
condition:
<layer ref={host} w={1} h={1} hide={hp > 0 || fade >= 255} />When the animation has to line up with something, store when it started and compute the frame from elapsed time, not from a count.
6. hide goes on what draws
Section titled “6. hide goes on what draws”Nothing under a hidden component updates, so hide the elements that draw and
leave a component that reads state or runs a timer shown. When something should
not exist at all, use && or return null rather than hide.
7. An entry point writes cells
Section titled “7. An entry point writes cells”A function the server calls takes values, writes the cells they change, and names no component.
export const bossHudPin = (pinned: Int): void => { const [, setPinned] = useState(bossHudPinned); setPinned(pinned);};8. Names say what a thing is
Section titled “8. Names say what a thing is”- Components are
PascalCaseand named for what they draw:HealthBar. - Cells and functions are
camelCase. A cell is named for the fact (bossHudPinned), not the handler that writes it (pinClicked). - Entry points are prefixed with the panel:
bossHudPin. - Helpers are named for their answer:
threatOf(which). - Constants are
UPPER_CASE.
A game id with no generated name is named after what the game calls it:
export const FEUD_KEBABMAN_POSTQUEST = 11876;A large panel splits by kind: <panel>.state.ts2 for the cells,
<panel>.component.ts2 for the components, <panel>.helpers.ts2 for the
entry points and arithmetic.