/*
* @title はてなハイク STAR FIGHTERS
* @description はてなハイクで実行すると、星を打ちあう簡単な対戦型シューティングが初まる。カーソルキーで移動、Zで星発射。ESCで終了
* @include http://h.hatena.ne.jp/*
* @license MIT License
* @require
*/
(function(){
var STARS = [
'http://s.hatena.ne.jp/images/star.gif',
'http://s.hatena.ne.jp/images/star-green.gif',
'http://s.hatena.ne.jp/images/star-red.gif',
'http://s.hatena.ne.jp/images/star-blue.gif',
'http://s.hatena.ne.jp/images/star-purple.gif'
];
var CSS_DIALOG = [
'z-index: 131072',
'opacity: 0.9',
'display: table',
'padding: 20px',,
'font-size: 40px',
'font-weight: 900',
'background: #C99',
'color: #FFF',
'-moz-border-radius: 20px',
'-webkit-border-radius: 20px',
'border-radius: 20px',
'position: fixed'
].join(";\n");
var CSS_NAMEPLATE= [
'background: #EEE',
'color: #339',
'font-size: 12px',
'-moz-border-radius: 5px',
'-webkit-border-radius: 5px',
'border-radius: 5px'
].join(";\n");
////////////////////////////////////////////////////////////
if(! 'forEach' in Array.prototype){
Array.prototype.forEach = function(cb){
for(var i=0,l=this.length;i<l;i++){
cb(this[i],i);
}
};
}
if(! 'map' in Array.prototype){
Array.prototype.map = function(cb){
var R = [];
for(var i=0,l=this.length;i<l;i++){
R.push(cb(this[i],i));
}
return R;
};
}
////////////////////////////////////////////////////////////
var defclass = function(spec, initializer){
var base = spec.base;
var klass = function(){};
klass.prototype = base.prototype;
var proto = new klass;
if(!('initialize' in proto)) proto.initialize = function(){};
var R = function(){
this.constructor = arguments.callee;
this.initialize.apply(this,arguments);
};
R.prototype = proto;
R._name = spec.name;
R._base = base;
R.getName = function(){ return this._name; };
R.getBaseClasses = function(){
var R = [this];
if('getBaseClasses' in base){
R.push.apply(R, base.getBaseClasses());
} else {
R.push(base);
}
return R;
};
initializer.apply(
proto,
[
base.prototype,
R
]
);
return R;
};
////////////////////////////////////////////////////////////
var AbstractKeyManager = defclass(
{
name: 'AbstractKeyManager',
base: Object
},
function(SUPER, Class){
this.initialize = function(){
this._hooks = [];
this._lastKey = null;
this._lastEvent = null;
};
this.addHook = function(hook){
this._hooks.push(hook);
};
this.deleteHook = function(hook){
var hooks = this._hooks;
var found = null;
for(var i=0,l=hooks.length;i<l;i++){
if(hook === hooks[i]){
found = i;
break;
}
}
if( found !== null ){
hooks.splice( found, 1 );
}
};
this._runHooks = function(){
var self = this;
this._hooks.forEach(
function(hook){
hook.apply(self, []);
}
);
};
this.pushKey = function(k){
this._lastKey = k;
this._lastEvent = 'push';
};
this.releaseKey = function(k){
this._lastKey = k;
this._lastEvent = 'release';
};
}
);
var ShotKeyManager = defclass(
{
name: 'ShotKeyManager',
base: AbstractKeyManager
},
function(SUPER, Class){
this.releaseKey = function(k){
SUPER.releaseKey.apply(this, [k]);
this._runHooks();
};
}
);
var ArrowKeyManager = defclass(
{
name: 'ArrowKeyManager',
base: AbstractKeyManager
},
function(SUPER, Class){
this.initialize = function(){
SUPER.initialize.apply(this,[]);
this.pressed = {n:0,s:0,w:0,e:0};
};
this.pushKey = function(k){
var pressed = this.pressed;
switch(k){
case 'n':
pressed.n = (pressed.s > 0) ? 2 : 1;
break;
case 's':
pressed.s = (pressed.n > 0) ? 2 : 1;
break;
case 'w':
pressed.w = (pressed.e > 0) ? 2 : 1;
break;
case 'e':
pressed.e = (pressed.w > 0) ? 2 : 1;
break;
}
this._runHooks();
};
this.releaseKey = (
function(){
var timer = null;
return function(k){
var pressed = this.pressed;
switch(k){
case 'n':
pressed.n = 0;
break;
case 's':
pressed.s = 0;
break;
case 'w':
pressed.w = 0;
break;
case 'e':
pressed.e = 0;
}
var self = this;
if(timer){
clearTimeout(timer);
timer = null;
}
timer = setTimeout(
function(){
self._runHooks();
timer = null;
},
50
);
};
}
)();
this.getCurrentDirection = function(){
var pressed = this.pressed;
var x = 0;
var y = 0;
if(pressed.s != pressed.n) y = pressed.s > pressed.n ? 1 : -1;
if(pressed.w != pressed.e) x = pressed.e > pressed.w ? 1 : -1;
return {x:x, y:y};
};
}
);
////////////////////////////////////////////////////////////
var Game = new (
defclass(
{
base: Object,
name: 'Game'
},
function(SUPER, Class){
this.initialize = function(){
this.allElems = {};
this.elemDic = {};
this.timer = null;
this.runnning = false;
this._initializer = null;
var akman = new ArrowKeyManager();
var skman = new ShotKeyManager();
this.listeners = [
[ document,
'keypress',
function(e){ e.preventDefault();},
false ],
[ window,
'keydown', function(e){
e.stopPropagation();
e.preventDefault();
// console.log(e.keyCode);
switch(e.keyCode){
case 37: //LEFT
akman.pushKey('w');
break;
case 38: //UP
akman.pushKey('n');
break;
case 39: //RIGHT
akman.pushKey('e');
break;
case 40: //DOWN
akman.pushKey('s');
break;
case 90:
skman.pushKey(1);
}
}, true ],
[ window,
'keyup', function(e){
e.stopPropagation();
e.preventDefault();
switch(e.keyCode){
case 37: //LEFT
akman.releaseKey('w');
break;
case 38: //UP
akman.releaseKey('n');
break;
case 39: //RIGHT
akman.releaseKey('e');
break;
case 40: //DOWN
akman.releaseKey('s');
break;
case 90: // Z
skman.releaseKey(1);
}
}, true]
];
this.arrowKeyManager = akman;
this.shotKeyManager = skman;
this.initEventListeners();
};
this.setInitializer = function(cb){
this._initializer = cb;
};
this.initEventListeners = function(){
this.listeners.forEach(
function(spec){
spec[0].addEventListener(spec[1],spec[2],spec[3]);
}
);
};
this.removeEventListeners = function(){
this.listeners.forEach(
function(spec){
spec[0].removeEventListener(spec[1],spec[2],spec[3]);
}
);
};
this.getElementsByClass = function(cls){
var elemDic = this.elemDic;
var slot = elemDic[cls._name || ''] || {};
var R = [];
for(var f in slot) R.push(slot[f]);
return R;
};
this.registerElement = function(elem){
var self = this;
var elemDic = this.elemDic;
this.allElems[elem.key] = elem;
elem.constructor.getBaseClasses().forEach(
function(cl){
if(!cl._name) return;
(elemDic[cl._name] || (elemDic[cl._name] = {}))[elem.key] = elem;
}
);
};
this.unregisterElement = function(elem){
delete this.allElems[elem.key];
var elemDic = this.elemDic;
elem.constructor.getBaseClasses().forEach(
function(cl){
if(!(cl._name && elemDic[cl._name])) return;
delete elemDic[cl._name][elem.key];
}
);
};
this.iter = function(){
var allElems = this.allElems;
var elemDic = this.elemDic;
var i,l, ff;
var elem;
for(f in allElems){
allElems[f].step();
}
var checked = {};
for(f in allElems){
elem = allElems[f];
var tclasses = elem.getTargetClasses();
l = tclasses.length;
if(l < 1) continue;
var chslot = (checked[f] || (checked[f] = {}));
for(i=0,l=tclasses.length;i<l;i++){
var edslot = elemDic[tclasses[i]];
for(ff in edslot){
if(chslot[ff]) continue;
chslot[ff] = true;
var target = edslot[ff];
if(!elem.checkHit(target)) continue;
(checked[target.key] ||
(checked[target.key] = {}))[f] = true;
elem.onHit(target);
target.onHit(elem);
}
}
}
for(f in allElems){
allElems[f].redraw();
}
};
this.end = function(){
this.runnnig = false;
this.removeEventListeners();
var allElems = this.allElems;
for(var f in allElems){
allElems[f].dispose();
}
};
this.reset = function(){
var allElems = this.allElems;
for(var f in allElems){
allElems[f].dispose();
}
if(this.timer) clearTimeout(this.timer);
this.start();
};
this.start = function(){
var self = this;
this._initializer();
this.runnning = true;
(function(){
if(!self.runnning) return;
self.iter();
self.timer = setTimeout(arguments.callee, 10);
})();
};
}) // end of defclass
); // end of new
var dialog = function(msg){
var div = document.createElement('div');
div.appendChild(document.createTextNode(msg));
div.style.cssText = CSS_DIALOG;
document.body.appendChild(div);
div.style.left = (( window.innerWidth - div.clientWidth ) / 2 )+'px';
div.style.top = (( window.innerHeight - div.clientHeight ) / 2 )+'px';
return div;
};
var StateLock = false;
var endgame = function(msg){
if(StateLock) return;
StateLock = true;
var div = dialog(msg);
setTimeout(
function(){
div.parentNode.removeChild(div);
Game.end();
var gameover = dialog('GAME OVER');
setTimeout(
function(){
gameover.parentNode.removeChild(gameover);
}, 3000
);
}, 3000);
};
var nextgame = function(msg){
if(StateLock) return;
StateLock = true;
var div = dialog(msg);
setTimeout(
function(){
div.parentNode.removeChild(div);
StateLock = false;
Game.reset();
}, 3000);
};
////////////////////////////////////////////////////////////
var genImage = function(url , container){
var img = document.createElement('img');
img.src = url;
return img;
};
var GameElement = defclass(
{
base: Object,
name: 'GameElement'
},
function(SUPER, Class){
var counter = 0;
this.initialize = function(x,y){
this.key = 'elem_'+ counter++;
this._x = x;
this._y = y;
this._pre_x = NaN;
this._pre_y = NaN;
this._speed = 1;
this._dx = 0;
this._dy = 0;
this._pdx = 0;
this._pdy = -1;
this._elem = this.makeElem();
this.initElem(this._elem);
Game.registerElement(this);
this.redraw();
};
this.makeElem = function(){
var div = document.createElement('div');
div.style.border = 'solid black 2px';
return div;
};
this.initElem = function(elem){
document.body.appendChild(elem);
elem.style.zIndex = 65536;
elem.style.position = 'fixed';
};
this.setSpeed = function(s){
this._speed = s;
};
this.setPosition = function(x,y){
this._x = x;
this._y = y;
};
this.getPosition = function(){
return {
x: this._x ,
y: this._y
};
};
this.getPreviousPosition = function(){
return {
x: this._pre_x,
y: this._pre_y
};
};
this.getSize = function(){
var elem = this._elem;
return {
width: elem.clientWidth,
height: elem.clientHeight
};
};
this.getHitAreaSize = function(){
return this.getSize();
};
this.getSpeed = function(){
return this._speed;
};
this.setDirection = function(x, y){
this._dx = x;
this._dy = y;
};
this.getDirection = function(){
return { x:this._dx, y:this._dy };
};
this.setShotDirection = function(x,y){
this._pdx = x;
this._pdy = y;
};
this.getShotDirection = function(){
return { x:this._pdx, y:this._pdy };
};
this.step = function(){
this._pre_x = this._x;
this._pre_y = this._y;
this._x = this._x + this._dx * this._speed;
this._y = this._y + this._dy * this._speed;
};
//
this.dispose = function(){
this.disposeElements();
Game.unregisterElement(this);
for(var f in this){
if('function' == typeof this[f]){
this[f] = function(){};
}
}
};
this.disposeElements = function(){
var img = this._elem;
if(img.parentNode) img.parentNode.removeChild(img);
};
// フレームごとの計算結果を元に
this.redraw = function(){
if(this._dispose){
this.dispose();
return;
}
var sx = this._x;
var sy = this._y;
var elem = this._elem;
elem.style.top = (sy - elem.clientHeight / 2) + 'px';
elem.style.left = (sx - elem.clientWidth / 2) + 'px';
};
// 当たり判定の対象となるクラス群
this.getTargetClasses = function(){
return [];
};
this.getHitRectangle = function(){
var size = this.getHitAreaSize();
var w = size.width;
var h = size.height;
var sx = this._x;
var sy = this._y;
var x0 = sx - w / 2;
var y0 = sy - h / 2;
return {
x1: x0,
y1: y0,
x2: x0 + w,
y2: y0 + h
};
};
// elem との当り判定を書く
// TODO すりぬけない方法について学習する
this.checkHit = function(elem){
var sr = this.getHitRectangle();
var er = elem.getHitRectangle();
var x0 = sr.x1;
var x1 = sr.x2;
var x2 = er.x1;
var x3 = er.x2;
var y0 = sr.y1;
var y1 = sr.y2;
var y2 = er.y1;
var y3 = er.y2;
return (x0 < x3 && x2 < x1 && y0 < y3 && y2 < y1);
};
// elem 衝突時の振舞い
this.onHit = function(elem){
};
this.onScreen = function(){
var sx = this._x;
var sy = this._y;
return (sx >= 0 && sx < window.innerWidth &&
sy >= 0 && sy < window.innerHeight);
};
this.backToScreen = function(){
var sx = this._x;
var sy = this._y;
if(sx < 0){
this._x = 0;
} else if (sx >= window.innerWidth){
this._x = window.innerWidth - 1;
}
if(sy < 0){
this._y = 0;
} else if (sy >= window.innerHeight){
this._y = window.innerHeight - 1;
}
};
this.pacmanWarp = function(){
var sx = this._x;
var sy = this._y;
};
}
);
var Explode = defclass(
{
base: GameElement,
name: 'Explode'
},
function(SUPER, Class){
this.initialize = function(x, y){
SUPER.initialize.apply(this, [x, y]);
this._frame = 0;
this._stars = null;
};
this.makeElem = function(){
var div = document.createElement('div');
div.style.width = '100px';
div.style.height = '100px';
var stars = [];
this.stars = stars;
for(var i =0; i < 16; i++){
var img = genImage(STARS[i % STARS.length]);
img.style.position = 'absolute';
img.width = 24;
img.height = 24;
stars.push(img);
div.appendChild( img );
}
return div;
};
this.step = function(){
this._frame++;
if(this._frame > 40){
this.dispose();
}
};
this.redraw = function(){
SUPER.redraw.apply(this, []);
var imgs = this.stars;
var frame = this._frame;
var sx = this._x;
var sy = this._y;
for(var i = 0;i<16;i++){
var rad = i / 16 * 2 * Math.PI;
var x = (Math.cos(rad)) * frame;
var y = (Math.sin(rad)) * frame;;
imgs[i].style.top = (50 + y) + 'px';
imgs[i].style.left = (50 + x) + 'px';
}
};
}
);
////////////////////////////////////////////////////////////
var Bullet = defclass(
{
base: GameElement,
name: 'Bullet'
},
function(SUPER, Class){
this.initialize = function(x,y){
SUPER.initialize.apply(this,[x, y]);
this.setDirection(0,-1);
this.setSpeed(10);
};
this.step = function(){
SUPER.step.apply(this, []);
if(!this.onScreen()) this._dispose = true;
};
}
);
var EBullet = defclass(
{
base: Bullet,
name: 'EBullet'
},
function(SUPER, Class){
this.makeElem = function(){
return genImage('http://s.hatena.ne.jp/images/star.gif');
};
}
);
var PBullet = defclass(
{
base: Bullet,
name: 'PBullet'
},
function(SUPER, Class){
this.makeElem = function(){
return genImage('http://s.hatena.ne.jp/images/star-green.gif');
};
}
);
////////////////////////////////////////////////////////////
var Rock = defclass(
{
base: GameElement,
name: 'Rock'
},
function(SUPER, Class){
this.makeElem = function(){
var img = genImage('http://h.hatena.ne.jp/favicon.ico');
img.width = 32;
img.height = 32;
return img;
};
this.getTargetClasses = function(){
return ['Bullet'];
};
this.onHit = function(elem){
if(elem instanceof Bullet){
var dir = elem.getDirection();
var dx = dir.x;
var dy = dir.y;
var pp = elem.getPreviousPosition();
var sp = this.getPosition();
var w = sp.x - pp.x;
var h = sp.y - pp.y;
var r = w == 0 ? (h < 0 ? -Infinity : Infinity) : h / w;
if ((w > 0 && r > 1) || (w < 0 && r < -1)){// from north
dy = - dy;
} else if(w > 0 && r < 1 && r >= -1){ // from west
dx = -dx;
} else if((w > 0 && r < -1) || (w < 0 && r >= 1)){// from sourh
dy = -dy;
} else { // from east
dx = -dx;
}
elem.setDirection(dx, dy);
}
};
}
);
////////////////////////////////////////////////////////////
var Motion;
var RandomMotion;
var User = defclass(
{
base: GameElement,
name: 'User'
}, function(SUPER,Class){
this.initialize = function(name){
this.name = name;
SUPER.initialize.apply(this,[0,0]);
this._defaultSpeed = 5;
this.setDirection(0, -1);
this.setSpeed(0);
};
this.makeElem = function(){
var name = this.name;
var div = document.createElement('div');
var img = genImage(
'http://www.st-hatena.com/users/' + name.substring(0,2) +
'/' + name + '/profile.gif'
);
div.appendChild(img);
var label = document.createElement('div');
div.appendChild(label);
label.innerHTML = 'id:' + name;
label.style.cssText = CSS_NAMEPLATE;
return div;
};
this.getHitAreaSize = function(){
var size = this.getSize();
return {
width: Math.floor(size.width / 3),
height: Math.floor(size.height / 3)
};
};
this.step = function(){
SUPER.step.apply(this, []);
if(!this.onScreen()){
this.backToScreen();
}
};
this.explode = function(){
new Explode(this._x, this._y);
this.dispose();
};
}
);
var Enemy = defclass(
{
base: User,
name: 'Enemy'
},
function(SUPER, Class){
this.initialize = function(name){
SUPER.initialize.apply(this,[name]);
var img = this._elem;
this._x = Math.floor((window.innerWidth - img.clientWidth ) / 2
);
this._y = img.clientHeight;
this._motion = new RandomMotion( this );
};
this.setMotion = function(motion){
this._motion = motion;
};
this.step = function(){
this._motion.step();
SUPER.step.apply(this, []);
};
this.getTargetClasses = function(){
return ['PBullet'];
};
this.onHit = function(elem){
if(elem instanceof PBullet){
this.explode();
nextgame('You Win!!');
}
};
}
);
var Player = defclass(
{
base: User,
name: 'Player'
}, function(SUPER, Class){
this.initialize = function(name){
SUPER.initialize.apply(this,[name]);
var self = this;
var img = this._elem;
this._arrowHook = function(){
var dir = this.getCurrentDirection();
self.setDirection(dir.x, dir.y);
};
Game.arrowKeyManager.addHook( this._arrowHook );
this._shotHook = function(){ self.shot(); };
Game.shotKeyManager.addHook( this._shotHook );
this._x =
Math.floor((window.innerWidth - img.clientWidth ) / 2
);
this._y =
(window.innerHeight - img.clientHeight - 10);
};
this.dispose = function(){
Game.arrowKeyManager.deleteHook( this._arrowHook );
Game.shotKeyManager.deleteHook( this._shothook );
SUPER.dispose.apply(this, []);
};
this.getTargetClasses = function(){
return ['Enemy', 'EBullet', 'Rock'];
};
this.onHit = function(elem){
if(elem instanceof Enemy){
this.explode();
elem.explode();
endgame('Draw');
}
if(elem instanceof EBullet){
this.explode();
endgame('You Lose');
}
};
this.shot = function(){
var pos = this.getPosition();
var bullet = new PBullet(pos.x, pos.y);
var dir = this.getDirection();
bullet.setDirection(dir.x, dir.y);
bullet.setSpeed(this._defaultSpeed + 2);
};
this.setDirection = function(x, y){
if(x == 0 && y == 0){
this.setSpeed(0);
} else {
this.setSpeed(this._defaultSpeed);
SUPER.setDirection.apply(this, [x,y]);
}
};
}
);
////////////////////////////////////////////////////////////
Motion = defclass(
{
base: Object,
name: 'Motion'
},
function(SUPER, Class){
this.initialize = function(elem){
this.elem = elem;
};
this.step = function(){
};
}
);
RandomMotion = defclass(
{
base: Motion,
name: 'RandomMotion'
},
function(SUPER, Class){
this.initialize = function(elem){
SUPER.initialize.apply(this, [elem]);
this._turn_at = null;
this._iter = 0;
this._changeDir();
};
this.getPlayersDirection = function(){
var elem = this.elem;
var player = (Game.getElementsByClass(Player) || [])[0] || null;
if(!player) return this.getRandomDirection();
var dx = player._x - elem._x;
var dy = player._y - elem._y;
var ddx, ddy;
if(Math.abs(dx) > Math.abs(dy)){
ddx = dx < 0 ? -1 : 1;
if(dy == 0){
ddy = 0;
} else {
ddy = dy / Math.abs(dx);
}
}else{
ddy = dy < 0 ? -1 : 1;
if(dx == 0){
ddx = 0;
} else{
ddx = dx / Math.abs(dy);
}
}
return {x:ddx, y:ddy};
};
this.getRandomDirection = function(){
return { x: Math.random() * 2 - 1,
y: Math.random() * 2 - 1 };
};
this._changeDir = function(){
this._turn_at = 50;
var elem = this.elem;
var dir = ((this._iter++ > 3 &&
Math.random() * 3 < 1)
? this.getPlayersDirection()
: this.getRandomDirection());
elem.setDirection(dir.x, dir.y);
elem.setSpeed(Math.random() * 10);
};
this.step = function(){
if(this._iter > 10){
switch(this._turn_at){
case 40:
this.shot(this.getPlayersDirection(), 10);
break;
case 30:
this.shot(this.getRandomDirection(), 10);
break;
case 20:
this.shot(this.getPlayersDirection(), 10);
break;
case 10:
this.shot(this.getRandomDirection(), 10);
break;
case 0:
this.shot(this.elem.getDirection(), this.elem.getSpeed() + 10);
break;
}
} else {
switch(this._turn_at){
case 40:
case 30:
case 20:
case 10:
case 0:
this.shot(this.elem.getDirection(), this.elem.getSpeed() + 10);
break;
}
}
if(this._turn_at-- < 0){
this._changeDir();
}
};
this.shot = function(dir, speed){
var pos = this.elem.getPosition();
var bullet = new EBullet(pos.x, pos.y);
bullet.setDirection(dir.x, dir.y);
bullet.setSpeed(speed);
};
}
);
////////////////////////////////////////////////////////////
var newgame = function(){
var name = document.evaluate(
'.//p[@class="username"]/a/text()',
document.body,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null).snapshotItem(0).data;
var enemies = document.evaluate(
'.//*[@id="leftbar"]//img[@class="profile-image"]/@alt',
document.body,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null
);
var enemy_id = enemies.snapshotItem(
Math.floor(
enemies.snapshotLength * Math.random()
)
).value;
for(var i=0; i < 10; i++){
var x = 30 + Math.random() * (window.innerWidth - 60);
var y = 30 + Math.random() * (window.innerHeight - 60);
new Rock(x,y);
}
new Player(name);
new Enemy(enemy_id);
};
Game.setInitializer(newgame);
Game.start();
})();