topical media & game development
lib-js-terminal-termlib-invaders.js / js
/*
termlib-invaders
a termlib.js application
(c) mass:werk (N. Landsteiner) 2008
based on JS/UIX invaders (c) mass:werk (N. Landsteiner) 2005
all rights reserved
v1.1
termlib-invaders is a simple text-mode invaders game.
requires termlib.js 1.4 or better.
requires a terminal set to at least 68 cols x 20 rows.
uses namespace 'invaders' in object 'env' of the termlib.js Terminal instance.
the entire code is contained inside the single global object 'TermlibInvaders'.
(the massive 'apply'-syntax is a bit awkward, but this is, which does it.)
example call from inside a termlib.js Terminal instance's handler:
if ( TermlibInvaders.start(this) ) {
return;
}
else {
// oops, terminal doesn't meet the requirements
this.write('Sorry, invaders failed.');
}
call with a max game screen of 80 cols x 25 rows:
TermlibInvaders.start(this, 80, 25);
*/
TermlibInvaders = {
version: '1.1 (original)',
start: function( termref, maxcols, maxrows ) {
if (!Terminal || !termref || parseFloat(termref.version)<1.4) {
// color support required
return false;
}
if (termref.conf.cols<68 || termref.conf.rows<20) {
// required min. dimensions: 68 x 20
return false;
}
var gc=TermlibInvaders.getStyleColorFromHexString;
termref.env.invaders= {
termref: termref,
maxCols: maxcols || 0,
maxRows: maxrows || 0,
charMode: termref.charMode,
paused: false,
moveAll: true,
// setup values
rows: 3,
cols: 5,
maxBombs: 3,
bombRate: 0.005,
timer: null,
delay: 50,
newWaveDelay: 1500,
textColor: gc(TermlibInvaders.textColor),
invaderColor: gc(TermlibInvaders.invaderColor),
invaderHitColor: gc(TermlibInvaders.invaderHitColor),
bombColor: gc(TermlibInvaders.bombColor),
blockColor: gc(TermlibInvaders.blockColor),
statusColor: gc(TermlibInvaders.statusColor),
shotColor: gc(TermlibInvaders.shotColor),
shipColor: gc(TermlibInvaders.shipColor),
shipHitColor: gc(TermlibInvaders.shipHitColor),
alertColor: gc(TermlibInvaders.alertColor),
frameColor: gc(TermlibInvaders.frameColor)
};
TermlibInvaders.init.apply(termref);
return true;
},
// color definitions (colors will match nearest webcolor)
textColor: '#00cc00',
invaderColor: '#00cc00',
invaderHitColor: '#66aa66',
bombColor: '#cccc00',
blockColor: '#bbbb00',
statusColor: '#00bb00',
shotColor: '#aacc00',
shipColor: '#aacc00',
shipHitColor: '#aaaaaa',
alertColor: '#ff9900',
// frame definitions
// the frame is only drawn, if the terminal is bigger
// than the game's max dimensions. if you do not want
// to draw any frames leave 'frameChar' empty ('').
frameChar: '*',
frameColor: '#777777',
// global assets
sprites: [
' ',' (^o^) ',' (^-^) ',' ( ) ',' ( )',
' (=^=) ',' ((.)) ',' ( . ) ','( (.) )',
' ( . ) ','( . )',' . ',' '
],
splashScreen: [
'\%c(#0c0)%+i** T E R M L I B - I N V A D E R S **%-i',
'',
'',
'\%c(#0c0)Instructions:',
'',
'\%c(#0c0) use cursor LEFT and RIGHT to move',
'\%c(#0c0) (or use vi movements alternatively)',
'\%c(#0c0) press space to fire',
'\%c(#0c0)',
'\%c(#0c0) press "q" or "esc" to quit,',
'\%c(#0c0) "p" to pause the game.',
'',
'',
'\%c(#0c0)%+r press any key to start the game %-r',
'',
'',
'\%c(#0c0)(c) mass:werk N.Landsteiner 2005-2008',
'\%c(#0c0)based on JS/UIX-Invaders by mass:werk'
],
splashScreenWidth: 40, // width of splash-screen in chars
gameOverScreen: [
' ',
'\%c(#f90) G A M E O V E R ! ',
' ',
'\%c(#0c0) press any key to restart,',
'\%c(#0c0) "q" or "esc" for quit. ',
' '
],
gameOverScreenWidth: 26,
invObject: function(y,x) {
this.x=x;
this.y=y;
this.status=1;
},
// handlers:
// 'this' refers to ther termlib.js Terminal instcance
// 'inv' refers to the TermlibInvaders instance
init: function() {
var inv=this.env.invaders;
// back up the terminal state
inv.termHandler=this.handler;
if (this.maxLines != this.conf.rows) {
inv.charBuf=new Array();
inv.styleBuf=new Array();
for (var r=this.conf.rows-1; r>=this.maxLines; r--) {
var cb=new Array();
var sb=new Array();
var tcb=this.charBuf[r];
var tsb=this.styleBuf[r];
for (var c=0; c=this.conf.cols; c++) {
cb[c]=tcb[c];
sb[c]=tsb[c];
}
inv.charBuf.push(cb);
inv.styleBuf.push(sb);
}
this.maxLines = this.conf.rows;
}
if (this.maxCols!=this.conf.cols) {
inv.termMaxCols=this.maxCols;
this.maxCols=this.conf.cols;
}
else {
inv.termMaxCols=-1;
}
inv.keyRepeatDelay1=this.keyRepeatDelay1;
inv.keyRepeatDelay2=this.keyRepeatDelay2;
this.keyRepeatDelay1=this.keyRepeatDelay2=inv.delay-1;
// output init-screen
this.clear();
TermlibInvaders.writeToCenter.apply(this, [TermlibInvaders.splashScreen, TermlibInvaders.splashScreenWidth]);
this.charMode=true;
this.lock=false;
this.handler=TermlibInvaders.splashScreenHandler;
},
splashScreenHandler: function() {
var key = this.inputChar;
if (key==this.termKey.ESC || key==113) {
TermlibInvaders.exit.apply(this);
return;
}
// setup the game
var inv=this.env.invaders;
TermlibInvaders.buildScreen.apply(this);
inv.maxRight=inv.width-7;
inv.wave=0;
inv.score=0;
var d=Math.floor(inv.width/5);
var d1=Math.floor((inv.width-3*d)/2);
inv.blockpos=new Array();
for (var i=0; i<4; i++) {
var x=d1+i*d;
inv.blockpos.push(x-1);
inv.blockpos.push(x);
inv.blockpos.push(x+1);
}
TermlibInvaders.newWave.apply(this);
},
newWave: function() {
this.clear();
var inv=this.env.invaders;
inv.wave++;
var s='W A V E # '+inv.wave;
var c=Math.floor((this.conf.cols-s.length)/2);
var r=Math.floor((this.conf.rows-3)/2)-4;
this.typeAt(r, c, s, 4 | inv.textColor);
this.typeAt(r+2, c, 'Get ready ...', inv.textColor);
inv.timer=setTimeout(function() { TermlibInvaders.waveStart.apply(inv.termref); }, inv.newWaveDelay);
this.lock=true;
},
waveStart: function() {
var inv=this.env.invaders;
clearTimeout(inv.timer);
this.clear();
TermlibInvaders.drawFrame.apply(this);
inv.smove=0;
inv.phase=1;
inv.dir=1;
inv.population=0;
inv.shot= inv.shotX= 0
inv.over=false;
inv.bombs=0;
inv.invrows=(inv.wave==2)? inv.rows+1:inv.rows;
inv.invcols=(inv.wave<=2)? inv.cols:inv.cols+1;
var changed=inv.changed=new Array();
inv.inv=new Array();
for (var r=0; r<inv.invrows; r++) {
var ir=inv.inv[r]=new Array();
for (var c=0; c<inv.invcols; c++) {
ir[c]=new TermlibInvaders.invObject(r*2+1,c*8);
inv.population++;
}
}
inv.block=this.getRowArray(inv.width, false);
for (var i=0; i<inv.blockpos.length; i++) {
var x=inv.blockpos[i];
inv.block[x]=true;
TermlibInvaders.drawSprite(this, inv.blockY, x, 'H', inv.blockColor);
}
inv.bomb=new Array();
inv.shipX=inv.shipCenter;
TermlibInvaders.drawScoreBG.apply(this);
TermlibInvaders.displayScore.apply(this);
TermlibInvaders.drawSprite(this, inv.shipY, inv.shipX, TermlibInvaders.sprites[5], inv.shipColor);
for (var i=0; i<this.maxLines; i++) {
this.redraw(i);
changed[i]=false;
}
inv.moveAll=true;
TermlibInvaders.invStep(inv);
inv.timer=setTimeout(function() { TermlibInvaders.mainLoop.apply(inv.termref); }, inv.delay);
this.lock=false;
this.handler=TermlibInvaders.keyHandler;
},
mainLoop: function() {
var inv=this.env.invaders;
clearTimeout(inv.timer);
var now=new Date();
var enterTime=now.getTime();
if (inv.smove) {
inv.shipX+=inv.smove;
inv.smove=0;
TermlibInvaders.drawSprite(this, inv.shipY, inv.shipX, TermlibInvaders.sprites[5], inv.shipColor);
}
var s=inv.sore;
TermlibInvaders.invStep(inv);
var changed=inv.changed;
for (var i=0; i<this.maxLines; i++) {
if (changed[i]) {
this.redraw(i);
changed[i]=false;
}
}
inv.moveAll=!inv.moveAll;
if (s!=inv.score) TermlibInvaders.displayScore.apply(this);
if (inv.population==0) {
this.lock=true;
inv.phase=-1;
inv.timer=setTimeout(function() { TermlibInvaders.waveEnd.apply(inv.termref); }, inv.delay*2);
}
else if (inv.invbottom==inv.shipY || inv.over) {
this.lock=true;
inv.phase=(inv.over)? 6:5;
TermlibInvaders.gameOver.apply(this);
}
else {
now=new Date();
var delay=Math.max(1, inv.delay-(now.getTime()-enterTime));
inv.timer=setTimeout(function() { TermlibInvaders.mainLoop.apply(inv.termref); }, delay);
}
},
invStep: function(inv) {
var term=inv.termref;
var br=0, bl=inv.right, bb=0, dir=inv.dir;
var linestep= ((inv.invleft==0) || (inv.invright==inv.maxRight));
var shot= (inv.shot>0), shotx=inv.shotX, shoty=inv.shipY-inv.shot;
var bomb= inv.bomb,block=inv.block, blocky=inv.blockY, isblockrow=false;
var sprites=TermlibInvaders.sprites, invclr=inv.invaderColor;
var moveAll=inv.moveAll;
if (shot && inv.shot>1) TermlibInvaders.drawSprite(term, shoty+1,shotx,' ',0);
for (var r=0; r<inv.invrows; r++) {
var ir=inv.inv[r];
for (var c=0; c<inv.invcols; c++) {
var i=ir[c];
if (i.status==1) {
if (moveAll && linestep) {
TermlibInvaders.drawSprite(term, i.y,i.x, sprites[0], invclr);
i.y++;
}
if (shot && shoty==i.y && shotx>i.x && shotx<(i.x+6)) {
i.status=2;
inv.population--;
inv.score+=50;
inv.shot=shot=0;
TermlibInvaders.drawSprite(term, i.y,i.x, sprites[3], inv.invaderHitColor);
}
else if (moveAll) {
TermlibInvaders.drawSprite(term, i.y,i.x, sprites[inv.phase], invclr );
if (i.y<inv.bombMaxY && inv.bombs<inv.maxBombs && Math.random()<inv.bombRate) {
for (var n=0; n<inv.maxBombs; n++) {
if (bomb[n]==null) {
bomb[n]=new TermlibInvaders.invObject(i.y+1,i.x+3);
inv.bombs++;
break;
}
}
}
if (i.y==blocky) isblockrow=true;
bb=Math.max(i.y,bb);
}
else {
i.x+=dir;
br=Math.max(i.x,br);
bl=Math.min(i.x,bl);
}
}
else if (i.status==2) {
TermlibInvaders.drawSprite(term, i.y,i.x, sprites[4], inv.invaderHitColor);
i.status=3
}
else if (i.status==3) {
TermlibInvaders.drawSprite(term, i.y,i.x, sprites[0], invclr);
i.status=0;
}
}
}
for (var n=0; n<inv.maxBombs; n++) {
var b=bomb[n];
if (b!=null) {
var _br=inv.top+b.y-1;
var _bc=inv.left+b.x;
if (term.charBuf [br] [bc]==86) TermlibInvaders.drawSprite(term, b.y-1,b.x, ' ', 0);
if (b.y==blocky && block[b.x]) {
block[b.x]=false;
TermlibInvaders.drawSprite(term, blocky,b.x, ' ', 0);
b=bomb[n]=null;
inv.bombs--;
}
else if (b.y==inv.shipY) {
if (b.x>inv.shipX && b.x<(inv.shipX+6)) {
inv.over=true;
}
else {
b=bomb[n]=null;
inv.bombs--;
}
}
else if (shot) {
if ((b.y==shoty || b.y==shoty+1) && Math.abs(b.x-shotx)<2) {
b=bomb[n]=null;
inv.bombs--;
inv.score+=5;
inv.shot=shot=0
}
}
if (b) {
TermlibInvaders.drawSprite(term, b.y,b.x, 'V', inv.bombColor);
b.y++;
}
}
}
if (shot) {
if (shoty>0) {
if (shoty==blocky && inv.block[shotx]) {
inv.block[shotx]=false;
TermlibInvaders.drawSprite(term, blocky,shotx, ' ', 0);
inv.shot=0;
}
else {
TermlibInvaders.drawSprite(term, shoty,shotx, '|', inv.shotColor);
inv.shot++;
}
}
else {
inv.shot=0;
}
}
if (moveAll) {
inv.invbottom=bb;
}
else {
inv.invleft=bl;
inv.invright=br;
if (dir==-1 && bl==0) {
inv.dir=1;
}
else if (dir==1 && br==inv.maxRight) {
inv.dir=-1;
}
inv.phase=(inv.phase==1)? 2:1;
}
// restore any overwritten blocks
if (isblockrow) {
var blockpos=inv.blockpos;
for (var i=0; i<inv.blockpos.length; i++) {
var x=blockpos[i];
if (block[x] && term.charBuf[inv.top+blocky][inv.left+x]<=32) {
TermlibInvaders.drawSprite(term, blocky,x, 'H', inv.blockColor);
}
}
}
},
waveEnd: function() {
var inv=this.env.invaders;
clearTimeout(inv.timer);
var drawblocks=false;
var r;
if (inv.phase==0) {
this.clear();
TermlibInvaders.drawFrame.apply(this);
TermlibInvaders.drawScoreBG.apply(this);
TermlibInvaders.displayScore.apply(this);
if (inv.width+1<this.maxCols || inv.height+1<this.maxLines) {
for (r=0; r<this.maxLines; r++) this.redraw(r);
}
drawblocks=true;
}
else {
r=inv.shipY-inv.phase;
TermlibInvaders.drawSprite(this, r, inv.shipX, TermlibInvaders.sprites[0], inv.shipColor);
this.redraw(inv.top+r);
if (inv.shipY-inv.phase==inv.blockY) drawblocks=true;
}
if (inv.phase==inv.shipY) {
inv.timer=setTimeout(function() { TermlibInvaders.newWave.apply(inv.termref); }, inv.delay);
}
else {
inv.phase++;
r=inv.shipY-inv.phase;
TermlibInvaders.drawSprite(this, r, inv.shipX, TermlibInvaders.sprites[5], inv.shipColor);
this.redraw(inv.top+r);
if (r==inv.blockY) drawblocks=true;
if (drawblocks) {
var block=inv.block;
var blockpos=inv.blockpos;
r=inv.blockY;
for (var i=0; i<inv.blockpos.length; i++) {
var x=blockpos[i];
if (block[x] && term.charBuf[inv.top+r][inv.left+x]<=32) {
TermlibInvaders.drawSprite(term, r,x, 'H', inv.blockColor);
}
}
this.redraw(inv.top+r)
}
inv.timer=setTimeout(function() { TermlibInvaders.waveEnd.apply(inv.termref); }, Math.max(10, inv.delay*2-inv.phase*2));
}
},
gameOver: function() {
var inv=this.env.invaders;
clearTimeout(inv.timer);
if (inv.phase>=TermlibInvaders.sprites.length) {
TermlibInvaders.writeToCenter.apply(this, [TermlibInvaders.gameOverScreen, TermlibInvaders.gameOverScreenWidth]);
this.lock=false;
this.handler=TermlibInvaders.splashScreenHandler;
}
else {
TermlibInvaders.drawSprite(this, inv.shipY,inv.shipX, TermlibInvaders.sprites[inv.phase++], inv.shipHitColor);
this.redraw(inv.top+inv.shipY);
inv.timer=setTimeout(function() { TermlibInvaders.gameOver.apply(inv.termref); }, inv.delay*3);
}
},
keyHandler: function() {
var inv=this.env.invaders;
var key=this.inputChar;
if (key==this.termKey.ESC || key==113) {
// esc or q
TermlibInvaders.exit.apply(this);
}
else if (key==112 || inv.paused) {
// p or paused
TermlibInvaders.pause.apply(this);
}
// cursor movements
else if (key==this.termKey.LEFT || key==104) {
// left
if (inv.shipX>0) inv.smove=-1;
return;
}
else if (key==this.termKey.RIGHT || key==108) {
// right
if (inv.shipX<inv.maxRight) inv.smove=1;
return;
}
else if (key==32) {
// space
if (inv.shot==0) {
inv.shot=1;
inv.shotX=inv.shipX+3;
}
}
},
pause: function() {
var inv=this.env.invaders;
clearTimeout(inv.timer);
inv.paused=!inv.paused;
var text=(inv.paused)? ' *** P A U S E D *** ' :' ';
this.typeAt(Math.floor(this.maxLines/2)-2, Math.floor((this.maxCols-text.length)/2), text, inv.alertColor);
if (!inv.paused) TermlibInvaders.mainLoop.apply(this);
},
drawSprite: function(termref, r,c,t,s) {
var inv=termref.env.invaders;
r+=inv.top;
c+=inv.left;
var cb=termref.charBuf[r];
var sb=termref.styleBuf[r];
for (var i=0; i<t.length; i++, c++) {
cb[c]=t.charCodeAt(i);
sb[c]=s;
}
inv.changed[r]=true;
},
drawScoreBG: function() {
var inv=this.env.invaders;
var srs=this.styleBuf[inv.statusRow];
var src=this.charBuf[inv.statusRow];
var clr=inv.statusColor | 1;
for (var c=inv.left; c<inv.right; c++) {
srs[c]=clr;
src[c]=0;
}
},
displayScore: function() {
var inv=this.env.invaders;
var text='Invaders | "q","esc": quit "p": pause | Wave: '+inv.wave+' Score: '+inv.score;
var x=inv.left+Math.floor((inv.width-text.length)/2);
var b=this.charBuf[inv.statusRow];
for (var i=0; i<text.length; i++) b[x+i]=text.charCodeAt(i);
this.redraw(inv.statusRow);
},
writeToCenter: function(buffer, bufferWidth) {
var sx = Math.max(0, Math.floor((this.maxCols-bufferWidth)/2));
var sy = Math.max(0, Math.floor((this.maxLines-buffer.length)/2));
for (var i=0; i<buffer.length; i++) {
this.cursorSet(sy+i, sx);
this.write(buffer[i]);
}
},
buildScreen: function() {
// (re)build a screen on max dimensions
this.clear();
var inv=this.env.invaders;
if (inv.maxCols>0 && this.maxCols>inv.maxCols) {
inv.width = inv.maxCols;
inv.left= Math.floor((this.maxCols-inv.maxCols)/2);
inv.right= inv.left+inv.width;
}
else {
inv.width= inv.right= this.maxCols;
inv.left=0;
}
if (inv.maxRows>0 && this.maxLines>inv.maxRows) {
inv.height = inv.maxRows;
inv.top= Math.floor((this.maxLines-inv.maxRows)/2);
inv.bottom=inv.top+inv.height;
}
else {
inv.height= inv.bottom= this.maxLines;
inv.top=0;
}
inv.shipCenter=Math.floor((inv.width-3)/2);
inv.statusRow=inv.bottom-1;
inv.maxRight=inv.width-7;
inv.shipY=inv.height-3;
inv.bombMaxY=inv.height-7;
inv.blockY=inv.height-5;
},
drawFrame: function() {
var inv=this.env.invaders;
if (TermlibInvaders.frameChar) {
var r0, r1, i;
var c = TermlibInvaders.frameChar.charCodeAt(0);
var cc= inv.frameColor;
if (inv.height+1<this.maxLines) {
r0=Math.max(inv.left-1, 0);
r1=Math.min(inv.right+1, this.maxCols);
var cb1=this.charBuf[inv.top-1];
var sb1=this.styleBuf[inv.top-1];
var cb2=this.charBuf[inv.bottom];
var sb2=this.styleBuf[inv.bottom];
for (i=r0; i<r1; i++) {
cb1[i]=cb2[i]=c;
sb1[i]=sb2[i]=cc;
}
}
if (inv.width+1<this.maxCols) {
r0=Math.max(inv.top-1, 0);
r1=Math.min(inv.bottom+1, this.maxLines);
var p1=inv.left-1;
var p2=inv.right;
for (i=r0; i<r1; i++) {
var b=this.charBuf[i];
b[p1]=b[p2]=c;
b=this.styleBuf[i];
b[p1]=b[p2]=cc;
}
}
}
},
exit: function() {
this.clear();
var inv=this.env.invaders;
// reset the terminal
this.handler=inv.termHandler;
if (inv.charBuf) {
for (var r=0; r<inv.charBuff.length; r++) {
var tr=this.maxLines-1;
this.charBuf[tr]=inv.charBuf[r];
this.styleBuf[tr]=inv.styleBuf[r];
this.redraw(tr);
this.maxLines--;
}
}
if (inv.termMaxCols>=0) this.maxCols=inv.termMaxCols;
this.keyRepeatDelay1=inv.keyRepeatDelay1;
this.keyRepeatDelay2=inv.keyRepeatDelay2;
delete inv.termref;
this.lock=false;
this.charMode=inv.charMode;
// delete instance and leave with a prompt
delete this.env.invaders;
this.prompt();
},
getStyleColorFromHexString: function(clr) {
// returns a stylevector for the given color-string
var cc=Terminal.prototype.globals.webifyColor(clr.replace(/^#/,''));
if (cc) {
return Terminal.prototype.globals.webColors[cc]*0x10000;
}
return 0;
}
};
// eof
(C) Æliens
04/09/2009
You may not copy or print any of this material without explicit permission of the author or the publisher.
In case of other copyright issues, contact the author.