@guinetik/gcanvas 1.0.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.
- package/.github/workflows/release.yaml +70 -0
- package/.jshintrc +4 -0
- package/.vscode/settings.json +22 -0
- package/CLAUDE.md +310 -0
- package/blackhole.jpg +0 -0
- package/demo.png +0 -0
- package/demos/CNAME +1 -0
- package/demos/animations.html +31 -0
- package/demos/basic.html +38 -0
- package/demos/baskara.html +31 -0
- package/demos/bezier.html +35 -0
- package/demos/beziersignature.html +29 -0
- package/demos/blackhole.html +28 -0
- package/demos/blob.html +35 -0
- package/demos/demos.css +289 -0
- package/demos/easing.html +28 -0
- package/demos/events.html +195 -0
- package/demos/fluent.html +647 -0
- package/demos/fractals.html +36 -0
- package/demos/genart.html +26 -0
- package/demos/gendream.html +26 -0
- package/demos/group.html +36 -0
- package/demos/home.html +587 -0
- package/demos/index.html +364 -0
- package/demos/isometric.html +34 -0
- package/demos/js/animations.js +452 -0
- package/demos/js/basic.js +204 -0
- package/demos/js/baskara.js +751 -0
- package/demos/js/bezier.js +692 -0
- package/demos/js/beziersignature.js +241 -0
- package/demos/js/blackhole/accretiondisk.obj.js +379 -0
- package/demos/js/blackhole/blackhole.obj.js +318 -0
- package/demos/js/blackhole/index.js +409 -0
- package/demos/js/blackhole/particle.js +56 -0
- package/demos/js/blackhole/starfield.obj.js +218 -0
- package/demos/js/blob.js +2263 -0
- package/demos/js/easing.js +477 -0
- package/demos/js/fluent.js +183 -0
- package/demos/js/fractals.js +931 -0
- package/demos/js/fractalworker.js +93 -0
- package/demos/js/genart.js +268 -0
- package/demos/js/gendream.js +209 -0
- package/demos/js/group.js +140 -0
- package/demos/js/info-toggle.js +25 -0
- package/demos/js/isometric.js +863 -0
- package/demos/js/kerr.js +1556 -0
- package/demos/js/lavalamp.js +590 -0
- package/demos/js/layout.js +354 -0
- package/demos/js/mondrian.js +285 -0
- package/demos/js/opacity.js +275 -0
- package/demos/js/painter.js +484 -0
- package/demos/js/particles-showcase.js +514 -0
- package/demos/js/particles.js +299 -0
- package/demos/js/patterns.js +397 -0
- package/demos/js/penrose/artifact.js +69 -0
- package/demos/js/penrose/blackhole.js +121 -0
- package/demos/js/penrose/constants.js +73 -0
- package/demos/js/penrose/game.js +943 -0
- package/demos/js/penrose/lore.js +278 -0
- package/demos/js/penrose/penrosescene.js +892 -0
- package/demos/js/penrose/ship.js +216 -0
- package/demos/js/penrose/sounds.js +211 -0
- package/demos/js/penrose/voidparticle.js +55 -0
- package/demos/js/penrose/voidscene.js +258 -0
- package/demos/js/penrose/voidship.js +144 -0
- package/demos/js/penrose/wormhole.js +46 -0
- package/demos/js/pipeline.js +555 -0
- package/demos/js/scene.js +304 -0
- package/demos/js/scenes.js +320 -0
- package/demos/js/schrodinger.js +410 -0
- package/demos/js/schwarzschild.js +1023 -0
- package/demos/js/shapes.js +628 -0
- package/demos/js/space/alien.js +171 -0
- package/demos/js/space/boom.js +98 -0
- package/demos/js/space/boss.js +353 -0
- package/demos/js/space/buff.js +73 -0
- package/demos/js/space/bullet.js +102 -0
- package/demos/js/space/constants.js +85 -0
- package/demos/js/space/game.js +1884 -0
- package/demos/js/space/hud.js +112 -0
- package/demos/js/space/laserbeam.js +179 -0
- package/demos/js/space/lightning.js +277 -0
- package/demos/js/space/minion.js +192 -0
- package/demos/js/space/missile.js +212 -0
- package/demos/js/space/player.js +430 -0
- package/demos/js/space/powerup.js +90 -0
- package/demos/js/space/starfield.js +58 -0
- package/demos/js/space/starpower.js +90 -0
- package/demos/js/spacetime.js +559 -0
- package/demos/js/svgtween.js +204 -0
- package/demos/js/tde/accretiondisk.js +418 -0
- package/demos/js/tde/blackhole.js +219 -0
- package/demos/js/tde/blackholescene.js +209 -0
- package/demos/js/tde/config.js +59 -0
- package/demos/js/tde/index.js +695 -0
- package/demos/js/tde/jets.js +290 -0
- package/demos/js/tde/lensedstarfield.js +147 -0
- package/demos/js/tde/tdestar.js +317 -0
- package/demos/js/tde/tidalstream.js +356 -0
- package/demos/js/tde_old/blackhole.obj.js +354 -0
- package/demos/js/tde_old/debris.obj.js +791 -0
- package/demos/js/tde_old/flare.obj.js +239 -0
- package/demos/js/tde_old/index.js +448 -0
- package/demos/js/tde_old/star.obj.js +812 -0
- package/demos/js/tiles.js +312 -0
- package/demos/js/tweendemo.js +79 -0
- package/demos/js/visibility.js +102 -0
- package/demos/kerr.html +28 -0
- package/demos/lavalamp.html +27 -0
- package/demos/layouts.html +37 -0
- package/demos/logo.svg +4 -0
- package/demos/loop.html +84 -0
- package/demos/mondrian.html +32 -0
- package/demos/og_image.png +0 -0
- package/demos/opacity.html +36 -0
- package/demos/painter.html +39 -0
- package/demos/particles-showcase.html +28 -0
- package/demos/particles.html +24 -0
- package/demos/patterns.html +33 -0
- package/demos/penrose-game.html +31 -0
- package/demos/pipeline.html +737 -0
- package/demos/scene.html +33 -0
- package/demos/scenes.html +96 -0
- package/demos/schrodinger.html +27 -0
- package/demos/schwarzschild.html +27 -0
- package/demos/shapes.html +16 -0
- package/demos/space.html +85 -0
- package/demos/spacetime.html +27 -0
- package/demos/svgtween.html +29 -0
- package/demos/tde.html +28 -0
- package/demos/tiles.html +28 -0
- package/demos/transforms.html +400 -0
- package/demos/tween.html +45 -0
- package/demos/visibility.html +33 -0
- package/disk_example.png +0 -0
- package/docs/README.md +222 -0
- package/docs/concepts/architecture-overview.md +204 -0
- package/docs/concepts/lifecycle.md +255 -0
- package/docs/concepts/rendering-pipeline.md +279 -0
- package/docs/concepts/tde-zorder.md +106 -0
- package/docs/concepts/two-layer-architecture.md +229 -0
- package/docs/getting-started/first-game.md +354 -0
- package/docs/getting-started/hello-world.md +269 -0
- package/docs/getting-started/installation.md +157 -0
- package/docs/modules/collision/README.md +453 -0
- package/docs/modules/fluent/README.md +1075 -0
- package/docs/modules/game/README.md +303 -0
- package/docs/modules/isometric-camera.md +210 -0
- package/docs/modules/isometric.md +275 -0
- package/docs/modules/painter/README.md +328 -0
- package/docs/modules/particle/README.md +559 -0
- package/docs/modules/shapes/README.md +221 -0
- package/docs/modules/shapes/base/euclidian.md +123 -0
- package/docs/modules/shapes/base/geometry2d.md +204 -0
- package/docs/modules/shapes/base/renderable.md +215 -0
- package/docs/modules/shapes/base/shape.md +262 -0
- package/docs/modules/shapes/base/transformable.md +243 -0
- package/docs/modules/shapes/hierarchy.md +218 -0
- package/docs/modules/state/README.md +577 -0
- package/docs/modules/util/README.md +99 -0
- package/docs/modules/util/camera3d.md +412 -0
- package/docs/modules/util/scene3d.md +395 -0
- package/index.html +17 -0
- package/jsdoc.json +50 -0
- package/package.json +55 -0
- package/readme.md +599 -0
- package/scripts/build-demo.js +69 -0
- package/scripts/bundle4llm.js +276 -0
- package/scripts/clearconsole.js +48 -0
- package/src/collision/collision-system.js +332 -0
- package/src/collision/collision.js +303 -0
- package/src/collision/index.js +10 -0
- package/src/fluent/fluent-game.js +430 -0
- package/src/fluent/fluent-go.js +1060 -0
- package/src/fluent/fluent-layer.js +152 -0
- package/src/fluent/fluent-scene.js +291 -0
- package/src/fluent/index.js +98 -0
- package/src/fluent/sketch.js +380 -0
- package/src/game/game.js +467 -0
- package/src/game/index.js +49 -0
- package/src/game/objects/go.js +220 -0
- package/src/game/objects/imagego.js +30 -0
- package/src/game/objects/index.js +54 -0
- package/src/game/objects/isometric-scene.js +260 -0
- package/src/game/objects/layoutscene.js +549 -0
- package/src/game/objects/scene.js +175 -0
- package/src/game/objects/scene3d.js +118 -0
- package/src/game/objects/text.js +221 -0
- package/src/game/objects/wrapper.js +232 -0
- package/src/game/pipeline.js +243 -0
- package/src/game/ui/button.js +396 -0
- package/src/game/ui/cursor.js +93 -0
- package/src/game/ui/fps.js +91 -0
- package/src/game/ui/index.js +5 -0
- package/src/game/ui/togglebutton.js +93 -0
- package/src/game/ui/tooltip.js +249 -0
- package/src/index.js +25 -0
- package/src/io/events.js +20 -0
- package/src/io/index.js +86 -0
- package/src/io/input.js +70 -0
- package/src/io/keys.js +152 -0
- package/src/io/mouse.js +61 -0
- package/src/io/touch.js +39 -0
- package/src/logger/debugtab.js +138 -0
- package/src/logger/index.js +3 -0
- package/src/logger/loggable.js +47 -0
- package/src/logger/logger.js +113 -0
- package/src/math/complex.js +37 -0
- package/src/math/constants.js +1 -0
- package/src/math/fractal.js +1271 -0
- package/src/math/gr.js +201 -0
- package/src/math/heat.js +202 -0
- package/src/math/index.js +12 -0
- package/src/math/noise.js +433 -0
- package/src/math/orbital.js +191 -0
- package/src/math/patterns.js +1339 -0
- package/src/math/penrose.js +259 -0
- package/src/math/quantum.js +115 -0
- package/src/math/random.js +195 -0
- package/src/math/tensor.js +1009 -0
- package/src/mixins/anchor.js +131 -0
- package/src/mixins/draggable.js +72 -0
- package/src/mixins/index.js +2 -0
- package/src/motion/bezier.js +132 -0
- package/src/motion/bounce.js +58 -0
- package/src/motion/easing.js +349 -0
- package/src/motion/float.js +130 -0
- package/src/motion/follow.js +125 -0
- package/src/motion/hop.js +52 -0
- package/src/motion/index.js +82 -0
- package/src/motion/motion.js +1124 -0
- package/src/motion/orbit.js +49 -0
- package/src/motion/oscillate.js +39 -0
- package/src/motion/parabolic.js +141 -0
- package/src/motion/patrol.js +147 -0
- package/src/motion/pendulum.js +48 -0
- package/src/motion/pulse.js +88 -0
- package/src/motion/shake.js +83 -0
- package/src/motion/spiral.js +144 -0
- package/src/motion/spring.js +150 -0
- package/src/motion/swing.js +47 -0
- package/src/motion/tween.js +92 -0
- package/src/motion/tweenetik.js +139 -0
- package/src/motion/waypoint.js +210 -0
- package/src/painter/index.js +8 -0
- package/src/painter/painter.colors.js +331 -0
- package/src/painter/painter.effects.js +230 -0
- package/src/painter/painter.img.js +229 -0
- package/src/painter/painter.js +295 -0
- package/src/painter/painter.lines.js +189 -0
- package/src/painter/painter.opacity.js +41 -0
- package/src/painter/painter.shapes.js +277 -0
- package/src/painter/painter.text.js +273 -0
- package/src/particle/emitter.js +124 -0
- package/src/particle/index.js +11 -0
- package/src/particle/particle-system.js +322 -0
- package/src/particle/particle.js +71 -0
- package/src/particle/updaters.js +170 -0
- package/src/shapes/arc.js +43 -0
- package/src/shapes/arrow.js +33 -0
- package/src/shapes/bezier.js +42 -0
- package/src/shapes/circle.js +62 -0
- package/src/shapes/clouds.js +56 -0
- package/src/shapes/cone.js +219 -0
- package/src/shapes/cross.js +70 -0
- package/src/shapes/cube.js +244 -0
- package/src/shapes/cylinder.js +254 -0
- package/src/shapes/diamond.js +48 -0
- package/src/shapes/euclidian.js +111 -0
- package/src/shapes/figure.js +115 -0
- package/src/shapes/geometry.js +220 -0
- package/src/shapes/group.js +375 -0
- package/src/shapes/heart.js +42 -0
- package/src/shapes/hexagon.js +26 -0
- package/src/shapes/image.js +192 -0
- package/src/shapes/index.js +111 -0
- package/src/shapes/line.js +29 -0
- package/src/shapes/pattern.js +90 -0
- package/src/shapes/pin.js +44 -0
- package/src/shapes/poly.js +31 -0
- package/src/shapes/prism.js +226 -0
- package/src/shapes/rect.js +35 -0
- package/src/shapes/renderable.js +333 -0
- package/src/shapes/ring.js +26 -0
- package/src/shapes/roundrect.js +95 -0
- package/src/shapes/shape.js +117 -0
- package/src/shapes/slice.js +26 -0
- package/src/shapes/sphere.js +314 -0
- package/src/shapes/sphere3d.js +537 -0
- package/src/shapes/square.js +15 -0
- package/src/shapes/star.js +99 -0
- package/src/shapes/svg.js +408 -0
- package/src/shapes/text.js +553 -0
- package/src/shapes/traceable.js +83 -0
- package/src/shapes/transform.js +357 -0
- package/src/shapes/transformable.js +172 -0
- package/src/shapes/triangle.js +26 -0
- package/src/sound/index.js +17 -0
- package/src/sound/sound.js +473 -0
- package/src/sound/synth.analyzer.js +149 -0
- package/src/sound/synth.effects.js +207 -0
- package/src/sound/synth.envelope.js +59 -0
- package/src/sound/synth.js +229 -0
- package/src/sound/synth.musical.js +160 -0
- package/src/sound/synth.noise.js +85 -0
- package/src/sound/synth.oscillators.js +293 -0
- package/src/state/index.js +10 -0
- package/src/state/state-machine.js +371 -0
- package/src/util/camera3d.js +438 -0
- package/src/util/index.js +6 -0
- package/src/util/isometric-camera.js +235 -0
- package/src/util/layout.js +317 -0
- package/src/util/position.js +147 -0
- package/src/util/tasks.js +47 -0
- package/src/util/zindex.js +287 -0
- package/src/webgl/index.js +9 -0
- package/src/webgl/shaders/sphere-shaders.js +994 -0
- package/src/webgl/webgl-renderer.js +388 -0
- package/tde.png +0 -0
- package/test/math/orbital.test.js +61 -0
- package/test/math/tensor.test.js +114 -0
- package/test/particle/emitter.test.js +204 -0
- package/test/particle/particle-system.test.js +310 -0
- package/test/particle/particle.test.js +116 -0
- package/test/particle/updaters.test.js +386 -0
- package/test/setup.js +120 -0
- package/test/shapes/euclidian.test.js +44 -0
- package/test/shapes/geometry.test.js +86 -0
- package/test/shapes/group.test.js +86 -0
- package/test/shapes/rectangle.test.js +64 -0
- package/test/shapes/transform.test.js +379 -0
- package/test/util/camera3d.test.js +428 -0
- package/test/util/scene3d.test.js +352 -0
- package/types/collision.d.ts +249 -0
- package/types/common.d.ts +155 -0
- package/types/game.d.ts +497 -0
- package/types/index.d.ts +309 -0
- package/types/io.d.ts +188 -0
- package/types/logger.d.ts +127 -0
- package/types/math.d.ts +268 -0
- package/types/mixins.d.ts +92 -0
- package/types/motion.d.ts +678 -0
- package/types/painter.d.ts +378 -0
- package/types/shapes.d.ts +864 -0
- package/types/sound.d.ts +672 -0
- package/types/state.d.ts +251 -0
- package/types/util.d.ts +253 -0
- package/vite.config.js +50 -0
- package/vitest.config.js +13 -0
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IsometricGame Demo
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the IsometricScene class for creating isometric tile-based games.
|
|
5
|
+
* Features a bouncing ball that can be controlled with WASD and Space to jump.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
Game,
|
|
9
|
+
GameObject,
|
|
10
|
+
IsometricScene,
|
|
11
|
+
IsometricCamera,
|
|
12
|
+
Button,
|
|
13
|
+
TextShape,
|
|
14
|
+
Painter,
|
|
15
|
+
Keys
|
|
16
|
+
} from "../../src/index";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Configuration for the isometric demo
|
|
20
|
+
*/
|
|
21
|
+
const CONFIG = {
|
|
22
|
+
gridSize: 10,
|
|
23
|
+
tileWidth: 64,
|
|
24
|
+
tileHeight: 32,
|
|
25
|
+
elevationScale: 1.0,
|
|
26
|
+
ball: {
|
|
27
|
+
baseRadius: 8,
|
|
28
|
+
color: "#3498db",
|
|
29
|
+
strokeColor: "#2980b9",
|
|
30
|
+
jumpPower: 4,
|
|
31
|
+
gravity: 0.25,
|
|
32
|
+
acceleration: 0.8, // grid units per second squared
|
|
33
|
+
maxVelocity: 0.12, // max grid units per frame
|
|
34
|
+
friction: 0.90,
|
|
35
|
+
bounceFactorWall: 0.5,
|
|
36
|
+
bounceFactorGround: 0.2,
|
|
37
|
+
},
|
|
38
|
+
grid: {
|
|
39
|
+
lineColor: "#ccc",
|
|
40
|
+
originColor: "#FF0000",
|
|
41
|
+
originRadius: 5,
|
|
42
|
+
},
|
|
43
|
+
// Platform layout: [x, y, width, depth, height, color]
|
|
44
|
+
platforms: [
|
|
45
|
+
// Starting platform (center) - big and low
|
|
46
|
+
{ x: -2, y: -2, w: 4, d: 4, h: 20, color: "#8B4513" },
|
|
47
|
+
|
|
48
|
+
// Ramp going up-right (stepping stones)
|
|
49
|
+
{ x: 2, y: -2, w: 2, d: 2, h: 35, color: "#A0522D" },
|
|
50
|
+
{ x: 4, y: -2, w: 2, d: 2, h: 50, color: "#A0522D" },
|
|
51
|
+
{ x: 6, y: -2, w: 3, d: 3, h: 65, color: "#CD853F" },
|
|
52
|
+
|
|
53
|
+
// High platform (top right)
|
|
54
|
+
{ x: 6, y: -6, w: 3, d: 3, h: 80, color: "#DEB887" },
|
|
55
|
+
|
|
56
|
+
// Bridge/ramp going back
|
|
57
|
+
{ x: 4, y: -6, w: 2, d: 2, h: 65, color: "#A0522D" },
|
|
58
|
+
{ x: 2, y: -6, w: 2, d: 2, h: 50, color: "#A0522D" },
|
|
59
|
+
{ x: 0, y: -6, w: 2, d: 2, h: 35, color: "#8B4513" },
|
|
60
|
+
|
|
61
|
+
// Side platform (green area)
|
|
62
|
+
{ x: -6, y: 0, w: 3, d: 3, h: 40, color: "#6B8E23" },
|
|
63
|
+
{ x: -6, y: 3, w: 2, d: 2, h: 25, color: "#556B2F" },
|
|
64
|
+
|
|
65
|
+
// Lower platform (blue)
|
|
66
|
+
{ x: 3, y: 3, w: 3, d: 3, h: 30, color: "#4682B4" },
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* An isometric 3D box/platform that the ball can stand on.
|
|
72
|
+
*/
|
|
73
|
+
class IsometricBox extends GameObject {
|
|
74
|
+
/**
|
|
75
|
+
* @param {Game} game - Game instance
|
|
76
|
+
* @param {IsometricScene} isoScene - The parent isometric scene
|
|
77
|
+
* @param {Object} options - Box configuration
|
|
78
|
+
* @param {number} options.x - Grid X position
|
|
79
|
+
* @param {number} options.y - Grid Y position
|
|
80
|
+
* @param {number} options.w - Width in grid units
|
|
81
|
+
* @param {number} options.d - Depth in grid units
|
|
82
|
+
* @param {number} options.h - Height in pixels
|
|
83
|
+
* @param {string} options.color - Base color
|
|
84
|
+
*/
|
|
85
|
+
constructor(game, isoScene, options) {
|
|
86
|
+
super(game);
|
|
87
|
+
this.isoScene = isoScene;
|
|
88
|
+
this.x = options.x;
|
|
89
|
+
this.y = options.y;
|
|
90
|
+
this.w = options.w;
|
|
91
|
+
this.d = options.d;
|
|
92
|
+
this.h = options.h;
|
|
93
|
+
this.baseColor = options.color;
|
|
94
|
+
|
|
95
|
+
// Calculate colors for shading
|
|
96
|
+
this.topColor = options.color;
|
|
97
|
+
this.leftColor = this.shadeColor(options.color, -30);
|
|
98
|
+
this.rightColor = this.shadeColor(options.color, -50);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Custom depth value for sorting - uses front corner for proper overlap
|
|
103
|
+
*/
|
|
104
|
+
get isoDepth() {
|
|
105
|
+
// Use the front-most corner (x+w, y+d) plus height
|
|
106
|
+
// Height factor matches ball's z factor for consistent sorting
|
|
107
|
+
return (this.x + this.w) + (this.y + this.d) + this.h * 0.5;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Shade a hex color by a percentage
|
|
112
|
+
* @param {string} color - Hex color
|
|
113
|
+
* @param {number} percent - Percentage to lighten/darken
|
|
114
|
+
* @returns {string} Shaded hex color
|
|
115
|
+
*/
|
|
116
|
+
shadeColor(color, percent) {
|
|
117
|
+
const num = parseInt(color.replace("#", ""), 16);
|
|
118
|
+
const amt = Math.round(2.55 * percent);
|
|
119
|
+
const R = Math.max(0, Math.min(255, (num >> 16) + amt));
|
|
120
|
+
const G = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amt));
|
|
121
|
+
const B = Math.max(0, Math.min(255, (num & 0x0000FF) + amt));
|
|
122
|
+
return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if a point is inside this box's X/Y bounds
|
|
127
|
+
* @param {number} px - Point X in grid
|
|
128
|
+
* @param {number} py - Point Y in grid
|
|
129
|
+
* @param {number} margin - Optional margin for collision (default 0.1)
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
containsPoint(px, py, margin = 0.1) {
|
|
133
|
+
return px >= this.x - margin && px < this.x + this.w + margin &&
|
|
134
|
+
py >= this.y - margin && py < this.y + this.d + margin;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get the surface height for landing.
|
|
139
|
+
* Always returns the platform height - collision detection handles whether to use it.
|
|
140
|
+
* @returns {number} Platform height
|
|
141
|
+
*/
|
|
142
|
+
getSurfaceHeight() {
|
|
143
|
+
return this.h;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Renders the isometric box with all visible faces.
|
|
148
|
+
* Draws back faces first, front faces last, based on camera angle.
|
|
149
|
+
*/
|
|
150
|
+
render() {
|
|
151
|
+
const scene = this.isoScene;
|
|
152
|
+
|
|
153
|
+
// Get camera angle (direction camera is looking FROM)
|
|
154
|
+
const cameraAngle = scene.camera ? scene.camera.angle : 0;
|
|
155
|
+
|
|
156
|
+
// Camera view direction (where camera is looking TOWARD)
|
|
157
|
+
// In isometric, default view looks toward +X +Y direction (angle π/4 from +X axis)
|
|
158
|
+
// Camera rotation rotates around Z axis
|
|
159
|
+
const viewDirection = Math.PI / 4 + cameraAngle;
|
|
160
|
+
|
|
161
|
+
// Get all 8 corners of the box
|
|
162
|
+
const topNW = scene.toIsometric(this.x, this.y, this.h);
|
|
163
|
+
const topNE = scene.toIsometric(this.x + this.w, this.y, this.h);
|
|
164
|
+
const topSE = scene.toIsometric(this.x + this.w, this.y + this.d, this.h);
|
|
165
|
+
const topSW = scene.toIsometric(this.x, this.y + this.d, this.h);
|
|
166
|
+
|
|
167
|
+
const botNW = scene.toIsometric(this.x, this.y, 0);
|
|
168
|
+
const botNE = scene.toIsometric(this.x + this.w, this.y, 0);
|
|
169
|
+
const botSE = scene.toIsometric(this.x + this.w, this.y + this.d, 0);
|
|
170
|
+
const botSW = scene.toIsometric(this.x, this.y + this.d, 0);
|
|
171
|
+
|
|
172
|
+
// Light direction (fixed in world space - from upper left)
|
|
173
|
+
const lightAngle = -Math.PI / 4;
|
|
174
|
+
|
|
175
|
+
// Define the 4 side faces with their world-space normal directions
|
|
176
|
+
const faces = [
|
|
177
|
+
{ // North face (Y-): normal points toward -Y
|
|
178
|
+
verts: [topNW, topNE, botNE, botNW],
|
|
179
|
+
normalAngle: -Math.PI / 2,
|
|
180
|
+
},
|
|
181
|
+
{ // East face (X+): normal points toward +X
|
|
182
|
+
verts: [topNE, topSE, botSE, botNE],
|
|
183
|
+
normalAngle: 0,
|
|
184
|
+
},
|
|
185
|
+
{ // South face (Y+): normal points toward +Y
|
|
186
|
+
verts: [topSE, topSW, botSW, botSE],
|
|
187
|
+
normalAngle: Math.PI / 2,
|
|
188
|
+
},
|
|
189
|
+
{ // West face (X-): normal points toward -X
|
|
190
|
+
verts: [topSW, topNW, botNW, botSW],
|
|
191
|
+
normalAngle: Math.PI,
|
|
192
|
+
}
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
// Calculate shading and visibility for each face
|
|
196
|
+
for (const face of faces) {
|
|
197
|
+
// Rotate the face normal by camera angle
|
|
198
|
+
const rotatedNormal = face.normalAngle + cameraAngle;
|
|
199
|
+
|
|
200
|
+
// Face visibility: a face is visible if its rotated normal
|
|
201
|
+
// has a component pointing toward the camera (away from view direction)
|
|
202
|
+
// In isometric, visible faces are those facing generally toward -Y screen direction
|
|
203
|
+
const normalToView = rotatedNormal - viewDirection;
|
|
204
|
+
face.facingCamera = Math.cos(normalToView) < 0;
|
|
205
|
+
|
|
206
|
+
// For depth sorting: faces with normals pointing more toward +Y screen
|
|
207
|
+
// (into the screen in isometric) should be drawn first
|
|
208
|
+
face.depth = Math.sin(rotatedNormal);
|
|
209
|
+
|
|
210
|
+
// Lighting: based on angle between world-space normal and light
|
|
211
|
+
const lightDiff = face.normalAngle - lightAngle;
|
|
212
|
+
const lightFactor = (Math.cos(lightDiff) + 1) / 2; // 0 to 1
|
|
213
|
+
const shadeFactor = -50 + lightFactor * 60; // Range from -50 to +10
|
|
214
|
+
face.color = this.shadeColor(this.baseColor, shadeFactor);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Sort faces: draw back-facing first, then front-facing
|
|
218
|
+
// Within each group, sort by depth
|
|
219
|
+
faces.sort((a, b) => {
|
|
220
|
+
// Back-facing faces drawn first
|
|
221
|
+
if (a.facingCamera !== b.facingCamera) {
|
|
222
|
+
return a.facingCamera ? 1 : -1;
|
|
223
|
+
}
|
|
224
|
+
// Then by depth (lower depth = further back = draw first)
|
|
225
|
+
return a.depth - b.depth;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Draw faces in order: back faces first (with strokes), then front faces (fill covers back strokes)
|
|
229
|
+
Painter.useCtx((ctx) => {
|
|
230
|
+
ctx.strokeStyle = "rgba(0,0,0,0.4)";
|
|
231
|
+
ctx.lineWidth = 1;
|
|
232
|
+
|
|
233
|
+
// Draw each face with fill AND stroke, in depth order
|
|
234
|
+
// Back faces are drawn first - their strokes will show at the back edges
|
|
235
|
+
// Front faces are drawn last - their fills will cover internal strokes
|
|
236
|
+
for (const face of faces) {
|
|
237
|
+
ctx.beginPath();
|
|
238
|
+
ctx.moveTo(face.verts[0].x, face.verts[0].y);
|
|
239
|
+
ctx.lineTo(face.verts[1].x, face.verts[1].y);
|
|
240
|
+
ctx.lineTo(face.verts[2].x, face.verts[2].y);
|
|
241
|
+
ctx.lineTo(face.verts[3].x, face.verts[3].y);
|
|
242
|
+
ctx.closePath();
|
|
243
|
+
ctx.fillStyle = face.color;
|
|
244
|
+
ctx.fill();
|
|
245
|
+
ctx.stroke();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Draw top face last (always on top)
|
|
249
|
+
ctx.beginPath();
|
|
250
|
+
ctx.moveTo(topNW.x, topNW.y);
|
|
251
|
+
ctx.lineTo(topNE.x, topNE.y);
|
|
252
|
+
ctx.lineTo(topSE.x, topSE.y);
|
|
253
|
+
ctx.lineTo(topSW.x, topSW.y);
|
|
254
|
+
ctx.closePath();
|
|
255
|
+
ctx.fillStyle = this.topColor;
|
|
256
|
+
ctx.fill();
|
|
257
|
+
ctx.stroke();
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Renders the isometric grid lines and origin marker.
|
|
264
|
+
*
|
|
265
|
+
* Uses the parent IsometricScene's toIsometric() method for projection.
|
|
266
|
+
* Should be added with zIndex = -1 to render behind other objects.
|
|
267
|
+
*/
|
|
268
|
+
class IsometricGrid extends GameObject {
|
|
269
|
+
/**
|
|
270
|
+
* @param {Game} game - Game instance
|
|
271
|
+
* @param {IsometricScene} isoScene - The parent isometric scene for projection
|
|
272
|
+
*/
|
|
273
|
+
constructor(game, isoScene) {
|
|
274
|
+
super(game);
|
|
275
|
+
this.isoScene = isoScene;
|
|
276
|
+
this.zIndex = -1; // Render behind other objects
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Renders the grid lines and origin marker
|
|
281
|
+
*/
|
|
282
|
+
render() {
|
|
283
|
+
const gridSize = this.isoScene.gridSize;
|
|
284
|
+
|
|
285
|
+
// Set up line style
|
|
286
|
+
Painter.colors.setStrokeColor(CONFIG.grid.lineColor);
|
|
287
|
+
Painter.lines.setLineWidth(1);
|
|
288
|
+
|
|
289
|
+
// Draw vertical grid lines (along X axis)
|
|
290
|
+
for (let x = -gridSize; x <= gridSize; x++) {
|
|
291
|
+
const start = this.isoScene.toIsometric(x, -gridSize);
|
|
292
|
+
const end = this.isoScene.toIsometric(x, gridSize);
|
|
293
|
+
Painter.lines.line(start.x, start.y, end.x, end.y);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Draw horizontal grid lines (along Y axis)
|
|
297
|
+
for (let y = -gridSize; y <= gridSize; y++) {
|
|
298
|
+
const start = this.isoScene.toIsometric(-gridSize, y);
|
|
299
|
+
const end = this.isoScene.toIsometric(gridSize, y);
|
|
300
|
+
Painter.lines.line(start.x, start.y, end.x, end.y);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Draw the origin marker in red for reference
|
|
304
|
+
const origin = this.isoScene.toIsometric(0, 0);
|
|
305
|
+
Painter.shapes.fillCircle(origin.x, origin.y, CONFIG.grid.originRadius, CONFIG.grid.originColor);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* A bouncing ball that can be controlled with WASD + Space.
|
|
311
|
+
*
|
|
312
|
+
* Uses grid coordinates (x, y) for position and z for height above ground.
|
|
313
|
+
* The IsometricScene projects the position automatically; this class handles
|
|
314
|
+
* shadow rendering and visual effects relative to the projected position.
|
|
315
|
+
*/
|
|
316
|
+
class Ball extends GameObject {
|
|
317
|
+
/**
|
|
318
|
+
* @param {Game} game - Game instance
|
|
319
|
+
* @param {IsometricScene} isoScene - The parent isometric scene for projection
|
|
320
|
+
* @param {IsometricBox[]} platforms - Array of platforms to collide with
|
|
321
|
+
*/
|
|
322
|
+
constructor(game, isoScene, platforms = []) {
|
|
323
|
+
super(game);
|
|
324
|
+
this.isoScene = isoScene;
|
|
325
|
+
this.platforms = platforms;
|
|
326
|
+
|
|
327
|
+
// Grid position (x, y) and height (z)
|
|
328
|
+
this.x = 0;
|
|
329
|
+
this.y = 0;
|
|
330
|
+
this.z = 30; // Start above the starting platform
|
|
331
|
+
|
|
332
|
+
// Visual properties
|
|
333
|
+
this.baseRadius = CONFIG.ball.baseRadius;
|
|
334
|
+
this.color = CONFIG.ball.color;
|
|
335
|
+
this.strokeColor = CONFIG.ball.strokeColor;
|
|
336
|
+
|
|
337
|
+
// Physics properties
|
|
338
|
+
this.jumpPower = CONFIG.ball.jumpPower;
|
|
339
|
+
this.gravity = CONFIG.ball.gravity;
|
|
340
|
+
this.speed = CONFIG.ball.speed;
|
|
341
|
+
this.friction = CONFIG.ball.friction;
|
|
342
|
+
this.bounceFactorWall = CONFIG.ball.bounceFactorWall;
|
|
343
|
+
this.bounceFactorGround = CONFIG.ball.bounceFactorGround;
|
|
344
|
+
|
|
345
|
+
// Velocity
|
|
346
|
+
this.velocityX = 0;
|
|
347
|
+
this.velocityY = 0; // Vertical velocity (for jumping)
|
|
348
|
+
this.velocityZ = 0; // Grid Y velocity (confusingly named in original)
|
|
349
|
+
|
|
350
|
+
this.isJumping = false;
|
|
351
|
+
this.groundHeight = 0; // Current ground level (0 or platform height)
|
|
352
|
+
|
|
353
|
+
// Rotation for soccer ball effect (radians)
|
|
354
|
+
this.rotationX = 0; // Rotation around X axis (from moving in Y)
|
|
355
|
+
this.rotationY = 0; // Rotation around Y axis (from moving in X)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Set the platforms array for collision detection
|
|
360
|
+
* @param {IsometricBox[]} platforms
|
|
361
|
+
*/
|
|
362
|
+
setPlatforms(platforms) {
|
|
363
|
+
this.platforms = platforms;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Custom depth value for sorting - ensures ball renders on top of platforms
|
|
368
|
+
*/
|
|
369
|
+
get isoDepth() {
|
|
370
|
+
// Find the platform we're over (if any) and use its front corner as base
|
|
371
|
+
let baseDepth = this.x + this.y;
|
|
372
|
+
|
|
373
|
+
for (const platform of this.platforms) {
|
|
374
|
+
if (platform.containsPoint(this.x, this.y, 0)) {
|
|
375
|
+
// Use the platform's front corner as our base depth
|
|
376
|
+
const platformFront = (platform.x + platform.w) + (platform.y + platform.d);
|
|
377
|
+
if (platformFront > baseDepth) {
|
|
378
|
+
baseDepth = platformFront;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Add height plus small offset to ensure we render on top of platforms
|
|
384
|
+
return baseDepth + this.z * 0.5 + 1;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Resets the ball to the starting platform
|
|
389
|
+
*/
|
|
390
|
+
resetPosition() {
|
|
391
|
+
// Start on the center platform
|
|
392
|
+
this.x = 0;
|
|
393
|
+
this.y = 0;
|
|
394
|
+
this.z = 30; // Above the starting platform (which is at height 20)
|
|
395
|
+
this.velocityX = 0;
|
|
396
|
+
this.velocityZ = 0;
|
|
397
|
+
this.velocityY = 0;
|
|
398
|
+
this.isJumping = false;
|
|
399
|
+
this.groundHeight = 0;
|
|
400
|
+
this.rotationX = 0;
|
|
401
|
+
this.rotationY = 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Updates ball physics and handles input
|
|
406
|
+
* @param {number} dt - Delta time in seconds
|
|
407
|
+
*/
|
|
408
|
+
update(dt) {
|
|
409
|
+
// Use config values for physics
|
|
410
|
+
const acceleration = CONFIG.ball.acceleration;
|
|
411
|
+
const maxVelocity = CONFIG.ball.maxVelocity;
|
|
412
|
+
|
|
413
|
+
// Frame-rate independent friction: friction^(dt*60) normalizes to 60 FPS feel
|
|
414
|
+
const frictionFactor = Math.pow(this.friction, dt * 60);
|
|
415
|
+
this.velocityX *= frictionFactor;
|
|
416
|
+
this.velocityZ *= frictionFactor;
|
|
417
|
+
|
|
418
|
+
// Handle movement input (WASD) - can move diagonally
|
|
419
|
+
if (Keys.isDown(Keys.W)) {
|
|
420
|
+
this.velocityZ -= acceleration * dt;
|
|
421
|
+
}
|
|
422
|
+
if (Keys.isDown(Keys.S)) {
|
|
423
|
+
this.velocityZ += acceleration * dt;
|
|
424
|
+
}
|
|
425
|
+
if (Keys.isDown(Keys.A)) {
|
|
426
|
+
this.velocityX -= acceleration * dt;
|
|
427
|
+
}
|
|
428
|
+
if (Keys.isDown(Keys.D)) {
|
|
429
|
+
this.velocityX += acceleration * dt;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Clamp velocity to prevent overshooting
|
|
433
|
+
this.velocityX = Math.max(-maxVelocity, Math.min(maxVelocity, this.velocityX));
|
|
434
|
+
this.velocityZ = Math.max(-maxVelocity, Math.min(maxVelocity, this.velocityZ));
|
|
435
|
+
|
|
436
|
+
// Calculate desired new position
|
|
437
|
+
let newX = this.x + this.velocityX;
|
|
438
|
+
let newY = this.y + this.velocityZ;
|
|
439
|
+
|
|
440
|
+
// --- HORIZONTAL COLLISION DETECTION ---
|
|
441
|
+
// Simple approach: check if new position would be inside any platform we can't climb
|
|
442
|
+
|
|
443
|
+
// Ball collision radius in grid units (generous to prevent visual clipping)
|
|
444
|
+
const ballRadius = 0.62;
|
|
445
|
+
const platformBounce = 1.2; // Bouncy! >1 means it bounces back harder
|
|
446
|
+
|
|
447
|
+
// --- COLLISION RESOLUTION ---
|
|
448
|
+
// For each platform, check collision and resolve with bounce
|
|
449
|
+
|
|
450
|
+
for (const platform of this.platforms) {
|
|
451
|
+
// Skip if we're high enough to be on this platform
|
|
452
|
+
if (this.z >= platform.h) continue;
|
|
453
|
+
|
|
454
|
+
// Platform bounds expanded by ball radius
|
|
455
|
+
const pLeft = platform.x - ballRadius;
|
|
456
|
+
const pRight = platform.x + platform.w + ballRadius;
|
|
457
|
+
const pTop = platform.y - ballRadius;
|
|
458
|
+
const pBottom = platform.y + platform.d + ballRadius;
|
|
459
|
+
|
|
460
|
+
// Check if new position would be inside this platform
|
|
461
|
+
const insideX = newX > pLeft && newX < pRight;
|
|
462
|
+
const insideY = newY > pTop && newY < pBottom;
|
|
463
|
+
|
|
464
|
+
if (insideX && insideY) {
|
|
465
|
+
// Calculate overlap on each axis
|
|
466
|
+
const overlapLeft = newX - pLeft;
|
|
467
|
+
const overlapRight = pRight - newX;
|
|
468
|
+
const overlapTop = newY - pTop;
|
|
469
|
+
const overlapBottom = pBottom - newY;
|
|
470
|
+
|
|
471
|
+
// Find minimum overlap (shortest way out)
|
|
472
|
+
const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom);
|
|
473
|
+
|
|
474
|
+
// Push out and bounce in the direction of minimum overlap
|
|
475
|
+
if (minOverlap === overlapLeft) {
|
|
476
|
+
newX = pLeft;
|
|
477
|
+
this.velocityX = -Math.abs(this.velocityX) * platformBounce;
|
|
478
|
+
} else if (minOverlap === overlapRight) {
|
|
479
|
+
newX = pRight;
|
|
480
|
+
this.velocityX = Math.abs(this.velocityX) * platformBounce;
|
|
481
|
+
} else if (minOverlap === overlapTop) {
|
|
482
|
+
newY = pTop;
|
|
483
|
+
this.velocityZ = -Math.abs(this.velocityZ) * platformBounce;
|
|
484
|
+
} else if (minOverlap === overlapBottom) {
|
|
485
|
+
newY = pBottom;
|
|
486
|
+
this.velocityZ = Math.abs(this.velocityZ) * platformBounce;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Apply the resolved position
|
|
492
|
+
this.x = newX;
|
|
493
|
+
this.y = newY;
|
|
494
|
+
|
|
495
|
+
// Update ball rotation based on movement (rolling effect)
|
|
496
|
+
// Physics: rotation = distance / radius
|
|
497
|
+
// For a ball of visual radius ~0.5 grid units, rolling 1 unit = 2 full rotations
|
|
498
|
+
const visualRadius = 0.5; // Grid units for rotation calculation
|
|
499
|
+
const distanceX = this.velocityX; // Distance moved this frame
|
|
500
|
+
const distanceY = this.velocityZ;
|
|
501
|
+
|
|
502
|
+
// Rotation in radians = distance / radius
|
|
503
|
+
this.rotationY += distanceX / visualRadius; // Moving in X rotates around Y axis
|
|
504
|
+
this.rotationX -= distanceY / visualRadius; // Moving in Y rotates around X axis
|
|
505
|
+
|
|
506
|
+
// Handle jump input
|
|
507
|
+
if (Keys.isDown(Keys.SPACE) && !this.isJumping) {
|
|
508
|
+
this.velocityY = this.jumpPower;
|
|
509
|
+
this.isJumping = true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Apply gravity (frame-rate independent)
|
|
513
|
+
this.velocityY -= this.gravity * dt * 60;
|
|
514
|
+
this.z += this.velocityY;
|
|
515
|
+
|
|
516
|
+
// --- VERTICAL COLLISION DETECTION ---
|
|
517
|
+
// Find what platforms we're currently over and can land on
|
|
518
|
+
this.groundHeight = 0;
|
|
519
|
+
|
|
520
|
+
for (const platform of this.platforms) {
|
|
521
|
+
// Check if we're within the platform's X/Y bounds
|
|
522
|
+
if (platform.containsPoint(this.x, this.y, 0)) {
|
|
523
|
+
// Get this platform's surface height
|
|
524
|
+
const surfaceHeight = platform.getSurfaceHeight();
|
|
525
|
+
if (surfaceHeight > this.groundHeight) {
|
|
526
|
+
this.groundHeight = surfaceHeight;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Ground/platform collision - bounce based on impact speed
|
|
532
|
+
if (this.z < this.groundHeight) {
|
|
533
|
+
this.z = this.groundHeight;
|
|
534
|
+
|
|
535
|
+
// Calculate bounce factor based on impact velocity
|
|
536
|
+
// Faster falls = bouncier landing
|
|
537
|
+
const impactSpeed = Math.abs(this.velocityY);
|
|
538
|
+
const minBounce = 0.3;
|
|
539
|
+
const maxBounce = 0.8;
|
|
540
|
+
const bounceFactor = Math.min(maxBounce, minBounce + impactSpeed * 0.05);
|
|
541
|
+
|
|
542
|
+
this.velocityY *= -bounceFactor;
|
|
543
|
+
|
|
544
|
+
// Only stop bouncing if really slow
|
|
545
|
+
if (Math.abs(this.velocityY) < 0.3) {
|
|
546
|
+
this.velocityY = 0;
|
|
547
|
+
this.isJumping = false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Fall through floor (no platform and below ground) - reset
|
|
552
|
+
if (this.z < 0 && this.groundHeight === 0) {
|
|
553
|
+
// Check if we're over any platform at all
|
|
554
|
+
let overAnyPlatform = false;
|
|
555
|
+
for (const platform of this.platforms) {
|
|
556
|
+
if (platform.containsPoint(this.x, this.y, 0)) {
|
|
557
|
+
overAnyPlatform = true;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// If not over any platform, we fell off - reset
|
|
562
|
+
if (!overAnyPlatform && this.z < -30) {
|
|
563
|
+
this.resetPosition();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Boundary collisions with grid edges - bouncy walls!
|
|
568
|
+
const gridSize = CONFIG.gridSize;
|
|
569
|
+
const effectiveBoundary = gridSize - ballRadius;
|
|
570
|
+
const gridBounce = 1.5; // Very bouncy grid walls!
|
|
571
|
+
|
|
572
|
+
// X boundary - clamp and bounce
|
|
573
|
+
if (this.x < -effectiveBoundary) {
|
|
574
|
+
this.x = -effectiveBoundary;
|
|
575
|
+
this.velocityX = Math.abs(this.velocityX) * gridBounce;
|
|
576
|
+
} else if (this.x > effectiveBoundary) {
|
|
577
|
+
this.x = effectiveBoundary;
|
|
578
|
+
this.velocityX = -Math.abs(this.velocityX) * gridBounce;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Y boundary - clamp and bounce
|
|
582
|
+
if (this.y < -effectiveBoundary) {
|
|
583
|
+
this.y = -effectiveBoundary;
|
|
584
|
+
this.velocityZ = Math.abs(this.velocityZ) * gridBounce;
|
|
585
|
+
} else if (this.y > effectiveBoundary) {
|
|
586
|
+
this.y = effectiveBoundary;
|
|
587
|
+
this.velocityZ = -Math.abs(this.velocityZ) * gridBounce;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Renders the ball as a gradient sphere with rotating stripe.
|
|
593
|
+
*/
|
|
594
|
+
render() {
|
|
595
|
+
const ctx = Painter.ctx;
|
|
596
|
+
|
|
597
|
+
// Get projected position at ground height (for shadow)
|
|
598
|
+
const shadowPos = this.isoScene.toIsometric(this.x, this.y, this.groundHeight);
|
|
599
|
+
// Get projected position at ball height
|
|
600
|
+
const ballPos = this.isoScene.toIsometric(this.x, this.y, this.z);
|
|
601
|
+
|
|
602
|
+
// Calculate perspective scaling based on distance from center
|
|
603
|
+
const distanceFromCenter = Math.abs(this.y);
|
|
604
|
+
const maxDistance = this.isoScene.gridSize;
|
|
605
|
+
const depthScale = 0.7 + (distanceFromCenter / maxDistance) * 0.6;
|
|
606
|
+
|
|
607
|
+
// Height factor - higher objects appear slightly smaller
|
|
608
|
+
const heightAboveGround = this.z - this.groundHeight;
|
|
609
|
+
const heightFactor = Math.max(0.7, 1 - Math.abs(heightAboveGround) / 200);
|
|
610
|
+
|
|
611
|
+
// Calculate final radius with perspective
|
|
612
|
+
const radius = (this.baseRadius * this.isoScene.gridSize) / 4 * heightFactor * depthScale;
|
|
613
|
+
|
|
614
|
+
// Shadow properties - shrinks and fades as ball rises above ground
|
|
615
|
+
const shadowScale = Math.max(0.2, 1 - Math.abs(heightAboveGround) / 100);
|
|
616
|
+
const shadowAlpha = Math.max(0.1, 0.3 - Math.abs(heightAboveGround) / 300);
|
|
617
|
+
|
|
618
|
+
// Draw shadow at ground level
|
|
619
|
+
Painter.shapes.fillEllipse(
|
|
620
|
+
shadowPos.x,
|
|
621
|
+
shadowPos.y + radius / 2,
|
|
622
|
+
radius * shadowScale,
|
|
623
|
+
(radius / 2) * shadowScale,
|
|
624
|
+
0,
|
|
625
|
+
`rgba(0, 0, 0, ${shadowAlpha})`
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
const cx = ballPos.x;
|
|
629
|
+
const cy = ballPos.y;
|
|
630
|
+
|
|
631
|
+
// Draw simple blue marble with light-aware gradient
|
|
632
|
+
ctx.save();
|
|
633
|
+
|
|
634
|
+
// Light direction based on ball rotation (simulates light from top-left)
|
|
635
|
+
// As ball rotates, the lit side shifts
|
|
636
|
+
const lightOffsetX = -0.3 + Math.sin(this.rotationY) * 0.15;
|
|
637
|
+
const lightOffsetY = -0.3 + Math.sin(this.rotationX) * 0.15;
|
|
638
|
+
|
|
639
|
+
// Main gradient - shifts with rotation for lighting effect
|
|
640
|
+
const gradient = ctx.createRadialGradient(
|
|
641
|
+
cx + lightOffsetX * radius, cy + lightOffsetY * radius, 0,
|
|
642
|
+
cx, cy, radius
|
|
643
|
+
);
|
|
644
|
+
gradient.addColorStop(0, "#7ec8e3"); // Bright highlight
|
|
645
|
+
gradient.addColorStop(0.25, "#4a9fd4"); // Light blue
|
|
646
|
+
gradient.addColorStop(0.5, "#2d7ab8"); // Mid blue
|
|
647
|
+
gradient.addColorStop(0.75, "#1a5a8c"); // Darker
|
|
648
|
+
gradient.addColorStop(1, "#0d3a5c"); // Shadow edge
|
|
649
|
+
|
|
650
|
+
ctx.beginPath();
|
|
651
|
+
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
652
|
+
ctx.fillStyle = gradient;
|
|
653
|
+
ctx.fill();
|
|
654
|
+
|
|
655
|
+
// Specular highlight (small, fixed position for glass look)
|
|
656
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
|
|
657
|
+
ctx.beginPath();
|
|
658
|
+
ctx.arc(cx - radius * 0.3, cy - radius * 0.3, radius * 0.1, 0, Math.PI * 2);
|
|
659
|
+
ctx.fill();
|
|
660
|
+
|
|
661
|
+
ctx.restore();
|
|
662
|
+
|
|
663
|
+
// Subtle outline
|
|
664
|
+
ctx.beginPath();
|
|
665
|
+
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
666
|
+
ctx.strokeStyle = "rgba(0, 40, 80, 0.3)";
|
|
667
|
+
ctx.lineWidth = 1;
|
|
668
|
+
ctx.stroke();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Main game class demonstrating IsometricScene usage.
|
|
674
|
+
*
|
|
675
|
+
* Creates an isometric scene with a grid and controllable ball.
|
|
676
|
+
* Use WASD to move, Space to jump.
|
|
677
|
+
*/
|
|
678
|
+
export class IsometricGame extends Game {
|
|
679
|
+
constructor(canvas) {
|
|
680
|
+
super(canvas);
|
|
681
|
+
this.enableFluidSize();
|
|
682
|
+
this.backgroundColor = "#ecf0f1";
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Initialize the game with isometric scene and objects
|
|
687
|
+
*/
|
|
688
|
+
init() {
|
|
689
|
+
super.init();
|
|
690
|
+
|
|
691
|
+
// Create the isometric camera for view rotation
|
|
692
|
+
// Use 90° steps for proper isometric look (45° causes visual flattening)
|
|
693
|
+
this.isoCamera = new IsometricCamera({
|
|
694
|
+
rotationStep: Math.PI / 2, // 90 degrees
|
|
695
|
+
animationDuration: 0.5,
|
|
696
|
+
easing: 'easeOutCubic',
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// Create the isometric scene centered on the canvas
|
|
700
|
+
this.isoScene = new IsometricScene(this, {
|
|
701
|
+
x: this.width / 2,
|
|
702
|
+
y: this.height / 2,
|
|
703
|
+
tileWidth: CONFIG.tileWidth,
|
|
704
|
+
tileHeight: CONFIG.tileHeight,
|
|
705
|
+
gridSize: CONFIG.gridSize,
|
|
706
|
+
elevationScale: CONFIG.elevationScale,
|
|
707
|
+
depthSort: true,
|
|
708
|
+
camera: this.isoCamera,
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Create and add the grid (renders behind everything)
|
|
712
|
+
const grid = new IsometricGrid(this, this.isoScene);
|
|
713
|
+
this.isoScene.add(grid);
|
|
714
|
+
|
|
715
|
+
// Create platforms from config
|
|
716
|
+
this.platforms = [];
|
|
717
|
+
for (const p of CONFIG.platforms) {
|
|
718
|
+
const platform = new IsometricBox(this, this.isoScene, {
|
|
719
|
+
x: p.x,
|
|
720
|
+
y: p.y,
|
|
721
|
+
w: p.w,
|
|
722
|
+
d: p.d,
|
|
723
|
+
h: p.h,
|
|
724
|
+
color: p.color,
|
|
725
|
+
});
|
|
726
|
+
this.platforms.push(platform);
|
|
727
|
+
this.isoScene.add(platform);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Create and add the ball (with platform references for collision)
|
|
731
|
+
this.ball = new Ball(this, this.isoScene, this.platforms);
|
|
732
|
+
this.isoScene.add(this.ball);
|
|
733
|
+
|
|
734
|
+
// Add the scene to the pipeline
|
|
735
|
+
this.pipeline.add(this.isoScene);
|
|
736
|
+
|
|
737
|
+
// Create rotation buttons and keyboard controls
|
|
738
|
+
this.createRotationButtons();
|
|
739
|
+
this.setupKeyboardControls();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Create arrow buttons for rotating the isometric view
|
|
744
|
+
*/
|
|
745
|
+
createRotationButtons() {
|
|
746
|
+
const buttonSize = 50;
|
|
747
|
+
const margin = 20;
|
|
748
|
+
|
|
749
|
+
// Left rotation button (counter-clockwise)
|
|
750
|
+
this.rotateLeftBtn = new Button(this, {
|
|
751
|
+
x: margin + buttonSize / 2,
|
|
752
|
+
y: this.height - margin - buttonSize / 2,
|
|
753
|
+
width: buttonSize,
|
|
754
|
+
height: buttonSize,
|
|
755
|
+
text: "◀",
|
|
756
|
+
font: "24px sans-serif",
|
|
757
|
+
colorDefaultBg: "#2c3e50",
|
|
758
|
+
colorDefaultStroke: "#34495e",
|
|
759
|
+
colorDefaultText: "#ecf0f1",
|
|
760
|
+
colorHoverBg: "#34495e",
|
|
761
|
+
colorHoverStroke: "#3498db",
|
|
762
|
+
colorHoverText: "#3498db",
|
|
763
|
+
colorPressedBg: "#1a252f",
|
|
764
|
+
colorPressedStroke: "#2980b9",
|
|
765
|
+
colorPressedText: "#2980b9",
|
|
766
|
+
onClick: () => this.isoCamera.rotateLeft(),
|
|
767
|
+
});
|
|
768
|
+
this.pipeline.add(this.rotateLeftBtn);
|
|
769
|
+
|
|
770
|
+
// Right rotation button (clockwise)
|
|
771
|
+
this.rotateRightBtn = new Button(this, {
|
|
772
|
+
x: margin + buttonSize * 1.5 + 10,
|
|
773
|
+
y: this.height - margin - buttonSize / 2,
|
|
774
|
+
width: buttonSize,
|
|
775
|
+
height: buttonSize,
|
|
776
|
+
text: "▶",
|
|
777
|
+
font: "24px sans-serif",
|
|
778
|
+
colorDefaultBg: "#2c3e50",
|
|
779
|
+
colorDefaultStroke: "#34495e",
|
|
780
|
+
colorDefaultText: "#ecf0f1",
|
|
781
|
+
colorHoverBg: "#34495e",
|
|
782
|
+
colorHoverStroke: "#3498db",
|
|
783
|
+
colorHoverText: "#3498db",
|
|
784
|
+
colorPressedBg: "#1a252f",
|
|
785
|
+
colorPressedStroke: "#2980b9",
|
|
786
|
+
colorPressedText: "#2980b9",
|
|
787
|
+
onClick: () => this.isoCamera.rotateRight(),
|
|
788
|
+
});
|
|
789
|
+
this.pipeline.add(this.rotateRightBtn);
|
|
790
|
+
|
|
791
|
+
// Angle display text
|
|
792
|
+
this.angleText = new TextShape("0°", {
|
|
793
|
+
font: "bold 18px monospace",
|
|
794
|
+
color: "#2c3e50",
|
|
795
|
+
align: "left",
|
|
796
|
+
baseline: "middle",
|
|
797
|
+
});
|
|
798
|
+
this.angleTextX = margin + buttonSize * 2 + 25;
|
|
799
|
+
this.angleTextY = this.height - margin - buttonSize / 2;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Render the angle display (called after pipeline)
|
|
804
|
+
*/
|
|
805
|
+
render() {
|
|
806
|
+
super.render();
|
|
807
|
+
|
|
808
|
+
// Update and render angle text
|
|
809
|
+
if (this.angleText && this.isoCamera) {
|
|
810
|
+
const degrees = Math.round(this.isoCamera.getAngleDegrees());
|
|
811
|
+
this.angleText.text = `${degrees}°`;
|
|
812
|
+
|
|
813
|
+
Painter.save();
|
|
814
|
+
Painter.translateTo(this.angleTextX, this.angleTextY);
|
|
815
|
+
this.angleText.render();
|
|
816
|
+
Painter.restore();
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Set up keyboard event listeners for camera rotation
|
|
822
|
+
*/
|
|
823
|
+
setupKeyboardControls() {
|
|
824
|
+
// Q to rotate left
|
|
825
|
+
this.events.on(Keys.Q, () => {
|
|
826
|
+
this.isoCamera.rotateLeft();
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// E to rotate right
|
|
830
|
+
this.events.on(Keys.E, () => {
|
|
831
|
+
this.isoCamera.rotateRight();
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Handle window resize to keep scene centered and buttons positioned
|
|
837
|
+
*/
|
|
838
|
+
onResize() {
|
|
839
|
+
if (this.isoScene) {
|
|
840
|
+
this.isoScene.x = this.width / 2;
|
|
841
|
+
this.isoScene.y = this.height / 2;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Reposition buttons and angle text
|
|
845
|
+
const buttonSize = 50;
|
|
846
|
+
const margin = 20;
|
|
847
|
+
|
|
848
|
+
if (this.rotateLeftBtn) {
|
|
849
|
+
this.rotateLeftBtn.x = margin + buttonSize / 2;
|
|
850
|
+
this.rotateLeftBtn.y = this.height - margin - buttonSize / 2;
|
|
851
|
+
}
|
|
852
|
+
if (this.rotateRightBtn) {
|
|
853
|
+
this.rotateRightBtn.x = margin + buttonSize * 1.5 + 10;
|
|
854
|
+
this.rotateRightBtn.y = this.height - margin - buttonSize / 2;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Reposition angle text
|
|
858
|
+
this.angleTextX = margin + buttonSize * 2 + 25;
|
|
859
|
+
this.angleTextY = this.height - margin - buttonSize / 2;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export default IsometricGame;
|