-
-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Marc Flerackers
committed
May 23, 2024
1 parent
17f0af3
commit 68c038d
Showing
6 changed files
with
760 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
kaboom({ | ||
scale: 0.5, | ||
background: [0, 0, 0], | ||
}); | ||
|
||
loadSprite("bean", "sprites/bean.png"); | ||
loadSprite("steel", "sprites/steel.png"); | ||
|
||
const TILE_WIDTH = 64; | ||
const TILE_HEIGHT = TILE_WIDTH; | ||
|
||
function createMazeMap(width, height) { | ||
const size = width * height; | ||
function getUnvisitedNeighbours(map, index) { | ||
const n = []; | ||
const x = Math.floor(index / width); | ||
if (x > 1 && map[index - 2] === 2) n.push(index - 2); | ||
if (x < width - 2 && map[index + 2] === 2) n.push(index + 2); | ||
if (index >= 2 * width && map[index - 2 * width] === 2) { | ||
n.push(index - 2 * width); | ||
} | ||
if (index < size - 2 * width && map[index + 2 * width] === 2) { | ||
n.push(index + 2 * width); | ||
} | ||
return n; | ||
} | ||
const map = new Array(size).fill(1, 0, size); | ||
map.forEach((_, index) => { | ||
const x = Math.floor(index / width); | ||
const y = Math.floor(index % width); | ||
if ((x & 1) === 1 && (y & 1) === 1) { | ||
map[index] = 2; | ||
} | ||
}); | ||
|
||
const stack = []; | ||
const startX = Math.floor(Math.random() * (width - 1)) | 1; | ||
const startY = Math.floor(Math.random() * (height - 1)) | 1; | ||
const start = startX + startY * width; | ||
map[start] = 0; | ||
stack.push(start); | ||
while (stack.length) { | ||
const index = stack.pop(); | ||
const neighbours = getUnvisitedNeighbours(map, index); | ||
if (neighbours.length > 0) { | ||
stack.push(index); | ||
const neighbour = | ||
neighbours[Math.floor(neighbours.length * Math.random())]; | ||
const between = (index + neighbour) / 2; | ||
map[neighbour] = 0; | ||
map[between] = 0; | ||
stack.push(neighbour); | ||
} | ||
} | ||
return map; | ||
} | ||
|
||
function createMazeLevelMap(width, height, options) { | ||
const symbols = options?.symbols || {}; | ||
const map = createMazeMap(width, height); | ||
const space = symbols[" "] || " "; | ||
const fence = symbols["#"] || "#"; | ||
const detail = [ | ||
space, | ||
symbols["╸"] || "╸", // 1 | ||
symbols["╹"] || "╹", // 2 | ||
symbols["┛"] || "┛", // 3 | ||
symbols["╺"] || "╺", // 4 | ||
symbols["━"] || "━", // 5 | ||
symbols["┗"] || "┗", // 6 | ||
symbols["┻"] || "┻", // 7 | ||
symbols["╻"] || "╻", // 8 | ||
symbols["┓"] || "┓", // 9 | ||
symbols["┃"] || "┃", // a | ||
symbols["┫"] || "┫", // b | ||
symbols["┏"] || "┏", // c | ||
symbols["┳"] || "┳", // d | ||
symbols["┣"] || "┣", // e | ||
symbols["╋ "] || "╋ ", // f | ||
]; | ||
const symbolMap = options?.detailed | ||
? map.map((s, index) => { | ||
if (s === 0) return space; | ||
const x = Math.floor(index % width); | ||
const leftWall = x > 0 && map[index - 1] == 1 ? 1 : 0; | ||
const rightWall = x < width - 1 && map[index + 1] == 1 ? 4 : 0; | ||
const topWall = index >= width && map[index - width] == 1 ? 2 : 0; | ||
const bottomWall = | ||
index < height * width - width && map[index + width] == 1 | ||
? 8 | ||
: 0; | ||
return detail[leftWall | rightWall | topWall | bottomWall]; | ||
}) | ||
: map.map((s) => { | ||
return s == 1 ? fence : space; | ||
}); | ||
const levelMap = []; | ||
for (let i = 0; i < height; i++) { | ||
levelMap.push(symbolMap.slice(i * width, i * width + width).join("")); | ||
} | ||
return levelMap; | ||
} | ||
|
||
const level = addLevel( | ||
createMazeLevelMap(15, 15, {}), | ||
{ | ||
tileWidth: TILE_WIDTH, | ||
tileHeight: TILE_HEIGHT, | ||
tiles: { | ||
"#": () => [ | ||
sprite("steel"), | ||
tile({ isObstacle: true }), | ||
], | ||
}, | ||
}, | ||
); | ||
|
||
const bean = level.spawn( | ||
[ | ||
sprite("bean"), | ||
anchor("center"), | ||
pos(32, 32), | ||
tile(), | ||
agent({ speed: 640, allowDiagonals: true }), | ||
"bean", | ||
], | ||
1, | ||
1, | ||
); | ||
|
||
onClick(() => { | ||
const pos = mousePos(); | ||
bean.setTarget(vec2( | ||
Math.floor(pos.x / TILE_WIDTH) * TILE_WIDTH + TILE_WIDTH / 2, | ||
Math.floor(pos.y / TILE_HEIGHT) * TILE_HEIGHT + TILE_HEIGHT / 2, | ||
)); | ||
}); | ||
|
||
onUpdate(() => { | ||
const pts = [bean.pos]; | ||
// This is overkill, since you theoretically only need to shoot rays to grid positions | ||
for (let i = 0; i < 360; i += 1) { | ||
const hit = level.raycast(bean.pos, Vec2.fromAngle(i)); | ||
pts.push(hit.point); | ||
} | ||
pts.push(pts[1]); | ||
drawPolygon({ | ||
pts: pts, | ||
color: rgb(255, 255, 100), | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
kaboom(); | ||
|
||
add([ | ||
pos(80, 80), | ||
circle(40), | ||
color(BLUE), | ||
area(), | ||
]); | ||
|
||
add([ | ||
pos(180, 210), | ||
circle(20), | ||
color(BLUE), | ||
area(), | ||
]); | ||
|
||
add([ | ||
pos(40, 180), | ||
rect(20, 40), | ||
color(BLUE), | ||
area(), | ||
]); | ||
|
||
add([ | ||
pos(140, 130), | ||
rect(60, 50), | ||
color(BLUE), | ||
area(), | ||
]); | ||
|
||
add([ | ||
pos(180, 40), | ||
polygon([vec2(-60, 60), vec2(0, 0), vec2(60, 60)]), | ||
color(BLUE), | ||
area(), | ||
]); | ||
|
||
add([ | ||
pos(280, 130), | ||
polygon([vec2(-20, 20), vec2(0, 0), vec2(20, 20)]), | ||
color(BLUE), | ||
area(), | ||
]); | ||
|
||
onUpdate(() => { | ||
const shapes = get("shape"); | ||
shapes.forEach(s1 => { | ||
if ( | ||
shapes.some(s2 => | ||
s1 !== s2 && s1.getShape().collides(s2.getShape()) | ||
) | ||
) { | ||
s1.color = RED; | ||
} else { | ||
s1.color = BLUE; | ||
} | ||
}); | ||
}); | ||
|
||
onDraw("selected", (s) => { | ||
const bbox = s.worldArea().bbox(); | ||
drawRect({ | ||
pos: bbox.pos.sub(s.pos), | ||
width: bbox.width, | ||
height: bbox.height, | ||
outline: { | ||
color: YELLOW, | ||
width: 1, | ||
}, | ||
fill: false, | ||
}); | ||
}); | ||
|
||
onMousePress(() => { | ||
const shapes = get("area"); | ||
const pos = mousePos(); | ||
const pickList = shapes.filter((shape) => shape.hasPoint(pos)); | ||
selection = pickList[pickList.length - 1]; | ||
if (selection) { | ||
get("selected").forEach(s => s.unuse("selected")); | ||
selection.use("selected"); | ||
} | ||
}); | ||
|
||
onMouseMove((pos, delta) => { | ||
get("selected").forEach(sel => { | ||
sel.moveBy(delta); | ||
}); | ||
get("turn").forEach(laser => { | ||
const oldVec = mousePos().sub(delta).sub(laser.pos); | ||
const newVec = mousePos().sub(laser.pos); | ||
laser.angle += oldVec.angleBetween(newVec); | ||
}); | ||
}); | ||
|
||
onMouseRelease(() => { | ||
get("selected").forEach(s => s.unuse("selected")); | ||
get("turn").forEach(s => s.unuse("turn")); | ||
}); | ||
|
||
function laser() { | ||
return { | ||
draw() { | ||
drawTriangle({ | ||
p1: vec2(-16, -16), | ||
p2: vec2(16, 0), | ||
p3: vec2(-16, 16), | ||
pos: vec2(0, 0), | ||
color: this.color, | ||
}); | ||
if (this.showRing || this.is("turn")) { | ||
drawCircle({ | ||
pos: vec2(0, 0), | ||
radius: 28, | ||
outline: { | ||
color: RED, | ||
width: 4, | ||
}, | ||
fill: false, | ||
}); | ||
} | ||
pushTransform(); | ||
pushRotate(-this.angle); | ||
const MAX_TRACE_DEPTH = 3; | ||
const MAX_DISTANCE = 400; | ||
let origin = this.pos; | ||
let direction = Vec2.fromAngle(this.angle).scale(MAX_DISTANCE); | ||
let traceDepth = 0; | ||
while (traceDepth < MAX_TRACE_DEPTH) { | ||
const hit = raycast(origin, direction, ["laser"]); | ||
if (!hit) { | ||
drawLine({ | ||
p1: origin.sub(this.pos), | ||
p2: origin.add(direction).sub(this.pos), | ||
width: 1, | ||
color: this.color, | ||
}); | ||
break; | ||
} | ||
const pos = hit.point.sub(this.pos); | ||
// Draw hit point | ||
drawCircle({ | ||
pos: pos, | ||
radius: 4, | ||
color: this.color, | ||
}); | ||
// Draw hit normal | ||
drawLine({ | ||
p1: pos, | ||
p2: pos.add(hit.normal.scale(20)), | ||
width: 1, | ||
color: BLUE, | ||
}); | ||
// Draw hit distance | ||
drawLine({ | ||
p1: origin.sub(this.pos), | ||
p2: pos, | ||
width: 1, | ||
color: this.color, | ||
}); | ||
// Offset the point slightly, otherwise it might be too close to the surface | ||
// and give internal reflections | ||
origin = hit.point.add(hit.normal.scale(0.001)); | ||
// Reflect vector | ||
direction = direction.reflect(hit.normal); | ||
traceDepth++; | ||
} | ||
popTransform(); | ||
}, | ||
showRing: false, | ||
}; | ||
} | ||
|
||
const ray = add([ | ||
pos(150, 270), | ||
rotate(-45), | ||
anchor("center"), | ||
rect(64, 64), | ||
area(), | ||
laser(0), | ||
color(RED), | ||
opacity(0.0), | ||
"laser", | ||
]); | ||
|
||
get("laser").forEach(laser => { | ||
laser.onHover(() => { | ||
laser.showRing = true; | ||
}); | ||
laser.onHoverEnd(() => { | ||
laser.showRing = false; | ||
}); | ||
laser.onClick(() => { | ||
get("selected").forEach(s => s.unuse("selected")); | ||
if (laser.pos.sub(mousePos()).slen() > 28 * 28) { | ||
laser.use("turn"); | ||
} else { | ||
laser.use("selected"); | ||
} | ||
}); | ||
}); |
Oops, something went wrong.