LevelEditorState.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. import { AsyncDataWriter } from "../../libraries/AsyncDataWriter.js";
  2. import { Camera } from "../../libraries/Camera.js";
  3. import { TileMap } from "../../libraries/components/TileMap.js";
  4. import { Point } from "../../libraries/spatial/Point.js";
  5. import { SpritesheetAtlas } from "../../libraries/SpritesheetAtlas.js";
  6. import { TweenManager, Tween, Easing } from "../../libraries/Tween.js";
  7. export class LevelEditorState {
  8. constructor(view) {
  9. this.stateMachine = view.stateMachine
  10. this.canvasBounds = null
  11. this.camera = new Camera()
  12. this.camera.scale = new Point(2, 2)
  13. this.tweenManager = new TweenManager()
  14. this.keysPressed = {}
  15. this.map = new TileMap()
  16. this.primaryTile = { x: 0, y: 0, atlas: 0 }
  17. this.secondaryTile = { x: 1, y: 0, atlas: 0 }
  18. this.history = []
  19. this.placementMode = "tile"
  20. }
  21. init(self) {
  22. let params = new URLSearchParams(window.location.search)
  23. let roomPath = params.get('room') ?? "room1"
  24. this.map.loadFromApi(roomPath).then(() => {
  25. this.map.mapData.spritesheets.forEach((sheetpath) => {
  26. window.open(`./sheetviewer.html?sheet=${sheetpath}`, `Spritesheet Viewer ${sheetpath}`, 'height=400,width=600')
  27. })
  28. })
  29. this.history = []
  30. }
  31. draw(ctx, scaledCanvas) {
  32. this.canvasBounds = scaledCanvas.bounds;
  33. if (!this.map.loaded) {
  34. return
  35. }
  36. this.camera.draw(ctx, scaledCanvas, () => {
  37. if (this.map.mapData) {
  38. ctx.strokeStyle = "#000000"
  39. ctx.beginPath()
  40. ctx.rect(
  41. this.map.mapData.bounds[0] * 16 - 8,
  42. this.map.mapData.bounds[1] * 16 - 8,
  43. this.map.mapData.bounds[2] * 16,
  44. this.map.mapData.bounds[3] * 16)
  45. ctx.stroke()
  46. }
  47. let mode = localStorage.getItem("teetopia-editor-placement") ?? "tile"
  48. switch (mode) {
  49. case "wall":
  50. this.map.draw(ctx, scaledCanvas)
  51. this.map.drawDecals(ctx, scaledCanvas)
  52. this.map.drawOverDecals(ctx, scaledCanvas)
  53. this.map.drawCollision(ctx, scaledCanvas)
  54. break;
  55. case "door":
  56. this.map.draw(ctx, scaledCanvas)
  57. this.map.drawDecals(ctx, scaledCanvas)
  58. this.map.drawOverDecals(ctx, scaledCanvas)
  59. this.map.drawDoors(ctx, scaledCanvas)
  60. break
  61. case "tile":
  62. this.map.draw(ctx, scaledCanvas)
  63. this.map.drawDecals(ctx, scaledCanvas)
  64. this.map.drawOverDecals(ctx, scaledCanvas)
  65. this.map.drawCoords(ctx, scaledCanvas)
  66. break
  67. case "decal-bottom":
  68. this.map.draw(ctx, scaledCanvas)
  69. this.map.drawDecals(ctx, scaledCanvas)
  70. this.map.drawDecalCoords(ctx, scaledCanvas)
  71. break
  72. case "decal-top":
  73. this.map.draw(ctx, scaledCanvas)
  74. this.map.drawOverDecals(ctx, scaledCanvas)
  75. this.map.drawDecalCoords(ctx, scaledCanvas)
  76. break
  77. default:
  78. break;
  79. }
  80. })
  81. ctx.save()
  82. ctx.scale(2, 2)
  83. ctx.translate(16, 16)
  84. ctx.strokeStyle = "green"
  85. ctx.beginPath()
  86. ctx.rect(-8, -8, 16, 16)
  87. ctx.closePath()
  88. ctx.stroke()
  89. ctx.fillStyle = "white"
  90. ctx.textAlign = "left"
  91. ctx.font = "18px Arial"
  92. ctx.fillText(this.placementMode, 16, 6)
  93. let primaryAtlas = this.map.getAtlas(this.primaryTile.atlas)
  94. if (primaryAtlas) {
  95. let sprite = primaryAtlas.getDrawableSprite(this.primaryTile.x, this.primaryTile.y)
  96. sprite.draw(ctx, scaledCanvas)
  97. ctx.textAlign = "right"
  98. ctx.font = "6px Arial"
  99. ctx.fillStyle = "rgba(255,255,255,0.4)"
  100. ctx.fillText(`${this.primaryTile.x},${this.primaryTile.y}`, 8, 8)
  101. }
  102. ctx.restore()
  103. ctx.save()
  104. ctx.scale(2, 2)
  105. ctx.translate(16, 48)
  106. ctx.strokeStyle = "blue"
  107. ctx.beginPath()
  108. ctx.rect(-8, -8, 16, 16)
  109. ctx.closePath()
  110. ctx.stroke()
  111. let secondAtlas = this.map.getAtlas(this.secondaryTile.atlas)
  112. if (secondAtlas) {
  113. let secondarySprite = secondAtlas.getDrawableSprite(this.secondaryTile.x, this.secondaryTile.y)
  114. secondarySprite.draw(ctx, scaledCanvas)
  115. ctx.textAlign = "right"
  116. ctx.font = "6px Arial"
  117. ctx.fillStyle = "rgba(255,255,255,0.4)"
  118. ctx.fillText(`${this.secondaryTile.x},${this.secondaryTile.y}`, 8, 8)
  119. }
  120. ctx.restore()
  121. }
  122. update(delta) {
  123. if (this.keysPressed['w'] || this.keysPressed['ArrowUp']) {
  124. this.camera.position.y -= 16
  125. }
  126. if (this.keysPressed['a'] || this.keysPressed['ArrowLeft']) {
  127. this.camera.position.x -= 16
  128. }
  129. if (this.keysPressed['s'] || this.keysPressed['ArrowDown']) {
  130. this.camera.position.y += 16
  131. }
  132. if (this.keysPressed['d'] || this.keysPressed['ArrowRight']) {
  133. this.camera.position.x += 16
  134. }
  135. this.camera.update(delta);
  136. this.tweenManager.update()
  137. if (!this.map.mapData) {
  138. return
  139. }
  140. let tileString = localStorage.getItem('teetopia-editor-primary') ?? `0,0,${this.map.mapData.spritesheets[0]}`
  141. let tile = tileString.split(",")
  142. this.primaryTile.x = parseInt(tile[0])
  143. this.primaryTile.y = parseInt(tile[1])
  144. let primaryAtlas = parseInt(this.map.mapData.spritesheets.indexOf(tile[2]))
  145. if (primaryAtlas == -1) {
  146. this.map.mapData.spritesheets.push(tile[2])
  147. primaryAtlas = this.map.mapData.spritesheets.length - 1
  148. this.map.reloadAtlas(primaryAtlas)
  149. }
  150. this.primaryTile.atlas = primaryAtlas
  151. let secondTileString = localStorage.getItem('teetopia-editor-secondary') ?? `1,0,${this.map.mapData.spritesheets[0]}`
  152. let secondTile = secondTileString.split(",")
  153. this.secondaryTile.x = parseInt(secondTile[0])
  154. this.secondaryTile.y = parseInt(secondTile[1])
  155. let secondaryAtlas = parseInt(this.map.mapData.spritesheets.indexOf(secondTile[2]))
  156. if (secondaryAtlas == -1) {
  157. this.map.mapData.spritesheets.push(secondTile[2])
  158. secondaryAtlas = this.map.mapData.spritesheets.length - 1
  159. this.map.reloadAtlas(secondaryAtlas)
  160. }
  161. this.secondaryTile.atlas = secondaryAtlas
  162. this.placementMode = localStorage.getItem('teetopia-editor-placement') ?? "tile"
  163. }
  164. enter() {
  165. this.init(this.stateMachine.getState("view"))
  166. this.registeredEvents = {}
  167. this.registeredEvents["resize"] = this.onResize
  168. this.registeredEvents["keydown"] = this.onKeyDown
  169. this.registeredEvents["keyup"] = this.onKeyUp
  170. this.registeredEvents["touchstart"] = this.onTouchStart
  171. this.registeredEvents['click'] = this.onClick
  172. this.registeredEvents['mousemove'] = this.onMouseMove
  173. this.registeredEvents['contextmenu'] = this.onClick
  174. for (let index in this.registeredEvents) {
  175. window.addEventListener(index, this.registeredEvents[index].bind(this))
  176. }
  177. }
  178. leave() {
  179. for (let index in this.registeredEvents) {
  180. window.removeEventListener(index, this.registeredEvents[index])
  181. }
  182. this.tweenManager.clear()
  183. }
  184. onFinish() {
  185. }
  186. onResize() {
  187. }
  188. onKeyDown(event) {
  189. this.keysPressed[event.key] = true
  190. switch (event.key) {
  191. case "s":
  192. if (event.ctrlKey) {
  193. event.preventDefault()
  194. this.saveRoom()
  195. this.keysPressed[event.key] = false
  196. return false
  197. }
  198. break
  199. }
  200. }
  201. async saveRoom() {
  202. let params = new URLSearchParams(window.location.search)
  203. let roomPath = params.get('room')
  204. let sessionInfoString = localStorage.getItem('teetopia-session');
  205. if (!sessionInfoString) {
  206. alert("You must be logged in to save a room.")
  207. return
  208. }
  209. let sessionInfo = JSON.parse(sessionInfoString)
  210. console.log(sessionInfo)
  211. let request = {
  212. token: sessionInfo.token,
  213. roomData: this.map.mapData,
  214. }
  215. if (request.roomData.owner == "") {
  216. request.roomData.owner = sessionInfo.email;
  217. }
  218. let result;
  219. try {
  220. result = await new AsyncDataWriter().post(`./api/room/${roomPath}/save`, request)
  221. } catch (e) {
  222. console.error(result, e);
  223. }
  224. alert(result.responseMessage)
  225. }
  226. onKeyUp(event) {
  227. event.preventDefault()
  228. this.keysPressed[event.key] = false
  229. switch (event.key) {
  230. case "Escape":
  231. window.location = "./index.html"
  232. break
  233. case "Control":
  234. break;
  235. case "z":
  236. if (event.ctrlKey) {
  237. if (this.history.length == 0) {
  238. return
  239. }
  240. let lastStep = this.history.pop()
  241. let mapTile = this.map.getTile(lastStep.position.x, lastStep.position.y)
  242. mapTile[0] = lastStep.previous[0]
  243. mapTile[1] = lastStep.previous[1]
  244. mapTile[2] = lastStep.previous[2]
  245. }
  246. break
  247. case "1":
  248. localStorage.setItem("teetopia-editor-placement", "tile")
  249. break;
  250. case "2":
  251. localStorage.setItem("teetopia-editor-placement", "decal-bottom")
  252. break;
  253. case "3":
  254. localStorage.setItem("teetopia-editor-placement", "decal-top")
  255. break;
  256. case "4":
  257. localStorage.setItem("teetopia-editor-placement", "wall")
  258. break;
  259. case "5":
  260. localStorage.setItem("teetopia-editor-placement", "door")
  261. break;
  262. default:
  263. // console.log(event.key, event)
  264. }
  265. }
  266. onTouchStart(event) {
  267. let touchPosition = new Point(event.changedTouches[0].clientX, event.changedTouches[0].clientY)
  268. let worldTouchPosition = this.camera.screenToWorld(touchPosition)
  269. let delta = worldTouchPosition.difference(this.player.position)
  270. if (Math.abs(delta.x) > Math.abs(delta.y)) {
  271. if (Math.sign(delta.x) < 0) {
  272. this.keysPressed["ArrowLeft"] = true
  273. } else {
  274. this.keysPressed["ArrowRight"] = true
  275. }
  276. } else if (Math.abs(delta.x) < Math.abs(delta.y)) {
  277. if (Math.sign(delta.y) < 0) {
  278. this.keysPressed["ArrowUp"] = true
  279. } else {
  280. this.keysPressed["ArrowDown"] = true
  281. }
  282. }
  283. }
  284. onClick(event) {
  285. event.preventDefault()
  286. let clickPosition = new Point(event.clientX, event.clientY)
  287. let worldClickPosition = this.camera.screenToWorld(clickPosition)
  288. worldClickPosition.snapToGrid({ width: 16, height: 16, margin: 1 })
  289. worldClickPosition.scale(1 / 16, 1 / 16)
  290. let mode = localStorage.getItem("teetopia-editor-placement") ?? "tile"
  291. switch (mode) {
  292. case "door":
  293. if (
  294. worldClickPosition.x > this.map.mapData.bounds[0] + this.map.mapData.bounds[2] ||
  295. worldClickPosition.y > this.map.mapData.bounds[1] + this.map.mapData.bounds[3] ||
  296. worldClickPosition.x < this.map.mapData.bounds[0] ||
  297. worldClickPosition.y < this.map.mapData.bounds[1]
  298. ) {
  299. return;
  300. }
  301. if (event.button == 0) {
  302. let shouldPlace = confirm(`Do you want to place an entrance at ${worldClickPosition.x}, ${worldClickPosition.y}?`)
  303. if (shouldPlace) {
  304. this.map.mapData.exits.push([worldClickPosition.x, worldClickPosition.y])
  305. alert(`Entrance placed at ${worldClickPosition.x}, ${worldClickPosition.y}`)
  306. }
  307. } else if (event.button == 2) {
  308. let roomFile = prompt("Please input the name of the desired room:", "room1")
  309. let roomExit = prompt("Please select an exit:", 0)
  310. let tile = this.map.getTile(worldClickPosition.x, worldClickPosition.y)
  311. tile[2][1] = [roomFile, roomExit]
  312. }
  313. break
  314. case "wall":
  315. try {
  316. let tile = this.map.getTile(worldClickPosition.x, worldClickPosition.y)
  317. let copiedTile = JSON.parse(JSON.stringify(tile))
  318. this.history.push({ "position": worldClickPosition, "previous": copiedTile })
  319. if (event.button == 2) {
  320. tile[2][0] = 0
  321. } else {
  322. tile[2][0] = 1
  323. }
  324. } catch (e) { }
  325. break;
  326. case "decal-bottom":
  327. try {
  328. let tile = this.map.getTile(worldClickPosition.x, worldClickPosition.y)
  329. let copiedTile = JSON.parse(JSON.stringify(tile))
  330. this.history.push({ "position": worldClickPosition, "previous": copiedTile })
  331. if (event.button == 2) {
  332. tile[3].pop()
  333. } else {
  334. tile[3].push([this.primaryTile.x, this.primaryTile.y, this.primaryTile.atlas])
  335. }
  336. } catch (e) { }
  337. break;
  338. case "decal-top":
  339. try {
  340. let tile = this.map.getTile(worldClickPosition.x, worldClickPosition.y)
  341. let copiedTile = JSON.parse(JSON.stringify(tile))
  342. this.history.push({ "position": worldClickPosition, "previous": copiedTile })
  343. if (event.button == 2) {
  344. tile[4].pop()
  345. } else {
  346. tile[4].push([this.primaryTile.x, this.primaryTile.y, this.primaryTile.atlas])
  347. }
  348. } catch (e) { }
  349. break;
  350. case "tile":
  351. default:
  352. try {
  353. let tile = this.map.getTile(worldClickPosition.x, worldClickPosition.y)
  354. if (!tile) {
  355. let newTile = []
  356. newTile[0] = [worldClickPosition.x, worldClickPosition.y]
  357. if (event.button == 2) {
  358. newTile[1] = [this.secondaryTile.x, this.secondaryTile.y, this.secondaryTile.atlas]
  359. } else {
  360. newTile[1] = [this.primaryTile.x, this.primaryTile.y, this.primaryTile.atlas]
  361. }
  362. newTile[2] = [0]
  363. newTile[3] = []
  364. newTile[4] = []
  365. this.map.mapData.tiles.push(newTile)
  366. return
  367. }
  368. let copiedTile = JSON.parse(JSON.stringify(tile))
  369. this.history.push({ "position": worldClickPosition, "previous": copiedTile })
  370. if (event.button == 2) {
  371. tile[1] = [this.secondaryTile.x, this.secondaryTile.y, this.secondaryTile.atlas]
  372. } else {
  373. tile[1] = [this.primaryTile.x, this.primaryTile.y, this.primaryTile.atlas]
  374. }
  375. } catch (e) {
  376. if ((
  377. worldClickPosition.x > this.map.mapData.bounds[0] + this.map.mapData.bounds[2] ||
  378. worldClickPosition.y > this.map.mapData.bounds[1] + this.map.mapData.bounds[3]
  379. ) && confirm("Do you want to expand the map?")) {
  380. // if (worldClickPosition.x < this.map.mapData.bounds[0]) {
  381. // this.map.mapData.bounds[2] = (this.map.mapData.bounds[2] - worldClickPosition.x) + this.map.mapData.bounds[0]
  382. // this.map.mapData.bounds[0] = worldClickPosition.x
  383. // }
  384. if (worldClickPosition.x > this.map.mapData.bounds[0] + this.map.mapData.bounds[2]) {
  385. this.map.mapData.bounds[2] = worldClickPosition.x - this.map.mapData.bounds[0]
  386. }
  387. // if (worldClickPosition.y < this.map.mapData.bounds[1]) {
  388. // this.map.mapData.bounds[3] = (this.map.mapData.bounds[3] - worldClickPosition.y) + this.map.mapData.bounds[1]
  389. // this.map.mapData.bounds[1] = worldClickPosition.y
  390. // }
  391. if (worldClickPosition.y > this.map.mapData.bounds[1] + this.map.mapData.bounds[3]) {
  392. this.map.mapData.bounds[3] = worldClickPosition.y - this.map.mapData.bounds[1]
  393. }
  394. }
  395. }
  396. break;
  397. }
  398. }
  399. onMouseMove(event) {
  400. event.preventDefault()
  401. if (event.buttons == 0) {
  402. return
  403. }
  404. let clickPosition = new Point(event.clientX, event.clientY)
  405. let worldClickPosition = this.camera.screenToWorld(clickPosition)
  406. worldClickPosition.snapToGrid({ width: 16, height: 16, margin: 1 })
  407. worldClickPosition.scale(1 / 16, 1 / 16)
  408. let mode = localStorage.getItem("teetopia-editor-placement") ?? "tile"
  409. switch (mode) {
  410. case "door":
  411. case "decal-bottom":
  412. case "decal-top":
  413. break;
  414. case "wall":
  415. try {
  416. let tile = this.map.getTile(worldClickPosition.x, worldClickPosition.y)
  417. if (event.buttons == 2) {
  418. if (tile[2][0] == 1) {
  419. let copiedTile = JSON.parse(JSON.stringify(tile))
  420. this.history.push({ "position": worldClickPosition, "previous": copiedTile })
  421. tile[2][0] = 0
  422. }
  423. } else if (event.buttons == 1) {
  424. if (tile[2][0] == 0) {
  425. let copiedTile = JSON.parse(JSON.stringify(tile))
  426. this.history.push({ "position": worldClickPosition, "previous": copiedTile })
  427. tile[2][0] = 1
  428. }
  429. }
  430. } catch (e) { }
  431. break;
  432. case "tile":
  433. default:
  434. try {
  435. let tile = this.map.getTile(worldClickPosition.x, worldClickPosition.y)
  436. if (!tile) {
  437. let newTile = []
  438. newTile[0] = [worldClickPosition.x, worldClickPosition.y]
  439. if (event.button == 2) {
  440. newTile[1] = [this.secondaryTile.x, this.secondaryTile.y, this.secondaryTile.atlas]
  441. } else {
  442. newTile[1] = [this.primaryTile.x, this.primaryTile.y, this.primaryTile.atlas]
  443. }
  444. newTile[2] = [0]
  445. newTile[3] = []
  446. newTile[4] = []
  447. this.map.mapData.tiles.push(newTile)
  448. return
  449. }
  450. if (event.buttons == 2) {
  451. if (tile[1][0] != this.secondaryTile.x ||
  452. tile[1][1] != this.secondaryTile.y ||
  453. tile[1][2] != this.secondaryTile.atlas) {
  454. let copiedTile = JSON.parse(JSON.stringify(tile))
  455. this.history.push({ "position": worldClickPosition, "previous": copiedTile })
  456. tile[1] = [this.secondaryTile.x, this.secondaryTile.y, this.secondaryTile.atlas]
  457. }
  458. } else if (event.buttons == 1) {
  459. if (tile[1][0] != this.primaryTile.x ||
  460. tile[1][1] != this.primaryTile.y ||
  461. tile[1][2] != this.primaryTile.atlas) {
  462. let copiedTile = JSON.parse(JSON.stringify(tile))
  463. this.history.push({ "position": worldClickPosition, "previous": copiedTile })
  464. tile[1] = [this.primaryTile.x, this.primaryTile.y, this.primaryTile.atlas]
  465. }
  466. }
  467. } catch (e) {
  468. }
  469. break;
  470. }
  471. }
  472. }