|
window.addEventListener("DOMContentLoaded",app); |
|
|
|
function app() { |
|
var scene, |
|
camera, |
|
renderer, |
|
raycaster, |
|
textureLoader, |
|
world, |
|
blocks, |
|
player, |
|
controlConfig = { |
|
backwards: 83, |
|
forwards: 87, |
|
left: 65, |
|
right: 68, |
|
jump: 32, |
|
respawn: 82 |
|
}, |
|
cursor = { |
|
x: 0, |
|
y: 0, |
|
// mobile only |
|
holding: false, |
|
holdTime: 0, |
|
triggerHoldTime: 20 |
|
}, |
|
randomInt = (min,max) => Math.round(Math.random() * (max - min)) + min; |
|
|
|
class World { |
|
constructor() { |
|
// Warning: Larger worlds can cause lag! |
|
this.size = 12; |
|
this.layers = 2; |
|
this.skyHeight = 64; |
|
this.voidHeight = 64; |
|
this.skyColor = 0xf1f1f1; |
|
this.lightColor = 0xffffff; |
|
this.gravity = 0.008; |
|
this.lighting = { |
|
ambient: new THREE.AmbientLight(this.lightColor,0.75), |
|
directional: new THREE.DirectionalLight(this.lightColor,0.5) |
|
}; |
|
|
|
// config ambient |
|
this.lighting.ambient.name = "Ambient Light"; |
|
scene.add(this.lighting.ambient); |
|
|
|
// config directional |
|
let sizeHalf = this.size/2; |
|
this.lighting.directional.name = "Directional Light"; |
|
this.lighting.directional.position.set(0,this.skyHeight,0); |
|
this.lighting.directional.castShadow = true; |
|
this.lighting.directional.shadow.camera = new THREE.OrthographicCamera( |
|
-sizeHalf - 1,sizeHalf + 1,sizeHalf + 1,-sizeHalf,0.5,2e4 |
|
); |
|
this.lighting.directional.shadow.mapSize = new THREE.Vector2(1024,1024); |
|
scene.add(this.lighting.directional); |
|
|
|
// blocks |
|
this.addBlock = (x,y,z,block,rad = 0) => { |
|
let newBlock = block.mesh.clone(), |
|
deg = rad * 180/Math.PI, |
|
// for multi-texture blocks, angle should be at nearest 90° |
|
rotY = block.multiTexture ? Math.round(deg / 90) * 90 : 0; |
|
|
|
// ensure angles to left are positive, right are negative |
|
if (rotY >= 360 || rotY <= -360) |
|
rotY %= 360; |
|
if (rotY == 270) |
|
rotY = -90; |
|
else if (rotY == -270) |
|
rotY = 90; |
|
|
|
newBlock.position.set(x,y,z); |
|
newBlock.rotation.y = rotY * Math.PI/180; |
|
scene.add(newBlock); |
|
}; |
|
|
|
// add them to world |
|
let sizeStart = !(this.size % 2) ? -sizeHalf : Math.round(-sizeHalf), |
|
layers = this.layers; |
|
for (let z = sizeStart; z < sizeHalf; ++z) { |
|
for (let y = 0; y < layers; ++y) { |
|
for (let x = sizeStart; x < sizeHalf; ++x) { |
|
// to ground level |
|
if (y == layers - 1) { |
|
this.addBlock(x,y,z,blocks[0]); |
|
// under ground level |
|
} else { |
|
let randBlockID = randomInt(1,blocks.length - 1); |
|
this.addBlock(x,y,z,blocks[randBlockID]); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
class Block { |
|
constructor(name,multiTexture = false) { |
|
var blockProtoGeo = new THREE.BoxBufferGeometry(1,1,1), |
|
blockMat, |
|
blockColor = 0xffffff, |
|
blockSpriteAtlas = name; |
|
|
|
if (multiTexture === true) { |
|
blockMat = []; |
|
|
|
for (let b in blockSpriteAtlas) { |
|
let path = blockSpriteAtlas[b], |
|
sprite = new THREE.MeshLambertMaterial({ |
|
color: blockColor, |
|
map: textureLoader.load(path) |
|
}); |
|
sprite.map.wrapS = THREE.RepeatWrapping; |
|
sprite.map.wrapT = THREE.RepeatWrapping; |
|
sprite.map.minFilter = THREE.NearestMipMapNearestFilter; |
|
sprite.map.magFilter = THREE.NearestFilter; |
|
sprite.map.repeat.set(1,1); |
|
blockMat.push(sprite); |
|
} |
|
|
|
} else { |
|
let path = blockSpriteAtlas.side, |
|
sprite = new THREE.MeshLambertMaterial({ |
|
color: blockColor, |
|
map: textureLoader.load(path) |
|
}); |
|
sprite.map.wrapS = THREE.RepeatWrapping; |
|
sprite.map.wrapT = THREE.RepeatWrapping; |
|
sprite.map.minFilter = THREE.NearestMipMapNearestFilter; |
|
sprite.map.magFilter = THREE.NearestFilter; |
|
sprite.map.repeat.set(1,1); |
|
blockMat = sprite; |
|
} |
|
|
|
this.multiTexture = multiTexture; |
|
this.mesh = new THREE.Mesh(blockProtoGeo,blockMat); |
|
this.mesh.material.needsUpdate = true; |
|
this.mesh.castShadow = true; |
|
this.mesh.receiveShadow = true; |
|
} |
|
} |
|
class Player { |
|
constructor(name,x = 0,y = 0,z = 0,preserveCam = false) { |
|
this.x = x; |
|
this.y = y; |
|
this.z = z; |
|
this.height = 1.7; |
|
this.width = 0.25; |
|
this.depth = 0.25; |
|
this.pitch = 0; |
|
this.yaw = 0; |
|
this.speed = 0.05; |
|
this.velocity = 0; |
|
this.jumpSpeed = 0.13; |
|
this.jumpVelocity = 0; |
|
this.xdir = ""; |
|
this.zdir = ""; |
|
this.attackRange = 5; |
|
this.lookingAt = null; |
|
this.jumping = false; |
|
this.onGround = false; |
|
this.dead = false; |
|
|
|
// interaction |
|
this.blockFaceHL = { |
|
mesh: new THREE.Mesh( |
|
new THREE.PlaneBufferGeometry(1,1), |
|
new THREE.MeshLambertMaterial({ |
|
color: 0xffffff, |
|
opacity: 0.5, |
|
transparent: true |
|
}) |
|
), |
|
dir: "" |
|
}; |
|
this.blockFaceHL.mesh.name = "Block Face Highlight"; |
|
|
|
this.highlightBlock = () => { |
|
raycaster.setFromCamera({x: 0, y: 0},camera); |
|
|
|
let intersects = raycaster.intersectObjects(scene.children), |
|
intersected = intersects.filter( |
|
child => child.object.type == "Mesh" && |
|
child.distance > this.depth && |
|
child.distance < this.attackRange && |
|
child.object.name != this.blockFaceHL.mesh.name |
|
), |
|
firstBlock = intersected[0]; |
|
|
|
if (intersected.length) { |
|
if (this.lookingAt == null) |
|
scene.add(this.blockFaceHL.mesh); |
|
|
|
this.lookingAt = firstBlock; |
|
|
|
let fbObj = firstBlock.object, |
|
fbObjRot = fbObj.rotation.y; |
|
|
|
this.blockFaceHL.mesh.position.set(fbObj.position.x,fbObj.position.y,fbObj.position.z); |
|
this.blockFaceHL.mesh.rotation.set(0,0,0); |
|
|
|
let face = firstBlock.faceIndex, |
|
minOpacity = 0.16, |
|
maxOpacity = 0.84, |
|
opacityRange = maxOpacity - minOpacity, |
|
ms = opacityRange * 1e3, |
|
opacityFromMin = new Date() % ms / ms, |
|
zFightFix = 1e-3; |
|
|
|
this.blockFaceHL.mesh.material.opacity = |
|
(opacityFromMin > maxOpacity/2 ? maxOpacity - opacityFromMin : opacityFromMin) + minOpacity; |
|
|
|
// face directions: 0,1—right, 2,3—left, 4,5—top, 6,7—bottom, 8,9—front, 10,11—back |
|
if ( |
|
(face < 2 && fbObjRot == 0) || |
|
(face >= 2 && face < 4 && Math.abs(fbObjRot) == Math.PI) || |
|
(face >= 8 && face < 10 && fbObjRot == Math.PI/2) || |
|
(face >= 10 && face < 12 && fbObjRot == -Math.PI/2) |
|
) { |
|
this.blockFaceHL.mesh.position.x += 0.5 + zFightFix; |
|
this.blockFaceHL.mesh.rotation.y += Math.PI/2; |
|
this.blockFaceHL.dir = "east"; |
|
|
|
} else if ( |
|
(face < 2 && Math.abs(fbObjRot) == Math.PI) || |
|
(face >= 2 && face < 4 && fbObjRot == 0) || |
|
(face >= 8 && face < 10 && fbObjRot == -Math.PI/2) || |
|
(face >= 10 && face < 12 && fbObjRot == Math.PI/2) |
|
) { |
|
this.blockFaceHL.mesh.position.x -= 0.5 + zFightFix; |
|
this.blockFaceHL.mesh.rotation.y -= Math.PI/2; |
|
this.blockFaceHL.dir = "west"; |
|
|
|
} else if (face >= 4 && face < 6) { |
|
this.blockFaceHL.mesh.position.y += 0.5 + zFightFix; |
|
this.blockFaceHL.mesh.rotation.x -= Math.PI/2; |
|
this.blockFaceHL.dir = "above"; |
|
|
|
} else if (face >= 6 && face < 8) { |
|
this.blockFaceHL.mesh.position.y -= 0.5 + zFightFix; |
|
this.blockFaceHL.mesh.rotation.x += Math.PI/2; |
|
this.blockFaceHL.dir = "below"; |
|
|
|
} else if ( |
|
(face < 2 && fbObjRot == -Math.PI/2) || |
|
(face >= 2 && face < 4 && fbObjRot == Math.PI/2) || |
|
(face >= 8 && face < 10 && fbObjRot == 0) || |
|
(face >= 10 && face < 12 && Math.abs(fbObjRot) == Math.PI) |
|
) { |
|
this.blockFaceHL.mesh.position.z += 0.5 + zFightFix; |
|
this.blockFaceHL.dir = "south"; |
|
|
|
} else if ( |
|
(face < 2 && fbObjRot == Math.PI/2) || |
|
(face >= 2 && face < 4 && fbObjRot == -Math.PI/2) || |
|
(face >= 8 && face < 10 && Math.abs(fbObjRot) == Math.PI) || |
|
(face >= 10 && face < 12 && fbObjRot == 0) |
|
) { |
|
this.blockFaceHL.mesh.position.z -= 0.5 + zFightFix; |
|
this.blockFaceHL.mesh.rotation.x -= Math.PI; |
|
this.blockFaceHL.dir = "north"; |
|
} |
|
|
|
} else if (this.lookingAt != null) { |
|
scene.remove(this.blockFaceHL.mesh); |
|
this.blockFaceHL.dir = ""; |
|
this.lookingAt = null; |
|
} |
|
}; |
|
this.lookAround = e => { |
|
let center = { |
|
x: window.innerWidth/2, |
|
y: window.innerHeight/2 |
|
}; |
|
if (e) { |
|
cursor.x = e.pageX; |
|
cursor.y = e.pageY; |
|
} |
|
this.pitch = -Math.atan((cursor.y - center.y) / center.y) * 2; |
|
this.yaw = -Math.atan((cursor.x - center.x) / center.x) * 8; |
|
}; |
|
this.build = e => { |
|
// destroy block |
|
if ((e && e.button === 2) || cursor.holdTime == cursor.triggerHoldTime) { |
|
if (this.lookingAt != null) |
|
scene.remove(this.lookingAt.object); |
|
|
|
// place block |
|
} else if ((e && e.button === 0) || (cursor.holdTime > 0 && cursor.holdTime < cursor.triggerHoldTime)) { |
|
let at = this.lookingAt; |
|
if (at != null) { |
|
let face = at.faceIndex, |
|
pos = at.object.position, |
|
rot = at.object.rotation, |
|
placeX = pos.x, |
|
placeY = pos.y, |
|
placeZ = pos.z; |
|
|
|
switch (this.blockFaceHL.dir) { |
|
case "east": |
|
++placeX; |
|
break; |
|
case "west": |
|
--placeX; |
|
break; |
|
case "above": |
|
++placeY; |
|
break; |
|
case "below": |
|
--placeY; |
|
break; |
|
case "south": |
|
++placeZ; |
|
break; |
|
case "north": |
|
--placeZ; |
|
break; |
|
default: |
|
break; |
|
} |
|
|
|
let xr = Math.round(this.x), |
|
yr = Math.round(this.y), |
|
zr = Math.round(this.z), |
|
pxr = Math.round(placeX), |
|
pyr = Math.round(placeY), |
|
pzr = Math.round(placeZ); |
|
|
|
// prevent placement in same position, touching player, and outside vertical boundaries |
|
if ( !(xr == pxr && (yr == pyr || yr == pyr - ~~this.height) && zr == pzr) && |
|
(pxr >= -world.size/2 && pxr < world.size/2) && |
|
(pyr >= 0 && pyr < world.skyHeight) && |
|
(pzr >= -world.size/2 && pzr < world.size/2) ) { |
|
let layers = world.layers - 1, |
|
currentY = Math.floor(placeY); |
|
|
|
if (currentY == layers) { |
|
world.addBlock(placeX,placeY,placeZ,blocks[0],player.yaw); |
|
} else { |
|
let randBlockID = randomInt(1,blocks.length - 1); |
|
world.addBlock(placeX,placeY,placeZ,blocks[randBlockID],player.yaw); |
|
} |
|
|
|
} |
|
} |
|
} |
|
}; |
|
this.xmove = e => { |
|
if (e.keyCode == controlConfig.left) |
|
this.xdir = "left"; |
|
|
|
else if (e.keyCode == controlConfig.right) |
|
this.xdir = "right"; |
|
}; |
|
this.jump = e => { |
|
if (e.keyCode == controlConfig.jump) |
|
this.jumping = true; |
|
}; |
|
this.zmove = e => { |
|
if (e.keyCode == controlConfig.backwards) |
|
this.zdir = "backwards"; |
|
|
|
else if (e.keyCode == controlConfig.forwards) |
|
this.zdir = "forwards"; |
|
}; |
|
this.xmoveStop = e => { |
|
if (!e || e.keyCode == controlConfig.left || e.keyCode == controlConfig.right) |
|
this.xdir = ""; |
|
}; |
|
this.jumpStop = e => { |
|
if (!e || e.keyCode == controlConfig.jump) |
|
this.jumping = false; |
|
}; |
|
this.zmoveStop = e => { |
|
if (!e || e.keyCode == controlConfig.backwards || e.keyCode == controlConfig.forwards) |
|
this.zdir = ""; |
|
}; |
|
this.midairMoveStop = () => { |
|
if (!this.onGround && this.jumpVelocity > 0) |
|
this.velocity = -this.jumpVelocity; |
|
}; |
|
this.die = e => { |
|
scene.remove(this.mesh); |
|
scene.remove(this.blockFaceHL.mesh); |
|
this.dead = true; |
|
return this.dead; |
|
}; |
|
this.updatePosition = () => { |
|
// moving |
|
let move = this.velocity < 0 ? 0 : this.velocity, |
|
rate = 0.01, |
|
yaw = this.yaw; |
|
|
|
if (this.xdir == "left") { |
|
this.x -= move * Math.cos(yaw); |
|
this.z += move * Math.sin(yaw); |
|
|
|
} else if (this.xdir == "right") { |
|
this.x += move * Math.cos(yaw); |
|
this.z -= move * Math.sin(yaw); |
|
} |
|
if (this.zdir == "backwards") { |
|
this.z += move * Math.cos(yaw); |
|
this.x += move * Math.sin(yaw); |
|
|
|
} else if (this.zdir == "forwards") { |
|
this.z -= move * Math.cos(yaw); |
|
this.x -= move * Math.sin(yaw); |
|
} |
|
|
|
// accelerate movement |
|
if (this.xdir != "" || this.zdir != "") { |
|
this.velocity += rate; |
|
if (this.velocity > this.speed) |
|
this.velocity = this.speed; |
|
|
|
} else if (this.xdir == "" && this.zdir == "") { |
|
this.velocity -= rate; |
|
if (this.velocity < 0) |
|
this.velocity = 0; |
|
} |
|
|
|
// jumping, falling |
|
if (this.jumpVelocity == 0) { |
|
if (this.jumping) { |
|
this.jumpVelocity = this.jumpSpeed; |
|
this.onGround = false; |
|
|
|
} else { |
|
this.onGround = true; |
|
} |
|
} |
|
this.y += this.jumpVelocity; |
|
this.jumpVelocity -= world.gravity; |
|
|
|
// touch blocks |
|
scene.traverse(child => { |
|
let cpos = child.position, |
|
excludedNames = [this.mesh.name, this.blockFaceHL.mesh.name], |
|
xFromPlyr = Math.abs(cpos.x - this.x), |
|
yFromPlyr = cpos.y - this.y, |
|
zFromPlyr = Math.abs(cpos.z - this.z); |
|
|
|
if (child instanceof THREE.Mesh && |
|
child.name != excludedNames[0] && child.name != excludedNames[1] && |
|
xFromPlyr <= 1 && (yFromPlyr <= this.height + 1 || yFromPlyr <= 1) && zFromPlyr <= 1) |
|
collideWithBlock(this,child); |
|
}); |
|
|
|
if (this.y < -world.voidHeight) |
|
this.die(); |
|
|
|
// camera keep up |
|
camera.position.x = this.x; |
|
camera.position.y = this.y + (this.height - 0.5); |
|
camera.position.z = this.z; |
|
|
|
if (preserveCam) |
|
this.lookAround(); |
|
|
|
camera.rotation.x = this.pitch; |
|
camera.rotation.y = this.yaw; |
|
}; |
|
|
|
// entity in scene |
|
this.mesh = new THREE.Object3D(); |
|
this.mesh.name = name; |
|
this.mesh.position.set(this.x,this.y,this.z); |
|
scene.add(this.mesh); |
|
} |
|
} |
|
|
|
var init = () => { |
|
// setup |
|
scene = new THREE.Scene(); |
|
camera = new THREE.PerspectiveCamera(45,window.innerWidth / window.innerHeight,0.1,2e4); |
|
renderer = new THREE.WebGLRenderer(); |
|
|
|
renderer.setPixelRatio(window.devicePixelRatio); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
renderer.shadowMap.enabled = true; |
|
raycaster = new THREE.Raycaster(); |
|
textureLoader = new THREE.TextureLoader(); |
|
|
|
// world generation, spawn player |
|
let BLOCK = BlockList(); |
|
blocks = [ |
|
new Block(BLOCK.siliconBlock), |
|
new Block(BLOCK.blueberryIMac,true), |
|
new Block(BLOCK.bondiIMac,true), |
|
new Block(BLOCK.grapeIMac,true), |
|
new Block(BLOCK.limeIMac,true), |
|
new Block(BLOCK.macintosh128k,true), |
|
new Block(BLOCK.strawberryIMac,true), |
|
new Block(BLOCK.tangerineIMac,true) |
|
]; |
|
world = new World(); |
|
player = new Player("Player",0,world.layers,0); |
|
|
|
// camera |
|
camera.position.set(player.x,player.y + (player.height - 0.5),player.z); |
|
camera.rotation.order = "YXZ"; |
|
camera.rotation.x = player.pitch; |
|
camera.rotation.y = player.yaw; |
|
camera.zoom = 0.5; |
|
camera.updateProjectionMatrix(); |
|
|
|
// render |
|
renderer.setClearColor(new THREE.Color(world.skyColor)); |
|
document.body.appendChild(renderer.domElement); |
|
}, |
|
collideWithBlock = (player,block) => { |
|
let pX = player.x, |
|
pY = player.y, |
|
pZ = player.z, |
|
pW = player.width, |
|
pH = player.height, |
|
pD = player.depth, |
|
bX = block.position.x, |
|
bY = block.position.y, |
|
bZ = block.position.z, |
|
bS = 1, // block size |
|
|
|
vectorX = pX - bX, |
|
vectorY = (pY + pH/2) - bY, |
|
vectorZ = pZ - bZ, |
|
hWidths = pW/2 + bS/2, |
|
hHeights = pH/2 + bS/2, |
|
hDepths = pD/2 + bS/2; |
|
|
|
if (Math.abs(vectorX) < hWidths && Math.abs(vectorY) < hHeights && Math.abs(vectorZ) < hDepths) { |
|
let pushX = hWidths - Math.abs(vectorX), |
|
pushY = hHeights - Math.abs(vectorY), |
|
pushZ = hDepths - Math.abs(vectorZ), |
|
reqXZSpace = 0.1; // to prevent clinging of block sides |
|
|
|
if (pushZ <= pushX && pushX <= pushY) { |
|
// hit south |
|
if (vectorZ > 0) { |
|
player.z += pushZ; |
|
// hit north |
|
} else { |
|
player.z -= pushZ; |
|
} |
|
player.midairMoveStop(); |
|
|
|
} else if (pushX <= pushZ && pushZ <= pushY) { |
|
// hit east |
|
if (vectorX > 0) { |
|
player.x += pushX; |
|
// hit west |
|
} else { |
|
player.x -= pushX; |
|
} |
|
player.midairMoveStop(); |
|
|
|
} else if (pushY <= pushX && pushY <= pushZ && pushX > reqXZSpace && pushZ > reqXZSpace) { |
|
// land on top |
|
if (vectorY > 0) { |
|
player.y = bY + bS/2; |
|
player.jumpVelocity = 0; |
|
// hit bottom |
|
} else { |
|
player.y -= pushY; |
|
player.jumpVelocity -= player.jumpSpeed; |
|
} |
|
} |
|
} |
|
}, |
|
adjustWindow = () => { |
|
camera.aspect = window.innerWidth / window.innerHeight; |
|
camera.updateProjectionMatrix(); |
|
renderer.setSize(window.innerWidth,window.innerHeight) |
|
}, |
|
renderScene = () => { |
|
player.updatePosition(); |
|
player.highlightBlock(); |
|
|
|
if (player.dead) |
|
player = new Player(player.name,0,world.layers,0,true); |
|
|
|
if (cursor.holding) { |
|
++cursor.holdTime; |
|
if (cursor.holdTime == cursor.triggerHoldTime) { |
|
player.build(); |
|
} |
|
} |
|
|
|
renderer.render(scene,camera); |
|
requestAnimationFrame(renderScene); |
|
}, |
|
run = () => { |
|
let promise = new Promise((resolve,reject) => { |
|
init(); |
|
resolve("World loaded"); |
|
}); |
|
promise.then(msg => { |
|
renderScene(); |
|
console.log(msg); |
|
}); |
|
}; |
|
|
|
run(); |
|
|
|
// game control |
|
window.addEventListener("resize",adjustWindow); |
|
window.addEventListener("contextmenu",e => { |
|
e.preventDefault(); |
|
}); |
|
|
|
// player control |
|
window.addEventListener("touchstart",e => { |
|
cursor.holding = true; |
|
cursor.holdTime = 0; |
|
}); |
|
window.addEventListener("touchmove",e => { |
|
player.lookAround(e); |
|
cursor.holdTime = 0; |
|
}); |
|
window.addEventListener("touchend",e => { |
|
if (cursor.holdTime < cursor.triggerHoldTime) { |
|
player.build(); |
|
cursor.holding = false; |
|
} |
|
}); |
|
window.addEventListener("mousemove",e => { player.lookAround(e) }); |
|
window.addEventListener("mousedown",e => {player.build(e) }); |
|
window.addEventListener("keydown",e => { player.xmove(e) }); |
|
window.addEventListener("keydown",e => { player.jump(e) }); |
|
window.addEventListener("keydown",e => { player.zmove(e) }); |
|
window.addEventListener("keyup",e => { player.xmoveStop(e) }); |
|
window.addEventListener("keyup",e => { player.jumpStop(e) }); |
|
window.addEventListener("keyup",e => { player.zmoveStop(e) }); |
|
window.addEventListener("keydown",e => { |
|
if (e.keyCode == controlConfig.respawn && player.die()) |
|
player = new Player(player.name,0,world.layers,0,true); |
|
}); |
|
} |