SLOPSHOPPER

GAME MODE · Equip

The model as equipment: EQUIP 모델 <model> 장착 · 교체는 /model at the start, in place of the model notice, and again when the model changes

newspinnertoast
★ 2v0.2.0MITupdated 2026-10-06Reasonofmoon/bitgame-mods/plugins/game-equip
A shopper browsing a rack in a slop shop
README

GAME MODE · Equip

The model is the session's equipment. At the start, and whenever it changes (/model, a fallback), the transcript says which one is on:

● game-equip: EQUIP 모델 Sonnet 5.5 장착 · 교체는 /model
● game-equip: EQUIP 모델 Opus 5.5 장착 · Sonnet 5.5에서 교체

Where the engine shows a model notice under the logo, that notice reads the same.

Part of the GAME MODE pack. Requires Claude Code 2.1.287+; built and tested on 2.1.291.

Install

claude plugin marketplace add Reasonofmoon/bitgame-mods
claude plugin install game-equip@bitgame-mods

Settings

intensity (casual)off · casual · hardcore (a toast on each change)
palette (nes)nes · gameboy · amber · theme

Known limits

  • In a session with an API key the engine draws no model notice, so the equip line is a transcript row of its own.

한국어

세션을 시작할 때와 모델이 바뀔 때 EQUIP 모델 … 장착 · 교체는 /model 줄로 지금 모델을 알려줍니다.

Source 2 files
hooks/register.tsx 78 lines
1import type { Register } from 'claude-code'
2
3import { intensityOf, paletteOf } from './palette'
4
5// GAME MODE · EQUIP
6//
7// The model is the session's equipment. At the start, and whenever it changes (/model,
8// a fallback), the transcript says which one is on:
9//   EQUIP 모델 Sonnet 5.5 장착 · 교체는 /model
10//   EQUIP 모델 Opus 5.5 장착 · Sonnet 5.5에서 교체
11// Where the engine shows a model notice under the logo, that notice reads the same.
12
13const MODEL_NOTICE = /\b(?:opus|sonnet|haiku|fable|mythos|claude-[a-z0-9.-]+)\b|\bmodel\b/i
14
15export const register: Register = (on, options) => {
16  const intensity = intensityOf(options.intensity)
17  const pal = paletteOf(options.palette)
18  let current = ''
19
20  on('session.start', async ($, e, next) => {
21    const result = await next(e)
22    if (intensity === 'off') return result
23    current = await $.session.model().catch(() => '')
24    if (current !== '') $.ui.log(equipLine(current))
25    return result
26  })
27
28  // A change the session made between turns (/model, a fallback): told at the next turn.
29  on('turn.start', async ($, e, next) => {
30    if (intensity !== 'off') {
31      const now = await $.session.model().catch(() => '')
32      if (now !== '' && current !== '' && now !== current) {
33        $.ui.log(equipLine(now, current))
34        if (intensity === 'hardcore') $.ui.toast(`EQUIP ${nameOf(now)}`)
35      }
36      if (now !== '') current = now
37    }
38    return next(e)
39  }).catch(($, e, next) => next(e))
40
41  on('classic.PostModelSwitch', async ($, e, next) => {
42    if (intensity !== 'off' && e.to_model !== current) {
43      $.ui.log(equipLine(e.to_model, e.from_model))
44      if (intensity === 'hardcore') $.ui.toast(`EQUIP ${nameOf(e.to_model)}`)
45      current = e.to_model
46    }
47    return next(e)
48  }).catch(($, e, next) => next(e))
49
50  // The engine's own model notice, where it shows one.
51  on('ui.render', { component: 'InfoNotice' }, async ($, e, next) => {
52    const isModel = e.props.command === 'model' || e.props.command === '/model' || MODEL_NOTICE.test(e.props.text)
53    if (intensity === 'off' || !isModel || current === '') return next(e)
54    const { Box, Text } = $.ui.resolve(e)
55    return (
56      <Box flexDirection="row" gap={1}>
57        <Text color={pal.title} bold>
58          EQUIP
59        </Text>
60        <Text color={pal.text}>{`모델 ${nameOf(current)} 장착`}</Text>
61        <Text color={pal.dim}>· 교체는 /model</Text>
62      </Box>
63    )
64  })
65}
66
67/** `claude-sonnet-5-5` → `Sonnet 5.5`; a name already plain stays. */
68export function nameOf(model: string): string {
69  const m = /^claude-([a-z]+)-(\d+)(?:-(\d+))?(?:-\d{8})?(?:\[.*\])?$/i.exec(model.trim())
70  if (m === null) return model
71  const family = (m[1] ?? '').charAt(0).toUpperCase() + (m[1] ?? '').slice(1)
72  return `${family} ${m[2]}${m[3] !== undefined ? `.${m[3]}` : ''}`
73}
74
75export function equipLine(model: string, from?: string): string {
76  return from === undefined ? `EQUIP 모델 ${nameOf(model)} 장착 · 교체는 /model` : `EQUIP 모델 ${nameOf(model)} 장착 · ${nameOf(from)}에서 교체`
77}
78
hooks/palette.ts 124 lines
1// GAME MODE shared look (copied into each GAME MODE plugin: a hooks module
2// may only import files of its own plugin). Change every copy together;
3// scripts/check.sh fails when the copies differ.
4//
5// nes / gameboy / amber are raw colors made for dark terminals, taken from
6// the design boards. `theme` uses Claude Code's theme keys, so it follows the
7// person's theme: pick it on a light background. Its `win` is empty: windows
8// draw with no fill there.
9
10export type PaletteName = 'nes' | 'gameboy' | 'amber' | 'theme'
11
12export type Palette = {
13  /** Headings, gold, the level. */
14  title: string
15  ok: string
16  warn: string
17  bad: string
18  info: string
19  dim: string
20  text: string
21  /** A window's border. */
22  frame: string
23  /** A window's fill; '' for none. */
24  win: string
25  /** The bright inner edge of a window, and a highlighted badge's text. */
26  edge: string
27  /** A bar's empty cells. */
28  off: string
29}
30
31const PALETTES: Record<PaletteName, Palette> = {
32  nes: {
33    title: '#f8d830',
34    ok: '#58d854',
35    warn: '#ff8a6a',
36    bad: '#f83800',
37    info: '#3cbcfc',
38    dim: '#b8b8f0',
39    text: '#fcfcfc',
40    frame: '#5060d8',
41    win: '#1c2a8a',
42    edge: '#fcfcfc',
43    off: '#2c3a98',
44  },
45  gameboy: {
46    title: '#e0f8d0',
47    ok: '#9bbc0f',
48    warn: '#e0f8d0',
49    bad: '#f8f8a0',
50    info: '#c4e890',
51    dim: '#8bac0f',
52    text: '#e0f8d0',
53    frame: '#9bbc0f',
54    win: '#1e4a1e',
55    edge: '#e0f8d0',
56    off: '#306230',
57  },
58  amber: {
59    title: '#ffd060',
60    ok: '#ffb000',
61    warn: '#ff7040',
62    bad: '#ff5a2a',
63    info: '#ffe0a0',
64    dim: '#c89040',
65    text: '#ffd9a0',
66    frame: '#ffb000',
67    win: '#2a1700',
68    edge: '#ffe0a0',
69    off: '#4a2a00',
70  },
71  theme: {
72    title: 'claude',
73    ok: 'success',
74    warn: 'warning',
75    bad: 'error',
76    info: 'suggestion',
77    dim: 'subtle',
78    text: 'text',
79    frame: 'promptBorder',
80    win: '',
81    edge: 'text',
82    off: 'subtle',
83  },
84}
85
86export function paletteOf(name: unknown): Palette {
87  return name === 'gameboy' || name === 'amber' || name === 'theme' ? PALETTES[name] : PALETTES.nes
88}
89
90export type Intensity = 'off' | 'casual' | 'hardcore'
91
92export function intensityOf(value: unknown): Intensity {
93  return value === 'off' || value === 'hardcore' ? value : 'casual'
94}
95
96/** A bar of `cells` blocks, `percent` of them full. */
97export function bar(percent: number, cells: number): string {
98  const full = Math.max(0, Math.min(cells, Math.round((percent / 100) * cells)))
99  return '█'.repeat(full) + '░'.repeat(cells - full)
100}
101
102/** Box props for a window in this palette: a double border and, where the palette has one, a fill. */
103export function windowProps(pal: Palette): { borderStyle: 'double'; borderColor: string; backgroundColor?: string; paddingX: number } {
104  return pal.win === ''
105    ? { borderStyle: 'double', borderColor: pal.frame, paddingX: 1 }
106    : { borderStyle: 'double', borderColor: pal.frame, backgroundColor: pal.win, paddingX: 1 }
107}
108
109/** `m:ss`, or `h:mm:ss` past an hour. */
110export function clock(ms: number): string {
111  const s = Math.max(0, Math.round(ms / 1000))
112  const h = Math.floor(s / 3600)
113  const m = Math.floor((s % 3600) / 60)
114  const sec = s % 60
115  const two = (n: number) => String(n).padStart(2, '0')
116  return h > 0 ? `${h}:${two(m)}:${two(sec)}` : `${m}:${two(sec)}`
117}
118
119/** A cost in the chosen currency: `$1.23`, or `₩1,722` at `krwPerUsd`. */
120export function money(usd: number, currency: 'usd' | 'krw', krwPerUsd: number): string {
121  if (currency === 'krw') return '₩' + String(Math.round(usd * krwPerUsd)).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
122  return '$' + usd.toFixed(2)
123}
124