@netforge/engine 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +147 -2
  5. package/dist/api/appearance.d.ts +37 -0
  6. package/dist/api/appearance.js +41 -0
  7. package/dist/api/cards.d.ts +105 -0
  8. package/dist/api/cards.js +133 -0
  9. package/dist/api/collector.d.ts +114 -0
  10. package/dist/api/collector.js +204 -0
  11. package/dist/api/dungeon.d.ts +58 -0
  12. package/dist/api/dungeon.js +216 -0
  13. package/dist/api/harness.d.ts +149 -0
  14. package/dist/api/harness.js +549 -0
  15. package/dist/api/idle.d.ts +71 -0
  16. package/dist/api/idle.js +104 -0
  17. package/dist/api/race-harness.d.ts +3 -0
  18. package/dist/api/race-harness.js +148 -0
  19. package/dist/api/registry.d.ts +241 -0
  20. package/dist/api/registry.js +394 -0
  21. package/dist/api/scheduler.d.ts +56 -0
  22. package/dist/api/scheduler.js +160 -0
  23. package/dist/api/schemas.d.ts +958 -0
  24. package/dist/api/schemas.js +859 -0
  25. package/dist/api/tower.d.ts +127 -0
  26. package/dist/api/tower.js +183 -0
  27. package/dist/game/adapter.d.ts +69 -0
  28. package/dist/game/adapter.js +341 -0
  29. package/dist/game/arena-bot.d.ts +66 -0
  30. package/dist/game/arena-bot.js +188 -0
  31. package/dist/game/bot.d.ts +95 -0
  32. package/dist/game/bot.js +285 -0
  33. package/dist/game/compile.d.ts +97 -0
  34. package/dist/game/compile.js +476 -0
  35. package/dist/game/director.d.ts +42 -0
  36. package/dist/game/director.js +179 -0
  37. package/dist/game/earn-source.d.ts +122 -0
  38. package/dist/game/earn-source.js +386 -0
  39. package/dist/game/earn.d.ts +49 -0
  40. package/dist/game/earn.js +55 -0
  41. package/dist/game/index.d.ts +37 -0
  42. package/dist/game/index.js +25 -0
  43. package/dist/game/platformer.d.ts +156 -0
  44. package/dist/game/platformer.js +619 -0
  45. package/dist/game/profile.d.ts +97 -0
  46. package/dist/game/profile.js +140 -0
  47. package/dist/game/race.d.ts +177 -0
  48. package/dist/game/race.js +386 -0
  49. package/dist/game/racer.d.ts +98 -0
  50. package/dist/game/racer.js +379 -0
  51. package/dist/game/runner.d.ts +286 -0
  52. package/dist/game/runner.js +824 -0
  53. package/dist/game/threats.d.ts +23 -0
  54. package/dist/game/threats.js +49 -0
  55. package/dist/game/view.d.ts +47 -0
  56. package/dist/game/view.js +123 -0
  57. package/dist/index.d.ts +33 -0
  58. package/dist/index.js +24 -0
  59. package/dist/net/bits.d.ts +33 -0
  60. package/dist/net/bits.js +135 -0
  61. package/dist/net/client.d.ts +75 -0
  62. package/dist/net/client.js +151 -0
  63. package/dist/net/index.d.ts +23 -0
  64. package/dist/net/index.js +23 -0
  65. package/dist/net/input.d.ts +49 -0
  66. package/dist/net/input.js +112 -0
  67. package/dist/net/interpolation.d.ts +37 -0
  68. package/dist/net/interpolation.js +97 -0
  69. package/dist/net/netsim.d.ts +22 -0
  70. package/dist/net/netsim.js +52 -0
  71. package/dist/net/prediction.d.ts +31 -0
  72. package/dist/net/prediction.js +101 -0
  73. package/dist/net/profile-client.d.ts +75 -0
  74. package/dist/net/profile-client.js +173 -0
  75. package/dist/net/protocol.d.ts +208 -0
  76. package/dist/net/protocol.js +8 -0
  77. package/dist/net/room-client.d.ts +57 -0
  78. package/dist/net/room-client.js +144 -0
  79. package/dist/net/schema.d.ts +78 -0
  80. package/dist/net/schema.js +121 -0
  81. package/dist/net/sim-adapter.d.ts +134 -0
  82. package/dist/net/sim-adapter.js +22 -0
  83. package/dist/net/snapshot.d.ts +61 -0
  84. package/dist/net/snapshot.js +301 -0
  85. package/dist/rules/adapter.d.ts +53 -0
  86. package/dist/rules/adapter.js +261 -0
  87. package/dist/rules/cards.d.ts +115 -0
  88. package/dist/rules/cards.js +547 -0
  89. package/dist/rules/collector.d.ts +161 -0
  90. package/dist/rules/collector.js +732 -0
  91. package/dist/rules/genres.d.ts +13 -0
  92. package/dist/rules/genres.js +38 -0
  93. package/dist/rules/harness.d.ts +16 -0
  94. package/dist/rules/harness.js +127 -0
  95. package/dist/rules/idle.d.ts +115 -0
  96. package/dist/rules/idle.js +370 -0
  97. package/dist/rules/index.d.ts +16 -0
  98. package/dist/rules/index.js +14 -0
  99. package/dist/rules/kit.d.ts +171 -0
  100. package/dist/rules/kit.js +88 -0
  101. package/dist/rules/tower.d.ts +214 -0
  102. package/dist/rules/tower.js +655 -0
  103. package/dist/runtime/arena.d.ts +86 -0
  104. package/dist/runtime/arena.js +269 -0
  105. package/dist/runtime/assets.d.ts +26 -0
  106. package/dist/runtime/assets.js +35 -0
  107. package/dist/runtime/camera.d.ts +113 -0
  108. package/dist/runtime/camera.js +247 -0
  109. package/dist/runtime/crowd.d.ts +18 -0
  110. package/dist/runtime/crowd.js +62 -0
  111. package/dist/runtime/feel/audio.d.ts +150 -0
  112. package/dist/runtime/feel/audio.js +327 -0
  113. package/dist/runtime/feel/clock.d.ts +52 -0
  114. package/dist/runtime/feel/clock.js +80 -0
  115. package/dist/runtime/feel/flash.d.ts +28 -0
  116. package/dist/runtime/feel/flash.js +60 -0
  117. package/dist/runtime/feel/index.d.ts +27 -0
  118. package/dist/runtime/feel/index.js +16 -0
  119. package/dist/runtime/feel/kit.d.ts +159 -0
  120. package/dist/runtime/feel/kit.js +226 -0
  121. package/dist/runtime/feel/knockback.d.ts +41 -0
  122. package/dist/runtime/feel/knockback.js +70 -0
  123. package/dist/runtime/feel/noise.d.ts +6 -0
  124. package/dist/runtime/feel/noise.js +30 -0
  125. package/dist/runtime/feel/numbers.d.ts +91 -0
  126. package/dist/runtime/feel/numbers.js +172 -0
  127. package/dist/runtime/feel/rumble.d.ts +40 -0
  128. package/dist/runtime/feel/rumble.js +58 -0
  129. package/dist/runtime/feel/sound.d.ts +96 -0
  130. package/dist/runtime/feel/sound.js +176 -0
  131. package/dist/runtime/feel/spring.d.ts +42 -0
  132. package/dist/runtime/feel/spring.js +95 -0
  133. package/dist/runtime/feel/trauma.d.ts +48 -0
  134. package/dist/runtime/feel/trauma.js +76 -0
  135. package/dist/runtime/feel/tween.d.ts +66 -0
  136. package/dist/runtime/feel/tween.js +180 -0
  137. package/dist/runtime/feel/types.d.ts +30 -0
  138. package/dist/runtime/feel/types.js +5 -0
  139. package/dist/runtime/index.d.ts +28 -0
  140. package/dist/runtime/index.js +26 -0
  141. package/dist/runtime/input.d.ts +38 -0
  142. package/dist/runtime/input.js +159 -0
  143. package/dist/runtime/interpolate.d.ts +6 -0
  144. package/dist/runtime/interpolate.js +13 -0
  145. package/dist/runtime/render/animation.d.ts +63 -0
  146. package/dist/runtime/render/animation.js +128 -0
  147. package/dist/runtime/render/atmosphere.d.ts +32 -0
  148. package/dist/runtime/render/atmosphere.js +59 -0
  149. package/dist/runtime/render/crowd-motion.d.ts +68 -0
  150. package/dist/runtime/render/crowd-motion.js +163 -0
  151. package/dist/runtime/render/decals.d.ts +64 -0
  152. package/dist/runtime/render/decals.js +222 -0
  153. package/dist/runtime/render/highlight.d.ts +29 -0
  154. package/dist/runtime/render/highlight.js +110 -0
  155. package/dist/runtime/render/index.d.ts +35 -0
  156. package/dist/runtime/render/index.js +22 -0
  157. package/dist/runtime/render/lighting.d.ts +50 -0
  158. package/dist/runtime/render/lighting.js +142 -0
  159. package/dist/runtime/render/particles.d.ts +117 -0
  160. package/dist/runtime/render/particles.js +289 -0
  161. package/dist/runtime/render/post.d.ts +92 -0
  162. package/dist/runtime/render/post.js +191 -0
  163. package/dist/runtime/render/presets.d.ts +57 -0
  164. package/dist/runtime/render/presets.js +116 -0
  165. package/dist/runtime/render/quality.d.ts +113 -0
  166. package/dist/runtime/render/quality.js +180 -0
  167. package/dist/runtime/render/rig.d.ts +29 -0
  168. package/dist/runtime/render/rig.js +107 -0
  169. package/dist/runtime/render/sprite.d.ts +34 -0
  170. package/dist/runtime/render/sprite.js +58 -0
  171. package/dist/runtime/render/stack.d.ts +74 -0
  172. package/dist/runtime/render/stack.js +132 -0
  173. package/dist/runtime/render/vat.d.ts +78 -0
  174. package/dist/runtime/render/vat.js +219 -0
  175. package/dist/runtime/renderer.d.ts +19 -0
  176. package/dist/runtime/renderer.js +18 -0
  177. package/dist/runtime/stats.d.ts +11 -0
  178. package/dist/runtime/stats.js +29 -0
  179. package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
  180. package/dist/runtime/styles/handdrawn/ground.js +282 -0
  181. package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
  182. package/dist/runtime/styles/handdrawn/index.js +94 -0
  183. package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
  184. package/dist/runtime/styles/handdrawn/ink.js +68 -0
  185. package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
  186. package/dist/runtime/styles/handdrawn/looks.js +42 -0
  187. package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
  188. package/dist/runtime/styles/handdrawn/paint.js +129 -0
  189. package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
  190. package/dist/runtime/styles/handdrawn/palette.js +70 -0
  191. package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
  192. package/dist/runtime/styles/handdrawn/textures.js +150 -0
  193. package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
  194. package/dist/runtime/styles/handdrawn/vfx.js +73 -0
  195. package/dist/runtime/styles/index.d.ts +9 -0
  196. package/dist/runtime/styles/index.js +32 -0
  197. package/dist/runtime/styles/kit.d.ts +193 -0
  198. package/dist/runtime/styles/kit.js +1 -0
  199. package/dist/runtime/styles/pixel/index.d.ts +46 -0
  200. package/dist/runtime/styles/pixel/index.js +167 -0
  201. package/dist/runtime/styles/ps1/index.d.ts +29 -0
  202. package/dist/runtime/styles/ps1/index.js +41 -0
  203. package/dist/runtime/styles/ps1/lights.d.ts +47 -0
  204. package/dist/runtime/styles/ps1/lights.js +146 -0
  205. package/dist/runtime/styles/ps1/materials.d.ts +62 -0
  206. package/dist/runtime/styles/ps1/materials.js +285 -0
  207. package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
  208. package/dist/runtime/styles/ps1/pipeline.js +111 -0
  209. package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
  210. package/dist/runtime/styles/ps1/surfaces.js +228 -0
  211. package/dist/runtime/styles/toon/index.d.ts +5 -0
  212. package/dist/runtime/styles/toon/index.js +178 -0
  213. package/dist/runtime/styles/toon/ink.d.ts +39 -0
  214. package/dist/runtime/styles/toon/ink.js +73 -0
  215. package/dist/runtime/styles/toon/look.d.ts +10 -0
  216. package/dist/runtime/styles/toon/look.js +112 -0
  217. package/dist/runtime/styles/toon/material.d.ts +74 -0
  218. package/dist/runtime/styles/toon/material.js +208 -0
  219. package/dist/runtime/styles/toon/normals.d.ts +26 -0
  220. package/dist/runtime/styles/toon/normals.js +127 -0
  221. package/dist/runtime/styles/toon/shadows.d.ts +24 -0
  222. package/dist/runtime/styles/toon/shadows.js +42 -0
  223. package/dist/runtime/styles/toon/vfx.d.ts +15 -0
  224. package/dist/runtime/styles/toon/vfx.js +47 -0
  225. package/dist/runtime/styles/view.d.ts +35 -0
  226. package/dist/runtime/styles/view.js +34 -0
  227. package/dist/runtime/styles/voxel/core.d.ts +114 -0
  228. package/dist/runtime/styles/voxel/core.js +577 -0
  229. package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
  230. package/dist/runtime/styles/voxel/geometry.js +40 -0
  231. package/dist/runtime/styles/voxel/index.d.ts +21 -0
  232. package/dist/runtime/styles/voxel/index.js +23 -0
  233. package/dist/runtime/styles/voxel/kit.d.ts +6 -0
  234. package/dist/runtime/styles/voxel/kit.js +11 -0
  235. package/dist/runtime/styles/voxel/look.d.ts +10 -0
  236. package/dist/runtime/styles/voxel/look.js +16 -0
  237. package/dist/runtime/styles/voxel/props.d.ts +9 -0
  238. package/dist/runtime/styles/voxel/props.js +52 -0
  239. package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
  240. package/dist/runtime/styles/voxel/shapes.js +83 -0
  241. package/dist/runtime/styles/voxel/world.d.ts +49 -0
  242. package/dist/runtime/styles/voxel/world.js +422 -0
  243. package/llms.txt +183 -0
  244. package/package.json +82 -3
@@ -0,0 +1,191 @@
1
+ import { ACESFilmicToneMapping, AgXToneMapping, Color, NeutralToneMapping, RenderPipeline, Vector2, Vector3, } from "three/webgpu";
2
+ import { abs, dot, float, length, max, mix, perspectiveDepthToViewZ, pass, pow, renderOutput, screenUV, smoothstep, uniform, vec2, vec3, vec4, } from "three/tsl";
3
+ import { bloom } from "three/addons/tsl/display/BloomNode.js";
4
+ import { fxaa } from "three/addons/tsl/display/FXAANode.js";
5
+ import { resolveLook } from "./presets.js";
6
+ function toneMappingOf(name) {
7
+ return name === "agx" ? AgXToneMapping : name === "neutral" ? NeutralToneMapping : ACESFilmicToneMapping;
8
+ }
9
+ /**
10
+ * The post stack: scene pass (HDR) -> bloom -> optional depth outline ->
11
+ * color grade (exposure, contrast, saturation, split tone, vignette, hurt
12
+ * pulse) -> tone mapping and sRGB (once, via renderOutput) -> FXAA.
13
+ *
14
+ * Tone mapping is the renderer's (`renderer.toneMapping`), so the same
15
+ * curve applies whether post is on (here, in renderOutput) or off
16
+ * (renderer.render to the canvas). Every knob is a uniform.
17
+ */
18
+ export class PostFX {
19
+ renderer;
20
+ scene;
21
+ camera;
22
+ pipeline;
23
+ /** Extra exposure multiplier inside the grade (the preset's exposure is renderer.toneMappingExposure). */
24
+ exposure = uniform(1);
25
+ contrast = uniform(1);
26
+ saturation = uniform(1);
27
+ shadowTint = uniform(new Vector3(1, 1, 1));
28
+ highlightTint = uniform(new Vector3(1, 1, 1));
29
+ vignette = uniform(0.3);
30
+ /** 0..1: the game drives this when the player is hurt (red edge pulse). */
31
+ hurt = uniform(0);
32
+ outlineColor = uniform(new Color(0x06080c));
33
+ /** Outline thickness in pixels. */
34
+ outlineThickness = uniform(1.25);
35
+ /** Relative depth jump that counts as an edge. */
36
+ outlineThreshold = uniform(0.012);
37
+ outlineStrength = uniform(0.85);
38
+ texel = uniform(new Vector2(1 / 1280, 1 / 720));
39
+ near = uniform(0.1);
40
+ far = uniform(500);
41
+ bloomNode = null;
42
+ bloomOn;
43
+ outlineOn;
44
+ fxaaOn;
45
+ bloomScale;
46
+ grade;
47
+ style;
48
+ enabled;
49
+ constructor(renderer, scene, camera, opts = {}) {
50
+ this.renderer = renderer;
51
+ this.scene = scene;
52
+ this.camera = camera;
53
+ this.pipeline = new RenderPipeline(renderer);
54
+ this.pipeline.outputColorTransform = false;
55
+ this.bloomOn = opts.bloom ?? true;
56
+ this.outlineOn = opts.outline ?? false;
57
+ this.fxaaOn = opts.fxaa ?? true;
58
+ this.enabled = opts.enabled ?? true;
59
+ this.bloomScale = opts.bloomScale ?? 0.5;
60
+ this.style = opts.style ?? null;
61
+ this.grade = { ...resolveLook(opts.preset).grade, ...opts.overrides };
62
+ this.applyGrade();
63
+ this.build();
64
+ }
65
+ /** Bloom strength, radius and threshold uniforms (null while bloom is off). */
66
+ get bloom() {
67
+ return this.bloomNode;
68
+ }
69
+ setPreset(preset, overrides = {}) {
70
+ this.grade = { ...resolveLook(preset).grade, ...overrides };
71
+ this.applyGrade();
72
+ }
73
+ applyGrade() {
74
+ const g = this.grade;
75
+ this.renderer.toneMapping = toneMappingOf(g.toneMapping);
76
+ // Exposure lives on the renderer so it applies with post on or off.
77
+ this.renderer.toneMappingExposure = g.exposure;
78
+ this.exposure.value = 1;
79
+ this.contrast.value = g.contrast;
80
+ this.saturation.value = g.saturation;
81
+ this.shadowTint.value.set(...g.shadowTint);
82
+ this.highlightTint.value.set(...g.highlightTint);
83
+ this.vignette.value = g.vignette;
84
+ if (this.bloomNode) {
85
+ this.bloomNode.strength.value = g.bloomStrength;
86
+ this.bloomNode.radius.value = g.bloomRadius;
87
+ this.bloomNode.threshold.value = g.bloomThreshold;
88
+ }
89
+ }
90
+ /** Toggle stages; rebuilds the node graph only when something changed. */
91
+ configure(o) {
92
+ const next = {
93
+ bloom: o.bloom ?? this.bloomOn,
94
+ outline: o.outline ?? this.outlineOn,
95
+ fxaa: o.fxaa ?? this.fxaaOn,
96
+ bloomScale: o.bloomScale ?? this.bloomScale,
97
+ };
98
+ if (o.enabled !== undefined)
99
+ this.enabled = o.enabled;
100
+ const changed = next.bloom !== this.bloomOn || next.outline !== this.outlineOn || next.fxaa !== this.fxaaOn;
101
+ this.bloomOn = next.bloom;
102
+ this.outlineOn = next.outline;
103
+ this.fxaaOn = next.fxaa;
104
+ if (next.bloomScale !== this.bloomScale) {
105
+ this.bloomScale = next.bloomScale;
106
+ this.bloomNode?.setResolutionScale(this.bloomScale);
107
+ }
108
+ if (changed)
109
+ this.build();
110
+ }
111
+ /** Apply a quality tier: low turns post off; medium drops bloom resolution; outline needs medium+. */
112
+ setQuality(tier, outlineWanted = this.outlineOn) {
113
+ if (tier === "low")
114
+ this.configure({ enabled: false });
115
+ else
116
+ this.configure({ enabled: true, bloomScale: tier === "high" ? 0.5 : 0.33, outline: outlineWanted });
117
+ }
118
+ get stages() {
119
+ return { enabled: this.enabled, bloom: this.bloomOn, outline: this.outlineOn, fxaa: this.fxaaOn };
120
+ }
121
+ /** Swap the art style's post stages (runtime/styles) and rebuild the graph. */
122
+ setStyle(style) {
123
+ this.style = style;
124
+ this.build();
125
+ }
126
+ build() {
127
+ const scenePass = pass(this.scene, this.camera);
128
+ this.style?.scenePass?.(scenePass);
129
+ const sceneColor = scenePass.getTextureNode("output");
130
+ const styleInput = { color: sceneColor, depth: scenePass.getTextureNode("depth"), texel: this.texel, near: this.near, far: this.far };
131
+ const color = this.style?.scene ? this.style.scene(styleInput) : sceneColor;
132
+ let hdr = color;
133
+ this.bloomNode = null;
134
+ if (this.bloomOn) {
135
+ const g = this.grade;
136
+ const b = bloom(color, g.bloomStrength, g.bloomRadius, g.bloomThreshold);
137
+ b.setResolutionScale(this.bloomScale);
138
+ this.bloomNode = b;
139
+ hdr = color.add(b);
140
+ }
141
+ let rgb = vec4(hdr).rgb;
142
+ if (this.outlineOn) {
143
+ const depth = scenePass.getTextureNode("depth");
144
+ const viewZ = (dx, dy) => perspectiveDepthToViewZ(depth.sample(screenUV.add(this.texel.mul(vec2(dx, dy)).mul(this.outlineThickness))).x, this.near, this.far);
145
+ const c = viewZ(0, 0);
146
+ const diff = abs(viewZ(1, 0).sub(c)).add(abs(viewZ(-1, 0).sub(c))).add(abs(viewZ(0, 1).sub(c))).add(abs(viewZ(0, -1).sub(c)));
147
+ const rel = diff.div(max(abs(c), float(0.001)));
148
+ const edge = smoothstep(this.outlineThreshold, this.outlineThreshold.mul(2.5), rel).mul(this.outlineStrength);
149
+ rgb = mix(rgb, vec3(this.outlineColor), edge);
150
+ }
151
+ if (this.style?.color)
152
+ rgb = this.style.color(rgb, scenePass);
153
+ // Grade in linear HDR, before tone mapping.
154
+ let c = rgb.mul(this.exposure);
155
+ const luma = dot(c, vec3(0.2126, 0.7152, 0.0722));
156
+ c = mix(vec3(luma), c, this.saturation);
157
+ c = pow(max(c, vec3(1e-5)).div(0.18), vec3(this.contrast)).mul(0.18);
158
+ const tone = smoothstep(0.0, 1.0, luma);
159
+ c = c.mul(mix(this.shadowTint, this.highlightTint, tone));
160
+ const d = length(screenUV.sub(0.5)).mul(1.414);
161
+ const v = smoothstep(0.35, 1.05, d);
162
+ c = c.mul(float(1).sub(v.mul(this.vignette)));
163
+ const hurtEdge = smoothstep(0.45, 1.15, d).mul(this.hurt).mul(0.85);
164
+ c = mix(c, c.mul(vec3(1.25, 0.35, 0.3)).add(vec3(0.22, 0.0, 0.0)), hurtEdge);
165
+ let out = renderOutput(vec4(c, 1));
166
+ if (this.style?.display)
167
+ out = this.style.display(out, styleInput);
168
+ this.pipeline.outputNode = this.fxaaOn ? fxaa(out) : out;
169
+ this.pipeline.needsUpdate = true;
170
+ }
171
+ /** Call when the canvas size changes (CSS pixels times pixel ratio handled inside). */
172
+ setSize(width, height) {
173
+ const pr = this.renderer.getPixelRatio();
174
+ this.texel.value.set(1 / Math.max(1, width * pr), 1 / Math.max(1, height * pr));
175
+ }
176
+ /** Draw the frame: through the stack, or straight to the canvas when disabled. */
177
+ render() {
178
+ const cam = this.camera;
179
+ if (cam.near !== undefined)
180
+ this.near.value = cam.near;
181
+ if (cam.far !== undefined)
182
+ this.far.value = cam.far;
183
+ if (this.enabled)
184
+ this.pipeline.render();
185
+ else
186
+ this.renderer.render(this.scene, this.camera);
187
+ }
188
+ dispose() {
189
+ this.pipeline.dispose();
190
+ }
191
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Look presets: one name picks the lighting, the atmosphere and the color
3
+ * grade together, so a template (or a generated game) says `preset: "dusk"`
4
+ * and gets a coherent picture. Plain data, node-testable.
5
+ */
6
+ export type LookPreset = "night" | "dusk" | "day" | "studio";
7
+ export declare const LOOK_PRESETS: readonly LookPreset[];
8
+ export interface LightingPreset {
9
+ /** Hemisphere sky and ground colors, intensity. */
10
+ readonly sky: number;
11
+ readonly ground: number;
12
+ readonly hemi: number;
13
+ /** Directional sun (or moon) color and intensity. */
14
+ readonly sun: number;
15
+ readonly sunIntensity: number;
16
+ /** Direction the light travels FROM, normalized at use (x, y, z). */
17
+ readonly sunDir: readonly [number, number, number];
18
+ /** Flat ambient fill (0 = none). */
19
+ readonly ambient: number;
20
+ readonly ambientColor: number;
21
+ /** Shadow darkness 0..1 (1 = full black shadows). */
22
+ readonly shadowStrength: number;
23
+ }
24
+ export interface AtmospherePreset {
25
+ /** Fog and horizon color. */
26
+ readonly fog: number;
27
+ /** Sky gradient: color at the top of the screen. */
28
+ readonly skyTop: number;
29
+ readonly mode: "linear" | "exp2";
30
+ /** Linear fog near and far (m), or exp2 density. */
31
+ readonly near: number;
32
+ readonly far: number;
33
+ readonly density: number;
34
+ /** Height fog density (0 = off) below `heightFogTop` m. */
35
+ readonly heightFog: number;
36
+ readonly heightFogTop: number;
37
+ }
38
+ export interface GradePreset {
39
+ readonly exposure: number;
40
+ readonly contrast: number;
41
+ readonly saturation: number;
42
+ /** Multiplier tints for shadows and highlights (split toning). */
43
+ readonly shadowTint: readonly [number, number, number];
44
+ readonly highlightTint: readonly [number, number, number];
45
+ readonly vignette: number;
46
+ readonly bloomStrength: number;
47
+ readonly bloomRadius: number;
48
+ readonly bloomThreshold: number;
49
+ readonly toneMapping: "aces" | "agx" | "neutral";
50
+ }
51
+ export interface Look {
52
+ readonly lighting: LightingPreset;
53
+ readonly atmosphere: AtmospherePreset;
54
+ readonly grade: GradePreset;
55
+ }
56
+ /** A preset by name; unknown names fall back to "studio" (never throws at runtime). */
57
+ export declare function resolveLook(name: string | undefined): Look;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Look presets: one name picks the lighting, the atmosphere and the color
3
+ * grade together, so a template (or a generated game) says `preset: "dusk"`
4
+ * and gets a coherent picture. Plain data, node-testable.
5
+ */
6
+ export const LOOK_PRESETS = ["night", "dusk", "day", "studio"];
7
+ const LOOKS = {
8
+ night: {
9
+ lighting: {
10
+ sky: 0x9fb0ff,
11
+ ground: 0x3a2c18,
12
+ hemi: 2.6,
13
+ sun: 0xc8d6ff,
14
+ sunIntensity: 3.4,
15
+ sunDir: [-0.45, 0.85, 0.3],
16
+ ambient: 0.35,
17
+ ambientColor: 0x8090c0,
18
+ shadowStrength: 0.75,
19
+ },
20
+ atmosphere: { fog: 0x121a2a, skyTop: 0x060912, mode: "linear", near: 32, far: 85, density: 0.018, heightFog: 0.06, heightFogTop: 1.2 },
21
+ grade: {
22
+ exposure: 1.5,
23
+ contrast: 1.08,
24
+ saturation: 1.12,
25
+ shadowTint: [0.86, 0.94, 1.15],
26
+ highlightTint: [1.08, 1.0, 0.9],
27
+ vignette: 0.42,
28
+ bloomStrength: 0.8,
29
+ bloomRadius: 0.45,
30
+ bloomThreshold: 0.82,
31
+ toneMapping: "aces",
32
+ },
33
+ },
34
+ dusk: {
35
+ lighting: {
36
+ sky: 0xffb38a,
37
+ ground: 0x3a2a3a,
38
+ hemi: 1.2,
39
+ sun: 0xffa060,
40
+ sunIntensity: 2.6,
41
+ sunDir: [0.7, 0.45, 0.35],
42
+ ambient: 0.1,
43
+ ambientColor: 0xffd0b0,
44
+ shadowStrength: 0.7,
45
+ },
46
+ atmosphere: { fog: 0x6a4a5c, skyTop: 0x2a2140, mode: "linear", near: 30, far: 110, density: 0.012, heightFog: 0.04, heightFogTop: 1.5 },
47
+ grade: {
48
+ exposure: 1.0,
49
+ contrast: 1.08,
50
+ saturation: 1.15,
51
+ shadowTint: [0.9, 0.88, 1.12],
52
+ highlightTint: [1.1, 1.0, 0.86],
53
+ vignette: 0.35,
54
+ bloomStrength: 0.65,
55
+ bloomRadius: 0.5,
56
+ bloomThreshold: 0.85,
57
+ toneMapping: "agx",
58
+ },
59
+ },
60
+ day: {
61
+ lighting: {
62
+ sky: 0xcfe6ff,
63
+ ground: 0x6b5a40,
64
+ hemi: 1.3,
65
+ sun: 0xfff2dc,
66
+ sunIntensity: 3.2,
67
+ sunDir: [0.4, 0.9, 0.25],
68
+ ambient: 0.15,
69
+ ambientColor: 0xffffff,
70
+ shadowStrength: 0.6,
71
+ },
72
+ atmosphere: { fog: 0xb8d4ee, skyTop: 0x6aa6e8, mode: "linear", near: 45, far: 160, density: 0.008, heightFog: 0, heightFogTop: 0 },
73
+ grade: {
74
+ exposure: 1.0,
75
+ contrast: 1.04,
76
+ saturation: 1.08,
77
+ shadowTint: [0.95, 0.98, 1.06],
78
+ highlightTint: [1.04, 1.01, 0.96],
79
+ vignette: 0.22,
80
+ bloomStrength: 0.35,
81
+ bloomRadius: 0.4,
82
+ bloomThreshold: 0.95,
83
+ toneMapping: "agx",
84
+ },
85
+ },
86
+ studio: {
87
+ lighting: {
88
+ sky: 0xffffff,
89
+ ground: 0x404040,
90
+ hemi: 1.4,
91
+ sun: 0xffffff,
92
+ sunIntensity: 2.4,
93
+ sunDir: [0.5, 1, 0.6],
94
+ ambient: 0.3,
95
+ ambientColor: 0xffffff,
96
+ shadowStrength: 0.5,
97
+ },
98
+ atmosphere: { fog: 0x22252c, skyTop: 0x101216, mode: "linear", near: 60, far: 200, density: 0.005, heightFog: 0, heightFogTop: 0 },
99
+ grade: {
100
+ exposure: 1.0,
101
+ contrast: 1.0,
102
+ saturation: 1.0,
103
+ shadowTint: [1, 1, 1],
104
+ highlightTint: [1, 1, 1],
105
+ vignette: 0.15,
106
+ bloomStrength: 0.3,
107
+ bloomRadius: 0.35,
108
+ bloomThreshold: 0.95,
109
+ toneMapping: "neutral",
110
+ },
111
+ },
112
+ };
113
+ /** A preset by name; unknown names fall back to "studio" (never throws at runtime). */
114
+ export function resolveLook(name) {
115
+ return (name !== undefined && LOOKS[name]) || LOOKS.studio;
116
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Quality tiers and dynamic resolution ("auto-detected
3
+ * low, medium or high (shadows, post-processing, resolution scale) with
4
+ * dynamic resolution to hold the frame rate"). The decision logic is pure
5
+ * so it is tested in node; `QualityManager` applies it to a renderer.
6
+ */
7
+ export type QualityTier = "low" | "medium" | "high";
8
+ export declare const QUALITY_TIERS: readonly QualityTier[];
9
+ /** What a tier turns on. */
10
+ export interface QualitySettings {
11
+ readonly tier: QualityTier;
12
+ /** Shadow map size in texels; 0 turns shadows off. */
13
+ readonly shadowMapSize: number;
14
+ /** Post-processing (bloom, grade, FXAA) on at all. */
15
+ readonly post: boolean;
16
+ /** Bloom render-target scale (fraction of the canvas). */
17
+ readonly bloomScale: number;
18
+ /** The toon outline pass may run (still opt-in per game). */
19
+ readonly outline: boolean;
20
+ /** Most device pixels per CSS pixel the tier renders. */
21
+ readonly maxPixelRatio: number;
22
+ /** Lowest pixel ratio dynamic resolution may drop to. */
23
+ readonly minPixelRatio: number;
24
+ /** GPU particle pool size. */
25
+ readonly particles: number;
26
+ }
27
+ export declare function qualitySettings(tier: QualityTier): QualitySettings;
28
+ /** What the browser tells us about the device. Every field optional: missing reads as "unknown". */
29
+ export interface DeviceHints {
30
+ readonly userAgent?: string;
31
+ /** `matchMedia("(pointer: coarse)").matches`. */
32
+ readonly coarsePointer?: boolean;
33
+ readonly hardwareConcurrency?: number;
34
+ /** `navigator.deviceMemory` (GB), Chrome only. */
35
+ readonly deviceMemory?: number;
36
+ readonly devicePixelRatio?: number;
37
+ readonly backend?: "webgpu" | "webgl2";
38
+ /** Screen width in CSS pixels. */
39
+ readonly screenWidth?: number;
40
+ }
41
+ /**
42
+ * Pick a tier from device hints. Phones and tablets start at medium (low on
43
+ * weak or WebGL-only ones); desktops start at high unless the CPU is small.
44
+ * Dynamic resolution then holds the frame rate within the tier.
45
+ */
46
+ export declare function detectTier(h: DeviceHints): QualityTier;
47
+ /** Read the hints from the running browser (safe in node: returns {}). */
48
+ export declare function browserHints(backend?: "webgpu" | "webgl2"): DeviceHints;
49
+ export interface DynamicResolutionOptions {
50
+ /** Frame budget, ms. Default 1000 / 60. */
51
+ readonly budgetMs?: number;
52
+ /** Step down when p95 exceeds budget x this. Default 1.26 (21 ms at 60 fps). */
53
+ readonly overFactor?: number;
54
+ /** Step up when p95 is under budget x this. Default 1.05 (17.5 ms at 60 fps). */
55
+ readonly underFactor?: number;
56
+ readonly step?: number;
57
+ readonly min?: number;
58
+ readonly max?: number;
59
+ /** Minimum ms between changes. Default 2000. */
60
+ readonly hysteresisMs?: number;
61
+ /** Frames needed in the window before deciding. Default 120. */
62
+ readonly minSamples?: number;
63
+ /** Rolling window size. Default 240 frames. */
64
+ readonly window?: number;
65
+ }
66
+ /**
67
+ * Pixel-ratio controller: trades pixels for frame time. Push every frame's
68
+ * interval; `update(now)` returns the new ratio when it changes, else null.
69
+ * After a change the window restarts so the next decision sees the new cost.
70
+ */
71
+ export declare class DynamicResolution {
72
+ private readonly buf;
73
+ private i;
74
+ private n;
75
+ private lastChange;
76
+ ratio: number;
77
+ readonly o: Required<DynamicResolutionOptions>;
78
+ constructor(initial: number, opts?: DynamicResolutionOptions);
79
+ push(frameMs: number): void;
80
+ p95(): number;
81
+ /** Clamp the ratio range (a tier change). Returns the new ratio if it moved. */
82
+ setRange(min: number, max: number): number | null;
83
+ private lastCheck;
84
+ update(now: number): number | null;
85
+ }
86
+ export interface QualityTarget {
87
+ setPixelRatio(ratio: number): void;
88
+ }
89
+ /**
90
+ * Holds the tier and the dynamic resolution for one renderer. Listeners
91
+ * (lighting, post, particles) re-read `settings` on `onChange`.
92
+ */
93
+ export declare class QualityManager {
94
+ private readonly target;
95
+ settings: QualitySettings;
96
+ readonly dynamic: DynamicResolution;
97
+ private readonly listeners;
98
+ private last;
99
+ constructor(target: QualityTarget, tier: QualityTier, opts?: {
100
+ devicePixelRatio?: number;
101
+ dynamic?: boolean;
102
+ budgetMs?: number;
103
+ });
104
+ /** Dynamic resolution on (default). */
105
+ enabled: boolean;
106
+ private readonly dpr;
107
+ get tier(): QualityTier;
108
+ get pixelRatio(): number;
109
+ onChange(fn: (s: QualitySettings) => void): void;
110
+ setTier(tier: QualityTier): void;
111
+ /** Call once per frame with the real time (ms). Returns true when the pixel ratio changed (resize needed). */
112
+ frame(now: number): boolean;
113
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Quality tiers and dynamic resolution ("auto-detected
3
+ * low, medium or high (shadows, post-processing, resolution scale) with
4
+ * dynamic resolution to hold the frame rate"). The decision logic is pure
5
+ * so it is tested in node; `QualityManager` applies it to a renderer.
6
+ */
7
+ export const QUALITY_TIERS = ["low", "medium", "high"];
8
+ const SETTINGS = {
9
+ low: { tier: "low", shadowMapSize: 0, post: false, bloomScale: 0.25, outline: false, maxPixelRatio: 1.25, minPixelRatio: 0.6, particles: 1024 },
10
+ medium: { tier: "medium", shadowMapSize: 1024, post: true, bloomScale: 0.33, outline: true, maxPixelRatio: 1.5, minPixelRatio: 0.75, particles: 2048 },
11
+ high: { tier: "high", shadowMapSize: 2048, post: true, bloomScale: 0.5, outline: true, maxPixelRatio: 2, minPixelRatio: 0.75, particles: 4096 },
12
+ };
13
+ export function qualitySettings(tier) {
14
+ return SETTINGS[tier];
15
+ }
16
+ const MOBILE_UA = /Android|iPhone|iPad|iPod|Mobile|Silk|Kindle|Opera Mini/i;
17
+ /**
18
+ * Pick a tier from device hints. Phones and tablets start at medium (low on
19
+ * weak or WebGL-only ones); desktops start at high unless the CPU is small.
20
+ * Dynamic resolution then holds the frame rate within the tier.
21
+ */
22
+ export function detectTier(h) {
23
+ const mobile = h.coarsePointer === true || MOBILE_UA.test(h.userAgent ?? "");
24
+ const cores = h.hardwareConcurrency ?? 4;
25
+ const mem = h.deviceMemory ?? 8;
26
+ if (mobile) {
27
+ if (cores <= 4 || mem <= 3 || h.backend === "webgl2")
28
+ return "low";
29
+ return "medium";
30
+ }
31
+ if (cores <= 2 || mem <= 2)
32
+ return "low";
33
+ if (cores <= 4 || h.backend === "webgl2")
34
+ return "medium";
35
+ return "high";
36
+ }
37
+ /** Read the hints from the running browser (safe in node: returns {}). */
38
+ export function browserHints(backend) {
39
+ const g = globalThis;
40
+ return {
41
+ ...(g.navigator?.userAgent !== undefined ? { userAgent: g.navigator.userAgent } : {}),
42
+ ...(g.matchMedia ? { coarsePointer: g.matchMedia("(pointer: coarse)").matches } : {}),
43
+ ...(g.navigator?.hardwareConcurrency !== undefined ? { hardwareConcurrency: g.navigator.hardwareConcurrency } : {}),
44
+ ...(g.navigator?.deviceMemory !== undefined ? { deviceMemory: g.navigator.deviceMemory } : {}),
45
+ ...(g.devicePixelRatio !== undefined ? { devicePixelRatio: g.devicePixelRatio } : {}),
46
+ ...(g.screen?.width !== undefined ? { screenWidth: g.screen.width } : {}),
47
+ ...(backend ? { backend } : {}),
48
+ };
49
+ }
50
+ /**
51
+ * Pixel-ratio controller: trades pixels for frame time. Push every frame's
52
+ * interval; `update(now)` returns the new ratio when it changes, else null.
53
+ * After a change the window restarts so the next decision sees the new cost.
54
+ */
55
+ export class DynamicResolution {
56
+ buf;
57
+ i = 0;
58
+ n = 0;
59
+ lastChange = -Infinity;
60
+ ratio;
61
+ o;
62
+ constructor(initial, opts = {}) {
63
+ this.o = {
64
+ budgetMs: 1000 / 60,
65
+ overFactor: 1.26,
66
+ underFactor: 1.05,
67
+ step: 0.25,
68
+ min: 0.75,
69
+ max: 2,
70
+ hysteresisMs: 2000,
71
+ minSamples: 120,
72
+ window: 240,
73
+ ...opts,
74
+ };
75
+ this.buf = new Float64Array(this.o.window);
76
+ this.ratio = Math.min(this.o.max, Math.max(this.o.min, initial));
77
+ }
78
+ push(frameMs) {
79
+ this.buf[this.i] = frameMs;
80
+ this.i = (this.i + 1) % this.buf.length;
81
+ this.n = Math.min(this.n + 1, this.buf.length);
82
+ }
83
+ p95() {
84
+ if (this.n === 0)
85
+ return 0;
86
+ const a = Array.from(this.buf.subarray(0, this.n)).sort((x, y) => x - y);
87
+ return a[Math.min(a.length - 1, Math.floor(0.95 * (a.length - 1)))] ?? 0;
88
+ }
89
+ /** Clamp the ratio range (a tier change). Returns the new ratio if it moved. */
90
+ setRange(min, max) {
91
+ this.o.min = min;
92
+ this.o.max = max;
93
+ const next = Math.min(max, Math.max(min, this.ratio));
94
+ if (next === this.ratio)
95
+ return null;
96
+ this.ratio = next;
97
+ return next;
98
+ }
99
+ lastCheck = -Infinity;
100
+ update(now) {
101
+ if (this.n < this.o.minSamples || now - this.lastChange < this.o.hysteresisMs)
102
+ return null;
103
+ // The p95 sorts the whole window: look at it at most twice a second, not every frame.
104
+ if (now - this.lastCheck < Math.min(500, this.o.hysteresisMs))
105
+ return null;
106
+ this.lastCheck = now;
107
+ const p95 = this.p95();
108
+ let next = this.ratio;
109
+ if (p95 > this.o.budgetMs * this.o.overFactor && this.ratio > this.o.min)
110
+ next = Math.max(this.o.min, this.ratio - this.o.step);
111
+ else if (p95 < this.o.budgetMs * this.o.underFactor && this.ratio < this.o.max)
112
+ next = Math.min(this.o.max, this.ratio + this.o.step);
113
+ if (next === this.ratio)
114
+ return null;
115
+ this.ratio = next;
116
+ this.lastChange = now;
117
+ this.n = 0;
118
+ this.i = 0;
119
+ return next;
120
+ }
121
+ }
122
+ /**
123
+ * Holds the tier and the dynamic resolution for one renderer. Listeners
124
+ * (lighting, post, particles) re-read `settings` on `onChange`.
125
+ */
126
+ export class QualityManager {
127
+ target;
128
+ settings;
129
+ dynamic;
130
+ listeners = [];
131
+ last = 0;
132
+ constructor(target, tier, opts = {}) {
133
+ this.target = target;
134
+ this.settings = qualitySettings(tier);
135
+ const dpr = opts.devicePixelRatio ?? 1;
136
+ const max = Math.min(dpr, this.settings.maxPixelRatio);
137
+ this.dynamic = new DynamicResolution(max, {
138
+ min: Math.min(max, this.settings.minPixelRatio),
139
+ max,
140
+ ...(opts.budgetMs !== undefined ? { budgetMs: opts.budgetMs } : {}),
141
+ });
142
+ this.enabled = opts.dynamic ?? true;
143
+ this.dpr = dpr;
144
+ target.setPixelRatio(this.dynamic.ratio);
145
+ }
146
+ /** Dynamic resolution on (default). */
147
+ enabled;
148
+ dpr;
149
+ get tier() {
150
+ return this.settings.tier;
151
+ }
152
+ get pixelRatio() {
153
+ return this.dynamic.ratio;
154
+ }
155
+ onChange(fn) {
156
+ this.listeners.push(fn);
157
+ }
158
+ setTier(tier) {
159
+ this.settings = qualitySettings(tier);
160
+ const max = Math.min(this.dpr, this.settings.maxPixelRatio);
161
+ const moved = this.dynamic.setRange(Math.min(max, this.settings.minPixelRatio), max);
162
+ if (moved !== null)
163
+ this.target.setPixelRatio(moved);
164
+ for (const l of this.listeners)
165
+ l(this.settings);
166
+ }
167
+ /** Call once per frame with the real time (ms). Returns true when the pixel ratio changed (resize needed). */
168
+ frame(now) {
169
+ if (this.last !== 0)
170
+ this.dynamic.push(now - this.last);
171
+ this.last = now;
172
+ if (!this.enabled)
173
+ return false;
174
+ const next = this.dynamic.update(now);
175
+ if (next === null)
176
+ return false;
177
+ this.target.setPixelRatio(next);
178
+ return true;
179
+ }
180
+ }
@@ -0,0 +1,29 @@
1
+ import { AnimationClip, Bone, SkinnedMesh } from "three/webgpu";
2
+ import type { BufferGeometry, Material } from "three/webgpu";
3
+ export interface SegmentRigOptions {
4
+ /** Total height, m. Default 1.2. */
5
+ readonly height?: number;
6
+ /** Radius at the base, m. Default 0.35. */
7
+ readonly radius?: number;
8
+ /** Radius at the top (a cone when 0). Default radius x 0.75. */
9
+ readonly topRadius?: number;
10
+ readonly radialSegments?: number;
11
+ readonly heightSegments?: number;
12
+ /** Use this geometry instead of a cylinder (it must stand on y = 0, up to `height`). */
13
+ readonly geometry?: BufferGeometry;
14
+ }
15
+ export interface SegmentRig {
16
+ readonly mesh: SkinnedMesh;
17
+ /** hip, spine, head. */
18
+ readonly bones: readonly [Bone, Bone, Bone];
19
+ /** "rest", "idle", "walk", "run", "hit", "attack", "die". */
20
+ readonly clips: readonly AnimationClip[];
21
+ }
22
+ /**
23
+ * A three-bone procedural character (hip, spine, head) skinned to a
24
+ * cylinder or any upright geometry, with a full animation set built in
25
+ * code: rest, idle, walk, run, hit, attack, die. Templates with procedural
26
+ * art get real skinned animation (and VAT crowds via bakeVertexAnimation)
27
+ * without any asset files.
28
+ */
29
+ export declare function buildSegmentRig(material: Material, opts?: SegmentRigOptions): SegmentRig;