Compare commits

...

14 Commits

13 changed files with 848 additions and 52 deletions

View File

@ -33,13 +33,134 @@
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"Object Layer 1",
"objects":[
{
"height":64,
"id":1,
"name":"hi_box",
"properties":[
{
"name":"color",
"type":"string",
"value":"red"
},
{
"name":"event",
"type":"string",
"value":"change_color"
}],
"rotation":0,
"type":"",
"visible":true,
"width":64,
"x":128,
"y":256
},
{
"height":64,
"id":4,
"name":"low_box",
"properties":[
{
"name":"color",
"type":"string",
"value":"green"
},
{
"name":"event",
"type":"string",
"value":"change_color"
},
{
"name":"interactEvent",
"type":"string",
"value":"log_test"
}],
"rotation":0,
"type":"",
"visible":true,
"width":64,
"x":128,
"y":384
},
{
"height":46.3794477161778,
"id":5,
"name":"sign_crate",
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
},
{
"name":"interactEvent",
"type":"string",
"value":"show_message"
},
{
"name":"messageText",
"type":"string",
"value":"I'm just a humble box!\nIt's possible to say more than one thing you know."
}],
"rotation":0,
"type":"",
"visible":true,
"width":41.2588439219327,
"x":331.477887674811,
"y":401.217123575356
},
{
"height":37.6264667354163,
"id":6,
"name":"",
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":23.6907383148917,
"x":403.02126592157,
"y":408.316842721369
},
{
"height":35.6754647565428,
"id":7,
"name":"",
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}],
"rotation":0,
"type":"",
"visible":true,
"width":30.3798879567435,
"x":656.930237743527,
"y":411.661417542295
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"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, 182, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 203, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 223, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
182, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
182, 0, 0, 0, 0, 188, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 189, 229, 0, 0, 0, 190, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
@ -64,8 +185,8 @@
"x":0,
"y":0
}],
"nextlayerid":3,
"nextobjectid":1,
"nextlayerid":4,
"nextobjectid":8,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.2",

View File

@ -12,13 +12,12 @@ const ROOM_ASSETS = {
}
export default class Assets {
constructor() {
constructor(game) {
this.game = game
this.assetMap = {}
}
get(assetName) {
console.log("getting", assetName)
console.log("from", this.assetMap)
return this.assetMap[assetName]
}
@ -44,7 +43,6 @@ export default class Assets {
}
loadImage(name, path) {
console.log(name, path)
return new Promise(resolve => {
const img = new Image()
this.assetMap[name] = img
@ -55,13 +53,13 @@ export default class Assets {
loadTileset(name, path) {
return fetch(path).then(rsp => rsp.json()).then(json => {
return this.assetMap[name] = new Tileset(json, name)
return this.assetMap[name] = new Tileset(this.game, json, name)
}).then(tileset => this.loadImages(tileset.imagesToLoad))
}
loadRoom(name, path) {
return fetch(path).then(rsp => rsp.json()).then(json => {
return this.assetMap[name] = new Room(json, name)
return this.assetMap[name] = new Room(this.game, json, name)
}).then(room => this.loadTilesets(room.tilesetsToLoad))
}
}

21
src/event.js Normal file
View File

@ -0,0 +1,21 @@
export default class Event {
constructor(name, action) {
this.name = name
this.action = action
this.triggered = false
this.triggeredThisFrame = false
}
trigger(object) {
this.triggeredThisFrame = true
if (!this.triggered) {
this.triggered = true
this.action(object)
}
}
nextFrame() {
this.triggered = this.triggeredThisFrame
this.triggeredThisFrame = false
}
}

View File

@ -1,40 +1,90 @@
import Player from "./player.js"
import Input from "./input.js"
import Event from "./event.js"
import Message from "./message.js"
import { average } from "./util.js"
export default class Game {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext("2d")
this.timestamp = 0
this.actors = []
this.actors.push(new Player(this, 200, 200))
this.player = new Player(this, 200, 200)
this.actors = [this.player]
this.input = new Input().initialize()
this.currentRoom = null
this.events = {
"log_test": new Event("log_test", () => console.log("Log events work!")),
"change_color": new Event("change_color", object => object.setProperty("color", "blue")),
"show_message": new Event("show_message", object => this.message = new Message(this, object.getProperty("messageText")))
}
this.message = null
this.fpsBuffer = [60, 60, 60, 60, 60, 60, 60, 60, 60, 60]
}
triggerEvent(eventName, object) {
const event = this.events[eventName]
if (event) event.trigger(object)
else console.error("Unknown event " + eventName)
}
start() {
this.currentRoom = this.assets.get("sampleRoom")
this.loadRoom(this.assets.get("sampleRoom"))
requestAnimationFrame(this.loop.bind(this))
}
loadRoom(room) {
this.currentRoom = room
this.currentRoom.objects.forEach(roomObject => this.actors.push(roomObject))
}
closeMessage(message) {
this.message = null
}
loop(timestamp) {
const dt = timestamp - this.timestamp
this.timestamp = timestamp
this.tick(dt)
this.dt = timestamp - this.timestamp
const fps = 1000 / this.dt
this.fpsBuffer.pop()
this.fpsBuffer.unshift(fps)
this.timestamp= timestamp
this.tick(this.dt)
this.draw()
requestAnimationFrame(this.loop.bind(this))
}
tick(dt) {
this.actors.forEach(actor => actor.tick(dt))
if (this.message) {
this.message?.tick(dt)
} else {
this.actors.forEach(actor => actor.tick(dt))
Object.values(this.events).forEach(e => e.nextFrame())
}
this.input.tick()
}
draw() {
const { canvas, ctx } = this
this.currentRoom.draw(ctx)
this.actors.forEach(actor => actor.draw(ctx))
this.message?.draw(ctx)
this.drawFps(ctx)
}
drawFps(ctx) {
ctx.fillStyle = "white"
ctx.fillRect(ctx.canvas.width, 0, -25, 20)
ctx.strokeStyle = "black"
ctx.textBaseline = "top"
ctx.textAlign = "right"
ctx.font = "bold 20px serif"
ctx.fillText(Math.round(average(this.fpsBuffer)), ctx.canvas.width, 0)
ctx.strokeText(Math.round(average(this.fpsBuffer)), ctx.canvas.width, 0)
}
}

View File

@ -5,7 +5,7 @@ document.addEventListener("DOMContentLoaded", async e => {
const canvas = document.getElementById("game-canvas")
const game = new Game(canvas)
game.assets = new Assets()
game.assets = new Assets(game)
await game.assets.load()
game.start()

View File

@ -12,11 +12,17 @@ export default class Input {
this.inputsToKeys = Object.fromEntries(Object.keys(this.keysToInputs).map(key => [this.keysToInputs[key], key]))
this.inputPressed = Object.fromEntries(Object.keys(this.inputsToKeys).map(key => [key, false]))
this.inputJustPressed = Object.fromEntries(Object.keys(this.inputsToKeys).map(key => [key, false]))
}
initialize() {
window.addEventListener("keydown", key => this.inputPressed[this.keyFromInput(key.key)] = true)
window.addEventListener("keyup", key => this.inputPressed[this.keyFromInput(key.key)] = false)
window.addEventListener("keydown", key => {
this.inputJustPressed[this.keyFromInput(key.key)] = true
this.inputPressed[this.keyFromInput(key.key)] = true
})
window.addEventListener("keyup", key => {
this.inputPressed[this.keyFromInput(key.key)] = false
})
return this
}
@ -24,6 +30,14 @@ export default class Input {
return !!this.inputPressed[inputName]
}
isInputJustPressed(inputName) {
return !!this.inputJustPressed[inputName]
}
tick() {
this.inputJustPressed = Object.fromEntries(Object.keys(this.inputsToKeys).map(key => [key, false]))
}
keyFromInput(input) {
return this.keysToInputs[input]
}

65
src/message.js Normal file
View File

@ -0,0 +1,65 @@
export default class Message {
constructor(game, text) {
this.game = game
this.text = text.split("\n")
this.currentText = this.text.shift()
this.textColor = "white"
this.backgroundColor = "black"
this.textIndex = 0
this.textProgress = 0.0
this.textSpeed = 0.08 // seconds per character
this.backgroundHeight = 40
this.textSize = 30
}
tick(dt) {
this.textProgress += (dt / 1000.0) / this.textSpeed
this.textIndex = Math.floor(this.textProgress)
const ijp = this.game.input.isInputJustPressed("interact")
if (ijp) {
if (this.messageComplete()) {
this.game.closeMessage(this)
} else if (this.lineComplete()) {
this.currentText = this.text.shift()
this.textProgress = 0.0
this.textIndex = 0
} else {
this.textProgress = this.currentText.length
}
}
}
draw(ctx) {
ctx.fillStyle = this.backgroundColor
ctx.fillRect(0, 0, ctx.canvas.width, this.backgroundHeight)
ctx.font = `bold ${this.textSize}px sans-serif`
ctx.textBaseline = "top"
ctx.textAlign = "left"
ctx.fillStyle = this.textColor
ctx.fillText(this.currentText.substring(0, this.textIndex), 5, 5)
if (this.messageComplete()) {
ctx.fillRect(
ctx.canvas.width - 20,
this.backgroundHeight - 20,
10, 10
)
} else if (this.lineComplete()) {
ctx.beginPath()
ctx.moveTo(ctx.canvas.width - 20, this.backgroundHeight - 20)
ctx.lineTo(ctx.canvas.width - 20, this.backgroundHeight - 10)
ctx.lineTo(ctx.canvas.width - 15, this.backgroundHeight - 15)
ctx.lineTo(ctx.canvas.width - 20, this.backgroundHeight - 20)
ctx.fill()
ctx.closePath()
}
}
lineComplete() {
return this.textIndex >= this.currentText.length + 3
}
messageComplete() {
return this.lineComplete() && !this.text.length
}
}

View File

@ -7,11 +7,12 @@ export default class Player extends Actor {
this.x = x
this.y = y
this.width = 32
this.height = 64
this.height = 32
this.xVel = 0
this.yVel = 0
this.color = "#56E"
this.playerDirection = { x: 0, y: 1 }
this.interactHitbox = null
}
tick(dt) {
@ -26,16 +27,51 @@ export default class Player extends Actor {
this.x += this.xVel
this.y += this.yVel
if (this.collidesWithAbsolutelyAnything()) {
this.x -= this.xVel
this.y -= this.yVel
}
if (!isZeroVector(dir)) this.playerDirection = dir
if (this.isInputPressed("interact")) this.createInteractHitbox()
else this.interactHitbox = null
}
collidesWithAbsolutelyAnything() {
return this.collidesWithTiles() || this.collidesWithObjects()
}
collidesWithObjects() {
const objects = this.game.currentRoom.objectsUnderRectangle(this)
return !!objects.find(object => object.collides())
}
collidesWithTiles() {
const tur = this.game.currentRoom.tilesUnderRectangle(this).filter(x => x)
const colliders = tur.filter(tile => tile.properties.find(prop => prop.name == "collides" && prop.value))
return !!colliders.length
}
createInteractHitbox() {
this.interactHitbox = {
width: this.width,
height: this.height,
x: this.x + (this.playerDirection.x * this.width / 2),
y: this.y + (this.playerDirection.y * this.height / 2),
}
}
isInputPressed(action) {
return this.game.input.isInputPressed.call(this.game.input, action)
}
inputDirection() {
const isInputPressed = this.game.input.isInputPressed.bind(this.game.input)
const dir = { x: 0, y: 0 }
if (isInputPressed("up")) dir.y -= 1
if (isInputPressed("down")) dir.y += 1
if (isInputPressed("left")) dir.x -= 1
if (isInputPressed("right")) dir.x += 1
if (this.isInputPressed("up")) dir.y -= 1
if (this.isInputPressed("down")) dir.y += 1
if (this.isInputPressed("left")) dir.x -= 1
if (this.isInputPressed("right")) dir.x += 1
if (Math.abs(dir.x, dir.y) == 2) {
dir.x *= SQRT_OF_TWO
@ -47,10 +83,11 @@ export default class Player extends Actor {
draw(ctx) {
this.color = `rgb(128 ${(this.playerDirection.x * 128) + 128} ${(this.playerDirection.y * 128) + 128}`
ctx.beginPath()
ctx.fillStyle = this.color
ctx.rect(this.x, this.y, this.width, this.height)
ctx.fill()
ctx.closePath()
ctx.fillRect(this.x, this.y, this.width, this.height)
if (this.interactHitbox) {
ctx.fillStyle = "#FF000088"
ctx.fillRect(this.interactHitbox.x, this.interactHitbox.y, this.interactHitbox.width, this.interactHitbox.height)
}
}
}

View File

@ -1,8 +1,13 @@
import RoomObject from "./roomObject.js"
import { doRectanglesOverlap } from "./util.js"
export default class Room {
constructor(json, name) {
constructor(game, json, name) {
this.game = game
this.json = json
this.name = name
console.log(json)
const objectJson = this.objectLayers.map(layer => layer.objects).flat()
this.objects = objectJson.map(RoomObject.fromJson.bind(null, this.game))
}
get tilesetsToLoad() {
@ -14,7 +19,6 @@ export default class Room {
}
populateTilesets(assets) {
console.log(this.json)
this.tilesets = this.json.tilesets.map((tileset, index) => {
const ts = assets.get(`${this.name}-${index}`)
ts.populateImage(assets)
@ -23,26 +27,73 @@ export default class Room {
}
draw(ctx) {
this.json.layers.forEach(layer => {
for (let y = 0; y < layer.height; y++) {
for (let x = 0; x < layer.width; x++) {
const index = x + (y * layer.width)
const tileIndex = layer.data[index] - 1
const tileset = this.tilesets[0]
const [sx, sy] = tileset.tileOffset(tileIndex)
ctx.drawImage(
tileset.image,
sx,
sy,
tileset.tileWidth,
tileset.tileHeight,
x * this.json.tilewidth,
y * this.json.tileheight,
this.json.tilewidth,
this.json.tileheight
this.tileLayers.forEach(this.drawTileLayer.bind(this, ctx))
}
drawLayer(ctx, layer) {
if (layer.type == "tilelayer") this.drawTileLayer(ctx, layer)
}
get tileLayers() {
return this.json.layers.filter(layer => layer.type == "tilelayer")
}
get objectLayers() {
return this.json.layers.filter(layer => layer.type == "objectgroup")
}
objectsUnderRectangle(rect) {
return this.objects.filter(object => doRectanglesOverlap(object, rect))
}
tilesUnderRectangle(rect) {
return this.tileLayers.map(layer => this.tilesUnderRectangleInLayer(layer, rect)).flat()
}
tilesUnderRectangleInLayer(layer, rect) {
return [{ x: rect.x, y: rect.y },
{ x: rect.x + rect.width, y: rect.y },
{ x: rect.x, y: rect.y + rect.height },
{ x: rect.x + rect.width, y: rect.y + rect.height }
].map(point => {
const tileset = this.tilesets[0]
const { x, y } = point
const tileX = Math.floor(x / tileset.tileWidth)
const tileY = Math.floor(y / tileset.tileHeight)
const index = tileX + (tileY * layer.width)
const tileIndex = layer.data[index] - 1
return tileset.tileAt(tileIndex)
})
}
drawTileLayer(ctx, layer) {
for (let y = 0; y < layer.height; y++) {
for (let x = 0; x < layer.width; x++) {
const index = x + (y * layer.width)
const tileIndex = layer.data[index] - 1
const tileset = this.tilesets[0]
const [sx, sy] = tileset.tileOffset(tileIndex)
ctx.drawImage(
tileset.image,
sx,
sy,
tileset.tileWidth,
tileset.tileHeight,
x * this.json.tilewidth,
y * this.json.tileheight,
this.json.tilewidth,
this.json.tileheight
)
if (tileset.collides(tileIndex)) {
ctx.fillStyle = "#aa660088"
ctx.fillRect(
x * this.json.tilewidth,
y * this.json.tileheight,
this.json.tilewidth,
this.json.tileheight
)
}
}
})
}
}
}

52
src/roomObject.js Normal file
View File

@ -0,0 +1,52 @@
import { doRectanglesOverlap } from "./util.js"
export default class RoomObject {
constructor(game) {
this.game = game
}
static fromJson(game, json) {
const roomObject = new RoomObject(game)
Object.entries(json).forEach(([key, value]) => roomObject[key] = value)
return roomObject
}
getProperty(name) {
const property = this.properties.find(p => p.name == name)
if (!property) {
// console.error(`Unknown property ${name} on ${this.name}`)
return null
}
return property.value
}
setProperty(name, value) {
const p = this.properties.find(p => p.name == name)
if (p) {
p.value = value
} else {
this.properties[name] = value
}
}
tick(dt) {
const { player } = this.game
if (doRectanglesOverlap(player, this)) {
const eventName = this.getProperty.call(this, "event")
if (eventName) this.game.triggerEvent(eventName, this)
}
if (player.interactHitbox && doRectanglesOverlap(player.interactHitbox, this)) {
const eventName = this.getProperty("interactEvent")
if (eventName) this.game.triggerEvent(eventName, this)
}
}
draw(ctx) {
ctx.fillStyle = this.getProperty("color") || "#00000000"
ctx.fillRect(this.x, this.y, this.width, this.height)
}
collides() {
return this.getProperty("collides") || false
}
}

View File

@ -1,7 +1,9 @@
export default class Tileset {
constructor(json, name) {
constructor(game, json, name) {
this.game = game
this.json = json
this.name = name
this.tiles = json.tiles
}
get imagesToLoad() {
@ -32,4 +34,15 @@ export default class Tileset {
(Math.floor(index / this.columns) * this.tileHeight)
]
}
tileAt(index) {
return this.tiles.find(tile => tile.id == index)
}
collides(index) {
const tile = this.tileAt(index)
if (!tile) return
return tile.properties.find(prop => prop.name == "collides").value == "true"
}
}

View File

@ -2,7 +2,29 @@ const SQRT_OF_TWO = Math.sqrt(2)
const isZeroVector = vector => !vector.x && !vector.y
const doRectanglesOverlap = (rect1, rect2) => {
return (
doLengthsOverlap({ x: rect1.x, width: rect1.width }, { x: rect2.x, width: rect2.width })
&&
doLengthsOverlap({ x: rect1.y, width: rect1.height }, { x: rect2.y, width: rect2.height })
)
}
const doLengthsOverlap = (l1, l2) => {
return !((l1.x + l1.width < l2.x) || (l2.x + l2.width) < l1.x)
}
function sum(array) {
return array.reduce((a, b) => a + b, 0)
}
function average(array) {
return sum(array) / array.length
}
export {
SQRT_OF_TWO,
isZeroVector
isZeroVector,
doRectanglesOverlap,
sum, average
}

View File

@ -8,6 +8,358 @@
"tilecount":260,
"tiledversion":"1.11.2",
"tileheight":64,
"tiles":[
{
"id":60,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":61,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":62,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":63,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":64,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":65,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":66,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":80,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":81,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":82,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":83,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":84,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":85,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":86,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":100,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":101,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":102,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":103,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":144,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":145,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":146,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":164,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":165,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":166,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":180,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":182,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":184,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":200,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":201,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":202,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":203,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":204,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":205,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":220,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":221,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":222,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":223,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":224,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":225,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":64,
"type":"tileset",
"version":"1.10"