diff --git a/json2map.rb b/json2map.rb new file mode 100644 index 0000000..4181152 --- /dev/null +++ b/json2map.rb @@ -0,0 +1,10 @@ +require "./celeste_map_reader" +require 'ruby2d' +require 'pry' +# fn = 'app/Content/Maps/1-ForsakenCity.bin' + +ARGV.each do |fn| + base = File.basename(fn, ".json") + a = CelesteMapReader.new(fn, fmt: :json) + a.write "bin/#{base}.bin" +end diff --git a/map2xml.rb b/map2xml.rb index ca48449..13c6a9a 100644 --- a/map2xml.rb +++ b/map2xml.rb @@ -1,9 +1,10 @@ require "./celeste_map_reader" require 'ruby2d' - # fn = 'app/Content/Maps/1-ForsakenCity.bin' ARGV.each do |fn| + base = File.basename(fn, ".bin") a = CelesteMapReader.new(fn) - File.open("#{File.basename(fn)}.xml", "wb") { |f| f.write a.root.inspect } + File.open("#{base}.xml", "wb") { |f| f.write a.root.inspect } + a.write_json("#{base}.json") end diff --git a/viewer.js b/viewer.js new file mode 100644 index 0000000..9edd4f5 --- /dev/null +++ b/viewer.js @@ -0,0 +1,448 @@ +// Demo https://codepen.io/deltabouche/full/WJPbaQ/ + +// Requires p5.js and lodash + +const MapBase = "http://localhost:3000/"; + +const Maps = [ + "0-Intro", + "1-ForsakenCity", + "1H-ForsakenCity", + "1X-ForsakenCity", + "2-OldSite", + "2H-OldSite", + "2X-OldSite", + "3-CelestialResort", + "3H-CelestialResort", + "3X-CelestialResort", + "4-GoldenRidge", + "4H-GoldenRidge", + "4X-GoldenRidge", + "5-MirrorTemple", + "5H-MirrorTemple", + "5X-MirrorTemple", + "6-Reflection", + "6H-Reflection", + "6X-Reflection", + "7-Summit", + "7H-Summit", + "7X-Summit", + "8-Epilogue", + "9-Core", + "9H-Core", + "9X-Core" +] + +let state = { + mapPath: "0-Intro", + dataReady: false, + camera: { + x: 200, + y: 200, + scale: 1 + }, + levelCamera: { + x: 5, + y: 5 + }, + screen: { + width: 1280, + height: 720 + }, + map: null, + thumbnails: [], + levelIndex: -1, + hoverLevel: -1 +}; + +const SolidColors = { + "0": "#000000", //0 + "1": "#FF0000", //1 + "2": "#00FF00", //2 + "3": "#0000FF", //3 + "4": "#FF00FF", //4 + "5": "#FFFF00", //5 + "6": "#00FFFF", //6 + "7": "#888888", //7 + "g": "#FF00FF" +}; + +function setupThumbnails() { + const levels = childByName(state.map.root, 'levels'); + let visIndex = 0; + _.each(levels.children, (lvl, lvlIndex) => { + let pg = createGraphics(lvl.attributes.width, lvl.attributes.height); + const solids = childrenByName(lvl, 'solids'); + _.each(solids, solid => { + let tx = 0; + let ty = 0; + _.each(solid.attributes.innerText, v => { + if (v == 10) { + ty++; + tx=0; + return; + } + const tRect = { + x: solid.attributes.offsetX + tx * 8 + 1, + y: solid.attributes.offsetY + ty * 8 + 1, + width: 7, + height: 7 + } + pg.noStroke(); + const color = SolidColors[String.fromCharCode(v)] || 200; + pg.fill(color); + pg.rect(tRect.x / 2, tRect.y/ 2, tRect.width/ 2, tRect.height/ 2); + tx++; + }); + }); + state.thumbnails.push(pg) + }); +} + +function resetCamera() { + state.camera = { x: 200, y: 200, scale: 0.5 }; +} + +function resetLevelCamera() { + state.levelCamera = { x: 5, y: 5 }; +} + +function loadMap(path) { + state.dataReady = false; + state.thumbnails = []; + state.map = null; + state.mapPath = path; + state.levelIndex = -1; + fetch(`${MapBase}/${state.mapPath}.json`).then(res => res.json()).then(json => { + state.map = json; + setupThumbnails(); + state.dataReady = true; + addLevelSelect(); + resetCamera(); + }) +} + +function loadLevel(idx) { + if (idx >= 0) resetLevelCamera(); + state.levelIndex = parseInt(idx, 10); +} + +function setup() { + addMapSelect(); + let canvasElement = createCanvas(state.screen.width, state.screen.height).elt; + canvasElement.addEventListener('contextmenu', event => { event.preventDefault(); }); + let context = canvasElement.getContext('2d'); + context.mozImageSmoothingEnabled = false; + context.webkitImageSmoothingEnabled = false; + context.msImageSmoothingEnabled = false; + context.imageSmoothingEnabled = false; + loadMap(state.mapPath); +} + +function childrenByName(el, name) { + return _.filter(el.children, { name }); +} + +function childByName(el, name) { + return _.find(el.children, { name }); +} + +function drawAxes() { + stroke("#0000FF"); + line(state.camera.x, 0, state.camera.x, state.screen.height); + line(0, state.camera.y, state.screen.width, state.camera.y); +} + +function drawHeadings() { + text(state.map.root.package, 20, 20); +} + +function screenTransformRect(rect) { + return { + x: rect.x * state.camera.scale + state.camera.x, + y: rect.y * state.camera.scale + state.camera.y, + width: rect.width * state.camera.scale, + height: rect.height * state.camera.scale + } +} + +function screenLevelTransformRect(rect) { + return { + x: (rect.x + state.levelCamera.x * 8) * 2, + y: (rect.y + state.levelCamera.y * 8) * 2, + width: rect.width * 2, + height: rect.height * 2 + } +} + +function pointInRect(x, y, rect) { + return x > rect.x && x < rect.x + rect.width && y > rect.y && y < rect.y + rect.height; +} + +function rectIsVisible(rect) { + return rect.x + rect.width > 0 && rect.y + rect.height > 0 && rect.x < state.screen.width && rect.y < state.screen.height +} + +function drawFillers() { + const fillers = childByName(state.map.root, 'Filler'); + _.each(fillers.children, (fillRect) => { + const drawRect = screenTransformRect({ + x: fillRect.attributes.x * 8, + y: fillRect.attributes.y * 8, + width: fillRect.attributes.w * 8, + height: fillRect.attributes.h * 8 + }); + if (!rectIsVisible(drawRect)) return; + stroke("#FF0000"); + fill("#330000"); + rect(drawRect.x, drawRect.y, drawRect.width, drawRect.height); + }); +} + +function drawLevels() { + const levels = childByName(state.map.root, 'levels'); + let visIndex = 0; + _.each(levels.children, (lvl, lvlIndex) => { + fill(255); + stroke(255); + const drawRect = screenTransformRect(lvl.attributes); + if (!rectIsVisible(drawRect)) return; + // text(`${drawRect.x}, ${drawRect.y}, ${drawRect.width}, ${drawRect.height}`, 10, 80 + 20 * visIndex); + visIndex++; + image(state.thumbnails[lvlIndex], drawRect.x, drawRect.y, drawRect.width, drawRect.height); + noFill(); + if (pointInRect(mouseX, mouseY, screenTransformRect(lvl.attributes))) { + text(lvl.attributes.name, 10, 80); + stroke(255); + state.hoverLevel = lvlIndex; + } else { + stroke(64); + } + rect(drawRect.x, drawRect.y, drawRect.width, drawRect.height); + }); +} + +function processInput() { + if (state.levelIndex === -1) { + if (keyIsDown(LEFT_ARROW)) { + state.camera.x += 8; + } + if (keyIsDown(RIGHT_ARROW)) { + state.camera.x -= 8; + } + if (keyIsDown(UP_ARROW)) { + state.camera.y += 8; + } + + if (keyIsDown(DOWN_ARROW)) { + state.camera.y -= 8; + } + } else { + if (keyIsDown(LEFT_ARROW)) { + state.levelCamera.x += 1; + } + if (keyIsDown(RIGHT_ARROW)) { + state.levelCamera.x -= 1; + } + if (keyIsDown(UP_ARROW)) { + state.levelCamera.y += 1; + } + + if (keyIsDown(DOWN_ARROW)) { + state.levelCamera.y -= 1; + } + } +} + +function keyPressed() { + if (keyCode === 81) { + state.camera.x *= 1.5; + state.camera.y *= 1.5; + state.camera.scale *= 1.5; + } + if (keyCode === 65) { + state.camera.x /= 1.5; + state.camera.y /= 1.5; + state.camera.scale /= 1.5; + } + if (keyCode === 27) { + let select = document.getElementById('level-select'); + select.value = -1; + loadLevel(-1); + } +} + +function mousePressed() { + if (mouseButton === RIGHT) { + if (state.hoverLevel >= 0) { + let idx = state.hoverLevel; + state.hoverLevel = -1; + let select = document.getElementById('level-select'); + select.value = idx; + loadLevel(idx); + } + } +} + +function drawHud() { + text(`${mouseX}, ${mouseY}`, 10, 50); +} + +function drawLoading() { + text("Loading...", state.screen.width / 2, state.screen.height / 2); +} + +function drawOverview() { + drawAxes(); + drawHud(); + drawHeadings(); + drawFillers(); + drawLevels(); +} + +function drawLevel() { + const levels = childByName(state.map.root, 'levels'); + const level = levels.children[state.levelIndex]; + + // draw surrounding levels + _.each(levels.children, (sLevel, i) => { + if (i == state.levelIndex) return; + const drawRect = screenLevelTransformRect({ + x: sLevel.attributes.x - level.attributes.x, + y: sLevel.attributes.y - level.attributes.y, + width: sLevel.attributes.width, + height: sLevel.attributes.height + }); + if (!rectIsVisible(drawRect)) return; + if (pointInRect(mouseX, mouseY, drawRect)) { + stroke(128, 128, 128, 255); + tint(255, 192); + state.hoverLevel = i; + } else { + stroke(128, 128, 128, 128); + tint(255, 128); + } + image(state.thumbnails[i], drawRect.x, drawRect.y, drawRect.width, drawRect.height); + noFill(); + rect(drawRect.x, drawRect.y, drawRect.width, drawRect.height); + tint(255, 255); + }); + + + // noFill(64); + // rect((state.levelCamera.x * 16) + 16, (state.levelCamera.y * 16) + 16, level.attributes.width * 2, level.attributes.height * 2); + + const solids = childByName(level, 'solids'); + const solidMap = solids.attributes.innerText; + let tx = 0; + let ty = 0; + + const levelDrawRect = screenLevelTransformRect({ + x: 0, + y: 0, + width: level.attributes.width, + height: level.attributes.height + }) + + image(state.thumbnails[state.levelIndex], levelDrawRect.x, levelDrawRect.y, levelDrawRect.width, levelDrawRect.height); + +// _.each(solidMap, (v, i) => { +// if (v === 10) { +// ty++; +// tx=0; +// return; +// } +// const drawRect = screenLevelTransformRect({ +// x: tx * 8, +// y: ty * 8, +// width: 8, +// height: 8 +// }) +// tx++; +// if (!rectIsVisible(drawRect)) return; + +// stroke(32); + +// // const color = SolidColors[String.fromCharCode(v)] || 200; +// // fill(color); + + +// // rect(drawRect.x, drawRect.y, drawRect.width, drawRect.height); +// fill(0); +// text(String.fromCharCode(v), drawRect.x + 6, drawRect.y + 12); +// }); + + const entities = childByName(level, 'entities'); + _.each(entities.children, entity => { + const drawRect = screenLevelTransformRect({ + x: entity.attributes.x - (entity.attributes.originX || 0), + y: entity.attributes.y - (entity.attributes.originY || 0), + width: entity.attributes.width || 8, + height: entity.attributes.height || 8 + }) + if (!rectIsVisible(drawRect)) return; + stroke(255, 255, 255, 128); + fill(255, 255, 255, 128); + text(entity.name, drawRect.x, drawRect.y); + stroke(255, 64, 64, 128); + fill(64, 0, 0, 128); + rect(drawRect.x, drawRect.y, drawRect.width, drawRect.height); + }) + stroke(128); + noFill(); + rect(levelDrawRect.x, levelDrawRect.y, levelDrawRect.width, levelDrawRect.height); +} + +function draw() { + processInput(); + background(0); + stroke(255); + fill(255); + if (state.dataReady) { + if (state.levelIndex === -1) { + drawOverview(); + } else { + drawLevel(); + } + } else { + drawLoading(); + } +} + +function addMapSelect() { + let mapSelectContainer = document.getElementById("map-select-container"); + let selectList = document.createElement("select"); + selectList.id = "map-select"; + mapSelectContainer.appendChild(selectList); + _.each(Maps, mapName => { + let option = document.createElement("option"); + option.value = mapName; + option.text = mapName; + selectList.appendChild(option); + }) + selectList.addEventListener('change', function(){ + loadMap(this.value); + }); +} + +function addLevelSelect() { + let levelSelectContainer = document.getElementById("level-select-container"); + levelSelectContainer.innerHTML = ""; + let selectList = document.createElement("select"); + selectList.id = "level-select"; + levelSelectContainer.appendChild(selectList); + let defOption = document.createElement("option"); + defOption.value = -1; + defOption.text = "Overview"; + selectList.appendChild(defOption); + const levels = childByName(state.map.root, 'levels'); + _.each(levels.children, (lvl, idx) => { + let option = document.createElement("option"); + option.value = idx; + option.text = lvl.attributes.name; + selectList.appendChild(option); + }) + selectList.addEventListener('change', function(){ + loadLevel(this.value); + }); +} \ No newline at end of file