Expand to 10 levels with 2-lane serpentine map layout
- Add levels 7-10 and move all 10 into campaign01 (campaign02 left empty as scaffolding) - Replace the old S-curve auto-layout with a 2-lane circuit: top lane runs right→left, bottom lane left→right, finish flag in the bottom-right corner. Comfortable up to ~12 levels per campaign. - Re-export node/flag sprites as tight 512x512 badges (badge IS the file) instead of centered-on-960x540 frames; update mapArt.js tables (BADGE_FILE_FRAC, BADGE_HIT_FRAC, NODE_ART_FRAMES) and asset manifest accordingly - Size the finish-flag nudge to stay on-screen when a custom `positions` layout ends at an edge (flips direction if out of bounds) - Guard against empty campaigns: skip path/flag rendering when there are no points, and bail early in _playFirst() - Shrink node pill font slightly so the longest tag fits under tightly spaced nodes - Update README.md and sprites.md to reflect the new sprite convention, lane geometry, and level count
This commit is contained in:
parent
5e49c055a2
commit
d6630dce3a
|
|
@ -34,7 +34,7 @@ src/config.js every tunable constant (physics, g-force threshold, camer
|
||||||
src/scenes/ Boot -> Preload -> Intro -> MainMenu -> LevelSelect (map) -> Play -> LevelScore -> LevelSelect / LevelFailed
|
src/scenes/ Boot -> Preload -> Intro -> MainMenu -> LevelSelect (map) -> Play -> LevelScore -> LevelSelect / LevelFailed
|
||||||
src/entities/ Bus, Kid, Terrain
|
src/entities/ Bus, Kid, Terrain
|
||||||
src/systems/ GForceMonitor, KidManager, InputController, CameraRig
|
src/systems/ GForceMonitor, KidManager, InputController, CameraRig
|
||||||
src/data/levels/ the 3 hand-authored levels
|
src/data/levels/ the hand-authored levels (10, split across the two campaigns)
|
||||||
src/util/ asset manifest, placeholder-texture generator, localStorage progress, small UI helper
|
src/util/ asset manifest, placeholder-texture generator, localStorage progress, small UI helper
|
||||||
assets/ where real art goes (see manifest below) - currently empty
|
assets/ where real art goes (see manifest below) - currently empty
|
||||||
```
|
```
|
||||||
|
|
|
||||||
64
sprites.md
64
sprites.md
|
|
@ -84,46 +84,54 @@ road ribbon snaking between level nodes, a finish flag past the last node,
|
||||||
and a banner plate behind the campaign title. There is one of each of these
|
and a banner plate behind the campaign title. There is one of each of these
|
||||||
per campaign, plus the shared node/flag/plate sprites.
|
per campaign, plus the shared node/flag/plate sprites.
|
||||||
|
|
||||||
**Important convention for node/flag/plate sprites:** they are drawn centered
|
**Important convention for these sprites:** each is displayed at a fixed
|
||||||
on a **960x540 transparent canvas** (1920x1080 at 2x) and displayed full-frame
|
display size with origin (0.5, 0.5) at its map position, so **keep the
|
||||||
at a position, so *where you draw the art on that canvas is where it appears
|
subject centered in its file** or it will show up shifted. The node badges
|
||||||
in the game*. Keep the subject centered in the frame, or it will show up
|
and the flag ship as tight 512x512 images (the badge/flag IS the file);
|
||||||
shifted.
|
`map_campaign_tag` is a full-screen 1920x1080 frame with the plate centered
|
||||||
|
near the top. Sizes below are the display sizes the game uses (design
|
||||||
|
units); the source file's own resolution only affects crispness.
|
||||||
|
|
||||||
| key | path | subject size (design units) | notes |
|
| key | path | subject size (design units) | notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `campaign01_bg` | `assets/backgrounds/campaign01_bg.png` | full frame 960x540 | Campaign 1 "Sunny Suburbs" background |
|
| `campaign01_bg` | `assets/backgrounds/campaign01_bg.png` | full frame 960x540 | Campaign 1 "Sunny Suburbs" background |
|
||||||
| `campaign02_bg` | `assets/backgrounds/campaign02_bg.png` | full frame 960x540 | Campaign 2 "Dusk Junction" background |
|
| `campaign02_bg` | `assets/backgrounds/campaign02_bg.png` | full frame 960x540 | Campaign 2 "Dusk Junction" background |
|
||||||
| `map_node` | `assets/ui/map_node.png` | ~450x450 badge centered | completed level node (number badge) |
|
| `map_node` | `assets/ui/map_node.png` | ~58x58 badge (512x512 file) | completed level node |
|
||||||
| `map_node_current` | `assets/ui/map_node_current.png` | **512x512 badge, no padding frame** (exception to the centered-frame rule) | the next level to play ("start here" node) |
|
| `map_node_current` | `assets/ui/map_node_current.png` | ~71x71 badge (512x512 file) | the next level to play ("start here" node) |
|
||||||
| `map_node_locked` | `assets/ui/map_node_locked.png` | ~450x450 badge centered | locked level node |
|
| `map_node_locked` | `assets/ui/map_node_locked.png` | ~58x58 badge (512x512 file) | locked level node |
|
||||||
| `map_finish` | `assets/ui/map_finish.png` | ~440x560 centered | flag past the last node |
|
| `map_finish` | `assets/ui/map_finish.png` | ~345x345 (512x512 file) | flag past the last node |
|
||||||
| `map_campaign_tag` | `assets/ui/map_campaign_tag.png` | ~1440x300 plate centered near top | banner behind the campaign title text (title is drawn on top at y≈82/540) |
|
| `map_campaign_tag` | `assets/ui/map_campaign_tag.png` | ~1440x300 plate centered near top | banner behind the campaign title text (title is drawn on top at y≈82/540) |
|
||||||
|
|
||||||
Per-campaign notes:
|
Per-campaign notes:
|
||||||
|
|
||||||
- **`campaign0X_bg.png`** - full 960x540 (or 2x) scene, no transparency needed.
|
- **`campaign0X_bg.png`** - full 960x540 (or 2x) scene, no transparency needed.
|
||||||
The top ~150 units are behind the title plate, so keep that area fairly
|
The top ~150 units are behind the title plate, so keep that area fairly
|
||||||
quiet. Leave the bottom 60% open road/grass for the path to sit on.
|
quiet. Node positions are auto-placed in a **2-lane serpentine** that sits
|
||||||
The node positions live in `src/data/levels/index.js` (`positions`, or
|
in the band **y ≈ 268-412** (design units) across the full map width
|
||||||
auto-layout via `src/data/levels/positioning.js`) - check where your
|
(x ≈ 90-870) - both lanes - so keep that horizontal strip fairly open and
|
||||||
nodes will land before you paint, and keep that corridor readable.
|
readable, and leave the bottom ~60 units clear for the `< Menu` / hint
|
||||||
|
chrome. The exact lane y-values live in `src/data/levels/positioning.js`
|
||||||
|
(`LANE_TOP`/`LANE_BOTTOM`) and per-campaign overrides in
|
||||||
|
`src/data/levels/index.js` (`positions`) - check where your nodes will
|
||||||
|
land before you paint.
|
||||||
- **Node states** share one silhouette (same size/placement) in three moods:
|
- **Node states** share one silhouette (same size/placement) in three moods:
|
||||||
`map_node` (cleared, e.g. blue with the level number), `map_node_current`
|
`map_node` (cleared), `map_node_current` (the one to play next - this one
|
||||||
(the one to play next, e.g. gold - this one gets a gentle idle pulse),
|
gets a gentle idle pulse), `map_node_locked` (greyed out, e.g. a padlock).
|
||||||
`map_node_locked` (greyed out, e.g. a padlock). If you want the badge to
|
All three ship as 512x512 badge images with no padding frame; their
|
||||||
be a different size than ~450 units, also adjust `BADGE_FRAC_OF_FRAME`
|
display size and hit area are driven by `NODE_ART_FRAMES` +
|
||||||
in `src/util/mapArt.js`. Note `map_node_current` currently ships as a
|
`BADGE_FILE_FRAC` per-key in `src/util/mapArt.js`, so a different frame
|
||||||
tight 512x512 image (no 960x540 padding frame); its size/hit area are
|
size there is fine as long as those tables are updated. If you want the
|
||||||
driven by `NODE_ART_FRAMES` + `BADGE_FILE_FRAC` per-key in `mapArt.js`,
|
badge to be a different display size than ~58-71 units, adjust
|
||||||
so a different frame size there is fine as long as those tables are
|
`BADGE_FRAC_OF_FRAME` / the per-state `BADGE_FILE_FRAC` entries in
|
||||||
updated.
|
`src/util/mapArt.js` (the click area follows the art, not these knobs).
|
||||||
- **`map_finish.png`** - the flag sits just past the last node, on the road,
|
- **`map_finish.png`** - the flag sits just past the last node, so a base/mound
|
||||||
so a base/mound that grounds it looks best; don't rely on the bottom of
|
that grounds it looks best. Currently ships as a tight 512x512 image (flag
|
||||||
the 960x540 canvas being the ground line. Currently ships as a tight
|
ink is ~60% of the width, full height); the scene displays it at 0.18x the
|
||||||
512x512 image (flag ink is ~60% of the width, full height); the scene
|
game screen, so keep the flag roughly centered in a square frame if you
|
||||||
displays it at 0.28x the game screen, so keep the flag roughly centered
|
re-export it. The auto-layout circuit ends at the bottom-right, so the
|
||||||
in a square frame if you re-export it.
|
flag lands in the bottom-right corner past the final node (with custom
|
||||||
|
`positions` the nudge flips to stay on-screen when the last node is at an
|
||||||
|
edge).
|
||||||
- **`map_campaign_tag.png`** - a plate/banner with empty center space; the
|
- **`map_campaign_tag.png`** - a plate/banner with empty center space; the
|
||||||
game writes `"<name> x/y cleared"` on top of it in dark ink (#1a1f29),
|
game writes `"<name> x/y cleared"` on top of it in dark ink (#1a1f29),
|
||||||
so the middle should be a light, uncluttered fill.
|
so the middle should be a light, uncluttered fill.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ import level03 from './level03.js';
|
||||||
import level04 from './level04.js';
|
import level04 from './level04.js';
|
||||||
import level05 from './level05.js';
|
import level05 from './level05.js';
|
||||||
import level06 from './level06.js';
|
import level06 from './level06.js';
|
||||||
|
import level07 from './level07.js';
|
||||||
|
import level08 from './level08.js';
|
||||||
|
import level09 from './level09.js';
|
||||||
|
import level10 from './level10.js';
|
||||||
|
|
||||||
// Campaigns group levels into themed "maps" for the level select screen.
|
// Campaigns group levels into themed "maps" for the level select screen.
|
||||||
// Each campaign gets its own background art (bgKey) and theme, so adding a
|
// Each campaign gets its own background art (bgKey) and theme, so adding a
|
||||||
|
|
@ -11,24 +15,27 @@ import level06 from './level06.js';
|
||||||
// row here. Node positions are in 960x540 design units (NOT multiplied by
|
// row here. Node positions are in 960x540 design units (NOT multiplied by
|
||||||
// WORLD_SCALE - the scene scales them), and the path is rendered as a line
|
// WORLD_SCALE - the scene scales them), and the path is rendered as a line
|
||||||
// through them in order. `positions` may be omitted to auto-place the
|
// through them in order. `positions` may be omitted to auto-place the
|
||||||
// campaign's levels along a gentle S-curve.
|
// campaign's levels: a 2-lane circuit (top lane right->left, bottom lane
|
||||||
|
// left->right, finish flag bottom-right) that comfortably fits up to ~12
|
||||||
|
// levels (see positioning.js). Give explicit `positions` only when you want
|
||||||
|
// a custom layout - the auto-layout keeps every node in the open band of
|
||||||
|
// the background art (y roughly 268-412) and clear of the title plate and
|
||||||
|
// bottom chrome.
|
||||||
export const CAMPAIGNS = [
|
export const CAMPAIGNS = [
|
||||||
{
|
{
|
||||||
id: 'campaign01',
|
id: 'campaign01',
|
||||||
name: 'Evergreen Lake',
|
name: 'Evergreen Lake',
|
||||||
bgKey: 'campaign01_bg',
|
bgKey: 'campaign01_bg',
|
||||||
levels: [level01, level02, level03],
|
levels: [level01, level02, level03, level04, level05, level06, level07, level08, level09, level10],
|
||||||
positions: [
|
|
||||||
{ x: 160, y: 392 },
|
|
||||||
{ x: 400, y: 330 },
|
|
||||||
{ x: 640, y: 380 },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'campaign02',
|
id: 'campaign02',
|
||||||
name: 'Underprivileged Community',
|
name: 'Underprivileged Community',
|
||||||
bgKey: 'campaign02_bg',
|
bgKey: 'campaign02_bg',
|
||||||
levels: [level04, level05, level06],
|
// Its old levels (level04-06) moved into campaign01 while it grows toward
|
||||||
|
// 10; add campaign02's own levels here as they're built. An empty
|
||||||
|
// campaign renders as a bare map with "0/0 cleared" - fine as scaffolding.
|
||||||
|
levels: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
// Scaffolding level - terrain is a copy of level06.
|
||||||
|
// Build out the real layout in editor.html, then replace this file.
|
||||||
|
export default {
|
||||||
|
id: "level07",
|
||||||
|
name: "Rush Hour",
|
||||||
|
description: "Kids get home before dinner!",
|
||||||
|
kidsAboard: 3,
|
||||||
|
timeLimit: 41,
|
||||||
|
startPosition: { x: 200, y: 700 },
|
||||||
|
startAngle: 0,
|
||||||
|
terrain: [
|
||||||
|
{ points: [
|
||||||
|
{ x: -400, y: 840 },
|
||||||
|
{ x: -330, y: 840 },
|
||||||
|
{ x: -260, y: 840 },
|
||||||
|
{ x: -190, y: 840 },
|
||||||
|
{ x: -120, y: 840 },
|
||||||
|
{ x: -50, y: 840 },
|
||||||
|
{ x: 20, y: 840 },
|
||||||
|
{ x: 90, y: 840 },
|
||||||
|
{ x: 160, y: 840 },
|
||||||
|
{ x: 230, y: 840 },
|
||||||
|
{ x: 300, y: 840 },
|
||||||
|
{ x: 370, y: 840 },
|
||||||
|
{ x: 440, y: 840 },
|
||||||
|
{ x: 510, y: 840 },
|
||||||
|
{ x: 580, y: 840 },
|
||||||
|
{ x: 650, y: 840 },
|
||||||
|
{ x: 720, y: 840 },
|
||||||
|
{ x: 790, y: 840 },
|
||||||
|
{ x: 860, y: 840 },
|
||||||
|
{ x: 930, y: 840 },
|
||||||
|
{ x: 1000, y: 840 },
|
||||||
|
{ x: 1070, y: 840 },
|
||||||
|
{ x: 1140, y: 840 },
|
||||||
|
{ x: 1210, y: 840 },
|
||||||
|
{ x: 1280, y: 840 },
|
||||||
|
{ x: 1350, y: 840 },
|
||||||
|
{ x: 1420, y: 840 },
|
||||||
|
{ x: 1490, y: 840 },
|
||||||
|
{ x: 1560, y: 840 },
|
||||||
|
{ x: 1630, y: 840 },
|
||||||
|
{ x: 1700, y: 840 },
|
||||||
|
{ x: 1770, y: 840 },
|
||||||
|
{ x: 1840, y: 840 },
|
||||||
|
{ x: 1910, y: 840 },
|
||||||
|
{ x: 1980, y: 840 },
|
||||||
|
{ x: 2050, y: 840 },
|
||||||
|
{ x: 2120, y: 840 },
|
||||||
|
{ x: 2190, y: 840 },
|
||||||
|
{ x: 2260, y: 840 },
|
||||||
|
{ x: 2330, y: 840 },
|
||||||
|
{ x: 2400, y: 840 },
|
||||||
|
{ x: 2470, y: 840 },
|
||||||
|
{ x: 2540, y: 840 },
|
||||||
|
{ x: 2610, y: 840 },
|
||||||
|
{ x: 2680, y: 840 },
|
||||||
|
{ x: 2750, y: 840 },
|
||||||
|
{ x: 2820, y: 840 },
|
||||||
|
{ x: 2890, y: 840 },
|
||||||
|
{ x: 2960, y: 840 },
|
||||||
|
{ x: 3030, y: 840 },
|
||||||
|
{ x: 3100, y: 840 },
|
||||||
|
{ x: 3170, y: 840 },
|
||||||
|
{ x: 3240, y: 840 },
|
||||||
|
{ x: 3310, y: 840 },
|
||||||
|
{ x: 3380, y: 840 },
|
||||||
|
{ x: 3450, y: 840 },
|
||||||
|
{ x: 3520, y: 840 },
|
||||||
|
{ x: 3590, y: 840 },
|
||||||
|
{ x: 3660, y: 840 },
|
||||||
|
{ x: 3730, y: 840 },
|
||||||
|
{ x: 3800, y: 840 },
|
||||||
|
{ x: 3870, y: 842 },
|
||||||
|
{ x: 3940, y: 846 },
|
||||||
|
{ x: 4010, y: 850 },
|
||||||
|
{ x: 4080, y: 858 },
|
||||||
|
{ x: 4150, y: 868 },
|
||||||
|
{ x: 4220, y: 878 },
|
||||||
|
{ x: 4290, y: 890 },
|
||||||
|
{ x: 4360, y: 904 },
|
||||||
|
{ x: 4430, y: 916 },
|
||||||
|
{ x: 4500, y: 930 },
|
||||||
|
{ x: 4570, y: 944 },
|
||||||
|
{ x: 4640, y: 956 },
|
||||||
|
{ x: 4710, y: 970 },
|
||||||
|
{ x: 4780, y: 982 },
|
||||||
|
{ x: 4850, y: 992 },
|
||||||
|
{ x: 4920, y: 1002 },
|
||||||
|
{ x: 4990, y: 1010 },
|
||||||
|
{ x: 5060, y: 1014 },
|
||||||
|
{ x: 5130, y: 1018 },
|
||||||
|
{ x: 5200, y: 1020 },
|
||||||
|
{ x: 5270, y: 1020 },
|
||||||
|
{ x: 5340, y: 1020 },
|
||||||
|
{ x: 5410, y: 1020 },
|
||||||
|
{ x: 5480, y: 1020 },
|
||||||
|
{ x: 5550, y: 1020 },
|
||||||
|
{ x: 5620, y: 1020 },
|
||||||
|
{ x: 5690, y: 1020 },
|
||||||
|
{ x: 5760, y: 1020 },
|
||||||
|
{ x: 5830, y: 1020 },
|
||||||
|
{ x: 5900, y: 1020 },
|
||||||
|
{ x: 5970, y: 1020 },
|
||||||
|
{ x: 6040, y: 1020 },
|
||||||
|
{ x: 6110, y: 1020 },
|
||||||
|
{ x: 6180, y: 1020 },
|
||||||
|
{ x: 6250, y: 1020 },
|
||||||
|
{ x: 6320, y: 1020 },
|
||||||
|
{ x: 6390, y: 1020 },
|
||||||
|
{ x: 6460, y: 1020 },
|
||||||
|
{ x: 6530, y: 1020 },
|
||||||
|
{ x: 6600, y: 1020 },
|
||||||
|
{ x: 6670, y: 1018 },
|
||||||
|
{ x: 6740, y: 1014 },
|
||||||
|
{ x: 6810, y: 1010 },
|
||||||
|
{ x: 6880, y: 1002 },
|
||||||
|
{ x: 6950, y: 992 },
|
||||||
|
{ x: 7020, y: 982 },
|
||||||
|
{ x: 7090, y: 970 },
|
||||||
|
{ x: 7160, y: 956 },
|
||||||
|
{ x: 7230, y: 944 },
|
||||||
|
{ x: 7300, y: 930 },
|
||||||
|
{ x: 7370, y: 916 },
|
||||||
|
{ x: 7440, y: 904 },
|
||||||
|
{ x: 7510, y: 890 },
|
||||||
|
{ x: 7580, y: 878 },
|
||||||
|
{ x: 7650, y: 868 },
|
||||||
|
{ x: 7720, y: 858 },
|
||||||
|
{ x: 7790, y: 850 },
|
||||||
|
{ x: 7860, y: 846 },
|
||||||
|
{ x: 7930, y: 842 },
|
||||||
|
{ x: 8000, y: 840 },
|
||||||
|
{ x: 8070, y: 840 },
|
||||||
|
{ x: 8140, y: 840 },
|
||||||
|
{ x: 8210, y: 840 },
|
||||||
|
{ x: 8280, y: 840 },
|
||||||
|
{ x: 8350, y: 840 },
|
||||||
|
{ x: 8420, y: 840 },
|
||||||
|
{ x: 8490, y: 840 },
|
||||||
|
{ x: 8560, y: 840 },
|
||||||
|
{ x: 8630, y: 840 },
|
||||||
|
{ x: 8700, y: 840 },
|
||||||
|
{ x: 8770, y: 840 },
|
||||||
|
{ x: 8840, y: 840 },
|
||||||
|
{ x: 8910, y: 840 },
|
||||||
|
{ x: 8980, y: 840 },
|
||||||
|
{ x: 9050, y: 840 },
|
||||||
|
{ x: 9120, y: 840 },
|
||||||
|
{ x: 9190, y: 840 },
|
||||||
|
{ x: 9260, y: 840 },
|
||||||
|
{ x: 9330, y: 840 },
|
||||||
|
{ x: 9400, y: 840 },
|
||||||
|
{ x: 9470, y: 840 },
|
||||||
|
{ x: 9540, y: 840 },
|
||||||
|
{ x: 9610, y: 840 },
|
||||||
|
{ x: 9680, y: 840 },
|
||||||
|
{ x: 9750, y: 840 },
|
||||||
|
{ x: 9820, y: 840 },
|
||||||
|
{ x: 9890, y: 840 },
|
||||||
|
{ x: 9960, y: 840 },
|
||||||
|
{ x: 10030, y: 840 },
|
||||||
|
{ x: 10100, y: 840 },
|
||||||
|
{ x: 10170, y: 840 },
|
||||||
|
{ x: 10240, y: 840 },
|
||||||
|
{ x: 10310, y: 840 },
|
||||||
|
{ x: 10380, y: 840 },
|
||||||
|
{ x: 10450, y: 840 },
|
||||||
|
{ x: 10520, y: 840 },
|
||||||
|
{ x: 10590, y: 840 },
|
||||||
|
{ x: 10660, y: 840 },
|
||||||
|
{ x: 10730, y: 840 },
|
||||||
|
{ x: 10800, y: 840 },
|
||||||
|
{ x: 10870, y: 840 },
|
||||||
|
{ x: 10940, y: 840 },
|
||||||
|
{ x: 11010, y: 840 },
|
||||||
|
{ x: 11080, y: 840 },
|
||||||
|
{ x: 11150, y: 840 },
|
||||||
|
{ x: 11220, y: 840 },
|
||||||
|
{ x: 11290, y: 840 },
|
||||||
|
{ x: 11360, y: 840 },
|
||||||
|
{ x: 11430, y: 840 },
|
||||||
|
{ x: 11500, y: 840 },
|
||||||
|
{ x: 11570, y: 840 },
|
||||||
|
{ x: 11640, y: 840 },
|
||||||
|
{ x: 11710, y: 840 },
|
||||||
|
{ x: 11780, y: 840 },
|
||||||
|
{ x: 11808, y: 840 },
|
||||||
|
{ x: 11878, y: 836 },
|
||||||
|
{ x: 11948, y: 822 },
|
||||||
|
{ x: 12018, y: 800 },
|
||||||
|
{ x: 12088, y: 768 },
|
||||||
|
{ x: 12158, y: 728 },
|
||||||
|
{ x: 12200, y: 700 },
|
||||||
|
{ x: 12270, y: 700 },
|
||||||
|
] },
|
||||||
|
{ points: [
|
||||||
|
{ x: 13704, y: 1180 },
|
||||||
|
{ x: 13774, y: 1180 },
|
||||||
|
{ x: 13844, y: 1180 },
|
||||||
|
{ x: 13914, y: 1180 },
|
||||||
|
{ x: 13984, y: 1180 },
|
||||||
|
{ x: 14054, y: 1180 },
|
||||||
|
{ x: 14080, y: 1180 },
|
||||||
|
{ x: 14150, y: 1180 },
|
||||||
|
{ x: 14220, y: 1180 },
|
||||||
|
{ x: 14290, y: 1180 },
|
||||||
|
{ x: 14360, y: 1180 },
|
||||||
|
{ x: 14430, y: 1180 },
|
||||||
|
{ x: 14500, y: 1180 },
|
||||||
|
{ x: 14570, y: 1180 },
|
||||||
|
{ x: 14640, y: 1180 },
|
||||||
|
{ x: 14710, y: 1180 },
|
||||||
|
{ x: 14780, y: 1180 },
|
||||||
|
{ x: 14850, y: 1180 },
|
||||||
|
{ x: 14920, y: 1180 },
|
||||||
|
{ x: 14990, y: 1180 },
|
||||||
|
{ x: 15060, y: 1180 },
|
||||||
|
{ x: 15130, y: 1180 },
|
||||||
|
{ x: 15200, y: 1180 },
|
||||||
|
{ x: 15270, y: 1180 },
|
||||||
|
{ x: 15340, y: 1180 },
|
||||||
|
{ x: 15410, y: 1180 },
|
||||||
|
{ x: 15480, y: 1180 },
|
||||||
|
{ x: 15550, y: 1180 },
|
||||||
|
{ x: 15620, y: 1180 },
|
||||||
|
{ x: 15690, y: 1180 },
|
||||||
|
{ x: 15760, y: 1180 },
|
||||||
|
{ x: 15830, y: 1180 },
|
||||||
|
{ x: 15900, y: 1180 },
|
||||||
|
{ x: 15970, y: 1180 },
|
||||||
|
{ x: 16040, y: 1180 },
|
||||||
|
{ x: 16110, y: 1180 },
|
||||||
|
{ x: 16180, y: 1180 },
|
||||||
|
{ x: 16250, y: 1180 },
|
||||||
|
{ x: 16320, y: 1180 },
|
||||||
|
{ x: 16390, y: 1180 },
|
||||||
|
{ x: 16460, y: 1180 },
|
||||||
|
{ x: 16530, y: 1180 },
|
||||||
|
{ x: 16600, y: 1180 },
|
||||||
|
{ x: 16670, y: 1180 },
|
||||||
|
{ x: 16740, y: 1180 },
|
||||||
|
{ x: 16810, y: 1180 },
|
||||||
|
{ x: 16880, y: 1180 },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
obstacles: [],
|
||||||
|
goal: { x: 16640, y: 940, width: 280, height: 640 },
|
||||||
|
cameraBounds: { x: -600, y: -100, width: 18280, height: 2080 },
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
// Scaffolding level - terrain is a copy of level06.
|
||||||
|
// Build out the real layout in editor.html, then replace this file.
|
||||||
|
export default {
|
||||||
|
id: "level08",
|
||||||
|
name: "Pothole Panic",
|
||||||
|
description: "Roads not built for a bus... or its cargo.",
|
||||||
|
kidsAboard: 3,
|
||||||
|
timeLimit: 41,
|
||||||
|
startPosition: { x: 200, y: 700 },
|
||||||
|
startAngle: 0,
|
||||||
|
terrain: [
|
||||||
|
{ points: [
|
||||||
|
{ x: -400, y: 840 },
|
||||||
|
{ x: -330, y: 840 },
|
||||||
|
{ x: -260, y: 840 },
|
||||||
|
{ x: -190, y: 840 },
|
||||||
|
{ x: -120, y: 840 },
|
||||||
|
{ x: -50, y: 840 },
|
||||||
|
{ x: 20, y: 840 },
|
||||||
|
{ x: 90, y: 840 },
|
||||||
|
{ x: 160, y: 840 },
|
||||||
|
{ x: 230, y: 840 },
|
||||||
|
{ x: 300, y: 840 },
|
||||||
|
{ x: 370, y: 840 },
|
||||||
|
{ x: 440, y: 840 },
|
||||||
|
{ x: 510, y: 840 },
|
||||||
|
{ x: 580, y: 840 },
|
||||||
|
{ x: 650, y: 840 },
|
||||||
|
{ x: 720, y: 840 },
|
||||||
|
{ x: 790, y: 840 },
|
||||||
|
{ x: 860, y: 840 },
|
||||||
|
{ x: 930, y: 840 },
|
||||||
|
{ x: 1000, y: 840 },
|
||||||
|
{ x: 1070, y: 840 },
|
||||||
|
{ x: 1140, y: 840 },
|
||||||
|
{ x: 1210, y: 840 },
|
||||||
|
{ x: 1280, y: 840 },
|
||||||
|
{ x: 1350, y: 840 },
|
||||||
|
{ x: 1420, y: 840 },
|
||||||
|
{ x: 1490, y: 840 },
|
||||||
|
{ x: 1560, y: 840 },
|
||||||
|
{ x: 1630, y: 840 },
|
||||||
|
{ x: 1700, y: 840 },
|
||||||
|
{ x: 1770, y: 840 },
|
||||||
|
{ x: 1840, y: 840 },
|
||||||
|
{ x: 1910, y: 840 },
|
||||||
|
{ x: 1980, y: 840 },
|
||||||
|
{ x: 2050, y: 840 },
|
||||||
|
{ x: 2120, y: 840 },
|
||||||
|
{ x: 2190, y: 840 },
|
||||||
|
{ x: 2260, y: 840 },
|
||||||
|
{ x: 2330, y: 840 },
|
||||||
|
{ x: 2400, y: 840 },
|
||||||
|
{ x: 2470, y: 840 },
|
||||||
|
{ x: 2540, y: 840 },
|
||||||
|
{ x: 2610, y: 840 },
|
||||||
|
{ x: 2680, y: 840 },
|
||||||
|
{ x: 2750, y: 840 },
|
||||||
|
{ x: 2820, y: 840 },
|
||||||
|
{ x: 2890, y: 840 },
|
||||||
|
{ x: 2960, y: 840 },
|
||||||
|
{ x: 3030, y: 840 },
|
||||||
|
{ x: 3100, y: 840 },
|
||||||
|
{ x: 3170, y: 840 },
|
||||||
|
{ x: 3240, y: 840 },
|
||||||
|
{ x: 3310, y: 840 },
|
||||||
|
{ x: 3380, y: 840 },
|
||||||
|
{ x: 3450, y: 840 },
|
||||||
|
{ x: 3520, y: 840 },
|
||||||
|
{ x: 3590, y: 840 },
|
||||||
|
{ x: 3660, y: 840 },
|
||||||
|
{ x: 3730, y: 840 },
|
||||||
|
{ x: 3800, y: 840 },
|
||||||
|
{ x: 3870, y: 842 },
|
||||||
|
{ x: 3940, y: 846 },
|
||||||
|
{ x: 4010, y: 850 },
|
||||||
|
{ x: 4080, y: 858 },
|
||||||
|
{ x: 4150, y: 868 },
|
||||||
|
{ x: 4220, y: 878 },
|
||||||
|
{ x: 4290, y: 890 },
|
||||||
|
{ x: 4360, y: 904 },
|
||||||
|
{ x: 4430, y: 916 },
|
||||||
|
{ x: 4500, y: 930 },
|
||||||
|
{ x: 4570, y: 944 },
|
||||||
|
{ x: 4640, y: 956 },
|
||||||
|
{ x: 4710, y: 970 },
|
||||||
|
{ x: 4780, y: 982 },
|
||||||
|
{ x: 4850, y: 992 },
|
||||||
|
{ x: 4920, y: 1002 },
|
||||||
|
{ x: 4990, y: 1010 },
|
||||||
|
{ x: 5060, y: 1014 },
|
||||||
|
{ x: 5130, y: 1018 },
|
||||||
|
{ x: 5200, y: 1020 },
|
||||||
|
{ x: 5270, y: 1020 },
|
||||||
|
{ x: 5340, y: 1020 },
|
||||||
|
{ x: 5410, y: 1020 },
|
||||||
|
{ x: 5480, y: 1020 },
|
||||||
|
{ x: 5550, y: 1020 },
|
||||||
|
{ x: 5620, y: 1020 },
|
||||||
|
{ x: 5690, y: 1020 },
|
||||||
|
{ x: 5760, y: 1020 },
|
||||||
|
{ x: 5830, y: 1020 },
|
||||||
|
{ x: 5900, y: 1020 },
|
||||||
|
{ x: 5970, y: 1020 },
|
||||||
|
{ x: 6040, y: 1020 },
|
||||||
|
{ x: 6110, y: 1020 },
|
||||||
|
{ x: 6180, y: 1020 },
|
||||||
|
{ x: 6250, y: 1020 },
|
||||||
|
{ x: 6320, y: 1020 },
|
||||||
|
{ x: 6390, y: 1020 },
|
||||||
|
{ x: 6460, y: 1020 },
|
||||||
|
{ x: 6530, y: 1020 },
|
||||||
|
{ x: 6600, y: 1020 },
|
||||||
|
{ x: 6670, y: 1018 },
|
||||||
|
{ x: 6740, y: 1014 },
|
||||||
|
{ x: 6810, y: 1010 },
|
||||||
|
{ x: 6880, y: 1002 },
|
||||||
|
{ x: 6950, y: 992 },
|
||||||
|
{ x: 7020, y: 982 },
|
||||||
|
{ x: 7090, y: 970 },
|
||||||
|
{ x: 7160, y: 956 },
|
||||||
|
{ x: 7230, y: 944 },
|
||||||
|
{ x: 7300, y: 930 },
|
||||||
|
{ x: 7370, y: 916 },
|
||||||
|
{ x: 7440, y: 904 },
|
||||||
|
{ x: 7510, y: 890 },
|
||||||
|
{ x: 7580, y: 878 },
|
||||||
|
{ x: 7650, y: 868 },
|
||||||
|
{ x: 7720, y: 858 },
|
||||||
|
{ x: 7790, y: 850 },
|
||||||
|
{ x: 7860, y: 846 },
|
||||||
|
{ x: 7930, y: 842 },
|
||||||
|
{ x: 8000, y: 840 },
|
||||||
|
{ x: 8070, y: 840 },
|
||||||
|
{ x: 8140, y: 840 },
|
||||||
|
{ x: 8210, y: 840 },
|
||||||
|
{ x: 8280, y: 840 },
|
||||||
|
{ x: 8350, y: 840 },
|
||||||
|
{ x: 8420, y: 840 },
|
||||||
|
{ x: 8490, y: 840 },
|
||||||
|
{ x: 8560, y: 840 },
|
||||||
|
{ x: 8630, y: 840 },
|
||||||
|
{ x: 8700, y: 840 },
|
||||||
|
{ x: 8770, y: 840 },
|
||||||
|
{ x: 8840, y: 840 },
|
||||||
|
{ x: 8910, y: 840 },
|
||||||
|
{ x: 8980, y: 840 },
|
||||||
|
{ x: 9050, y: 840 },
|
||||||
|
{ x: 9120, y: 840 },
|
||||||
|
{ x: 9190, y: 840 },
|
||||||
|
{ x: 9260, y: 840 },
|
||||||
|
{ x: 9330, y: 840 },
|
||||||
|
{ x: 9400, y: 840 },
|
||||||
|
{ x: 9470, y: 840 },
|
||||||
|
{ x: 9540, y: 840 },
|
||||||
|
{ x: 9610, y: 840 },
|
||||||
|
{ x: 9680, y: 840 },
|
||||||
|
{ x: 9750, y: 840 },
|
||||||
|
{ x: 9820, y: 840 },
|
||||||
|
{ x: 9890, y: 840 },
|
||||||
|
{ x: 9960, y: 840 },
|
||||||
|
{ x: 10030, y: 840 },
|
||||||
|
{ x: 10100, y: 840 },
|
||||||
|
{ x: 10170, y: 840 },
|
||||||
|
{ x: 10240, y: 840 },
|
||||||
|
{ x: 10310, y: 840 },
|
||||||
|
{ x: 10380, y: 840 },
|
||||||
|
{ x: 10450, y: 840 },
|
||||||
|
{ x: 10520, y: 840 },
|
||||||
|
{ x: 10590, y: 840 },
|
||||||
|
{ x: 10660, y: 840 },
|
||||||
|
{ x: 10730, y: 840 },
|
||||||
|
{ x: 10800, y: 840 },
|
||||||
|
{ x: 10870, y: 840 },
|
||||||
|
{ x: 10940, y: 840 },
|
||||||
|
{ x: 11010, y: 840 },
|
||||||
|
{ x: 11080, y: 840 },
|
||||||
|
{ x: 11150, y: 840 },
|
||||||
|
{ x: 11220, y: 840 },
|
||||||
|
{ x: 11290, y: 840 },
|
||||||
|
{ x: 11360, y: 840 },
|
||||||
|
{ x: 11430, y: 840 },
|
||||||
|
{ x: 11500, y: 840 },
|
||||||
|
{ x: 11570, y: 840 },
|
||||||
|
{ x: 11640, y: 840 },
|
||||||
|
{ x: 11710, y: 840 },
|
||||||
|
{ x: 11780, y: 840 },
|
||||||
|
{ x: 11808, y: 840 },
|
||||||
|
{ x: 11878, y: 836 },
|
||||||
|
{ x: 11948, y: 822 },
|
||||||
|
{ x: 12018, y: 800 },
|
||||||
|
{ x: 12088, y: 768 },
|
||||||
|
{ x: 12158, y: 728 },
|
||||||
|
{ x: 12200, y: 700 },
|
||||||
|
{ x: 12270, y: 700 },
|
||||||
|
] },
|
||||||
|
{ points: [
|
||||||
|
{ x: 13704, y: 1180 },
|
||||||
|
{ x: 13774, y: 1180 },
|
||||||
|
{ x: 13844, y: 1180 },
|
||||||
|
{ x: 13914, y: 1180 },
|
||||||
|
{ x: 13984, y: 1180 },
|
||||||
|
{ x: 14054, y: 1180 },
|
||||||
|
{ x: 14080, y: 1180 },
|
||||||
|
{ x: 14150, y: 1180 },
|
||||||
|
{ x: 14220, y: 1180 },
|
||||||
|
{ x: 14290, y: 1180 },
|
||||||
|
{ x: 14360, y: 1180 },
|
||||||
|
{ x: 14430, y: 1180 },
|
||||||
|
{ x: 14500, y: 1180 },
|
||||||
|
{ x: 14570, y: 1180 },
|
||||||
|
{ x: 14640, y: 1180 },
|
||||||
|
{ x: 14710, y: 1180 },
|
||||||
|
{ x: 14780, y: 1180 },
|
||||||
|
{ x: 14850, y: 1180 },
|
||||||
|
{ x: 14920, y: 1180 },
|
||||||
|
{ x: 14990, y: 1180 },
|
||||||
|
{ x: 15060, y: 1180 },
|
||||||
|
{ x: 15130, y: 1180 },
|
||||||
|
{ x: 15200, y: 1180 },
|
||||||
|
{ x: 15270, y: 1180 },
|
||||||
|
{ x: 15340, y: 1180 },
|
||||||
|
{ x: 15410, y: 1180 },
|
||||||
|
{ x: 15480, y: 1180 },
|
||||||
|
{ x: 15550, y: 1180 },
|
||||||
|
{ x: 15620, y: 1180 },
|
||||||
|
{ x: 15690, y: 1180 },
|
||||||
|
{ x: 15760, y: 1180 },
|
||||||
|
{ x: 15830, y: 1180 },
|
||||||
|
{ x: 15900, y: 1180 },
|
||||||
|
{ x: 15970, y: 1180 },
|
||||||
|
{ x: 16040, y: 1180 },
|
||||||
|
{ x: 16110, y: 1180 },
|
||||||
|
{ x: 16180, y: 1180 },
|
||||||
|
{ x: 16250, y: 1180 },
|
||||||
|
{ x: 16320, y: 1180 },
|
||||||
|
{ x: 16390, y: 1180 },
|
||||||
|
{ x: 16460, y: 1180 },
|
||||||
|
{ x: 16530, y: 1180 },
|
||||||
|
{ x: 16600, y: 1180 },
|
||||||
|
{ x: 16670, y: 1180 },
|
||||||
|
{ x: 16740, y: 1180 },
|
||||||
|
{ x: 16810, y: 1180 },
|
||||||
|
{ x: 16880, y: 1180 },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
obstacles: [],
|
||||||
|
goal: { x: 16640, y: 940, width: 280, height: 640 },
|
||||||
|
cameraBounds: { x: -600, y: -100, width: 18280, height: 2080 },
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
// Scaffolding level - terrain is a copy of level06.
|
||||||
|
// Build out the real layout in editor.html, then replace this file.
|
||||||
|
export default {
|
||||||
|
id: "level09",
|
||||||
|
name: "The Switchback",
|
||||||
|
description: "Left, right, left, right. Stay calm.",
|
||||||
|
kidsAboard: 3,
|
||||||
|
timeLimit: 41,
|
||||||
|
startPosition: { x: 200, y: 700 },
|
||||||
|
startAngle: 0,
|
||||||
|
terrain: [
|
||||||
|
{ points: [
|
||||||
|
{ x: -400, y: 840 },
|
||||||
|
{ x: -330, y: 840 },
|
||||||
|
{ x: -260, y: 840 },
|
||||||
|
{ x: -190, y: 840 },
|
||||||
|
{ x: -120, y: 840 },
|
||||||
|
{ x: -50, y: 840 },
|
||||||
|
{ x: 20, y: 840 },
|
||||||
|
{ x: 90, y: 840 },
|
||||||
|
{ x: 160, y: 840 },
|
||||||
|
{ x: 230, y: 840 },
|
||||||
|
{ x: 300, y: 840 },
|
||||||
|
{ x: 370, y: 840 },
|
||||||
|
{ x: 440, y: 840 },
|
||||||
|
{ x: 510, y: 840 },
|
||||||
|
{ x: 580, y: 840 },
|
||||||
|
{ x: 650, y: 840 },
|
||||||
|
{ x: 720, y: 840 },
|
||||||
|
{ x: 790, y: 840 },
|
||||||
|
{ x: 860, y: 840 },
|
||||||
|
{ x: 930, y: 840 },
|
||||||
|
{ x: 1000, y: 840 },
|
||||||
|
{ x: 1070, y: 840 },
|
||||||
|
{ x: 1140, y: 840 },
|
||||||
|
{ x: 1210, y: 840 },
|
||||||
|
{ x: 1280, y: 840 },
|
||||||
|
{ x: 1350, y: 840 },
|
||||||
|
{ x: 1420, y: 840 },
|
||||||
|
{ x: 1490, y: 840 },
|
||||||
|
{ x: 1560, y: 840 },
|
||||||
|
{ x: 1630, y: 840 },
|
||||||
|
{ x: 1700, y: 840 },
|
||||||
|
{ x: 1770, y: 840 },
|
||||||
|
{ x: 1840, y: 840 },
|
||||||
|
{ x: 1910, y: 840 },
|
||||||
|
{ x: 1980, y: 840 },
|
||||||
|
{ x: 2050, y: 840 },
|
||||||
|
{ x: 2120, y: 840 },
|
||||||
|
{ x: 2190, y: 840 },
|
||||||
|
{ x: 2260, y: 840 },
|
||||||
|
{ x: 2330, y: 840 },
|
||||||
|
{ x: 2400, y: 840 },
|
||||||
|
{ x: 2470, y: 840 },
|
||||||
|
{ x: 2540, y: 840 },
|
||||||
|
{ x: 2610, y: 840 },
|
||||||
|
{ x: 2680, y: 840 },
|
||||||
|
{ x: 2750, y: 840 },
|
||||||
|
{ x: 2820, y: 840 },
|
||||||
|
{ x: 2890, y: 840 },
|
||||||
|
{ x: 2960, y: 840 },
|
||||||
|
{ x: 3030, y: 840 },
|
||||||
|
{ x: 3100, y: 840 },
|
||||||
|
{ x: 3170, y: 840 },
|
||||||
|
{ x: 3240, y: 840 },
|
||||||
|
{ x: 3310, y: 840 },
|
||||||
|
{ x: 3380, y: 840 },
|
||||||
|
{ x: 3450, y: 840 },
|
||||||
|
{ x: 3520, y: 840 },
|
||||||
|
{ x: 3590, y: 840 },
|
||||||
|
{ x: 3660, y: 840 },
|
||||||
|
{ x: 3730, y: 840 },
|
||||||
|
{ x: 3800, y: 840 },
|
||||||
|
{ x: 3870, y: 842 },
|
||||||
|
{ x: 3940, y: 846 },
|
||||||
|
{ x: 4010, y: 850 },
|
||||||
|
{ x: 4080, y: 858 },
|
||||||
|
{ x: 4150, y: 868 },
|
||||||
|
{ x: 4220, y: 878 },
|
||||||
|
{ x: 4290, y: 890 },
|
||||||
|
{ x: 4360, y: 904 },
|
||||||
|
{ x: 4430, y: 916 },
|
||||||
|
{ x: 4500, y: 930 },
|
||||||
|
{ x: 4570, y: 944 },
|
||||||
|
{ x: 4640, y: 956 },
|
||||||
|
{ x: 4710, y: 970 },
|
||||||
|
{ x: 4780, y: 982 },
|
||||||
|
{ x: 4850, y: 992 },
|
||||||
|
{ x: 4920, y: 1002 },
|
||||||
|
{ x: 4990, y: 1010 },
|
||||||
|
{ x: 5060, y: 1014 },
|
||||||
|
{ x: 5130, y: 1018 },
|
||||||
|
{ x: 5200, y: 1020 },
|
||||||
|
{ x: 5270, y: 1020 },
|
||||||
|
{ x: 5340, y: 1020 },
|
||||||
|
{ x: 5410, y: 1020 },
|
||||||
|
{ x: 5480, y: 1020 },
|
||||||
|
{ x: 5550, y: 1020 },
|
||||||
|
{ x: 5620, y: 1020 },
|
||||||
|
{ x: 5690, y: 1020 },
|
||||||
|
{ x: 5760, y: 1020 },
|
||||||
|
{ x: 5830, y: 1020 },
|
||||||
|
{ x: 5900, y: 1020 },
|
||||||
|
{ x: 5970, y: 1020 },
|
||||||
|
{ x: 6040, y: 1020 },
|
||||||
|
{ x: 6110, y: 1020 },
|
||||||
|
{ x: 6180, y: 1020 },
|
||||||
|
{ x: 6250, y: 1020 },
|
||||||
|
{ x: 6320, y: 1020 },
|
||||||
|
{ x: 6390, y: 1020 },
|
||||||
|
{ x: 6460, y: 1020 },
|
||||||
|
{ x: 6530, y: 1020 },
|
||||||
|
{ x: 6600, y: 1020 },
|
||||||
|
{ x: 6670, y: 1018 },
|
||||||
|
{ x: 6740, y: 1014 },
|
||||||
|
{ x: 6810, y: 1010 },
|
||||||
|
{ x: 6880, y: 1002 },
|
||||||
|
{ x: 6950, y: 992 },
|
||||||
|
{ x: 7020, y: 982 },
|
||||||
|
{ x: 7090, y: 970 },
|
||||||
|
{ x: 7160, y: 956 },
|
||||||
|
{ x: 7230, y: 944 },
|
||||||
|
{ x: 7300, y: 930 },
|
||||||
|
{ x: 7370, y: 916 },
|
||||||
|
{ x: 7440, y: 904 },
|
||||||
|
{ x: 7510, y: 890 },
|
||||||
|
{ x: 7580, y: 878 },
|
||||||
|
{ x: 7650, y: 868 },
|
||||||
|
{ x: 7720, y: 858 },
|
||||||
|
{ x: 7790, y: 850 },
|
||||||
|
{ x: 7860, y: 846 },
|
||||||
|
{ x: 7930, y: 842 },
|
||||||
|
{ x: 8000, y: 840 },
|
||||||
|
{ x: 8070, y: 840 },
|
||||||
|
{ x: 8140, y: 840 },
|
||||||
|
{ x: 8210, y: 840 },
|
||||||
|
{ x: 8280, y: 840 },
|
||||||
|
{ x: 8350, y: 840 },
|
||||||
|
{ x: 8420, y: 840 },
|
||||||
|
{ x: 8490, y: 840 },
|
||||||
|
{ x: 8560, y: 840 },
|
||||||
|
{ x: 8630, y: 840 },
|
||||||
|
{ x: 8700, y: 840 },
|
||||||
|
{ x: 8770, y: 840 },
|
||||||
|
{ x: 8840, y: 840 },
|
||||||
|
{ x: 8910, y: 840 },
|
||||||
|
{ x: 8980, y: 840 },
|
||||||
|
{ x: 9050, y: 840 },
|
||||||
|
{ x: 9120, y: 840 },
|
||||||
|
{ x: 9190, y: 840 },
|
||||||
|
{ x: 9260, y: 840 },
|
||||||
|
{ x: 9330, y: 840 },
|
||||||
|
{ x: 9400, y: 840 },
|
||||||
|
{ x: 9470, y: 840 },
|
||||||
|
{ x: 9540, y: 840 },
|
||||||
|
{ x: 9610, y: 840 },
|
||||||
|
{ x: 9680, y: 840 },
|
||||||
|
{ x: 9750, y: 840 },
|
||||||
|
{ x: 9820, y: 840 },
|
||||||
|
{ x: 9890, y: 840 },
|
||||||
|
{ x: 9960, y: 840 },
|
||||||
|
{ x: 10030, y: 840 },
|
||||||
|
{ x: 10100, y: 840 },
|
||||||
|
{ x: 10170, y: 840 },
|
||||||
|
{ x: 10240, y: 840 },
|
||||||
|
{ x: 10310, y: 840 },
|
||||||
|
{ x: 10380, y: 840 },
|
||||||
|
{ x: 10450, y: 840 },
|
||||||
|
{ x: 10520, y: 840 },
|
||||||
|
{ x: 10590, y: 840 },
|
||||||
|
{ x: 10660, y: 840 },
|
||||||
|
{ x: 10730, y: 840 },
|
||||||
|
{ x: 10800, y: 840 },
|
||||||
|
{ x: 10870, y: 840 },
|
||||||
|
{ x: 10940, y: 840 },
|
||||||
|
{ x: 11010, y: 840 },
|
||||||
|
{ x: 11080, y: 840 },
|
||||||
|
{ x: 11150, y: 840 },
|
||||||
|
{ x: 11220, y: 840 },
|
||||||
|
{ x: 11290, y: 840 },
|
||||||
|
{ x: 11360, y: 840 },
|
||||||
|
{ x: 11430, y: 840 },
|
||||||
|
{ x: 11500, y: 840 },
|
||||||
|
{ x: 11570, y: 840 },
|
||||||
|
{ x: 11640, y: 840 },
|
||||||
|
{ x: 11710, y: 840 },
|
||||||
|
{ x: 11780, y: 840 },
|
||||||
|
{ x: 11808, y: 840 },
|
||||||
|
{ x: 11878, y: 836 },
|
||||||
|
{ x: 11948, y: 822 },
|
||||||
|
{ x: 12018, y: 800 },
|
||||||
|
{ x: 12088, y: 768 },
|
||||||
|
{ x: 12158, y: 728 },
|
||||||
|
{ x: 12200, y: 700 },
|
||||||
|
{ x: 12270, y: 700 },
|
||||||
|
] },
|
||||||
|
{ points: [
|
||||||
|
{ x: 13704, y: 1180 },
|
||||||
|
{ x: 13774, y: 1180 },
|
||||||
|
{ x: 13844, y: 1180 },
|
||||||
|
{ x: 13914, y: 1180 },
|
||||||
|
{ x: 13984, y: 1180 },
|
||||||
|
{ x: 14054, y: 1180 },
|
||||||
|
{ x: 14080, y: 1180 },
|
||||||
|
{ x: 14150, y: 1180 },
|
||||||
|
{ x: 14220, y: 1180 },
|
||||||
|
{ x: 14290, y: 1180 },
|
||||||
|
{ x: 14360, y: 1180 },
|
||||||
|
{ x: 14430, y: 1180 },
|
||||||
|
{ x: 14500, y: 1180 },
|
||||||
|
{ x: 14570, y: 1180 },
|
||||||
|
{ x: 14640, y: 1180 },
|
||||||
|
{ x: 14710, y: 1180 },
|
||||||
|
{ x: 14780, y: 1180 },
|
||||||
|
{ x: 14850, y: 1180 },
|
||||||
|
{ x: 14920, y: 1180 },
|
||||||
|
{ x: 14990, y: 1180 },
|
||||||
|
{ x: 15060, y: 1180 },
|
||||||
|
{ x: 15130, y: 1180 },
|
||||||
|
{ x: 15200, y: 1180 },
|
||||||
|
{ x: 15270, y: 1180 },
|
||||||
|
{ x: 15340, y: 1180 },
|
||||||
|
{ x: 15410, y: 1180 },
|
||||||
|
{ x: 15480, y: 1180 },
|
||||||
|
{ x: 15550, y: 1180 },
|
||||||
|
{ x: 15620, y: 1180 },
|
||||||
|
{ x: 15690, y: 1180 },
|
||||||
|
{ x: 15760, y: 1180 },
|
||||||
|
{ x: 15830, y: 1180 },
|
||||||
|
{ x: 15900, y: 1180 },
|
||||||
|
{ x: 15970, y: 1180 },
|
||||||
|
{ x: 16040, y: 1180 },
|
||||||
|
{ x: 16110, y: 1180 },
|
||||||
|
{ x: 16180, y: 1180 },
|
||||||
|
{ x: 16250, y: 1180 },
|
||||||
|
{ x: 16320, y: 1180 },
|
||||||
|
{ x: 16390, y: 1180 },
|
||||||
|
{ x: 16460, y: 1180 },
|
||||||
|
{ x: 16530, y: 1180 },
|
||||||
|
{ x: 16600, y: 1180 },
|
||||||
|
{ x: 16670, y: 1180 },
|
||||||
|
{ x: 16740, y: 1180 },
|
||||||
|
{ x: 16810, y: 1180 },
|
||||||
|
{ x: 16880, y: 1180 },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
obstacles: [],
|
||||||
|
goal: { x: 16640, y: 940, width: 280, height: 640 },
|
||||||
|
cameraBounds: { x: -600, y: -100, width: 18280, height: 2080 },
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
// Scaffolding level - terrain is a copy of level06.
|
||||||
|
// Build out the real layout in editor.html, then replace this file.
|
||||||
|
export default {
|
||||||
|
id: "level10",
|
||||||
|
name: "Lakeside Finale",
|
||||||
|
description: "One last lap of Evergreen Lake. Make it count!",
|
||||||
|
kidsAboard: 3,
|
||||||
|
timeLimit: 41,
|
||||||
|
startPosition: { x: 200, y: 700 },
|
||||||
|
startAngle: 0,
|
||||||
|
terrain: [
|
||||||
|
{ points: [
|
||||||
|
{ x: -400, y: 840 },
|
||||||
|
{ x: -330, y: 840 },
|
||||||
|
{ x: -260, y: 840 },
|
||||||
|
{ x: -190, y: 840 },
|
||||||
|
{ x: -120, y: 840 },
|
||||||
|
{ x: -50, y: 840 },
|
||||||
|
{ x: 20, y: 840 },
|
||||||
|
{ x: 90, y: 840 },
|
||||||
|
{ x: 160, y: 840 },
|
||||||
|
{ x: 230, y: 840 },
|
||||||
|
{ x: 300, y: 840 },
|
||||||
|
{ x: 370, y: 840 },
|
||||||
|
{ x: 440, y: 840 },
|
||||||
|
{ x: 510, y: 840 },
|
||||||
|
{ x: 580, y: 840 },
|
||||||
|
{ x: 650, y: 840 },
|
||||||
|
{ x: 720, y: 840 },
|
||||||
|
{ x: 790, y: 840 },
|
||||||
|
{ x: 860, y: 840 },
|
||||||
|
{ x: 930, y: 840 },
|
||||||
|
{ x: 1000, y: 840 },
|
||||||
|
{ x: 1070, y: 840 },
|
||||||
|
{ x: 1140, y: 840 },
|
||||||
|
{ x: 1210, y: 840 },
|
||||||
|
{ x: 1280, y: 840 },
|
||||||
|
{ x: 1350, y: 840 },
|
||||||
|
{ x: 1420, y: 840 },
|
||||||
|
{ x: 1490, y: 840 },
|
||||||
|
{ x: 1560, y: 840 },
|
||||||
|
{ x: 1630, y: 840 },
|
||||||
|
{ x: 1700, y: 840 },
|
||||||
|
{ x: 1770, y: 840 },
|
||||||
|
{ x: 1840, y: 840 },
|
||||||
|
{ x: 1910, y: 840 },
|
||||||
|
{ x: 1980, y: 840 },
|
||||||
|
{ x: 2050, y: 840 },
|
||||||
|
{ x: 2120, y: 840 },
|
||||||
|
{ x: 2190, y: 840 },
|
||||||
|
{ x: 2260, y: 840 },
|
||||||
|
{ x: 2330, y: 840 },
|
||||||
|
{ x: 2400, y: 840 },
|
||||||
|
{ x: 2470, y: 840 },
|
||||||
|
{ x: 2540, y: 840 },
|
||||||
|
{ x: 2610, y: 840 },
|
||||||
|
{ x: 2680, y: 840 },
|
||||||
|
{ x: 2750, y: 840 },
|
||||||
|
{ x: 2820, y: 840 },
|
||||||
|
{ x: 2890, y: 840 },
|
||||||
|
{ x: 2960, y: 840 },
|
||||||
|
{ x: 3030, y: 840 },
|
||||||
|
{ x: 3100, y: 840 },
|
||||||
|
{ x: 3170, y: 840 },
|
||||||
|
{ x: 3240, y: 840 },
|
||||||
|
{ x: 3310, y: 840 },
|
||||||
|
{ x: 3380, y: 840 },
|
||||||
|
{ x: 3450, y: 840 },
|
||||||
|
{ x: 3520, y: 840 },
|
||||||
|
{ x: 3590, y: 840 },
|
||||||
|
{ x: 3660, y: 840 },
|
||||||
|
{ x: 3730, y: 840 },
|
||||||
|
{ x: 3800, y: 840 },
|
||||||
|
{ x: 3870, y: 842 },
|
||||||
|
{ x: 3940, y: 846 },
|
||||||
|
{ x: 4010, y: 850 },
|
||||||
|
{ x: 4080, y: 858 },
|
||||||
|
{ x: 4150, y: 868 },
|
||||||
|
{ x: 4220, y: 878 },
|
||||||
|
{ x: 4290, y: 890 },
|
||||||
|
{ x: 4360, y: 904 },
|
||||||
|
{ x: 4430, y: 916 },
|
||||||
|
{ x: 4500, y: 930 },
|
||||||
|
{ x: 4570, y: 944 },
|
||||||
|
{ x: 4640, y: 956 },
|
||||||
|
{ x: 4710, y: 970 },
|
||||||
|
{ x: 4780, y: 982 },
|
||||||
|
{ x: 4850, y: 992 },
|
||||||
|
{ x: 4920, y: 1002 },
|
||||||
|
{ x: 4990, y: 1010 },
|
||||||
|
{ x: 5060, y: 1014 },
|
||||||
|
{ x: 5130, y: 1018 },
|
||||||
|
{ x: 5200, y: 1020 },
|
||||||
|
{ x: 5270, y: 1020 },
|
||||||
|
{ x: 5340, y: 1020 },
|
||||||
|
{ x: 5410, y: 1020 },
|
||||||
|
{ x: 5480, y: 1020 },
|
||||||
|
{ x: 5550, y: 1020 },
|
||||||
|
{ x: 5620, y: 1020 },
|
||||||
|
{ x: 5690, y: 1020 },
|
||||||
|
{ x: 5760, y: 1020 },
|
||||||
|
{ x: 5830, y: 1020 },
|
||||||
|
{ x: 5900, y: 1020 },
|
||||||
|
{ x: 5970, y: 1020 },
|
||||||
|
{ x: 6040, y: 1020 },
|
||||||
|
{ x: 6110, y: 1020 },
|
||||||
|
{ x: 6180, y: 1020 },
|
||||||
|
{ x: 6250, y: 1020 },
|
||||||
|
{ x: 6320, y: 1020 },
|
||||||
|
{ x: 6390, y: 1020 },
|
||||||
|
{ x: 6460, y: 1020 },
|
||||||
|
{ x: 6530, y: 1020 },
|
||||||
|
{ x: 6600, y: 1020 },
|
||||||
|
{ x: 6670, y: 1018 },
|
||||||
|
{ x: 6740, y: 1014 },
|
||||||
|
{ x: 6810, y: 1010 },
|
||||||
|
{ x: 6880, y: 1002 },
|
||||||
|
{ x: 6950, y: 992 },
|
||||||
|
{ x: 7020, y: 982 },
|
||||||
|
{ x: 7090, y: 970 },
|
||||||
|
{ x: 7160, y: 956 },
|
||||||
|
{ x: 7230, y: 944 },
|
||||||
|
{ x: 7300, y: 930 },
|
||||||
|
{ x: 7370, y: 916 },
|
||||||
|
{ x: 7440, y: 904 },
|
||||||
|
{ x: 7510, y: 890 },
|
||||||
|
{ x: 7580, y: 878 },
|
||||||
|
{ x: 7650, y: 868 },
|
||||||
|
{ x: 7720, y: 858 },
|
||||||
|
{ x: 7790, y: 850 },
|
||||||
|
{ x: 7860, y: 846 },
|
||||||
|
{ x: 7930, y: 842 },
|
||||||
|
{ x: 8000, y: 840 },
|
||||||
|
{ x: 8070, y: 840 },
|
||||||
|
{ x: 8140, y: 840 },
|
||||||
|
{ x: 8210, y: 840 },
|
||||||
|
{ x: 8280, y: 840 },
|
||||||
|
{ x: 8350, y: 840 },
|
||||||
|
{ x: 8420, y: 840 },
|
||||||
|
{ x: 8490, y: 840 },
|
||||||
|
{ x: 8560, y: 840 },
|
||||||
|
{ x: 8630, y: 840 },
|
||||||
|
{ x: 8700, y: 840 },
|
||||||
|
{ x: 8770, y: 840 },
|
||||||
|
{ x: 8840, y: 840 },
|
||||||
|
{ x: 8910, y: 840 },
|
||||||
|
{ x: 8980, y: 840 },
|
||||||
|
{ x: 9050, y: 840 },
|
||||||
|
{ x: 9120, y: 840 },
|
||||||
|
{ x: 9190, y: 840 },
|
||||||
|
{ x: 9260, y: 840 },
|
||||||
|
{ x: 9330, y: 840 },
|
||||||
|
{ x: 9400, y: 840 },
|
||||||
|
{ x: 9470, y: 840 },
|
||||||
|
{ x: 9540, y: 840 },
|
||||||
|
{ x: 9610, y: 840 },
|
||||||
|
{ x: 9680, y: 840 },
|
||||||
|
{ x: 9750, y: 840 },
|
||||||
|
{ x: 9820, y: 840 },
|
||||||
|
{ x: 9890, y: 840 },
|
||||||
|
{ x: 9960, y: 840 },
|
||||||
|
{ x: 10030, y: 840 },
|
||||||
|
{ x: 10100, y: 840 },
|
||||||
|
{ x: 10170, y: 840 },
|
||||||
|
{ x: 10240, y: 840 },
|
||||||
|
{ x: 10310, y: 840 },
|
||||||
|
{ x: 10380, y: 840 },
|
||||||
|
{ x: 10450, y: 840 },
|
||||||
|
{ x: 10520, y: 840 },
|
||||||
|
{ x: 10590, y: 840 },
|
||||||
|
{ x: 10660, y: 840 },
|
||||||
|
{ x: 10730, y: 840 },
|
||||||
|
{ x: 10800, y: 840 },
|
||||||
|
{ x: 10870, y: 840 },
|
||||||
|
{ x: 10940, y: 840 },
|
||||||
|
{ x: 11010, y: 840 },
|
||||||
|
{ x: 11080, y: 840 },
|
||||||
|
{ x: 11150, y: 840 },
|
||||||
|
{ x: 11220, y: 840 },
|
||||||
|
{ x: 11290, y: 840 },
|
||||||
|
{ x: 11360, y: 840 },
|
||||||
|
{ x: 11430, y: 840 },
|
||||||
|
{ x: 11500, y: 840 },
|
||||||
|
{ x: 11570, y: 840 },
|
||||||
|
{ x: 11640, y: 840 },
|
||||||
|
{ x: 11710, y: 840 },
|
||||||
|
{ x: 11780, y: 840 },
|
||||||
|
{ x: 11808, y: 840 },
|
||||||
|
{ x: 11878, y: 836 },
|
||||||
|
{ x: 11948, y: 822 },
|
||||||
|
{ x: 12018, y: 800 },
|
||||||
|
{ x: 12088, y: 768 },
|
||||||
|
{ x: 12158, y: 728 },
|
||||||
|
{ x: 12200, y: 700 },
|
||||||
|
{ x: 12270, y: 700 },
|
||||||
|
] },
|
||||||
|
{ points: [
|
||||||
|
{ x: 13704, y: 1180 },
|
||||||
|
{ x: 13774, y: 1180 },
|
||||||
|
{ x: 13844, y: 1180 },
|
||||||
|
{ x: 13914, y: 1180 },
|
||||||
|
{ x: 13984, y: 1180 },
|
||||||
|
{ x: 14054, y: 1180 },
|
||||||
|
{ x: 14080, y: 1180 },
|
||||||
|
{ x: 14150, y: 1180 },
|
||||||
|
{ x: 14220, y: 1180 },
|
||||||
|
{ x: 14290, y: 1180 },
|
||||||
|
{ x: 14360, y: 1180 },
|
||||||
|
{ x: 14430, y: 1180 },
|
||||||
|
{ x: 14500, y: 1180 },
|
||||||
|
{ x: 14570, y: 1180 },
|
||||||
|
{ x: 14640, y: 1180 },
|
||||||
|
{ x: 14710, y: 1180 },
|
||||||
|
{ x: 14780, y: 1180 },
|
||||||
|
{ x: 14850, y: 1180 },
|
||||||
|
{ x: 14920, y: 1180 },
|
||||||
|
{ x: 14990, y: 1180 },
|
||||||
|
{ x: 15060, y: 1180 },
|
||||||
|
{ x: 15130, y: 1180 },
|
||||||
|
{ x: 15200, y: 1180 },
|
||||||
|
{ x: 15270, y: 1180 },
|
||||||
|
{ x: 15340, y: 1180 },
|
||||||
|
{ x: 15410, y: 1180 },
|
||||||
|
{ x: 15480, y: 1180 },
|
||||||
|
{ x: 15550, y: 1180 },
|
||||||
|
{ x: 15620, y: 1180 },
|
||||||
|
{ x: 15690, y: 1180 },
|
||||||
|
{ x: 15760, y: 1180 },
|
||||||
|
{ x: 15830, y: 1180 },
|
||||||
|
{ x: 15900, y: 1180 },
|
||||||
|
{ x: 15970, y: 1180 },
|
||||||
|
{ x: 16040, y: 1180 },
|
||||||
|
{ x: 16110, y: 1180 },
|
||||||
|
{ x: 16180, y: 1180 },
|
||||||
|
{ x: 16250, y: 1180 },
|
||||||
|
{ x: 16320, y: 1180 },
|
||||||
|
{ x: 16390, y: 1180 },
|
||||||
|
{ x: 16460, y: 1180 },
|
||||||
|
{ x: 16530, y: 1180 },
|
||||||
|
{ x: 16600, y: 1180 },
|
||||||
|
{ x: 16670, y: 1180 },
|
||||||
|
{ x: 16740, y: 1180 },
|
||||||
|
{ x: 16810, y: 1180 },
|
||||||
|
{ x: 16880, y: 1180 },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
obstacles: [],
|
||||||
|
goal: { x: 16640, y: 940, width: 280, height: 640 },
|
||||||
|
cameraBounds: { x: -600, y: -100, width: 18280, height: 2080 },
|
||||||
|
};
|
||||||
|
|
@ -1,25 +1,60 @@
|
||||||
// Layout math for campaign map node positions. All coordinates are in 960x540
|
// Layout math for campaign map node positions. All coordinates are in 960x540
|
||||||
// design units (the scene multiplies by WORLD_SCALE when drawing).
|
// design units (the scene multiplies by WORLD_SCALE when drawing).
|
||||||
|
//
|
||||||
|
// Node badges render at ~58-71 design units in diameter (see nodeDisplaySize
|
||||||
|
// in util/mapArt.js). The free band of the map (below the title plate, whose
|
||||||
|
// ink ends at y≈158, and above the bottom chrome at y≈490) is ~330 units
|
||||||
|
// tall, which leaves room for two lanes of nodes with the ribbon's loop
|
||||||
|
// between them:
|
||||||
|
//
|
||||||
|
// * lane 1 (top, y=268): runs RIGHT -> LEFT, ending short of the left edge
|
||||||
|
// * lane 2 (bottom, y=412): runs LEFT -> RIGHT, ending short of the right
|
||||||
|
// edge so the finish flag fits in the bottom-right corner past it
|
||||||
|
//
|
||||||
|
// ...which reads as one continuous road: start top-right, across the top,
|
||||||
|
// down the left side, across the bottom, flag at the bottom-right. Both
|
||||||
|
// lanes use ~660 of the 780 usable width; the 120-unit ends are what make
|
||||||
|
// the left connector and the right flag fit without touching a node.
|
||||||
|
// Comfortable for up to 12 levels (13 starts getting tight - give explicit
|
||||||
|
// `positions` for exotic counts).
|
||||||
|
|
||||||
const MAP_MARGIN_X = 110;
|
const MAP_WIDTH = 960;
|
||||||
const MAP_TOP = 250;
|
const LANE_TOP = 268;
|
||||||
const MAP_BOTTOM = 470;
|
const LANE_BOTTOM = 412;
|
||||||
|
// Usable span of each lane (keeps a 120-unit end zone free, see above).
|
||||||
|
const LANE_SPAN = 660;
|
||||||
|
// Where each lane starts. Lane 1 starts at the top-right, lane 2 at the
|
||||||
|
// bottom-left.
|
||||||
|
const LANE1_X0 = 870; // lane 1 runs from here LEFTWARDS
|
||||||
|
const LANE2_X0 = 90; // lane 2 runs from here RIGHTWARDS
|
||||||
|
|
||||||
// Auto-places a campaign's levels along a gentle S-curve, left to right.
|
// Auto-places a campaign's levels along the 2-lane circuit. Handles any
|
||||||
// Used when a campaign in index.js omits explicit `positions`.
|
// count >= 1. For a bespoke look, give explicit `positions` in index.js -
|
||||||
|
// those are used verbatim.
|
||||||
export function autoLayoutPoints(count) {
|
export function autoLayoutPoints(count) {
|
||||||
if (count <= 0) return [];
|
if (count <= 0) return [];
|
||||||
if (count === 1) return [{ x: 480, y: 360 }];
|
if (count === 1) return [{ x: 480, y: Math.round((LANE_TOP + LANE_BOTTOM) / 2) }];
|
||||||
const usableWidth = 960 - MAP_MARGIN_X * 2;
|
// Two levels: one per lane, offset toward each other so the connector is
|
||||||
const midY = (MAP_TOP + MAP_BOTTOM) / 2;
|
// a diagonal (not a vertical pair, which would be closer than the badges
|
||||||
const amplitude = (MAP_BOTTOM - MAP_TOP) / 2 - 20;
|
// are wide).
|
||||||
|
if (count === 2) {
|
||||||
|
return [
|
||||||
|
{ x: (LANE1_X0 + LANE2_X0) / 2 - 60, y: LANE_TOP },
|
||||||
|
{ x: (LANE1_X0 + LANE2_X0) / 2 + 60, y: LANE_BOTTOM },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const firstRow = Math.ceil(count / 2);
|
||||||
|
const secondRow = count - firstRow;
|
||||||
const points = [];
|
const points = [];
|
||||||
for (let i = 0; i < count; i++) {
|
// Lane 1: right -> left.
|
||||||
const t = i / (count - 1);
|
for (let k = 0; k < firstRow; k++) {
|
||||||
// ease x slightly so endpoints breathe at the map edges
|
const t = firstRow === 1 ? 0 : k / (firstRow - 1);
|
||||||
const x = MAP_MARGIN_X + usableWidth * t;
|
points.push({ x: Math.round(LANE1_X0 - LANE_SPAN * t), y: LANE_TOP });
|
||||||
const y = midY + Math.sin(t * Math.PI * 1.5) * amplitude;
|
}
|
||||||
points.push({ x: Math.round(x), y: Math.round(y) });
|
// Lane 2: left -> right.
|
||||||
|
for (let k = 0; k < secondRow; k++) {
|
||||||
|
const t = secondRow === 1 ? 0 : k / (secondRow - 1);
|
||||||
|
points.push({ x: Math.round(LANE2_X0 + LANE_SPAN * t), y: LANE_BOTTOM });
|
||||||
}
|
}
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { CAMPAIGNS } from '../data/levels/index.js';
|
||||||
import { autoLayoutPoints, sampleSpline } from '../data/levels/positioning.js';
|
import { autoLayoutPoints, sampleSpline } from '../data/levels/positioning.js';
|
||||||
import { isLevelComplete, getLevelResult } from '../util/progress.js';
|
import { isLevelComplete, getLevelResult } from '../util/progress.js';
|
||||||
import { playMenuMusic } from '../util/music.js';
|
import { playMenuMusic } from '../util/music.js';
|
||||||
import { nodeDisplaySize, scalePos, BADGE_FILE_FRAC, BADGE_HIT_RADIUS_MULT } from '../util/mapArt.js';
|
import { nodeDisplaySize, scalePos, BADGE_HIT_FRAC, BADGE_HIT_RADIUS_MULT } from '../util/mapArt.js';
|
||||||
|
|
||||||
const NODE_HOVER = 1.12;
|
const NODE_HOVER = 1.12;
|
||||||
const NODE_IDLE_PULSE = 1.05;
|
const NODE_IDLE_PULSE = 1.05;
|
||||||
|
|
@ -24,10 +24,13 @@ const TITLE_STYLE = {
|
||||||
|
|
||||||
const PILL_STYLE = {
|
const PILL_STYLE = {
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: `${14 * WORLD_SCALE}px`,
|
// 13 (not 14) so the longest tag - "10/10 kids saved" on a cleared node -
|
||||||
|
// fits comfortably under ~100-unit-spaced nodes once a campaign grows past
|
||||||
|
// a handful of levels.
|
||||||
|
fontSize: `${13 * WORLD_SCALE}px`,
|
||||||
color: '#1a1f29',
|
color: '#1a1f29',
|
||||||
backgroundColor: '#ffffffaa',
|
backgroundColor: '#ffffffaa',
|
||||||
padding: { x: 10 * WORLD_SCALE, y: 5 * WORLD_SCALE },
|
padding: { x: 9 * WORLD_SCALE, y: 5 * WORLD_SCALE },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class LevelSelectScene extends Phaser.Scene {
|
export default class LevelSelectScene extends Phaser.Scene {
|
||||||
|
|
@ -109,26 +112,42 @@ export default class LevelSelectScene extends Phaser.Scene {
|
||||||
const bg = this.add.image(0, 0, campaign.bgKey).setOrigin(0, 0).setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setDepth(DEPTH.bg);
|
const bg = this.add.image(0, 0, campaign.bgKey).setOrigin(0, 0).setDisplaySize(GAME_WIDTH, GAME_HEIGHT).setDepth(DEPTH.bg);
|
||||||
this._groups.bg.push(bg);
|
this._groups.bg.push(bg);
|
||||||
|
|
||||||
|
if (points.length >= 2) {
|
||||||
const spline = sampleSpline(points, 18);
|
const spline = sampleSpline(points, 18);
|
||||||
const path = this.add.graphics().setDepth(DEPTH.path);
|
const path = this.add.graphics().setDepth(DEPTH.path);
|
||||||
this._traceRibbon(path, spline, 84 * WORLD_SCALE, 0x1a1f29, 1);
|
this._traceRibbon(path, spline, 84 * WORLD_SCALE, 0x1a1f29, 1);
|
||||||
this._traceRibbon(path, spline, 62 * WORLD_SCALE, 0xf4f0e6, 1);
|
this._traceRibbon(path, spline, 62 * WORLD_SCALE, 0xf4f0e6, 1);
|
||||||
this._traceDashes(path, spline);
|
this._traceDashes(path, spline);
|
||||||
this._groups.path.push(path);
|
this._groups.path.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
// Finish flag just past the last node, nudged along the path direction.
|
// Finish flag just past the last node, nudged along the path direction.
|
||||||
|
// (The auto-layout circuit ends at the bottom-right, so this nudge lands
|
||||||
|
// in the corner; if an explicit `positions` layout ends at an edge and the
|
||||||
|
// nudge would push the flag off-screen, flip it so it stays on.)
|
||||||
|
if (points.length) {
|
||||||
const last = points[points.length - 1];
|
const last = points[points.length - 1];
|
||||||
const prev = points[Math.max(0, points.length - 2)] || { x: last.x - 100, y: last.y };
|
const prev = points[Math.max(0, points.length - 2)] || { x: last.x - 100, y: last.y };
|
||||||
const ang = Math.atan2(last.y - prev.y, last.x - prev.x);
|
const ang = Math.atan2(last.y - prev.y, last.x - prev.x);
|
||||||
|
const nudge = 95 * WORLD_SCALE;
|
||||||
|
let fx = last.x + Math.cos(ang) * nudge;
|
||||||
|
let fy = last.y + Math.sin(ang) * nudge;
|
||||||
|
const inBounds = (x, y) => x > 60 * WORLD_SCALE && x < GAME_WIDTH - 60 * WORLD_SCALE && y > 160 * WORLD_SCALE && y < GAME_HEIGHT - 60 * WORLD_SCALE;
|
||||||
|
if (!inBounds(fx, fy)) {
|
||||||
|
fx = last.x - Math.cos(ang) * nudge;
|
||||||
|
fy = last.y - Math.sin(ang) * nudge;
|
||||||
|
}
|
||||||
const flag = this.add
|
const flag = this.add
|
||||||
.image(last.x + Math.cos(ang) * 95 * WORLD_SCALE, last.y + Math.sin(ang) * 95 * WORLD_SCALE, 'map_finish')
|
.image(fx, fy, 'map_finish')
|
||||||
.setOrigin(0.5)
|
.setOrigin(0.5)
|
||||||
.setDisplaySize(GAME_WIDTH * 0.18, GAME_HEIGHT * 0.18)
|
.setDisplaySize(GAME_WIDTH * 0.18, GAME_HEIGHT * 0.18)
|
||||||
.setDepth(DEPTH.flag);
|
.setDepth(DEPTH.flag);
|
||||||
this._groups.path.push(flag);
|
this._groups.path.push(flag);
|
||||||
|
}
|
||||||
|
|
||||||
// Nodes + labels. Per-key display sizes: map_node_current ships as a tight
|
// Nodes + labels. All node textures are 512x512 badges; per-key display
|
||||||
// 512x512 badge while the others are 960x540 frames (see mapArt.js).
|
// sizes come from mapArt.js (map_node_current displays larger than the
|
||||||
|
// others).
|
||||||
levels.forEach((level, i) => {
|
levels.forEach((level, i) => {
|
||||||
const p = points[i];
|
const p = points[i];
|
||||||
const state = campaignUnlocked && (completed[i] || i === current) ? (completed[i] ? 'done' : 'current') : 'locked';
|
const state = campaignUnlocked && (completed[i] || i === current) ? (completed[i] ? 'done' : 'current') : 'locked';
|
||||||
|
|
@ -136,21 +155,20 @@ export default class LevelSelectScene extends Phaser.Scene {
|
||||||
const { width: nw, height: nh } = nodeDisplaySize(key);
|
const { width: nw, height: nh } = nodeDisplaySize(key);
|
||||||
const node = this.add.image(p.x, p.y, key).setOrigin(0.5).setDisplaySize(nw, nh).setDepth(DEPTH.node);
|
const node = this.add.image(p.x, p.y, key).setOrigin(0.5).setDisplaySize(nw, nh).setDepth(DEPTH.node);
|
||||||
|
|
||||||
// The badge is a small centered circle inside a big mostly-transparent
|
// The badge fills its texture file, so the interactive area is a
|
||||||
// texture frame, so the interactive area must be a circle around the
|
// circle at the badge's full diameter (BADGE_HIT_FRAC = 1.0, + the
|
||||||
// badge itself - otherwise the whole overlapping frames would be
|
// BADGE_HIT_RADIUS_MULT allowance), not the whole texture frame.
|
||||||
// clickable.
|
|
||||||
//
|
//
|
||||||
// Phaser hit-tests in LOCAL space: the pointer is inverse-transformed by
|
// Phaser hit-tests in LOCAL space: the pointer is inverse-transformed by
|
||||||
// the object's position/scale (InputPlugin.checkGameObjects), then
|
// the object's position/scale (InputPlugin.checkGameObjects), then
|
||||||
// displayOrigin is added (InputManager.pointWithinInteractiveObject).
|
// displayOrigin is added (InputManager.pointWithinInteractiveObject).
|
||||||
// Both steps are expressed in UNSCALED source-frame pixels, so the badge
|
// Both steps are expressed in UNSCALED source-frame pixels, so the badge
|
||||||
// center (the node's world position) lands at (displayOriginX,
|
// center (the node's world position) lands at (displayOriginX,
|
||||||
// displayOriginY) - the frame's center for these origin-0.5 textures -
|
// displayOriginY) - the center of these origin-0.5 textures - and radii
|
||||||
// and radii must be given in frame pixels too (world radius = radius *
|
// must be given in frame pixels too (world radius = radius * scale).
|
||||||
// scale). The hover/entrance tweens only change the scale, so the hit
|
// The hover/entrance tweens only change the scale, so the hit circle
|
||||||
// circle stays glued to the visible badge at all times.
|
// stays glued to the visible badge at all times.
|
||||||
const hitRadius = ((node.width * BADGE_FILE_FRAC[key]) / 2) * BADGE_HIT_RADIUS_MULT;
|
const hitRadius = ((node.width * BADGE_HIT_FRAC[key]) / 2) * BADGE_HIT_RADIUS_MULT;
|
||||||
const hitArea = new Phaser.Geom.Circle(node.displayOriginX, node.displayOriginY, hitRadius);
|
const hitArea = new Phaser.Geom.Circle(node.displayOriginX, node.displayOriginY, hitRadius);
|
||||||
const hitTest = (shape, x, y) => (x - shape.x) * (x - shape.x) + (y - shape.y) * (y - shape.y) <= shape.radius * shape.radius;
|
const hitTest = (shape, x, y) => (x - shape.x) * (x - shape.x) + (y - shape.y) * (y - shape.y) <= shape.radius * shape.radius;
|
||||||
|
|
||||||
|
|
@ -253,6 +271,8 @@ export default class LevelSelectScene extends Phaser.Scene {
|
||||||
this.campaignIndex = next;
|
this.campaignIndex = next;
|
||||||
|
|
||||||
const all = [...this._groups.bg, ...this._groups.path, ...this._groups.nodes, ...this._groups.tags];
|
const all = [...this._groups.bg, ...this._groups.path, ...this._groups.nodes, ...this._groups.tags];
|
||||||
|
// `all` is never empty (the background is always built), so onComplete is
|
||||||
|
// guaranteed to fire - empty campaigns transition fine through this path.
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: all,
|
targets: all,
|
||||||
alpha: 0,
|
alpha: 0,
|
||||||
|
|
@ -268,6 +288,7 @@ export default class LevelSelectScene extends Phaser.Scene {
|
||||||
|
|
||||||
_playFirst() {
|
_playFirst() {
|
||||||
const campaign = CAMPAIGNS[this.campaignIndex];
|
const campaign = CAMPAIGNS[this.campaignIndex];
|
||||||
|
if (!campaign.levels.length) return; // campaign with no levels yet - nothing to play
|
||||||
// Mirror the map's unlock rule: the campaign must be open (previous
|
// Mirror the map's unlock rule: the campaign must be open (previous
|
||||||
// campaign cleared) before any of its levels can be played.
|
// campaign cleared) before any of its levels can be played.
|
||||||
const campaignUnlocked = this.campaignIndex === 0 || CAMPAIGNS[this.campaignIndex - 1].levels.every((l) => isLevelComplete(l.id));
|
const campaignUnlocked = this.campaignIndex === 0 || CAMPAIGNS[this.campaignIndex - 1].levels.every((l) => isLevelComplete(l.id));
|
||||||
|
|
|
||||||
|
|
@ -24,18 +24,14 @@ export const ASSET_MANIFEST = [
|
||||||
{ key: 'logo', path: 'assets/backgrounds/logo.png', width: 320 * WORLD_SCALE, height: 173 * WORLD_SCALE, kind: 'rect', color: 0xe040a0 },
|
{ key: 'logo', path: 'assets/backgrounds/logo.png', width: 320 * WORLD_SCALE, height: 173 * WORLD_SCALE, kind: 'rect', color: 0xe040a0 },
|
||||||
// Shared full-bleed background for MainMenuScene.
|
// Shared full-bleed background for MainMenuScene.
|
||||||
{ key: 'bg_menu', path: 'assets/backgrounds/bg_menu.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x8fc7e8 },
|
{ key: 'bg_menu', path: 'assets/backgrounds/bg_menu.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x8fc7e8 },
|
||||||
// LevelSelect map art. The node/path images are 960x540 canvases with the
|
// LevelSelect map art. The node and flag sprites are tight 512x512 badges
|
||||||
// sprite centered (see makeMap* in the placeholder generator) so the code
|
// (the badge IS the file) displayed at the sizes computed by nodeDisplaySize()
|
||||||
// can just display them full-frame at a node's (x,y) - real art should do
|
// in mapArt.js; the width/height below only feed the placeholder fallback.
|
||||||
// the same, or use a transparent canvas with the art centered.
|
|
||||||
{ key: 'campaign01_bg', path: 'assets/backgrounds/campaign01_bg.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x8fc7e8 },
|
{ key: 'campaign01_bg', path: 'assets/backgrounds/campaign01_bg.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x8fc7e8 },
|
||||||
{ key: 'campaign02_bg', path: 'assets/backgrounds/campaign02_bg.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x3a346e },
|
{ key: 'campaign02_bg', path: 'assets/backgrounds/campaign02_bg.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x3a346e },
|
||||||
// 960x540 canvas, badge centered (see the LEVEL SELECT section of sprites.md).
|
{ key: 'map_node', path: 'assets/ui/map_node.png', width: 512, height: 512, kind: 'circle', color: 0x2255aa },
|
||||||
{ key: 'map_node', path: 'assets/ui/map_node.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'circle', color: 0x2255aa },
|
|
||||||
// 512x512 badge art (NOT the 960x540 centered-frame convention of the other
|
|
||||||
// node sprites) - see NODE_ART_FRAMES + nodeDisplaySize() in mapArt.js.
|
|
||||||
{ key: 'map_node_current', path: 'assets/ui/map_node_current.png', width: 512, height: 512, kind: 'circle', color: 0xdea834 },
|
{ key: 'map_node_current', path: 'assets/ui/map_node_current.png', width: 512, height: 512, kind: 'circle', color: 0xdea834 },
|
||||||
{ key: 'map_node_locked', path: 'assets/ui/map_node_locked.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'circle', color: 0x606876 },
|
{ key: 'map_node_locked', path: 'assets/ui/map_node_locked.png', width: 512, height: 512, kind: 'circle', color: 0x606876 },
|
||||||
{ key: 'map_finish', path: 'assets/ui/map_finish.png', width: 512, height: 512, kind: 'rect', color: 0xde5046 },
|
{ key: 'map_finish', path: 'assets/ui/map_finish.png', width: 512, height: 512, kind: 'rect', color: 0xde5046 },
|
||||||
{ key: 'map_campaign_tag', path: 'assets/ui/map_campaign_tag.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0xf4f0e6 },
|
{ key: 'map_campaign_tag', path: 'assets/ui/map_campaign_tag.png', width: 960 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0xf4f0e6 },
|
||||||
{ key: 'favicon', path: 'assets/favicon.png', width: 32, height: 32, kind: 'rect', color: 0x2255aa },
|
{ key: 'favicon', path: 'assets/favicon.png', width: 32, height: 32, kind: 'rect', color: 0x2255aa },
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,50 @@
|
||||||
// Shared map geometry for the LevelSelect map nodes + path ribbon.
|
// Shared map geometry for the LevelSelect map nodes + path ribbon.
|
||||||
//
|
//
|
||||||
// Most map UI art is generated on 960x540 canvases with the sprite centered
|
// The node textures (and map_finish) are tight 512x512 badge images - the
|
||||||
// (documented in sprites.md). The node textures follow that convention EXCEPT
|
// badge IS the file, no padding frame. Their display size and hit area are
|
||||||
// map_node_current, which ships as a 512x512 badge image with no padding frame
|
// derived from the tables below, so the code stays correct if a file is
|
||||||
// (see NODE_ART_FRAMES below).
|
// ever re-exported at a different size (NODE_ART_FRAMES is the place to
|
||||||
|
// change then).
|
||||||
import { GAME_WIDTH, GAME_HEIGHT, WORLD_SCALE } from '../config.js';
|
import { GAME_WIDTH, GAME_HEIGHT, WORLD_SCALE } from '../config.js';
|
||||||
|
|
||||||
// How much of a game screen wide each node's texture FRAME is displayed at.
|
// Overall size knob: node badge display size = GAME_WIDTH * BADGE_FRAC_OF_FRAME
|
||||||
// The frame is mostly transparent padding around the badge, so this is NOT
|
// * BADGE_FILE_FRAC[key]. With the current table that's ~58 design units for
|
||||||
// the badge-to-frame ratio (see BADGE_FILE_FRAC) - it only sets display size.
|
// the regular badges (map_node/locked) and ~71 for map_node_current
|
||||||
|
// (~116/~142 screen pixels at WORLD_SCALE = 2) - the same on-map scale the
|
||||||
|
// nodes had before the art was re-exported as tight badges. Raise this (or
|
||||||
|
// the per-state fractions below) to make every node badge bigger on the map.
|
||||||
export const BADGE_FRAC_OF_FRAME = 0.47;
|
export const BADGE_FRAC_OF_FRAME = 0.47;
|
||||||
|
|
||||||
// Fraction of the texture file's width spanned by the badge's ink, PER NODE
|
// Per-state share of the target badge size. The shipped art fills the file,
|
||||||
// STATE. map_node/map_node_locked are 1920x1080 frames with the ~450px badge
|
// so these are pure size choices (NOT measurements): map_node_current is
|
||||||
// centered, so ink spans ~0.24 of the file width (measured on the shipped
|
// deliberately the largest - it's the level to play next and gets the idle
|
||||||
// art). map_node_current is a 512x512 image that IS the badge, so its ink
|
// pulse - with the cleared and locked badges a bit smaller.
|
||||||
// spans the whole file. Re-measure an entry if that file's art changes.
|
//
|
||||||
|
// NOTE: BADGE_FILE_FRAC only affects DISPLAY size, never the click area.
|
||||||
|
// The hit circle is sized from the art itself (BADGE_HIT_FRAC = 1.0 = full
|
||||||
|
// badge, times BADGE_HIT_RADIUS_MULT), and since all three states ship
|
||||||
|
// full-frame ink every node's click area is the same regardless of these
|
||||||
|
// display fractions.
|
||||||
export const BADGE_FILE_FRAC = {
|
export const BADGE_FILE_FRAC = {
|
||||||
map_node: 0.24,
|
map_node: 0.128,
|
||||||
map_node_current: 0.30,
|
map_node_current: 0.155,
|
||||||
map_node_locked: 0.24,
|
map_node_locked: 0.128,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hit-circle radius as a multiple of the VISIBLE badge's radius - slightly
|
// Hit-circle radius = (node.width * BADGE_HIT_FRAC[key] / 2) *
|
||||||
// generous so clicks near the edge feel natural without reaching the road.
|
// BADGE_HIT_RADIUS_MULT, in source-frame pixels. The badge fills its file,
|
||||||
|
// so 1.0 gives a click area exactly the badge - slightly generous via
|
||||||
|
// BADGE_HIT_RADIUS_MULT so clicks near the edge feel natural without
|
||||||
|
// reaching the road. (BADGE_FILE_FRAC no longer feeds the hit area - it's
|
||||||
|
// display size only.)
|
||||||
|
export const BADGE_HIT_FRAC = {
|
||||||
|
map_node: 1.0,
|
||||||
|
map_node_current: 1.0,
|
||||||
|
map_node_locked: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Slightly generous so clicks near the edge feel natural without reaching
|
||||||
|
// the road.
|
||||||
export const BADGE_HIT_RADIUS_MULT = 1.1;
|
export const BADGE_HIT_RADIUS_MULT = 1.1;
|
||||||
|
|
||||||
// Road ribbon display size, in design units.
|
// Road ribbon display size, in design units.
|
||||||
|
|
@ -35,9 +56,7 @@ export function frameAnchorDisplayWidth() {
|
||||||
return GAME_WIDTH;
|
return GAME_WIDTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Source pixel dimensions of each node texture. The old 960x540 frames were
|
// Source pixel dimensions of each node texture (all currently 512x512).
|
||||||
// 2x exports; the current map_node_current is a tight 512x512 badge. Used to
|
|
||||||
// normalize display size + hit radius regardless of frame size.
|
|
||||||
export const NODE_ART_FRAMES = {
|
export const NODE_ART_FRAMES = {
|
||||||
map_node: { w: 512, h: 512 },
|
map_node: { w: 512, h: 512 },
|
||||||
map_node_current: { w: 512, h: 512 },
|
map_node_current: { w: 512, h: 512 },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue