Last active
May 22, 2020 06:26
-
-
Save tomaes/4e193f514219ce14a48456003f9f8c3d to your computer and use it in GitHub Desktop.
16-bit DOS game. Download binary from https://tomaes.itch.io/ninja-blitz
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
Ninja Blitz | |
A spin on city bomber games | |
for 1980s era 16 bit DOS PCs | |
designed & written by | |
Thomas "tomaes" Gruetzmacher | |
in May 2020 | |
LICENSE: CC BY-NC 4.0 | |
https://creativecommons.org/licenses/by-nc/4.0/legalcode | |
} | |
{ | |
2020-05-22: $12.3 audio toggle during attack mode fix | |
2020-05-20: $12.2 level highscore, optimized code (-> smaller .exe) | |
2020-05-15: $11.2 public release | |
} | |
uses crt; | |
const REV = '$12.3'; { 2020-05-08/09/10 ..15+} | |
LASTX = 40; | |
LASTY = 50; | |
LASTLV = 9; { final level } | |
CHR_BO = '*'; CHR_B2 = #254; {big dot} | |
CHR_PL = '='; CHR_B1 = #219; {full char} | |
CHR_AT = 'v'; CHR_B3 = '|' ; | |
CHR_TR = '.'; CHR_SC = '$' ; | |
CHR_TR2= ' '; | |
MAP_EMPTY = 0; MAP_CASH = 2; | |
MAP_BLOCK = 1; MAP_GOLD = 3; | |
MAP_DETON = 4; | |
KEY_ENTER = #13; KEY_ESC = #27; | |
KEY_DOWN = #80; KEY_SPACE = #32; | |
PL_LV_COLORS : array[1..10] of byte = | |
( White, LightRed, LightMagenta, LightGray, LightCyan, | |
Blue, Red, Magenta, DarkGray+Blink, Black ); | |
var px, py : byte; { player pos } | |
ay, ar : byte; { attack-from y pos, attack range/time } | |
hp, ohp : byte; { attack HP } | |
ps, ph : byte; { player shots'n'hits } | |
cc : byte; { base city color } | |
lv : byte; { level } | |
sc, hl : integer; { score, highest level } | |
at : boolean; { attack mode } | |
ch : char; | |
playSfx : boolean; | |
f : array[1..LASTX, 1..LASTY] of 0..4; | |
(********************************************************) | |
function rnd(_r: word): word; | |
begin | |
rnd := random(_r); | |
end; | |
function onein(_r: word): boolean; | |
begin | |
onein := (rnd(_r) = 0); | |
end; | |
procedure gxy(_x, _y: byte); | |
begin | |
gotoxy(_x, _y); | |
end; | |
procedure color(_c: byte); | |
begin | |
textcolor(_c); | |
end; | |
procedure waitForKey; | |
var _ch: char; | |
begin | |
_ch := readkey; | |
end; | |
procedure stopSfx; | |
begin | |
nosound; | |
end; | |
procedure sfx(_f,_d: word); | |
begin | |
if playSfx then sound(_f); | |
delay(_d); | |
if playSfx then stopSfx; | |
end; | |
procedure toggleAudio; | |
begin | |
playSfx := not playSfx; | |
if not playSfx then stopSfx; | |
end; | |
(********************************************************) | |
procedure delPlayerYline; | |
var y: byte; | |
begin | |
for y :=1 to py-1 do begin | |
gxy(px, y); | |
write(' '); | |
end; | |
end; | |
procedure eoLevelAnimation; | |
var x, y: byte; | |
i, c: byte; | |
begin | |
for i := ord('9') downto ord(' ') do begin | |
case i-ord(' ') of {explosion fade-out} | |
0..10: c := Blue; | |
11..15: c := DarkGray; | |
16..20: c := LightGray; | |
21..22: c := Yellow; | |
23: c := LightMagenta; | |
24..25: c := White; | |
end; | |
color(c); | |
for y := 1 to LASTY do begin | |
for x := 1 to LASTX do begin | |
if (f[x,y] <> 0) then begin | |
gxy(x,y); | |
case rnd(3) of | |
0: write( CHR_B1 ); | |
1: write( CHR_B2 ); | |
2: write( CHR_B3 ); | |
end; | |
end; | |
end; | |
end; | |
sfx(1000 - i*10, 20); | |
end; | |
end; | |
procedure renderScore; | |
var s: string; | |
y: byte; | |
begin | |
str(sc, s); | |
{ ready to detonate? indicate in score display } | |
if sc >= 1000 then begin | |
if (sc mod 2 = 0) then insert( CHR_BO, s, 0 ) | |
else insert( ' ', s, 0) | |
end | |
else insert( CHR_SC, s, 0 ); | |
for y := 1 to length(s) do | |
begin | |
gxy(1, y); write(s[y]); | |
end; | |
gxy(1, length(s)+1 ); | |
write(' '); | |
str(lv, s); | |
insert('#', s, 0); | |
for y := 1 to length(s) do | |
begin | |
gxy(LASTX, y); write(s[y]); | |
end; | |
end; | |
procedure renderHowTo; | |
var txt : string; | |
i,c : byte; | |
_d : byte; | |
begin | |
txt := '#'#15'briefing'#4'##'; | |
txt := txt + 'x hit space to dive down#'; | |
txt := txt + 'x try to grab ('#14'$'#4') from up high#'; | |
txt := txt + 'x avoid going through walls#'; | |
txt := txt + 'x if you have $1000, attack ('#12'*'#4')#'; | |
txt := txt + ' to detonate everything and#'; | |
txt := txt + ' move on to the next stage#'; | |
txt := txt + 'x the game ends when your#'; | |
txt := txt + ' cash score is gone!##'#6; | |
txt := txt + 'good luck!'; | |
_d := 30; | |
gxy(1,13); | |
for i := 1 to length(txt) do begin | |
if keypressed then begin | |
ch := readkey; | |
if ch in ['m','M'] then toggleAudio | |
else _d := 0; | |
end; | |
case txt[i] of | |
chr(Black).. | |
chr(White) : color( ord(txt[i]) ); | |
'#' : write( #10#13#10#13' > '); | |
else write( UpCase(txt[i]) ); | |
end; | |
if (txt[i] <> ' ') then | |
begin | |
if _d > 0 then sfx(200 + i, 1); | |
delay(_d); | |
end; | |
end; | |
end; | |
procedure makeLevel; | |
var x, y, r : byte; | |
h : byte; | |
begin | |
cc := rnd(10) + 1; | |
fillChar(f, sizeof(f), 0 ); | |
for x:= 6 to LASTX - 5 do | |
begin | |
{ height of building } | |
h := LASTY - rnd(10+lv div 2) + lv div 2 - 1; | |
for y:= LASTY downto h do | |
begin | |
color(cc + y mod 4); | |
f[x, y] := MAP_BLOCK; | |
gxy(x,y); | |
if (y mod 2 = 0) then | |
write(CHR_B1) | |
else if not onein(7) then write(CHR_B2) | |
else write(CHR_B3); | |
end; | |
end; | |
r := 0; | |
{ render prizes (fewer for higher levels) } | |
repeat | |
inc(r); | |
repeat | |
x := 6 + rnd(LASTX - 10); | |
y := LASTY - rnd(LASTY div 4); | |
until f[x, y] = MAP_BLOCK; | |
gxy(x, y); | |
color(Yellow); | |
write(CHR_SC); | |
f[x, y] := MAP_CASH; | |
until (r > 10-lv); { saved score becomes important in later levels } | |
{ (sometimes) render a big prize at the bottom} | |
if onein(10) then begin | |
x := 3 + rnd(LASTX - 4); | |
y := LASTY; | |
f[x,y] := MAP_GOLD; | |
gxy(x,y); | |
color( LightMagenta ); | |
write(CHR_SC); | |
end; | |
{render detonator at the bottom} | |
x := 3 + rnd(LASTX - 4); | |
y := LASTY; | |
f[x,y] := MAP_DETON; | |
gxy(x,y); | |
color(LightRed); | |
write(CHR_BO); | |
end; | |
procedure shufflePrizes; { shuffle prizes with adjacent space randomly } | |
var x,y, rx: word; | |
begin | |
for x := 2 to LASTX-1 do begin | |
for y := 1 to LASTY-1 do | |
if f[x,y] = MAP_CASH then break; | |
if (f[x,y] = MAP_CASH) then begin | |
repeat | |
rx := x + random(3)-1; | |
until rx <> x; | |
if rx < 1 then rx := 1; | |
if rx > LASTX-1 then rx := LASTX-1; | |
if (f[rx, y ] = MAP_EMPTY) and | |
(f[rx, y+1] <> MAP_EMPTY) then | |
begin | |
f[x, y] := MAP_EMPTY; | |
f[rx, y] := MAP_CASH; | |
gxy(x, y); write(' '); | |
gxy(rx,y); color(Yellow); write(CHR_SC); | |
end; | |
end; | |
end; | |
end; | |
procedure resetPlayer; | |
begin | |
ps := 0; ph := 0; | |
px := 2; py := 1; | |
ar := 0; ay := py; | |
hp := 5 + rnd(4); | |
ohp:= hp; | |
at := false; | |
end; | |
(********************************************************) | |
var i: integer; | |
label re; | |
begin | |
randomize; | |
textmode(Font8x8); | |
playSfx := true; | |
renderHowTo; | |
waitForKey; | |
hl := 1; | |
re: | |
lv := 1; | |
sc := 300; | |
clrscr; | |
makeLevel; | |
resetPlayer; | |
gxy(16, LASTY div 2 - 4); | |
color(White); | |
write('GET READY'); | |
sfx(1000, 20); | |
delay(1000); | |
gxy(1, LASTY div 2 - 4); clrEol; | |
repeat | |
dec(sc); | |
color( PL_LV_COLORS[lv] ); | |
renderScore; | |
gxy(px, py); | |
if at then begin | |
if ar > 0 then write(CHR_AT) | |
end | |
else write(CHR_PL); | |
if not at then sfx( sc*2, 2); | |
if at then begin | |
if playSfx then sound(80+ar*5); | |
delay(10); | |
end | |
else delay(100); | |
if (lv > 2) and not at then shufflePrizes; { lv 3+ prizes can be moving targets } | |
if at then inc(ar); { attack range bonus: long shots prefered } | |
if at then begin { render attack trail } | |
for i := ay+1 to ay+ar-1 do begin | |
gxy(px, i); | |
if ((i-ar) mod 30 = 0) then write(CHR_TR2) | |
else write(CHR_TR); | |
end; | |
end else begin | |
gxy(px, py); | |
write(' '); | |
end; | |
case f[px,py] of | |
MAP_BLOCK: begin | |
if at then begin { attack block hit } | |
dec(hp); | |
dec(sc, (10-hp)*10 ); | |
sfx(500, 50); | |
end else | |
sc := 0; { normal block crash: instant death } | |
end; | |
MAP_CASH: begin | |
inc(sc, 300); { cash hit; possible from side } | |
sfx(200, 50); | |
sfx(400, 50); | |
hp := 0; | |
inc(ph); | |
inc(sc, ar*2) { range bonus } | |
end; | |
MAP_GOLD: begin | |
inc(sc, 300*2); { big cash/gold hit } | |
sfx(300, 50); | |
sfx(500, 50); | |
sfx(600, 50); | |
hp := 0; | |
inc(ph); | |
inc(sc, ar*2) { range bonus } | |
end; | |
MAP_DETON: begin { end-of-level detonator hit} | |
eoLevelAnimation; | |
dec(sc, 1000); | |
inc(lv); | |
clrscr; | |
for i := 0 to 400 do sfx( rnd(1000), 1 ); | |
makeLevel; | |
resetPlayer; | |
if (sc > 0) and (sc < 500) then sc := 500; { min. score when alive in the next level } | |
end; | |
end; | |
f[px, py] := MAP_EMPTY; | |
if not at then | |
begin | |
if (px < LASTX - 1) then inc(px) else | |
begin | |
px := 2; | |
inc(py); | |
end; | |
end else begin | |
inc(py); | |
if (py > LASTY) and (ohp - hp = 0) then | |
begin | |
dec(sc, 50); { blank shot } | |
sfx(30, 200); { silent on windows! } | |
end; | |
if (py > LASTY) or (hp = 0) then begin | |
delPlayerYLine; { continue from attack position } | |
py := ay; | |
inc(py); { ...but closer to the bottom } | |
at := false; | |
hp := 5 + rnd(4); | |
ohp:= hp; | |
end; | |
end; | |
if keypressed then | |
begin | |
ch := readkey; | |
if not at and ((ch = KEY_SPACE) or (ch = KEY_DOWN)) then | |
begin | |
gxy(px, py); write(' '); | |
at := true; | |
ay := py; | |
ar := 0; | |
inc(ps); | |
end; | |
{ pray to the cheat gods } | |
if ch = '$' then begin | |
i := rnd(1000)-500; | |
inc(sc, i); | |
sfx(580+i, 200); | |
end; | |
{ skip level cheat } | |
if ch = '>' then begin | |
inc(lv); | |
clrscr; | |
makeLevel; | |
resetPlayer; | |
end; | |
{ play sounds or not } | |
if ch in ['M','m'] then toggleAudio; | |
end; | |
{ user exit, player crash or level cap } | |
until (ch = KEY_ESC) or (sc <= 0) or (lv > LASTLV); | |
sfx(100, 100); | |
gxy(15, LASTY div 2 - 4); | |
color(White); | |
if (lv > LASTLV) then writeln('.!AMAZiNG!.') | |
else writeln('NiNjA BLiTZ'); | |
gxy(11, LASTY div 2 - 2); | |
color(Red); | |
writeln('HITS:', ph, '/', ps,' ON LEVEL:', lv ); | |
gxy(10, LASTY div 2 + 1); | |
color(Red + Blink); | |
writeln('<ENTER> TO TRY AGAIN!'); | |
gxy(14, LASTY div 2 + 5); | |
if lv > hl then begin | |
hl := lv; | |
color(Yellow); | |
write(' BEST LEVEL!'); | |
end else begin | |
color(cc); | |
write(' max level:', hl, ' '); | |
end; | |
repeat | |
ch := readkey; | |
until ch in [KEY_ESC, KEY_ENTER]; | |
if (ch = KEY_ENTER) then goto re; | |
textmode(c80); | |
color(White); | |
writeln('Written in May 2020 (rev. ', REV,')'#13#10); | |
color(cc); | |
writeln('> github.com/tomaes'); | |
writeln('> tomaes.itch.io'); | |
delay(1000); | |
end. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment