mirror of
https://github.com/zenorogue/hyperrogue.git
synced 2024-11-23 13:07:16 +00:00
1128 lines
34 KiB
C++
1128 lines
34 KiB
C++
// 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 {
|
|
int keyaction[SCANCODES];
|
|
int joyaction[MAXJOY][MAXBUTTON];
|
|
int axeaction[MAXJOY][MAXAXE];
|
|
int 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<int> 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; i<numplayers(); i++) {
|
|
int idir = (3 * i) % cwt.at->type;
|
|
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<string> 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<string> 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<string> 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<string> pancmds = {
|
|
"pan up", "pan right", "pan down", "pan left",
|
|
"rotate left", "rotate right", "home",
|
|
"world overview", "review your quest", "inventory", "main menu"
|
|
};
|
|
|
|
vector<string> 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;
|
|
|
|
int* axeconfigs[24]; int numaxeconfigs;
|
|
int* dzconfigs[24];
|
|
|
|
string listkeys(config& scfg, int id) {
|
|
#if CAP_SDL
|
|
string lk = "";
|
|
for(int i=0; i<SCANCODES; i++)
|
|
if(scfg.keyaction[i] == id)
|
|
#if CAP_SDL2
|
|
lk = lk + " " + SDL_GetScancodeName(SDL_Scancode(i));
|
|
#else
|
|
lk = lk + " " + SDL_GetKeyName(SDLKey(i));
|
|
#endif
|
|
#if CAP_SDLJOY
|
|
for(int i=0; i<numsticks; i++) for(int k=0; k<SDL_JoystickNumButtons(sticks[i]) && k<MAXBUTTON; k++)
|
|
if(scfg.joyaction[i][k] == id) {
|
|
lk = lk + " " + cts('A'+i)+"-B"+its(k);
|
|
}
|
|
for(int i=0; i<numsticks; i++) for(int k=0; k<SDL_JoystickNumHats(sticks[i]) && k<MAXHAT; k++)
|
|
for(int d=0; d<4; d++)
|
|
if(scfg.hataction[i][k][d] == id) {
|
|
lk = lk + " " + cts('A'+i)+"-"+"URDL"[d];
|
|
}
|
|
#endif
|
|
return lk;
|
|
#else
|
|
return "";
|
|
#endif
|
|
}
|
|
|
|
#define SCJOY 16
|
|
|
|
string dsc(int id) {
|
|
string buf = XLAT(" (%1 $$$, %2 kills, %3 deaths)",
|
|
its(multi::treasures[id]),
|
|
its(multi::kills[id]),
|
|
its(multi::deaths[id])
|
|
);
|
|
if(friendly_fire)
|
|
buf += XLAT(" (%1 pkills)", its(multi::pkills[id]));
|
|
if(self_hits)
|
|
buf += XLAT(" (%1 self)", its(multi::suicides[id]));
|
|
return buf;
|
|
}
|
|
|
|
EX void resetScores() {
|
|
for(int i=0; i<MAXPLAYER; i++)
|
|
multi::treasures[i] = multi::kills[i] = multi::deaths[i] = multi::pkills[i] = multi::suicides[i] = 0;
|
|
}
|
|
|
|
bool configdead;
|
|
|
|
void handleConfig(int sym, int uni);
|
|
|
|
EX string player_count_name(int p) {
|
|
return
|
|
p == 2 ? XLAT("two players") :
|
|
p == 3 ? XLAT("three players") :
|
|
p == 4 ? XLAT("four players") :
|
|
p == 5 ? XLAT("five players") :
|
|
p == 6 ? XLAT("six players") :
|
|
p == 7 ? XLAT("seven players") :
|
|
XLAT("one player");
|
|
}
|
|
|
|
struct key_configurer {
|
|
|
|
int sc;
|
|
vector<string>& shmupcmdtable;
|
|
string caption;
|
|
int setwhat;
|
|
config *which_config;
|
|
|
|
key_configurer(int sc, vector<string>& 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; i<isize(shmupcmdtable); i++) if(shmupcmdtable[i][0])
|
|
dialog::addSelItem(XLAT(shmupcmdtable[i]), listkeys(*which_config, 16*sc+i),
|
|
setwhat ? (setwhat>1 && 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<string>& sct, string caption) {
|
|
return key_configurer(sc, sct, caption, scfg_default);
|
|
}
|
|
|
|
EX reaction_t get_key_configurer(int sc, vector<string>& sct, string caption, config &cfg) {
|
|
return key_configurer(sc, sct, caption, cfg);
|
|
}
|
|
|
|
EX reaction_t get_key_configurer(int sc, vector<string>& 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<numsticks; j++) {
|
|
for(int ax=0; ax<SDL_JoystickNumAxes(sticks[j]) && ax < MAXAXE; ax++) if(numaxeconfigs<24) {
|
|
int y = SDL_JoystickGetAxis(sticks[j], ax);
|
|
string buf = " ";
|
|
if(configdead)
|
|
buf += its(y);
|
|
else {
|
|
while(y > 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<MAXPLAYER; i++)
|
|
kills[i] = deaths[i] = treasures[i] = 0;
|
|
});
|
|
|
|
dialog::addSelItem(XLAT("keyboard & joysticks"), "", 'k');
|
|
dialog::add_action(multi::configure);
|
|
add_edit(split_screen);
|
|
if(shmup::on && !racing::on) {
|
|
add_edit(pvp_mode);
|
|
add_edit(friendly_fire);
|
|
if(pvp_mode)
|
|
dialog::addInfo(XLAT("PvP grants infinite lives -- achievements disabled"));
|
|
else if(friendly_fire)
|
|
dialog::addInfo(XLAT("friendly fire off -- achievements disabled"));
|
|
else if(split_screen)
|
|
dialog::addInfo(XLAT("achievements disabled in split screen"));
|
|
else
|
|
dialog::addBreak(100);
|
|
}
|
|
else {
|
|
dialog::addInfo(XLAT("PvP available only in shmup"));
|
|
dialog::addBreak(400);
|
|
}
|
|
if(multi::players == 2 && !split_screen)
|
|
add_edit(two_focus);
|
|
else
|
|
dialog::addBreak(100);
|
|
}
|
|
else dialog::addBreak(600);
|
|
|
|
dialog::addBack();
|
|
dialog::display();
|
|
}
|
|
|
|
#if HDR
|
|
#define NUMACT 128
|
|
|
|
enum pcmds {
|
|
pcForward, pcBackward, pcTurnLeft, pcTurnRight,
|
|
pcMoveUp, pcMoveRight, pcMoveDown, pcMoveLeft,
|
|
pcFire, pcFace, pcFaceFire,
|
|
pcDrop, pcCenter, pcOrbPower, pcOrbKey
|
|
};
|
|
#endif
|
|
|
|
EX int actionspressed[NUMACT], axespressed[SHMUPAXES], lactionpressed[NUMACT];
|
|
|
|
void pressaction(int id) {
|
|
if(id >= 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; i<SCANCODES; i++)
|
|
param_i(scfg.keyaction[i], prefix + string("key:")+its(i));
|
|
|
|
for(int i=0; i<MAXJOY; i++) {
|
|
string pre = prefix + "joystick "+cts('A'+i);
|
|
for(int j=0; j<MAXBUTTON; j++)
|
|
param_i(scfg.joyaction[i][j], pre+"-B"+its(j));
|
|
for(int j=0; j<MAXAXE; j++) {
|
|
param_i(scfg.axeaction[i][j], pre+" axis "+its(j));
|
|
param_i(scfg.deadzoneval[i][j], pre+" deadzone "+its(j));
|
|
}
|
|
for(int j=0; j<MAXHAT; j++) for(int k=0; k<4; k++) {
|
|
param_i(scfg.hataction[i][j][k], pre+" hat "+its(j)+" "+"URDL"[k]);
|
|
}
|
|
}
|
|
}
|
|
|
|
EX void clear_config(config& scfg) {
|
|
for(int i=0; i<SCANCODES; i++) scfg.keyaction[i] = 0;
|
|
}
|
|
|
|
EX void initConfig() {
|
|
auto& scfg = scfg_default;
|
|
|
|
int* t = scfg.keyaction;
|
|
|
|
#if CAP_SDL2
|
|
|
|
t[SDL_SCANCODE_W] = 16 + 4;
|
|
t[SDL_SCANCODE_D] = 16 + 5;
|
|
t[SDL_SCANCODE_S] = 16 + 6;
|
|
t[SDL_SCANCODE_A] = 16 + 7;
|
|
|
|
t[SDL_SCANCODE_KP_8] = 16 + 4;
|
|
t[SDL_SCANCODE_KP_6] = 16 + 5;
|
|
t[SDL_SCANCODE_KP_2] = 16 + 6;
|
|
t[SDL_SCANCODE_KP_4] = 16 + 7;
|
|
|
|
t[SDL_SCANCODE_F] = 16 + pcFire;
|
|
t[SDL_SCANCODE_G] = 16 + pcFace;
|
|
t[SDL_SCANCODE_H] = 16 + pcFaceFire;
|
|
t[SDL_SCANCODE_R] = 16 + pcDrop;
|
|
t[SDL_SCANCODE_T] = 16 + pcOrbPower;
|
|
t[SDL_SCANCODE_Y] = 16 + pcCenter;
|
|
|
|
t[SDL_SCANCODE_I] = 32 + 4;
|
|
t[SDL_SCANCODE_L] = 32 + 5;
|
|
t[SDL_SCANCODE_K] = 32 + 6;
|
|
t[SDL_SCANCODE_J] = 32 + 7;
|
|
t[SDL_SCANCODE_SEMICOLON] = 32 + 8;
|
|
t[SDL_SCANCODE_APOSTROPHE] = 32 + 9;
|
|
t[SDL_SCANCODE_P] = 32 + 10;
|
|
t[SDL_SCANCODE_LEFTBRACKET] = 32 + pcCenter;
|
|
|
|
t[SDL_SCANCODE_UP] = 48 ;
|
|
t[SDL_SCANCODE_RIGHT] = 48 + 1;
|
|
t[SDL_SCANCODE_DOWN] = 48 + 2;
|
|
t[SDL_SCANCODE_LEFT] = 48 + 3;
|
|
t[SDL_SCANCODE_PAGEUP] = 48 + 4;
|
|
t[SDL_SCANCODE_PAGEDOWN] = 48 + 5;
|
|
t[SDL_SCANCODE_HOME] = 48 + 6;
|
|
|
|
#else
|
|
t[(int)'w'] = 16 + 4;
|
|
t[(int)'d'] = 16 + 5;
|
|
t[(int)'s'] = 16 + 6;
|
|
t[(int)'a'] = 16 + 7;
|
|
|
|
#if !ISMOBILE
|
|
t[SDLK_KP8] = 16 + 4;
|
|
t[SDLK_KP6] = 16 + 5;
|
|
t[SDLK_KP2] = 16 + 6;
|
|
t[SDLK_KP4] = 16 + 7;
|
|
#endif
|
|
|
|
t[(int)'f'] = 16 + pcFire;
|
|
t[(int)'g'] = 16 + pcFace;
|
|
t[(int)'h'] = 16 + pcFaceFire;
|
|
t[(int)'r'] = 16 + pcDrop;
|
|
t[(int)'t'] = 16 + pcOrbPower;
|
|
t[(int)'y'] = 16 + pcCenter;
|
|
|
|
t[(int)'i'] = 32 + 4;
|
|
t[(int)'l'] = 32 + 5;
|
|
t[(int)'k'] = 32 + 6;
|
|
t[(int)'j'] = 32 + 7;
|
|
t[(int)';'] = 32 + 8;
|
|
t[(int)'\''] = 32 + 9;
|
|
t[(int)'p'] = 32 + 10;
|
|
t[(int)'['] = 32 + pcCenter;
|
|
|
|
#if !ISMOBILE
|
|
t[SDLK_UP] = 48 ;
|
|
t[SDLK_RIGHT] = 48 + 1;
|
|
t[SDLK_DOWN] = 48 + 2;
|
|
t[SDLK_LEFT] = 48 + 3;
|
|
t[SDLK_PAGEUP] = 48 + 4;
|
|
t[SDLK_PAGEDOWN] = 48 + 5;
|
|
t[SDLK_HOME] = 48 + 6;
|
|
#endif
|
|
#endif
|
|
|
|
scfg.joyaction[0][0] = 16 + pcFire;
|
|
scfg.joyaction[0][1] = 16 + pcOrbPower;
|
|
scfg.joyaction[0][2] = 16 + pcDrop;
|
|
scfg.joyaction[0][3] = 16 + pcCenter;
|
|
scfg.joyaction[0][4] = 16 + pcFace;
|
|
scfg.joyaction[0][5] = 16 + pcFaceFire;
|
|
|
|
scfg.joyaction[1][0] = 32 + pcFire;
|
|
scfg.joyaction[1][1] = 32 + pcOrbPower;
|
|
scfg.joyaction[1][2] = 32 + pcDrop;
|
|
scfg.joyaction[1][3] = 32 + pcCenter;
|
|
scfg.joyaction[1][4] = 32 + pcFace;
|
|
scfg.joyaction[1][5] = 32 + pcFaceFire;
|
|
|
|
scfg.axeaction[0][0] = 4;
|
|
scfg.axeaction[0][1] = 5;
|
|
scfg.axeaction[0][3] = 2;
|
|
scfg.axeaction[0][4] = 3;
|
|
|
|
scfg.axeaction[1][0] = 8;
|
|
scfg.axeaction[1][1] = 9;
|
|
|
|
// ULRD
|
|
scfg.hataction[0][0][0] = 16 + 0;
|
|
scfg.hataction[0][0][1] = 16 + 3;
|
|
scfg.hataction[0][0][2] = 16 + 1;
|
|
scfg.hataction[0][0][3] = 16 + 2;
|
|
scfg.hataction[0][1][0] = 16 + 4;
|
|
scfg.hataction[0][1][1] = 16 + 7;
|
|
scfg.hataction[0][1][2] = 16 + 5;
|
|
scfg.hataction[0][1][3] = 16 + 6;
|
|
|
|
scfg.hataction[1][0][0] = 32 + 0;
|
|
scfg.hataction[1][0][1] = 32 + 3;
|
|
scfg.hataction[1][0][2] = 32 + 1;
|
|
scfg.hataction[1][0][3] = 32 + 2;
|
|
scfg.hataction[1][1][0] = 32 + 4;
|
|
scfg.hataction[1][1][1] = 32 + 7;
|
|
scfg.hataction[1][1][2] = 32 + 5;
|
|
scfg.hataction[1][1][3] = 32 + 6;
|
|
|
|
int charidtable[MAXPLAYER] = {0, 1, 4, 6, 2, 3, 8};
|
|
|
|
for(int i=0; i<MAXPLAYER; i++) {
|
|
initcs(multi::scs[i]);
|
|
multi::scs[i].charid = charidtable[i];
|
|
}
|
|
|
|
multi::scs[0].uicolor = 0xC00000FF;
|
|
multi::scs[1].uicolor = 0x00C000FF;
|
|
multi::scs[2].uicolor = 0x0000C0FF;
|
|
multi::scs[3].uicolor = 0xC0C000FF;
|
|
multi::scs[4].uicolor = 0xC000C0FF;
|
|
multi::scs[5].uicolor = 0x00C0C0FF;
|
|
multi::scs[6].uicolor = 0xC0C0C0FF;
|
|
|
|
set_char_by_name(multi::scs[2], "rudy");
|
|
set_char_by_name(multi::scs[5], "princess");
|
|
set_char_by_name(multi::scs[4], "worker");
|
|
multi::scs[4].skincolor = 0x303030FF;
|
|
multi::scs[1].haircolor = 0x40FF40FF;
|
|
|
|
#if CAP_CONFIG
|
|
param_i(multi::players, "mode-number of players")->be_non_editable();
|
|
param_b(multi::split_screen, "splitscreen", false)
|
|
->editable("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');
|
|
param_b(alwaysuse, "use configured keys");
|
|
|
|
for(int i=0; i<7; i++) paramset(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<NUMACT; i++)
|
|
lactionpressed[i] = actionspressed[i],
|
|
actionspressed[i] = 0;
|
|
|
|
for(int i=0; i<SHMUPAXES; i++) axespressed[i] = 0;
|
|
|
|
for(int i=0; i<KEYSTATES; i++) if(keystate[i])
|
|
pressaction(scfg.keyaction[i]);
|
|
|
|
#if CAP_SDLJOY
|
|
for(int j=0; j<numsticks; j++) {
|
|
|
|
for(int b=0; b<SDL_JoystickNumButtons(sticks[j]) && b<MAXBUTTON; b++)
|
|
if(SDL_JoystickGetButton(sticks[j], b))
|
|
pressaction(scfg.joyaction[j][b]);
|
|
|
|
for(int b=0; b<SDL_JoystickNumHats(sticks[j]) && b<MAXHAT; b++) {
|
|
int stat = SDL_JoystickGetHat(sticks[j], b);
|
|
if(stat & SDL_HAT_UP) pressaction(scfg.hataction[j][b][0]);
|
|
if(stat & SDL_HAT_RIGHT) pressaction(scfg.hataction[j][b][1]);
|
|
if(stat & SDL_HAT_DOWN) pressaction(scfg.hataction[j][b][2]);
|
|
if(stat & SDL_HAT_LEFT) pressaction(scfg.hataction[j][b][3]);
|
|
}
|
|
|
|
for(int b=0; b<SDL_JoystickNumAxes(sticks[j]) && b<MAXAXE; b++) {
|
|
int value = SDL_JoystickGetAxis(sticks[j], b);
|
|
int dz = scfg.deadzoneval[j][b];
|
|
if(value > 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<players; i++) if(playerActive(i)) q++;
|
|
return q;
|
|
}
|
|
|
|
EX cell *multiPlayerTarget(int i) {
|
|
cellwalker cwti = multi::player[i];
|
|
if(!cwti.at) return NULL;
|
|
int dir = multi::whereto[i].d;
|
|
if(dir == MD_UNDECIDED) return NULL;
|
|
if(dir == MD_USE_ORB) return multi::whereto[i].tgt;
|
|
if(dir >= 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<multi::players; i++)
|
|
if(multi::playerActive(i) && (playerpos(i) == c || isNeighbor(c, playerpos(i)))) {
|
|
countplayers++;
|
|
if(multi::whereto[i].d == MD_UNDECIDED) countplayers_undecided++;
|
|
}
|
|
|
|
for(int i=0; i<multi::players; i++)
|
|
if(multi::playerActive(i) && (playerpos(i) == c || isNeighbor(c, playerpos(i)))) {
|
|
int& cdir = multi::whereto[i].d;
|
|
int scdir = cdir;
|
|
bool isUndecided = cdir == MD_UNDECIDED;
|
|
if(countplayers_undecided > 0 && ! isUndecided) continue;
|
|
if(playerpos(i) == c)
|
|
multi::whereto[i].d = MD_WAIT;
|
|
else {
|
|
for(int d=0; d<playerpos(i)->type; 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 }
|
|
|
|
}
|