diff --git a/client/src/side/README.md b/client/src/side/README.md index 936362c6..092f965d 100644 --- a/client/src/side/README.md +++ b/client/src/side/README.md @@ -118,7 +118,9 @@ each prompt on the way. Before each arrow it presses E where nothing else takes player didn't move. It takes doors and lifts from their cell, and stairs and ladders from 1.4 tiles off their end, near the edge of the reach. Then it runs along the High Street from a press of Shift, checking the speed and the blend, jumps the rooftops' one-, two- and three-cell gaps, the last -running, checking it lands on the far roof, walks off the clock house's roof into the lane, opens +running, checking it lands on the far roof, and the metro's jumps up to its secrets, walks off the +clock house's roof into the lane, walks down into the metro, through it into the clinic and back up +to Banyan Row, checks a run jump at the metro's high ledge falls short, opens the news at every terminal on E, orders at the café's counter on E, and talks to Lachlan on E. It steps Playwright's clock rather than waiting, and the *Side walk* workflow runs it on every PR. diff --git a/client/src/side/maps.test.ts b/client/src/side/maps.test.ts index d022615d..7fdcc5b5 100644 --- a/client/src/side/maps.test.ts +++ b/client/src/side/maps.test.ts @@ -5,12 +5,13 @@ import { cellOf, climbAt, groundRow, isStanding, isWalkable } from './maps'; const map = (id: string) => village.map(id)!; describe('the side-view maps', () => { - it('reads the five streets and every floor', () => { + it('reads the five streets, the metro and every floor', () => { expect(village.streets.map(({ id }) => id).sort()).toEqual([ 'banyan_row', 'high_street', 'kapok_walk', 'lychee_lane', + 'metro', 'rooftops', ]); expect(village.floors('coffee_shop').map(({ id }) => id)).toEqual([ @@ -18,7 +19,7 @@ describe('the side-view maps', () => { 'coffee_shop_floor_1', 'coffee_shop_floor_2', ]); - expect(village.maps.size).toBe(5 + 31); + expect(village.maps.size).toBe(6 + 32); }); it('puts each floor on its street, four rows above the one below', () => { diff --git a/client/src/side/walk.e2e.ts b/client/src/side/walk.e2e.ts index 19784c65..4cdd6e14 100644 --- a/client/src/side/walk.e2e.ts +++ b/client/src/side/walk.e2e.ts @@ -223,15 +223,22 @@ async function runLeg(page: Page, api: Api, index: number): Promise { } } -/** Over a gap on the rooftops: from `from`, right until past `takeOff`, then Space, landing on row `y` past `land`. */ -type Jump = { name: string; from: Place; takeOff: number; land: number; run: boolean }; +/** Over a gap: from `from`, right until past `takeOff`, then Space, landing past `land` on row `onto`, or else `from`'s. */ +type Jump = { name: string; from: Place; takeOff: number; land: number; run: boolean; onto?: number }; const roof = (x: number, y: number): Place => ({ map: 'rooftops', x, y }); +const metro = (x: number, y: number): Place => ({ map: 'metro', x, y }); const JUMPS: Jump[] = [ { name: 'over the one-cell gap from the bakery', from: roof(6, 12), takeOff: 9.4, land: 10.75, run: false }, { name: 'over the two-cell gap to the tea house', from: roof(18, 8), takeOff: 21.6, land: 23.25, run: false }, { name: 'over the three-cell gap to the laundry, running', from: roof(26, 8), takeOff: 30.3, land: 33.25, run: true }, + { name: 'from the landing onto the catwalk over the stairwell', from: metro(1, 9), takeOff: 4, land: 6.5, onto: 7, run: true }, + { name: "from the train's roof onto the catwalk in the vault", from: metro(38, 12), takeOff: 42.3, land: 44.75, onto: 10, run: true }, + { name: 'up the tunnel wall from the pipe ledge', from: metro(76, 12), takeOff: 78.3, land: 80.75, onto: 10, run: true }, + { name: 'up the tunnel wall to the alcove', from: metro(81, 10), takeOff: 82.3, land: 84.75, onto: 8, run: true }, ]; +/** A run jump at the metro's high ledge, a row higher than the jump, falls back to the floor. */ +const SHORT: Jump = { name: 'at the high ledge, short of it', from: metro(101, 15), takeOff: 105, land: 104, onto: 15, run: true }; /** Jumps a gap on the rooftops with Space held, and lands on the far side rather than in the lane. */ async function jumpLeg(page: Page, api: Api, jump: Jump, index: number): Promise { @@ -252,11 +259,58 @@ async function jumpLeg(page: Page, api: Api, jump: Jump, index: number): Promise if (jump.run) await page.keyboard.press('Shift'); await run(page, 100); const landed = (await api.state())!; - if (landed.y !== jump.from.y || landed.x < jump.land) { - throw new Error(`${jump.name}: landed at ${landed.x.toFixed(2)}, ${landed.y}, not on row ${jump.from.y} past ${jump.land}`); + const row = jump.onto ?? jump.from.y; + if (landed.map !== jump.from.map || landed.y !== row || landed.x < jump.land) { + throw new Error(`${jump.name}: landed at ${landed.x.toFixed(2)}, ${landed.y}, not on row ${row} past ${jump.land}`); } } +/** Walks along the row, running, to within `reach` of `x`. */ +async function walkTo(page: Page, api: Api, x: number, reach: number, what: string): Promise { + const from = (await api.state())!.x; + if (Math.abs(from - x) <= reach) return; + const key = x > from ? 'ArrowRight' : 'ArrowLeft'; + await page.keyboard.down(key); + await until(page, async () => Math.abs((await api.state())!.x - x) <= reach, 40, `${what}: walking to ${x}`, FRAME_MS); + await page.keyboard.up(key); + await run(page, 200); +} + +/** Presses E, or holds up or down, until the player is at `arrive`. */ +async function take(page: Page, api: Api, key: 'e' | Hold, arrive: Place, what: string): Promise { + if (key === 'e') await page.keyboard.press('e'); + else await page.keyboard.down(key); + await until(page, async () => near(await api.state(), arrive), SECONDS_TO_ARRIVE, `${what}: arriving at ${JSON.stringify(arrive)}`); + if (key !== 'e') await page.keyboard.up(key); + await run(page, 300); +} + +/** From Banyan Row down through the metro into the clinic and back, by the keys alone. */ +async function metroLeg(page: Page, walk: Api, index: number): Promise { + const name = 'through the metro and back'; + await teleport(page, walk, { map: 'banyan_row', x: 59, y: 18 }, name); + await run(page, 300); + await page.keyboard.press('Shift'); + await walkTo(page, walk, 62, REACH.cell, name); + await take(page, walk, 'e', metro(2, 9), `${name}, down the stairway`); + await shot(page, index, 'into the metro'); + await walkTo(page, walk, 4, REACH.edge, name); + await take(page, walk, 'ArrowDown', metro(7, 12), `${name}, down to the ticket hall`); + await walkTo(page, walk, 19, REACH.edge, name); + await take(page, walk, 'ArrowDown', metro(22, 15), `${name}, down to the platform`); + await walkTo(page, walk, 89, REACH.cell, name); + await shot(page, index, 'at the clinic'); + await take(page, walk, 'e', { map: 'clinic_floor_0', x: 1, y: 2 }, `${name}, into the clinic`); + await take(page, walk, 'e', metro(89, 15), `${name}, out of the clinic`); + await walkTo(page, walk, 22, REACH.edge, name); + await take(page, walk, 'ArrowUp', metro(19, 12), `${name}, up to the ticket hall`); + await walkTo(page, walk, 7, REACH.edge, name); + await take(page, walk, 'ArrowUp', metro(4, 9), `${name}, up to the landing`); + await walkTo(page, walk, 2, REACH.cell, name); + await take(page, walk, 'e', { map: 'banyan_row', x: 62, y: 18 }, `${name}, up to Banyan Row`); + await page.keyboard.press('Shift'); +} + /** Walks off the clock house's roof and lands in the lane, a fall of eleven rows. */ async function fallLeg(page: Page, api: Api, index: number): Promise { const name = "off the clock house's roof"; @@ -356,6 +410,8 @@ try { ['running with Shift', (i) => runLeg(page, api, i)], ...JUMPS.map((jump): [string, (index: number) => Promise] => [`jumping ${jump.name}`, (i) => jumpLeg(page, api, jump, i)]), ["falling off the clock house's roof", (i) => fallLeg(page, api, i)], + ['through the metro and back', (i) => metroLeg(page, api, i)], + [`jumping ${SHORT.name}`, (i) => jumpLeg(page, api, SHORT, i)], ...(await api.terminals()).map((at): [string, (index: number) => Promise] => [ `E at the terminal on ${at.map}`, (i) => terminalLeg(page, api, at, i), diff --git a/content/papercraft/README.md b/content/papercraft/README.md index 968c23af..3b7f8ea8 100644 --- a/content/papercraft/README.md +++ b/content/papercraft/README.md @@ -135,3 +135,20 @@ IS-Net's weights (`isnet-general-use.onnx`, 179 MB, rembg's release) download on [`references/`](references/) holds the five style references from #295's study (0.75 MB): street, house, rooftops, furniture and characters. The build reads them from here, never from `docs/`. +[`references/cartoon/`](references/cartoon/) holds #329's revised cartoon references, street and +interior (0.47 MB), for the tunnels. + +## The tunnels + +The metro under the village (`content/street/README.md`'s *The metro*) is painted in #329's revised +cartoon, kept a little dirty: its layers (`TUNNEL_LAYERS`), the furniture marked `cartoon`, the +clinic's front (its `Look` is `cartoon`) and its room paper (`CARTOON_PAPERS`). Their prompts end in +`painter.CARTOON` rather than the papercraft style, and they're painted from the cartoon references. +The rest of the pipeline is the same, with three additions: + +- `near-metro` is a solid tiled wall 4.6 m tall, so it hides the foot of `mid-metro`, which the + client draws 2.75 m up. It's cut as a `ground` piece, keyed on its top row's colour (`cut` + `above`), since the wall fills its sides. +- The clinic's `Look` is `whole`: one painting however wide, with no seam where units would meet. +- The metro's catwalks and steps are furniture drawn over the street's own ledges and steps, which + stay papercraft. diff --git a/content/papercraft/fronts/clinic.webp b/content/papercraft/fronts/clinic.webp new file mode 100644 index 00000000..6f181136 Binary files /dev/null and b/content/papercraft/fronts/clinic.webp differ diff --git a/content/papercraft/furniture/bedroll.webp b/content/papercraft/furniture/bedroll.webp new file mode 100644 index 00000000..dace5cf7 Binary files /dev/null and b/content/papercraft/furniture/bedroll.webp differ diff --git a/content/papercraft/furniture/breaker_box.webp b/content/papercraft/furniture/breaker_box.webp new file mode 100644 index 00000000..4f2f810f Binary files /dev/null and b/content/papercraft/furniture/breaker_box.webp differ diff --git a/content/papercraft/furniture/crates.webp b/content/papercraft/furniture/crates.webp new file mode 100644 index 00000000..ab374575 Binary files /dev/null and b/content/papercraft/furniture/crates.webp differ diff --git a/content/papercraft/furniture/dead_terminal.webp b/content/papercraft/furniture/dead_terminal.webp new file mode 100644 index 00000000..3c841401 Binary files /dev/null and b/content/papercraft/furniture/dead_terminal.webp differ diff --git a/content/papercraft/furniture/freight_lift.webp b/content/papercraft/furniture/freight_lift.webp new file mode 100644 index 00000000..d56ddb30 Binary files /dev/null and b/content/papercraft/furniture/freight_lift.webp differ diff --git a/content/papercraft/furniture/metro_bench.webp b/content/papercraft/furniture/metro_bench.webp new file mode 100644 index 00000000..46ac1bc6 Binary files /dev/null and b/content/papercraft/furniture/metro_bench.webp differ diff --git a/content/papercraft/furniture/metro_entrance.webp b/content/papercraft/furniture/metro_entrance.webp new file mode 100644 index 00000000..63cb88a2 Binary files /dev/null and b/content/papercraft/furniture/metro_entrance.webp differ diff --git a/content/papercraft/furniture/metro_steps.webp b/content/papercraft/furniture/metro_steps.webp new file mode 100644 index 00000000..05fc5aab Binary files /dev/null and b/content/papercraft/furniture/metro_steps.webp differ diff --git a/content/papercraft/furniture/pipes.webp b/content/papercraft/furniture/pipes.webp new file mode 100644 index 00000000..d2ce74c0 Binary files /dev/null and b/content/papercraft/furniture/pipes.webp differ diff --git a/content/papercraft/furniture/rubble.webp b/content/papercraft/furniture/rubble.webp new file mode 100644 index 00000000..51cd2cbb Binary files /dev/null and b/content/papercraft/furniture/rubble.webp differ diff --git a/content/papercraft/furniture/scrap_stall.webp b/content/papercraft/furniture/scrap_stall.webp new file mode 100644 index 00000000..62037b08 Binary files /dev/null and b/content/papercraft/furniture/scrap_stall.webp differ diff --git a/content/papercraft/furniture/ticket_machine.webp b/content/papercraft/furniture/ticket_machine.webp new file mode 100644 index 00000000..d858a4e3 Binary files /dev/null and b/content/papercraft/furniture/ticket_machine.webp differ diff --git a/content/papercraft/furniture/train_carriage.webp b/content/papercraft/furniture/train_carriage.webp new file mode 100644 index 00000000..fb3fb0b9 Binary files /dev/null and b/content/papercraft/furniture/train_carriage.webp differ diff --git a/content/papercraft/furniture/tunnel_arch.webp b/content/papercraft/furniture/tunnel_arch.webp new file mode 100644 index 00000000..f571349d Binary files /dev/null and b/content/papercraft/furniture/tunnel_arch.webp differ diff --git a/content/papercraft/furniture/turnstile.webp b/content/papercraft/furniture/turnstile.webp new file mode 100644 index 00000000..76358583 Binary files /dev/null and b/content/papercraft/furniture/turnstile.webp differ diff --git a/content/papercraft/glow/clinic.webp b/content/papercraft/glow/clinic.webp new file mode 100644 index 00000000..d6a303f7 Binary files /dev/null and b/content/papercraft/glow/clinic.webp differ diff --git a/content/papercraft/layers/far-metro.webp b/content/papercraft/layers/far-metro.webp new file mode 100644 index 00000000..46e52f07 Binary files /dev/null and b/content/papercraft/layers/far-metro.webp differ diff --git a/content/papercraft/layers/front-metro.webp b/content/papercraft/layers/front-metro.webp new file mode 100644 index 00000000..89a6a937 Binary files /dev/null and b/content/papercraft/layers/front-metro.webp differ diff --git a/content/papercraft/layers/ground-metro.webp b/content/papercraft/layers/ground-metro.webp new file mode 100644 index 00000000..2dac2534 Binary files /dev/null and b/content/papercraft/layers/ground-metro.webp differ diff --git a/content/papercraft/layers/metro-vault.webp b/content/papercraft/layers/metro-vault.webp new file mode 100644 index 00000000..c166eb7e Binary files /dev/null and b/content/papercraft/layers/metro-vault.webp differ diff --git a/content/papercraft/layers/mid-metro.webp b/content/papercraft/layers/mid-metro.webp new file mode 100644 index 00000000..4ca1ca3f Binary files /dev/null and b/content/papercraft/layers/mid-metro.webp differ diff --git a/content/papercraft/layers/near-metro.webp b/content/papercraft/layers/near-metro.webp new file mode 100644 index 00000000..b4ef9abb Binary files /dev/null and b/content/papercraft/layers/near-metro.webp differ diff --git a/content/papercraft/layout.json b/content/papercraft/layout.json index 781ef9d3..c029ca24 100644 --- a/content/papercraft/layout.json +++ b/content/papercraft/layout.json @@ -11,6 +11,17 @@ "width": 6, "x": 4 }, + "clinic": { + "door": 1, + "height": 3, + "id": "clinic", + "interior": true, + "murals": [], + "storeys": 1, + "street": "metro", + "width": 10, + "x": 88 + }, "clock_house": { "door": null, "height": 11, @@ -287,6 +298,10 @@ 2, 1 ], + "bedroll": [ + 2, + 1 + ], "bench": [ 2, 1 @@ -303,6 +318,10 @@ 1, 2 ], + "breaker_box": [ + 1, + 2 + ], "bush": [ 1, 1 @@ -311,10 +330,22 @@ 2, 1 ], + "catwalk": [ + 8, + 2 + ], "counter": [ 3, 1 ], + "crates": [ + 1, + 1 + ], + "dead_terminal": [ + 1, + 2 + ], "espresso_machine": [ 1, 1 @@ -323,6 +354,10 @@ 2, 2 ], + "freight_lift": [ + 2, + 4 + ], "grinder": [ 1, 1 @@ -355,6 +390,18 @@ 3, 1 ], + "metro_bench": [ + 2, + 1 + ], + "metro_entrance": [ + 1, + 3 + ], + "metro_steps": [ + 4, + 4 + ], "news_terminal": [ 1, 2 @@ -375,6 +422,10 @@ 4, 1 ], + "pipes": [ + 12, + 1 + ], "plant": [ 1, 1 @@ -391,6 +442,14 @@ 8, 1 ], + "rubble": [ + 2, + 1 + ], + "scrap_stall": [ + 3, + 2 + ], "sewing_machine": [ 1, 2 @@ -415,10 +474,26 @@ 2, 1 ], + "ticket_machine": [ + 1, + 2 + ], + "train_carriage": [ + 10, + 3 + ], "tree": [ 2, 4 ], + "tunnel_arch": [ + 3, + 8 + ], + "turnstile": [ + 2, + 1 + ], "wardrobe": [ 1, 2 @@ -434,6 +509,9 @@ "stairs" ], "rooms": { + "clinic": [ + "clinic" + ], "coffee_shop": [ "cafe", "cafe_upstairs", @@ -499,6 +577,7 @@ "high_street": 84, "kapok_walk": 34, "lychee_lane": 68, + "metro": 110, "rooftops": 60 } } diff --git a/content/papercraft/manifest.json b/content/papercraft/manifest.json index 2a62e147..d23b3adb 100644 --- a/content/papercraft/manifest.json +++ b/content/papercraft/manifest.json @@ -120,6 +120,14 @@ "ground": "layers/ground-street", "foreground": "layers/front-plants" }, + "metro": { + "sky": "layers/metro-vault", + "far": "layers/far-metro", + "mid": "layers/mid-metro", + "near": "layers/near-metro", + "ground": "layers/ground-metro", + "foreground": "layers/front-metro" + }, "rooftops": { "sky": "layers/sky", "far": "layers/far-hills", @@ -141,6 +149,14 @@ "glow": "glow/bakery", "floors": [] }, + "clinic": { + "street": "metro", + "front": "fronts/clinic", + "glow": "glow/clinic", + "floors": [ + "papers/clinic-tiles" + ] + }, "clock_house": { "street": "rooftops", "front": "fronts/clock_house", @@ -434,6 +450,56 @@ "lift": { "piece": "platforms/lift" }, + "metro_entrance": { + "piece": "furniture/metro_entrance" + }, + "train_carriage": { + "piece": "furniture/train_carriage" + }, + "catwalk": { + "piece": "platforms/catwalk", + "stretch": true + }, + "metro_steps": { + "piece": "furniture/metro_steps" + }, + "freight_lift": { + "piece": "furniture/freight_lift" + }, + "scrap_stall": { + "piece": "furniture/scrap_stall" + }, + "turnstile": { + "piece": "furniture/turnstile" + }, + "ticket_machine": { + "piece": "furniture/ticket_machine" + }, + "metro_bench": { + "piece": "furniture/metro_bench" + }, + "dead_terminal": { + "piece": "furniture/dead_terminal" + }, + "breaker_box": { + "piece": "furniture/breaker_box" + }, + "crates": { + "piece": "furniture/crates" + }, + "rubble": { + "piece": "furniture/rubble" + }, + "bedroll": { + "piece": "furniture/bedroll" + }, + "pipes": { + "piece": "furniture/pipes", + "stretch": true + }, + "tunnel_arch": { + "piece": "furniture/tunnel_arch" + }, "armchair": { "piece": "furniture/armchair", "variants": { @@ -478,6 +544,7 @@ "platforms": { "rope_bridge": "platforms/rope_bridge", "lift": "platforms/lift", + "catwalk": "platforms/catwalk", "stairs": "platforms/stairs", "ladder": "platforms/ladder", "steps": "platforms/steps", @@ -504,6 +571,22 @@ "eaves": 3.3, "hash": "2401bf6e1ceb5eb08eb1" }, + "fronts/clinic": { + "file": "fronts/clinic.webp", + "size": [ + 1464, + 512 + ], + "height": 4.267, + "kind": "front", + "anchor": [ + 0.5, + 0.9719 + ], + "width": 11.0, + "eaves": 3.3, + "hash": "1ad4b803349fe5760384" + }, "fronts/clock_house": { "file": "fronts/clock_house.webp", "size": [ @@ -952,6 +1035,20 @@ ], "hash": "0f42e4e51ed89dcb692c" }, + "furniture/bedroll": { + "file": "furniture/bedroll.webp", + "size": [ + 208, + 54 + ], + "height": 0.45, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "95dd1505a49df6c89e43" + }, "furniture/bedside_table": { "file": "furniture/bedside_table.webp", "size": [ @@ -1022,6 +1119,20 @@ ], "hash": "f33ddefbb753f008f071" }, + "furniture/breaker_box": { + "file": "furniture/breaker_box.webp", + "size": [ + 78, + 168 + ], + "height": 1.4, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "c331890130c19dc84d24" + }, "furniture/bush": { "file": "furniture/bush.webp", "size": [ @@ -1064,6 +1175,34 @@ ], "hash": "1dd42126d7f2956f9438" }, + "furniture/crates": { + "file": "furniture/crates.webp", + "size": [ + 128, + 120 + ], + "height": 1.0, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "fd73e48116d9e02cf73a" + }, + "furniture/dead_terminal": { + "file": "furniture/dead_terminal.webp", + "size": [ + 113, + 180 + ], + "height": 1.5, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "85598dbe2d0c5fc776cd" + }, "furniture/espresso_machine": { "file": "furniture/espresso_machine.webp", "size": [ @@ -1106,6 +1245,20 @@ ], "hash": "8b89ab3a5fb3b7465fc6" }, + "furniture/freight_lift": { + "file": "furniture/freight_lift.webp", + "size": [ + 281, + 528 + ], + "height": 4.4, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "04d76636a39e0f4d23df" + }, "furniture/fridge": { "file": "furniture/fridge.webp", "size": [ @@ -1232,6 +1385,48 @@ ], "hash": "ca013f27686914c5b9cc" }, + "furniture/metro_bench": { + "file": "furniture/metro_bench.webp", + "size": [ + 205, + 108 + ], + "height": 0.9, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "fea1192a44a8dc674c8d" + }, + "furniture/metro_entrance": { + "file": "furniture/metro_entrance.webp", + "size": [ + 191, + 276 + ], + "height": 2.3, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "b4e4743385530a3809ef" + }, + "furniture/metro_steps": { + "file": "furniture/metro_steps.webp", + "size": [ + 395, + 403 + ], + "height": 3.36, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "0f04a6431f174e124e29" + }, "furniture/news_terminal": { "file": "furniture/news_terminal.webp", "size": [ @@ -1316,6 +1511,20 @@ ], "hash": "308c9957cc07e41a5139" }, + "furniture/pipes": { + "file": "furniture/pipes.webp", + "size": [ + 221, + 96 + ], + "height": 0.8, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "ba0b8ea15ae6308862ca" + }, "furniture/plant": { "file": "furniture/plant.webp", "size": [ @@ -1358,6 +1567,34 @@ ], "hash": "59f20f5d645ada037b12" }, + "furniture/rubble": { + "file": "furniture/rubble.webp", + "size": [ + 186, + 84 + ], + "height": 0.7, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "8d1612c61127eb44cd00" + }, + "furniture/scrap_stall": { + "file": "furniture/scrap_stall.webp", + "size": [ + 284, + 264 + ], + "height": 2.2, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "7a04ad93737175fdb250" + }, "furniture/sewing_machine": { "file": "furniture/sewing_machine.webp", "size": [ @@ -1498,6 +1735,34 @@ ], "hash": "401a7b89d86675aafc93" }, + "furniture/ticket_machine": { + "file": "furniture/ticket_machine.webp", + "size": [ + 107, + 228 + ], + "height": 1.9, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "da9640f38f7599ce31f3" + }, + "furniture/train_carriage": { + "file": "furniture/train_carriage.webp", + "size": [ + 1293, + 403 + ], + "height": 3.36, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "5014d1ee651d3b238a7a" + }, "furniture/tree": { "file": "furniture/tree.webp", "size": [ @@ -1512,6 +1777,34 @@ ], "hash": "ed8504e13697dc6cfada" }, + "furniture/tunnel_arch": { + "file": "furniture/tunnel_arch.webp", + "size": [ + 919, + 1056 + ], + "height": 8.8, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "fb96f3eab7fd963722c5" + }, + "furniture/turnstile": { + "file": "furniture/turnstile.webp", + "size": [ + 187, + 132 + ], + "height": 1.1, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "ea6fc04803b8ea09d76d" + }, "furniture/wardrobe": { "file": "furniture/wardrobe.webp", "size": [ @@ -1570,6 +1863,22 @@ "eaves": 3.3, "hash": "5d3d5ca5a160e01b32d6" }, + "glow/clinic": { + "file": "glow/clinic.webp", + "size": [ + 1464, + 512 + ], + "height": 4.267, + "kind": "glow", + "anchor": [ + 0.5, + 0.9719 + ], + "width": 11.0, + "eaves": 3.3, + "hash": "62bdb57987439ab833bd" + }, "glow/clock_house": { "file": "glow/clock_house.webp", "size": [ @@ -1916,6 +2225,16 @@ "kind": "layer", "hash": "49fda8e4ed22b6a338e6" }, + "layers/far-metro": { + "file": "layers/far-metro.webp", + "size": [ + 1304, + 239 + ], + "height": 5.0, + "kind": "layer", + "hash": "1dc9ec12a11fc536830b" + }, "layers/far-river": { "file": "layers/far-river.webp", "size": [ @@ -1926,6 +2245,16 @@ "kind": "layer", "hash": "ebd2607209dcac26ba4f" }, + "layers/front-metro": { + "file": "layers/front-metro.webp", + "size": [ + 1204, + 137 + ], + "height": 2.0, + "kind": "strip", + "hash": "7eb0d2b5f1b277cf6cfc" + }, "layers/front-plants": { "file": "layers/front-plants.webp", "size": [ @@ -1946,6 +2275,16 @@ "kind": "strip", "hash": "0fa02b1bf85dbc5a276b" }, + "layers/ground-metro": { + "file": "layers/ground-metro.webp", + "size": [ + 1272, + 142 + ], + "height": 2.6, + "kind": "ground", + "hash": "40d8f1dd76c876618a67" + }, "layers/ground-park": { "file": "layers/ground-park.webp", "size": [ @@ -1986,6 +2325,16 @@ "kind": "ground", "hash": "7e7c800bc23d43310793" }, + "layers/metro-vault": { + "file": "layers/metro-vault.webp", + "size": [ + 1284, + 696 + ], + "height": 12.0, + "kind": "paper", + "hash": "b669d10d4853127e9c65" + }, "layers/mid-homes": { "file": "layers/mid-homes.webp", "size": [ @@ -1996,6 +2345,16 @@ "kind": "strip", "hash": "afa7456de86c7ddb8ce2" }, + "layers/mid-metro": { + "file": "layers/mid-metro.webp", + "size": [ + 1000, + 352 + ], + "height": 9.0, + "kind": "strip", + "hash": "037fdebfdbc4158b8ae0" + }, "layers/mid-riverbank": { "file": "layers/mid-riverbank.webp", "size": [ @@ -2046,6 +2405,16 @@ "kind": "strip", "hash": "06435d046c00c7d5b416" }, + "layers/near-metro": { + "file": "layers/near-metro.webp", + "size": [ + 1020, + 452 + ], + "height": 4.6, + "kind": "ground", + "hash": "b19f36fab01212e05ba4" + }, "layers/near-walls": { "file": "layers/near-walls.webp", "size": [ @@ -2136,6 +2505,16 @@ "kind": "paper", "hash": "0c093b37b40a4e6cbdc8" }, + "papers/clinic-tiles": { + "file": "papers/clinic-tiles.webp", + "size": [ + 592, + 392 + ], + "height": 3.267, + "kind": "paper", + "hash": "d894de3c5830044b01db" + }, "papers/dining-red": { "file": "papers/dining-red.webp", "size": [ @@ -2216,6 +2595,20 @@ "kind": "paper", "hash": "9417c99e48e217d9df2f" }, + "platforms/catwalk": { + "file": "platforms/catwalk.webp", + "size": [ + 397, + 259 + ], + "height": 2.16, + "kind": "sprite", + "anchor": [ + 0.5, + 1.0 + ], + "hash": "c149f83495895f7043ab" + }, "platforms/door": { "file": "platforms/door.webp", "size": [ diff --git a/content/papercraft/papers/clinic-tiles.webp b/content/papercraft/papers/clinic-tiles.webp new file mode 100644 index 00000000..12712e0c Binary files /dev/null and b/content/papercraft/papers/clinic-tiles.webp differ diff --git a/content/papercraft/platforms/catwalk.webp b/content/papercraft/platforms/catwalk.webp new file mode 100644 index 00000000..24db3268 Binary files /dev/null and b/content/papercraft/platforms/catwalk.webp differ diff --git a/content/papercraft/references/cartoon/interior.jpg b/content/papercraft/references/cartoon/interior.jpg new file mode 100644 index 00000000..8cfb5883 Binary files /dev/null and b/content/papercraft/references/cartoon/interior.jpg differ diff --git a/content/papercraft/references/cartoon/street.jpg b/content/papercraft/references/cartoon/street.jpg new file mode 100644 index 00000000..e9fd2203 Binary files /dev/null and b/content/papercraft/references/cartoon/street.jpg differ diff --git a/content/street/README.md b/content/street/README.md index 1c6d2bb1..bd2d2cc5 100644 --- a/content/street/README.md +++ b/content/street/README.md @@ -12,7 +12,7 @@ cd server && uv run simile street Output is deterministic, and `tests/street` fails if a file drifts from the builder. -- `.tmj`: the five streets. +- `.tmj`: the five streets, and the metro under them. - `floors/_floor_.tmj`: each floor of each building, `0` for the ground floor. - `markers.png`: the tiny tileset the tile layers use, so Tiled shows them. - `jump.json`: the player's jump (#331), from `server/simile/street/jump.py`. The client plays it @@ -22,7 +22,8 @@ Output is deterministic, and `tests/street` fails if a file drifts from the buil squares are doors, yellow and orange lines stairs and ladders between floors, pink lines the lift. Violet dots are spots, with a tick the way they face (up for `north`, down for `south`). Yellow squares are entries, teal boxes locations, orange boxes things to use, cyan boxes mural - walls from `bottom` to `top`, and thin grey boxes furniture. Mural walls and doors to other + walls from `bottom` to `top`, and thin grey boxes furniture. Dark stretches are shaded navy, + places salvage can lie are green diamonds and secrets pink ones. Mural walls and doors to other streets are named in the ground under them. ## The grid @@ -60,14 +61,21 @@ Every map has every layer, bottom to top: | `doors` | Objects. One tile per link with an end on this map (below). | | `interactables` | Objects, as today, on floors. | | `spots` | Objects, as today. | +| `light` | Objects. Rectangles, each a stretch of the map that's lit or dark, named by its id, with a `lit` property. Only the metro has any yet, and every cell of it is in one. | +| `pickups` | Objects. One tile per standing cell where something lies to pick up, named by its id. Its type is `salvage`, a place salvage can lie, or `secret`, which holds `money`, or an item: `item` (its id), `item_kind`, `item_name` and `item_weight`, as `Item` has them. | The furniture names are `bar_counter`, `bed`, `bench`, `bicycle`, `bin`, `bookshelf`, `bush`, `cafe_table`, `counter`, `espresso_machine`, `fountain`, `grinder`, `kitchen`, `kitchen_counter`, `lamp`, `laser_cutter`, `lift`, `loom`, `menu_board`, `news_terminal`, `noodle_counter`, `noodle_table`, `palm_tree`, `pegboard`, `plant`, `plinth`, `printer_3d`, `rope_bridge`, -`sewing_machine`, `shelf`, `sofa`, `stool`, `stove`, `table`, `tree`, `wardrobe` and `workbench`. -A table is drawn with a chair either side. A `rope_bridge` stands on the solid row under it, its -deck. +`sewing_machine`, `shelf`, `sofa`, `stool`, `stove`, `table`, `tree`, `wardrobe` and `workbench`, +and in the metro `bedroll`, `breaker_box`, `catwalk`, `crates`, `dead_terminal`, `freight_lift`, +`metro_bench`, `metro_steps`, `pipes`, `rubble`, `scrap_stall`, `ticket_machine`, `train_carriage`, +`tunnel_arch` and `turnstile`, with `metro_entrance` beside the stairway down on Banyan Row. A table +is drawn with a chair either side. A `rope_bridge` stands on the solid row under it, its deck. A +`catwalk` covers its solid row and the row above it, its deck on the solid row's top, and the +`train_carriage` its roof's solid row and everything under it. `metro_steps` covers a flight of stairs +and the cells at its ends, over the street's own steps. ## Map properties @@ -78,7 +86,7 @@ deck. | `label` | Streets | Its name, e.g. `High Street`. | | `building` | Floors | Its building's location id. | | `floor` | Floors | Its number, `0` for the ground floor. | -| `room` | Floors | What the room is, for its papers: `living_room`, `bedroom`, `cafe`, `cafe_upstairs`, `flat`, `noodle_bar`, `dining_room`, `workshop`, `loft` or `gallery`. | +| `room` | Floors | What the room is, for its papers: `living_room`, `bedroom`, `cafe`, `cafe_upstairs`, `flat`, `noodle_bar`, `dining_room`, `workshop`, `loft`, `gallery` or `clinic`. | | `street`, `street_x`, `street_y` | Floors | The street its building stands on, and the street cell under the floor map's top-left cell, so its dollhouse is drawn in place. | ## Objects @@ -123,7 +131,7 @@ west end. The farthest two places are about 70 s' walk apart, against today's 47 | `high_street` High Street | 84 | `maker_space` (2–17), `corner_shop` (19–24, a mural), `coffee_shop` (25–44, a mural) with `cafe_terrace` in front, `square` (45–64) with the fountain at 55–56, `terminal_1` at 60 and the doors to Kapok Walk (46), Lychee Lane (50) and Banyan Row (64), `noodle_bar` (65–81) | | `lychee_lane` Lychee Lane | 68 | `house_1` (2–8), `house_2` (9–15), the door to the High Street (17), `house_3` (19–25), `house_4` (26–32), `terminal_2` at the park gate (34), `park` (36–59) with a lookout up a ladder at 45, `tower` (61–66, a mural) | | `kapok_walk` Kapok Walk | 34 | a garden (1–8) with the lane to the Rooftops (2), `house_5` (9–15), `house_6` (16–22), `house_7` (23–29), the door to the High Street (32) | -| `banyan_row` Banyan Row | 68 | `house_8` (2–8), `house_9` (9–15), `house_10` (16–22, a mural), the door to the High Street (24), `gallery` (26–43), `garden_flats` (45–50, a mural), then two stairs down from 53 to the river's promenade, row 18, with `terminal_3` at 60 | +| `banyan_row` Banyan Row | 68 | `house_8` (2–8), `house_9` (9–15), `house_10` (16–22, a mural), the door to the High Street (24), `gallery` (26–43), `garden_flats` (45–50, a mural), then two stairs down from 53 to the river's promenade, row 18, with `terminal_3` at 60 and the stairway down to the metro at 62 | | `rooftops` The Rooftops | 60 | The door to Kapok Walk (1) on a back lane, a ladder (3) up onto the `bakery`'s roof (4–9, row 12), a gap, walkways up the alley (11–14, rows 12 and 10), the roofs of the `print_works` (15–21), the `tea_house` (24–30) and the `laundry` (34–39), all row 8, with gaps of two and three cells between them, a `palm_tree` in the second, a `rope_bridge` (40–47) to a balcony (48–49), and a ladder (49) up to the `clock_house`'s roof (50–56, row 4) | | Building | Street | Width | Floors | Links between floors | @@ -163,6 +171,45 @@ west end. The farthest two places are about 70 s' walk apart, against today's 47 anywhere lands in the lane, which runs under everything back to the door and the first ladder. +## The metro + +`metro`, the Old Metro (#350), is an abandoned station under the village, 110 wide. Its stairway +down is a door like any other, `banyan_row_to_metro`, from Banyan Row's river promenade (62, row +18) to a landing at the top of the stairs (2, row 9). Like the Rooftops it's for the player alone +(`JUMPING`), and it's in `UNDERGROUND`, the streets under the village. Its locations, the clinic +and the scrap trader's stall (`scrap_stall`), are `amenity`s no resident's activity names, and its +only doors are the stairway and the clinic's, so no resident has anywhere to go there and no route +passes through it. + +| x | | +|---|---| +| 0–19 | The landing (row 9), a flight down to the ticket hall (row 12) with the `turnstile` and `ticket_machine`, and a flight down to the platform (row 15). A catwalk over the first flight, at row 7, is a run jump from the landing. | +| 20–54 | The platform: the scrap trader's `scrap_stall`, benches, and a dead `train_carriage` (33–42) with a ladder (32) up to its roof, row 12. A catwalk along the vault (45–52, row 10) is a jump from the roof's east end. | +| 55–86 | The `tunnel_arch`, then the service tunnel, dark either side of the maintenance alcove (71–73) with the plug. In the east half a ladder (75) climbs to a pipe ledge (76–78, row 12), and a catwalk (81–82, row 10) and an alcove (85–86, row 8) go on up the wall, each a jump from the last. | +| 87–109 | The depot: the clinic (88–97), the `freight_lift` down to the sewers (100–101), a `dead_terminal` (104), and the high ledge (106–109, row 12) against the east wall. | + +**What later tasks build on.** Each has an id that stays put; `simile.street.metro` names them. + +| Task | Hook | Id, and where | +|---|---|---| +| S4 | Dark stretches, in `light` | `metro_vault` (19–57, rows 0–11), `metro_tunnel_west` (58–70), `metro_tunnel_east` (74–86). The rest is lit: `metro_entrance`, `metro_platform`, `metro_maintenance` and `metro_depot`. | +| S4 | The maintenance plug, a `machine` | `metro_plug`, stood at from (72, 15), in the lit alcove between the tunnel's two dark halves. | +| S6 | Places salvage can lie, in `pickups` | `metro_salvage_1` on the train's roof (38, 12), `_2` (63, 15) and `_3` (68, 15) in the west tunnel, `_4` on the pipe ledge (77, 12), `_5` (83, 15), and `_6` on the high ledge (108, 12). | +| S6 | The scrap trader's stall, and its spot | The location `scrap_stall` (24–28, rows 13–15, entry 28). The spot is `metro_scrap_trader`, `standing` at (26, 15) facing `south`, at the `scrap_stall` piece. | +| S7 | The clinic, and the doctor's stall spot | `clinic`, an `amenity` on `clinic_floor_0` (room `clinic`, 10 wide), its door at (89, 15). The doctor's spot is `clinic_doctor`, `standing` at (6, 2) facing `south`. The room is bare for S7 to furnish. | +| S8 | The high ledge, a `machine` | `metro_high_ledge`, its four cells (106–109, row 12). It's three rows above the floor under it (`LEGS_ROWS`): out of reach of today's jump, two rows up (`rise` 2.3 tiles), and in reach of the legs if they jump three. Salvage waits on it. | +| S8 | The dead terminal, a `machine` | `metro_terminal`, stood at from (104, 15). | +| S8, S9 | The lift down to the sewers, a `machine` | `metro_lift`, stood at from (100, 15) and (101, 15). Its foot on the metro, where a link to the sewers would end, is (100, 15) (`LIFT_FOOT`). No link leads from it yet. | + +**Secrets** are `secret` pickups, each a jump that walking and climbing alone don't reach: +`metro_secret_stairwell` (8, 7, 25 money), `metro_secret_catwalk` (52, 10, 40 money) and +`metro_secret_alcove` (86, 8, a `ration_pack`, food, 0.5 kg). + +**Drawing it** (`server/simile/papercraft/`): the metro's layers, furniture, the clinic's front and +room paper are painted in #329's revised cartoon, a little dirty. Its catwalks are furniture over +their solid rows, which hides the street's wooden ledge under them, and the train's roof is its +picture's top edge. + ## Ids Every location, door, spot, interactable and mural wall id in `content/map/` is here, and no map id diff --git a/content/street/banyan_row.tmj b/content/street/banyan_row.tmj index a4f49105..8ebc39c8 100644 --- a/content/street/banyan_row.tmj +++ b/content/street/banyan_row.tmj @@ -318,6 +318,18 @@ "width": 16, "x": 1056, "y": 256 + }, + { + "height": 48, + "id": 11, + "name": "metro_entrance", + "properties": [], + "rotation": 0, + "type": "metro_entrance", + "visible": true, + "width": 16, + "x": 976, + "y": 256 } ], "opacity": 1, @@ -333,7 +345,7 @@ "objects": [ { "height": 48, - "id": 11, + "id": 12, "name": "terminal_3", "properties": [ { @@ -373,7 +385,7 @@ "objects": [ { "height": 16, - "id": 12, + "id": 13, "name": "banyan_row", "properties": [ { @@ -411,7 +423,7 @@ }, { "height": 16, - "id": 13, + "id": 14, "name": "garden_flats", "properties": [ { @@ -461,7 +473,7 @@ "objects": [ { "height": 16, - "id": 14, + "id": 15, "name": "house_8", "properties": [ { @@ -499,7 +511,7 @@ }, { "height": 16, - "id": 15, + "id": 16, "name": "house_9", "properties": [ { @@ -537,7 +549,7 @@ }, { "height": 16, - "id": 16, + "id": 17, "name": "house_10", "properties": [ { @@ -575,7 +587,7 @@ }, { "height": 16, - "id": 17, + "id": 18, "name": "gallery", "properties": [ { @@ -613,7 +625,7 @@ }, { "height": 16, - "id": 18, + "id": 19, "name": "banyan_row_to_high_street", "properties": [ { @@ -648,6 +660,44 @@ "width": 16, "x": 384, "y": 240 + }, + { + "height": 16, + "id": 20, + "name": "banyan_row_to_metro", + "properties": [ + { + "name": "kind", + "type": "string", + "value": "door" + }, + { + "name": "side", + "type": "string", + "value": "outside" + }, + { + "name": "to_map", + "type": "string", + "value": "metro" + }, + { + "name": "to_x", + "type": "int", + "value": 2 + }, + { + "name": "to_y", + "type": "int", + "value": 9 + } + ], + "rotation": 0, + "type": "door", + "visible": true, + "width": 16, + "x": 992, + "y": 288 } ], "opacity": 1, @@ -674,7 +724,7 @@ "objects": [ { "height": 16, - "id": 19, + "id": 21, "name": "viewing-18-32", "properties": [ { @@ -692,7 +742,7 @@ }, { "height": 16, - "id": 20, + "id": 22, "name": "viewing-34-31", "properties": [ { @@ -710,7 +760,7 @@ }, { "height": 16, - "id": 21, + "id": 23, "name": "terminal-46-37", "properties": [ { @@ -732,10 +782,32 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, - "nextobjectid": 22, + "nextlayerid": 13, + "nextobjectid": 24, "orientation": "orthogonal", "properties": [ { diff --git a/content/street/floors/clinic_floor_0.tmj b/content/street/floors/clinic_floor_0.tmj new file mode 100644 index 00000000..7c08ae64 --- /dev/null +++ b/content/street/floors/clinic_floor_0.tmj @@ -0,0 +1,353 @@ +{ + "compressionlevel": -1, + "height": 4, + "infinite": false, + "layers": [ + { + "data": [ + 2,0,0,0,0,0,0,0,0,2, + 2,0,0,0,0,0,0,0,0,2, + 2,0,0,0,0,0,0,0,0,2, + 2,2,2,2,2,2,2,2,2,2 + ], + "height": 4, + "id": 1, + "name": "solid", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 10, + "x": 0, + "y": 0 + }, + { + "data": [ + 0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0 + ], + "height": 4, + "id": 2, + "name": "climbable", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 10, + "x": 0, + "y": 0 + }, + { + "data": [ + 1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1, + 1,0,0,0,0,0,0,0,0,1, + 1,1,1,1,1,1,1,1,1,1 + ], + "height": 4, + "id": 3, + "name": "collision", + "opacity": 1, + "type": "tilelayer", + "visible": false, + "width": 10, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 4, + "name": "buildings", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 5, + "name": "furniture", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 6, + "name": "locations", + "objects": [ + { + "height": 48, + "id": 1, + "name": "clinic", + "properties": [ + { + "name": "entry_x", + "type": "int", + "value": 3 + }, + { + "name": "entry_y", + "type": "int", + "value": 2 + }, + { + "name": "label", + "type": "string", + "value": "The Clinic" + } + ], + "rotation": 0, + "type": "amenity", + "visible": true, + "width": 128, + "x": 16, + "y": 0 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 7, + "name": "murals", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 8, + "name": "doors", + "objects": [ + { + "height": 16, + "id": 2, + "name": "clinic", + "properties": [ + { + "name": "kind", + "type": "string", + "value": "door" + }, + { + "name": "side", + "type": "string", + "value": "inside" + }, + { + "name": "to_map", + "type": "string", + "value": "metro" + }, + { + "name": "to_x", + "type": "int", + "value": 89 + }, + { + "name": "to_y", + "type": "int", + "value": 15 + } + ], + "rotation": 0, + "type": "door", + "visible": true, + "width": 16, + "x": 16, + "y": 32 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 9, + "name": "interactables", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 10, + "name": "spots", + "objects": [ + { + "height": 16, + "id": 3, + "name": "clinic_doctor", + "properties": [ + { + "name": "facing", + "type": "string", + "value": "south" + } + ], + "rotation": 0, + "type": "standing", + "visible": true, + "width": 16, + "x": 96, + "y": 32 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 13, + "nextobjectid": 4, + "orientation": "orthogonal", + "properties": [ + { + "name": "building", + "type": "string", + "value": "clinic" + }, + { + "name": "floor", + "type": "int", + "value": 0 + }, + { + "name": "kind", + "type": "string", + "value": "floor" + }, + { + "name": "room", + "type": "string", + "value": "clinic" + }, + { + "name": "street", + "type": "string", + "value": "metro" + }, + { + "name": "street_x", + "type": "int", + "value": 88 + }, + { + "name": "street_y", + "type": "int", + "value": 13 + }, + { + "name": "view", + "type": "string", + "value": "side" + } + ], + "renderorder": "right-down", + "tiledversion": "1.11.2", + "tileheight": 16, + "tilesets": [ + { + "columns": 4, + "firstgid": 1, + "image": "../markers.png", + "imageheight": 16, + "imagewidth": 64, + "margin": 0, + "name": "markers", + "spacing": 0, + "tilecount": 4, + "tileheight": 16, + "tiles": [ + { + "id": 0, + "properties": [ + { + "name": "name", + "type": "string", + "value": "blocked" + } + ] + }, + { + "id": 1, + "properties": [ + { + "name": "name", + "type": "string", + "value": "solid" + } + ] + }, + { + "id": 2, + "properties": [ + { + "name": "name", + "type": "string", + "value": "stairs" + } + ] + }, + { + "id": 3, + "properties": [ + { + "name": "name", + "type": "string", + "value": "ladder" + } + ] + } + ], + "tilewidth": 16 + } + ], + "tilewidth": 16, + "type": "map", + "version": "1.10", + "width": 10 +} diff --git a/content/street/floors/coffee_shop_floor_0.tmj b/content/street/floors/coffee_shop_floor_0.tmj index 416eccb9..a58dda78 100644 --- a/content/street/floors/coffee_shop_floor_0.tmj +++ b/content/street/floors/coffee_shop_floor_0.tmj @@ -570,9 +570,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 23, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/coffee_shop_floor_1.tmj b/content/street/floors/coffee_shop_floor_1.tmj index a1244cf0..01cd0114 100644 --- a/content/street/floors/coffee_shop_floor_1.tmj +++ b/content/street/floors/coffee_shop_floor_1.tmj @@ -593,9 +593,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 27, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/coffee_shop_floor_2.tmj b/content/street/floors/coffee_shop_floor_2.tmj index a4a23172..602a367e 100644 --- a/content/street/floors/coffee_shop_floor_2.tmj +++ b/content/street/floors/coffee_shop_floor_2.tmj @@ -242,9 +242,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 8, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/gallery_floor_0.tmj b/content/street/floors/gallery_floor_0.tmj index a971e567..2a0cae3f 100644 --- a/content/street/floors/gallery_floor_0.tmj +++ b/content/street/floors/gallery_floor_0.tmj @@ -543,9 +543,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 20, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/gallery_floor_1.tmj b/content/street/floors/gallery_floor_1.tmj index a6c4b8b5..b679b0e8 100644 --- a/content/street/floors/gallery_floor_1.tmj +++ b/content/street/floors/gallery_floor_1.tmj @@ -613,9 +613,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 25, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/gallery_floor_2.tmj b/content/street/floors/gallery_floor_2.tmj index a9b8cdea..ecae85e2 100644 --- a/content/street/floors/gallery_floor_2.tmj +++ b/content/street/floors/gallery_floor_2.tmj @@ -577,9 +577,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 23, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_10_floor_0.tmj b/content/street/floors/house_10_floor_0.tmj index b1a2dadf..89937b6a 100644 --- a/content/street/floors/house_10_floor_0.tmj +++ b/content/street/floors/house_10_floor_0.tmj @@ -266,9 +266,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 6, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_10_floor_1.tmj b/content/street/floors/house_10_floor_1.tmj index 582c7c64..c9d93754 100644 --- a/content/street/floors/house_10_floor_1.tmj +++ b/content/street/floors/house_10_floor_1.tmj @@ -194,9 +194,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 4, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_1_floor_0.tmj b/content/street/floors/house_1_floor_0.tmj index c4bb643f..0555a9be 100644 --- a/content/street/floors/house_1_floor_0.tmj +++ b/content/street/floors/house_1_floor_0.tmj @@ -266,9 +266,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 6, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_1_floor_1.tmj b/content/street/floors/house_1_floor_1.tmj index e3370baf..e0d60e0f 100644 --- a/content/street/floors/house_1_floor_1.tmj +++ b/content/street/floors/house_1_floor_1.tmj @@ -194,9 +194,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 4, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_2_floor_0.tmj b/content/street/floors/house_2_floor_0.tmj index f1a7c2e1..6699b246 100644 --- a/content/street/floors/house_2_floor_0.tmj +++ b/content/street/floors/house_2_floor_0.tmj @@ -266,9 +266,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 6, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_2_floor_1.tmj b/content/street/floors/house_2_floor_1.tmj index efdb02c3..98804ee0 100644 --- a/content/street/floors/house_2_floor_1.tmj +++ b/content/street/floors/house_2_floor_1.tmj @@ -194,9 +194,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 4, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_3_floor_0.tmj b/content/street/floors/house_3_floor_0.tmj index 292c48b3..5fd631bf 100644 --- a/content/street/floors/house_3_floor_0.tmj +++ b/content/street/floors/house_3_floor_0.tmj @@ -266,9 +266,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 6, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_3_floor_1.tmj b/content/street/floors/house_3_floor_1.tmj index db241a92..7f7c8c71 100644 --- a/content/street/floors/house_3_floor_1.tmj +++ b/content/street/floors/house_3_floor_1.tmj @@ -194,9 +194,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 4, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_4_floor_0.tmj b/content/street/floors/house_4_floor_0.tmj index 88f7c37c..a7db4943 100644 --- a/content/street/floors/house_4_floor_0.tmj +++ b/content/street/floors/house_4_floor_0.tmj @@ -266,9 +266,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 6, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_4_floor_1.tmj b/content/street/floors/house_4_floor_1.tmj index 3ad64c8a..98a2ca8d 100644 --- a/content/street/floors/house_4_floor_1.tmj +++ b/content/street/floors/house_4_floor_1.tmj @@ -194,9 +194,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 4, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_5_floor_0.tmj b/content/street/floors/house_5_floor_0.tmj index 3273f3eb..092b7a6d 100644 --- a/content/street/floors/house_5_floor_0.tmj +++ b/content/street/floors/house_5_floor_0.tmj @@ -266,9 +266,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 6, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_5_floor_1.tmj b/content/street/floors/house_5_floor_1.tmj index 6da41897..8c0d2d2e 100644 --- a/content/street/floors/house_5_floor_1.tmj +++ b/content/street/floors/house_5_floor_1.tmj @@ -194,9 +194,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 4, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_6_floor_0.tmj b/content/street/floors/house_6_floor_0.tmj index 4c265473..a3ebc186 100644 --- a/content/street/floors/house_6_floor_0.tmj +++ b/content/street/floors/house_6_floor_0.tmj @@ -266,9 +266,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 6, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_6_floor_1.tmj b/content/street/floors/house_6_floor_1.tmj index 95175090..8667f659 100644 --- a/content/street/floors/house_6_floor_1.tmj +++ b/content/street/floors/house_6_floor_1.tmj @@ -194,9 +194,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 4, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_7_floor_0.tmj b/content/street/floors/house_7_floor_0.tmj index a4bb0844..5bade6b4 100644 --- a/content/street/floors/house_7_floor_0.tmj +++ b/content/street/floors/house_7_floor_0.tmj @@ -266,9 +266,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 6, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_7_floor_1.tmj b/content/street/floors/house_7_floor_1.tmj index 4e22e3f5..3e07b268 100644 --- a/content/street/floors/house_7_floor_1.tmj +++ b/content/street/floors/house_7_floor_1.tmj @@ -194,9 +194,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 4, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_8_floor_0.tmj b/content/street/floors/house_8_floor_0.tmj index 3f35b2e0..4c11ac22 100644 --- a/content/street/floors/house_8_floor_0.tmj +++ b/content/street/floors/house_8_floor_0.tmj @@ -266,9 +266,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 6, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_8_floor_1.tmj b/content/street/floors/house_8_floor_1.tmj index 6f412581..241c57d5 100644 --- a/content/street/floors/house_8_floor_1.tmj +++ b/content/street/floors/house_8_floor_1.tmj @@ -194,9 +194,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 4, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_9_floor_0.tmj b/content/street/floors/house_9_floor_0.tmj index 8a6fe869..283e0727 100644 --- a/content/street/floors/house_9_floor_0.tmj +++ b/content/street/floors/house_9_floor_0.tmj @@ -266,9 +266,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 6, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/house_9_floor_1.tmj b/content/street/floors/house_9_floor_1.tmj index a42a1d05..d1b38b9c 100644 --- a/content/street/floors/house_9_floor_1.tmj +++ b/content/street/floors/house_9_floor_1.tmj @@ -194,9 +194,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 4, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/maker_space_floor_0.tmj b/content/street/floors/maker_space_floor_0.tmj index ed2c1668..6f9c5bc8 100644 --- a/content/street/floors/maker_space_floor_0.tmj +++ b/content/street/floors/maker_space_floor_0.tmj @@ -588,9 +588,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 24, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/maker_space_floor_1.tmj b/content/street/floors/maker_space_floor_1.tmj index 96da6cd7..d0b246d2 100644 --- a/content/street/floors/maker_space_floor_1.tmj +++ b/content/street/floors/maker_space_floor_1.tmj @@ -461,9 +461,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 18, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/maker_space_floor_2.tmj b/content/street/floors/maker_space_floor_2.tmj index 66059a91..4c7ac8f5 100644 --- a/content/street/floors/maker_space_floor_2.tmj +++ b/content/street/floors/maker_space_floor_2.tmj @@ -351,9 +351,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 13, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/noodle_bar_floor_0.tmj b/content/street/floors/noodle_bar_floor_0.tmj index dd7455fa..d34b87ea 100644 --- a/content/street/floors/noodle_bar_floor_0.tmj +++ b/content/street/floors/noodle_bar_floor_0.tmj @@ -550,9 +550,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 23, "orientation": "orthogonal", "properties": [ diff --git a/content/street/floors/noodle_bar_floor_1.tmj b/content/street/floors/noodle_bar_floor_1.tmj index 7e644781..11b42e63 100644 --- a/content/street/floors/noodle_bar_floor_1.tmj +++ b/content/street/floors/noodle_bar_floor_1.tmj @@ -507,9 +507,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 23, "orientation": "orthogonal", "properties": [ diff --git a/content/street/high_street.tmj b/content/street/high_street.tmj index e0e1afb7..e0ebffd1 100644 --- a/content/street/high_street.tmj +++ b/content/street/high_street.tmj @@ -1158,9 +1158,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 47, "orientation": "orthogonal", "properties": [ diff --git a/content/street/kapok_walk.tmj b/content/street/kapok_walk.tmj index 3873b1c3..329c9fd8 100644 --- a/content/street/kapok_walk.tmj +++ b/content/street/kapok_walk.tmj @@ -515,9 +515,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 14, "orientation": "orthogonal", "properties": [ diff --git a/content/street/lychee_lane.tmj b/content/street/lychee_lane.tmj index 3bc9cbeb..9d24adc3 100644 --- a/content/street/lychee_lane.tmj +++ b/content/street/lychee_lane.tmj @@ -824,9 +824,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 29, "orientation": "orthogonal", "properties": [ diff --git a/content/street/metro.tmj b/content/street/metro.tmj new file mode 100644 index 00000000..129998d7 --- /dev/null +++ b/content/street/metro.tmj @@ -0,0 +1,1094 @@ +{ + "compressionlevel": -1, + "height": 20, + "infinite": false, + "layers": [ + { + "data": [ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2 + ], + "height": 20, + "id": 1, + "name": "solid", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 110, + "x": 0, + "y": 0 + }, + { + "data": [ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ], + "height": 20, + "id": 2, + "name": "climbable", + "opacity": 1, + "type": "tilelayer", + "visible": true, + "width": 110, + "x": 0, + "y": 0 + }, + { + "data": [ + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ], + "height": 20, + "id": 3, + "name": "collision", + "opacity": 1, + "type": "tilelayer", + "visible": false, + "width": 110, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 4, + "name": "buildings", + "objects": [ + { + "height": 48, + "id": 1, + "name": "clinic", + "properties": [ + { + "name": "interior", + "type": "bool", + "value": true + }, + { + "name": "label", + "type": "string", + "value": "The Clinic" + }, + { + "name": "storeys", + "type": "int", + "value": 1 + } + ], + "rotation": 0, + "type": "building", + "visible": true, + "width": 160, + "x": 1408, + "y": 208 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 5, + "name": "furniture", + "objects": [ + { + "height": 64, + "id": 2, + "name": "metro_steps", + "properties": [], + "rotation": 0, + "type": "metro_steps", + "visible": true, + "width": 64, + "x": 64, + "y": 144 + }, + { + "height": 64, + "id": 3, + "name": "metro_steps", + "properties": [], + "rotation": 0, + "type": "metro_steps", + "visible": true, + "width": 64, + "x": 304, + "y": 192 + }, + { + "height": 32, + "id": 4, + "name": "catwalk", + "properties": [], + "rotation": 0, + "type": "catwalk", + "visible": true, + "width": 32, + "x": 112, + "y": 112 + }, + { + "height": 16, + "id": 5, + "name": "turnstile", + "properties": [], + "rotation": 0, + "type": "turnstile", + "visible": true, + "width": 32, + "x": 176, + "y": 192 + }, + { + "height": 32, + "id": 6, + "name": "ticket_machine", + "properties": [], + "rotation": 0, + "type": "ticket_machine", + "visible": true, + "width": 16, + "x": 240, + "y": 176 + }, + { + "height": 32, + "id": 7, + "name": "scrap_stall", + "properties": [], + "rotation": 0, + "type": "scrap_stall", + "visible": true, + "width": 48, + "x": 400, + "y": 224 + }, + { + "height": 16, + "id": 8, + "name": "metro_bench", + "properties": [], + "rotation": 0, + "type": "metro_bench", + "visible": true, + "width": 32, + "x": 464, + "y": 240 + }, + { + "height": 48, + "id": 9, + "name": "train_carriage", + "properties": [], + "rotation": 0, + "type": "train_carriage", + "visible": true, + "width": 160, + "x": 528, + "y": 208 + }, + { + "height": 32, + "id": 10, + "name": "catwalk", + "properties": [], + "rotation": 0, + "type": "catwalk", + "visible": true, + "width": 128, + "x": 720, + "y": 160 + }, + { + "height": 16, + "id": 11, + "name": "crates", + "properties": [], + "rotation": 0, + "type": "crates", + "visible": true, + "width": 16, + "x": 704, + "y": 240 + }, + { + "height": 16, + "id": 12, + "name": "metro_bench", + "properties": [], + "rotation": 0, + "type": "metro_bench", + "visible": true, + "width": 32, + "x": 784, + "y": 240 + }, + { + "height": 128, + "id": 13, + "name": "tunnel_arch", + "properties": [], + "rotation": 0, + "type": "tunnel_arch", + "visible": true, + "width": 48, + "x": 880, + "y": 128 + }, + { + "height": 16, + "id": 14, + "name": "pipes", + "properties": [], + "rotation": 0, + "type": "pipes", + "visible": true, + "width": 192, + "x": 944, + "y": 192 + }, + { + "height": 16, + "id": 15, + "name": "rubble", + "properties": [], + "rotation": 0, + "type": "rubble", + "visible": true, + "width": 32, + "x": 960, + "y": 240 + }, + { + "height": 16, + "id": 16, + "name": "bedroll", + "properties": [], + "rotation": 0, + "type": "bedroll", + "visible": true, + "width": 32, + "x": 1040, + "y": 240 + }, + { + "height": 32, + "id": 17, + "name": "breaker_box", + "properties": [], + "rotation": 0, + "type": "breaker_box", + "visible": true, + "width": 16, + "x": 1152, + "y": 224 + }, + { + "height": 32, + "id": 18, + "name": "catwalk", + "properties": [], + "rotation": 0, + "type": "catwalk", + "visible": true, + "width": 48, + "x": 1216, + "y": 192 + }, + { + "height": 32, + "id": 19, + "name": "catwalk", + "properties": [], + "rotation": 0, + "type": "catwalk", + "visible": true, + "width": 32, + "x": 1296, + "y": 160 + }, + { + "height": 32, + "id": 20, + "name": "catwalk", + "properties": [], + "rotation": 0, + "type": "catwalk", + "visible": true, + "width": 32, + "x": 1360, + "y": 128 + }, + { + "height": 16, + "id": 21, + "name": "rubble", + "properties": [], + "rotation": 0, + "type": "rubble", + "visible": true, + "width": 32, + "x": 1264, + "y": 240 + }, + { + "height": 64, + "id": 22, + "name": "freight_lift", + "properties": [], + "rotation": 0, + "type": "freight_lift", + "visible": true, + "width": 32, + "x": 1600, + "y": 192 + }, + { + "height": 32, + "id": 23, + "name": "dead_terminal", + "properties": [], + "rotation": 0, + "type": "dead_terminal", + "visible": true, + "width": 16, + "x": 1664, + "y": 224 + }, + { + "height": 32, + "id": 24, + "name": "catwalk", + "properties": [], + "rotation": 0, + "type": "catwalk", + "visible": true, + "width": 64, + "x": 1696, + "y": 192 + }, + { + "height": 16, + "id": 25, + "name": "crates", + "properties": [], + "rotation": 0, + "type": "crates", + "visible": true, + "width": 16, + "x": 1728, + "y": 192 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 6, + "name": "locations", + "objects": [ + { + "height": 48, + "id": 26, + "name": "scrap_stall", + "properties": [ + { + "name": "entry_x", + "type": "int", + "value": 28 + }, + { + "name": "entry_y", + "type": "int", + "value": 15 + }, + { + "name": "label", + "type": "string", + "value": "The scrap trader's stall" + } + ], + "rotation": 0, + "type": "amenity", + "visible": true, + "width": 80, + "x": 384, + "y": 208 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 7, + "name": "murals", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 8, + "name": "doors", + "objects": [ + { + "height": 16, + "id": 27, + "name": "clinic", + "properties": [ + { + "name": "kind", + "type": "string", + "value": "door" + }, + { + "name": "side", + "type": "string", + "value": "outside" + }, + { + "name": "to_map", + "type": "string", + "value": "clinic_floor_0" + }, + { + "name": "to_x", + "type": "int", + "value": 1 + }, + { + "name": "to_y", + "type": "int", + "value": 2 + } + ], + "rotation": 0, + "type": "door", + "visible": true, + "width": 16, + "x": 1424, + "y": 240 + }, + { + "height": 16, + "id": 28, + "name": "banyan_row_to_metro", + "properties": [ + { + "name": "kind", + "type": "string", + "value": "door" + }, + { + "name": "side", + "type": "string", + "value": "inside" + }, + { + "name": "to_map", + "type": "string", + "value": "banyan_row" + }, + { + "name": "to_x", + "type": "int", + "value": 62 + }, + { + "name": "to_y", + "type": "int", + "value": 18 + } + ], + "rotation": 0, + "type": "door", + "visible": true, + "width": 16, + "x": 32, + "y": 144 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 9, + "name": "interactables", + "objects": [ + { + "height": 32, + "id": 29, + "name": "metro_plug", + "properties": [ + { + "name": "label", + "type": "string", + "value": "Maintenance plug" + }, + { + "name": "stand", + "type": "string", + "value": "72,15" + } + ], + "rotation": 0, + "type": "machine", + "visible": true, + "width": 16, + "x": 1152, + "y": 224 + }, + { + "height": 64, + "id": 30, + "name": "metro_lift", + "properties": [ + { + "name": "label", + "type": "string", + "value": "Maintenance lift" + }, + { + "name": "stand", + "type": "string", + "value": "100,15 101,15" + } + ], + "rotation": 0, + "type": "machine", + "visible": true, + "width": 32, + "x": 1600, + "y": 192 + }, + { + "height": 32, + "id": 31, + "name": "metro_terminal", + "properties": [ + { + "name": "label", + "type": "string", + "value": "Dead terminal" + }, + { + "name": "stand", + "type": "string", + "value": "104,15" + } + ], + "rotation": 0, + "type": "machine", + "visible": true, + "width": 16, + "x": 1664, + "y": 224 + }, + { + "height": 16, + "id": 32, + "name": "metro_high_ledge", + "properties": [ + { + "name": "label", + "type": "string", + "value": "High ledge" + }, + { + "name": "stand", + "type": "string", + "value": "106,12 107,12 108,12 109,12" + } + ], + "rotation": 0, + "type": "machine", + "visible": true, + "width": 64, + "x": 1696, + "y": 192 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 10, + "name": "spots", + "objects": [ + { + "height": 16, + "id": 33, + "name": "metro_scrap_trader", + "properties": [ + { + "name": "facing", + "type": "string", + "value": "south" + } + ], + "rotation": 0, + "type": "standing", + "visible": true, + "width": 16, + "x": 416, + "y": 240 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [ + { + "height": 320, + "id": 34, + "name": "metro_entrance", + "properties": [ + { + "name": "lit", + "type": "bool", + "value": true + } + ], + "rotation": 0, + "type": "light", + "visible": true, + "width": 304, + "x": 0, + "y": 0 + }, + { + "height": 192, + "id": 35, + "name": "metro_vault", + "properties": [ + { + "name": "lit", + "type": "bool", + "value": false + } + ], + "rotation": 0, + "type": "light", + "visible": true, + "width": 624, + "x": 304, + "y": 0 + }, + { + "height": 128, + "id": 36, + "name": "metro_platform", + "properties": [ + { + "name": "lit", + "type": "bool", + "value": true + } + ], + "rotation": 0, + "type": "light", + "visible": true, + "width": 624, + "x": 304, + "y": 192 + }, + { + "height": 320, + "id": 37, + "name": "metro_tunnel_west", + "properties": [ + { + "name": "lit", + "type": "bool", + "value": false + } + ], + "rotation": 0, + "type": "light", + "visible": true, + "width": 208, + "x": 928, + "y": 0 + }, + { + "height": 320, + "id": 38, + "name": "metro_maintenance", + "properties": [ + { + "name": "lit", + "type": "bool", + "value": true + } + ], + "rotation": 0, + "type": "light", + "visible": true, + "width": 48, + "x": 1136, + "y": 0 + }, + { + "height": 320, + "id": 39, + "name": "metro_tunnel_east", + "properties": [ + { + "name": "lit", + "type": "bool", + "value": false + } + ], + "rotation": 0, + "type": "light", + "visible": true, + "width": 208, + "x": 1184, + "y": 0 + }, + { + "height": 320, + "id": 40, + "name": "metro_depot", + "properties": [ + { + "name": "lit", + "type": "bool", + "value": true + } + ], + "rotation": 0, + "type": "light", + "visible": true, + "width": 368, + "x": 1392, + "y": 0 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [ + { + "height": 16, + "id": 41, + "name": "metro_secret_stairwell", + "properties": [ + { + "name": "money", + "type": "float", + "value": 25.0 + } + ], + "rotation": 0, + "type": "secret", + "visible": true, + "width": 16, + "x": 128, + "y": 112 + }, + { + "height": 16, + "id": 42, + "name": "metro_salvage_1", + "properties": [], + "rotation": 0, + "type": "salvage", + "visible": true, + "width": 16, + "x": 608, + "y": 192 + }, + { + "height": 16, + "id": 43, + "name": "metro_secret_catwalk", + "properties": [ + { + "name": "money", + "type": "float", + "value": 40.0 + } + ], + "rotation": 0, + "type": "secret", + "visible": true, + "width": 16, + "x": 832, + "y": 160 + }, + { + "height": 16, + "id": 44, + "name": "metro_salvage_2", + "properties": [], + "rotation": 0, + "type": "salvage", + "visible": true, + "width": 16, + "x": 1008, + "y": 240 + }, + { + "height": 16, + "id": 45, + "name": "metro_salvage_3", + "properties": [], + "rotation": 0, + "type": "salvage", + "visible": true, + "width": 16, + "x": 1088, + "y": 240 + }, + { + "height": 16, + "id": 46, + "name": "metro_salvage_4", + "properties": [], + "rotation": 0, + "type": "salvage", + "visible": true, + "width": 16, + "x": 1232, + "y": 192 + }, + { + "height": 16, + "id": 47, + "name": "metro_secret_alcove", + "properties": [ + { + "name": "item", + "type": "string", + "value": "ration_pack" + }, + { + "name": "item_kind", + "type": "string", + "value": "food" + }, + { + "name": "item_name", + "type": "string", + "value": "Ration pack" + }, + { + "name": "item_weight", + "type": "float", + "value": 0.5 + } + ], + "rotation": 0, + "type": "secret", + "visible": true, + "width": 16, + "x": 1376, + "y": 128 + }, + { + "height": 16, + "id": 48, + "name": "metro_salvage_5", + "properties": [], + "rotation": 0, + "type": "salvage", + "visible": true, + "width": 16, + "x": 1328, + "y": 240 + }, + { + "height": 16, + "id": 49, + "name": "metro_salvage_6", + "properties": [], + "rotation": 0, + "type": "salvage", + "visible": true, + "width": 16, + "x": 1728, + "y": 192 + } + ], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + } + ], + "nextlayerid": 13, + "nextobjectid": 50, + "orientation": "orthogonal", + "properties": [ + { + "name": "kind", + "type": "string", + "value": "street" + }, + { + "name": "label", + "type": "string", + "value": "The Old Metro" + }, + { + "name": "view", + "type": "string", + "value": "side" + } + ], + "renderorder": "right-down", + "tiledversion": "1.11.2", + "tileheight": 16, + "tilesets": [ + { + "columns": 4, + "firstgid": 1, + "image": "markers.png", + "imageheight": 16, + "imagewidth": 64, + "margin": 0, + "name": "markers", + "spacing": 0, + "tilecount": 4, + "tileheight": 16, + "tiles": [ + { + "id": 0, + "properties": [ + { + "name": "name", + "type": "string", + "value": "blocked" + } + ] + }, + { + "id": 1, + "properties": [ + { + "name": "name", + "type": "string", + "value": "solid" + } + ] + }, + { + "id": 2, + "properties": [ + { + "name": "name", + "type": "string", + "value": "stairs" + } + ] + }, + { + "id": 3, + "properties": [ + { + "name": "name", + "type": "string", + "value": "ladder" + } + ] + } + ], + "tilewidth": 16 + } + ], + "tilewidth": 16, + "type": "map", + "version": "1.10", + "width": 110 +} diff --git a/content/street/preview.png b/content/street/preview.png index faa99019..135f756d 100644 Binary files a/content/street/preview.png and b/content/street/preview.png differ diff --git a/content/street/rooftops.tmj b/content/street/rooftops.tmj index c6e0b4f2..59f47c12 100644 --- a/content/street/rooftops.tmj +++ b/content/street/rooftops.tmj @@ -455,9 +455,31 @@ "visible": true, "x": 0, "y": 0 + }, + { + "draworder": "topdown", + "id": 11, + "name": "light", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 + }, + { + "draworder": "topdown", + "id": 12, + "name": "pickups", + "objects": [], + "opacity": 1, + "type": "objectgroup", + "visible": true, + "x": 0, + "y": 0 } ], - "nextlayerid": 11, + "nextlayerid": 13, "nextobjectid": 15, "orientation": "orthogonal", "properties": [ diff --git a/server/simile/papercraft/build.py b/server/simile/papercraft/build.py index 10a966ba..0ad7b933 100644 --- a/server/simile/papercraft/build.py +++ b/server/simile/papercraft/build.py @@ -292,7 +292,10 @@ def finish(self, piece: Painting) -> dict: image = band_rows(image) height = piece.height or image.height / PPM elif piece.kind in ("layer", "strip", "ground"): - mask = self.key(raw) if piece.kind == "ground" else self.mask(raw) + if "above" in piece.cut: + mask = cutout.clean(cutout.key(raw, bg=top_backdrop(raw))) + else: + mask = self.key(raw) if piece.kind == "ground" else self.mask(raw) if piece.kind != "strip": mask = cutout.skyline(mask) image = tileable(trim_rows(cutout.unmix(raw, mask))) diff --git a/server/simile/papercraft/catalogue.py b/server/simile/papercraft/catalogue.py index d4bf7997..162d126d 100644 --- a/server/simile/papercraft/catalogue.py +++ b/server/simile/papercraft/catalogue.py @@ -6,9 +6,10 @@ from simile.papercraft.fronts import GREY, Front, backdrop, unit_plans, units from simile.papercraft.layout import Layout, load_layout -from simile.papercraft.painter import styled +from simile.papercraft.painter import cartoon, styled from simile.papercraft.sketches import SKETCHES from simile.papercraft.village import ( + CARTOON_PAPERS, CLAY, GROUNDS, LOOKS, @@ -46,6 +47,24 @@ ) PAPER_CARD = "all matt paper and card, with visible paper fibres and cut edges, not glossy" NOTHING_BUT_PLANTS = " Only plants, fences and garden things: no houses, no buildings, no people." +CARTOON_LEAD = "Make a new picture in exactly the cartoon style of the reference picture: " +CARTOON_REDRAW = ( + "Redraw image 1 as {subject}, hand-drawn in the cartoon style of image 2: thick scratchy dark " + "ink outlines around every shape, flat colours with simple soft shading, and a little wear. " + "Keep every shape exactly where it is, with the same outlines and colours, seen exactly from " + "the side with no perspective, on a plain {back} background, and no shadow on the background." +) +CARTOON_TEXTURE = ( + "A flat hand-drawn texture that fills the whole frame edge to edge: {}. Drawn with scratchy " + "dark ink lines and flat colours, a few smudges and scuffs, even light, no objects, no edges." +) +CARTOON_PAPER_LEAD = ( + "Make a flat texture in the colours and cartoon style of the reference picture, showing none " + "of its objects: " +) +TUNNELS = "cartoon/street" +TUNNEL_ROOMS = "cartoon/interior" +"""The tunnels' references, from #329's revised cartoon.""" @dataclass(frozen=True) @@ -80,7 +99,7 @@ class Painting: reseed: int = 0 """Added to the seed, to paint a rejected painting again from the same prompt.""" cut: tuple[str, ...] = () - """For a sprite, what else to cut away: `shadows` and `gaps` (see `cutout.tidy`).""" + """What else to cut: a sprite's `shadows` and `gaps`, or `above` a ground piece's top row.""" @property def seed(self) -> int: @@ -100,6 +119,19 @@ def paper(key: str, pattern: str, size: tuple[int, int] = (768, 512), **kw) -> P return Painting(f"papers/{key}", "paper", prompt, size, ("house",), **kw) +def cartoon_paper(key: str, pattern: str, size: tuple[int, int] = (768, 512)) -> Painting: + prompt = cartoon(CARTOON_PAPER_LEAD + CARTOON_TEXTURE.format(pattern)) + return Painting(f"papers/{key}", "paper", prompt, size, (TUNNEL_ROOMS,)) + + +def tunnel_layer( + key: str, kind: Kind, subject: str, size: tuple[int, int], height: float, **kw +) -> Painting: + """A layer of the tunnels, in the cartoon.""" + prompt = cartoon(CARTOON_LEAD + subject) + return Painting(f"layers/{key}", kind, prompt, size, (TUNNELS,), height, **kw) + + SKY = Painting( "layers/sky", "paper", @@ -261,6 +293,75 @@ def paper(key: str, pattern: str, size: tuple[int, int] = (768, 512), **kw) -> P 2.0, ), ] +NO_STREET = " No people, no trains, no trees, no sky, no buildings." +NEAR_WALL = 4.6 +"""Metres: tall enough to hide the foot of the mid layer, which the client puts 2.75 m up.""" +TUNNEL_LAYERS = [ + tunnel_layer( + "metro-vault", + "paper", + "A plain dark backdrop of the inside of a huge old underground metro hall: a curved " + "vault of dim cream tiles between heavy dark steel ribs, fading into deep shadow towards " + "the top, with a few hanging cables. Only the vault: no floor, no platform." + NO_STREET, + (1536, 768), + 12.0, + ), + tunnel_layer( + "far-metro", + "layer", + "A long strip of the upper vault of an old metro hall seen from inside: a row of big " + "round tiled arches between dark steel ribs, with cable trays and dead lamps hanging " + "from them, in dim cream, grey and teal, lying on a plain flat white background with " + "white space above and below it." + NO_STREET, + (1536, 512), + 5.0, + ), + tunnel_layer( + "mid-metro", + "strip", + "A long run of the tiled back wall of an old metro station seen straight from the front: " + "a wall of dim cream tiles with a dark teal band, a row of tall round arches, empty " + "frames where posters once hung, old pipes and dead lamps, all standing on one straight " + "baseline, on a plain flat white background with white space above it. No signs, no " + "tracks, no rails." + NO_STREET, + (1536, 640), + 9.0, + ), + tunnel_layer( + "near-metro", + "ground", + "A long run of the lower back wall of an old metro platform seen straight from the front: " + "a solid wall of grimy cream tiles from the ground up to a dark teal band along its " + "straight top edge, with old pipes and cable trays running along it, and sacks, crates " + "and tarps piled at its foot, running from the left edge of the frame to the right edge, " + "on a plain flat white background with white space above it. Only the wall, pipes and " + "junk." + NO_STREET, + (1536, 768), + NEAR_WALL, + cut=("above",), + ), + tunnel_layer( + "ground-metro", + "ground", + "A long horizontal strip of an old metro platform's floor seen exactly from the side: " + "worn grey concrete paving on top with a faded yellow safety line along it, and a thick " + "band of dark grimy concrete below, running from the left edge of the frame to the right " + "edge, on a plain white background.", + (1536, 384), + 2.6, + ), + tunnel_layer( + "front-metro", + "strip", + "A long low row of old metro rails on dark wooden sleepers with gravel, rubble and loose " + "cables, seen straight from the front, " + + STRIP + + " Only rails, rubble and cables." + + NO_STREET, + (1536, 512), + 2.0, + ), +] SLAB = paper( "slab", "the cut edge of thick corrugated brown cardboard, a long band", (1024, 256), height=0.3 ) @@ -288,6 +389,8 @@ class Item: folder: str = "furniture" variants: dict[str, str] = field(default_factory=dict) """Colour variants: name, and what's recoloured to what.""" + cartoon: bool = False + """Painted in the tunnels' cartoon rather than papercraft.""" STREET = "street" @@ -513,6 +616,146 @@ class Item: sketch="lift", folder="platforms", ), + "metro_entrance": Item( + "a small narrow old metro station entrance seen straight from the front: a low arched " + "canopy of dark green painted iron and glass on two slim iron posts, over a dark doorway " + "where a stairway goes down, with a small round glowing sign showing a simple train " + "pictogram on top of the canopy", + 2.3, + (512, 896), + ref=STREET, + ), + "train_carriage": Item( + "an abandoned old metro train carriage seen exactly from the side: a long boxy body of " + "faded cream painted steel with a teal stripe, a row of dark windows, two pairs of " + "closed sliding doors, a flat grey roof with low vents, and its bogies and wheels", + 3.36, + ref=TUNNELS, + sketch="train_carriage", + cut=("outline",), + cartoon=True, + ), + "catwalk": Item( + "a steel maintenance catwalk seen exactly from the side: a deck of steel grating on a " + "riveted plate girder, a thin steel handrail on posts above it, and an angled steel " + "strut under each end", + 2.16, + ref=TUNNELS, + sketch="catwalk", + folder="platforms", + stretch=True, + cut=("outline",), + cartoon=True, + ), + "metro_steps": Item( + "a flight of old metro station steps seen exactly from the side, rising to the left: " + "worn concrete treads with dark steel nosings, over a solid side wall of grimy cream " + "tiles", + 3.36, + ref=TUNNELS, + sketch="metro_steps", + cut=("outline",), + cartoon=True, + ), + "freight_lift": Item( + "an old dead freight lift seen from the front: its car behind a folding steel grille " + "gate, in a riveted steel frame, under a winding wheel in a steel housing", + 4.4, + ref=TUNNELS, + sketch="freight_lift", + cut=("outline",), + cartoon=True, + ), + "scrap_stall": Item( + "a scrap trader's stall: a battered trestle table piled with salvaged electronics, coils " + "of copper wire, gears and old circuit boards, under a patched canvas awning on two " + "poles, with a crate of parts beside it", + 2.2, + (896, 768), + ref=TUNNEL_ROOMS, + cartoon=True, + ), + "turnstile": Item( + "a pair of old metro turnstiles side by side: two waist-high posts of scuffed stainless " + "steel, each with three rotating metal bars sticking out to one side and a little round " + "card reader on top, dead and dusty", + 1.1, + (768, 512), + ref=TUNNEL_ROOMS, + cartoon=True, + ), + "ticket_machine": Item( + "an old metro ticket machine: a tall cabinet of scuffed steel and faded teal panels with " + "a dark dead screen, a coin slot and a little ticket tray", + 1.9, + (512, 896), + ref=TUNNEL_ROOMS, + cartoon=True, + ), + "metro_bench": Item( + "an old metro platform bench seen straight from the front: a row of three moulded orange " + "plastic seats on a steel frame, scuffed and dusty", + 0.9, + (896, 512), + ref=TUNNEL_ROOMS, + cartoon=True, + ), + "dead_terminal": Item( + "an old metro control console: a waist-high cabinet of grimy grey steel with a dark " + "cracked screen on top, a row of unlit buttons and a small round data port, dead", + 1.5, + (512, 768), + ref=TUNNEL_ROOMS, + cartoon=True, + ), + "breaker_box": Item( + "a maintenance power point: a grey steel breaker box on a short steel post, with a " + "yellow and black hazard stripe, a big power socket, a coiled cable and a small " + "glowing green light", + 1.4, + (512, 768), + ref=TUNNEL_ROOMS, + cartoon=True, + ), + "crates": Item( + "a stack of two battered wooden crates", 1.0, (640, 640), ref=TUNNELS, cartoon=True + ), + "rubble": Item( + "a low heap of broken concrete chunks, bricks and twisted rusty rebar, twice as wide as " + "it is tall", + 0.7, + (768, 448), + ref=TUNNELS, + cartoon=True, + ), + "bedroll": Item( + "an abandoned sleeping spot on the floor seen from the side: a rolled-out stained " + "sleeping bag on flattened cardboard, with a tin mug and a candle stub", + 0.45, + (768, 384), + side=True, + ref=TUNNEL_ROOMS, + cartoon=True, + ), + "pipes": Item( + "a long run of three old pipes one above the other along a wall, of rusty iron and " + "faded green-painted steel, held by plain brackets, with no valves", + 0.8, + (1280, 352), + stretch=True, + ref=TUNNELS, + cartoon=True, + ), + "tunnel_arch": Item( + "a tall archway at the mouth of an old metro tunnel, seen straight from the front: two " + "square pillars of grimy cream tiles joined by a round concrete arch, with cables and a " + "dead lamp on it, and nothing in the opening", + 8.8, + (512, 1280), + ref=TUNNELS, + cut=("gaps",), + cartoon=True, + ), } EXTRAS: dict[str, Item] = { "armchair": Item( @@ -637,10 +880,13 @@ class Item: def item(name: str, spec: Item, layout: Layout) -> Painting: key = f"{spec.folder}/{name}" + style, lead, redraw = ( + (cartoon, CARTOON_LEAD, CARTOON_REDRAW) if spec.cartoon else (styled, LEAD, REDRAW) + ) if spec.sketch: sketch = SKETCHES[spec.sketch]() back = "medium grey" if sketch.getpixel((0, 0)) == GREY else "white" - prompt = styled(REDRAW.format(subject=spec.subject, back=back)) + prompt = style(redraw.format(subject=spec.subject, back=back)) refs = (spec.ref,) size, height = sketch.size, spec.height return Painting(key, "sprite", prompt, size, refs, height, sketch=spec.sketch, cut=spec.cut) @@ -648,7 +894,7 @@ def item(name: str, spec: Item, layout: Layout) -> Painting: backdrop_text = SIDE_SPRITE if spec.side else SPRITE if spec.grey: backdrop_text = backdrop_text.replace("plain flat white", "plain flat medium grey") - prompt = styled(LEAD + f"2D game asset: {spec.subject}. {backdrop_text}") + prompt = style(lead + f"2D game asset: {spec.subject}. {backdrop_text}") return Painting( key, "sprite", prompt, size, (spec.ref,), spec.height, reseed=spec.reseed, cut=spec.cut ) @@ -706,8 +952,27 @@ def _side(unit: Front) -> str: return "left" if middle < 0.4 else "right" if middle > 0.6 else "middle" +def cartoon_front_prompt(look: Look, unit: Front) -> str: + """A front in the tunnels' cartoon, naming only what its plan has.""" + extras = ["a flat roof", "small windows lit from inside"] + parts = ["the walls", "the roof", "every window"] + if unit.door is not None: + extras += ["a glass door", "a glowing neon medical cross pictogram over the door"] + parts += ["the glass door", "the small round wall light"] + ground = "grey" if backdrop(unit) == GREY else "white" + return cartoon( + f"Redraw image 1 as the front of {look.look}, with {', '.join(extras)}, hand-drawn in " + "the cartoon style: thick scratchy dark ink outlines around every shape, flat colours " + "with simple soft shading, and a little wear. Keep every shape exactly where it is, with " + f"the same outlines and colours: {', '.join(parts)}. Seen straight from the front, a " + f"flat elevation with no perspective, on a plain {ground} background. No people." + ) + + def front_prompt(look: Look, unit: Front) -> str: """The prompt for one unit of a front, naming only what its plan has.""" + if look.cartoon: + return cartoon_front_prompt(look, unit) roof = ( "a flat roof with a low parapet" if look.roof_shape == "flat" @@ -759,10 +1024,11 @@ def night_prompt(look: Look, unit: Front) -> str: if unit.lanterns: lit.append("the paper lanterns") ground = "grey" if backdrop(unit) == GREY else "white" - return styled( - f"The same picture at night: warm yellow light glows from {', '.join(lit)}, the paper " - f"walls are in deep blue shadow, and the background stays plain {ground}. Keep every " - "shape exactly where it is, and add nothing." + style, walls = (cartoon, "walls") if look.cartoon else (styled, "paper walls") + return style( + f"The same picture at night: warm yellow light glows from {', '.join(lit)}, the {walls} " + f"are in deep blue shadow, and the background stays plain {ground}. Keep every shape " + "exactly where it is, and add nothing." ) @@ -789,13 +1055,17 @@ def fronts(layout: Layout) -> list[Painting]: def room_papers(layout: Layout) -> list[Painting]: used = sorted({p for floors in papers(layout).values() for p in floors}) - return [paper(key, PAPERS[key]) for key in used] + [SLAB] + made = [ + cartoon_paper(key, PAPERS[key]) if key in CARTOON_PAPERS else paper(key, PAPERS[key]) + for key in used + ] + return [*made, SLAB] def paintings(layout: Layout | None = None) -> list[Painting]: """Every painting, in the order they're painted: a redrawn painting after its base.""" layout = layout or load_layout() - return [*LAYERS, *room_papers(layout), *fronts(layout), *items(layout)] + return [*LAYERS, *TUNNEL_LAYERS, *room_papers(layout), *fronts(layout), *items(layout)] PARALLAX = {"sky": 0.04, "far": 0.2, "mid": 0.45, "near": 0.85, "ground": 1.0, "foreground": 1.3} diff --git a/server/simile/papercraft/cutout.py b/server/simile/papercraft/cutout.py index 458c0551..f6f3158c 100644 --- a/server/simile/papercraft/cutout.py +++ b/server/simile/papercraft/cutout.py @@ -64,12 +64,14 @@ def background(image: Image.Image) -> np.ndarray: return np.median(border, axis=0) -def key(image: Image.Image, lo: float = 10, hi: float = 34) -> Image.Image: +def key( + image: Image.Image, lo: float = 10, hi: float = 34, bg: np.ndarray | None = None +) -> Image.Image: """A colour key: the background is whatever near-background colour touches the border.""" import cv2 a = np.asarray(image.convert("RGB"), dtype=np.float32) - d = np.abs(a - background(image)).max(axis=2) + d = np.abs(a - (background(image) if bg is None else bg)).max(axis=2) near = (d < hi).astype(np.uint8) _, labels = cv2.connectedComponents(near, connectivity=4) edge = np.unique(np.concatenate([labels[0], labels[-1], labels[:, 0], labels[:, -1]])) diff --git a/server/simile/papercraft/fronts.py b/server/simile/papercraft/fronts.py index 5da45a94..55db25fd 100644 --- a/server/simile/papercraft/fronts.py +++ b/server/simile/papercraft/fronts.py @@ -74,6 +74,8 @@ class Front: one_shop: bool = False """In a terrace, only the unit with the door has shop windows, and the others windows with awnings: FLUX boards up the middle of a shop window with no door near it.""" + unit: float = UNIT + """The widest it's painted in one go.""" @property def rise(self) -> float: @@ -233,7 +235,7 @@ def canvas_size(front: Front, ppm: float | None = None) -> tuple[int, int]: def units(front: Front) -> list[tuple[float, Front]]: """`front` as a terrace of units no wider than `UNIT`, each a whole front of its own with its own roof, and where each starts. The cuts keep clear of the door, its lamp and murals.""" - count = int(np.ceil(front.width / UNIT)) + count = int(np.ceil(front.width / front.unit)) keep = [b for b in (front.door_box, front.lamp_box(), *front.murals) if b] cuts = [0.0] for i in range(1, count): diff --git a/server/simile/papercraft/painter.py b/server/simile/papercraft/painter.py index e7abacad..1c2a9bc8 100644 --- a/server/simile/papercraft/painter.py +++ b/server/simile/papercraft/painter.py @@ -19,6 +19,13 @@ "papercraft diorama made of cut coloured paper and cardboard layers, visible paper edges and " "fibres, soft drop shadows between layers, handmade crafty look" ) +CARTOON = ( + "hand-drawn 2D cartoon game art: thick dark brown ink outlines that are scratchy, uneven and " + "slightly broken, as if drawn with a worn dip pen, flat colour fills with simple soft " + "shading, a little worn and dirty with a few smudges, scuffs and stains, a slightly muted " + "palette, chunky rounded simplified shapes, bold readable silhouettes" +) +"""#329's revised cartoon, a little dirty: the tunnels' style.""" NO_TEXT = " No text, no lettering, no logos, no words on signs." REFERENCE_WIDTH = 768 @@ -28,9 +35,13 @@ def styled(subject: str) -> str: return f"{subject} Art style: {STYLE}.{NO_TEXT}" +def cartoon(subject: str) -> str: + """`subject` in the tunnels' cartoon, with the ban on lettering.""" + return f"{subject} Art style: {CARTOON}.{NO_TEXT}" + + def reference(name: str, width: int = REFERENCE_WIDTH) -> Image.Image: - """A reference picture (`street`, `house`, `rooftops`, `furniture` or `characters`) scaled to - `width`, so conditioning on it stays cheap.""" + """A reference picture from `references/`, scaled to `width` to keep conditioning cheap.""" image = Image.open(REFERENCES / f"{name}.jpg").convert("RGB") height = round(image.height * width / image.width / 16) * 16 return image.resize((width, height), Image.Resampling.LANCZOS) diff --git a/server/simile/papercraft/sketches.py b/server/simile/papercraft/sketches.py index 3861e889..69e4c8cb 100644 --- a/server/simile/papercraft/sketches.py +++ b/server/simile/papercraft/sketches.py @@ -158,6 +158,98 @@ def steps(rise: float = 2.2, run: float = 2.2, count: int = 8) -> Image.Image: return s.image +STEEL: Rgb = (112, 122, 120) +DARK_STEEL: Rgb = (64, 70, 72) +CREAM: Rgb = (222, 214, 186) +TEAL: Rgb = (58, 132, 130) +WINDOW: Rgb = (58, 68, 82) +HAZARD: Rgb = (232, 188, 52) + + +def train_carriage(length: float = 11.0, height: float = 3.36) -> Image.Image: + """A metro carriage's side on its bogies, its flat roof along the top edge to stand on.""" + s = Sheet(length, height, 120) + for x in (1.6, length - 1.6): + s.rect(x - 1.0, 0.3, x + 1.0, 0.5, DARK_STEEL) + for wheel in (x - 0.6, x + 0.6): + s.ellipse(wheel - 0.38, 0.0, wheel + 0.38, 0.76, IRON) + s.rect(0.05, 0.45, length - 0.05, height - 0.2, CREAM, DARK_STEEL) + s.rect(0.05, 1.05, length - 0.05, 1.3, TEAL) + s.rect(0.0, height - 0.2, length, height, STEEL, DARK_STEEL) + for x in np.arange(0.9, length - 0.5, 1.2): + s.rect(x, height - 0.17, x + 0.5, height - 0.05, DARK_STEEL) + doors = (length * 0.27, length * 0.73) + for x in np.arange(0.5, length - 0.6, 1.05): + if all(abs(x + 0.35 - d) > 1.0 for d in doors): + s.rect(x, 1.6, x + 0.75, 2.6, WINDOW, DARK_STEEL) + for d in doors: + for side in (-1, 1): + x0, x1 = sorted((d, d + side * 0.62)) + s.rect(x0, 0.5, x1, 2.75, CREAM, DARK_STEEL) + s.rect(x0 + 0.12, 1.6, x1 - 0.12, 2.55, WINDOW) + return s.image + + +def catwalk(length: float = 3.3, height: float = 2.16, deck: float = 1.16) -> Image.Image: + """A railed steel deck on a girder, to stretch, its deck `deck` up from the bottom.""" + s = Sheet(length, height, 200) + s.rect(0.0, deck - 0.6, length, deck - 0.08, STEEL, DARK_STEEL) + for x in np.arange(0.2, length, 0.55): + s.rect(x - 0.025, deck - 0.6, x + 0.025, deck - 0.08, DARK_STEEL) + s.rect(0.0, deck - 0.08, length, deck, DARK_STEEL) + for x in np.arange(0.05, length, 1.1): + s.rect(x, deck, x + 0.06, height - 0.04, DARK_STEEL) + for y in (deck + 0.5, height - 0.06): + s.rect(0.0, y, length, y + 0.05, STEEL, DARK_STEEL) + for x, out in ((0.12, 0.3), (length - 0.12, -0.3)): + s.polygon( + [(x, deck - 0.6), (x + out * 0.3, deck - 0.6), (x + out, 0.0), (x + out * 0.8, 0.0)], + DARK_STEEL, + ) + s.image.paste((255, 255, 255), (0, 0, round(s.x0), s.image.height)) + s.image.paste((255, 255, 255), (round(s.x0 + length * s.ppm), 0, s.image.width, s.image.height)) + return s.image + + +def metro_steps(run: float = 3.3, rise: float = 3.36, count: int = 12) -> Image.Image: + """Tiled steps rising to the left, a little fuller than the street's, to cover them.""" + s = Sheet(run, rise, 150) + h, d = rise / count, (run - 0.3) / count + profile = [(run, 0.0)] + for i in range(count): + x = run - i * d + profile += [(x, (i + 1) * h), (x - d, (i + 1) * h)] + s.polygon([*profile, (0.0, rise), (0.0, 0.0)], CREAM, DARK_STEEL) + for i in range(count): + x = run - i * d + s.rect(x - d, (i + 1) * h - 0.07, x, (i + 1) * h, DARK_STEEL) + for y in np.arange(0.42, rise - 0.3, 0.42): + s.rect(0.0, y - 0.015, run - (y // h) * d - 0.05, y + 0.015, (196, 188, 160)) + return s.image + + +def freight_lift(width: float = 2.2, height: float = 4.4) -> Image.Image: + """A dead freight lift's car behind a grille gate, under its winding gear.""" + s = Sheet(width, height, 180) + s.rect(0.15, 0.0, width - 0.15, 3.5, (44, 48, 52)) + s.rect(0.25, 0.1, width - 0.25, 2.5, (92, 86, 74), DARK_STEEL) + s.rect(0.25, 0.0, width - 0.25, 0.12, HAZARD) + for x in np.arange(0.3, width - 0.4, 0.3): + s.rect(x - 0.02, 0.12, x + 0.02, 2.5, STEEL) + for y in np.arange(0.12, 2.4, 0.6): + s.polygon([(x, y), (x + 0.05, y), (x + 0.3, y + 0.6), (x + 0.25, y + 0.6)], STEEL) + s.polygon([(x + 0.3, y), (x + 0.25, y), (x, y + 0.6), (x + 0.05, y + 0.6)], STEEL) + for x in (width / 2 - 0.25, width / 2 + 0.25): + s.rect(x - 0.015, 2.5, x + 0.015, 3.7, IRON) + s.rect(0.0, 0.0, 0.15, 3.6, STEEL, DARK_STEEL) + s.rect(width - 0.15, 0.0, width, 3.6, STEEL, DARK_STEEL) + s.rect(-0.05, 3.5, width + 0.05, 3.62, DARK_STEEL) + s.rect(0.2, 3.62, width - 0.2, height - 0.05, STEEL, DARK_STEEL) + s.ellipse(width / 2 - 0.35, 3.7, width / 2 + 0.35, 4.3, IRON) + s.ellipse(width / 2 - 0.12, 3.88, width / 2 + 0.12, 4.12, STEEL) + return s.image + + def bathtub() -> Image.Image: s = Sheet(1.7, 0.75, 400, GREY) s.polygon([(0.0, 0.62), (1.7, 0.62), (1.6, 0.2), (0.1, 0.2)], WHITE, (200, 196, 188)) @@ -197,6 +289,10 @@ def fridge() -> Image.Image: "steps": steps, "ledge": ledge, "rope_bridge": rope_bridge, + "train_carriage": train_carriage, + "catwalk": catwalk, + "metro_steps": metro_steps, + "freight_lift": freight_lift, "bathtub": bathtub, "washbasin": washbasin, "fridge": fridge, diff --git a/server/simile/papercraft/village.py b/server/simile/papercraft/village.py index 54636f6b..5d0da27f 100644 --- a/server/simile/papercraft/village.py +++ b/server/simile/papercraft/village.py @@ -3,7 +3,7 @@ from dataclasses import dataclass -from simile.papercraft.fronts import WINDOW, Box, Front, Rgb, Roof +from simile.papercraft.fronts import UNIT, WINDOW, Box, Front, Rgb, Roof from simile.papercraft.layout import Facade, Layout TILE = 1.1 @@ -35,6 +35,9 @@ class Street: "far-river", "mid-riverbank", "near-hedges", "ground-street", "front-reeds" ), "rooftops": Street("far-hills", "mid-rooftops", "near-walls", "ground-street", "front-plants"), + "metro": Street( + "far-metro", "mid-metro", "near-metro", "ground-metro", "front-metro", sky="metro-vault" + ), } GROUNDS = { "square": "ground-plaza", @@ -65,6 +68,12 @@ class Look: """Ask outright for the mural wall to stay empty, where FLUX fills it otherwise.""" reseed: int = 0 """Added to the seed, to paint a rejected front again.""" + cartoon: bool = False + """Painted in the tunnels' cartoon rather than papercraft.""" + plants: bool = True + """A potted plant each side of the door.""" + whole: bool = False + """Painted in one go however wide, for a front that isn't a terrace.""" WOOD: Rgb = (126, 88, 60) @@ -254,6 +263,20 @@ class Look: shop=(80, 120, 168), flower_boxes=BOXES, ), + "clinic": Look( + "a small back-alley clinic built into an old metro station office of cracked pale " + "green tiles", + (178, 204, 186), + (60, 84, 82), + (88, 92, 96), + "flat", + glass_door=True, + window=(1.3, 1.1), + cartoon=True, + plants=False, + whole=True, + reseed=1, + ), "clock_house": Look( "a three-storey townhouse of pale sandstone with a flat roof", (216, 204, 180), @@ -283,7 +306,11 @@ class Look: "loft-boards": "strips of pale pine-coloured card laid across like boards, with a few " "knots drawn in brown pencil", "plaster-white": "plain warm white plaster with a faint soft texture", + "clinic-tiles": "pale green-white wall tiles in a neat grid with dark grout, a few chipped " + "tiles and faint stains", } +CARTOON_PAPERS = frozenset({"clinic-tiles"}) +"""Papers painted in the tunnels' cartoon.""" ROOMS: dict[str, tuple[str, ...]] = { "living_room": ("living-ochre", "living-blue", "living-green"), "bedroom": ("bedroom-rose", "bedroom-sage", "bedroom-lilac"), @@ -295,6 +322,7 @@ class Look: "workshop": ("block-grey",), "loft": ("loft-boards",), "gallery": ("plaster-white",), + "clinic": ("clinic-tiles",), } """Each kind of room's papers. Rooms of one kind take turns, by their building's place in the village, so neighbours differ.""" @@ -348,8 +376,10 @@ def front(facade: Facade, look: Look) -> Front: flower_boxes=look.flower_boxes, murals=tuple(murals), glass_door=look.glass_door, + plants=look.plants, window=look.window, one_shop=look.one_shop, + unit=width if look.whole else UNIT, ) diff --git a/server/simile/street/metro.py b/server/simile/street/metro.py new file mode 100644 index 00000000..4dde6829 --- /dev/null +++ b/server/simile/street/metro.py @@ -0,0 +1,192 @@ +"""The abandoned metro station under the village: content/street/README.md's *The metro*.""" + +from simile.schema import ( + Door, + Facing, + Interactable, + InteractableKind, + Item, + ItemKind, + Location, + LocationKind, + MapTile, + Tile, + TileArea, +) +from simile.street.plan import Light, Pickup, Plan +from simile.street.village import GROUND, Building, street +from simile.world.map import SpotKind + +METRO = "metro" +WIDTH = 110 +ENTRANCE = MapTile(map="banyan_row", x=62, y=GROUND + 3) +"""The stairway down's door on the river promenade under Banyan Row.""" +HALL = GROUND - 3 +"""The ticket hall's row, a flight up from the platform.""" +LANDING = HALL - 3 +"""The row stood on at the top of the stairway, where the door arrives.""" +ARRIVAL = MapTile(map=METRO, x=2, y=LANDING) + +CLINIC = "clinic" +SCRAP_STALL = "scrap_stall" +SCRAP_TRADER = "metro_scrap_trader" +"""The scrap trader's stall spot, by their stall on the platform.""" +DOCTOR = "clinic_doctor" +"""The doctor's stall spot, in the clinic.""" +PLUG = "metro_plug" +LIFT = "metro_lift" +DEAD_TERMINAL = "metro_terminal" +HIGH_LEDGE = "metro_high_ledge" +LIFT_FOOT = MapTile(map=METRO, x=100, y=GROUND) +"""Where the lift down to the sewers stops on the metro.""" +LEGS_ROWS = 3 +"""Rows up the high ledge assumes the hydraulic legs jump: one more than today's.""" + +SALVAGE = "salvage" +SECRET = "secret" +RATION_PACK = Item(id="ration_pack", kind=ItemKind.FOOD, name="Ration pack", weight=0.5) + + +def catwalk(plan: Plan, x: int, row: int, width: int) -> None: + """A deck standing on row `row`, drawn as a steel catwalk over its solid row.""" + plan.fill(x, row + 1, width, 1) + plan.piece("catwalk", x, row, width, 2) + + +def machine(plan: Plan, item_id: str, label: str, area: TileArea, stand: list[Tile]) -> None: + kind = InteractableKind.MACHINE + plan.interactables.append( + Interactable(id=item_id, label=label, kind=kind, map=plan.id, area=area, stand=stand) + ) + + +def light(plan: Plan, light_id: str, x: int, y: int, width: int, height: int, lit: bool) -> None: + area = TileArea(x=x, y=y, width=width, height=height) + plan.light.append(Light(light_id, area, lit)) + + +def salvage(plan: Plan, number: int, x: int, y: int) -> None: + plan.pickups.append(Pickup(f"metro_salvage_{number}", SALVAGE, x, y)) + + +def secret( + plan: Plan, secret_id: str, x: int, y: int, money: float = 0.0, item: Item | None = None +) -> None: + plan.pickups.append(Pickup(secret_id, SECRET, x, y, money, item)) + + +def flight(plan: Plan, foot: tuple[int, int], rows: int) -> None: + """Stairs rising west from `foot`, drawn as the metro's tiled steps over the street's.""" + x, y = foot + plan.stairs(foot, east=False, steps=rows - 1) + plan.piece("metro_steps", x - rows, y - rows, rows + 1, rows + 1) + + +def entrance(plan: Plan) -> None: + """A landing, two flights down to the platform, and a catwalk over the first.""" + plan.fill(0, HALL + 1, 20, GROUND - HALL) + plan.fill(0, LANDING + 1, 5, HALL - LANDING) + flight(plan, (7, HALL), HALL - LANDING) + flight(plan, (22, GROUND), GROUND - HALL) + catwalk(plan, 7, LANDING - 2, 2) + secret(plan, "metro_secret_stairwell", 8, LANDING - 2, money=25.0) + plan.piece("turnstile", 11, HALL, 2, 1) + plan.piece("ticket_machine", 15, HALL - 1, 1, 2) + + +def platform(plan: Plan) -> None: + """The scrap stall, and a dead train with a catwalk a jump above its roof.""" + plan.piece("scrap_stall", 25, GROUND - 1, 3, 2) + plan.spot(SCRAP_TRADER, SpotKind.STANDING, 26, GROUND, Facing.SOUTH) + plan.locations.append( + Location( + id=SCRAP_STALL, + name="The scrap trader's stall", + kind=LocationKind.AMENITY, + map=plan.id, + area=TileArea(x=24, y=GROUND - 2, width=5, height=3), + entry=Tile(x=28, y=GROUND), + ) + ) + plan.piece("metro_bench", 29, GROUND, 2, 1) + roof = GROUND - 3 + plan.piece("train_carriage", 33, roof + 1, 10, GROUND - roof) + plan.fill(33, roof + 1, 10, 1) + plan.ladder((32, GROUND), GROUND - roof) + salvage(plan, 1, 38, roof) + catwalk(plan, 45, roof - 2, 8) + secret(plan, "metro_secret_catwalk", 52, roof - 2, money=40.0) + plan.piece("crates", 44, GROUND, 1, 1) + plan.piece("metro_bench", 49, GROUND, 2, 1) + plan.piece("tunnel_arch", 55, GROUND - 7, 3, 8) + + +def tunnel(plan: Plan) -> None: + """The plug's alcove between dark stretches, and a climb up the wall to the alcove.""" + plan.piece("pipes", 59, GROUND - 3, 12, 1) + plan.piece("rubble", 60, GROUND, 2, 1) + salvage(plan, 2, 63, GROUND) + plan.piece("bedroll", 65, GROUND, 2, 1) + salvage(plan, 3, 68, GROUND) + plan.piece("breaker_box", 72, GROUND - 1, 1, 2) + area = TileArea(x=72, y=GROUND - 1, width=1, height=2) + machine(plan, PLUG, "Maintenance plug", area, [Tile(x=72, y=GROUND)]) + ledge = GROUND - 3 + plan.ladder((75, GROUND), GROUND - ledge) + catwalk(plan, 76, ledge, 3) + salvage(plan, 4, 77, ledge) + catwalk(plan, 81, ledge - 2, 2) + catwalk(plan, 85, ledge - 4, 2) + secret(plan, "metro_secret_alcove", 86, ledge - 4, item=RATION_PACK) + plan.piece("rubble", 79, GROUND, 2, 1) + salvage(plan, 5, 83, GROUND) + + +def depot(plan: Plan) -> list[Plan]: + """The clinic, the lift, the dead terminal and the high ledge.""" + clinic = Building(plan, CLINIC, "The Clinic", LocationKind.AMENITY, 88, 10, ("clinic",)) + clinic.door(1) + clinic.place(entry=3) + [room] = clinic.floors + room.spot(DOCTOR, SpotKind.STANDING, 6, 2, Facing.SOUTH) + plan.piece("freight_lift", LIFT_FOOT.x, GROUND - 3, 2, 4) + area = TileArea(x=LIFT_FOOT.x, y=GROUND - 3, width=2, height=4) + stand = [Tile(x=LIFT_FOOT.x, y=GROUND), Tile(x=LIFT_FOOT.x + 1, y=GROUND)] + machine(plan, LIFT, "Maintenance lift", area, stand) + plan.piece("dead_terminal", 104, GROUND - 1, 1, 2) + area = TileArea(x=104, y=GROUND - 1, width=1, height=2) + machine(plan, DEAD_TERMINAL, "Dead terminal", area, [Tile(x=104, y=GROUND)]) + ledge = GROUND - LEGS_ROWS + catwalk(plan, 106, ledge, 4) + area = TileArea(x=106, y=ledge, width=4, height=1) + machine(plan, HIGH_LEDGE, "High ledge", area, [Tile(x=x, y=ledge) for x in range(106, 110)]) + plan.piece("crates", 108, ledge, 1, 1) + salvage(plan, 6, 108, ledge) + return clinic.floors + + +def lighting(plan: Plan) -> None: + """Every cell in one stretch, lit or dark.""" + height = plan.height + light(plan, "metro_entrance", 0, 0, 19, height, lit=True) + light(plan, "metro_vault", 19, 0, 39, GROUND - 3, lit=False) + light(plan, "metro_platform", 19, GROUND - 3, 39, height - GROUND + 3, lit=True) + light(plan, "metro_tunnel_west", 58, 0, 13, height, lit=False) + light(plan, "metro_maintenance", 71, 0, 3, height, lit=True) + light(plan, "metro_tunnel_east", 74, 0, 13, height, lit=False) + light(plan, "metro_depot", 87, 0, WIDTH - 87, height, lit=True) + + +def metro(village: Plan) -> list[Plan]: + """The metro and the clinic's floor, joined to `village`, the street its stairway is on.""" + plan = street(METRO, "The Old Metro", WIDTH) + entrance(plan) + platform(plan) + tunnel(plan) + floors = depot(plan) + lighting(plan) + door = Door(id=f"{village.id}_to_{METRO}", outside=ENTRANCE, inside=ARRIVAL) + village.doors.append(door) + plan.doors.append(door) + village.piece("metro_entrance", ENTRANCE.x - 1, ENTRANCE.y - 2, 1, 3) + return [plan, *floors] diff --git a/server/simile/street/plan.py b/server/simile/street/plan.py index e824fc00..eb5f564f 100644 --- a/server/simile/street/plan.py +++ b/server/simile/street/plan.py @@ -3,7 +3,7 @@ from itertools import count from typing import Any -from simile.schema import Door, DoorKind, Facing, Interactable, Location, TileArea +from simile.schema import Door, DoorKind, Facing, Interactable, Item, Location, TileArea from simile.world.map import ( CLIMBABLE_LAYER, COLLISION_LAYER, @@ -24,6 +24,8 @@ TILE_SIZE = 16 BUILDINGS_LAYER = "buildings" FURNITURE_LAYER = "furniture" +LIGHT_LAYER = "light" +PICKUPS_LAYER = "pickups" MARKERS = ("blocked", "solid", "stairs", "ladder") """The marker tiles, in tile id order, that the tile layers use.""" MARKERS_IMAGE = "markers.png" @@ -42,6 +44,27 @@ class Piece: area: TileArea +@dataclass(frozen=True) +class Light: + """A stretch of a map that's lit, or dark without a lamp.""" + + id: str + area: TileArea + lit: bool + + +@dataclass(frozen=True) +class Pickup: + """A standing cell where `salvage` can lie, or a `secret` holding money or an item.""" + + id: str + kind: str + x: int + y: int + money: float = 0.0 + item: Item | None = None + + @dataclass(frozen=True) class Facade: """A building's front on a street, covering its storeys above the street's standing row.""" @@ -69,6 +92,8 @@ def __init__(self, map_id: str, width: int, height: int, kind: MapKind, **props: self.doors: list[Door] = [] self.interactables: list[Interactable] = [] self.spots: list[Spot] = [] + self.light: list[Light] = [] + self.pickups: list[Pickup] = [] def fill(self, x: int, y: int, width: int, height: int) -> None: for cell in ((cx, cy) for cy in range(y, y + height) for cx in range(x, x + width)): @@ -117,6 +142,7 @@ def check(self) -> None: for tile in item.stand ] stops += [(f"{wall.id}'s stand", wall.stand.x, wall.stand.y) for wall in self.murals] + stops += [(f"pickup {p.id}", p.x, p.y) for p in self.pickups] stops += [ (f"{door.id}'s end", end.x, end.y) for door in self.doors @@ -129,6 +155,9 @@ def check(self) -> None: ids = [spot.id for spot in self.spots] if len(ids) != len(set(ids)): raise ValueError(f"{self.id} has two spots with one id") + for area in (light.area for light in self.light): + self._bounds(area.x, area.y) + self._bounds(area.x + area.width - 1, area.y + area.height - 1) def _climbable(self, x: int, y: int, kind: DoorKind) -> None: self._bounds(x, y) @@ -163,6 +192,8 @@ def tmj(plan: Plan, markers: str) -> dict[str, Any]: DOORS_LAYER: [_door(next(ids), door, plan.id) for door in plan.doors], INTERACTABLES_LAYER: [_interactable(next(ids), item) for item in plan.interactables], SPOTS_LAYER: [_spot(next(ids), spot) for spot in plan.spots], + LIGHT_LAYER: [_light(next(ids), light) for light in plan.light], + PICKUPS_LAYER: [_pickup(next(ids), pickup) for pickup in plan.pickups], } for name, group in objects.items(): layers.append(_object_layer(len(layers) + 1, name, group)) @@ -342,3 +373,18 @@ def _wall(object_id: int, wall: MuralWall) -> dict[str, Any]: } area = TileArea(x=wall.x, y=wall.y, width=wall.width, height=1) return _rectangle(object_id, wall.id, "mural_wall", area, props) + + +def _light(object_id: int, light: Light) -> dict[str, Any]: + return _rectangle(object_id, light.id, "light", light.area, {"lit": light.lit}) + + +def _pickup(object_id: int, pickup: Pickup) -> dict[str, Any]: + props: dict[str, Any] = {} + if pickup.money: + props["money"] = pickup.money + if pickup.item is not None: + item = pickup.item + props |= {"item": item.id, "item_kind": item.kind.value, "item_name": item.name} + props["item_weight"] = item.weight + return _rectangle(object_id, pickup.id, pickup.kind, _tile(pickup.x, pickup.y), props) diff --git a/server/simile/street/preview.py b/server/simile/street/preview.py index 80f6d8cf..b02d83ef 100644 --- a/server/simile/street/preview.py +++ b/server/simile/street/preview.py @@ -29,6 +29,9 @@ "ink": (30, 30, 40, 255), "blocked": (220, 60, 60, 120), "furniture": (150, 140, 128, 255), + "dark": (20, 24, 60, 90), + "salvage": (64, 196, 120, 255), + "secret": (255, 64, 160, 255), } COLOURS["solid"] = COLOURS["ground"] LINKS = {DoorKind.STAIRS: "stairs", DoorKind.LADDER: "ladder", DoorKind.LIFT: "lift"} @@ -180,6 +183,16 @@ def plan(self, plan: Plan, dx: int, dy: int, cells: bool = True) -> None: self.outline(TileArea(x=end.x, y=end.y, width=1, height=1), colour, dx, dy) for location in plan.locations: self.cell(location.entry.x + dx, location.entry.y + dy, "entry", inset=5) + for area in (light.area for light in plan.light if not light.lit): + box = self.box(area.x + dx, area.y + dy, area.width, area.height) + x0, y0, x1, y1 = (round(v) for v in box) + shade = Image.new("RGBA", (x1 - x0 + 1, y1 - y0 + 1), COLOURS["dark"]) + self.image.alpha_composite(shade, (x0, y0)) + for pickup in plan.pickups: + cx, cy = self.middle(pickup.x, pickup.y, dx, dy) + r = SCALE * 0.3 + diamond = [(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)] + self.draw.polygon(diamond, fill=COLOURS[pickup.kind], outline=COLOURS["ink"]) for spot in plan.spots: cx, cy = self.middle(spot.x, spot.y, dx, dy) tx, ty = TICKS[spot.facing] diff --git a/server/simile/street/village.py b/server/simile/street/village.py index 040051b6..0501cfbd 100644 --- a/server/simile/street/village.py +++ b/server/simile/street/village.py @@ -25,8 +25,10 @@ """A floor's standing row.""" STOOL_M = 0.25 """How much higher a stool's seat is than a chair's.""" -JUMPING = frozenset({"rooftops"}) -"""Streets only the player gets all over, by jumping. Nobody else has anywhere to go there.""" +JUMPING = frozenset({"rooftops", "metro"}) +"""Streets only the player gets all over, by jumping. No resident goes there.""" +UNDERGROUND = frozenset({"metro"}) +"""Streets under the village.""" N, E, W = Facing.NORTH, Facing.EAST, Facing.WEST SEAT, BAR, DESK, VIEWING = SpotKind.SEAT, SpotKind.BAR, SpotKind.DESK, SpotKind.VIEWING @@ -553,6 +555,8 @@ def rooftops() -> Plan: def village() -> list[Plan]: """Every street, then every floor, with the streets joined.""" + from simile.street.metro import metro + high, high_floors = high_street() lychee, lychee_floors = lychee_lane() kapok, kapok_floors = kapok_walk() @@ -562,8 +566,9 @@ def village() -> list[Plan]: join(high, 50, lychee, 17) join(high, 64, banyan, 24) join(kapok, 2, roofs, 1) - plans = [high, lychee, kapok, banyan, roofs, *high_floors, *lychee_floors, *kapok_floors] - plans += banyan_floors + below, *below_floors = metro(banyan) + plans = [high, lychee, kapok, banyan, roofs, below, *high_floors, *lychee_floors] + plans += [*kapok_floors, *banyan_floors, *below_floors] for plan in plans: plan.check() return plans diff --git a/server/tests/papercraft/test_fronts.py b/server/tests/papercraft/test_fronts.py index 381a416e..d29c8a14 100644 --- a/server/tests/papercraft/test_fronts.py +++ b/server/tests/papercraft/test_fronts.py @@ -62,7 +62,7 @@ def test_a_front_is_painted_as_units_of_a_size_flux_keeps_to(building: str) -> N width, height = plan.image.size assert width % 16 == 0 and height % 16 == 0 and max(width, height) <= 1536 assert 0.8e6 < width * height < 1.4e6 - assert unit.width <= UNIT + assert unit.width <= UNIT or len(parts) == 1 assert unit.door is None or f.door == pytest.approx(x + unit.door) assert sum(unit.width for _, unit in units(f)) == pytest.approx(f.width) assert sum(unit.door is not None for _, unit in units(f)) == (f.door is not None) diff --git a/server/tests/street/positions.json b/server/tests/street/positions.json index f128241e..87bbd56a 100644 --- a/server/tests/street/positions.json +++ b/server/tests/street/positions.json @@ -2909,5 +2909,1012 @@ "y": 10, "last": null, "kept": false + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 3.04, + "y": 9, + "last": null, + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 3.58, + "y": 9, + "last": [ + 3.04, + 9 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 4, + "y": 9, + "last": [ + 3.58, + 9 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 4.29, + "y": 9.29, + "last": [ + 4, + 9 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 4.59, + "y": 9.59, + "last": [ + 4.29, + 9.29 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 4.88, + "y": 9.88, + "last": [ + 4.59, + 9.59 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 5.18, + "y": 10.18, + "last": [ + 4.88, + 9.88 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 5.47, + "y": 10.47, + "last": [ + 5.18, + 10.18 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 5.76, + "y": 10.76, + "last": [ + 5.47, + 10.47 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 6.06, + "y": 11.06, + "last": [ + 5.76, + 10.76 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 6.35, + "y": 11.35, + "last": [ + 6.06, + 11.06 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 6.65, + "y": 11.65, + "last": [ + 6.35, + 11.35 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 6.94, + "y": 11.94, + "last": [ + 6.65, + 11.65 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 7, + "y": 12, + "last": [ + 6.94, + 11.94 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 7.04, + "y": 12, + "last": [ + 7, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 7.58, + "y": 12, + "last": [ + 7.04, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 8.12, + "y": 12, + "last": [ + 7.58, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 8.66, + "y": 12, + "last": [ + 8.12, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 9.2, + "y": 12, + "last": [ + 8.66, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 9.75, + "y": 12, + "last": [ + 9.2, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 10.29, + "y": 12, + "last": [ + 9.75, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 10.83, + "y": 12, + "last": [ + 10.29, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 11.37, + "y": 12, + "last": [ + 10.83, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 11.91, + "y": 12, + "last": [ + 11.37, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 12.45, + "y": 12, + "last": [ + 11.91, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 12.99, + "y": 12, + "last": [ + 12.45, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 13.53, + "y": 12, + "last": [ + 12.99, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 14.07, + "y": 12, + "last": [ + 13.53, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 14.61, + "y": 12, + "last": [ + 14.07, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 15.15, + "y": 12, + "last": [ + 14.61, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 15.69, + "y": 12, + "last": [ + 15.15, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 16.24, + "y": 12, + "last": [ + 15.69, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 16.78, + "y": 12, + "last": [ + 16.24, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 17.32, + "y": 12, + "last": [ + 16.78, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 17.86, + "y": 12, + "last": [ + 17.32, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 18.4, + "y": 12, + "last": [ + 17.86, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 18.94, + "y": 12, + "last": [ + 18.4, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 19.25, + "y": 12.25, + "last": [ + 18.94, + 12 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 19.54, + "y": 12.54, + "last": [ + 19.25, + 12.25 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 19.84, + "y": 12.84, + "last": [ + 19.54, + 12.54 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 20.13, + "y": 13.13, + "last": [ + 19.84, + 12.84 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 20.43, + "y": 13.43, + "last": [ + 20.13, + 13.13 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 20.72, + "y": 13.72, + "last": [ + 20.43, + 13.43 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 21.01, + "y": 14.01, + "last": [ + 20.72, + 13.72 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 21.31, + "y": 14.31, + "last": [ + 21.01, + 14.01 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 21.6, + "y": 14.6, + "last": [ + 21.31, + 14.31 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 21.9, + "y": 14.9, + "last": [ + 21.6, + 14.6 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 22, + "y": 15, + "last": [ + 21.9, + 14.9 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 22.04, + "y": 15, + "last": [ + 22, + 15 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 22.58, + "y": 15, + "last": [ + 22.04, + 15 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 23.12, + "y": 15, + "last": [ + 22.58, + 15 + ], + "kept": true + }, + { + "why": "down the metro's stairway to the platform", + "map": "metro", + "x": 23.62, + "y": 15, + "last": [ + 23.12, + 15 + ], + "kept": true + }, + { + "why": "off the landing onto the catwalk over the stairwell", + "map": "metro", + "x": 1.08, + "y": 9, + "last": null, + "kept": true + }, + { + "why": "off the landing onto the catwalk over the stairwell", + "map": "metro", + "x": 2.16, + "y": 9, + "last": [ + 1.08, + 9 + ], + "kept": true + }, + { + "why": "off the landing onto the catwalk over the stairwell", + "map": "metro", + "x": 3.25, + "y": 9, + "last": [ + 2.16, + 9 + ], + "kept": true + }, + { + "why": "off the landing onto the catwalk over the stairwell", + "map": "metro", + "x": 4.33, + "y": 8.29, + "last": [ + 3.25, + 9 + ], + "kept": true + }, + { + "why": "off the landing onto the catwalk over the stairwell", + "map": "metro", + "x": 5.41, + "y": 6.89, + "last": [ + 4.33, + 8.29 + ], + "kept": true + }, + { + "why": "off the landing onto the catwalk over the stairwell", + "map": "metro", + "x": 6.49, + "y": 6.9, + "last": [ + 5.41, + 6.89 + ], + "kept": true + }, + { + "why": "off the landing onto the catwalk over the stairwell", + "map": "metro", + "x": 7.57, + "y": 7, + "last": [ + 6.49, + 6.9 + ], + "kept": true + }, + { + "why": "off the landing onto the catwalk over the stairwell", + "map": "metro", + "x": 7.66, + "y": 7, + "last": [ + 7.57, + 7 + ], + "kept": true + }, + { + "why": "from the train's roof onto the catwalk in the vault", + "map": "metro", + "x": 38.08, + "y": 12, + "last": null, + "kept": true + }, + { + "why": "from the train's roof onto the catwalk in the vault", + "map": "metro", + "x": 39.16, + "y": 12, + "last": [ + 38.08, + 12 + ], + "kept": true + }, + { + "why": "from the train's roof onto the catwalk in the vault", + "map": "metro", + "x": 40.25, + "y": 12, + "last": [ + 39.16, + 12 + ], + "kept": true + }, + { + "why": "from the train's roof onto the catwalk in the vault", + "map": "metro", + "x": 41.33, + "y": 12, + "last": [ + 40.25, + 12 + ], + "kept": true + }, + { + "why": "from the train's roof onto the catwalk in the vault", + "map": "metro", + "x": 42.41, + "y": 12, + "last": [ + 41.33, + 12 + ], + "kept": true + }, + { + "why": "from the train's roof onto the catwalk in the vault", + "map": "metro", + "x": 43.49, + "y": 10.36, + "last": [ + 42.41, + 12 + ], + "kept": true + }, + { + "why": "from the train's roof onto the catwalk in the vault", + "map": "metro", + "x": 44.57, + "y": 9.7, + "last": [ + 43.49, + 10.36 + ], + "kept": true + }, + { + "why": "from the train's roof onto the catwalk in the vault", + "map": "metro", + "x": 45.65, + "y": 10, + "last": [ + 44.57, + 9.7 + ], + "kept": true + }, + { + "why": "from the train's roof onto the catwalk in the vault", + "map": "metro", + "x": 46.24, + "y": 10, + "last": [ + 45.65, + 10 + ], + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 81.08, + "y": 10, + "last": null, + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 82.16, + "y": 10, + "last": [ + 81.08, + 10 + ], + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 83.25, + "y": 8.98, + "last": [ + 82.16, + 10 + ], + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 84.33, + "y": 7.79, + "last": [ + 83.25, + 8.98 + ], + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 85.41, + "y": 8, + "last": [ + 84.33, + 7.79 + ], + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 86.41, + "y": 8, + "last": [ + 85.41, + 8 + ], + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 86.49, + "y": 8, + "last": [ + 86.41, + 8 + ], + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 87.57, + "y": 8.43, + "last": [ + 86.49, + 8 + ], + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 88.65, + "y": 10.57, + "last": [ + 87.57, + 8.43 + ], + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 89.74, + "y": 14.17, + "last": [ + 88.65, + 10.57 + ], + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 90.82, + "y": 15, + "last": [ + 89.74, + 14.17 + ], + "kept": true + }, + { + "why": "up the tunnel wall to the alcove, and down again", + "map": "metro", + "x": 91.32, + "y": 15, + "last": [ + 90.82, + 15 + ], + "kept": true + }, + { + "why": "a run jump short of the high ledge", + "map": "metro", + "x": 101.08, + "y": 15, + "last": null, + "kept": true + }, + { + "why": "a run jump short of the high ledge", + "map": "metro", + "x": 102.16, + "y": 15, + "last": [ + 101.08, + 15 + ], + "kept": true + }, + { + "why": "a run jump short of the high ledge", + "map": "metro", + "x": 103.25, + "y": 15, + "last": [ + 102.16, + 15 + ], + "kept": true + }, + { + "why": "a run jump short of the high ledge", + "map": "metro", + "x": 104.33, + "y": 15, + "last": [ + 103.25, + 15 + ], + "kept": true + }, + { + "why": "a run jump short of the high ledge", + "map": "metro", + "x": 105.41, + "y": 14.71, + "last": [ + 104.33, + 15 + ], + "kept": true + }, + { + "why": "a run jump short of the high ledge", + "map": "metro", + "x": 106.49, + "y": 15, + "last": [ + 105.41, + 14.71 + ], + "kept": true + }, + { + "why": "a run jump short of the high ledge", + "map": "metro", + "x": 107.57, + "y": 15, + "last": [ + 106.49, + 15 + ], + "kept": true + }, + { + "why": "a run jump short of the high ledge", + "map": "metro", + "x": 108.65, + "y": 15, + "last": [ + 107.57, + 15 + ], + "kept": true + }, + { + "why": "a run jump short of the high ledge", + "map": "metro", + "x": 109, + "y": 15, + "last": [ + 108.65, + 15 + ], + "kept": true + }, + { + "why": "in the ticket hall's floor", + "map": "metro", + "x": 10, + "y": 14, + "last": null, + "kept": false + }, + { + "why": "in the dead train's roof", + "map": "metro", + "x": 37.5, + "y": 13, + "last": null, + "kept": false + }, + { + "why": "in the high ledge's catwalk", + "map": "metro", + "x": 107, + "y": 13, + "last": null, + "kept": false + }, + { + "why": "high above the platform, past any jump", + "map": "metro", + "x": 30, + "y": 4, + "last": null, + "kept": false } ] diff --git a/server/tests/street/test_metro.py b/server/tests/street/test_metro.py new file mode 100644 index 00000000..67a97d9e --- /dev/null +++ b/server/tests/street/test_metro.py @@ -0,0 +1,250 @@ +import json +from collections import deque +from dataclasses import replace +from typing import Any + +import pytest + +from simile.behaviour.scoring import Places +from simile.schema import DoorKind, Facing, InteractableKind, LocationKind +from simile.street import moves +from simile.street.jump import JUMP +from simile.street.metro import ( + ARRIVAL, + CLINIC, + DEAD_TERMINAL, + DOCTOR, + ENTRANCE, + HIGH_LEDGE, + LEGS_ROWS, + LIFT, + LIFT_FOOT, + METRO, + PLUG, + SCRAP_STALL, + SCRAP_TRADER, +) +from simile.street.plan import LIGHT_LAYER, PICKUPS_LAYER +from simile.street.village import GROUND, UNDERGROUND +from simile.world.map import STREET_DIR, SpotKind, load_maps, with_every_location +from simile.world.movement import Point, inside +from simile.world.paths import Spot, door_ends, find_route, steps + +MAPS = load_maps(STREET_DIR) +TOWN = with_every_location(MAPS) +METRO_MAP = MAPS[METRO] +CLINIC_FLOOR = f"{CLINIC}_floor_0" +BELOW = {METRO: METRO_MAP, CLINIC_FLOOR: MAPS[CLINIC_FLOOR]} +START = Spot(METRO, ARRIVAL.x, ARRIVAL.y) + + +def objects(layer: str) -> list[dict[str, Any]]: + data = json.loads((STREET_DIR / f"{METRO}.tmj").read_text()) + [found] = [each for each in data["layers"] if each["name"] == layer] + return found["objects"] + + +def props(obj: dict[str, Any]) -> dict[str, Any]: + return {p["name"]: p["value"] for p in obj.get("properties", [])} + + +def cell(obj: dict[str, Any]) -> tuple[int, int]: + return obj["x"] // 16, obj["y"] // 16 + + +def machine(item_id: str) -> Any: + [item] = [item for item in METRO_MAP.interactables if item.id == item_id] + return item + + +def stands(item_id: str) -> set[Spot]: + return {Spot(METRO, tile.x, tile.y) for tile in machine(item_id).stand} + + +def reachable(jumps: bool) -> set[Spot]: + """Every cell under the village the player can get to from the top of the stairway.""" + doors = {a: b for a, b in door_ends(BELOW).items()} + seen, queue = {START}, deque([START]) + while queue: + here = queue.popleft() + nexts = [Spot(here.map, *c) for c, _ in steps(BELOW[here.map], (here.x, here.y), jumps)] + if here in doors: + nexts.append(doors[here]) + for there in nexts: + if there not in seen: + seen.add(there) + queue.append(there) + return seen + + +def walkable() -> set[Spot]: + return { + Spot(town.id, x, y) + for town in BELOW.values() + for y in range(town.height) + for x in range(town.width) + if town.walkable(x, y) + } + + +def test_a_stairway_goes_down_through_a_door_on_banyan_rows_promenade() -> None: + [door] = [d for d in TOWN.doors if METRO in (d.outside.map, d.inside.map) and d.id != CLINIC] + + assert door.id == "banyan_row_to_metro" and door.kind is DoorKind.DOOR + assert (door.outside, door.inside) == (ENTRANCE, ARRIVAL) + assert ENTRANCE.y > GROUND, "on the river promenade, below Banyan Row's street" + assert ARRIVAL.y < GROUND + top = max(x for x in range(METRO_MAP.width) if METRO_MAP.standing(x, ARRIVAL.y)) + assert METRO_MAP.climb(top + 1, ARRIVAL.y + 1) is DoorKind.STAIRS + assert {METRO} == UNDERGROUND + + +def test_you_can_walk_down_through_the_metro_to_the_clinic_and_back() -> None: + doors = door_ends(MAPS) + promenade = Spot(ENTRANCE.map, ENTRANCE.x - 2, ENTRANCE.y) + clinic = TOWN.location(CLINIC) + inside = Spot(clinic.map, clinic.entry.x, clinic.entry.y) + + there = find_route(MAPS, promenade, inside, doors) + back = find_route(MAPS, inside, promenade, doors) + + assert there is not None and back is not None + assert {spot.map for spot in there} == {"banyan_row", METRO, CLINIC_FLOOR} + assert any(METRO_MAP.climb(spot.x, spot.y) is DoorKind.STAIRS for spot in there) + + +def test_every_cell_but_the_high_ledge_is_in_reach_with_todays_jump() -> None: + jumped = reachable(jumps=True) + + assert jumped == walkable() - stands(HIGH_LEDGE) + assert not jumped & stands(HIGH_LEDGE) + + +def test_the_high_ledge_is_a_row_above_todays_jump_and_the_legs_reach_it( + monkeypatch: pytest.MonkeyPatch, +) -> None: + ledge = machine(HIGH_LEDGE) + row = ledge.stand[0].y + under = {(t.x, y) for t in ledge.stand for y in range(row + 1, METRO_MAP.height)} + floor = min(y for x, y in under if METRO_MAP.standing(x, y)) + + assert ledge.kind is InteractableKind.MACHINE + assert floor - row == LEGS_ROWS > JUMP.rows and JUMP.rise < LEGS_ROWS + monkeypatch.setattr(moves, "JUMP", replace(JUMP, rows=LEGS_ROWS)) + assert stands(HIGH_LEDGE) <= reachable(jumps=True) + + +def test_the_lift_and_the_dead_terminal_are_machines_you_walk_up_to_that_lead_nowhere_yet() -> None: + walked = reachable(jumps=False) + ends = {(end.map, end.x, end.y) for d in TOWN.doors for end in (d.outside, d.inside)} + + for item_id in (LIFT, DEAD_TERMINAL): + assert machine(item_id).kind is InteractableKind.MACHINE + assert stands(item_id) <= walked + assert not {(s.map, s.x, s.y) for s in stands(item_id)} & ends + assert Spot(METRO, LIFT_FOOT.x, LIFT_FOOT.y) in stands(LIFT) + + +def test_the_plug_is_a_machine_in_a_lit_alcove_between_dark_stretches_of_tunnel() -> None: + [plug] = stands(PLUG) + lights = {o["name"]: (cell(o), o["width"] // 16, props(o)["lit"]) for o in objects(LIGHT_LAYER)} + (x0, _), width, lit = lights["metro_maintenance"] + + assert machine(PLUG).kind is InteractableKind.MACHINE + assert plug in reachable(jumps=False) + assert lit and x0 <= plug.x < x0 + width + assert not lights["metro_tunnel_west"][2] and not lights["metro_tunnel_east"][2] + + +def test_the_light_areas_cover_every_cell_of_the_metro_once() -> None: + covered: list[tuple[int, int]] = [] + for area in objects(LIGHT_LAYER): + (x, y), w, h = cell(area), area["width"] // 16, area["height"] // 16 + assert area["type"] == "light" and isinstance(props(area)["lit"], bool) + covered += [(cx, cy) for cy in range(y, y + h) for cx in range(x, x + w)] + dark = {o["name"] for o in objects(LIGHT_LAYER) if not props(o)["lit"]} + + assert sorted(covered) == sorted( + (x, y) for y in range(METRO_MAP.height) for x in range(METRO_MAP.width) + ) + assert dark == {"metro_vault", "metro_tunnel_west", "metro_tunnel_east"} + + +def test_every_secret_needs_a_jump_and_holds_money_or_an_item() -> None: + secrets = [o for o in objects(PICKUPS_LAYER) if o["type"] == "secret"] + jumped, walked = reachable(jumps=True), reachable(jumps=False) + + assert len(secrets) == 3 + for secret in secrets: + here = Spot(METRO, *cell(secret)) + held = props(secret) + assert here in jumped and here not in walked, secret["name"] + assert ("money" in held) != ("item" in held), secret["name"] + assert held.get("money", 1) > 0 + + +def test_salvage_lies_about_the_metro_and_one_piece_waits_on_the_high_ledge() -> None: + salvage = [o for o in objects(PICKUPS_LAYER) if o["type"] == "salvage"] + cells = {Spot(METRO, *cell(o)) for o in salvage} + + assert len(salvage) == 6 and all(not props(o) for o in salvage) + assert all(METRO_MAP.standing(spot.x, spot.y) for spot in cells) + assert cells & stands(HIGH_LEDGE) + assert cells - stands(HIGH_LEDGE) <= reachable(jumps=False) + + +def test_the_scrap_trader_and_the_doctor_have_stall_spots() -> None: + [trader] = [s for s in METRO_MAP.spots if s.id == SCRAP_TRADER] + [doctor] = MAPS[CLINIC_FLOOR].spots + + assert (trader.kind, trader.facing) == (SpotKind.STANDING, Facing.SOUTH) + assert inside(TOWN.location(SCRAP_STALL), Point(trader.x, trader.y, METRO)) + assert doctor.id == DOCTOR and (doctor.kind, doctor.facing) == (trader.kind, trader.facing) + walked = reachable(jumps=False) + assert {Spot(METRO, trader.x, trader.y), Spot(CLINIC_FLOOR, doctor.x, doctor.y)} <= walked + + +def test_the_clinic_is_a_room_of_its_own_off_the_metro() -> None: + clinic = TOWN.location(CLINIC) + [door] = [d for d in TOWN.doors if d.id == CLINIC] + + assert clinic.kind is LocationKind.AMENITY and clinic.map == CLINIC_FLOOR + assert MAPS[CLINIC_FLOOR].building == CLINIC and MAPS[CLINIC_FLOOR].floor == 0 + assert door.outside.map == METRO and door.inside.map == CLINIC_FLOOR + + +def test_no_resident_has_anywhere_to_go_in_the_metro() -> None: + places = Places(TOWN) + placed = {loc.map for locations in places.by_activity.values() for loc in locations} + metro_ends = [end for d in TOWN.doors for end in (d.outside, d.inside) if end.map == METRO] + + assert {loc.id for loc in TOWN.locations if loc.map in BELOW} == {CLINIC, SCRAP_STALL} + assert {loc.kind for loc in TOWN.locations if loc.map in BELOW} == {LocationKind.AMENITY} + assert not placed & set(BELOW) + assert len(metro_ends) == 2, "the stairway and the clinic's door: a dead end" + + +def test_the_hooks_keep_their_ids_and_cells() -> None: + machines = {item.id: [(t.x, t.y) for t in item.stand] for item in METRO_MAP.interactables} + pickups = {o["name"]: cell(o) for o in objects(PICKUPS_LAYER)} + spots = {s.id: (s.map, s.x, s.y) for town in BELOW.values() for s in town.spots} + + assert machines == { + PLUG: [(72, 15)], + LIFT: [(100, 15), (101, 15)], + DEAD_TERMINAL: [(104, 15)], + HIGH_LEDGE: [(106, 12), (107, 12), (108, 12), (109, 12)], + } + assert spots == {SCRAP_TRADER: (METRO, 26, 15), DOCTOR: (CLINIC_FLOOR, 6, 2)} + assert pickups == { + "metro_secret_stairwell": (8, 7), + "metro_salvage_1": (38, 12), + "metro_secret_catwalk": (52, 10), + "metro_salvage_2": (63, 15), + "metro_salvage_3": (68, 15), + "metro_salvage_4": (77, 12), + "metro_secret_alcove": (86, 8), + "metro_salvage_5": (83, 15), + "metro_salvage_6": (108, 12), + } + assert (LIFT_FOOT.map, LIFT_FOOT.x, LIFT_FOOT.y) == (METRO, 100, 15) diff --git a/server/tests/street/test_village.py b/server/tests/street/test_village.py index fc123142..a545bb93 100644 --- a/server/tests/street/test_village.py +++ b/server/tests/street/test_village.py @@ -12,8 +12,17 @@ from simile.cli import create_cli from simile.schema import DoorKind, Facing, MapTile, Pose from simile.sim import Clock, EntityMoved, FakeWallClock, World -from simile.street.plan import BUILDINGS_LAYER, FURNITURE_LAYER -from simile.street.village import FLOOR_ROW, GROUND, JUMPING, STOOL_M, STOREY, village +from simile.street.metro import HIGH_LEDGE, METRO +from simile.street.plan import BUILDINGS_LAYER, FURNITURE_LAYER, LIGHT_LAYER, PICKUPS_LAYER +from simile.street.village import ( + FLOOR_ROW, + GROUND, + JUMPING, + STOOL_M, + STOREY, + UNDERGROUND, + village, +) from simile.world import Movement, Outcome, Walk from simile.world.map import ( STREET_DIR, @@ -74,9 +83,10 @@ def test_every_map_is_a_side_view_street_or_floor() -> None: "high_street", "kapok_walk", "lychee_lane", + "metro", "rooftops", ] - assert len(FLOORS) == 31 + assert len(FLOORS) == 32 assert len(STREETS) + len(FLOORS) == len(MAPS) for floor in FLOORS: assert floor.id == f"{floor.building}_floor_{floor.floor}" @@ -132,17 +142,24 @@ def reachable(start: Spot, jumps: bool = False) -> set[Spot]: return seen -def test_every_cell_of_every_map_is_reachable_from_every_street() -> None: +[LEDGE] = [item for item in MAPS[METRO].interactables if item.id == HIGH_LEDGE] +LOCKED = {Spot(METRO, t.x, t.y) for t in LEDGE.stand} +"""Out of reach until a bionic opens them.""" + + +def test_every_cell_of_every_map_but_the_locked_ones_is_reachable_from_every_street() -> None: every_cell = { Spot(town.id, x, y) for town in MAPS.values() for y in range(town.height) for x in range(town.width) if town.walkable(x, y) - } + } - LOCKED jumped = {cell for cell in every_cell if cell.map in JUMPING} for street in STREETS: - start = next(Spot(street.id, x, GROUND) for x in range(street.width)) + start = next( + Spot(street.id, x, GROUND) for x in range(street.width) if street.standing(x, GROUND) + ) assert reachable(start, jumps=True) == every_cell, street.id walked = reachable(start) assert every_cell - jumped <= walked, street.id @@ -198,6 +215,7 @@ def test_streets_join_through_doors_outside_on_the_first_street() -> None: "high_street_to_kapok_walk": ("high_street", "kapok_walk"), "high_street_to_lychee_lane": ("high_street", "lychee_lane"), "kapok_walk_to_rooftops": ("kapok_walk", "rooftops"), + "banyan_row_to_metro": ("banyan_row", "metro"), } assert {door.kind for door in TOWN.doors if door.id in joins} == {DoorKind.DOOR} @@ -379,6 +397,8 @@ def test_streets_have_a_flight_of_stairs_and_a_ladder() -> None: ("banyan_row", DoorKind.STAIRS), ("lychee_lane", DoorKind.LADDER), ("rooftops", DoorKind.LADDER), + ("metro", DoorKind.STAIRS), + ("metro", DoorKind.LADDER), } @@ -396,7 +416,7 @@ def test_the_readme_names_every_street_building_room_and_piece_of_furniture() -> def test_every_map_has_the_layers_and_furniture_has_names() -> None: names = ["solid", "climbable", "collision", BUILDINGS_LAYER, FURNITURE_LAYER, "locations"] - names += ["murals", "doors", "interactables", "spots"] + names += ["murals", "doors", "interactables", "spots", LIGHT_LAYER, PICKUPS_LAYER] furniture = set() for path in [*STREET_DIR.glob("*.tmj"), *(STREET_DIR / "floors").glob("*.tmj")]: data = json.loads(path.read_text()) @@ -408,9 +428,14 @@ def test_every_map_has_the_layers_and_furniture_has_names() -> None: assert {"cafe_table", "bench", "bed", "workbench", "fountain", "news_terminal"} <= furniture -def test_the_farthest_two_places_are_less_than_a_minute_and_a_half_apart() -> None: +def test_the_villages_farthest_two_places_are_less_than_a_minute_and_a_half_apart() -> None: + below = {plan.id for plan in PLANS.values() if plan.properties.get("street") in UNDERGROUND} doors = door_ends(MAPS) - entries = [Spot(loc.map, loc.entry.x, loc.entry.y) for loc in TOWN.locations] + entries = [ + Spot(loc.map, loc.entry.x, loc.entry.y) + for loc in TOWN.locations + if loc.map not in below | UNDERGROUND + ] routes = [find_route(MAPS, a, b, doors) for a, b in itertools.combinations(entries, 2)] longest = max(route_length(route) for route in routes if route is not None) diff --git a/server/tests/world/test_paths.py b/server/tests/world/test_paths.py index f205ef4b..afb9cd74 100644 --- a/server/tests/world/test_paths.py +++ b/server/tests/world/test_paths.py @@ -190,5 +190,6 @@ def test_only_the_streets_are_outdoors() -> None: "high_street", "kapok_walk", "lychee_lane", + "metro", "rooftops", }