387 lines
12 KiB
JavaScript
387 lines
12 KiB
JavaScript
|
function init() {
|
||
|
const map = document.querySelector('.map')
|
||
|
const spriteContainer = document.querySelector('.sprite_container')
|
||
|
const instruction = document.querySelector('.instruction')
|
||
|
const sprite = document.querySelector('.sprite')
|
||
|
const motionSpeed = 150
|
||
|
const height = 10
|
||
|
const width = 20
|
||
|
const delay = 20
|
||
|
let cellSize = 40
|
||
|
let spritePos = -cellSize
|
||
|
let x = 0
|
||
|
let y = 0
|
||
|
let instructionTimer
|
||
|
let motionTimer
|
||
|
let displayTimer
|
||
|
let boneTimer
|
||
|
let start = 42
|
||
|
let goal = 0
|
||
|
let carryOn = true
|
||
|
let triedAnotherWay = false
|
||
|
let route = []
|
||
|
let happy
|
||
|
let happyFrame = 10
|
||
|
|
||
|
const cellsWithWalls = [
|
||
|
31,45,69,68,71,73,82,83,85,88,91,94,95,96,98,105,110,111,112,123,125,128,137,141,142,143,145,146,154,156,157,171
|
||
|
]
|
||
|
|
||
|
const bone = `
|
||
|
<svg class ="bone" x="0px" y="0px" width="100%" height="100%" viewBox="0 0 151.9 151.9" >
|
||
|
<style type="text/css">
|
||
|
.st0{fill:#FFFFFF;}
|
||
|
</style>
|
||
|
<polygon class="st0" points="39.2,53.7 44.6,56.9 50.4,62.3 102.8,62.3 106.4,59.3 111.2,53.7 120.8,53.7 125.3,58 130.4,63.8
|
||
|
130.4,67.2 125.1,73.2 120.6,77.3 124.8,81.5 130.4,86.5 130.4,91.6 125.9,95.7 120.8,100.4 111.4,100.4 106.9,96.3 101.9,91.6
|
||
|
50,91.4 45.9,95.7 40.8,100.4 30.9,100.4 26.4,96.3 21.7,91.2 21.7,86.7 26.4,82.2 30.9,77.1 26.4,72.5 21.7,67.2 21.7,63.2
|
||
|
27,58.1 30.7,53.7 "/>
|
||
|
<g>
|
||
|
<g>
|
||
|
<g>
|
||
|
<path d="M24.1,70.2c-1.6,0-3.1,0-4.7,0c0-3.2,0-6.3,0-9.5c1.6,0,3.1,0,4.7,0c0-1.6,0-3.2,0-4.7c1.5,0,3,0,4.5,0c0-1.5,0-3,0-4.5
|
||
|
c4.8,0,9.5,0,14.3,0c0,1.5,0,2.9,0,4.5c1.6,0,3.2,0,4.8,0c0,1.6,0,3.2,0,4.7c18.9,0,37.7,0,56.5,0c0-1.6,0-3.1,0-4.7
|
||
|
c1.6,0,3.2,0,4.8,0c0-1.5,0-3,0-4.5c4.7,0,9.4,0,14.1,0c0,1.5,0,3,0,4.5c1.6,0,3.1,0,4.7,0c0,1.6,0,3.2,0,4.7c1.6,0,3.2,0,4.8,0
|
||
|
c0,3.2,0,6.4,0,9.6c-1.6,0-3.1,0-4.7,0c0,0,0,0,0,0c0-3.1,0-6.2,0-9.3c-1.6,0-3.1,0-4.7,0c0-1.6,0-3.1,0-4.7c-4.7,0-9.4,0-14.1,0
|
||
|
c0,1.6,0,3.1,0,4.7c-1.6,0-3.2,0-4.8,0c0,1.6,0,3,0,4.5c-18.9,0-37.7,0-56.5,0c0-1.5,0-3,0-4.5c-1.6,0-3.2,0-4.8,0
|
||
|
c0-1.6,0-3.2,0-4.7c-4.7,0-9.4,0-14,0c0,1.5,0,3,0,4.6c-1.6,0-3.2,0-4.7,0C24.2,64,24.2,67.1,24.1,70.2
|
||
|
C24.1,70.2,24.1,70.2,24.1,70.2z"/>
|
||
|
<path d="M127.9,93.8c0,1.6,0,3.1,0,4.7c-1.6,0-3.1,0-4.8,0c0,1.6,0,3.1,0,4.7c-4.7,0-9.3,0-14,0c0-1.6,0-3.1,0-4.7
|
||
|
c-1.6,0-3.2,0-4.8,0c0-1.6,0-3.2,0-4.8c-18.9,0-37.7,0-56.5,0c0,1.6,0,3.1,0,4.7c-1.6,0-3.2,0-4.9,0c0,1.6,0,3.2,0,4.7
|
||
|
c-4.7,0-9.3,0-14,0c-0.1-0.1-0.2-0.2-0.3-0.3c0-1.4,0-2.9,0-4.4c-1.5,0-3,0-4.6,0c0-1.6,0-3.2,0-4.7c0,0,0,0,0,0
|
||
|
c1.6,0,3.1,0,4.7,0c0,1.6,0,3,0,4.6c4.7,0,9.3,0,14,0c0-1.5,0-3,0-4.7c1.6,0,3.2,0,4.8,0c0-1.6,0-3.1,0-4.6c18.9,0,37.7,0,56.5,0
|
||
|
c0,1.5,0,3,0,4.6c1.6,0,3.2,0,4.8,0c0,1.7,0,3.2,0,4.7c4.7,0,9.3,0,14,0c0-1.5,0-3,0-4.5C124.8,93.8,126.3,93.8,127.9,93.8
|
||
|
C127.9,93.8,127.9,93.8,127.9,93.8z"/>
|
||
|
<path d="M24.1,93.8c-1.6,0-3.1,0-4.7,0c0-3.2,0-6.3,0-9.5c1.6,0,3.1,0,4.7,0c0-1.6,0-3.2,0-4.8c1.5,0,3,0,4.5,0c0-1.5,0-3,0-4.6
|
||
|
c-1.5,0-3,0-4.5,0c0-1.6,0-3.2,0-4.7c0,0,0,0,0,0c1.6,0,3.1,0,4.7,0c0,1.6,0,3,0,4.6c1.5,0,3,0,4.2,0c0.3,0.2,0.4,0.2,0.4,0.2
|
||
|
c0,1.7,0,3.2,0,4.8c-1.6,0-3.1,0-4.6,0c0,1.5,0,3,0,4.6c-1.6,0-3.2,0-4.7,0C24.2,87.6,24.2,90.7,24.1,93.8
|
||
|
C24.1,93.8,24.1,93.8,24.1,93.8z"/>
|
||
|
<path d="M127.9,70.2c0,1.6,0,3.1,0,4.7c-1.6,0-3.2,0-4.7,0c0,1.5,0,3,0,4.5c1.5,0,3.1,0,4.7,0c0,1.6,0,3.2,0,4.8
|
||
|
c1.6,0,3.2,0,4.8,0c0,3.2,0,6.3,0,9.6c-1.6,0-3.1,0-4.7,0c0,0,0,0,0,0c0-3.1,0-6.2,0-9.3c-1.6,0-3.1,0-4.7,0c0-1.6,0-3.1,0-4.7
|
||
|
c-1.6,0-3.2,0-4.7,0c0-1.6,0-3.2,0-4.9c1.5,0,3.1,0,4.7,0c0-1.7,0-3.1,0-4.7C124.8,70.1,126.3,70.1,127.9,70.2
|
||
|
C127.9,70.2,127.9,70.2,127.9,70.2z"/>
|
||
|
</g>
|
||
|
</g>
|
||
|
</g>
|
||
|
</svg>
|
||
|
`
|
||
|
|
||
|
const searchMemory = new Array(height * width).fill('').map(()=>{
|
||
|
return {
|
||
|
path: null,
|
||
|
searched: false,
|
||
|
prev: null
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const mapMap = ()=>{
|
||
|
const mapArr = []
|
||
|
for (let i = 0; i < (width * height); i++){
|
||
|
mapArr.push(i)
|
||
|
}
|
||
|
|
||
|
return mapArr.map((ele,i)=>{
|
||
|
const dataX = i % width
|
||
|
const dataY = Math.floor(i / width)
|
||
|
|
||
|
return `
|
||
|
<div
|
||
|
class="map_tile"
|
||
|
data-index=${i}
|
||
|
data-x=${dataX}
|
||
|
data-y=${dataY}
|
||
|
>
|
||
|
</div>
|
||
|
`
|
||
|
}).join('')
|
||
|
}
|
||
|
|
||
|
map.innerHTML = mapMap()
|
||
|
const mapTiles = document.querySelectorAll('.map_tile')
|
||
|
|
||
|
const setX = num =>{
|
||
|
x = num
|
||
|
spriteContainer.style.left = `${x}px`
|
||
|
}
|
||
|
|
||
|
const setY = num =>{
|
||
|
y = num
|
||
|
spriteContainer.style.top = `${y}px`
|
||
|
}
|
||
|
|
||
|
const setSpritePos = num =>{
|
||
|
spritePos = num
|
||
|
sprite.style.marginLeft = `${spritePos}px`
|
||
|
}
|
||
|
|
||
|
const positionSprite = pos =>{
|
||
|
const paraX = pos % width * cellSize
|
||
|
const paraY = Math.floor(pos / width) * cellSize
|
||
|
setX(paraX)
|
||
|
setY(paraY)
|
||
|
}
|
||
|
|
||
|
const NoWall = pos =>{
|
||
|
return !mapTiles[pos].classList.contains('wall')
|
||
|
}
|
||
|
|
||
|
const setUpWalls = ()=>{
|
||
|
cellsWithWalls.forEach(cell=>{
|
||
|
mapTiles[cell].classList.add('wall')
|
||
|
})
|
||
|
mapTiles.forEach(tile=>{
|
||
|
if (tile.dataset.y === '0' ||
|
||
|
tile.dataset.y === '9' ||
|
||
|
tile.dataset.x === '0' ||
|
||
|
tile.dataset.x === '19') tile.classList.add('wall')
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const clearTiles = ()=>{
|
||
|
mapTiles[goal].innerHTML = ''
|
||
|
mapTiles.forEach(tile=>{
|
||
|
tile.className = 'map_tile'
|
||
|
})
|
||
|
setUpWalls()
|
||
|
}
|
||
|
|
||
|
const spriteWalk = e=>{
|
||
|
if (!e) return
|
||
|
|
||
|
const direction = e.key ? e.key.toLowerCase().replace('arrow','') : e
|
||
|
const current = (x / cellSize) + ((y / cellSize) * width)
|
||
|
let m = -cellSize
|
||
|
switch (direction) {
|
||
|
case 'right':
|
||
|
if (x < ((width - 1) * cellSize) && NoWall(current + 1)) setX(x + cellSize)
|
||
|
m = spritePos === m * 8 ? m * 9 : m * 8
|
||
|
break
|
||
|
case 'left':
|
||
|
if (x > 0 && NoWall(current - 1)) setX(x - cellSize)
|
||
|
m = spritePos === m * 6 ? m * 7 : m * 6
|
||
|
break
|
||
|
case 'up':
|
||
|
if (y > 0 && NoWall(current - width)) setY(y - cellSize)
|
||
|
m = spritePos === m * 3 ? m * 5 : m * 3
|
||
|
break
|
||
|
case 'down':
|
||
|
if (y < ((height - 1) * cellSize) && NoWall(current + width)) setY(y + cellSize)
|
||
|
m = spritePos === m * 0 ? m * 2 : m * 0
|
||
|
break
|
||
|
default:
|
||
|
console.log('invalid command')
|
||
|
}
|
||
|
setSpritePos(m)
|
||
|
start = (x / cellSize) + ((y / cellSize) * width)
|
||
|
if (goal === (x / cellSize) + ((y / cellSize) * width)) {
|
||
|
boneTimer = setTimeout(()=>{
|
||
|
mapTiles[goal].innerHTML = ''
|
||
|
animateHappyDog()
|
||
|
},100)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const chainMotion = (instruction,index) => {
|
||
|
if (index >= instruction.length) return
|
||
|
|
||
|
spriteWalk(instruction[index])
|
||
|
motionTimer = setTimeout(()=>{
|
||
|
chainMotion(instruction, index + 1)
|
||
|
},motionSpeed)
|
||
|
}
|
||
|
|
||
|
const displayPath = current =>{
|
||
|
searchMemory[current].path = 'path'
|
||
|
mapTiles[current].classList.add('path')
|
||
|
let prev = searchMemory[current].prev
|
||
|
|
||
|
//! when sprite is one square away from start and prev is undefined, prev is corrected.
|
||
|
if (current - width === start ||
|
||
|
current + width === start ||
|
||
|
current - 1 === start ||
|
||
|
current + 1 === start) prev = start
|
||
|
|
||
|
let direction
|
||
|
if (current - width === prev) direction = 'down'
|
||
|
if (current + width === prev) direction = 'up'
|
||
|
if (current - 1 === prev) direction = 'right'
|
||
|
if (current + 1 === prev) direction = 'left'
|
||
|
|
||
|
if (prev) route.push(direction)
|
||
|
|
||
|
if (!prev) {
|
||
|
route.push('reset')
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (prev === start || !prev) {
|
||
|
const reversedRoute = route.reverse()
|
||
|
chainMotion(reversedRoute,0)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
displayTimer = setTimeout(()=>{
|
||
|
displayPath(prev)
|
||
|
},delay)
|
||
|
}
|
||
|
|
||
|
const distanceBetween = (a,b) =>{
|
||
|
return Math.abs(a % width - b % width) + Math.abs(Math.floor(a / width) - Math.floor(b / width))
|
||
|
}
|
||
|
|
||
|
const decideNextMove = (current, count) =>{
|
||
|
if (!carryOn) return
|
||
|
const possibleDestination = [
|
||
|
current + 1,
|
||
|
current - 1,
|
||
|
current - width,
|
||
|
current + width
|
||
|
]
|
||
|
const mapInfo = []
|
||
|
if (possibleDestination.filter(cell=> cell === goal).length === 1) {
|
||
|
carryOn = false
|
||
|
searchMemory[goal].prev = current
|
||
|
displayPath(goal)
|
||
|
return
|
||
|
}
|
||
|
possibleDestination.forEach(option=>{
|
||
|
if (mapTiles[option] &&
|
||
|
!mapTiles[option].classList.contains('wall') &&
|
||
|
!searchMemory[option].searched &&
|
||
|
option !== start) {
|
||
|
mapInfo.push(
|
||
|
{
|
||
|
cell: option,
|
||
|
prev: current,
|
||
|
distanceFromStart: distanceBetween(start,option),
|
||
|
distanceToGoal: distanceBetween(goal,option)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
})
|
||
|
const minValue = Math.min(...mapInfo.map(cell=> cell.distanceFromStart + cell.distanceToGoal))
|
||
|
const optionsWithMinValue = mapInfo.filter(cell=> (cell.distanceFromStart + cell.distanceToGoal) === minValue)
|
||
|
|
||
|
mapInfo.filter(cell=> (cell.distanceFromStart + cell.distanceToGoal) !== minValue).forEach(option=>{
|
||
|
mapTiles[option.cell].classList.add('sub_node')
|
||
|
})
|
||
|
|
||
|
if (optionsWithMinValue.length === 0 && !triedAnotherWay) {
|
||
|
triedAnotherWay = true
|
||
|
tryAnotherWay(count)
|
||
|
}
|
||
|
|
||
|
optionsWithMinValue.forEach(option=>{
|
||
|
searchMemory[option.cell].searched = true
|
||
|
searchMemory[option.cell].prev = current
|
||
|
mapTiles[option.cell].classList.add('node')
|
||
|
setTimeout(()=>{
|
||
|
decideNextMove(option.cell, count + 1)
|
||
|
},delay)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const tryAnotherWay = count =>{
|
||
|
if (!carryOn) return
|
||
|
const possibleDestination = [
|
||
|
start + 1,
|
||
|
start - 1,
|
||
|
start - width,
|
||
|
start + width
|
||
|
]
|
||
|
possibleDestination.forEach(path=>{
|
||
|
if (mapTiles[path] &&
|
||
|
!searchMemory[path].searched &&
|
||
|
!mapTiles[path].classList.contains('wall')) {
|
||
|
decideNextMove(path, count + 1)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const resetMotion = () =>{
|
||
|
clearTimeout(motionTimer)
|
||
|
clearTimeout(displayTimer)
|
||
|
clearTimeout(boneTimer)
|
||
|
clearInterval(happy)
|
||
|
clearTiles()
|
||
|
searchMemory.forEach(memory=>{
|
||
|
memory.path = null,
|
||
|
memory.searched = false,
|
||
|
memory.prev = null
|
||
|
})
|
||
|
route = []
|
||
|
carryOn = true
|
||
|
triedAnotherWay = false
|
||
|
}
|
||
|
|
||
|
const triggerMotion = e =>{
|
||
|
if (Number(e.target.dataset.index) === goal || !e.target.dataset.index) return
|
||
|
instruction.classList.add('hide')
|
||
|
clearTimeout(instructionTimer)
|
||
|
instructionTimer = setTimeout(()=>{
|
||
|
instruction.classList.remove('hide')
|
||
|
},5 * 1000)
|
||
|
mapTiles[goal].innerHTML = ''
|
||
|
goal = Number(e.target.dataset.index)
|
||
|
if (mapTiles[goal].classList.contains('wall')) return
|
||
|
resetMotion()
|
||
|
mapTiles[start].classList.add('start')
|
||
|
positionSprite(start)
|
||
|
mapTiles[goal].classList.add('goal')
|
||
|
mapTiles[goal].innerHTML = bone
|
||
|
searchMemory[start].path = 'start'
|
||
|
searchMemory[goal].path = 'goal'
|
||
|
decideNextMove(start, 0)
|
||
|
}
|
||
|
|
||
|
const animateHappyDog = () =>{
|
||
|
happy = setInterval(()=>{
|
||
|
happyFrame = happyFrame === 10 ? 11 : 10
|
||
|
setSpritePos(happyFrame * -cellSize)
|
||
|
},150)
|
||
|
}
|
||
|
|
||
|
//! comment this bit out to enable keyboard control
|
||
|
// window.addEventListener('keydown', spriteWalk)
|
||
|
|
||
|
const resize = () =>{
|
||
|
const wrapper = document.querySelector('.wrapper')
|
||
|
let pWidth = 800
|
||
|
if (wrapper.offsetWidth < 800) {
|
||
|
pWidth = wrapper.offsetWidth
|
||
|
}
|
||
|
cellSize = Math.floor(pWidth / width)
|
||
|
const calcWidth = Math.floor(pWidth / width) * width
|
||
|
const mapCover = document.querySelector('.map_cover')
|
||
|
|
||
|
//*resize sprite
|
||
|
positionSprite(start)
|
||
|
setSpritePos(-cellSize)
|
||
|
sprite.style.height = `${cellSize}px`
|
||
|
sprite.style.width = `${cellSize * 12}px`
|
||
|
spriteContainer.style.height = `${cellSize}px`
|
||
|
spriteContainer.style.width = `${cellSize}px`
|
||
|
|
||
|
//* resize map
|
||
|
map.style.width = `${calcWidth}px`
|
||
|
map.style.height = `${calcWidth / 2}px`
|
||
|
mapCover.style.width = `${calcWidth}px`
|
||
|
mapCover.style.height = `${calcWidth / 2}px`
|
||
|
mapCover.style.marginTop = `-${calcWidth / 2}px`
|
||
|
mapTiles.forEach(tile=>{
|
||
|
tile.style.width = `${cellSize}px`
|
||
|
tile.style.height = `${cellSize}px`
|
||
|
})
|
||
|
}
|
||
|
|
||
|
//*setup
|
||
|
setUpWalls()
|
||
|
positionSprite(start)
|
||
|
mapTiles.forEach(mapTile=>{
|
||
|
mapTile.addEventListener('click', triggerMotion)
|
||
|
})
|
||
|
resize()
|
||
|
window.addEventListener('resize', resize)
|
||
|
}
|
||
|
|
||
|
window.addEventListener('DOMContentLoaded', init)
|