medieval-kit/my-registry/models/coin-pouch/model.ts
CRT 50c7fc60b9 Translate the whole repository to English
The kit is going public, so everything a reader sees is now English:
27 model sources, the core vocabulary, meta.ts (titles, descriptions and
every slider label), the viewer UI, all script output, REFERENCE.md and
the READMEs.

Rules applied: only comments, doc comments and human-facing strings were
touched. Identifiers, numbers, logic, registry item ids, part names and
material slot names are unchanged — those are a published contract and
were already English.

Verified rather than assumed: for all 53 changed .ts files the code
skeleton (source with comments stripped and string contents replaced by a
placeholder) is byte-identical before and after. Full gate is green —
typecheck, registry conformance, ~510 model checks, GLB round trip and a
production build.

Also fixed here:
- index.html and viewer.html were missed by the translation pass because
  the file list only covered .ts/.md/.css. Both had lang="tr" and Turkish
  titles.
- The viewer formatted triangle counts with toLocaleString('tr-TR'), which
  prints 15.425 rather than 15,425.
- REFERENCE.md's model table header did not match what
  scripts/catalog-table.ts emits, so the next regeneration would have
  silently changed it. Aligned to the generator.
- Removed scripts/probe.ts and scripts/zdiag.ts; they were throwaway
  diagnostics that got committed by accident.
2026-08-21 14:35:13 +07:00

147 lines
5.9 KiB
TypeScript

/**
* @medieval-kit/coin-pouch
*
* A drawstring leather pouch with coins spilled beside it.
*
* On its own the pouch has a problem: a closed leather bag cannot be told apart
* from a stone at any distance. What shows what is inside are the SPILLED
* coins; without them the model is a "lump", not a "pouch". So the coins are
* not an optional garnish, they are how the model gets read at all.
*
* The coin layout is thought through too: money spilled on the ground does not
* form a neat circle, it scatters one way out of the pouch's mouth and part of
* it lands on top of itself. An evenly spaced ring always looked "placed".
*
* Period note: a silver penny is thin and SMALL — a little over a centimetre.
* The thick, golden, oversized coin is a fantasy image; here the proportion is
* kept close to the real one, and that makes the pouch's scale read right too.
*/
import type { BufferGeometry } from 'three'
import {
bandGeometry,
createKitModel,
createTinter,
jitter,
latheGeometry,
mergeColoured,
prismGeometry,
roughenGeometry,
type Level,
} from '../core/index.ts'
export interface CoinPouchConfig {
/** Height of the pouch (metres). */
readonly height: number
/** Widest radius of the pouch (metres). */
readonly radius: number
/** How full it is. 0.3 is half empty and slumped, 1 is stuffed. */
readonly fill: number
/** Number of coins spilled outside. */
readonly coins: number
/** Coin radius (metres). */
readonly coinRadius: number
readonly seed: number
}
export const coinPouchDefaults: CoinPouchConfig = {
height: 0.1,
radius: 0.042,
fill: 0.85,
coins: 9,
coinRadius: 0.011,
seed: 89,
}
export type CoinPouchParts = 'pouch' | 'cord' | 'coins'
export function createModel(overrides: Partial<CoinPouchConfig> = {}) {
return createKitModel<CoinPouchConfig, 'leather' | 'cloth' | 'brass', CoinPouchParts>({
id: 'coin-pouch',
defaults: coinPouchDefaults,
slots: ['leather', 'cloth', 'brass'],
build: ({ config, random }) => {
const tint = createTinter(random)
const fill = Math.max(0.15, Math.min(1, config.fill))
const floor = 0 // the pouch sits on the ground
const wide = config.radius * (0.68 + fill * 0.4)
const neckY = config.height * (0.62 + fill * 0.14)
// --- Pouch --------------------------------------------------------------
// A smaller sack, but a different profile: the pouch gathers ABOVE the
// cord and pulls upward, it does not flop outward the way the sack does.
// That gap is the gap between a pouch and a plain bag.
const profile: Level[] = [
{ y: floor, radius: wide * 0.62 },
{ y: config.height * 0.1, radius: wide * 0.94 },
{ y: config.height * 0.3 * fill + config.height * 0.12, radius: wide },
{ y: neckY - config.height * 0.14, radius: wide * 0.78 },
{ y: neckY, radius: config.radius * 0.3 },
{ y: neckY + config.height * 0.16, radius: config.radius * 0.26 },
{ y: neckY + config.height * 0.28, radius: config.radius * 0.34 },
]
const pouch = latheGeometry(profile, 9, [0, 0, 0], tint('leather', -0.05, 0.9), {
colourTop: tint('leather', 0.06, 0.9),
capTop: true,
})
roughenGeometry(pouch, config.radius * 0.055, { salt: 51, scaleY: 0.6 })
// --- Drawstring ------------------------------------------------------------
const cordPieces: BufferGeometry[] = [bandGeometry(
config.radius * 0.31, neckY, config.height * 0.05, config.radius * 0.055, 8,
tint('cloth', -0.18, 0.8), { inner: true },
)]
// The two hanging ends. Orient first, then translate.
for (const side of [-1, 1]) {
const tail = prismGeometry(
config.radius * 0.04, config.radius * 0.028, config.height * 0.3, 4,
[0, -config.height * 0.15, 0], tint('cloth', -0.12, 0.8),
)
tail.rotateZ(side * 0.7 + jitter(random, 0.15))
tail.rotateY(random() * Math.PI * 2)
tail.translate(side * config.radius * 0.28, neckY, 0)
cordPieces.push(tail)
}
// --- Spilled coins -----------------------------------------------------------
const count = Math.max(0, Math.round(config.coins))
const coinPieces: BufferGeometry[] = []
// Scatter DIRECTION: all one way, because spilled money flows to one side.
const spillAngle = random() * Math.PI * 2
for (let i = 0; i < count; i += 1) {
const t = (i + 0.6) / count
// Distance grows with a square root: piled at the pouch, thin further out.
const distance = wide * (1.15 + Math.sqrt(t) * 2.4)
const spread = jitter(random, 0.75) * (0.35 + t * 0.65)
const angle = spillAngle + spread
const thickness = config.coinRadius * (0.13 + random() * 0.06)
// The overlapping ones: every third coin lands on top of the previous.
const stack = i % 3 === 2 ? thickness * 1.6 : 0
const coin = prismGeometry(
config.coinRadius * (0.92 + random() * 0.16),
config.coinRadius * (0.9 + random() * 0.16),
thickness, 9, [0, 0, 0], tint('brass', jitter(random, 0.06), 0.5),
)
// Some do not land flat: the ones leaning on edge give the pile depth.
const tilt = random() < 0.22 ? 0.5 + random() * 0.7 : jitter(random, 0.12)
coin.rotateX(tilt)
coin.rotateY(random() * Math.PI * 2)
coin.translate(
Math.sin(angle) * distance,
floor + thickness / 2 + stack + Math.sin(tilt) * config.coinRadius * 0.5,
Math.cos(angle) * distance,
)
coinPieces.push(coin)
}
return {
pouch: { slot: 'leather' as const, geometry: mergeColoured([pouch]) },
cord: { slot: 'cloth' as const, geometry: mergeColoured(cordPieces) },
coins: coinPieces.length > 0
? { slot: 'brass' as const, geometry: mergeColoured(coinPieces) }
: undefined,
}
},
}, overrides)
}