// Copyright (c) 1999 Philip A. Hardin (pahardin@cs.utexas.edu) // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License v2 or later. #include // to get tolower() #include // to get strncpy() #include // to get INT_MAX, LONG_MAX #include #include // to get XK_* #include // ugh, this baby's big #include "player.h" #include "xform.h" const coord COORD_MAX= LONG_MAX, HOMESPACING= 8; // spacing 'twixt home positions bool player::autoPilotAllowed= true; bool player::autoGunnerAllowed= true; bool player::pauseAllowed= true; bool player::paused= false; bool player::shadowsOn= true; bool player::mayShootBallDefault= true; bool player::mayShootVhclDefault= true; int player::defaultAccuracy= 4; int player::defaultFrequency= 3; bool player::refitWindow= true; int player::shellsPerPlayer= 3; /*-------------------------------------------------------------------------*/ // In: newDispName = X display name for a human player, or // "comp" or "comp..." for a computer player player::player(const char *newDispName, int newTeamNum, int newMemberNum, bool useGL) : gt(useGL), teamNum(newTeamNum), memberNum(newMemberNum), gunnerAccuracy(defaultAccuracy), gunnerFrequency(defaultFrequency), mayShootBall(mayShootBallDefault), mayShootVhcl(mayShootVhclDefault), train(NULL) { int len= strlen(newDispName); if (len >MAXDISPCHARS) len= MAXDISPCHARS; strncpy(dispName,newDispName,len); dispName[len]= '\0'; if (strncmp(dispName,"comp",4)==0 and len <=7) { computerPlayer= true; autoPilot= autoGunner= true; active= false; char *s= dispName +4; if (*s >='0' and *s <='9') gunnerAccuracy= *(s++) - '0'; if (*s >='0' and *s <='9') gunnerFrequency= *(s++) - '0'; if (*s) { char c= tolower(*s); mayShootBall= (c=='b' or c=='a'); mayShootVhcl= (c=='v' or c=='a'); if (c != 'b' and c != 'v' and c != 'a' and c != 'n') cerr << "bad auto gunner option '" << c << "': use (a)ll, (b)all, (v)ehicle, or (n)one" << endl; } } else { computerPlayer= false; autoPilot= autoGunner= false; active= true; } } /*------------------------------------------------------------------------*/ // Set the defaults for the auto-gunner. When players are created, their // auto-gunners get whatever the current defaults are. void player::SetGunnerDefaults(const char* s) { if (*s >='0' and *s <='9') defaultAccuracy= *(s++) - '0'; if (*s >='0' and *s <='9') defaultFrequency= *(s++) - '0'; if (*s) { char c= tolower(*s); mayShootBallDefault= (c=='b' or c=='a'); mayShootVhclDefault= (c=='v' or c=='a'); if (c != 'b' and c != 'v' and c != 'a' and c != 'n') cerr << "bad auto gunner option '" << c << "': use (a)ll, (b)all, (v)ehicle, or (n)one" << endl; } } /*-------------------------------------------------------------------------*/ // Init this player for the start of a new game round. void player::InitForRound(gobList& gobs, coord hqDist, int numTeams, gobList* track) { if (teamNum != -1) { homePos.Ang()= -MA_PI/2 + (teamNum * 2*MA_PI)/numTeams; homePos.Cart()= pt3d(hqDist-2*HOMESPACING, (1+memberNum)*HOMESPACING); if (memberNum%2 == 0) homePos.Cart().y= -homePos.Cart().y -HOMESPACING; homePos.Cart() >>= homePos.Ang(); homePos.Ang() += MA_PI; vhcl= gobs.insert(gobs.end(),new tankGob(homePos,pt3d(0,0),teamNum)); } else { homePos= pt3d(0,0,0); vhcl= gobs.insert(gobs.end(), new tranGob(homePos,pt3d(0,0,0),teamNum,track)); } usingHqView= false; ammo= shellsPerPlayer; statusNeedsRedrawing= true; statusShowingVhcl= true; } /*-------------------------------------------------------------------------*/ void player::Draw(gobList& gobs, team teams[], int playerNum, hrznGob* horizon, void (*DrawTeams)(bbGfxTarget&)) { gt.DoubleBufferBegin(); DrawView(gobs,teams,playerNum,horizon); DrawStatus(teams,DrawTeams); gt.DoubleBufferEnd(); } /*-------------------------------------------------------------------------*/ // Draw the 3-D view for this player. void player::DrawView(gobList& gobs, team teams[], int playerNum, hrznGob *horizon) { tmtrx viewPos= Vhcl()->ViewPos(); // both cartesian & angular position pt3d eyePos= viewPos.Cart(); // just cartesian position if (usingHqView) { eyePos= teams[teamNum].hq->ViewPos().Cart(); pt3d vhclCenter= pt3d(0,0,Vhcl()->Shape()->Box().Center().z) >> Vhcl()->WorldPos(); viewPos= tcomp(eyePos,(vhclCenter -eyePos).Ang3d()); } // set the horizon's gob's "position" to maintain illusion of a horizon :-) horizon->SetWorldPos(viewPos); // sort all the non-part gobs by distance from viewer gobList::iterator gi; vector refs; for_(gi,gobs) refs.push_back(gobRef(*gi,(**gi).DistFromViewer(eyePos))); ::stable_sort(refs.begin(),refs.end()); // now insert gob parts into the list, in correct drawing order gobList allGobs; vector::iterator gr; for_(gr,refs) (**gr).GetViewOrder(eyePos,allGobs); Vhcl()->EyeIsInside(not usingHqView); // compute the world-to-view transformation tmtrx worldToView= viewPos.Inverse(); // Map "view" coords to "window" coords (this is a bit kludgey) // Map x to z, y to x, and z to y // Also, scale the final x & y to do some of the 3D->2D screen projection double sc= gt.gfxSize.x; tmtrx::fourby3 viewToWindow= {{0,0,1},{-sc,0,0},{0,-sc,0},{0,0,0}}; if (gt.UseGL()) { // For openGL, reverse window y and reverse window z // Also, don't do the window "pre-scaling" viewToWindow[0][2]= -1; viewToWindow[1][0]= -1; viewToWindow[2][1]= 1; } worldToView= worldToView * tmtrx(viewToWindow); if (not horizon->Solid()) gt.ClearGfx(); // set up view frustum pt3d frustumNormals[4]; frustumNormals[0]= (pt3d(1, 2, 0) >> viewPos) -viewPos.Cart(); frustumNormals[1]= (pt3d(1,-2, 0) >> viewPos) -viewPos.Cart(); frustumNormals[2]= (pt3d(1, 0, 10./3) >> viewPos) -viewPos.Cart(); frustumNormals[3]= (pt3d(1, 0,-10./3) >> viewPos) -viewPos.Cart(); frustumNormals[0].Normalize(); frustumNormals[1].Normalize(); frustumNormals[2].Normalize(); frustumNormals[3].Normalize(); // draw the horizon, then the playfield (they're the last two gobRefs) pt3d eye= viewPos.Cart(); gi= allGobs.end(); (**(--gi)).Draw(gt,worldToView,playerNum,eye); // horizon (**(--gi)).Draw(gt,worldToView,playerNum,eye); // playfield // Next, draw any shadows ulong tempColors[totalColors]; if (shadowsOn) { fori(totalColors) tempColors[i]= gt.Colors()[c_gray20]; gobList::iterator savedGi= gi; while (gi != allGobs.begin()) { gi--; rgn3 *shadow= (**gi).Shadow(); if (shadow) { // project flattened shadow onto the ground! tmtrx pos= (**gi).WorldPos(); pos[0][2] *= 1e-6; // I'm worried about making a "singular" mtrx pos[1][2] *= 1e-6; // here, so I don't quite zero this column... pos[2][2] *= 1e-6; pos[3][2]= 0; //pos.UnitScaling()= false; if (shadow->InFrustum(eye,frustumNormals,pos)) { view v; v.Update(shadow,pos*worldToView); gt.SetForeground(gt.Colors()[c_gray20]); shadow->Draw(v.pts, gt, eye<0) { gi--; if ((**gi).Shape()->InFrustum(eye,frustumNormals,(**gi).WorldPos())) { const ulong *colors= gt.Colors(); if ((**gi).TeamNum() != -1) { colors= tempColors; fori(totalColors) tempColors[i]= gt.Colors()[teams[(**gi).TeamNum()].colorNums[i]]; } (**gi).Draw(gt,worldToView,playerNum,eye,colors); } } Vhcl()->EyeIsInside(false); } /*-------------------------------------------------------------------------*/ void player::CloseXStuff() { XUnloadFont(gt.disp,gt.font->fid); XFreePixmap(gt.disp,gt.pm); XFreeGC(gt.disp,gt.gc); XFreeGC(gt.disp,gt.pmgc); XCloseDisplay(gt.disp); active= false; } /*-------------------------------------------------------------------------*/ // Handle a key press event. void player::HandleKeyPress(KeySym k, bool pressed, gobList& gobs, tranGob* bbTrain) { gobList::iterator gi; switch(k) { case XK_a: // toggle player/autonomous pilot if (pressed and autoPilotAllowed) { autoPilot= not autoPilot; statusNeedsRedrawing= true; if (not autoPilot) Vhcl()->ctrl= pt3d(0,0); } break; case XK_g: // toggle player/autonomous gunner if (pressed and autoGunnerAllowed) { autoGunner= not autoGunner; statusNeedsRedrawing= true; } break; case XK_p: if (pressed and pauseAllowed) { paused= not paused; statusNeedsRedrawing= true; } break; case XK_q: case XK_Escape: if (!pressed) CloseXStuff(); break; case XK_v: if (pressed) { usingHqView= not usingHqView; statusNeedsRedrawing= true; } break; case XK_Insert: if (pressed) { if (train) { train->teamNum= -1; train= NULL; } else { if (bbTrain and bbTrain->TeamNum()==-1) { train= bbTrain; train->teamNum= teamNum; } } } break; case XK_Tab: if (pressed) { statusShowingVhcl= not statusShowingVhcl; statusNeedsRedrawing= true; } break; default: if (not paused) { if (train==NULL) gobs.Update(Vhcl()->Control(k,pressed,&ammo),vhcl); else Vhcl()->Control(k,pressed,&ammo); } break; } } /*-------------------------------------------------------------------------*/ // Handle an operating system event for this player. void player::HandleEvents(gobList& gobs, tranGob* bbTrain) { XEvent event; KeySym k; while (active and XPending(gt.disp) >0) { XNextEvent(gt.disp, &event); switch (event.type) { case Expose: statusNeedsRedrawing= true; break; case KeyPress: k= XLookupKeysym((XKeyEvent *)(&event),0); HandleKeyPress(k,true,gobs,bbTrain); break; case KeyRelease: k= XLookupKeysym((XKeyEvent *)(&event),0); HandleKeyPress(k,false,gobs,bbTrain); break; case ConfigureNotify: gt.HandleResize(&event,refitWindow); break; } } } /*-------------------------------------------------------------------------*/ // Return a ptr to the team that this player wants to attack. team* player::TeamToAttack(int numTeams,team teams[],const gob& ball) { coord teamHqDist= COORD_MAX; team *teamToAttack= NULL; forii(numTeams) if (i != teamNum and (teamToAttack==NULL or teams[i].score > teamToAttack->score or (teams[i].score==teamToAttack->score and (ball.pos.Cart() -teams[i].hq->pos.Cart()).FastDist() pos.Cart()).FastDist(); } return teamToAttack ? teamToAttack : &teams[0]; } /*-------------------------------------------------------------------------*/ // Decide the player's current destination, depending on which team he's // trying to attack and where the ball is. void player::GetDest(const team& teamToGet, const gob& ball, pt2d& dest, pt2d& ballToHq) { pt2d ballPos, hqPos, vhclPos, vhclToBall; coord ballToDestDist; ballPos= (pt2d)(ball.pos.Cart()); hqPos= (pt2d)(teamToGet.hq->pos.Cart()); vhclPos= (pt2d)(Vhcl()->pos.Cart()); vhclToBall= ballPos -vhclPos; ballToDestDist= vhclToBall.FastDist()*0.68; if (ballToDestDist >16) ballToDestDist= 16; ballToHq= (hqPos -ballPos).Normalized(); if (vhclToBall.Dot(ballToHq) >0) dest= ballPos -ballToHq*ballToDestDist; else { pt2d toLeft= vhclToBall.Normalized(ballToDestDist).Left90(); if (toLeft.Dot(ballToHq) <0) dest= ballPos +toLeft; else dest= ballPos -toLeft; } } /*-------------------------------------------------------------------------*/ // Control or partially control the player's vehicle using the auto-pilot // and/or the auto-gunner. void player::AutoPlay(gobList& gobs,int numTeams,team teams[],gob& ball) { team *teamToAttack= TeamToAttack(numTeams,teams,ball); // If this is the train player, but a real player currently has control of // the train, then don't do anything if (teamNum==-1 and Vhcl()->TeamNum() != teamNum) return; gob *target= NULL; if (autoGunner) { if (mayShootBall and Vhcl()->CanMakeGoal(*(teamToAttack->hq),ball)) target= &ball; else if (mayShootVhcl) target= Vhcl()->PreferredVhclTarget(ball.WorldPos().Cart()); } if (autoPilot) { pt3d dest; pt3d ballToHq; GetDest(*teamToAttack,ball,dest,ballToHq); dest.z= ball.Center().z*0.7; ballToHq.z= 0; // if we have a tgt, and if this vhcl must turn itself in order to aim, if (target and Vhcl()->Shape()== &heliRgn) { // if the dest is far away and the target is almost in line with it, pt3d selfToTgt= target->pos.Cart() - Vhcl()->pos.Cart(); pt3d selfToDest= dest - Vhcl()->pos.Cart(); if (selfToDest.Dist() >25 and selfToDest.Normalized().Dot(selfToTgt.Normalized()) >0.95) { // then the target becomes the destination dest= target->pos.Cart(); ballToHq= pt3d(0,0,0); } } tcomp selfToDest; Vhcl()->Control(dest,ballToHq,selfToDest); } // The auto gunner may override some of the auto pilot's decisions, // so the auto gunner needs to be invoked after the auto pilot. if (autoGunner) Vhcl()->Control(&ammo,target,gunnerAccuracy,gunnerFrequency); } /*-------------------------------------------------------------------------*/ // Draw this player's status in the text subwindow. void player::DrawStatus(const team teams[],void (*ShowTeams)(bbGfxTarget&)) { statusNeedsRedrawing= true; // legacy stuff... if (Vhcl()->newlyCreated) { statusNeedsRedrawing= true; Vhcl()->newlyCreated= false; } if (statusNeedsRedrawing) gt.ClearStatus(); if (not statusShowingVhcl) { if (statusNeedsRedrawing) { ShowTeams(gt); statusNeedsRedrawing= false; } return; } // Draw Vehicle Status gt.cursor= pt2d(0,0); gt.rightmost= 0; const team& t= teams[TeamNum()]; Vhcl()->DrawStatus(gt,t.colorNums[c_teamPrimary], t.colorNums[c_teamSecondary],statusNeedsRedrawing); if (statusNeedsRedrawing) { // Draw Keyboard Controls gt.SetForeground(gt.Color(c_black)); gt.SetBackground(gt.Color(c_white)); XDrawLine(gt.disp,gt.win,gt.gc, 182, (int)(gt.gfxSize.y), 182, (int)(gt.gfxSize.y+gt.textSize.y-1)); pt2d temp(gt.rightmost+4,0); gt.cursor= temp; gt << " Keyboard Controls"; gt.rightmost= (int)temp.x; Vhcl()->DrawControls(gt); gt.Tab(1,3); gt << " Q V "; gt << "quit HQ "; gt << "game view "; gt.Box(1,3).Box(7,3,1,1,usingHqView); if (autoPilotAllowed) { gt.Tab(1,3); gt << " A"; gt << "auto"; gt << "pilot"; gt.Box(2,3,1,1,autoPilot); } if (autoGunnerAllowed) { gt.Tab(1,3); gt << " G"; gt << " auto"; gt << "gunner"; gt.Box(2,3,1,1,autoGunner); } if (pauseAllowed) { gt.Tab(1,3); gt << " P"; gt << "pause"; gt.Box(2,3,1,1,paused); } gt.Tab(1,3); gt << " Tab"; gt << "teams"; gt.Box(1,3,3); bool someIsHidden= gt.cursorOrg.x +gt.rightmost*gt.fontSize.x >gt.textSize.x; gt.Tab(3,1); gt << " BattleBall 2.1"; gt << " by"; gt << "Philip A. Hardin"; //gt << "www.cs.utexas.edu/users/pahardin/bb.html"; if (someIsHidden) { gt.cursor= pt2d((gt.textSize.x -gt.cursorOrg.x)/gt.fontSize.x -6.5,0); gt << "more->"; } statusNeedsRedrawing= false; } }