// Hyperbolic Rogue -- multiplayer features // Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details /** \file multi.cpp * \brief multiplayer features, also input configuration */ #include "hyper.h" namespace hr { EX namespace multi { #if HDR static constexpr int SCANCODES = 512; static constexpr int MAXJOY = 8; static constexpr int MAXBUTTON = 64; static constexpr int MAXAXE = 16; static constexpr int MAXHAT = 4; struct config { char keyaction[SCANCODES]; char joyaction[MAXJOY][MAXBUTTON]; char axeaction[MAXJOY][MAXAXE]; char hataction[MAXJOY][MAXHAT][4]; int deadzoneval[MAXJOY][MAXAXE]; }; #endif EX config scfg_default; EX charstyle scs[MAXPLAYER]; EX bool split_screen; EX bool pvp_mode; EX bool friendly_fire = true; EX bool self_hits; EX bool two_focus; EX int players = 1; EX cellwalker player[MAXPLAYER]; EX vector revive_queue; // queue for revival EX cell *origpos[MAXPLAYER], *origtarget[MAXPLAYER]; EX bool flipped[MAXPLAYER]; // treasure collection, kill, and death statistics EX int treasures[MAXPLAYER], kills[MAXPLAYER], deaths[MAXPLAYER], pkills[MAXPLAYER], suicides[MAXPLAYER]; EX bool alwaysuse = false; EX void recall() { for(int i=0; itype; cell *c2 = cwt.at->move(idir); makeEmpty(c2); if(!passable(c2, NULL, P_ISPLAYER)) c2 = cwt.at; multi::player[i].at = c2; multi::player[i].spin = 0; multi::flipped[i] = true; multi::whereto[i].d = MD_UNDECIDED; } } EX shiftmatrix whereis[MAXPLAYER]; EX shiftmatrix crosscenter[MAXPLAYER]; EX double ccdist[MAXPLAYER]; EX cell *ccat[MAXPLAYER]; bool combo[MAXPLAYER]; EX int cpid; // player id -- an extra parameter for player-related functions EX int cpid_edit; // cpid currently being edited EX movedir whereto[MAXPLAYER]; // player's target cell EX double mdx[MAXPLAYER], mdy[MAXPLAYER]; // movement vector for the next move static constexpr int CMDS = 15; static constexpr int CMDS_PAN = 11; vector playercmds_shmup = { "forward", "backward", "turn left", "turn right", "move up", "move right", "move down", "move left", "throw a knife", "face the pointer", "throw at the pointer", "drop Dead Orb", "center the map on me", "Orb power (target: mouse)", "Orb power (target: facing)" }; vector playercmds_shmup3 = { "rotate up", "rotate down", "rotate left", "rotate right", "move forward", "strafe right", "move backward", "strafe left", "throw a knife", "face the pointer", "throw at the pointer", "drop Dead Orb", "center the map on me", "Orb power (target: mouse)", "Orb power (target: facing)" }; vector playercmds_turn = { "move up-right", "move up-left", "move down-right", "move down-left", "move up", "move right", "move down", "move left", "stay in place (left + right)", "cancel move", "leave the game", "drop Dead Orb (up + down)", "center the map on me", "", "" }; vector pancmds = { "pan up", "pan right", "pan down", "pan left", "rotate left", "rotate right", "home", "world overview", "review your quest", "inventory", "main menu" }; vector pancmds3 = { "look up", "look right", "look down", "look left", "rotate left", "rotate right", "home", "world overview", "review your quest", "inventory", "main menu", "scroll forward", "scroll backward" }; #if HDR #define SHMUPAXES_BASE 4 #define SHMUPAXES ((SHMUPAXES_BASE) + 4 * (MAXPLAYER)) #define SHMUPAXES_CUR ((SHMUPAXES_BASE) + 4 * playercfg) #endif EX const char* axemodes[SHMUPAXES] = { "do nothing", "rotate view", "panning X", "panning Y", "player 1 X", "player 1 Y", "player 1 go", "player 1 spin", "player 2 X", "player 2 Y", "player 2 go", "player 2 spin", "player 3 X", "player 3 Y", "player 3 go", "player 3 spin", "player 4 X", "player 4 Y", "player 4 go", "player 4 spin", "player 5 X", "player 5 Y", "player 5 go", "player 5 spin", "player 6 X", "player 6 Y", "player 6 go", "player 6 spin", "player 7 X", "player 7 Y", "player 7 go", "player 7 spin" }; EX const char* axemodes3[4] = { "do nothing", "camera forward", "camera rotate X", "camera rotate Y" }; EX int centerplayer = -1; char* axeconfigs[24]; int numaxeconfigs; int* dzconfigs[24]; string listkeys(config& scfg, int id) { #if CAP_SDL string lk = ""; for(int i=0; i& shmupcmdtable; string caption; int setwhat; config *which_config; key_configurer(int sc, vector& sct, const string& caption, config& w) : sc(sc), shmupcmdtable(sct), caption(caption), setwhat(0), which_config(&w) { } void operator() () { dialog::init(caption); getcstat = ' '; for(int i=0; i1 && i == (setwhat&15) ? '?' : 0) : 'a'+i); else dialog::addBreak(100); if(setwhat == 1) dialog::addItem(XLAT("press a key to unassign"), 0); else if(setwhat) dialog::addItem(XLAT("press a key for '%1'", XLAT(shmupcmdtable[setwhat&15])), 0); else dialog::addItem(XLAT("unassign a key"), 'z'); dialog::display(); keyhandler = [this] (int sym, int uni) { if(!setwhat) dialog::handleNavigation(sym, uni); if(sym) { if(setwhat) { int scan = key_to_scan(sym); if(scan >= 0 && scan < SCANCODES) which_config->keyaction[scan] = setwhat; setwhat = 0; } else if(uni >= 'a' && uni < 'a' + isize(shmupcmdtable) && shmupcmdtable[uni-'a'][0]) setwhat = 16*sc+uni - 'a'; else if(uni == 'z') setwhat = 1; else if(doexiton(sym, uni)) popScreen(); } }; #if CAP_SDLJOY joyhandler = [this] (SDL_Event& ev) { if(ev.type == SDL_JOYBUTTONDOWN && setwhat) { int joyid = ev.jbutton.which; int button = ev.jbutton.button; if(joyid < 8 && button < 32) which_config->joyaction[joyid][button] = setwhat; setwhat = 0; return true; } else if(ev.type == SDL_JOYHATMOTION && setwhat) { int joyid = ev.jhat.which; int hat = ev.jhat.hat; int dir = 4; if(ev.jhat.value == SDL_HAT_UP) dir = 0; if(ev.jhat.value == SDL_HAT_RIGHT) dir = 1; if(ev.jhat.value == SDL_HAT_DOWN) dir = 2; if(ev.jhat.value == SDL_HAT_LEFT) dir = 3; printf("%d %d %d\n", joyid, hat, dir); if(joyid < 8 && hat < 4 && dir < 4) { which_config->hataction[joyid][hat][dir] = setwhat; setwhat = 0; return true; } } return false; }; #endif } }; EX reaction_t get_key_configurer(int sc, vector& sct, string caption) { return key_configurer(sc, sct, caption, scfg_default); } EX reaction_t get_key_configurer(int sc, vector& sct, string caption, config &cfg) { return key_configurer(sc, sct, caption, cfg); } EX reaction_t get_key_configurer(int sc, vector& sct) { return key_configurer(sc, sct, sc == 1 ? XLAT("configure player 1") : sc == 2 ? XLAT("configure player 2") : sc == 3 ? XLAT("configure panning") : sc == 4 ? XLAT("configure player 3") : sc == 5 ? XLAT("configure player 4") : sc == 6 ? XLAT("configure player 5") : sc == 7 ? XLAT("configure player 6") : sc == 8 ? XLAT("configure player 7") : "", scfg_default ); } #if CAP_SDLJOY struct joy_configurer { bool shmupcfg, racecfg; int playercfg; config& scfg; joy_configurer(int playercfg, config& scfg) : playercfg(playercfg), scfg(scfg) {} void operator() () { dialog::init(); getcstat = ' '; numaxeconfigs = 0; for(int j=0; j 10000) buf += "+", y -= 10000; while(y < -10000) buf += "-", y += 10000; if(y>0) buf += "+"; if(y<0) buf += "-"; } axeconfigs[numaxeconfigs] = &(scfg.axeaction[j][ax]); dzconfigs[numaxeconfigs] = &(scfg.deadzoneval[j][ax]); char aa = *axeconfigs[numaxeconfigs]; string what = configdead ? its(scfg.deadzoneval[j][ax]) : (GDIM == 3 && (aa%SHMUPAXES < 4)) ? XLAT(axemodes3[aa%SHMUPAXES]) : XLAT(axemodes[aa%SHMUPAXES]); dialog::addSelItem(XLAT("Joystick %1, axis %2", cts('A'+j), its(ax)) + buf, what, 'a'+numaxeconfigs); numaxeconfigs++; } } dialog::addBoolItem(XLAT("Configure dead zones"), (configdead), 'z'); dialog::display(); keyhandler = [this] (int sym, int uni) { dialog::handleNavigation(sym, uni); if(sym) { char xuni = uni | 96; if(xuni >= 'a' && xuni < 'a' + numaxeconfigs) { if(configdead) dialog::editNumber( (*dzconfigs[xuni - 'a']), 0, 65536, 100, 0, XLAT("Configure dead zones"), ""); else { int v = (*axeconfigs[xuni - 'a']); v += (shiftmul>0?1:-1); v += SHMUPAXES_CUR; v %= SHMUPAXES_CUR; (*axeconfigs[xuni - 'a']) = v; } } else if(xuni == 'z') configdead = !configdead; else if(doexiton(sym, uni)) popScreen(); } }; } }; #endif EX const char *axmodes[7] = {"OFF", "auto", "light", "heavy", "arrows", "WASD keys", "VI keys"}; struct shmup_configurer { void operator()() { #if CAP_SDL cmode = sm::SHMUPCONFIG | sm::SIDE | sm::DARKEN; gamescreen(); dialog::init(XLAT("keyboard & joysticks")); bool haveconfig = shmup::on || players > 1 || multi::alwaysuse; if(haveconfig) dialog::addItem(XLAT("configure player 1"), '1'); else dialog::addBreak(100); if(players > 1) dialog::addItem(XLAT("configure player 2"), '2'); else if(players == 1 && !shmup::on) dialog::addSelItem(XLAT("input"), multi::alwaysuse ? XLAT("config") : XLAT("default"), 'a'); else dialog::addBreak(100); if(players > 2) dialog::addItem(XLAT("configure player 3"), '3'); #if CAP_SDLJOY else if(!haveconfig) dialog::addItem(XLAT("old style joystick configuration"), 'b'); #endif else dialog::addBreak(100); if(players > 3) dialog::addItem(XLAT("configure player 4"), '4'); else if(!shmup::on && !multi::alwaysuse) { dialog::addBoolItem(XLAT("smooth scrolling"), smooth_scrolling, 'c'); } else if(alwaysuse) dialog::addInfo(XLAT("note: configured input is designed for")); else dialog::addBreak(100); if(players > 4) dialog::addItem(XLAT("configure player 5"), '5'); else if(!shmup::on && !multi::alwaysuse) { if(GDIM == 2) { dialog::addSelItem(XLAT("help for keyboard users"), XLAT(axmodes[vid.axes]), 'h'); dialog::add_action([] {vid.axes += 70 + (shiftmul > 0 ? 1 : -1); vid.axes %= 7; } ); } else dialog::addBreak(100); } else if(alwaysuse) dialog::addInfo(XLAT("multiplayer and shmup mode; some features")); else dialog::addBreak(100); if(players > 5) dialog::addItem(XLAT("configure player 6"), '6'); else if(alwaysuse) dialog::addInfo(XLAT("work worse if you use it.")); else dialog::addBreak(100); if(players > 6) dialog::addItem(XLAT("configure player 7"), '7'); else dialog::addBreak(100); if(shmup::on || multi::alwaysuse || players > 1) dialog::addItem(XLAT("configure panning and general keys"), 'p'); else dialog::addBreak(100); #if CAP_SDLJOY if(numsticks > 0) { if(shmup::on || multi::alwaysuse || players > 1) dialog::addItem(XLAT("configure joystick axes"), 'j'); else dialog::addBreak(100); } #endif dialog::addBreak(50); dialog::addHelp(); dialog::addBack(); dialog::display(); keyhandler = [this] (int sym, int uni) { return handleConfig(sym, uni); }; #endif } void handleConfig(int sym, int uni) { auto& cmdlist = shmup::on ? (WDIM == 3 ? playercmds_shmup3 : playercmds_shmup) : playercmds_turn; dialog::handleNavigation(sym, uni); if(0) ; #if CAP_SDL else if(uni == '1') pushScreen(get_key_configurer(1, cmdlist)); else if(uni == '2') pushScreen(get_key_configurer(2, cmdlist)); else if(uni == 'p') pushScreen(get_key_configurer(3, GDIM == 3 ? pancmds3 : pancmds)); else if(uni == '3') pushScreen(get_key_configurer(4, cmdlist)); else if(uni == '4') pushScreen(get_key_configurer(5, cmdlist)); else if(uni == '5') pushScreen(get_key_configurer(6, cmdlist)); else if(uni == '6') pushScreen(get_key_configurer(7, cmdlist)); else if(uni == '7') pushScreen(get_key_configurer(8, cmdlist)); #if CAP_SDLJOY else if(uni == 'j') pushScreen(joy_configurer(players, scfg_default)); #endif else if(uni == 'a') multi::alwaysuse = !multi::alwaysuse; #if CAP_SDLJOY else if(uni == 'b') pushScreen(showJoyConfig); #endif else if(uni == 'c') smooth_scrolling = !smooth_scrolling; #endif else if(doexiton(sym, uni)) popScreen(); } }; EX void configure() { pushScreen(shmup_configurer()); } EX void showConfigureMultiplayer() { cmode = sm::SIDE | sm::MAYDARK; gamescreen(); dialog::init("multiplayer"); for(int i=1; i <= MAXPLAYER; i++) { string s = player_count_name(i); if(i <= players) s += dsc(i-1); dialog::addBoolItem(s, i == multi::players, '0' + i); if(!dual::state) dialog::add_action([i] { dialog::do_if_confirmed([i] { stop_game(); players = i; if(multi::players > 1 && !shmup::on) bow::weapon = bow::wBlade; start_game(); }); }); } add_edit(self_hits); if(multi::players > 1) { dialog::addItem(XLAT("reset per-player statistics"), 'r'); dialog::add_action([] { for(int i=0; i= 0 && id < NUMACT) actionspressed[id]++; } EX int key_to_scan(int sym) { #if CAP_SDL2 return SDL_GetScancodeFromKey(sym); #else return sym; #endif } EX bool notremapped(int sym) { auto& scfg = scfg_default; int sc = key_to_scan(sym); if(sc < 0 || sc >= SCANCODES) return true; int k = scfg.keyaction[sc]; if(k == 0) return true; k /= 16; if(k > 3) k--; else if(k==3) k = 0; return k > multi::players; } EX void sconfig_savers(config& scfg, string prefix) { // unfortunately we cannot use key names here because SDL is not yet initialized for(int i=0; ieditable("split screen mode", 's'); param_b(multi::pvp_mode, "pvp_mode", false) ->editable("player vs player", 'v'); param_b(multi::friendly_fire, "friendly_fire", true) ->editable("friendly fire", 'f'); param_b(multi::self_hits, "self_hits", false) ->editable("self hits", 'h'); param_b(multi::two_focus, "two_focus", false) ->editable("auto-adjust dual-focus projections", 'f'); addsaver(alwaysuse, "use configured keys"); for(int i=0; i<7; i++) addsaver(multi::scs[i], "player"+its(i)); sconfig_savers(scfg, ""); #endif } EX void get_actions(config& scfg) { #if !ISMOBILE const Uint8 *keystate = SDL12_GetKeyState(NULL); for(int i=0; i dz) value -= dz; else if(value < -dz) value += dz; else value = 0; axespressed[scfg.axeaction[j][b] % SHMUPAXES] += value; } } #endif #endif } EX void handleInput(int delta, config &scfg) { #if CAP_SDL double d = delta / 500.; get_actions(scfg); const Uint8 *keystate = SDL12_GetKeyState(NULL); if(keystate[SDL12(SDLK_LCTRL, SDL_SCANCODE_LCTRL)] || keystate[SDL12(SDLK_RCTRL, SDL_SCANCODE_RCTRL)]) d /= 5; double panx = actionspressed[49] - actionspressed[51] + axespressed[2] / 32000.0; double pany = actionspressed[50] - actionspressed[48] + axespressed[3] / 32000.0; double panspin = actionspressed[52] - actionspressed[53]; double panmove = actionspressed[59] - actionspressed[60]; if(GDIM == 3) panmove += axespressed[1] / 32000.0; else panspin += axespressed[1] / 32000.0; if(actionspressed[54]) { centerplayer = -1, playermoved = true; centerpc(100); } if(actionspressed[55] && !lactionpressed[55]) get_o_key().second(); if(actionspressed[56] && !lactionpressed[56]) showMissionScreen(); #if CAP_INV if(actionspressed[57] && !lactionpressed[57] && inv::on) pushScreen(inv::show); #endif if(actionspressed[58] && !lactionpressed[58]) pushScreen(showGameMenu); panx *= d; pany *= d; panspin *= d; panmove *= d; #if CAP_MOUSEGRAB if(lctrlclick) { panx += mouseaim_x / 2; pany += mouseaim_y / 2; mouseaim_x = mouseaim_y = 0; } #endif if(panx || pany || panspin || (GDIM == 3 && panmove)) { if(GDIM == 2) { View = xpush(-panx) * ypush(-pany) * spin(panspin) * View; playermoved = false; } else { View = cspin(0, 2, -panx) * cspin(1, 2, -pany) * spin(panspin) * cpush(2, panmove) * View; if(panmove) playermoved = false; } } #endif } EX void handleInput(int delta) { handleInput(delta, scfg_default); } EX int tableid[7] = {1, 2, 4, 5, 6, 7, 8}; EX void leaveGame(int i) { multi::player[i].at = NULL; multi::deaths[i]++; revive_queue.push_back(i); checklastmove(); } EX bool playerActive(int p) { if(multi::players == 1 || shmup::on) return true; return player[p].at; } EX int activePlayers() { int q = 0; for(int i=0; i= 0) cwti = cwti + dir + wstep; return cwti.at; } EX void checklastmove() { for(int i: player_indices()) { multi::cpid = i; cwt = multi::player[i]; break; } if(multi::activePlayers() == 1) { multi::checkonly = true; checkmove(); multi::checkonly = false; } } bool needinput = true; EX void handleMulti(int delta) { multi::handleInput(delta); shiftmatrix bcwtV = cwtV; cellwalker bcwt = cwt; bool alldecided = !needinput; if(multi::players == 1) { multi::cpid = 0; multi::whereis[0] = cwtV; multi::player[0] = cwt; } for(int i: player_indices()) { using namespace multi; // todo refactor cpid = i; int b = 16*tableid[cpid]; for(int ik=0; ik<8; ik++) if(actionspressed[b+ik]) playermoved = true; for(int ik=0; ik<16; ik++) if(actionspressed[b+ik] && !lactionpressed[b+ik]) multi::combo[i] = false; bool anypressed = false; int jb = 4*tableid[cpid]; for(int ik=0; ik<4; ik++) if(axespressed[jb+ik]) anypressed = true, playermoved = true, multi::combo[i] = false; double mdx = (actionspressed[b+0] + actionspressed[b+2] - actionspressed[b+1] - actionspressed[b+3]) * .7 + actionspressed[b+pcMoveRight] - actionspressed[b+pcMoveLeft] + axespressed[jb]/30000.; double mdy = (actionspressed[b+3] + actionspressed[b+2] - actionspressed[b+1] - actionspressed[b+0]) * .7 + actionspressed[b+pcMoveDown] - actionspressed[b+pcMoveUp] + axespressed[jb+1]/30000.; if((actionspressed[b+pcMoveRight] && actionspressed[b+pcMoveLeft]) || (actionspressed[b+pcMoveUp] && actionspressed[b+pcMoveDown])) multi::mdx[i] = multi::mdy[i] = 0; multi::mdx[i] = multi::mdx[i] * (1 - delta / 1000.) + mdx * delta / 2000.; multi::mdy[i] = multi::mdy[i] * (1 - delta / 1000.) + mdy * delta / 2000.; if(WDIM == 2) { if(mdx != 0 || mdy != 0) if(!multi::combo[i]) { cwtV = multi::whereis[i]; cwt = multi::player[i]; flipplayer = multi::flipped[i]; multi::whereto[i] = vectodir(hpxy(multi::mdx[i], multi::mdy[i])); } } if(multi::actionspressed[b+pcFire] || (multi::actionspressed[b+pcMoveLeft] && multi::actionspressed[b+pcMoveRight])) multi::combo[i] = true, multi::whereto[i].d = MD_WAIT; if(multi::actionspressed[b+pcFace]) multi::whereto[i].d = MD_UNDECIDED; cwt.at = multi::player[i].at; if(multi::ccat[i] && !multi::combo[i] && targetRangedOrb(multi::ccat[i], roMultiCheck)) { multi::whereto[i].d = MD_USE_ORB; multi::whereto[i].tgt = multi::ccat[i]; } if(multi::actionspressed[b+pcFaceFire] && activePlayers() > 1) { addMessage(XLAT("Left the game.")); multi::leaveGame(i); } if(actionspressed[b+pcDrop] || (multi::actionspressed[b+pcMoveUp] && multi::actionspressed[b+pcMoveDown])) multi::combo[i] = true, multi::whereto[i].d = MD_DROP; if(actionspressed[b+pcCenter]) { centerplayer = cpid; centerpc(100); playermoved = true; } if(multi::whereto[i].d == MD_UNDECIDED) alldecided = false; for(int ik=0; ik<16; ik++) if(actionspressed[b+ik]) anypressed = true; if(anypressed) alldecided = false, needinput = false; else multi::mdx[i] = multi::mdy[i] = 0; } cwtV = bcwtV; cwt = bcwt; if(alldecided) { flashMessages(); // check for crashes needinput = true; for(int i: player_indices()) { origpos[i] = player[i].at; origtarget[i] = multiPlayerTarget(i); } for(int i: player_indices()) for(int j: player_indices()) if(i != j) { if(origtarget[i] == origtarget[j]) { addMessage("Two players cannot move/attack the same location!"); return; } /* if(multiPlayerTarget(i) == multi::player[j].at) { addMessage("Cannot move into the current location of another player!"); return; } if(celldistance(multiPlayerTarget(i), multiPlayerTarget(j)) > 8) { addMessage("Players cannot get that far away!"); return; } */ } if(multi::players == 1) { if(movepcto(multi::whereto[0])) multi::whereto[0].d = MD_UNDECIDED; return; } multi::cpid = 0; if(multimove()) { multi::aftermove = false; if(shmup::delayed_safety) { activateSafety(shmup::delayed_safety_land); shmup::delayed_safety = false; checklastmove(); } else { monstersTurn(); checklastmove(); } } } } EX void mousemovement(cell *c) { if(!c) return; int countplayers = 0; int countplayers_undecided = 0; for(int i=0; i 0 && ! isUndecided) continue; if(playerpos(i) == c) multi::whereto[i].d = MD_WAIT; else { for(int d=0; dtype; d++) { cdir = d; if(multi::multiPlayerTarget(i) == c) break; cdir = scdir; cwt = multi::player[i]; calcMousedest(); auto& sd = multi::whereto[i].subdir; sd = mousedest.subdir; if(sd == 0) sd = 1; } } } needinput = ((countplayers == 2 && !countplayers_undecided) || countplayers_undecided >= 2); } EX } }