Appearance
Lighting
clientsideMytharyn has a tile-based lighting system: a script darkens the world with an ambient light level, and light sources attached to NPCs brighten it again — torches, lamps, lighthouses, glowing pickups. Walls (blocking tiles) absorb light, so lights cast soft shadows.
Lighting is entirely clientside. Each client computes its own lightmap; nothing is sent over the network. Two pieces work together:
- setambient / resetambient — global functions, called from a clientside weapon, that set how dark the world is.
- drawaslight and the
light*properties — called from a clientside NPC script, they turn that NPC into a light source.
Ambient light
setambient(r, g, b) sets the ambient light level, 0-255 per channel. At the default 255,255,255 lighting is off — the lighting pass doesn't run at all, so levels without lighting look and cost exactly the same as before. Anything below full multiplies the world by that color and lets light sources show through. resetambient() restores full light.
ts
// example: weapons/lighttest.client.ts
export function onPlayerChats(who: ChatPlayer, chat: string): void {
if (who.id !== player.id)
return
if (chat === '/night')
setambient(25, 25, 55) // dark, slightly blue moonlight
else if (chat === '/day')
resetambient()
}The channels are independent, so ambient light also tints the world: a warm dusk (200,140,110), a cold cave (40,50,70). A day/night cycle is just setambient called periodically with an interpolated color:
ts
// weapons/daynight.client.ts — one full cycle every 10 minutes
const CYCLE = 600
const NIGHT = [30, 30, 60]
export function onCreated() {
this.t = 0
this.acc = 0
}
export function onUpdate(dt: number) {
this.t = (this.t + dt) % CYCLE
this.acc += dt
if (this.acc < 0.5) return // no need to update every frame
this.acc = 0
const day = (Math.cos(this.t / CYCLE * 2 * Math.PI) + 1) / 2 // 1 = noon, 0 = midnight
setambient(
NIGHT[0] + (255 - NIGHT[0]) * day,
NIGHT[1] + (255 - NIGHT[1]) * day,
NIGHT[2] + (255 - NIGHT[2]) * day)
}Both calls are batched like other client commands. The ambient level is session state, not level state:
- It persists across level changes (like setfocus), so set it once rather than on every level entry.
- It resets to full light when a weapon is removed from the player, so a weapon that's taken away can't leave the world stuck in darkness. Values outside 0-255 are clamped.
Per-area darkness
Because ambient light is global, "this dungeon is dark" is a script decision: have a weapon watch player.level (or an event such as a level change) and call setambient / resetambient accordingly.
Light sources
Only NPCs can be light sources. In a clientside NPC script (or a class the NPC joins), call drawaslight() and configure the light with properties on this:
ts
// clientside half of a torch NPC
export function onCreated() {
this.lightcolor = '255,170,80' // warm orange
this.lightradius = 6 // tiles
this.lightintensity = 1
this.drawaslight()
}
export function onUpdate() {
// flicker
this.lightintensity = 0.9 + Math.random() * 0.2
}| Property | Default | Meaning |
|---|---|---|
| lightcolor | '255,255,255' | Light color, 'r,g,b' 0-255. |
| lightintensity | 1 | Brightness at the source. Above 1 overdrives the core so full brightness reaches further out (max 10). 0 turns the light off. |
| lightradius | 8 | Distance in tiles at which the light fades out in open air (max 48). |
| lightshape | 'circle' | 'circle', 'diamond', 'square', 'cone' or 'ring'. |
| lightdir | 0 | Cone aim in degrees, clockwise: 0 = up, 90 = right. |
| lightarc | 90 | Cone width in degrees (the full wedge, max 360). |
| lightmode | 0 | 0 = visible only in darkness; 1 = also an additive glow, visible by day. |
Things to know:
drawaslight()is one-way, likeshowCharacter(). There's no "stop being a light" call — setlightintensity = 0to switch it off, and back up to switch it on again.- The light follows the NPC. It is centered on the NPC's shape (or its image) and moves with
this.x/this.y, so a moving NPC carries its light along. - Walls cast shadows. Blocking (type-22) tiles absorb light far faster than open air, for every shape. Areas outside the level count as solid, so light doesn't leak into the void.
- Overlapping lights don't over-brighten: where two lights overlap, the brighter one wins per channel rather than adding up.
- Light properties are per client. Each player's client runs the NPC's clientside script, so every player sees the same lights as long as the script sets them deterministically.
Shapes
'circle'— a round glow (the default).'diamond'— pointed on the axes.'square'— even brightness out to a square edge.'cone'— a directional wedge aimed withlightdir,lightarcdegrees wide. Animatelightdirfor a lighthouse beam or a flashlight.'ring'— a glowing band at about two-thirds of the radius with a dark center.
ts
// a rotating lighthouse beam
export function onCreated() {
this.lightshape = 'cone'
this.lightarc = 40
this.lightradius = 20
this.lightcolor = '255,250,200'
this.drawaslight()
}
export function onUpdate(dt: number) {
this.lightdir = (this.lightdir + dt * 45) % 360 // 45 degrees per second
}Light modes
By default (lightmode = 0) a light only shows where setambient has darkened the world — at full ambient it's invisible. That's what you want for torches and lamps that "turn on" at night.
With lightmode = 1 the light is also drawn as an additive glow on top of the scene, the same by day or night, so it stays visible at full ambient; at night it still lights the tiles under it like mode 0. Use it for magical glows, pickups and effects that should shine in daylight. Additive light saturates quickly — keep lightintensity low (about 0.3-0.6) for a subtle glow:
ts
// example: classes/flare.client.ts — an orange glow visible any time of day
export function onCreated() {
this.lightshape = 'circle'
this.lightmode = 1
this.lightintensity = 0.25
this.lightcolor = '255,100,0'
this.drawaslight()
}What gets lit
The lightmap is applied over the whole world pass: tiles, name tags, NPCs, players, projectiles, world-space script images and particles are all darkened by ambient light and brightened by light sources. Screen-space images (screen = true), chat bubbles and GUI controls draw in later passes and are never lit — which is what you want for HUDs.
Glowing effects at night
World-space images and particles are darkened like everything else. For a spark or spell effect that should glow in the dark, pair it with a lightmode = 1 NPC light, or use additive particles (particle.mode = 0) on top of a light source.
See the Day/night & torches tutorial for a complete setup.