A recreation of the Battle of Yavin using threejs.
Explosions: http://codepen.io/Xanmia/pen/DoljI Planets: https://github.com/jeromeetienne/threex.planets
A Pen by Carson Britt on CodePen.
A recreation of the Battle of Yavin using threejs.
Explosions: http://codepen.io/Xanmia/pen/DoljI Planets: https://github.com/jeromeetienne/threex.planets
A Pen by Carson Britt on CodePen.
| nav.grid | |
| .col-1-4 | |
| ul.controls-left | |
| li: button.btn-pause.active | |
| li: button.btn-audio | |
| .col-1-2.center.banner | |
| h1 Death Star Trench Run | |
| a(target="_blank", href="https://www.youtube.com/watch?v=SEEOcqbeclg") For full effect, open this in another tab | |
| p | |
| .control: <strong>Move:</strong> WASD or ARROWS | |
| .control: <strong>Laser:</strong> SPACE | |
| .control: <strong>Computer:</strong> TAB | |
| p Fend off the rebels until their base is within firing range! Press Any Key To Start! | |
| .col-1-4 | |
| ul.controls-right.right | |
| li: button.btn-laser | |
| li: button.btn-computer <i></i> | |
| .computer.hidden | |
| .hits.left | |
| img(src="data:image/png;base64,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") | |
| .x × | |
| .xwing-count.number.number-first 0 | |
| .xwing-count.number.number-last 0 | |
| .counter | |
| span.number.number-0 0 | |
| span.number.number-1 0 | |
| span.number.number-2 0 | |
| span.number.number-3 0 | |
| span.number.number-4 0 | |
| span.number.number-5 0 |
| var scene, camera, renderer, controls, | |
| light, shadowLight, backLight, trench, | |
| stars, yavin, moon, lasers, laser_lights, | |
| beam, xwing, fighters; | |
| //========================================= | |
| // SETTINGS | |
| //========================================= | |
| var KEYS = { | |
| W: {code: '87', pressed: false}, | |
| A: {code: '65', pressed: false}, | |
| S: {code: '83', pressed: false}, | |
| D: {code: '68', pressed: false}, | |
| P: {code: '80', pressed: false}, | |
| up: {code: '38', pressed: false}, | |
| down: {code: '40', pressed: false}, | |
| left: {code: '37', pressed: false}, | |
| right: {code: '39', pressed: false}, | |
| space: {code: '32', pressed: false}, | |
| tab: {code: '9', pressed: false} | |
| }; | |
| var SOUNDS = { | |
| tiefighter: new Audio("https://s3.amazonaws.com/cbritt/codepen/fighter.mp3"), | |
| computer: new Audio("https://s3.amazonaws.com/cbritt/codepen/computer.mp3"), | |
| explosion: new Audio("https://s3.amazonaws.com/cbritt/codepen/exp-2.mp3"), | |
| xwing: new Audio("https://s3.amazonaws.com/cbritt/codepen/xwing.mp3"), | |
| laser: new Audio("https://s3.amazonaws.com/cbritt/codepen/laser.mp3"), | |
| lasers: [] | |
| }; | |
| var MODES = { | |
| COMPUTER: 'computer', | |
| SOLID: 'solid', | |
| LOSE: 'lose', | |
| WIN: 'win' | |
| }; | |
| var COLORS = { | |
| WHITE: 0xffffff, | |
| WHITE_BLUE: 0xDBE6FF, | |
| BLACK: 0x000000, | |
| GREY: 0x999999, | |
| RED: 0xFF3333, | |
| GREEN: 0x00FF00, | |
| BLUE: 0x2E70FF, | |
| YELLOW: 0xE0B300, | |
| DARK_GREY: 0x0e0e11, | |
| ORANGE: 0xFFAD14, | |
| BRIGHT_RED: 0xFF0000 | |
| }; | |
| var TEXTURES = { | |
| moon: "https://s3.amazonaws.com/cbritt/codepen/ZQhIPUP.jpg", | |
| stars: "https://s3.amazonaws.com/cbritt/codepen/Y1ocEbG.png", | |
| yavin: "https://s3.amazonaws.com/cbritt/codepen/m41aDH8.jpg" | |
| }; | |
| var CONFIG = { | |
| debug: false, | |
| paused: true, | |
| audio: true, | |
| speed: 3, | |
| fov: 90, | |
| view_distance: 5000, | |
| mode: MODES.SOLID, | |
| fog_density: 0.01, | |
| module_count: 40, | |
| module_width: 5, | |
| module_height: 50, | |
| module_length: 51, | |
| trench_width: 40, | |
| detail_max: 20, | |
| detail_offset: 4, | |
| laser_speed: 14, | |
| laser_max: 8, | |
| laser_length: 50, | |
| laser_color: COLORS.GREEN, | |
| laser_beam_offset: 1.5, | |
| strafe_speed: 0.7, | |
| strafe_max: 12, | |
| fighter_max: 1, | |
| fighter_speed: 3, | |
| xwing_remaining: 30, | |
| xwing_hits: 0, | |
| explosion_speed: 3, | |
| explosion_radius: 50, | |
| explosion_particles: 200, | |
| explosion_color: COLORS.ORANGE, | |
| moon_speed: 0.1, | |
| beam_speed: 100, | |
| counter: 5400 | |
| }; | |
| CONFIG._counter = CONFIG.counter; | |
| CONFIG.trench_length = CONFIG.module_count*CONFIG.module_length; | |
| CONFIG.camera_start = new THREE.Vector3(0, CONFIG.module_height/4, CONFIG.trench_length-CONFIG.module_length); | |
| CONFIG.laser_light_start = CONFIG.trench_length+CONFIG.laser_length/2; | |
| //========================================= | |
| // SETUP | |
| //========================================= | |
| //create scene, renderer, and camera | |
| function setupScene() { | |
| THREE.ImageUtils.crossOrigin = ''; | |
| var ratio = window.innerWidth / window.innerHeight; | |
| scene = new THREE.Scene(); | |
| scene.fog = new THREE.FogExp2( COLORS.DARK_GREY, CONFIG.fog_density ); | |
| renderer = new THREE.WebGLRenderer({alpha: true,antialias: true, premultipliedAlpha: false }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMapEnabled = true; | |
| renderer.shadowMapType = THREE.PCFSoftShadowMap; | |
| document.body.appendChild(renderer.domElement); | |
| camera = new THREE.PerspectiveCamera( CONFIG.fov, ratio, 1, CONFIG.view_distance); | |
| camera.position.set(CONFIG.camera_start.x, CONFIG.camera_start.y, CONFIG.camera_start.z); | |
| camera.lookAt(new THREE.Vector3(0, camera.position.y, 0)); | |
| controls = new THREE.OrbitControls( camera, renderer.domElement ); | |
| controls.enableDamping = true; | |
| controls.dampingFactor = 0.25; | |
| controls.enabled = false; | |
| stats = new Stats(); | |
| stats.domElement.style.position = 'absolute'; | |
| stats.domElement.style.top = '0px'; | |
| window.addEventListener('resize', resize, false); | |
| if(CONFIG.debug){ | |
| controls.enabled = true; | |
| document.body.appendChild( stats.domElement ); | |
| } | |
| } | |
| //window resize event | |
| function resize() { | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| } | |
| //create lights | |
| function setupLights() { | |
| //global illumination light | |
| light = new THREE.HemisphereLight(COLORS.WHITE_BLUE, COLORS.WHITE_BLUE, 0.4); | |
| //primary shadow light | |
| shadowLight = new THREE.DirectionalLight(COLORS.WHITE, 0.5); | |
| shadowLight.position.set(-CONFIG.trench_width-50, CONFIG.module_height+30, CONFIG.trench_length/2); | |
| shadowLight.target.position.set(0, CONFIG.module_height/2-20, CONFIG.trench_length/2); | |
| shadowLight.target.updateMatrixWorld(); | |
| shadowLight.shadowCameraFar = 150; | |
| shadowLight.shadowCameraLeft = -(CONFIG.trench_length/2+CONFIG.module_length); | |
| shadowLight.shadowCameraRight = (CONFIG.trench_length/2+CONFIG.module_length); | |
| shadowLight.shadowCameraBottom = -20; | |
| shadowLight.shadowCameraTop = 50; | |
| shadowLight.shadowDarkness = 0.85; | |
| shadowLight.shadowBias = 0.005; | |
| shadowLight.castShadow = true; | |
| //secondary shadow light | |
| backLight = new THREE.DirectionalLight(COLORS.WHITE, 0.4); | |
| backLight.position.set(50, CONFIG.module_height, CONFIG.trench_length/2); | |
| backLight.target.position.set(0, 0, CONFIG.trench_length/2); | |
| backLight.target.updateMatrixWorld(); | |
| //laser lights need to be setup before the lasers are created at runtime | |
| //to render properly, their positions are set to out of frame | |
| laser_lights = []; | |
| for(var i=0;i<CONFIG.laser_max;i++){ | |
| var l = new THREE.PointLight(CONFIG.laser_color, 1, 40); | |
| l.position.set(0, 10, CONFIG.laser_light_start); | |
| laser_lights.push(l); | |
| scene.add(l); | |
| } | |
| scene.add(light, backLight, shadowLight); | |
| } | |
| //preload sounds so multiple versions can be played | |
| function setupSounds(){ | |
| for(var i=0;i<CONFIG.laser_max;i++){ | |
| var sound = new Audio('https://dl.dropboxusercontent.com/u/3656711/blaster.mp3'); | |
| sound.volume = 0.2; | |
| SOUNDS.lasers.push(sound); | |
| } | |
| SOUNDS.computer.volume = 0.5; | |
| SOUNDS.computer.loop = true; | |
| SOUNDS.tiefighter.volume = 0.3; | |
| SOUNDS.xwing.volume = 0.4; | |
| SOUNDS.explosion.volume = 0.3; | |
| } | |
| //========================================= | |
| // OBJECTS | |
| //========================================= | |
| //main trench | |
| var Trench = function(){ | |
| this.child_index = CONFIG.module_count-1; | |
| this.group = new THREE.Object3D(); | |
| this.dx = 0; | |
| for(i=0;i<CONFIG.module_count;i++){ | |
| var section = new THREE.Object3D(); | |
| var mod = new Module(i*CONFIG.module_length); | |
| section.add(mod.group); | |
| section.add(mod.lights); | |
| section.add(mod.edges); | |
| this.group.add(section); | |
| } | |
| scene.add(this.group); | |
| this.update = function(){ | |
| this.group.position.z += CONFIG.speed; | |
| this.dx += CONFIG.speed; | |
| //one modules' distance travled | |
| //shift the furthest module to the beginning | |
| //note: module length needs to be divisible by speed or the trench will slowly shift back | |
| if(this.dx >= CONFIG.module_length){ | |
| var mod = this.group.children[this.child_index]; | |
| mod.position.z -= CONFIG.trench_length; | |
| //mod.position.z = 0; | |
| mod.updateMatrix(); | |
| this.child_index = (this.child_index <= 0) ? CONFIG.module_count-1 : this.child_index-1; | |
| this.dx = 0; | |
| } | |
| }; | |
| }; | |
| //trench section | |
| var Module = function(z) { | |
| this.parts = []; | |
| //shorthand | |
| var w = CONFIG.module_width; | |
| var h = CONFIG.module_height; | |
| var l = CONFIG.module_length; | |
| var t = CONFIG.trench_width; | |
| var w_half = w/2; | |
| var h_half = h/2; | |
| var l_half = l/2; | |
| var t_half = t/2; | |
| //basic trench walls | |
| var left = getBoxMesh(w, h, l, -t_half - w_half, h_half, z ); | |
| var right = getBoxMesh(w, h, l, t_half + w_half, h_half, z ); | |
| var bottom = getBoxMesh(t, w, l, 0, -w_half, z ); | |
| //detail left wall | |
| var detail_left = getDetail(l, h-w); | |
| detail_left.position.set(t_half, h_half, z); | |
| detail_left.rotation.y = Math.PI/2; | |
| //detail right wall | |
| var detail_right = getDetail(l, h-w); | |
| detail_right.position.set(-t_half, h_half, z); | |
| detail_right.rotation.y = -Math.PI/2; | |
| //detail bottom | |
| var detail_bottom = getDetail(l, t); | |
| detail_bottom.position.set(0, 0, z); | |
| detail_bottom.rotation.x = Math.PI/2; | |
| //detail lights | |
| this.lights = getDetailLights(l, t); | |
| this.lights.position.set(0, 0, z); | |
| //merged mesh | |
| this.parts.push(left, right, bottom, detail_left, detail_right, detail_bottom); | |
| this.group = mergeMeshes(this.parts); | |
| this.edges = new THREE.EdgesHelper( this.group.clone(), COLORS.YELLOW ); | |
| this.edges.visible = false; | |
| this.group.name = "solid"; | |
| this.edges.name = "edges"; | |
| this.lights.name = "lights"; | |
| }; | |
| //the planet yavin | |
| var Yavin = function(){ | |
| var yavin_geometry = new THREE.SphereGeometry(2000, 32, 32); | |
| var yavin_texture = THREE.ImageUtils.loadTexture(TEXTURES.yavin); | |
| yavin_texture.repeat.set( 2, 2 ); | |
| var yavin_material = new THREE.MeshPhongMaterial({ map: yavin_texture, fog: false }); | |
| this.group = new THREE.Mesh(yavin_geometry, yavin_material); | |
| this.group.position.set(-1200, 2400, 800); | |
| this.group.rotation.z = Math.PI/4; | |
| this.group.rotation.x = Math.PI/3; | |
| scene.add(this.group); | |
| }; | |
| //yavin 4 | |
| var Moon = function(){ | |
| var moon_geometry = new THREE.SphereGeometry(300, 32, 32); | |
| var moon_texture = THREE.ImageUtils.loadTexture(TEXTURES.moon); | |
| var moon_material = new THREE.MeshPhongMaterial({map: moon_texture,fog: false }); | |
| this.group = new THREE.Mesh(moon_geometry, moon_material); | |
| this.group.position.set(200, 1800, -700); | |
| this.group.rotation.x = -Math.PI/2; | |
| this.group.rotation.z = -Math.PI/2; | |
| scene.add(this.group); | |
| this.update = function(){ | |
| if(!this.explosion){ | |
| this.group.position.x += CONFIG.moon_speed; | |
| this.group.position.y -= CONFIG.moon_speed/2; | |
| } | |
| else{ | |
| this.explosion.update(); | |
| } | |
| }; | |
| this.explode = function(){ | |
| scene.remove(this.group); | |
| this.explosion = new Explosion(this.group.position.clone(), { | |
| particles: 500, | |
| radius: 1000, | |
| size: 10, | |
| speed: 15, | |
| mute: true | |
| }, function(){ | |
| win(); | |
| }); | |
| }; | |
| }; | |
| //background stars | |
| var Starfield = function(){ | |
| var stars_geometry = new THREE.SphereGeometry(CONFIG.view_distance/2, 32, 32); | |
| var stars_texture = THREE.ImageUtils.loadTexture(TEXTURES.stars); | |
| var stars_material = new THREE.MeshBasicMaterial({ map: stars_texture, side: THREE.BackSide, fog:false }); | |
| this.group = new THREE.Mesh(stars_geometry, stars_material); | |
| scene.add(this.group); | |
| }; | |
| //single laser burst | |
| var Laser = function(){ | |
| this.group = new THREE.Object3D(); | |
| //z is bigger than the x, y threshold | |
| //to account for the speed of the laser | |
| var hit_threshold = 2.5; | |
| var hit_threshold_z = 15; | |
| this.start = new THREE.Vector3(camera.position.x, camera.position.y - 5, 0); | |
| var material = new THREE.LineBasicMaterial({ color: CONFIG.laser_color, linewidth: 2, fog:false }); | |
| var geometry = new THREE.Geometry(); | |
| geometry.vertices.push(new THREE.Vector3(0, this.start.y, 0)); | |
| geometry.vertices.push(new THREE.Vector3(0, this.start.y, CONFIG.laser_length)); | |
| //A laser contains two beams | |
| var line_l = new THREE.Line(geometry, material); | |
| var line_r = new THREE.Line(geometry, material); | |
| line_l.position.x = this.start.x - CONFIG.laser_beam_offset; | |
| line_r.position.x = this.start.x + CONFIG.laser_beam_offset; | |
| this.group.add(line_l, line_r); | |
| this.group.position.z = CONFIG.trench_length; | |
| this.update = function(){ | |
| this.group.position.z -= CONFIG.laser_speed; | |
| }; | |
| //collision detection with xwing | |
| this.hit = function(){ | |
| //no xwing to hit | |
| if(!xwing) return false; | |
| //xwing currently exploding | |
| if(xwing.explosion && xwing.explosion.exploding) return false; | |
| //chcek collsion | |
| var dx = Math.abs(this.start.x - xwing.group.position.x); | |
| var dy = Math.abs(this.start.y - xwing.group.position.y); | |
| var dz = Math.abs(this.group.position.z - xwing.group.position.z); | |
| //console.log("diff: "+dx.toFixed(1)+"-"+dy.toFixed(1)+"-"+dz ); | |
| if(dx <= hit_threshold && dy <= hit_threshold && dz <= hit_threshold_z ){ | |
| registerHit(); | |
| return true; | |
| } | |
| }; | |
| }; | |
| //create lasers dynamically | |
| var LaserSpawner = function(){ | |
| this.items = []; | |
| this.fire = function(){ | |
| if(this.items.length<CONFIG.laser_max && !KEYS.space.pressed && !CONFIG.paused){ | |
| var laser = new Laser(); | |
| //get first available laser light. | |
| var light = _.find(laser_lights, function(l){ | |
| return l.position.z >= CONFIG.laser_light_start; | |
| }); | |
| scene.add(laser.group); | |
| this.items.push({ | |
| laser: laser, | |
| light: light | |
| }); | |
| if(CONFIG.audio) { | |
| var sound = _.find(SOUNDS.lasers, function(s){ | |
| return s.paused; | |
| }); | |
| sound.play(); | |
| } | |
| } | |
| }; | |
| this.update = function(){ | |
| var self = this; | |
| self.items.forEach(function(item){ | |
| if(item.laser.hit()){ | |
| item.light.position.z = CONFIG.laser_light_start; | |
| scene.remove(item.laser.group); | |
| _.pull(self.items, item); | |
| xwing.explode(); | |
| } | |
| else if(item.laser.group.position.z < 0){ | |
| item.light.position.z = CONFIG.laser_light_start; | |
| scene.remove(item.laser.group); | |
| _.pull(self.items, item); | |
| } | |
| else{ | |
| item.laser.update(); | |
| if(CONFIG.mode != MODES.COMPUTER){ | |
| item.light.position.z -= CONFIG.laser_speed; | |
| } | |
| } | |
| }); | |
| }; | |
| }; | |
| //Tie fighter | |
| var TieFighter = function(start, end, squad){ | |
| this.parts = []; | |
| //movement vector | |
| this.dir = start.clone().sub(end).normalize().multiplyScalar(CONFIG.fighter_speed); | |
| var geometry = new THREE.SphereGeometry( 2, 32, 32 ); | |
| var material = new THREE.MeshLambertMaterial( {color: 0x999999, fog:false } ); | |
| var core = new THREE.Mesh( geometry, material ); | |
| //struts | |
| var s1 = getBoxMesh(5, 1, 1, -2, 0, 0); | |
| var s2 = getBoxMesh(5, 1, 1, 2, 0, 0); | |
| //wings | |
| var w1 = getBoxMesh(0.5, 12, 7, 5, 0, 0); | |
| var w2 = getBoxMesh(0.5, 12, 7, -5, 0, 0); | |
| //merged | |
| this.parts.push(core, s1, s2, w1, w2); | |
| this.mesh = mergeMeshes(this.parts); | |
| this.group = this.mesh; | |
| //add more fighters flying in unison | |
| if(squad){ | |
| this.mesh_2 = this.mesh.clone(); | |
| this.mesh_3 = this.mesh_2.clone(); | |
| if(chance(80)){ | |
| this.mesh_2.position.x += _.random(-20, 20); | |
| this.mesh_2.position.y += _.random(20, 50); | |
| this.mesh_2.position.z += _.random(-20, 20); | |
| this.group.add(this.mesh_2); | |
| } | |
| if(chance(50)){ | |
| this.mesh_3.position.x += _.random(30, 50); | |
| this.mesh_3.position.y += _.random(-5, 5); | |
| this.mesh_3.position.z += _.random(-50, -60); | |
| this.group.add(this.mesh_3); | |
| } | |
| } | |
| this.group.position.set(start.x, start.y, start.z); | |
| this.group.lookAt(end); | |
| this.update = function(){ | |
| this.group.position.x -= this.dir.x; | |
| this.group.position.z -= this.dir.z; | |
| }; | |
| }; | |
| //random tiefighters flying overhead | |
| var TieFighterSpawner = function(){ | |
| this.items = []; | |
| this.locked = false; | |
| var x_threshold = 150; | |
| var max_z = CONFIG.trench_length-100; | |
| var min_z = CONFIG.trench_length-400; | |
| this.spawn = function(){ | |
| var dir = Math.round(Math.random()) * 2 - 1; | |
| var y = CONFIG.module_height*2; | |
| var start = new THREE.Vector3(dir*x_threshold, y, _.random(min_z, max_z)); | |
| var end = new THREE.Vector3(-dir*x_threshold, y, _.random(min_z, max_z)); | |
| var fighter = new TieFighter(start, end, true); | |
| this.items.push(fighter); | |
| scene.add(fighter.group); | |
| if(CONFIG.audio && !CONFIG.paused && CONFIG.mode != MODES.COMPUTER) SOUNDS.tiefighter.play(); | |
| this.locked = false; | |
| }; | |
| this.update = function(){ | |
| var self = this; | |
| if(self.items.length < CONFIG.fighter_max && !self.locked && CONFIG.mode != MODES.COMPUTER){ | |
| self.locked = true; | |
| setTimeout(function(){ self.spawn(); }, _.random(5000, 10000)); | |
| } | |
| self.items.forEach(function(fighter){ | |
| if(Math.abs(fighter.group.position.x) > x_threshold){ | |
| scene.remove(fighter.group); | |
| _.pull(self.items, fighter); | |
| } | |
| else{ | |
| fighter.update(); | |
| } | |
| }); | |
| }; | |
| }; | |
| //xwing fighter | |
| var Xwing = function(){ | |
| var z = CONFIG.trench_length-130; | |
| var y = 15; | |
| var x = 0; | |
| this.parts = []; | |
| this.inside_trench = true; | |
| this.strafe_active = false; | |
| this.strafe_dir = new THREE.Vector3(x, y, z); | |
| this.strafe_end = new THREE.Vector3(x, y, z); | |
| this.strafe_start = new THREE.Vector3(x, y, z); | |
| //core | |
| var core = getBoxMesh(2, 2, 10, 0, 0, -5); | |
| //wings | |
| var wing_1 = getBoxMesh(12, 0.3, 1.3, 0, 0, -0.7); | |
| var wing_2 = getBoxMesh(12, 0.3, 1.3, 0, 0, -0.7); | |
| wing_1.rotation.z = Math.PI/7; | |
| wing_2.rotation.z = -Math.PI/7; | |
| //guns | |
| var g1 = getCylinderMesh(0.3, 0.3, 4, -5.6, 2.5, -2); | |
| var g2 = getCylinderMesh(0.3, 0.3, 4, -5.6, -2.5, -2); | |
| var g3 = getCylinderMesh(0.3, 0.3, 4, 5.6, 2.5, -2); | |
| var g4 = getCylinderMesh(0.3, 0.3, 4, 5.6, -2.5, -2); | |
| //engines | |
| var e1 = getCylinderMesh(0.7, 0.7, 2, -1.5, -1, -1); | |
| var e2 = getCylinderMesh(0.7, 0.7, 2, -1.5, 1, -1); | |
| var e3 = getCylinderMesh(0.7, 0.7, 2, 1.5, -1, -1); | |
| var e4 = getCylinderMesh(0.7, 0.7, 2, 1.5, 1, -1); | |
| //lights | |
| var l1 = new THREE.PointLight(COLORS.RED, 35, 0.7); | |
| var l2 = new THREE.PointLight(COLORS.RED, 35, 0.7); | |
| var l3 = new THREE.PointLight(COLORS.RED, 35, 0.7); | |
| var l4 = new THREE.PointLight(COLORS.RED, 35, 0.7); | |
| l1.position.set(1.5, 1, 0.3); | |
| l2.position.set(1.5, -1, 0.3); | |
| l3.position.set(-1.5, 1, 0.3); | |
| l4.position.set(-1.5, -1, 0.3); | |
| //shadow of xwing in trench | |
| var shadow = new THREE.SpotLight(COLORS.WHITE, 10); | |
| shadow.onlyShadow = true; | |
| shadow.castShadow = true; | |
| shadow.position.set(x, y+35, z); | |
| shadow.target.position.set(x,y,z); | |
| shadow.target.updateMatrixWorld(); | |
| //shadow.shadowCameraVisible = true; | |
| shadow.shadowDarkness = 0.7; | |
| shadow.shadowCameraNear = 1; | |
| shadow.shadowCameraFar = 80; | |
| scene.add(shadow); | |
| //lights | |
| this.lights = new THREE.Object3D(); | |
| this.lights.add( l1, l2, l3, l4); | |
| this.lights.name = "lights"; | |
| //merged mesh | |
| this.parts.push(core, wing_1, wing_2); | |
| this.parts.push(g1, g2, g3, g4); | |
| this.parts.push(e1, e2, e3, e4); | |
| this.mesh = mergeMeshes(this.parts); | |
| this.mesh.receiveShadow = false; | |
| this.mesh.material = new THREE.MeshLambertMaterial({ color: 0xcccccc }); | |
| this.mesh.name = "mesh"; | |
| //edges | |
| this.edges = new THREE.EdgesHelper( this.mesh.clone(), COLORS.BRIGHT_RED ); | |
| this.edges.visible = false; | |
| this.edges.name = "edges"; | |
| //group | |
| this.group = new THREE.Object3D(); | |
| this.group.add(this.mesh, this.lights, this.edges); | |
| this.group.position.set(x, y, z); | |
| scene.add(this.group); | |
| this.update = function(){ | |
| if(this.explosion){ | |
| this.explosion.update(); | |
| } | |
| if(!this.inside_trench){ | |
| this.enterTrench(); | |
| } | |
| else{ | |
| this.strafe(); | |
| } | |
| }; | |
| //move arround randomly on the x y axis | |
| this.strafe = function(){ | |
| var max_x = 14; | |
| var max_y = 20; | |
| //no destination, get a new point | |
| if(!this.strafe_active){ | |
| this.strafe_start = new THREE.Vector3(this.group.position.x, this.group.position.y, this.group.position.z); | |
| this.strafe_end.x = x - max_x + (Math.random()*(max_x*2)); | |
| this.strafe_end.y = (y-5) + (Math.random()*(max_y*2)); | |
| this.strafe_active = true; | |
| } | |
| //move to the point | |
| else{ | |
| var strafe_speed = this.getSpeed(this.group.position, this.strafe_end); | |
| this.strafe_dir = this.strafe_start.clone().sub(this.strafe_end).normalize().multiplyScalar(strafe_speed); | |
| this.group.position.x -= this.strafe_dir.x; | |
| this.group.position.y -= this.strafe_dir.y; | |
| this.group.position.z = z; | |
| var xdiff = Math.abs(this.group.position.x - this.strafe_end.x) < 2; | |
| var ydiff = Math.abs(this.group.position.y - this.strafe_end.y) < 2; | |
| var xbound = Math.abs(this.group.position.x) > x+max_x; | |
| var ybound = Math.abs(this.group.position.y) > y+max_y; | |
| //arrived or out of bounds | |
| if((xdiff && ydiff) || xbound || ybound){ | |
| this.strafe_active = false; | |
| } | |
| } | |
| }; | |
| //get speed for sudo smooth movement | |
| this.getSpeed = function(prev_vector, next_vector){ | |
| var x = Math.abs(prev_vector.x - next_vector.x); | |
| var y = Math.abs(prev_vector.y - next_vector.y); | |
| var greatest =( x > y ? x : y).toFixed(2); | |
| //increase this for a smoother strafe movement | |
| var speedModifier = 16; | |
| var speed = (greatest/speedModifier).toFixed(2); | |
| return speed; | |
| }; | |
| //move from outside to inside trench | |
| this.enterTrench = function(){ | |
| if(this.group.position.y < y){ | |
| this.inside_trench = true; | |
| return; | |
| } | |
| this.group.position.y-=0.4; | |
| this.group.position.z-=0.43; | |
| }; | |
| //when explosion occurs reset to starting position outside trench | |
| this.reset = function(){ | |
| this.group.position.y += (CONFIG.module_height+CONFIG.module_height/4); | |
| this.group.position.z += 70; | |
| this.group.position.x = 0; | |
| if(CONFIG.audio && !CONFIG.paused) SOUNDS.xwing.play(); | |
| this.inside_trench = false; | |
| }; | |
| //create exposion at location of xwing | |
| this.explode = function(){ | |
| if(this.explosion) scene.remove(this.explosion.particles); | |
| this.explosion = new Explosion(new THREE.Vector3(this.group.position.x, this.group.position.y, this.group.position.z)); | |
| this.reset(); | |
| }; | |
| }; | |
| //create an explosion: http://codepen.io/Xanmia/pen/DoljI | |
| var Explosion = function(position, opts, cb){ | |
| cb = cb || function(){}; | |
| opts = opts || {}; | |
| var radius = opts.radius || CONFIG.explosion_radius; | |
| var particles = opts.particles || CONFIG.explosion_particles; | |
| var speed = opts.speed || CONFIG.explosion_speed; | |
| var size = opts.size || 0.4; | |
| var mute = opts.mute || false; | |
| this.dirs = []; | |
| var material = new THREE.PointCloudMaterial({ size: size, color: CONFIG.explosion_color, fog: false}); | |
| var geometry = new THREE.Geometry(); | |
| for (var i=0;i<particles;i++){ | |
| var vertex = new THREE.Vector3(); | |
| vertex.x = position.x; | |
| vertex.y = position.y; | |
| vertex.z = position.z; | |
| geometry.vertices.push(vertex); | |
| this.dirs.push({ | |
| x:(Math.random() * speed)-(speed/2), | |
| y:(Math.random() * speed)-(speed/2), | |
| z:(Math.random() * speed)-(speed/2) | |
| }); | |
| } | |
| this.exploding = true; | |
| this.particles = new THREE.PointCloud(geometry, material); | |
| scene.add(this.particles); | |
| if(CONFIG.audio && !mute) SOUNDS.explosion.play(); | |
| this.update = function(){ | |
| if (this.exploding){ | |
| var pCount = particles; | |
| while(pCount--) { | |
| var particle = this.particles.geometry.vertices[pCount]; | |
| var dx = Math.abs(particle.x) > Math.abs(position.x) + radius; | |
| var dy = Math.abs(particle.y) > Math.abs(position.y) + radius; | |
| var dz = Math.abs(particle.z) > Math.abs(position.z) + radius; | |
| if(dx || dy || dz){ | |
| this.exploding = false; | |
| cb(); | |
| return; | |
| } | |
| particle.x += this.dirs[pCount].x; | |
| particle.y += this.dirs[pCount].y; | |
| particle.z += this.dirs[pCount].z; | |
| } | |
| this.particles.geometry.verticesNeedUpdate = true; | |
| } | |
| else{ | |
| scene.remove(this.particles); | |
| } | |
| }; | |
| }; | |
| //fire the superweapon | |
| var Beam = function(){ | |
| //create the full beam out of frame | |
| //and them translate it rather than scaling | |
| this.firing = true; | |
| this.end = moon.group.position.clone(); | |
| this.start = new THREE.Vector3(400,0,CONFIG.trench_length/2); | |
| //total lenght of the beam | |
| this.dist = (this.start.clone().sub(this.end).length())*2; | |
| //current distance to the moon | |
| this.dist_to_moon = 100000000; | |
| //direction vector | |
| this.dir = this.start.clone().sub(this.end).normalize(); | |
| //reset the beam behing the start point at creation | |
| this.initial_scale = this.dir.clone().multiplyScalar(this.dist/2); | |
| //how much to translate by at each frame | |
| this.update_scale = this.dir.clone().multiplyScalar(CONFIG.beam_speed); | |
| var material = new THREE.MeshBasicMaterial({ color: COLORS.GREEN, fog:false }); | |
| var geom = new THREE.CylinderGeometry(7, 7, this.dist); | |
| this.mesh = new THREE.Mesh(geom, material); | |
| this.mesh.rotation.x = Math.PI/2; | |
| this.group = new THREE.Object3D(); | |
| this.group.add(this.mesh); | |
| this.group.position.x = this.start.x+this.initial_scale.x; | |
| this.group.position.y = this.start.y+this.initial_scale.y; | |
| this.group.position.z = this.start.z+this.initial_scale.z; | |
| this.group.lookAt(this.end); | |
| scene.add(this.group); | |
| if(CONFIG.audio) SOUNDS.laser.play(); | |
| this.update = function(){ | |
| if(!this.firing) return; | |
| if(this.hit()){ | |
| this.firing = false; | |
| scene.remove(this.group); | |
| moon.explode(); | |
| } | |
| else{ | |
| this.group.position.x -= this.update_scale.x; | |
| this.group.position.y -= this.update_scale.y; | |
| this.group.position.z -= this.update_scale.z; | |
| } | |
| }; | |
| this.hit = function(){ | |
| var old_dist = this.dist_to_moon; | |
| this.dist_to_moon = this.group.position.clone().sub(this.end).length() + (this.dist/2); | |
| return (this.dist_to_moon > old_dist); | |
| }; | |
| }; | |
| //========================================= | |
| // HELPER FUNCTIONS | |
| //========================================= | |
| //returns true percent of the time | |
| function chance(percent){ | |
| return (Math.random() < percent/100.0); | |
| } | |
| //create a box mesh with a geometry and material | |
| function getBoxMesh(w, h, l, x, y, z) { | |
| var material = new THREE.MeshLambertMaterial({ color: COLORS.WHITE, fog:false }); | |
| var geom = new THREE.BoxGeometry(w, h, l); | |
| var mesh = new THREE.Mesh(geom, material); | |
| mesh.position.set(x || 0, y || 0, z || 0); | |
| mesh.receiveShadow = true; | |
| mesh.castShadow = true; | |
| return mesh; | |
| } | |
| //create a Cylinder mesh with a geometry and material | |
| function getCylinderMesh(t, b, l, x, y, z) { | |
| var material = new THREE.MeshLambertMaterial({ color: COLORS.WHITE, fog:false }); | |
| var geom = new THREE.CylinderGeometry(t, b, l); | |
| var mesh = new THREE.Mesh(geom, material); | |
| mesh.rotation.x = Math.PI/2; | |
| mesh.position.set(x || 0, y || 0, z || 0); | |
| mesh.receiveShadow = true; | |
| mesh.castShadow = true; | |
| return mesh; | |
| } | |
| //merge geometries of meshes | |
| function mergeMeshes (meshes) { | |
| var material = meshes[0].material; | |
| var combined = new THREE.Geometry(); | |
| for (var i = 0; i < meshes.length; i++) { | |
| meshes[i].updateMatrix(); | |
| combined.merge(meshes[i].geometry, meshes[i].matrix); | |
| } | |
| var mesh = new THREE.Mesh(combined, material); | |
| mesh.castShadow = true; | |
| mesh.receiveShadow = true; | |
| return mesh; | |
| } | |
| //add detail (smaller boxes) to the walls | |
| function getDetail(lw, lh){ | |
| var i, w, h, x, y; | |
| var details = []; | |
| //add some big details | |
| for(i=0;i<CONFIG.detail_max;i++){ | |
| var depth = _.random(CONFIG.detail_offset/4, CONFIG.detail_offset, true); | |
| w = _.random(lw/5, lw/3*2, true); | |
| h = _.random(lh/5, lh/3*2, true); | |
| x = _.random(-lw+w/2, lw-w/2, true); | |
| y = _.random(-(lh)+(h/2), (lh/2)-(h/6), true); | |
| details.push(getBoxMesh(w, h, depth, x, y, -depth/2)); | |
| } | |
| //add some smaller details | |
| for(i=0;i<CONFIG.detail_max;i++){ | |
| var depth = _.random(CONFIG.detail_offset, CONFIG.detail_offset+1, true); | |
| w = _.random(lw/15, lw/12, true); | |
| h = _.random(lh/10, lh/5, true); | |
| x = _.random(-lw+w/2, lw-w/2, true); | |
| y = _.random(-(lh)+(h/2), (lh/2)-(h/4), true); | |
| details.push(getBoxMesh(w, h, depth, x, y, -depth/2)); | |
| } | |
| return mergeMeshes(details); | |
| } | |
| //add detail light particles on the modules | |
| function getDetailLights(lw, lh){ | |
| var x, y, z; | |
| var geom = new THREE.Geometry(); | |
| var max = 20; | |
| //left | |
| for(var i=0; i<max;i++){ | |
| var x = (-lh/2)+CONFIG.detail_offset; | |
| var y = _.random(0, CONFIG.module_height-5, true); | |
| var z = _.random(-lh/2, lh/2, true); | |
| geom.vertices.push(new THREE.Vector3( x, y, z)); | |
| } | |
| //right | |
| for(var i=0; i<max;i++){ | |
| x = (lh/2)-CONFIG.detail_offset; | |
| y = _.random(0, CONFIG.module_height-5, true); | |
| z = _.random(-lh/2, lh/2, true); | |
| geom.vertices.push(new THREE.Vector3( x, y, z)); | |
| } | |
| //bottom | |
| for(var i=0; i<max;i++){ | |
| x = _.random(-lw/2, lw/2, true); | |
| y = CONFIG.detail_offset-1; | |
| z = _.random(-lh/2, lh/2, true); | |
| geom.vertices.push(new THREE.Vector3( x, y, z)); | |
| } | |
| var material = new THREE.PointCloudMaterial({ color: 0xCCCCCC, size:0.1 }); | |
| var dots = new THREE.PointCloud( geom, material ); | |
| return dots; | |
| } | |
| //update the distance value for computer mode | |
| function updateCounter(){ | |
| if(CONFIG.speed){ | |
| CONFIG.counter = CONFIG.counter < 0 ? 0 : CONFIG.counter-1; | |
| //time out fire the weapon | |
| if(CONFIG.counter < 1 && !beam) { beam = new Beam(); } | |
| var num = ("000000"+CONFIG.counter); | |
| //force monospace by updating each character | |
| //in a seperate element with fixed width | |
| num = num.substr(num.length-6).split(""); | |
| for(var i=0;i<num.length;i++){ | |
| document.querySelector('.number-'+i).innerHTML = num[i]; | |
| } | |
| } | |
| } | |
| //toggle modes | |
| function toggleMode(){ | |
| if(CONFIG.paused) togglePause(); | |
| document.querySelector('body').classList.toggle('mode-computer'); | |
| document.querySelector('.computer').classList.toggle('hidden'); | |
| document.querySelector('.btn-computer').classList.toggle('active'); | |
| mode = (CONFIG.mode == MODES.SOLID) ? MODES.COMPUTER : MODES.SOLID; | |
| CONFIG.mode = mode; | |
| stars.group.visible = !stars.group.visible; | |
| for(var i=0;i<trench.group.children.length;i++){ | |
| var solid = trench.group.children[i].getObjectByName("solid"); | |
| var edges = trench.group.children[i].getObjectByName("edges"); | |
| var lights = trench.group.children[i].getObjectByName("lights"); | |
| edges.visible = !edges.visible; | |
| lights.visible = !lights.visible; | |
| } | |
| if(mode == MODES.COMPUTER){ | |
| if(CONFIG.audio && !CONFIG.paused) SOUNDS.computer.play(); | |
| scene.fog.density = 0; | |
| scene.remove(light, shadowLight, backLight); | |
| CONFIG.laser_color = COLORS.BRIGHT_RED; | |
| CONFIG.explosion_color = COLORS.BRIGHT_RED; | |
| } | |
| else { | |
| SOUNDS.computer.pause(); | |
| scene.fog.density = CONFIG.fog_density; | |
| scene.add(light, shadowLight, backLight); | |
| CONFIG.laser_color = COLORS.GREEN; | |
| CONFIG.explosion_color = COLORS.ORANGE; | |
| } | |
| var xm = xwing.group.getObjectByName("mesh"); | |
| var xl = xwing.group.getObjectByName("lights"); | |
| var xe = xwing.group.getObjectByName("edges"); | |
| xm.visible = !xm.visible; | |
| xl.visible = !xl.visible; | |
| xe.visible = !xe.visible; | |
| } | |
| //mute or unmute audio | |
| function toggleAudio(){ | |
| document.querySelector('.btn-audio').classList.toggle('active'); | |
| if(CONFIG.mode == MODES.COMPUTER && !CONFIG.paused) toggleSound(SOUNDS.computer); | |
| CONFIG.audio = !CONFIG.audio; | |
| } | |
| //pause the speed | |
| function togglePause(){ | |
| document.querySelector('html').classList.toggle('paused'); | |
| document.querySelector('.btn-pause').classList.toggle('active'); | |
| if(CONFIG.mode == MODES.COMPUTER && CONFIG.audio) toggleSound(SOUNDS.computer); | |
| CONFIG.paused = !CONFIG.paused; | |
| } | |
| //toggle a sound file | |
| function toggleSound(sound){ | |
| if(sound.paused) sound.play(); | |
| else sound.pause(); | |
| } | |
| //move camera arround in the trench on the xy axies | |
| function strafe(){ | |
| var up = KEYS.up.pressed || KEYS.W.pressed; | |
| var down = KEYS.down.pressed || KEYS.S.pressed; | |
| var left = KEYS.left.pressed || KEYS.A.pressed; | |
| var right = KEYS.right.pressed || KEYS.D.pressed; | |
| if(up && camera.position.y < CONFIG.camera_start.y + CONFIG.strafe_max+7){ | |
| camera.position.y += CONFIG.strafe_speed; | |
| } | |
| if(down && camera.position.y > CONFIG.camera_start.y-2){ | |
| camera.position.y -= CONFIG.strafe_speed; | |
| } | |
| if(left && camera.position.x > CONFIG.camera_start.x - CONFIG.strafe_max){ | |
| camera.position.x -= CONFIG.strafe_speed; | |
| } | |
| if(right && camera.position.x < CONFIG.camera_start.x + CONFIG.strafe_max){ | |
| camera.position.x += CONFIG.strafe_speed; | |
| } | |
| } | |
| //turn on debug mode with orbit camera and stats | |
| function enableDebug(){ | |
| CONFIG.debug = KEYS.P.pressed ? !CONFIG.debug : CONFIG.debug; | |
| document.body.appendChild( stats.domElement ); | |
| controls.enabled = true; | |
| } | |
| //register a laser hit on an xwing | |
| function registerHit(){ | |
| CONFIG.xwing_hits++; | |
| //var remaining = CONFIG.xwing_remaining - CONFIG.xwing_hits; | |
| //if(remaining < 1) win(); | |
| if(CONFIG.xwing_hits > 9){ | |
| document.querySelector('.xwing-count.number-first').innerHTML = CONFIG.xwing_hits.toString().charAt(0); | |
| document.querySelector('.xwing-count.number-last').innerHTML = CONFIG.xwing_hits.toString().charAt(1); | |
| } | |
| else{ | |
| document.querySelector('.xwing-count.number-last').innerHTML = CONFIG.xwing_hits; | |
| } | |
| } | |
| //win state all fighters destroyed | |
| function win(){ | |
| reset(); | |
| } | |
| //reset to starting conditions | |
| function reset(){ | |
| //reset it | |
| } | |
| //========================================= | |
| // EVENTS | |
| //========================================= | |
| //pause button click | |
| document.querySelector('.btn-pause').addEventListener('click', function() { | |
| togglePause(); | |
| }); | |
| //computer button click | |
| document.querySelector('.btn-computer').addEventListener('click', function() { | |
| toggleMode(); | |
| }); | |
| //laser button click | |
| document.querySelector('.btn-laser').addEventListener('click', function() { | |
| lasers.fire(); | |
| }); | |
| //audio button click | |
| document.querySelector('.btn-audio').addEventListener('click', function() { | |
| toggleAudio(); | |
| }); | |
| //keydown events | |
| document.onkeydown = function(e){ | |
| //unpause once any key is pressed | |
| if(CONFIG.paused) togglePause(); | |
| //fire laser | |
| if (e.keyCode == KEYS.space.code) { | |
| e.preventDefault(); | |
| lasers.fire(); | |
| } | |
| //enable debug | |
| if (e.keyCode == KEYS.P.code) { | |
| enableDebug(); | |
| } | |
| //toggle computer mode | |
| if (e.keyCode == KEYS.tab.code) { | |
| e.preventDefault(); | |
| toggleMode(); | |
| } | |
| //if the pressed key is in the KEYS obj set its pressed val to true | |
| var matches = _.pick(KEYS, function(val){ return val.code == e.keyCode; }); | |
| if (Object.keys(matches).length){ | |
| KEYS[Object.keys(matches)[0]].pressed = true; | |
| } | |
| }; | |
| //keyup events | |
| document.onkeyup = function(e){ | |
| //if the pressed key is in the KEYS obj set its pressed val to false | |
| var matches = _.pick(KEYS, function(val){ return val.code == e.keyCode; }); | |
| if (Object.keys(matches).length){ | |
| KEYS[Object.keys(matches)[0]].pressed = false; | |
| } | |
| }; | |
| //========================================= | |
| // MAIN | |
| //========================================= | |
| //initial setup | |
| var init = function(){ | |
| setupScene(); | |
| setupLights(); | |
| setupSounds(); | |
| trench = new Trench(); | |
| stars = new Starfield(); | |
| yavin = new Yavin(); | |
| moon = new Moon(); | |
| lasers = new LaserSpawner(); | |
| fighters = new TieFighterSpawner(); | |
| xwing = new Xwing(); | |
| }; | |
| //main animation loop | |
| var render = function() { | |
| requestAnimationFrame(render); | |
| renderer.render(scene, camera); | |
| stats.update(); | |
| controls.update(); | |
| if(!CONFIG.paused){ | |
| strafe(); | |
| updateCounter(); | |
| if(moon) moon.update(); | |
| if(trench) trench.update(); | |
| if(trench) lasers.update(); | |
| if(fighters) fighters.update(); | |
| if(xwing) xwing.update(); | |
| if(beam) beam.update(); | |
| } | |
| }; | |
| //run it | |
| init(); | |
| render(); |
| <script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script> | |
| <script src="//cdnjs.cloudflare.com/ajax/libs/three.js/r70/three.min.js"></script> | |
| <script src="http://threejs.org/examples/js/controls/OrbitControls.js"></script> | |
| <script src="http://threejs.org/examples/js/libs/stats.min.js"></script> |
| $red:#F21313; | |
| $green:#00FF6E; | |
| $yellow:#E0B300; | |
| $button:rgba(white, 0.8); | |
| $button-bg:rgba(white, 0.2); | |
| $button-active:rgba($red, 1); | |
| $button-active-bg:rgba($red, 0.5); | |
| $text:#555; | |
| $pad: 10px; | |
| .hidden{ display:none; } | |
| .left{float:left;} | |
| .right{float:right;} | |
| .center{text-align: center;} | |
| .sudo{ | |
| content:""; | |
| position:absolute; | |
| display:block; | |
| } | |
| *, *:before, *:after{ | |
| outline:none; | |
| box-sizing: border-box; | |
| } | |
| //GRID | |
| //=============================== | |
| .grid:after{ | |
| content: ""; | |
| display: table; | |
| clear: both; | |
| } | |
| [class*='col-'] { | |
| float: left; | |
| padding-right: $pad; | |
| .grid &:last-of-type { | |
| padding-right: 0; | |
| } | |
| } | |
| .col-2-3 { width: 66.66%; } | |
| .col-1-3 { width: 33.33%; } | |
| .col-1-2 { width: 50%; } | |
| .col-1-4 { width: 25%; } | |
| .col-1-8 { width: 12.5%; } | |
| .grid-pad { | |
| padding: $pad 0 $pad $pad; | |
| [class*='col-']:last-of-type { | |
| padding-right: $pad; | |
| } | |
| } | |
| body{ | |
| font-family:helvetica, arial, sans-serif; | |
| background: black; | |
| overflow:hidden; | |
| font-size: 14px; | |
| color:$text; | |
| padding:0px; | |
| margin:0px; | |
| } | |
| //NAV | |
| //=============================== | |
| nav{ | |
| top:0; | |
| left:0; | |
| width:100%; | |
| padding:15px; | |
| position:absolute; | |
| transition:all 0.3s; | |
| background: transparent; | |
| border-bottom-width:3px; | |
| border-bottom-style:solid; | |
| border-bottom-color:transparent; | |
| } | |
| nav ul{ list-style:none; padding:0; margin:0; } | |
| nav ul li {display:inline-block; vertical-align: top; } | |
| .paused nav {background:rgba(black, 0.9); border-bottom-color:#111;} | |
| .controls-right li{ margin-left:15px; } | |
| .controls-left li{ margin-right:15px; } | |
| .banner{ opacity:0; transition:all 0.3s;} | |
| .paused .banner{ opacity:1; } | |
| .banner h1{ margin:0 0 5px 0; font-size: 24px;} | |
| .banner p{ margin:0; } | |
| .banner .control{display: inline-block; padding:0 10px;} | |
| .banner a{ | |
| display: inline-block; | |
| text-decoration: none; | |
| background:$text; | |
| color:black; | |
| padding:8px 15px; | |
| border-radius:4px; | |
| margin:12px 0; | |
| &:hover{background:darken($text, 10%);} | |
| } | |
| //BUTTONS | |
| //=============================== | |
| button{ | |
| width:53px; | |
| height:53px; | |
| display:block; | |
| cursor: pointer; | |
| position:relative; | |
| border-width:3px; | |
| border-radius: 2px; | |
| border-style:solid; | |
| border-color:$button; | |
| background:$button-bg; | |
| transition:all 0.2s; | |
| &:before, &:after, i:after, i:before{ | |
| transition:all 0.2s; | |
| border-color:$button; | |
| background:$button; | |
| } | |
| } | |
| button.active, button:active{ | |
| border-color:$button-active; | |
| background:$button-active-bg; | |
| &:before, &:after, i:after, i:before{ | |
| border-color:$button-active; | |
| background:$button-active; | |
| } | |
| } | |
| //BUTTON ICONS | |
| //=============================== | |
| .btn-pause:before{ | |
| @extend .sudo; | |
| width:10px; | |
| height:25px; | |
| top:11px; | |
| left:26px; | |
| } | |
| .btn-pause:after{ | |
| @extend .sudo; | |
| width:10px; | |
| height:25px; | |
| top:11px; | |
| left:11px; | |
| border:none; | |
| } | |
| .btn-pause.active:before{ | |
| display:none; | |
| } | |
| .btn-pause.active:after{ | |
| @extend .sudo; | |
| width: 0; | |
| height: 0; | |
| top: 9px; | |
| left: 13px; | |
| border-style: solid; | |
| border-width: 15px 0 15px 25px; | |
| background:transparent; | |
| border-color: transparent transparent transparent $button-active; | |
| } | |
| .btn-computer:before{ | |
| @extend .sudo; | |
| height:10px; | |
| width:30px; | |
| left:9px; | |
| top:9px; | |
| } | |
| .btn-computer:after{ | |
| @extend .sudo; | |
| height:10px; | |
| width:30px; | |
| left:9px; | |
| top:30px; | |
| } | |
| .btn-computer i:before{ | |
| @extend .sudo; | |
| height:5px; | |
| width:5px; | |
| left:9px; | |
| top:22px; | |
| } | |
| .btn-computer i:after{ | |
| @extend .sudo; | |
| height:5px; | |
| width:5px; | |
| left:34px; | |
| top:22px; | |
| } | |
| .btn-laser:before{ | |
| @extend .sudo; | |
| width:30px; | |
| height:15px; | |
| bottom:10px; | |
| left:9px; | |
| border-width:1px; | |
| border-radius: 0 0 30px 30px; | |
| } | |
| .btn-laser:after{ | |
| @extend .sudo; | |
| width:0; | |
| height:0; | |
| top: 9px; | |
| left: 17px; | |
| border-style: solid; | |
| border-width: 0 7.5px 10px 7.5px; | |
| background:transparent; | |
| border-color: transparent transparent $button transparent; | |
| } | |
| .btn-laser:active:after{ | |
| background:transparent; | |
| border-color: transparent transparent $button-active transparent; | |
| } | |
| .btn-audio:before{ | |
| @extend .sudo; | |
| width: 10px; | |
| height: 15px; | |
| bottom: 16px; | |
| left: 7px; | |
| border-radius: 10px 0 0 10px; | |
| } | |
| .btn-audio:after{ | |
| @extend .sudo; | |
| width: 0; | |
| height: 35px; | |
| top: 6px; | |
| left: 20px; | |
| border-style: solid; | |
| border-width: 10px 17px 10px 0; | |
| background:transparent; | |
| border-color: transparent $button transparent transparent; | |
| } | |
| .btn-audio.active:after, .btn-audio:active:after{ | |
| background:transparent; | |
| border-color: transparent $button-active transparent transparent !important; | |
| } | |
| //TARGETING COMPUTER | |
| //=============================== | |
| .computer > *{ | |
| position:absolute; | |
| background:black; | |
| padding:15px 25px 6px 25px; | |
| font-size: 24px; | |
| color:red; | |
| bottom:0; | |
| } | |
| .computer .label{ | |
| margin-bottom:10px; | |
| } | |
| .computer .hits{ | |
| left:0; | |
| border-top: 2px solid $yellow; | |
| border-right: 2px solid $yellow; | |
| border-top-right-radius:10px; | |
| > *{display: inline-block; vertical-align: top;} | |
| .x{ margin:0 10px; font-size:20px; margin-top:15px;} | |
| .xwing-count{ margin-top: 3px; position:relative;} | |
| .xwing-count:before{ | |
| @extend .sudo; | |
| background:black; | |
| height:5px; | |
| width:100%; | |
| top:46%; | |
| } | |
| } | |
| .computer .counter{ | |
| right:0; | |
| text-align: center; | |
| border-top: 2px solid $yellow; | |
| border-left: 2px solid $yellow; | |
| border-top-left-radius:10px; | |
| } | |
| .computer .counter:after{ | |
| @extend .sudo; | |
| background: black; | |
| height:5px; | |
| width:100%; | |
| top:50%; | |
| left:0; | |
| } | |
| .computer .number{ | |
| width:35px; | |
| text-align: center; | |
| display:inline-block; | |
| font-weight: bold; | |
| font-family: 'Poiret One', sans-serif; | |
| text-shadow: 0 0 4px rgba(red, 0.6); | |
| font-size:40px; | |
| color:red; | |
| } |