Skip to content
Permalink
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
694 lines (610 sloc) 23.2 KB
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Fathom - simple website analytics - https://github.com/usefathom/fathom -->
<script>
(function(f, a, t, h, o, m){
a[h]=a[h]||function(){
(a[h].q=a[h].q||[]).push(arguments)
};
o=f.createElement('script'),
m=f.getElementsByTagName('script')[0];
o.async=1; o.src=t; o.id='fathom-script';
m.parentNode.insertBefore(o,m)
})(document, window, '//stats.josh.earth/tracker.js', 'fathom');
fathom('set', 'siteId', 'GISNV');
fathom('trackPageview');
</script>
<!-- / Fathom -->
<meta charset="UTF-8">
<title>Jingle Smash: WebVR Action Puzzle Game</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<!-- required -->
<script src="../boilerplate/node_modules/three/build/three.min.js"></script>
<!-- stats (fps, polygons, fps) -->
<script src="../boilerplate/vrstats.js" type="module"></script>
<script src="./node_modules/cannon/build/cannon.js"></script>
<style type="text/css">
html, body {
margin:0;
padding:0;
overflow: hidden;
}
#overlay {
position: fixed;
font-size: 5vh;
width: 100vw;
height: 100vh;
background-color: #64c937;
text-align: center;
}
#loading-indicator {
display: block;
}
h1 {
font-family: sans-serif;
font-size: 300%;
color: white;
}
h2 {
font-family: sans-serif;
font-size: 140%;
}
button {
font-family: sans-serif;
font-size: 100%;
display: inline-block;
padding: 0.5em;
border: 5px solid black;
border-radius: 1em;
background: url("textures/candycane.png");
background-size: 50%;
color: black;
}
button:hover:enabled {
background: white;
}
button[disabled], button:disabled {
background: gray;
}
a {
font-family: sans-serif;
color: white;
text-decoration: underline;
}
h1 {
font-size: 300%;
font-family: Georgia;
color: #da2020;
font-variant: small-caps;
font-weight: bold;
text-shadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0, 0, 0, .1), 0 0 5px rgba(0, 0, 0, .1), 0 1px 3px rgba(0, 0, 0, .3), 0 3px 5px rgba(0, 0, 0, .2), 0 5px 10px rgba(0, 0, 0, .25), 0 10px 10px rgba(0, 0, 0, .2), 0 20px 20px rgba(0, 0, 0, .15);
}
</style>
</head>
<body>
<div id="overlay">
<h1>Jingle Smash</h1>
<h2>Smash all the blue crystal boxes with just three ornaments.</h2>
<div id="loading-indicator">
<label>loading</label>
<progress max="100" value="0" id="progress"></progress>
</div>
<button id="enter-button" disabled>VR not supported, play anyway</button>
<div>
<p><a href="https://vr.josh.earth/">by Josh Marinacci</a></p>
</div>
</div>
<script type="module">
import {POINTER_CLICK, POINTER_PRESS, POINTER_RELEASE, Pointer} from '../boilerplate/pointer.js'
import VRStats from "../boilerplate/vrstats.js"
import {BlockService,BLOCK_TYPES} from "./BlockService.js"
import FireBallAction from "./FireBallAction.js"
import AudioService from "./AudioService.js"
import {t2 as T2} from "./t2.js"
import SimpleText from "./SimpleText.js"
import GPUParticleSystem from "../particles/GPUParticleSystem.js"
import VRManager, {VR_DETECTED} from "../boilerplate/vrmanager.js"
const CONSTS = {
BASE_URL: "https://vr.josh.earth/360/doc/",
docid: "tumble_level5",
}
function parseOptions(opts) {
let loc = document.location
if (loc.search) {
const parts = loc.search.substring(1).split('&')
parts.forEach((part)=>{
const keyval = part.split('=')
opts[keyval[0]] = keyval[1]
})
}
return opts;
}
const opts = parseOptions({
docid:null
})
CONSTS.docid = opts.docid
console.log(CONSTS)
//JQuery-like selector
const $ = (sel) => document.querySelector(sel)
const on = (elem, type, cb) => elem.addEventListener(type,cb)
const off = (elem, type, cb) => elem.removeEventListener(type,cb)
const toRad = (deg) => deg * Math.PI/180
const DEBUG = {
SCORE:false,
SKYBOX:true,
AUDIO:true,
}
if(typeof opts.AUDIO !== 'undefined') {
if(opts.AUDIO.toLowerCase() === 'false') {
console.log('setting to false')
DEBUG.AUDIO = false
}
}
// global constants and variables for your app go here
let camera, scene, renderer, pointer, stats, vrmanager;
const game = {
blockService:null,
audioService:null,
click_sphere: null,
backdropSphere: null,
texture_loader: new THREE.TextureLoader(),
actions: {
fire_ball:null
},
physics:{
floorBody:null,
},
currentLevel:0,
ballcount:0,
crystalcount:999,
levels: [
"tumble_level1",
"tumble_level2",
"tumble_level5",
"tumble_level6",
"tumble_level7",
"tumble_level8",
"tumble_level10",
"tumble_level11",
"tumble_level12",
"tumble_level13",
"tumble_level14",
"tumble_level9",
"tumble_level15",
]
}
if(CONSTS.docid !== null) game.levels.unshift(CONSTS.docid)
function destroyCrystal(target) {
//play glass break sound
game.audioService.play("crash")
//remove the block from the scene and simulation
game.blockService.removeFromSimulation(target)
//add explosion effects
const v = new THREE.Vector3()
target.userData.block.obj.localToWorld(v)
playParticles(v)
}
function remap(val, smin, smax, emin, emax) {
const t = (val-smin)/(smax-smin)
return (emax-emin)*t + emin
}
let parts
let pendingParticles = []
function initParticles() {
const options = {
maxParticles: 10000,
position: new THREE.Vector3(0,0,0),
positionRandomness: 0.0,
baseVelocity: new THREE.Vector3(0.0, 0.0, 0.0),
velocity: new THREE.Vector3(0.0, 0.0, 0.0),
velocityRandomness: 0.3,
acceleration: new THREE.Vector3(0.0,0.0,0.0),
color: new THREE.Color(0.5,0.3,1.0),
endColor: new THREE.Color(0.5,0.0,1.0),
colorRandomness: 0.5,
lifetime: 3.0,
fadeIn: 0.1,
fadeOut: 0.2,
size: 40,
sizeRandomness: 1.0,
particleSpriteTex: game.texture_loader.load('./textures/spark.png'),
blending: THREE.AdditiveBlending,
onTick:(system,time) => {
pendingParticles.forEach(v => {
options.position.copy(v)
for (let i = 0; i < 100; i++) {
const v = 3.0
options.velocity.x = remap(Math.random(),0,1,-v,v)
options.velocity.y = remap(Math.random(),0,1,-v,v)
options.velocity.z = remap(Math.random(),0,1,-v,v)
system.spawnParticle(options);
}
})
pendingParticles = []
}
}
parts = new GPUParticleSystem(options)
parts.userData.skipRaycast = true
scene.add(parts)
}
function playParticles(v) {
pendingParticles.push(v)
}
function handleCollision(e) {
if(game.blockService.ignore_collisions) return
//ignore tiny collisions
if(Math.abs(e.contact.getImpactVelocityAlongNormal() < 1.0)) return
//when ball hits moving block,
if(e.body.jtype === BLOCK_TYPES.BALL) {
if( e.target.jtype === BLOCK_TYPES.WALL) {
game.audioService.play('click')
}
if (e.target.jtype === BLOCK_TYPES.BLOCK) {
//hit a block, just make the thunk sound
game.audioService.play('click')
}
}
//if crystal hits anything and the impact was strong enought
if(e.body.jtype === BLOCK_TYPES.CRYSTAL || e.target.jtype === BLOCK_TYPES.CRYSTAL) {
if(Math.abs(e.contact.getImpactVelocityAlongNormal() >= 2.0)) {
return destroyCrystal(e.target)
}
}
// console.log(`collision: body ${e.body.jtype} target ${e.target.jtype}`)
}
function loadStructure() {
console.log("fetching",game.currentLevel, game.levels[game.currentLevel])
return fetch(`${CONSTS.BASE_URL}${game.levels[game.currentLevel]}?cachebust=${Math.random()}`)
.then(res => res.json())
.then(res => {
return game.blockService.loadFromJSON(res)
})
}
function initBall(scene) {
game.click_sphere = new THREE.Mesh(
new THREE.SphereGeometry(4),
new THREE.MeshLambertMaterial({color: 'green', side: THREE.BackSide, visible: false})
)
game.click_sphere.visible = true
game.click_sphere.userData.clickable = true
game.click_sphere.position.set(0, 0, 0)
scene.add(game.click_sphere)
}
function initFireball() {
game.actions.fire_ball = new FireBallAction(scene, pointer.controller1, game)
on(game.click_sphere,POINTER_PRESS,game.actions.fire_ball.startPressSphere)
on(game.click_sphere,POINTER_RELEASE,game.actions.fire_ball.endPressSphere)
game.actions.fire_ball.setEnabled(false)
}
function initLights() {
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
//set the background color of the scene
scene.background = new THREE.Color( 0xcccccc );
//a standard light
const light = new THREE.DirectionalLight( 0xffffff, 0.5 );
light.castShadow = true
light.shadow.mapSize.width = 512
light.shadow.mapSize.height = 512
light.shadow.camera.right = 10
light.shadow.camera.top = 10
light.position.set( 0, 1, 0.5 ).normalize();
light.userData.skipRaycast = true
scene.add( light );
const ambient = new THREE.AmbientLight(0xffffff,0.5)
ambient.userData.skipRaycast = true
scene.add(ambient)
}
function initBackground() {
//background image
if(DEBUG.SKYBOX) {
const sky = new THREE.Mesh(
new THREE.SphereGeometry(50),
new THREE.MeshBasicMaterial({
color:'white',
map:game.texture_loader.load('./textures/sky2.jpg'),
side: THREE.BackSide
})
)
sky.userData.skipRaycast = true
scene.add(sky)
}
const tex = game.texture_loader.load('./textures/candycane.png')
tex.wrapS = THREE.RepeatWrapping
tex.wrapT = THREE.RepeatWrapping
tex.repeat.set(8,8)
const background = new THREE.Group()
const candyCones = new THREE.Geometry()
candyCones.merge(new THREE.ConeGeometry(1,10,16,8).translate(-22,5,0))
candyCones.merge(new THREE.ConeGeometry(1,10,16,8).translate(22,5,0))
candyCones.merge(new THREE.ConeGeometry(1,10,16,8).translate(7,5,-30))
candyCones.merge(new THREE.ConeGeometry(1,10,16,8).translate(-13,5,-20))
background.add(new THREE.Mesh(candyCones,new THREE.MeshLambertMaterial({ color:'white', map:tex,})))
const greenCones = new THREE.Geometry()
greenCones.merge(new THREE.ConeGeometry(1,5,16,8).translate(-15,2,-5))
greenCones.merge(new THREE.ConeGeometry(1,5,16,8).translate(-8,2,-28))
greenCones.merge(new THREE.ConeGeometry(1,5,16,8).translate(-8.5,0,-25))
greenCones.merge(new THREE.ConeGeometry(1,5,16,8).translate(15,2,-5))
greenCones.merge(new THREE.ConeGeometry(1,5,16,8).translate(14,0,-3))
background.add(new THREE.Mesh(greenCones,new THREE.MeshLambertMaterial({color:'green', map:tex,})))
const dome_geo = new THREE.Geometry()
//left
dome_geo.merge(new THREE.SphereGeometry(6).translate(-20,-4,0))
dome_geo.merge(new THREE.SphereGeometry(10).translate(-25,-5,-10))
//right
dome_geo.merge(new THREE.SphereGeometry(10).translate(30,-5,-10))
dome_geo.merge(new THREE.SphereGeometry(6).translate(27,-3,2))
//front
dome_geo.merge(new THREE.SphereGeometry(15).translate(0,-6,-40))
dome_geo.merge(new THREE.SphereGeometry(7).translate(-15,-3,-30))
dome_geo.merge(new THREE.SphereGeometry(4).translate(7,-1,-25))
//back
dome_geo.merge(new THREE.SphereGeometry(15).translate(0,-6,40))
dome_geo.merge(new THREE.SphereGeometry(7).translate(-15,-3,30))
dome_geo.merge(new THREE.SphereGeometry(4).translate(7,-1,25))
background.add(new THREE.Mesh(dome_geo,new THREE.MeshLambertMaterial({color:'white'})))
background.position.set(0,0,0)
background.userData.skipRaycast = true
scene.add(background)
}
function initAudio() {
game.audioService = new AudioService({enabled:DEBUG.AUDIO})
game.audioService.load("click","./sounds/plink.wav")
game.audioService.load("crash","./sounds/crash1.wav")
game.audioService.load("thunk","./sounds/thunk.wav")
game.audioService.load("bg","./music/sugarplum.mp3")
.then(()=>{
if(!DEBUG.AUDIO) return
game.bg_music = game.audioService.play("bg")
game.bg_music.loop = true
})
}
function initCount() {
if(!DEBUG.SCORE) return
game.crystalCountText = new SimpleText(2,1);
game.crystalCountText.setText("playing")
game.crystalCountText.position.set(0,0,-1.5)
game.crystalCountText.userData.skipRaycast = true
scene.add(game.crystalCountText)
}
function initText() {
game.text = new SimpleText(2,1,256)
game.text.backgroundColor = 'white'
game.text.fheight = 60
game.text.font = '50px sans-serif'
game.text.setText("Jingle Smash\ndestroy blue blocks\nclick to play")
game.text.position.set(0,2,-2.0)
game.text.userData.skipRaycast = true
scene.add(game.text)
}
function startLevel() {
console.log("starting the level")
loadStructure().then(()=>{
T2.sequence()
.then(()=>{
game.blockService.startPlaying()
})
.then(T2.prop({
target:game.backdropSphere.material,
property:'opacity',
from:1.0,
to:0.0,
duration:0.5,
}))
.then(()=>{
game.backdropSphere.visible = false
game.text.visible = false
})
.then(()=>{
game.actions.fire_ball.setEnabled(true)
game.actions.fire_ball.resetBall()
})
.start()
})
}
let lastScoreUpdate = 0
function updateScore(time) {
if(!game.blockService.isPlaying()) return
if(time-lastScoreUpdate < 1.0) return
lastScoreUpdate = time
game.crystalcount = game.blockService.getAllBlocks()
.filter(b => b.physicsType === BLOCK_TYPES.CRYSTAL)
.filter(b => b.obj.visible)
.length
if(DEBUG.SCORE) game.crystalCountText.setText(`crystals ${count}`)
// console.log("updating ball count",game.ballcount,'current level',game.currentLevel, 'crystals left',game.crystalcount)
const now = Date.now()
const activeBalls = game.blockService.balls
.filter(ball => (now - ball.userData.fireTime < 4000))
.length
// console.log('active balls',activeBalls)
if(game.ballcount >= 3 && game.crystalcount > 0 && activeBalls <= 0) {
// console.log("used up our balls and still have crystals left")
restartLevel()
return
}
if(game.crystalcount <= 0) {
game.currentLevel++
if(game.currentLevel >= game.levels.length) {
winGame()
} else {
endLevel()
}
return
}
}
function restartLevel() {
game.ballcount = 0
game.blockService.stopPlaying()
game.actions.fire_ball.setEnabled(false)
T2.sequence()
.then(()=>{
game.text.visible = true
game.text.setText("Out of balls\nRestarting")
game.backdropSphere.visible = true
})
.then(T2.prop({
target:game.backdropSphere.material,
property:'opacity',
from:0.0,
to:1.0,
duration:0.5,
}))
.then(()=>{
pointer.waitSceneClick(()=>startLevel())
})
.start()
}
function winGame() {
game.ballcount = 0
game.blockService.stopPlaying()
game.actions.fire_ball.setEnabled(false)
game.text.visible = true
game.text.setText("you won!\nplay again?")
const cb = () => {
game.currentLevel = 0
startLevel()
off(game.click_sphere,POINTER_CLICK,cb)
}
on(game.click_sphere,POINTER_CLICK,cb)
}
function endLevel() {
game.ballcount = 0
game.blockService.stopPlaying()
game.actions.fire_ball.setEnabled(false)
T2.sequence()
.then(()=>{
game.text.visible = true
game.text.setText("Next Level")
game.backdropSphere.visible = true
})
.then(T2.prop({
target:game.backdropSphere.material,
property:'opacity',
from:0.0,
to:1.0,
duration:0.5,
}))
.then(()=>{
pointer.waitSceneClick(()=>startLevel())
})
.start()
}
function initContent(scene,camera,renderer) {
game.blockService = new BlockService(scene)
game.blockService.on('collision',handleCollision)
game.blockService.on('fireball',()=> game.ballcount++ )
initAudio()
initLights()
initParticles()
initBackground()
initCount()
initText()
game.backdropSphere = new THREE.Mesh(
new THREE.SphereGeometry(4),
new THREE.MeshLambertMaterial({color:'red', side: THREE.BackSide, transparent:true, opacity:1.0})
)
game.backdropSphere.userData.skipRaycast = true
scene.add(game.backdropSphere)
// enable stats visible inside VR
stats = new VRStats(renderer)
camera.add(stats)
camera.userData.skipRaycast = true
scene.add(camera)
const pointer_opts = {
//Pointer searches everything in the scene by default
//override this to match just certain things
intersectionFilter: ((o) => o.userData.clickable),
//eliminate from raycasting recursion to make it faster
recurseFilter: (o) => {
if(o.userData && o.userData.skipRaycast === true) return false
return true
},
//make the camera pan when moving the mouse. good for simulating head turning on desktop
cameraFollowMouse:true,
// set to true to move the controller node forward and tilt with the mouse.
// good for testing VR controls on desktop
mouseSimulatesController:false,
enableLaser: true,
laserLength:8,
enableMoveEvents:false,
}
initBall(scene)
loadStructure()
let detected = false
on($("#enter-button"),'click',(e)=> {
e.preventDefault()
e.stopPropagation()
//hide the overlay
$("#overlay").style.visibility = 'hidden'
//if vr,
if(detected) {
pointer = new Pointer(scene,renderer,camera, pointer_opts)
vrmanager.enterVR()
} else {
pointer_opts.mouseSimulatesController = true
pointer = new Pointer(scene,renderer,camera, pointer_opts)
}
pointer.controller1.userData.skipRaycast = true
initFireball()
pointer.waitSceneClick(()=>startLevel())
})
on(vrmanager,VR_DETECTED,()=>{
detected = true
$("#enter-button").innerText = "enter vr"
})
$("#enter-button").removeAttribute('disabled',false)
}
function render(time) {
//if(stats) stats.update(time)
if(pointer) pointer.tick(time)
T2.update(time)
game.blockService.update(time)
updateScore(time/1000)
if(game.actions.fire_ball) game.actions.fire_ball.updatePressSphere(time)
renderer.render( scene, camera );
if(parts) parts.update(time)
}
// you shouldn't need to modify much below here
function initScene() {
//create DIV for the canvas
const container = document.createElement( 'div' );
document.body.appendChild( container );
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.1, 100 );
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.gammaOutput = true
renderer.vr.enabled = true;
container.appendChild( renderer.domElement );
vrmanager = new VRManager(renderer)
initContent(scene,camera,renderer)
window.addEventListener( 'resize', ()=>{
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}, false );
THREE.DefaultLoadingManager.onStart = (url, loaded, total) => {
console.log(`loading ${url}. loaded ${loaded} of ${total}`)
}
THREE.DefaultLoadingManager.onLoad = () => {
console.log(`loading complete`)
console.log("really setting it up now")
$('#loading-indicator').style.display = 'none'
}
THREE.DefaultLoadingManager.onProgress = (url, loaded, total) => {
console.log(`prog ${url}. loaded ${loaded} of ${total}`)
$("#progress").setAttribute('value',100*(loaded/total))
}
THREE.DefaultLoadingManager.onError = (url) => {
console.log(`error loading ${url}`)
}
}
// initPage()
initScene()
renderer.setAnimationLoop(render)
</script>
</body>
</html>
You can’t perform that action at this time.