feat(blob): honour surface tags — oil luge, water rinse, drag
The controller now resolves the surface under the blob from the ground collider's userData.surface (the course already tags them) and reacts: - slip (oil/ice): steering-scrub + brake throttled ~0.1x → you luge across, and even a grippy green blob slides (Min friction combine beats grip); - drag (honey/mud/water): horizontal speed bleeds off (sticky/heavy feel); - rinse (water): a small periodic paint:request-cleanse — an underwater shortcut costs you your loadout (GDD §5.5). The surfaces.ts framework existed but nothing in the game consumed the behaviour tags; this wires it, per that file's own "the controller queries the surface" contract. Added a demo oil slick (applySurface 'oil' + Min combine) on the machine-pad centre lane. Browser-verified: on oil the blob slides ~2.7u vs 0.42u on normal ground; resting on the milk river rinsed red 56.7% → 35.7% in 2s. honey-climb and mud-antipaint left as marked ponytail TODOs. build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
856683832b
commit
333f11b9db
@ -13,6 +13,7 @@
|
||||
import * as THREE from 'three'
|
||||
import type { System, World } from '../contracts'
|
||||
import type { BlobHandle } from './createBlob'
|
||||
import { SURFACES, type SurfaceName } from '../machine/surfaces'
|
||||
|
||||
export interface BlobControllerOptions {
|
||||
/** Base top speed (m/s) before speedMul. */
|
||||
@ -85,6 +86,7 @@ export function createBlobController(
|
||||
let prevVy = 0
|
||||
let lastSize = -1
|
||||
let lastMassMul = -1
|
||||
let rinseAccum = 0 // water-rinse cadence (GDD §5.5)
|
||||
|
||||
const fwd = new THREE.Vector3()
|
||||
const right = new THREE.Vector3()
|
||||
@ -132,6 +134,19 @@ export function createBlobController(
|
||||
}
|
||||
grounded = nowGrounded
|
||||
|
||||
// ---- surface under the blob (GDD §5.5): oil/ice = slip, honey/mud/water
|
||||
// = drag, water = rinse. The course tags colliders `userData.surface`; the
|
||||
// grounded ray already excludes self, so its hit is the ground we resolve.
|
||||
// ponytail: honey `climb` and mud `antipaint` are not wired yet — they need
|
||||
// more than a drag knob (wall-climb traversal / buff suppression).
|
||||
let surf: (typeof SURFACES)[SurfaceName] | null = null
|
||||
if (hit) {
|
||||
const name = (hit.collider as unknown as { userData?: { surface?: string } })
|
||||
.userData?.surface as SurfaceName | undefined
|
||||
if (name && SURFACES[name]) surf = SURFACES[name]
|
||||
}
|
||||
const slip = !!surf && surf.tags.includes('slip')
|
||||
|
||||
// ---- camera-relative move basis (flatten camera dir onto ground) ----
|
||||
camera.getWorldDirection(fwd)
|
||||
fwd.y = 0
|
||||
@ -165,16 +180,19 @@ export function createBlobController(
|
||||
body.applyImpulse({ x: move.x * imp, y: 0, z: move.z * imp }, true)
|
||||
}
|
||||
if (grounded) {
|
||||
// scrub sideways drift so turns don't feel like ice (grip tightens it)
|
||||
// scrub sideways drift so turns don't feel like ice (grip tightens it).
|
||||
// On a slip surface the scrub is throttled right down: you luge, you
|
||||
// can't just carve — even a grippy green blob slides (oil beats grip).
|
||||
const along2 = v.x * move.x + v.z * move.z
|
||||
const perpX = v.x - along2 * move.x
|
||||
const perpZ = v.z - along2 * move.z
|
||||
const k = Math.min(1, (2 + m.grip * 10) * dt)
|
||||
const k = Math.min(1, (2 + m.grip * 10) * dt) * (slip ? 0.12 : 1)
|
||||
body.applyImpulse({ x: -perpX * mass * k, y: 0, z: -perpZ * mass * k }, true)
|
||||
}
|
||||
} else if (grounded) {
|
||||
// no input on ground: brake. grip = how hard we kill the slide.
|
||||
const k = Math.min(1, (6 + m.grip * 22) * dt)
|
||||
// no input on ground: brake. grip = how hard we kill the slide; a slip
|
||||
// surface all but removes the brake so you keep sliding.
|
||||
const k = Math.min(1, (6 + m.grip * 22) * dt) * (slip ? 0.08 : 1)
|
||||
body.applyImpulse({ x: -v.x * mass * k, y: 0, z: -v.z * mass * k }, true)
|
||||
}
|
||||
|
||||
@ -190,6 +208,29 @@ export function createBlobController(
|
||||
events.emit('blob:jumped', {})
|
||||
}
|
||||
|
||||
// ---- surface effects (only while grounded on a tagged surface) ----
|
||||
if (surf && grounded) {
|
||||
// drag: honey/mud/water bleed horizontal speed (sticky / heavy feel)
|
||||
if (surf.drag > 0) {
|
||||
const keep = Math.max(0, 1 - surf.drag)
|
||||
const lv = body.linvel()
|
||||
body.setLinvel({ x: lv.x * keep, y: lv.y, z: lv.z * keep }, true)
|
||||
}
|
||||
// rinse: water gradually washes paint off — an underwater shortcut costs
|
||||
// you your loadout (GDD §5.5). Small fraction on a fixed cadence.
|
||||
if (surf.tags.includes('rinse')) {
|
||||
rinseAccum += dt
|
||||
if (rinseAccum >= 0.4) {
|
||||
rinseAccum = 0
|
||||
events.emit('paint:request-cleanse', { fraction: 0.03 })
|
||||
}
|
||||
} else {
|
||||
rinseAccum = 0
|
||||
}
|
||||
} else {
|
||||
rinseAccum = 0
|
||||
}
|
||||
|
||||
prevVy = body.linvel().y
|
||||
},
|
||||
}
|
||||
|
||||
23
src/game.ts
23
src/game.ts
@ -11,7 +11,10 @@ import { createBlobController } from './blob/controller'
|
||||
import { createBlobFeel } from './blob/feel'
|
||||
import { createFollowCamera } from './blob/camera'
|
||||
import { PaintSkin, PaintCannon, BuffSystem, PaintHUD, installPaint } from './paint/index'
|
||||
import { createBucketDump, createBubbleArch, createConveyorBelt, createPaintMine, createColorGate } from './machine/index'
|
||||
import {
|
||||
createBucketDump, createBubbleArch, createConveyorBelt, createPaintMine, createColorGate,
|
||||
SURFACES, applySurface,
|
||||
} from './machine/index'
|
||||
import { installZones } from './course/zones'
|
||||
import { assets } from './assets/registry'
|
||||
import { hasOverrides } from './assets/idb'
|
||||
@ -93,6 +96,24 @@ export function installGame(world: World) {
|
||||
impulse: 7, direction: [0.8, 0.5, 0], size: [3, 3],
|
||||
})
|
||||
|
||||
// ---- oil slick: a slippery patch on the flat approach. applySurface gives
|
||||
// it oil friction with a Min combine rule, so it stays slick under ANY blob
|
||||
// (even a grippy green one), and the userData tag lets the controller throttle
|
||||
// steering + add drag → you luge across it. Demo of the surface framework. ----
|
||||
{
|
||||
const oilMat = new THREE.MeshStandardMaterial({
|
||||
color: SURFACES.oil.color, roughness: 0.12, metalness: 0.35,
|
||||
})
|
||||
const oil = new THREE.Mesh(new THREE.BoxGeometry(6, 0.08, 4), oilMat)
|
||||
oil.position.set(0, 0.28, -40) // machine-pad centre lane (top y0.24), clear of the fork exits + hazard mine
|
||||
oil.receiveShadow = true
|
||||
world.scene.add(oil)
|
||||
const oilCol = world.physics.createCollider(
|
||||
world.rapier.ColliderDesc.cuboid(3, 0.04, 2).setTranslation(0, 0.28, -40))
|
||||
applySurface(world.rapier, oilCol, 'oil')
|
||||
;(oilCol as unknown as { userData?: unknown }).userData = { surface: 'oil' }
|
||||
}
|
||||
|
||||
// ---- colour-keyed gate: a GREEN shortcut through the centre lane, just
|
||||
// before the finish. Roll over the green reward mine upstream and you arrive
|
||||
// green enough for the gate to sink into the floor — a straight blast to the
|
||||
|
||||
Loading…
Reference in New Issue
Block a user