#include #include #include #include #include #include #include #include #include #include "dat.h" #include "fns.h" #include "mixer.h" enum { PCBlack, PCWhite, PCRed, PCGreen, PCShip, PCYellow, PCBlue, PCWater, PCWaves, PCBrown, PCShadow, NCOLORS }; enum { CMid, CMqueued, CMlayout, CMoid, CMwait, CMplay, CMwehit, CMwemiss, CMtheyhit, CMtheymiss, CMmatches, /* list opening */ CMmatch, /* list entry */ CMendmatches, /* list closure */ CMwatching, CMwin, CMlose, CMplayeroutlay, CMplayerhit, CMplayermiss, CMplayerplays, CMplayerwon, CMstats, }; Cmdtab svcmd[] = { CMid, "id", 1, CMqueued, "queued", 1, CMlayout, "layout", 1, CMoid, "oid", 2, CMwait, "wait", 1, CMplay, "play", 1, CMwehit, "hit", 1, CMwemiss, "miss", 1, CMtheyhit, "hit", 2, CMtheymiss, "miss", 2, CMmatches, "matches", 1, CMmatch, "m", 4, CMendmatches, "endmatches", 1, CMwatching, "watching", 4, CMwin, "win", 1, CMlose, "lose", 1, CMplayeroutlay, "outlayed", 3, CMplayerhit, "hit", 3, CMplayermiss, "miss", 3, CMplayerplays, "plays", 2, CMplayerwon, "won", 2, CMstats, "stats", 2, }; int debug; int silent; Cursor patrolcursor = { {0, 0}, { 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x03, 0x80, 0x06, 0x80, 0x06, 0xc0, 0x0e, 0xe0, 0x1e, 0xf0, 0x1f, 0xf8, 0x3f, 0x00, 0x01, 0x00, 0x41, 0x02, 0x7f, 0xfe, 0x7f, 0xfc, 0x3f, 0xf8, 0x00, 0x00, }, { 0x03, 0x80, 0x06, 0x80, 0x04, 0xc0, 0x0c, 0x40, 0x09, 0x60, 0x19, 0x30, 0x31, 0x18, 0x21, 0x0c, 0x60, 0x04, 0x40, 0xfc, 0xfe, 0x87, 0xbe, 0xfd, 0x80, 0x01, 0x80, 0x03, 0xc0, 0x06, 0x7f, 0xfc, }, }; Cursor waitcursor = { {0, 0}, { 0x01, 0x80, 0x03, 0xc0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x03, 0xc0, 0x0f, 0xf0, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x0f, 0xf0, 0x1f, 0xf8, 0x3f, 0xfc, 0x3f, 0xfc, }, { 0x01, 0x80, 0x03, 0xc0, 0x07, 0xe0, 0x04, 0x20, 0x04, 0x20, 0x06, 0x60, 0x02, 0x40, 0x0c, 0x30, 0x10, 0x08, 0x14, 0x08, 0x14, 0x28, 0x12, 0x28, 0x0a, 0x50, 0x16, 0x68, 0x20, 0x04, 0x3f, 0xfc, } }; Cursor boxcursor = { {-7, -7}, { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, { 0x00, 0x00, 0x7f, 0xfe, 0x7f, 0xfe, 0x7f, 0xfe, 0x70, 0x0e, 0x70, 0x0e, 0x70, 0x0e, 0x70, 0x0e, 0x70, 0x0e, 0x70, 0x0e, 0x70, 0x0e, 0x70, 0x0e, 0x7f, 0xfe, 0x7f, 0xfe, 0x7f, 0xfe, 0x00, 0x00 } }; Cursor aimcursor = { {-7, -7}, { 0x1f, 0xf8, 0x3f, 0xfc, 0x7f, 0xfe, 0xfb, 0xdf, 0xf3, 0xcf, 0xe3, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xc7, 0xf3, 0xcf, 0x7b, 0xdf, 0x7f, 0xfe, 0x3f, 0xfc, 0x1f, 0xf8, }, { 0x00, 0x00, 0x0f, 0xf0, 0x31, 0x8c, 0x21, 0x84, 0x41, 0x82, 0x41, 0x82, 0x41, 0x82, 0x7f, 0xfe, 0x7f, 0xfe, 0x41, 0x82, 0x41, 0x82, 0x41, 0x82, 0x21, 0x84, 0x31, 0x8c, 0x0f, 0xf0, 0x00, 0x00, } }; char deffont[] = "/lib/font/bit/pelm/unicode.9.font"; char assetdir[] = "/sys/games/lib/battleship"; //char assetdir[] = "assets"; char titlefontpath[] = "font/gunmetal/gunmetal.48.font"; Font *titlefont; char winspec[32]; char uid[8+1], oid[8+1]; Image *coverimg; Sprite *spritetab[NVFX]; Vfx vfxqueue; Channel *drawchan; Channel *reconnc; Channel *ingress, *egress; Mousectl *mctl; /* only used to update the cursor */ Keyboardctl *kctl; /* only used to ignore key presses during specific interactions */ RFrame worldrf; Image *pal[NCOLORS]; Image *screenb; Image *tiletab[NTILES]; AudioSource *playlist[NSOUNDS]; Board alienboard; Board localboard; Ship armada[NSHIPS]; Ship *curship; int layoutdone; Point2 lastshot; Menulist *matches; MatchInfo match; /* of which we are an spectator */ int nplayers; struct { Image *c; /* color */ char *s; /* banner text */ AudioSource *snd; /* victory or defeat bg sfx */ } conclusion; int gamestate; static void PvP_handler(Button *) { if(gamestate != Waiting0) return; chanprint(egress, "play %d\n", GMPvP); } static void PvAI_handler(Button *) { if(gamestate != Waiting0) return; chanprint(egress, "play %d\n", GMPvAI); } Button mainbtns[] = { { .label = "PvP", .handler = PvP_handler }, { .label = "PvAI", .handler = PvAI_handler }, }; Point fromworld(Point2 p) { p = invrframexform(p, worldrf); return Pt(p.x,p.y); } Point2 toworld(Point p) { return rframexform(Pt2(p.x,p.y,1), worldrf); } Point fromboard(Board *b, Point2 p) { p = invrframexform(invrframexform(p, *b), worldrf); return Pt(p.x,p.y); } Point2 toboard(Board *b, Point p) { Point2 np; np = rframexform(rframexform(Pt2(p.x,p.y,1), worldrf), *b); np.x = floor(np.x); np.y = floor(np.y); return np; } int rectXarmada(Rectangle r) { int i; for(i = 0; i < nelem(armada); i++) if(curship != &armada[i] && rectXrect(r, armada[i].bbox)) return 1; return 0; } Rectangle mkshipbbox(Point2 p, int o, int ncells) { Point2 sv; assert(o == OH || o == OV); sv = o == OH? Vec2(1,0): Vec2(0,1); return Rpt( fromboard(&localboard, p), fromboard(&localboard, addpt2(addpt2(p, mulpt2(sv, ncells)), Vec2(sv.y,sv.x))) ); } void csetcursor(Mousectl *mc, Cursor *c) { static Cursor *oc; if(c == oc) return; setcursor(mc, c); oc = c; } void playvfx(int idx, Point p, int times) { if(idx < 0 || idx > NVFX) return; addvfx(&vfxqueue, newvfx(spritetab[idx]->clone(spritetab[idx]), p, times)); } void resetgame(void) { int i; memset(localboard.map, Twater, MAPW*MAPH); memset(alienboard.map, Twater, MAPW*MAPH); for(i = 0; i < nelem(armada); i++){ armada[i].bbox = ZR; memset(armada[i].hit, 0, armada[i].ncells*sizeof(int)); } curship = nil; layoutdone = 0; oid[0] = 0; gamestate = Waiting0; conclusion.s = nil; csetcursor(mctl, nil); if(!silent){ stopaudio(conclusion.snd); conclusion.snd = nil; playaudio(playlist[SBG0]); } } Point vstring(Image *dst, Point p, Image *src, Point sp, Font *f, char *s) { while(*s){ stringn(dst, p, src, sp, f, s++, 1); p.y += font->height; } return p; } Image * gettileimage(int type) { if(type < 0 || type > nelem(tiletab)) return nil; return tiletab[type]; } void drawtile(Image *dst, Board *b, Point2 cell, int type) { Point p; Image *ti; p = fromboard(b, cell); ti = gettileimage(type); if(ti == nil) return; draw(dst, Rpt(p, addpt(p, Pt(TW,TH))), ti, nil, ZP); } void drawship(Image *dst, Ship *s) { Point2 p, sv; int i; if(!rectinrect(s->bbox, localboard.bbox)) return; p = s->p; assert(s->orient == OH || s->orient == OV); sv = s->orient == OH? Vec2(1,0): Vec2(0,1); for(i = 0; i < s->ncells; i++){ drawtile(dst, &localboard, p, s->hit[i]? Thit: Tship); p = addpt2(p, sv); } } void drawarmada(Image *dst) { int i; for(i = 0; i < nelem(armada); i++) drawship(dst, &armada[i]); } void drawboard(Image *dst, Board *b) { int i, j; border(dst, b->bbox, -Borderwidth, pal[PCBrown], ZP); for(i = 0; i < MAPW; i++) for(j = 0; j < MAPH; j++) drawtile(dst, b, Pt2(i,j,1), b->map[i][j]); } void drawtitle(Image *dst) { static char s[] = "BATTLESHIP"; string(dst, Pt(SCRW/2 - stringwidth(titlefont, s)/2, 0), pal[PCWhite], ZP, titlefont, s); draw(dst, rectaddpt(coverimg->r, subpt(dst->r.max, subpt(coverimg->r.max, coverimg->r.min))), coverimg, nil, ZP); } void drawgameoptions(Image *dst) { Button *b; for(b = mainbtns; b < mainbtns+nelem(mainbtns); b++){ draw(dst, b->r, pal[b->status? PCBlack: PCWhite], nil, ZP); border(dst, b->r, Btnborder, pal[PCWater], ZP); string(dst, addpt(b->r.min, Pt(Dx(b->r)/2 - stringwidth(font, b->label)/2, Btnpadding + Btnborder)), pal[b->status? PCWhite: PCBlack], ZP, font, b->label); } } void drawgamestats(Image *dst) { Point p, dim; char buf[32]; if(nplayers < 1) return; snprint(buf, sizeof buf, "%d player%s connected", nplayers, nplayers == 1? "": "s"); dim = stringsize(font, buf); p = Pt(matches->r.min.x + Dx(matches->r)/2, matches->r.max.y + font->height); p.x -= dim.x/2; string(dst, p, pal[PCWater], ZP, font, buf); } void drawinfo(Image *dst) { Point p; char *s, buf[32]; int i; s = ""; switch(gamestate){ case Watching: snprint(buf, sizeof buf, "watching %s vs. %s", match.pl[0].uid, match.pl[1].uid); s = buf; break; case Ready: s = "looking for players"; break; case Outlaying: s = "place the fleet"; break; case Waiting: s = "wait for your turn"; break; case Playing: s = "your turn"; break; } p = Pt(SCRW/2 - stringwidth(font, s)/2, 0); string(dst, p, pal[PCWhite], ZP, font, s); s = gamestate == Watching? "PLAYER 1": "TARGET"; p = subpt(alienboard.bbox.min, Pt(font->width+2+Borderwidth,0)); vstring(dst, p, pal[PCWhite], ZP, font, s); s = gamestate == Watching? "PLAYER 2": "LOCAL"; p = Pt(localboard.bbox.max.x+2+Borderwidth, localboard.bbox.min.y); vstring(dst, p, pal[PCWhite], ZP, font, s); p = Pt(alienboard.bbox.max.x+2+Borderwidth, alienboard.bbox.min.y); vstring(dst, p, pal[PCWhite], ZP, font, gamestate == Watching? match.pl[1].uid: oid); p = subpt(localboard.bbox.min, Pt(font->width+2+Borderwidth,0)); vstring(dst, p, pal[PCWhite], ZP, font, gamestate == Watching? match.pl[0].uid: uid); switch(gamestate){ case Outlaying: if(curship != nil){ snprint(buf, sizeof buf, "%s (%d)", shipname(curship-armada), curship->ncells); p = Pt(SCRW/2 - stringwidth(font, buf)/2, SCRH-Boardmargin); string(dst, p, pal[PCYellow], ZP, font, buf); s = "MMB to rotate the ship"; p = Pt(SCRW/2 - stringwidth(font, s)/2, SCRH-Boardmargin+font->height); string(dst, p, pal[PCYellow], ZP, font, s); }else{ s = "done with the layout?"; p = Pt(SCRW/2 - stringwidth(font, s)/2, SCRH-Boardmargin); string(dst, p, pal[PCYellow], ZP, font, s); s = "LMB/MMB = No, RMB = Yes"; p = Pt(SCRW/2 - stringwidth(font, s)/2, SCRH-Boardmargin+font->height); string(dst, p, pal[PCYellow], ZP, font, s); } break; case Watching: for(i = 0; i < nelem(match.pl); i++) if(match.pl[i].state == Playing){ snprint(buf, sizeof buf, "it's %s's turn", match.pl[i].uid); p = Pt(SCRW/2 - stringwidth(font, buf)/2, SCRH-Boardmargin); string(dst, p, pal[PCBlue], ZP, font, buf); return; } s = "waiting for players to"; p = Pt(SCRW/2 - stringwidth(font, s)/2, SCRH-Boardmargin); string(dst, p, pal[PCBlue], ZP, font, s); s = "lay out their fleet"; p = Pt(SCRW/2 - stringwidth(font, s)/2, SCRH-Boardmargin+font->height); string(dst, p, pal[PCBlue], ZP, font, s); break; } } void drawconclusion(Image *dst) { static char s[] = "press any key to continue"; if(conclusion.s == nil) return; draw(dst, dst->r, pal[PCShadow], nil, ZP); string(dst, Pt(SCRW/2 - stringwidth(font, conclusion.s)/2, font->height+5), conclusion.c, ZP, font, conclusion.s); string(dst, Pt(SCRW/2 - stringwidth(font, s)/2, 10*font->height+5), pal[PCWhite], ZP, font, s); } void redraw(void) { Vfx *vfx; lockdisplay(display); draw(screenb, screenb->r, pal[PCBlack], nil, ZP); switch(gamestate){ case Waiting0: drawtitle(screenb); drawgameoptions(screenb); matches->draw(matches, screenb); drawgamestats(screenb); break; default: drawboard(screenb, &alienboard); drawboard(screenb, &localboard); drawarmada(screenb); drawinfo(screenb); break; } for(vfx = vfxqueue.next; vfx != &vfxqueue; vfx = vfx->next) vfx->draw(vfx, screenb); drawconclusion(screenb); draw(screen, screen->r, screenb, nil, ZP); flushimage(display, 1); unlockdisplay(display); } void resize(void) { int fd; lockdisplay(display); if(getwindow(display, Refnone) < 0) sysfatal("resize failed"); unlockdisplay(display); if(Dx(screen->r) != SCRW || Dy(screen->r) != SCRH){ fd = open("/dev/wctl", OWRITE); if(fd >= 0){ fprint(fd, "resize %s", winspec); close(fd); } } nbsend(drawchan, nil); } void initpalette(void) { pal[PCBlack] = display->black; pal[PCWhite] = display->white; pal[PCRed] = eallocimage(display, Rect(0,0,1,1), screen->chan, 1, DRed); pal[PCGreen] = eallocimage(display, Rect(0,0,1,1), screen->chan, 1, DGreen); pal[PCShip] = eallocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xAAAAAAFF); pal[PCYellow] = eallocimage(display, Rect(0,0,1,1), screen->chan, 1, DDarkyellow); pal[PCWater] = eallocimage(display, Rect(0,0,1,1), screen->chan, 1, DPalegreyblue); pal[PCWaves] = eallocimage(display, Rect(0,0,1,1), screen->chan, 1, DPalebluegreen); pal[PCBlue] = pal[PCWaves]; pal[PCBrown] = eallocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x806000FF); pal[PCShadow] = eallocimage(display, Rect(0,0,1,1), RGBA32, 1, 0x0000007f); } void inittiles(void) { int i, x, y; Point pts[2]; for(i = 0; i < nelem(tiletab); i++){ tiletab[i] = eallocimage(display, Rect(0,0,TW,TH), screen->chan, 0, DNofill); switch(i){ case Twater: draw(tiletab[i], tiletab[i]->r, pal[PCWater], nil, ZP); for(pts[0] = ZP, x = 0; x < TW; x++){ y = sin(x)*TH/2; pts[1] = Pt(x,y+TH/2); line(tiletab[i], pts[0], pts[1], Endsquare, Endsquare, 0, pal[PCWaves], ZP); pts[0] = pts[1]; } break; case Tship: draw(tiletab[i], tiletab[i]->r, pal[PCShip], nil, ZP); fillellipse(tiletab[i], Pt(TW/2,TH/2), 2, 2, pal[PCBlack], ZP); break; case Thit: draw(tiletab[i], tiletab[i]->r, pal[PCRed], nil, ZP); pts[0] = Pt(TW/2-TW/4,TH/2-TH/4); pts[1] = Pt(TW/2+TW/4,TH/2+TH/4); line(tiletab[i], pts[0], pts[1], Endsquare, Endsquare, 1, pal[PCBlack], ZP); pts[0].y += TH/2; pts[1].y -= TH/2; line(tiletab[i], pts[0], pts[1], Endsquare, Endsquare, 1, pal[PCBlack], ZP); break; case Tmiss: draw(tiletab[i], tiletab[i]->r, tiletab[Twater], nil, ZP); ellipse(tiletab[i], Pt(TW/2,TH/2), 6, 6, 1, pal[PCWhite], ZP); break; } } } void initmainbtns(void) { Button *b; Point btnsize; btnsize.x = Btnborder + Btnpadding + 100 + Btnpadding + Btnborder; btnsize.y = Btnborder + Btnpadding + font->height + Btnpadding + Btnborder; for(b = mainbtns; b < mainbtns+nelem(mainbtns); b++){ b->status = BRest; if(b == mainbtns) b->r.min = Pt(SCRW/2 - Btnpadding - 100/2, 8*font->height); else b->r.min = addpt(b[-1].r.min, Pt(0, btnsize.y+4)); b->r.max = addpt(b->r.min, btnsize); } } void initboards(void) { memset(alienboard.map, Twater, MAPW*MAPH); alienboard.p = Pt2(Boardmargin+Borderwidth,Boardmargin+Borderwidth,1); alienboard.bx = Vec2(TW,0); alienboard.by = Vec2(0,TH); alienboard.bbox = Rpt(fromworld(alienboard.p), fromworld(addpt2(alienboard.p, Pt2(TW*MAPW,TH*MAPH,1)))); memset(localboard.map, Twater, MAPW*MAPH); localboard.p = addpt2(alienboard.p, Vec2(0,MAPH*TH+Borderwidth+TH+Borderwidth)); localboard.bx = Vec2(TW,0); localboard.by = Vec2(0,TH); localboard.bbox = Rpt(fromworld(localboard.p), fromworld(addpt2(localboard.p, Pt2(TW*MAPW,TH*MAPH,1)))); } void initarmada(void) { Ship *s; int i; for(i = 0; i < nelem(armada); i++){ s = &armada[i]; s->ncells = shiplen(i); s->orient = OV; s->hit = emalloc(s->ncells*sizeof(int)); memset(s->hit, 0, s->ncells*sizeof(int)); } } void initvfx(void) { char aux[64]; snprint(aux, sizeof aux, "%s/%s", assetdir, "vfx/battleship.png"); coverimg = readpngimage(aux); snprint(aux, sizeof aux, "%s/%s", assetdir, "vfx/hit.png"); spritetab[VFXHit] = readpngsprite(aux, ZP, Rect(0, 0, 32, 32), 12, 100); snprint(aux, sizeof aux, "%s/%s", assetdir, "vfx/miss.png"); spritetab[VFXMiss] = readpngsprite(aux, ZP, Rect(0, 0, 32, 32), 7, 150); snprint(aux, sizeof aux, "%s/%s", assetdir, "vfx/shining.png"); spritetab[VFXShine] = readpngsprite(aux, ZP, Rect(0, 0, 50, 50), 11, 100); initvfxq(&vfxqueue); } void initsfx(void) { struct { char *path; double gain; int loops; } sndtab[NSOUNDS] = { [SBG0] {"sfx/bg0.mp3", 1.0, 1}, [SBG1] {"sfx/bg1.mp3", 1.0, 1}, [SBG2] {"sfx/bg2.mp3", 1.0, 1}, [SCANNON] {"sfx/cannon.mp3", 5.0, 0}, [SWATER] {"sfx/water.mp3", 3.0, 0}, [SVICTORY] {"sfx/victory.mp3", 1.0, 1}, [SDEFEAT] {"sfx/defeat.mp3", 1.0, 1}, }; int i; char aux[64]; initaudio(44100); audio_set_master_gain(0.5); for(i = 0; i < NSOUNDS; i++){ snprint(aux, sizeof aux, "%s/%s", assetdir, sndtab[i].path); playlist[i] = loadaudiosource(aux); if(playlist[i] == nil) sysfatal("loadaudiosource: %r"); audio_set_gain(playlist[i], sndtab[i].gain); audio_set_loop(playlist[i], sndtab[i].loops); } playaudio(playlist[SBG0]); } int confirmdone(Mousectl *mc) { Cursor anchor = { {0, 0}, { 0x00, 0x00, 0x00, 0x1e, 0x01, 0x92, 0x30, 0xd2, 0x70, 0x7e, 0x60, 0x70, 0x40, 0xf8, 0x41, 0xcc, 0x43, 0x84, 0x47, 0x00, 0x4e, 0x00, 0x5c, 0x00, 0x78, 0x18, 0x70, 0x38, 0x7f, 0xf0, 0x00, 0x00, }, { 0x00, 0x3f, 0x03, 0xe1, 0x7a, 0x6d, 0xcb, 0x2d, 0x89, 0x81, 0x99, 0x8f, 0xb3, 0x07, 0xa6, 0x33, 0xac, 0x7a, 0xb8, 0xce, 0xb1, 0x80, 0xa3, 0x3c, 0x86, 0x64, 0x8f, 0xc4, 0x80, 0x0c, 0xff, 0xf8, }, }; csetcursor(mc, &anchor); while(mc->buttons != 0) /* ignore any buttons already pressed */ readmouse(mc); while(mc->buttons == 0) /* wait for a button press */ readmouse(mc); if(mc->buttons != 4){ while(nbrecv(kctl->c, nil)) /* flush key presses */ csetcursor(mc, nil); return 0; } while(mc->buttons){ /* commit action on button release */ if(mc->buttons != 4){ while(nbrecv(kctl->c, nil)); csetcursor(mc, nil); return 0; } readmouse(mc); } while(nbrecv(kctl->c, nil)); csetcursor(mc, nil); return 1; } void sendlayout(void) { char buf[NSHIPS*(1+3+1)+1]; int i, n; n = 0; for(i = 0; i < nelem(armada); i++){ assert(sizeof(buf) - n > 1+3+1); if(i != 0) buf[n++] = ','; n += cell2coords(buf+n, sizeof(buf) - n, armada[i].p); buf[n++] = armada[i].orient == OH? 'h': 'v'; } buf[n] = 0; chanprint(egress, "layout %s\n", buf); layoutdone++; } void lmb(Mousectl *mc) { Point2 cell; Button *b; char buf[3+1]; if(conclusion.s != nil) return; switch(gamestate){ case Waiting0: for(b = mainbtns; b < mainbtns+nelem(mainbtns); b++) if(ptinrect(mc->xy, b->r)){ b->handler(b); break; } break; case Outlaying: if(!ptinrect(mc->xy, localboard.bbox)) break; if(curship != nil && rectinrect(curship->bbox, localboard.bbox)){ if(++curship >= armada+nelem(armada)){ curship = nil; redraw(); if(confirmdone(mc)) sendlayout(); else curship = &armada[0]; }else if(curship != &armada[0]) curship->orient = (curship-1)->orient; nbsend(drawchan, nil); } break; case Playing: if(!ptinrect(mc->xy, alienboard.bbox)) break; if(!silent) playaudio(playlist[SCANNON]); cell = toboard(&alienboard, mc->xy); cell2coords(buf, sizeof buf, cell); if(gettile(&alienboard, cell) == Twater){ chanprint(egress, "shoot %s\n", buf); lastshot = cell; } break; } } void mmb(Mousectl *mc) { if(gamestate != Outlaying) return; if(curship != nil){ curship->orient = curship->orient == OH? OV: OH; curship->bbox = mkshipbbox(curship->p, curship->orient, curship->ncells); if(debug) fprint(2, "curship orient %c\n", curship->orient == OH? 'h': 'v'); /* steer it, captain! don't let it go off-board! */ if(!rectinrect(curship->bbox, localboard.bbox)){ switch(curship->orient){ case OH: curship->bbox.min.x -= curship->bbox.max.x - localboard.bbox.max.x; curship->bbox.max.x = localboard.bbox.max.x; break; case OV: curship->bbox.min.y -= curship->bbox.max.y - localboard.bbox.max.y; curship->bbox.max.y = localboard.bbox.max.y; break; } curship->p = toboard(&localboard, curship->bbox.min); moveto(mc, addpt(screen->r.min, curship->bbox.min)); readmouse(mc); /* ignore mmb click triggered by moveto */ } /* …nor ram allied ships! */ if(rectXarmada(curship->bbox)) curship->bbox = ZR; nbsend(drawchan, nil); } } void mouse(Mousectl *mc) { static Mouse oldm; Rectangle newbbox; Button *b; int selmatch; mc->xy = subpt(mc->xy, screen->r.min); if(gamestate == Waiting0){ for(b = mainbtns; b < mainbtns+nelem(mainbtns); b++){ if(ptinrect(mc->xy, b->r)){ if(b->status == BRest) playvfx(VFXShine, addpt(b->r.min, Pt(Btnborder, Btnborder)), 1); b->status = BHover; }else b->status = BRest; } nbsend(drawchan, nil); if((selmatch = matches->update(matches, mc, drawchan)) >= 0){ if(debug) fprint(2, "selected match id %d title %s\n", matches->entries[selmatch].id, matches->entries[selmatch].title); chanprint(egress, "watch %d\n", matches->entries[selmatch].id); } } if(gamestate == Outlaying && curship != nil){ newbbox = mkshipbbox(toboard(&localboard, mc->xy), curship->orient, curship->ncells); if(ptinrect(mc->xy, localboard.bbox)) csetcursor(mctl, &boxcursor); else csetcursor(mctl, nil); if(rectinrect(newbbox, localboard.bbox) && !rectXarmada(newbbox)){ curship->p = toboard(&localboard, mc->xy); curship->bbox = newbbox; nbsend(drawchan, nil); } } if(gamestate == Playing && conclusion.s == nil) if(ptinrect(mc->xy, alienboard.bbox)) csetcursor(mctl, &aimcursor); else csetcursor(mctl, nil); if(oldm.buttons != mc->buttons) switch(mc->buttons){ case 1: lmb(mc); break; case 2: mmb(mc); break; } oldm = mc->Mouse; } void key(Rune r) { if(conclusion.s != nil){ resetgame(); nbsend(drawchan, nil); return; } switch(r){ case Kdel: threadexitsall(nil); case Kesc: case 'q': if(gamestate == Waiting0) threadexitsall(nil); nbsend(reconnc, nil); break; } } void celebrate(void) { static char s[] = "YOU WON!"; conclusion.c = pal[PCGreen]; conclusion.s = s; if(!silent){ conclusion.snd = playlist[SVICTORY]; stopaudio(playlist[SBG2]); playaudio(conclusion.snd); } } void keelhaul(void) { static char s[] = "...YOU LOST"; conclusion.c = pal[PCRed]; conclusion.s = s; if(!silent){ conclusion.snd = playlist[SDEFEAT]; stopaudio(playlist[SBG2]); playaudio(conclusion.snd); } } void announcewinner(char *winner) { static char s[16]; if(winner == nil) return; snprint(s, sizeof s, "%s WON", winner); conclusion.c = pal[PCGreen]; conclusion.s = s; if(!silent){ conclusion.snd = playlist[SVICTORY]; stopaudio(playlist[SBG2]); playaudio(conclusion.snd); } } void processcmd(char *cmd) { Cmdbuf *cb; Cmdtab *ct; Point2 cell; uchar buf[BY2MAP]; int i, idx; struct { int high, off; } menuparams; if(debug) fprint(2, "rcvd '%s'\n", cmd); cb = parsecmd(cmd, strlen(cmd)); ct = lookupcmd(cb, svcmd, nelem(svcmd)); if(ct == nil){ free(cb); return; } switch(ct->index){ case CMwin: celebrate(); break; case CMlose: keelhaul(); break; } switch(gamestate){ case Waiting0: switch(ct->index){ case CMid: chanprint(egress, "id %s\n", uid); break; case CMqueued: gamestate = Ready; csetcursor(mctl, &patrolcursor); break; case CMmatches: if(!matches->filling){ menuparams.high = matches->high; menuparams.off = matches->off; matches->clear(matches); matches->filling = 1; } break; case CMmatch: if(matches->filling) matches->add(matches, strtoul(cb->f[1], nil, 10), smprint("%s vs %s", cb->f[2], cb->f[3])); break; case CMendmatches: if(matches->filling){ matches->high = min(menuparams.high, matches->nentries-1); matches->off = menuparams.off > matches->nentries - matches->maxvis? 0: menuparams.off; matches->filling = 0; } break; case CMwatching: match.id = strtoul(cb->f[1], nil, 10); snprint(match.pl[0].uid, sizeof match.pl[0].uid, "%s", cb->f[2]); snprint(match.pl[1].uid, sizeof match.pl[1].uid, "%s", cb->f[3]); match.pl[0].state = Outlaying; match.pl[1].state = Outlaying; match.bl[0] = &localboard; match.bl[1] = &alienboard; gamestate = Watching; if(!silent){ stopaudio(playlist[SBG0]); playaudio(playlist[SBG2]); } break; case CMstats: nplayers = strtoul(cb->f[1], nil, 10); break; } break; case Ready: switch(ct->index){ case CMlayout: gamestate = Outlaying; curship = &armada[0]; if(!silent){ stopaudio(playlist[SBG0]); playaudio(playlist[SBG2]); } break; case CMoid: snprint(oid, sizeof oid, "%s", cb->f[1]); break; } break; case Watching: switch(ct->index){ case CMplayeroutlay: idx = strtoul(cb->f[1], nil, 10); if(dec64(buf, sizeof buf, cb->f[2], strlen(cb->f[2])) < 0) sysfatal("dec64 failed"); bitunpackmap(match.bl[idx], buf, sizeof buf); match.pl[idx].state = Waiting; break; case CMplayerhit: idx = strtoul(cb->f[1], nil, 10); cell = coords2cell(cb->f[2]); settile(match.bl[idx^1], cell, Thit); playvfx(VFXHit, addpt(fromboard(match.bl[idx^1], cell), Pt(TW/2, TH/2)), 1); break; case CMplayermiss: idx = strtoul(cb->f[1], nil, 10); cell = coords2cell(cb->f[2]); settile(match.bl[idx^1], cell, Tmiss); playvfx(VFXMiss, addpt(fromboard(match.bl[idx^1], cell), Pt(TW/2, TH/2)), 1); break; case CMplayerplays: idx = strtoul(cb->f[1], nil, 10); match.pl[idx].state = Playing; match.pl[idx^1].state = Waiting; break; case CMplayerwon: idx = strtoul(cb->f[1], nil, 10); announcewinner(match.pl[idx].uid); break; } break; case Outlaying: switch(ct->index){ case CMwait: gamestate = Waiting; csetcursor(mctl, &waitcursor); break; case CMplay: gamestate = Playing; break; } break; case Playing: switch(ct->index){ case CMwait: gamestate = Waiting; csetcursor(mctl, &waitcursor); break; case CMwehit: settile(&alienboard, lastshot, Thit); playvfx(VFXHit, addpt(fromboard(&alienboard, lastshot), Pt(TW/2, TH/2)), 1); break; case CMwemiss: if(!silent) playaudio(playlist[SWATER]); settile(&alienboard, lastshot, Tmiss); playvfx(VFXMiss, addpt(fromboard(&alienboard, lastshot), Pt(TW/2, TH/2)), 1); break; } break; case Waiting: switch(ct->index){ case CMplay: gamestate = Playing; csetcursor(mctl, nil); break; case CMtheyhit: cell = coords2cell(cb->f[1]); for(i = 0; i < nelem(armada); i++) if(ptinrect(fromboard(&localboard, cell), armada[i].bbox)){ playvfx(VFXHit, addpt(fromboard(&localboard, cell), Pt(TW/2, TH/2)), 1); cell = subpt2(cell, armada[i].p); armada[i].hit[(int)vec2len(cell)] = 1; break; } break; case CMtheymiss: cell = coords2cell(cb->f[1]); settile(&localboard, cell, Tmiss); playvfx(VFXMiss, addpt(fromboard(&localboard, cell), Pt(TW/2, TH/2)), 1); break; } break; } free(cb); nbsend(drawchan, nil); } void soundproc(void *) { Biobuf *aout; uchar buf[1024]; threadsetname("soundproc"); aout = Bopen("/dev/audio", OWRITE); if(aout == nil) sysfatal("Bopen: %r"); for(;;){ processaudio((void*)buf, sizeof(buf)/2); Bwrite(aout, buf, sizeof buf); } } void timerproc(void *) { Vfx *vfx; uvlong t0, Δt, φt, acc; int refresh; threadsetname("timer"); t0 = nsec(); acc = 0; for(;;){ Δt = nsec() - t0; φt = Δt/1000000ULL; acc += Δt; refresh = 0; for(vfx = vfxqueue.next; vfx != &vfxqueue; vfx = vfx->next){ vfx->step(vfx, φt); refresh = 1; } if(refresh) nbsend(drawchan, nil); if(gamestate == Waiting0 && acc >= 5*SEC){ chanprint(egress, "watch\n"); chanprint(egress, "stats\n"); acc = 0; } t0 += Δt; sleep(HZ2MS(20)); } } void netrecvthread(void *arg) { Ioproc *io; char buf[256], *e; int n, tot, fd; fd = *(int*)arg; io = ioproc(); tot = 0; while((n = ioread(io, fd, buf+tot, sizeof(buf)-1-tot)) > 0){ tot += n; buf[tot] = 0; while((e = strchr(buf, '\n')) != nil){ *e++ = 0; processcmd(buf); tot -= e-buf; memmove(buf, e, tot); } if(tot >= sizeof(buf)-1) tot = 0; } closeioproc(io); threadexits(nil); } void netsendthread(void *arg) { char *s; int fd; fd = *(int*)arg; while(recv(egress, &s) > 0){ if(write(fd, s, strlen(s)) != strlen(s)) break; if(debug) fprint(2, "sent '%s'\n", s); free(s); } threadexits(nil); } void usage(void) { fprint(2, "usage: %s [-ds] addr\n", argv0); threadexitsall("usage"); } void threadmain(int argc, char *argv[]) { char aux[64]; char *addr; char *user; int fd, cfd; Mousectl *mc; Keyboardctl *kc; Rune r; GEOMfmtinstall(); ARGBEGIN{ case 'd': debug++; break; case 's': silent++; break; default: usage(); }ARGEND if(argc != 1) usage(); snprint(winspec, sizeof winspec, "-dx %d -dy %d", SCRW, SCRH); if(debug && newwindow(winspec) < 0) sysfatal("newwindow: %r"); if(initdraw(nil, deffont, "bts") < 0) sysfatal("initdraw: %r"); if((mc = initmouse(nil, screen)) == nil) sysfatal("initmouse: %r"); if((kc = initkeyboard(nil)) == nil) sysfatal("initkeyboard: %r"); display->locking = 1; unlockdisplay(display); resize(); mctl = mc; kctl = kc; if((user = getenv("user")) == nil) user = getuser(); snprint(uid, sizeof uid, "%s", user); screenb = eallocimage(display, Rect(0,0,SCRW,SCRH), screen->chan, 0, DNofill); worldrf.p = Pt2(0,0,1); worldrf.bx = Vec2(1,0); worldrf.by = Vec2(0,1); snprint(aux, sizeof aux, "%s/%s", assetdir, titlefontpath); titlefont = openfont(display, aux); if(titlefont == nil) sysfatal("openfont: %r"); initpalette(); inittiles(); initmainbtns(); initboards(); initarmada(); initvfx(); matches = newmenulist(14*font->height, "ongoing matches"); gamestate = Waiting0; if(!silent){ initsfx(); proccreate(soundproc, nil, mainstacksize); } addr = netmkaddr(argv[0], "tcp", "3047"); if(debug) fprint(2, "connecting to %s\n", addr); fd = dial(addr, nil, nil, &cfd); if(fd < 0) sysfatal("dial: %r"); else if(debug) fprint(2, "line established\n"); drawchan = chancreate(sizeof(void*), 1); reconnc = chancreate(sizeof(void*), 1); ingress = chancreate(sizeof(char*), 8); egress = chancreate(sizeof(char*), 8); threadcreate(netrecvthread, &fd, mainstacksize); threadcreate(netsendthread, &fd, mainstacksize); nbsend(drawchan, nil); proccreate(timerproc, nil, mainstacksize); enum { MOUSE, RESIZE, KEYS, DRAW, RECONN, NONE }; Alt a[] = { [MOUSE] {mc->c, &mc->Mouse, CHANRCV}, [RESIZE] {mc->resizec, nil, CHANRCV}, [KEYS] {kc->c, &r, CHANRCV}, [DRAW] {drawchan, nil, CHANRCV}, [RECONN] {reconnc, nil, CHANRCV}, [NONE] {nil, nil, CHANEND} }; for(;;) switch(alt(a)){ case MOUSE: mouse(mc); break; case RESIZE: resize(); break; case KEYS: key(r); break; case DRAW: redraw(); break; case RECONN: if(debug) fprint(2, "reconnecting to %s\n", addr); write(cfd, "close", 5); close(cfd); close(fd); fd = dial(addr, nil, nil, &cfd); if(fd < 0) sysfatal("dial: %r"); else if(debug) fprint(2, "line established\n"); threadcreate(netrecvthread, &fd, mainstacksize); threadcreate(netsendthread, &fd, mainstacksize); resetgame(); nbsend(drawchan, nil); break; default: sysfatal("input thread interrupted"); } }