component
GameConsole
A games console (tower, slab, or cube) with up to four controllers laid out in front, to sit under a TV.
⌖ Explore the library in 3D → · View as Markdown
Add it
npx @runek/cli add gameconsole
Pulls @runek/core@^0.15.0.
Use it
import { GameConsole } from './runek/GameConsole'
<GameConsole position={[0, 0, 0]} seed={1} />
Or as a node in a world file (worlds as data):
{ "type": "GameConsole", "props": { "position": [0, 0, 0], "seed": 1 } }
Props
| Prop | Type | Default | Description |
|---|---|---|---|
style |
"tower" | "slab" | "cube" |
Seeded when unset. | |
controllers |
number |
Controllers laid out in front of the console, 0 to 4. Seeded (1 or 2) when unset. | |
color |
color |
Console and controllers. Seeded (black or white) when unset. | |
accent |
color |
Power light and trim. Seeded when unset. | |
on |
boolean |
true |
Powered on: the light glows. |
position |
[x, y, z] |
[0, 0, 0] |
Position [x, y, z] in units (1 unit = 1 m, Y-up). |
rotation |
[x, y, z] |
[0, 0, 0] |
Euler rotation [x, y, z] in radians. |
seed |
number |
1 |
Seed for the deterministic variation: same seed, same result. |
Registry manifest
registry manifest /r/components/gameconsole.json Self-contained JSON: inlined source plus resolved dependencies, exactly whatrunek add gameconsole fetches.
Browse the whole catalog in the gallery →.
Source (GameConsole.tsx)
import { pick, range, rng, sub, useWorld, type WorldComponentProps } from '@runek/core'
import { useMemo } from 'react'
/** Standing upright (`tower`), lying flat (`slab`), or a small `cube`. */
export type GameConsoleStyle = 'tower' | 'slab' | 'cube'
export interface GameConsoleProps extends WorldComponentProps {
/** Seeded when unset. */
style?: GameConsoleStyle
/** Controllers laid out in front of the console, 0 to 4. Seeded (1 or 2) when unset. */
controllers?: number
/** Console and controllers. Seeded (black or white) when unset. */
color?: string
/** Power light and trim. Seeded when unset. */
accent?: string
/** Powered on: the light glows. */
on?: boolean
}
const STYLES: GameConsoleStyle[] = ['tower', 'slab', 'cube']
const BODIES = ['#18191c', '#18191c', '#eceef0', '#2a2d33']
const LIGHTS = ['#38c6f4', '#7bd34f', '#e4405f', '#f0f0f0']
const BUTTONS = ['#7bd34f', '#e4405f', '#38c6f4', '#f0c419']
// Body size in meters, [width, height, depth].
const SIZES: Record<GameConsoleStyle, [number, number, number]> = {
tower: [0.1, 0.33, 0.26],
slab: [0.36, 0.07, 0.26],
cube: [0.17, 0.15, 0.17],
}
function Controller({ color, accent, u }: { color: string; accent: string; u: number }) {
return (
<group>
<mesh position={[0, 0.02 * u, 0]} castShadow>
<boxGeometry args={[0.1 * u, 0.025 * u, 0.055 * u]} />
<meshStandardMaterial color={color} roughness={0.5} />
</mesh>
{/* grips: two rounded handles swept back toward the player */}
{[-1, 1].map((s) => (
<mesh
key={s}
position={[s * 0.052 * u, 0.018 * u, 0.025 * u]}
rotation={[Math.PI / 2 - 0.3, 0, s * 0.35]}
castShadow
>
<capsuleGeometry args={[0.017 * u, 0.04 * u, 4, 10]} />
<meshStandardMaterial color={color} roughness={0.5} />
</mesh>
))}
{[-1, 1].map((s) => (
<mesh key={`stick${s}`} position={[s * 0.022 * u, 0.04 * u, 0.012 * u]}>
<cylinderGeometry args={[0.008 * u, 0.007 * u, 0.012 * u, 12]} />
<meshStandardMaterial color="#2a2a2e" roughness={0.8} />
</mesh>
))}
{BUTTONS.map((c, i) => {
const a = (i / 4) * Math.PI * 2
return (
<mesh
key={c}
position={[
0.042 * u + Math.cos(a) * 0.008 * u,
0.034 * u,
-0.008 * u + Math.sin(a) * 0.008 * u,
]}
>
<sphereGeometry args={[0.004 * u, 8, 6]} />
<meshStandardMaterial color={c} roughness={0.3} />
</mesh>
)
})}
<mesh position={[-0.042 * u, 0.034 * u, -0.008 * u]}>
<boxGeometry args={[0.018 * u, 0.004 * u, 0.006 * u]} />
<meshStandardMaterial color="#2a2a2e" />
</mesh>
<mesh position={[0, 0.0331 * u, -0.02 * u]}>
<boxGeometry args={[0.012 * u, 0.001 * u, 0.004 * u]} />
<meshBasicMaterial color={accent} toneMapped={false} />
</mesh>
</group>
)
}
/**
* A games console with its controllers, to sit on the media console under a `Tv`: an upright
* `tower`, a flat `slab`, or a small `cube`, with a power light and a vent, and up to four
* controllers laid out in front of it (local +Z). Its origin is the console's foot. Decorative:
* no collider.
*/
export function GameConsole({
position = [0, 0, 0],
rotation = [0, 0, 0],
seed = 1,
style,
controllers,
color,
accent,
on = true,
}: GameConsoleProps) {
const { unit } = useWorld()
const u = unit
const look = useMemo(() => {
const r = rng(sub(seed, 0))
const rolled = {
style: pick(r, STYLES),
count: r() < 0.5 ? 1 : 2,
body: pick(r, BODIES),
light: pick(r, LIGHTS),
}
const count = Math.max(0, Math.min(4, Math.round(controllers ?? rolled.count)))
const pads = Array.from({ length: count }, (_, i) => ({
x: (i - (count - 1) / 2) * 0.16 * u + range(r, -0.015, 0.015) * u,
z: (0.23 + range(r, -0.02, 0.03)) * u,
turn: range(r, -0.35, 0.35),
}))
return {
style: style ?? rolled.style,
body: color ?? rolled.body,
light: accent ?? rolled.light,
pads,
}
}, [seed, style, controllers, color, accent, u])
const [w, h, d] = SIZES[look.style].map((v) => v * u)
const trim = look.body === '#eceef0' ? '#18191c' : '#3a3d44'
return (
<group position={position} rotation={rotation}>
{look.style === 'tower' && (
<mesh position={[0, 0.006 * u, 0]} receiveShadow>
<boxGeometry args={[w * 1.6, 0.012 * u, d * 0.7]} />
<meshStandardMaterial color={trim} roughness={0.6} />
</mesh>
)}
<group position={[0, (look.style === 'tower' ? 0.012 * u : 0) + h / 2, 0]}>
<mesh castShadow receiveShadow>
<boxGeometry args={[w, h, d]} />
<meshStandardMaterial color={look.body} roughness={0.35} metalness={0.1} />
</mesh>
{/* a dark vent band across the front, and the power light */}
<mesh position={[0, look.style === 'slab' ? 0 : h * 0.3, d / 2 + 0.0005 * u]}>
<planeGeometry args={[w * 0.92, look.style === 'slab' ? h * 0.3 : h * 0.08]} />
<meshStandardMaterial color={trim} roughness={0.8} />
</mesh>
<mesh
position={[
look.style === 'slab' ? w * 0.4 : 0,
look.style === 'slab' ? 0 : -h * 0.35,
d / 2 + 0.001 * u,
]}
>
<circleGeometry args={[0.006 * u, 12]} />
{on ? (
<meshBasicMaterial color={look.light} toneMapped={false} />
) : (
<meshStandardMaterial color="#333" />
)}
</mesh>
</group>
{look.pads.map((p) => (
<group
key={`${p.x.toFixed(4)}:${p.z.toFixed(4)}`}
position={[p.x, 0, p.z]}
rotation={[0, p.turn, 0]}
>
<Controller color={look.body} accent={look.light} u={u} />
</group>
))}
</group>
)
}