// Hyperbolic Rogue - PC movement // Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details /** \file pcmove.cpp * \brief PC movements */ #include "hyper.h" namespace hr { EX bool keepLightning = false; EX bool seenSevenMines = false; /** \brief have we been warned about the Haunted Woods? */ EX bool hauntedWarning; /** \brief is the Survivalist achievement still valid? have we received it? */ EX bool survivalist; EX void fail_survivalist() { changes.value_set(survivalist, false); } /** \brief last move was invisible */ EX bool invismove = false; /** \brief last move was invisible due to Orb of Fish (thus Fish still see you)*/ EX bool invisfish = false; /** \brief if false, make the PC look in direction cwt.spin (after attack); otherwise, make them look the other direction (after move) */ EX bool flipplayer = true; /** \brief Cellwalker describing the single player. Also used temporarily in shmup and multiplayer modes. */ EX cellwalker cwt; EX cell*& singlepos() { return cwt.at; } EX inline bool singleused() { return !(shmup::on || multi::players > 1); } /** \brief should we center the screen on the PC? */ EX bool playermoved = true; /** \brief did the player cheat? how many times? */ EX int cheater = 0; /** \brief lands visited -- unblock some modes */ EX bool landvisited[landtypes]; EX int noiseuntil; // noise until the given turn EX void createNoise(int t) { noiseuntil = max(noiseuntil, turncount+t); invismove = false; if(shmup::on) shmup::visibleFor(100 * t); } #if HDR enum eLastmovetype { lmSkip, lmMove, lmAttack, lmPush, lmTree, lmInstant }; extern eLastmovetype lastmovetype, nextmovetype; enum eForcemovetype { fmSkip, fmMove, fmAttack, fmInstant, fmActivate }; extern eForcemovetype forcedmovetype; #endif EX bool hit_anything; EX namespace orbbull { cell *prev[MAXPLAYER]; eLastmovetype prevtype[MAXPLAYER]; int count; bool is(cell *c1, cell *c2, cell *c3) { int lp = neighborId(c2, c1); int ln = neighborId(c2, c3); return lp >= 0 && ln >= 0 && anglestraight(c2, lp, ln); } EX void gainBullPowers() { items[itOrbShield]++; orbused[itOrbShield] = true; items[itOrbThorns]++; orbused[itOrbThorns] = true; items[itOrbHorns]++; orbused[itOrbHorns] = true; } EX void check() { int cp = multi::cpid; if(cp < 0 || cp >= MAXPLAYER) cp = 0; if(!items[itOrbBull]) { prev[cp] = NULL; return; } bool seq = false; if(prev[cp] && prevtype[cp] == lmMove && lastmovetype == lmMove) seq = is(prev[cp], lastmove, cwt.at); if(prev[cp] && prevtype[cp] == lmMove && lastmovetype == lmAttack) seq = is(prev[cp], cwt.at, lastmove); if(prev[cp] && prevtype[cp] == lmAttack && lastmovetype == lmAttack && count) seq = lastmove == prev[cp]; if(prev[cp] && prevtype[cp] == lmAttack && lastmovetype == lmMove && count) seq = cwt.at == prev[cp]; prev[cp] = lastmove; prevtype[cp] = lastmovetype; if(seq) { if(lastmovetype == lmMove) count++; gainBullPowers(); } else count = 0; } EX } bool pcmove::checkNeedMove(bool checkonly, bool attacking) { if(items[itOrbDomination] > ORBBASE && cwt.at->monst) return false; int flags = 0; bool drown = false; if(cwt.at->monst) { if(vmsg(miRESTRICTED, siMONSTER, cwt.at, cwt.at->monst)) { if(isMountable(cwt.at->monst)) addMessage(XLAT("You need to dismount %the1!", cwt.at->monst)); else addMessage(XLAT("You need to move to give space to %the1!", cwt.at->monst)); } } else if(cwt.at->wall == waRoundTable) { if(markOrb2(itOrbAether)) return false; if(vmsg(miRESTRICTED, siWALL, cwt.at, moNone)) addMessage(XLAT("It would be impolite to land on the table!")); } else if(cwt.at->wall == waLake) { drown = true; if(markOrb2(itOrbAether)) return false; if(markOrb2(itOrbFish)) return false; if(in_gravity_zone(cwt.at) && passable(cwt.at, NULL, P_ISPLAYER)) return false; flags |= AF_FALL; if(vmsg(miWALL, siWALL, cwt.at, moNone)) addMessage(XLAT("Ice below you is melting! RUN!")); } else if(!attacking && cellEdgeUnstable(cwt.at)) { if(markOrb2(itOrbAether)) return false; if(in_gravity_zone(cwt.at) && passable(cwt.at, NULL, P_ISPLAYER)) return false; if(vmsg(miRESTRICTED, siGRAVITY, cwt.at, moNone)) addMessage(XLAT("Nothing to stand on here!")); return true; } else if(among(cwt.at->wall, waSea, waCamelotMoat, waLake, waDeepWater)) { drown = true; if(markOrb(itOrbFish)) return false; if(markOrb2(itOrbAether)) return false; if(in_gravity_zone(cwt.at) && passable(cwt.at, NULL, P_ISPLAYER)) return false; if(vmsg(miWALL, siWALL, cwt.at, moNone)) addMessage(XLAT("You have to run away from the water!")); } else if(cwt.at->wall == waClosedGate) { if(markOrb2(itOrbAether)) return false; if(vmsg(miWALL, siWALL, cwt.at, moNone)) addMessage(XLAT("The gate is closing right on you! RUN!")); } else if(isFire(cwt.at) && !markOrb(itOrbWinter) && !markOrb(itCurseWater) && !markOrb2(itOrbShield)) { if(markOrb2(itOrbAether)) return false; if(vmsg(miWALL, siWALL, cwt.at, moNone)) addMessage(XLAT("This spot will be burning soon! RUN!")); } else if(cwt.at->wall == waMagma && !markOrb(itOrbWinter) && !markOrb(itCurseWater) && !markOrb2(itOrbShield)) { if(markOrb2(itOrbAether)) return false; if(in_gravity_zone(cwt.at) && passable(cwt.at, cwt.at, P_ISPLAYER)) return false; if(vmsg(miWALL, siWALL, cwt.at, moNone)) addMessage(XLAT("Run away from the lava!")); } else if(cwt.at->wall == waChasm) { if(markOrb2(itOrbAether)) return false; if(in_gravity_zone(cwt.at) && passable(cwt.at, cwt.at, P_ISPLAYER)) return false; flags |= AF_FALL; if(vmsg(miWALL, siWALL, cwt.at, moNone)) addMessage(XLAT("The floor has collapsed! RUN!")); } else if(items[itOrbAether] > ORBBASE && !passable(cwt.at, NULL, P_ISPLAYER | P_NOAETHER)) { if(markOrb2(itOrbAether)) return false; vmsg(miWALL, siWALL, cwt.at, moNone); return true; } else if(!passable(cwt.at, NULL, P_ISPLAYER)) { if(isFire(cwt.at)) return false; // already checked: have Shield if(markOrb2(itOrbAether)) return false; if(vmsg(miWALL, siWALL, cwt.at, moNone)) addMessage(XLAT("Your Aether power has expired! RUN!")); } else return false; if(hardcore && !checkonly) { if(cwt.at->monst) yasc_message = XLAT("did not leave %the1", cwt.at->monst); else if(cwt.at->wall == waChasm) yasc_message = XLAT("fell into a chasm"); else if(cwt.at->wall == waRoundTable) yasc_message = XLAT("died by politeness"); else if(cwt.at->wall == waClosedGate) yasc_message = XLAT("crushed by a gate"); else if(drown) yasc_message = XLAT("drowned in %the1", cwt.at->wall); else yasc_message = XLAT("did not leave %the1", cwt.at->wall); killHardcorePlayer(multi::cpid, flags); } return true; } EX cell *lastmove; eLastmovetype lastmovetype, nextmovetype; eForcemovetype forcedmovetype; #if HDR struct pcmove { bool switchplaces; bool checkonly; bool errormsgs; int origd; bool fmsMove, fmsAttack, fmsActivate; int d; int subdir; /** used to tell perform_actual_move() that this is a boat move and thus we should not pick up items */ bool boatmove; bool good_tortoise; flagtype attackflags; bool movepcto(); bool actual_move(); bool stay(); bool after_instant(bool kl); bool perform_actual_move(); bool after_move(); bool perform_move_or_jump(); bool swing(); bool boat_move(); bool after_escape(); bool move_if_okay(); bool attack(); bool checkNeedMove(bool checkonly, bool attacking); void tell_why_cannot_attack(); void tell_why_impassable(); void handle_friendly_ivy(); bool try_shooting(bool auto_target); movei mi, mip; pcmove() : mi(nullptr, nullptr, 0), mip(nullptr, nullptr, 0) {} bool vmsg(moveissue mi); bool vmsg(int code, int subissue_code, cell *where, eMonster m) { moveissue mi; mi.type = code; mi.subtype = subissue_code; mi.monster = m; mi.where = where; return vmsg(mi); } bool vmsg_threat() { return vmsg(miTHREAT, siMONSTER, who_kills_me_cell, who_kills_me); } }; #endif EX cell *global_pushto; bool pcmove::vmsg(moveissue mi) { checked_move_issue = mi; changes.rollback(); return errormsgs && !checkonly; } EX bool movepcto(int d, int subdir IS(1), bool checkonly IS(false)) { checked_move_issue.type = miVALID; pcmove pcm; pcm.checkonly = checkonly; pcm.d = d; pcm.subdir = subdir; auto b = pcm.movepcto(); global_pushto = pcm.mip.t; return b; } bool pcmove::try_shooting(bool auto_target) { hit_anything = false; if(auto_target) { auto b = bow::auto_path(); if(!b) { if(!isWall(cwt.peek())) { changes.rollback(); if(!checkonly) addMessage(XLAT("Cannot hit anything by shooting this direction!")); } return false; } addMessage(XLAT("Fire!")); } items[itCrossbow] = bow::loading_time(); cell *fst = cwt.peek(); if(bow::bowpath.size() >= 1) fst = bow::bowpath[1].prev.at; eMonster blocked = fst->monst; bow::shoot(); int v = -1; for(auto p: bow::bowpath) if(p.next.at == cwt.at && (p.flags & bow::bpFIRST)) v = p.next.spin; if(v >= 0) sideAttack(cwt.at, v, moPlayer, 0); if(items[itOrbGravity]) { gravity_state = get_static_gravity(cwt.at); if(gravity_state) markOrb(itOrbGravity); } if(againstRose(cwt.at, nullptr) && !scentResistant() && (againstRose(cwt.at, fst) || blocked == fst->monst)) { if(vmsg(miRESTRICTED, siROSE, nullptr, moNone)) { addMessage(XLAT("You cannot stay in place and shoot, those roses smell too nicely.") + its(celldistance(cwt.at, fst))); } return false; } if(cellEdgeUnstable(cwt.at) || cwt.at->land == laWhirlpool) { if(checkonly) return true; if(changes.on) changes.commit(); addMessage(XLAT("(shooting while unstable -- no turn passes)")); checkmove(); return true; } if(checkNeedMove(checkonly, false)) return false; swordAttackStatic(); nextmovetype = hit_anything ? lmAttack : lmSkip; lastmovetype = hit_anything ? lmAttack : lmSkip; lastmove = NULL; while(bow::rusalka_curses--) rusalka_curse(); mi = movei(cwt.at, STAY); if(last_gravity_state && !gravity_state) playerMoveEffects(mi); if(monstersnear_add_pmi(mi)) { if(vmsg_threat()) wouldkill("%The1 would catch you!"); return false; } if(checkonly) return true; if(changes.on) changes.commit(); if(cellUnstable(cwt.at) && !markOrb(itOrbAether)) doesFallSound(cwt.at); return after_move(); } bool pcmove::movepcto() { reset_spill(); if(dual::state == 1) return dual::movepc(d, subdir, checkonly); if(d >= 0 && !checkonly && subdir != 1 && subdir != -1) printf("subdir = %d\n", subdir); mip.t = NULL; switchplaces = false; warning_shown = false; suicidal = false; if(d == MD_USE_ORB) return targetRangedOrb(multi::whereto[multi::cpid].tgt, roMultiGo); errormsgs = multi::players == 1 || multi::cpid == multi::players-1; if(hardcore && !canmove) return false; if(!checkonly && d >= 0) { flipplayer = false; if(multi::players > 1) multi::flipped[multi::cpid] = false; } DEBBI(checkonly ? 0 : DF_TURN, ("movepc")); if(!checkonly) invismove = false; boatmove = false; if(multi::players > 1) lastmountpos[multi::cpid] = cwt.at; else lastmountpos[0] = cwt.at; bool fatigued = false; if(againstRose(cwt.at, NULL) && d<0 && !scentResistant()) { fatigued = items[itFatigue] >= 8; if(!fatigued) { if(vmsg(miRESTRICTED, siROSE, nullptr, moNone)) addMessage(XLAT("You just cannot stand in place, those roses smell too nicely.")); return false; } } gravity_state = gsNormal; fmsMove = forcedmovetype == fmSkip || forcedmovetype == fmMove; fmsAttack = forcedmovetype == fmSkip || forcedmovetype == fmAttack; fmsActivate = forcedmovetype == fmSkip || forcedmovetype == fmActivate; changes.init(checkonly); changes.value_keep(bow::bowpath_map); bow::bowpath_map.clear(); if(fatigued) addMessage(XLAT("The roses smell nicely, but you are just too tired to care.")); bool b = (d >= 0 && bow::fire_mode) ? false : (d >= 0) ? actual_move() : stay(); if(checkonly || !b) { changes.rollback(); if(!checkonly) flipplayer = false; if(!b && items[itCrossbow] == 0 && bow::crossbow_mode() && bow::bump_to_shoot && d >= 0 && !checkonly) { changes.init(checkonly); if(bow::fire_mode) { origd = d; cwt += d; mirror::act(d, mirror::SPINSINGLE); } changes.value_keep(bow::bowpath_map); b = try_shooting(true); if(checkonly || !b) changes.rollback(); } } else if(changes.on) { println(hlog, "error: not commited!"); changes.commit(); } if(b && !checkonly) bow::fire_mode = false; if(!b) { // bool try_instant = (forcedmovetype == fmInstant) || (forcedmovetype == fmSkip && !passable(c2, cwt.at, P_ISPLAYER | P_MIRROR | P_USEBOAT | P_FRIENDSWAP)); if(items[itOrbFlash]) { if(checkonly) { nextmovetype = lmInstant; return true; } if(warning_shown || orbProtection(itOrbFlash)) return true; activateFlash(); checkmove(); return true; } if(items[itOrbLightning]) { if(checkonly) { nextmovetype = lmInstant; return true; } if(warning_shown || orbProtection(itOrbLightning)) return true; activateLightning(); checkmove(); return true; } if(who_kills_me == moOutlaw && items[itRevolver] && !checkonly) { cell *c2 = cwt.cpeek(); forCellEx(c3, c2) forCellEx(c4, c3) if(c4->monst == moOutlaw) { eItem i = targetRangedOrb(c4, roCheck); if(i == itRevolver) { targetRangedOrb(c4, roKeyboard); return true; } } } } return b; } bool pcmove::after_move() { if(checkonly) return true; invisfish = false; if(items[itOrbFish]) { invisfish = true; for(cell *pc: player_positions()) if(!isWatery(pc)) invisfish = false; if(d < 0) invisfish = false; // no invisibility if staying still if(invisfish) invismove = true, markOrb(itOrbFish); } last_gravity_state = gravity_state; if(multi::players == 1) monstersTurn(); save_memory(); check_total_victory(); if(items[itWhirlpool] && cwt.at->land != laWhirlpool) achievement_gain_once("WHIRL1"); if(items[itLotus] >= 25 && !isHaunted(cwt.at->land) && survivalist) achievement_gain_once("SURVIVAL"); if(seenSevenMines && cwt.at->land != laMinefield) { changes.value_set(seenSevenMines, false); achievement_gain("SEVENMINE"); } DEBB(DF_TURN, ("done")); return true; } bool pcmove::swing() { sideAttack(cwt.at, d, moPlayer, 0); mirror::act(origd, mirror::SPINMULTI | mirror::ATTACK); if(monstersnear_add_pmi(movei(cwt.at, STAY))) { if(nextmovetype == lmAttack ? vmsg(miWALL, siWALL, mi.t, who_kills_me) : vmsg_threat()) wouldkill("You would be killed by %the1!"); return false; } if(checkonly) return true; if(changes.on) changes.commit(); animateAttack(mi, LAYER_SMALL); if(survivalist && isHaunted(mi.t->land)) survivalist = false; lastmovetype = lmTree; lastmove = mi.t; swordAttackStatic(); return after_move(); } bool pcmove::after_instant(bool kl) { changes.commit(); keepLightning = kl; bfs(); keepLightning = false; if(multi::players > 1) { multi::whereto[multi::cpid].d = MD_UNDECIDED; return false; } checkmove(); return true; } EX void copy_metadata(cell *x, const gcell *y) { x->wall = y->wall; x->monst = y->monst; x->item = y->item; x->mondir = y->mondir; x->stuntime = y->stuntime; x->hitpoints = y->hitpoints; x->monmirror = y->monmirror; x->LHU = y->LHU; if(isIcyLand(x)) { x->landparam = y->landparam; } x->wparam = y->wparam; } #if HDR extern void playSound(cell *c, const string& fname, int vol); /** \brief A structure to keep track of changes made during the player movement. * * This is a singleton object, \link hr::changes \endlink. */ struct changes_t { vector rollbacks; vector commits; bool on; bool checking; /** * \brief Start keeping track of changes, perform changes. * * init(false) if you intend to commit the changes (if successful), or * init(true) if you just want to check whether the move would be successful, * without performing it if it is. */ void init(bool ch) { on = true; ccell(cwt.at); forCellEx(c1, cwt.at) ccell(c1); value_keep(kills); value_keep(items); value_keep(orbused); value_keep(hrngen); checking = ch; } /** \brief Commit the changes. Should only be called after init(false). */ void commit() { on = false; for(auto& p: commits) p(); rollbacks.clear(); commits.clear(); } /** \brief Rollback the changes. */ void rollback(int pos = 0) { on = false; while(!rollbacks.empty()) { rollbacks.back()(); rollbacks.pop_back(); } rollbacks.clear(); commits.clear(); } /** \brief The changes to cell c will be rolled back when rollback() is called. */ void ccell(cell *c) { if(!on) return; gcell a = *c; rollbacks.push_back([c, a] { copy_metadata(c, &a); }); } /** \brief Set the value of what to value. This change will be rolled back if necessary. */ template void value_set(T& what, T value) { if(!on) { what = value; return; } if(what == value) return; T old = what; rollbacks.push_back([&what, old] { what = old; }); what = value; } /** \brief Add step to the value of what. This change will be rolled back if necessary. */ template void value_add(T& what, T step) { value_keep(what); what += step; } template void value_inc(T& what) { value_add(what, 1); } /** \brief Any change to the value of what will be rolled back if necessary. */ template void value_keep(T& what) { if(!on) return; T old = what; rollbacks.push_back([&what, old] { what = old; }); } /** \brief Like value_keep but for maps. */ template void map_value(map& vmap, V& key) { if(vmap.count(key)) { auto val = vmap[key]; at_rollback([&vmap, key, val] { vmap[key] = val; }); } else { at_rollback([&vmap, key] { vmap.erase(key); }); } } /** \brief Perform the given action on commit. @see LATE */ void at_commit(reaction_t act) { if(!on) act(); else commits.emplace_back(act); } /** \brief Perform the given action on rollback. */ void at_rollback(reaction_t act) { if(on) rollbacks.emplace_back(act); } void push_push(cell *tgt) { pushes.push_back(tgt); auto v = [] { pushes.pop_back(); }; rollbacks.push_back(v); commits.push_back(v); } }; #endif /** \brief The only instance of hr::changes_t */ EX changes_t changes; /** * Auxiliary function for hr::apply_chaos(). Returns whether the cell attribute LHU * should be switched. */ bool switch_lhu_in(eLand l) { return among(l, laBrownian, laMinefield, laTerracotta, laHive); } /** \brief how should be the direction from 'src' be mirrored to 'dst' */ EX int chaos_mirror_dir(int dir, cellwalker src, cellwalker dst) { if(dir >= dst.at->type) return dir; return (dst-src.to_spin(dir)).spin; } #if HDR template void swap_data(T& data, cell *c1, cell *c2) { changes.map_value(data, c1); changes.map_value(data, c2); if(data.count(c1) && data.count(c2)) swap(data[c1], data[c2]); else if(data.count(c1)) data[c2] = data[c1], data.erase(c1); else if(data.count(c2)) data[c1] = data[c2], data.erase(c2); } #endif /** \brief Apply the Orb of Chaos. * * We assume that the player moves from cwt.peek, in * in the direction given by cwt.spin. */ void apply_chaos() { auto wa = cwt+1+wstep; auto wb = cwt-1+wstep; cell *ca = wa.at; cell *cb = wb.at; if(dice::swap_forbidden(ca, cb)) return; if(dice::swap_forbidden(cb, ca)) return; if(!items[itOrbChaos] || chaos_forbidden(ca) || chaos_forbidden(cb)) return; if(ca && is_paired(ca->monst)) killMonster(ca, moPlayer); if(cb && is_paired(cb->monst)) killMonster(cb, moPlayer); destroyTrapsOn(ca); destroyTrapsOn(cb); if (ca->wall == waStone) destroyTrapsAround(ca); if (cb->wall == waStone) destroyTrapsAround(cb); changes.ccell(ca); changes.ccell(cb); /* needs to be called separately for Shadows */ if(ca->monst == moShadow) checkStunKill(ca); if(cb->monst == moShadow) checkStunKill(cb); gcell coa = *ca; gcell cob = *cb; if(ca->monst != cb->monst) markOrb(itOrbChaos); if(ca->wall != cb->wall) markOrb(itOrbChaos); if(ca->item != cb->item) markOrb(itOrbChaos); copy_metadata(ca, &cob); copy_metadata(cb, &coa); ca->item = coa.item; cb->item = cob.item; moveItem(ca, cb, false); if(!switch_lhu_in(ca->land)) ca->LHU = coa.LHU; if(!switch_lhu_in(cb->land)) cb->LHU = cob.LHU; if(ca->monst && !(isFriendly(ca) && markOrb(itOrbEmpathy))) { ca->stuntime = min(ca->stuntime + 3, 15), markOrb(itOrbChaos); checkStunKill(ca); } if(cb->monst && !(isFriendly(cb) && markOrb(itOrbEmpathy))) { cb->stuntime = min(cb->stuntime + 3, 15), markOrb(itOrbChaos); checkStunKill(cb); } ca->monmirror = !ca->monmirror; cb->monmirror = !cb->monmirror; ca->mondir = chaos_mirror_dir(ca->mondir, wb, wa); cb->mondir = chaos_mirror_dir(cb->mondir, wa, wb); if(isPrincess(ca) && !isPrincess(cb)) princess::move(movei{cb, ca, JUMP}); if(isPrincess(cb) && !isPrincess(ca)) princess::move(movei{ca, cb, JUMP}); if(ca->monst == moTortoise || cb->monst == moTortoise) { tortoise::move_adult(ca, cb); } if(dice::on(ca) || dice::on(cb)) { dice::chaos_swap(wa, wb); } } bool pcmove::actual_move() { origd = d; if(d >= 0) { cwt += d; dynamicval b(changes.on, false); mirror::act(d, mirror::SPINSINGLE); d = cwt.spin; } if(d != -1 && !checkonly) playermoved = true; mi = movei(cwt.at, d); cell *& c2 = mi.t; if(c2 == &out_of_bounds) return false; good_tortoise = c2->monst == moTortoise && tortoise::seek() && !tortoise::diff(tortoise::getb(c2)) && !c2->item; if(items[itOrbGravity]) { if(c2->monst && !should_switchplace(cwt.at, c2)) gravity_state = get_static_gravity(cwt.at); else gravity_state = get_move_gravity(cwt.at, c2); if(gravity_state) markOrb(itOrbGravity); } if(againstRose(cwt.at, c2) && !scentResistant()) { if(vmsg(miRESTRICTED, siROSE, nullptr, moNone)) addMessage("Those roses smell too nicely. You have to come towards them."); return false; } if(items[itOrbDomination] > ORBBASE && isMountable(c2->monst) && fmsMove) { if(checkonly) { nextmovetype = lmMove; return true; } if(!isMountable(cwt.at->monst)) dragon::target = NULL; movecost(cwt.at, c2, 3); flipplayer = true; if(multi::players > 1) multi::flipped[multi::cpid] = true; invismove = (turncount >= noiseuntil) && items[itOrbInvis] > 0; killFriendlyIvy(); return perform_move_or_jump(); } if(isActivable(c2) && fmsActivate) { if(checkonly) { nextmovetype = lmInstant; return true; } activateActiv(c2, true); return after_instant(false); } #if CAP_COMPLEX2 if(c2->monst == moAnimatedDie) { mip = determinePush(cwt, subdir, [] (movei mi) { return canPushThumperOn(mi, cwt.at); }); if(mip.proper()) { auto tgt = roll_effect(mip, dice::data[c2]); if(tgt.happy() > 0) { changes.ccell(c2); c2->monst = moNone; c2->wall = waRichDie; } else { if(vmsg(miWALL, siWALL, c2, c2->monst)) addMessage(XLAT("You can only push this die if the highest number would be on the top!")); return false; } } else if(mip.d == NO_SPACE) { if(vmsg(miWALL, siWALL, c2, c2->monst)) addMessage(XLAT("No room to push %the1.", c2->monst)); return false; } } #endif if(isPushable(c2->wall) && !c2->monst && !nonAdjacentPlayer(c2, cwt.at) && fmsMove) { mip = determinePush(cwt, subdir, [] (movei mi) { return canPushThumperOn(mi, cwt.at); }); if(mip.t) changes.ccell(mip.t); if(mip.d == NO_SPACE) { if(vmsg(miWALL, siWALL, c2, moNone)) addMessage(XLAT("No room to push %the1.", c2->wall)); return false; } nextmovetype = lmMove; addMessage(XLAT("You push %the1.", c2->wall)); lastmovetype = lmPush; lastmove = cwt.at; pushThumper(mip); changes.push_push(mip.t); return perform_actual_move(); } if(c2->item == itHolyGrail && roundTableRadius(c2) < newRoundTableRadius()) { if(vmsg(miRESTRICTED, siITEM, c2, moNone)) addMessage(XLAT("That was not a challenge. Find a larger castle!")); return false; } if(c2->item == itOrbYendor && !checkonly && !peace::on && !itemHiddenFromSight(c2) && yendor::check(c2)) { return false; } if(isWatery(c2) && !nonAdjacentPlayer(cwt.at,c2) && !c2->monst && cwt.at->wall == waBoat && fmsMove) return boat_move(); if(!c2->monst && cwt.at->wall == waBoat && cwt.at->item != itOrbYendor && boatGoesThrough(c2) && markOrb(itOrbWater) && !nonAdjacentPlayer(c2, cwt.at) && fmsMove) { if(c2->item && collectItem(c2, cwt.at)) return true; changes.ccell(c2); placeWater(c2, cwt.at); moveBoat(mi); boatmove = true; return perform_actual_move(); } return after_escape(); } void blowaway_message(cell *c2) { addMessage(airdist(c2) < 3 ? XLAT("The Air Elemental blows you away!") : XLAT("You cannot go against the wind!")); } EX void tortoise_hero_message(cell *c2) { bool fem = playergender() == GEN_F; playSound(c2, fem ? "heal-princess" : "heal-prince"); addMessage(fem ? XLAT("You are now a tortoise heroine!") : XLAT("You are now a tortoise hero!")); } bool pcmove::boat_move() { cell *& c2 = mi.t; if(againstWind(c2, cwt.at)) { if(vmsg(miRESTRICTED, siWIND, c2, moNone)) blowaway_message(c2); return false; } if(againstCurrent(c2, cwt.at) && !markOrb(itOrbWater)) { if(markOrb(itOrbFish) || markOrb(itOrbAether) || gravity_state) return after_escape(); if(vmsg(miRESTRICTED, siCURRENT, c2, moNone)) addMessage(XLAT("You cannot go against the current!")); return false; } if(cwt.at->item == itOrbYendor) { if(markOrb(itOrbFish) || markOrb(itOrbAether) || gravity_state) return after_escape(); if(vmsg(miRESTRICTED, siITEM, c2, moNone)) addMessage(XLAT("The Orb of Yendor is locked in with powerful magic.")); return false; } nextmovetype = lmMove; moveBoat(mi); boatmove = true; return perform_actual_move(); } void pcmove::tell_why_cannot_attack() { cell *& c2 = mi.t; if(c2->monst == moWorm || c2->monst == moWormtail || c2->monst == moWormwait) addMessage(XLAT("You cannot attack Sandworms directly!")); else if(c2->monst == moHexSnake || c2->monst == moHexSnakeTail) addMessage(XLAT("You cannot attack Rock Snakes directly!")); else if(nonAdjacentPlayer(c2, cwt.at)) addMessage(XLAT("You cannot attack diagonally!")); else if(thruVine(c2, cwt.at)) addMessage(XLAT("You cannot attack through the Vine!")); else if(c2->monst == moTentacle || c2->monst == moTentacletail || c2->monst == moTentaclewait || c2->monst == moTentacleEscaping) addMessage(XLAT("You cannot attack Tentacles directly!")); else if(c2->monst == moHedge && !markOrb(itOrbThorns)) { addMessage(XLAT("You cannot attack %the1 directly!", c2->monst)); if(bow::crossbow_mode()) addMessage(XLAT("Stab them by shooting around them.")); else addMessage(XLAT("Stab them by walking around them.")); } else if(c2->monst == moRoseBeauty || isBull(c2->monst) || c2->monst == moButterfly) addMessage(XLAT("You cannot attack %the1!", c2->monst)); else if(c2->monst == moFlailer && !c2->stuntime) { addMessage(XLAT("You cannot attack %the1 directly!", c2->monst)); addMessage(XLAT("Make him hit himself by walking away from him.")); } else if(c2->monst == moShadow) addMessage(XLAT("You cannot defeat the Shadow!")); else if(c2->monst == moGreater || c2->monst == moGreaterM) addMessage(XLAT("You cannot defeat the Greater Demon yet!")); else if(c2->monst == moDraugr) addMessage(XLAT("Your mundane weapon cannot hurt %the1!", c2->monst)); else if(isRaider(c2->monst)) addMessage(XLAT("You cannot attack Raiders directly!")); else if(isSwitch(c2->monst)) addMessage(XLAT("You cannot attack Jellies in their wall form!")); else if(c2->monst == moAngryDie) addMessage(XLAT("This die is really angry at you!")); else if((attackflags & AF_WEAK) && isIvy(c2)) addMessage(XLAT("You are too weakened to attack %the1!", c2->monst)); else if(isWorm(cwt.at->monst) && isWorm(c2->monst) && wormhead(cwt.at) == wormhead(c2) && cwt.at->monst != moTentacleGhost && c2->monst != moTentacleGhost) addMessage(XLAT("You cannot attack your own mount!")); else if(checkOrb(c2->monst, itOrbShield)) addMessage(XLAT("A magical shield protects %the1!", c2->monst)); else if(bow::crossbow_mode() && !bow::bump_to_shoot) addMessage(XLAT("You have no melee weapon!")); else if(bow::crossbow_mode() && items[itCrossbow]) addMessage(XLAT("Your crossbow is still reloading!")); else if(bow::crossbow_mode()) addMessage(XLAT("Trying to fire.")); else addMessage(XLAT("For some reason... cannot attack!")); } bool pcmove::after_escape() { cell*& c2 = mi.t; bool push_behind = c2->wall == waBigStatue || (among(c2->wall, waCTree, waSmallTree, waBigTree, waShrub, waVinePlant) && !c2->monst && markOrb(itOrbWoods)); if(thruVine(c2, cwt.at) && markOrb(itOrbWoods)) push_behind = true; if(push_behind && !c2->monst && !nonAdjacentPlayer(c2, cwt.at) && fmsMove) { eWall what = c2->wall; if(!thruVine(c2, cwt.at) && !canPushStatueOn(cwt.at, P_ISPLAYER)) { if(vmsg(miRESTRICTED, siWALL, c2, moNone)) { if(isFire(cwt.at)) addMessage(XLAT("You have to escape first!")); else addMessage(XLAT("There is not enough space!")); } return false; } changes.ccell(c2); changes.ccell(cwt.at); c2->wall = cwt.at->wall; c2->wparam = cwt.at->wparam; if(doesnotFall(cwt.at)) { cwt.at->wall = what; if(cellHalfvine(what)) c2->wall = waNone, cwt.at->wall = waVinePlant; } nextmovetype = lmMove; addMessage(XLAT("You push %the1 behind you!", what)); animateMovement(mi.rev(), LAYER_BOAT); changes.push_push(cwt.at); return perform_actual_move(); } bool attackable; attackable = c2->wall == waBigTree || c2->wall == waSmallTree || (c2->wall == waShrub && items[itOrbSlaying]) || (c2->wall == waMirrorWall && !bow::crossbow_mode()); if(attackable && markOrb(itOrbAether) && c2->wall != waMirrorWall) attackable = false; bool nm; nm = attackable; if(forcedmovetype == fmAttack) attackable = true; attackable = attackable && (!c2->monst || isFriendly(c2)); attackable = attackable && !nonAdjacentPlayer(cwt.at,c2); bool dont_attack = items[itOrbFlash] || items[itOrbLightning]; if(attackable && fmsAttack && !dont_attack && !items[itCurseWeakness]) { if(checkNeedMove(checkonly, true)) return false; nextmovetype = nm ? lmAttack : lmSkip; if(c2->wall == waSmallTree || (c2->wall == waBigTree && markOrb(itOrbSlaying))) { drawParticles(c2, winf[c2->wall].color, 4); addMessage(XLAT("You chop down the tree.")); playSound(c2, "hit-axe" + pick123()); changes.ccell(c2); c2->wall = waNone; spread_plague(cwt.at, c2, mi.d, moPlayer); return swing(); } else if(c2->wall == waShrub && markOrb(itOrbSlaying)) { drawParticles(c2, winf[c2->wall].color, 4); addMessage(XLAT("You chop down the shrub.")); playSound(c2, "hit-axe" + pick123()); changes.ccell(c2); c2->wall = waNone; spread_plague(cwt.at, c2, mi.d, moPlayer); return swing(); } else if(c2->wall == waBigTree) { drawParticles(c2, winf[c2->wall].color, 8); addMessage(XLAT("You start chopping down the tree.")); playSound(c2, "hit-axe" + pick123()); changes.ccell(c2); c2->wall = waSmallTree; return swing(); } if(!peace::on && !bow::crossbow_mode()) { if(c2->wall == waMirrorWall) addMessage(XLAT("You swing your sword at the mirror.")); else if(c2->wall) addMessage(XLAT("You swing your sword at %the1.", c2->wall)); else addMessage(XLAT("You swing your sword.")); return swing(); } return false; } else if(c2->monst == moKnight) { #if CAP_COMPLEX2 if(vmsg(miWALL, siMONSTER, c2, c2->monst)) camelot::knightFlavorMessage(c2); #endif return false; } else if(c2->monst && (!isFriendly(c2) || c2->monst == moTameBomberbird || isMountable(c2->monst)) && !(peace::on && !good_tortoise)) return attack(); else if(!passable(c2, cwt.at, P_USEBOAT | P_ISPLAYER | P_MIRROR | P_MONSTER)) { tell_why_impassable(); return false; } else if(markOrb(itCurseFatigue) && items[itFatigue] + fatigue_cost(mi) > 10) { if(vmsg(miRESTRICTED, siFATIGUE, nullptr, moNone)) addMessage(XLAT("You are too fatigued!")); return false; } else if(fmsMove) return move_if_okay(); else return false; } bool pcmove::move_if_okay() { cell*& c2 = mi.t; #if CAP_COMPLEX2 if(mine::marked_mine(c2) && !mine::safe() && !checkonly && warningprotection(XLAT("Are you sure you want to step there?"))) return false; #endif if(snakelevel(c2) <= snakelevel(cwt.at)-2) { bool can_leave = false; forCellEx(c3, c2) if(passable(c3, c2, P_ISPLAYER | P_MONSTER)) can_leave = true; if(!can_leave && !checkonly && warningprotection(XLAT("Are you sure you want to step there?"))) return false; } if(switchplace_prevent(cwt.at, c2, *this)) return false; if(!checkonly && warningprotection_hit(do_we_stab_a_friend(mi, moPlayer))) return false; nextmovetype = lmMove; return perform_actual_move(); } void pcmove::tell_why_impassable() { cell*& c2 = mi.t; if(nonAdjacentPlayer(cwt.at,c2)) { if(vmsg(miRESTRICTED, siWARP, c2, moNone)) addMessage(geosupport_football() < 2 ? XLAT("You cannot move between the cells without dots here!") : XLAT("You cannot move between the triangular cells here!") ); } else if(againstWind(c2, cwt.at)) { if(vmsg(miRESTRICTED, siWIND, c2, moNone)) blowaway_message(c2); } else if(anti_alchemy(c2, cwt.at)) { if(vmsg(miRESTRICTED, siWALL, c2, moNone)) addMessage(XLAT("Wrong color!")); } else if(c2->wall == waRoundTable) { if(vmsg(miRESTRICTED, siWALL, c2, moNone)) addMessage(XLAT("It would be impolite to land on the table!")); } else if(snakelevel(cwt.at) >= 3 && snakelevel(c2) == 0 && !isWall(c2)) { if(vmsg(miRESTRICTED, siWALL, cwt.at, moNone)) addMessage(XLAT("You would get hurt!", c2->wall)); } else if(cellEdgeUnstable(cwt.at) && cellEdgeUnstable(c2)) { if(vmsg(miRESTRICTED, siGRAVITY, c2, moNone)) addMessage(XLAT("Gravity does not allow this!")); } else if(c2->wall == waChasm && c2->land == laDual) { if(vmsg(miRESTRICTED, siWALL, c2, moNone)) addMessage(XLAT("You cannot move there!")); } else if(!c2->wall) { if(vmsg(miRESTRICTED, siUNKNOWN, c2, moNone)) addMessage(XLAT("You cannot move there!")); } else { if(vmsg(miWALL, siWALL, c2, moNone)) addMessage(XLAT("You cannot move through %the1!", c2->wall)); } } EX void rusalka_curse() { changes.ccell(cwt.at); if(cwt.at->wall == waNone) cwt.at->wall = waShallow; else if(cwt.at->wall == waShallow || isAlch(cwt.at->wall)) cwt.at->wall = waDeepWater; } bool pcmove::attack() { auto& c2 = mi.t; if(!fmsAttack) return false; if(items[itOrbFlash] || items[itOrbLightning]) return false; attackflags = AF_NORMAL; if(items[itOrbSpeed]&1) attackflags |= AF_FAST; if(items[itOrbSlaying]) attackflags |= AF_CRUSH; if(items[itCurseWeakness]) attackflags |= AF_WEAK; bool ca = bow::crossbow_mode() ? good_tortoise : canAttack(cwt.at, moPlayer, c2, c2->monst, attackflags); if(!ca) { if(forcedmovetype == fmAttack) { if(monstersnear_add_pmi(movei(cwt.at, STAY))) { if(vmsg_threat()) wouldkill("%The1 would get you!"); return false; } nextmovetype = lmSkip; addMessage(XLAT("You swing your sword at %the1.", c2->monst)); return swing(); } if(vmsg(miENTITY, siMONSTER, c2, c2->monst)) tell_why_cannot_attack(); return false; } // mip.t=c2 means that the monster is not destroyed and thus // still counts for lightning in monstersnear mip = movei(c2, nullptr, NO_SPACE); if(items[itCurseWeakness] || (isStunnable(c2->monst) && c2->hitpoints > 1)) { if(monsterPushable(c2)) mip = determinePush(cwt, subdir, [] (movei mi) { return passable(mi.t, mi.s, P_BLOW); }); else mip.t = c2; if(mip.t) changes.ccell(mip.t); changes.push_push(mip.t); } if(!(isWatery(cwt.at) && c2->monst == moWaterElemental) && checkNeedMove(checkonly, true)) return false; if(c2->monst == moTameBomberbird && warningprotection_hit(moTameBomberbird)) return false; nextmovetype = lmAttack; mirror::act(origd, mirror::SPINMULTI | mirror::ATTACK); int tk = tkills(); plague_kills = 0; if(good_tortoise) { changes.ccell(c2); c2->stuntime = 2; changes.at_commit([c2] { items[itBabyTortoise] += (ls::hv_structure() ? 9 : 4); updateHi(itBabyTortoise, items[itBabyTortoise]); c2->item = itBabyTortoise; tortoise::babymap[c2] = tortoise::seekbits; tortoise_hero_message(c2); achievement_collection(itBabyTortoise); }); } else { eMonster m = c2->monst; if(m) { if((attackflags & AF_CRUSH) && !canAttack(cwt.at, moPlayer, c2, c2->monst, attackflags ^ AF_CRUSH ^ AF_MUSTKILL)) markOrb(itOrbSlaying); if(c2->monst == moTerraWarrior && hrand(100) > 2 * items[itTerra]) { if(hrand(2 + jiangshi_on_screen) < 2) changes.value_add(wandering_jiangshi, 1); } attackMonster(c2, attackflags | AF_MSG, moPlayer); if(m == moRusalka) rusalka_curse(); changes.ccell(c2); // salamanders are stunned for longer time when pushed into a wall if(c2->monst == moSalamander && (mip.t == c2 || !mip.t)) c2->stuntime = 10; if(!c2->monst || isAnyIvy(m)) { spread_plague(cwt.at, c2, mi.d, moPlayer); produceGhost(c2, m, moPlayer); } if(mip.proper()) pushMonster(mip); animateAttack(mi, LAYER_SMALL); } } sideAttack(cwt.at, d, moPlayer, tkills() - tk - plague_kills); lastmovetype = lmAttack; lastmove = c2; swordAttackStatic(); if(monstersnear_add_pmi(movei(cwt.at, STAY))) { if(vmsg_threat()) wouldkill("You would be killed by %the1!"); return false; } if(checkonly) return true; if(changes.on) changes.commit(); return after_move(); } EX bool chaos_forbidden(cell *c) { return do_not_touch_this_wall(c) || isMultitile(c->monst); } EX int fatigue_cost(const movei& mi) { return gravityLevelDiff(mi.t, mi.s) + (snakelevel(mi.t) - snakelevel(mi.s)) + (againstWind(mi.s, mi.t) ? 0 : 1); } bool alchMayDuplicate(eWall w) { return !isDie(w) && w != waBoat && w != waArrowTrap; } bool pcmove::perform_actual_move() { cell*& c2 = mi.t; changes.at_commit([&] { flipplayer = true; if(multi::players > 1) multi::flipped[multi::cpid] = true; }); if(c2->item && isAlch(c2)) { if(alchMayDuplicate(cwt.at->wall)) { c2->wall = conditional_flip_slime(mi.mirror(), cwt.at->wall); c2->wparam = cwt.at->wparam; } else c2->wall = waNone; } #if CAP_COMPLEX2 if(c2->wall == waRoundTable) { addMessage(XLAT("You jump over the table!")); } if(cwt.at->wall == waRoundTable) camelot::roundTableMessage(c2); #endif invismove = (turncount >= noiseuntil) && items[itOrbInvis] > 0; if(items[itOrbFire]) { invismove = false; if(makeflame(cwt.at, 10, false)) markOrb(itOrbFire); } if(items[itCurseWater]) { invismove = false; if(makeshallow(mi.s, 10, false)) markOrb(itCurseWater); } if(markOrb(itCurseFatigue) && !markOrb(itOrbAether)) items[itFatigue] += fatigue_cost(mi); handle_friendly_ivy(); if(items[itOrbDigging]) { invismove = false; if(earthMove(mi)) markOrb(itOrbDigging); } movecost(cwt.at, c2, 1); if(!boatmove && collectItem(c2, cwt.at)) return true; if(doPickupItemsWithMagnetism(c2)) return true; if(isIcyLand(cwt.at) && cwt.at->wall == waNone && markOrb(itOrbWinter)) { invismove = false; cwt.at->wall = waIcewall; } if(items[itOrbWinter]) forCellEx(c3, c2) if(c3->wall == waIcewall && c3->item) { changes.ccell(c3); markOrb(itOrbWinter); eItem it = c3->item; if(collectItem(c3, cwt.at)) return true; if(!c3->item) animate_item_throw(c3, c2, it); } movecost(cwt.at, c2, 2); handle_switchplaces(cwt.at, c2, switchplaces); return perform_move_or_jump(); } void pcmove::handle_friendly_ivy() { cell*& c2 = mi.t; bool haveIvy = false; forCellEx(c3, cwt.at) if(c3->monst == moFriendlyIvy) haveIvy = true; bool killIvy = haveIvy; if(items[itOrbNature]) { if(c2->monst != moFriendlyIvy && strictlyAgainstGravity(c2, cwt.at, false, MF_IVY)) { invismove = false; } else if(cwt.at->monst) invismove = false; else if(haveIvy || !cellEdgeUnstable(cwt.at, MF_IVY)) { cwt.at->monst = moFriendlyIvy; cwt.at->mondir = neighborId(cwt.at, c2); invismove = false; markOrb(itOrbNature); killIvy = false; } } if(killIvy) killFriendlyIvy(); } bool pcmove::perform_move_or_jump() { lastmovetype = lmMove; lastmove = cwt.at; apply_chaos(); stabbingAttack(mi, moPlayer); changes.value_keep(cwt); cwt += wstep; mirror::act(origd, mirror::SPINMULTI | mirror::ATTACK | mirror::GO); auto pmi = player_move_info(mi); playerMoveEffects(mi); if(mi.t->monst == moFriendlyIvy) changes.ccell(mi.t), mi.t->monst = moNone; if(monstersnear_add_pmi(pmi)) { if(vmsg_threat()) wouldkill("%The1 would kill you there!"); return false; } if(checkonly) return true; if(changes.on) changes.commit(); if(switchplaces) { indAnimateMovement(mi, LAYER_SMALL); indAnimateMovement(mi.rev(), LAYER_SMALL); commitAnimations(LAYER_SMALL); } else animateMovement(mi, LAYER_SMALL); current_display->which_copy = current_display->which_copy * adj(mi); countLocalTreasure(); landvisited[cwt.at->land] = true; afterplayermoved(); return after_move(); } bool pcmove::stay() { if(items[itOrbGravity]) { gravity_state = get_static_gravity(cwt.at); if(gravity_state) markOrb(itOrbGravity); } lastmovetype = lmSkip; lastmove = NULL; if(checkNeedMove(checkonly, false)) return false; swordAttackStatic(); nextmovetype = lmSkip; mi = movei(cwt.at, STAY); if(last_gravity_state && !gravity_state) playerMoveEffects(mi); if(d == -2) dropGreenStone(cwt.at); items[itFatigue] -= 5; if(items[itFatigue] < 0) items[itFatigue] = 0; if(monstersnear_add_pmi(mi)) { if(vmsg_threat()) wouldkill("%The1 would get you!"); return false; } if(checkonly) return true; if(changes.on) changes.commit(); if(cellUnstable(cwt.at) && !markOrb(itOrbAether)) doesFallSound(cwt.at); return after_move(); } #if HDR inline bool movepcto(const movedir& md) { return movepcto(md.d, md.subdir); } #endif EX bool warning_shown; EX bool warningprotection(const string& s) { if(hardcore) return false; if(multi::activePlayers() > 1) return false; if(items[itWarning]) return false; warning_shown = true; pushScreen([s] () { cmode = sm::DARKEN; gamescreen(); dialog::addBreak(250); dialog::init(XLAT("WARNING"), 0xFF0000, 150, 100); dialog::addBreak(500); dialog::addInfo(s); dialog::addBreak(500); dialog::addItem(XLAT("YES"), 'y'); dialog::lastItem().scale = 200; auto yes = [] () { items[itWarning] = 1; popScreen(); }; dialog::add_action(yes); dialog::add_key_action(SDLK_RETURN, yes); dialog::addItem(XLAT("NO"), 'n'); dialog::lastItem().scale = 200; dialog::add_action([] () { items[itWarning] = 0; popScreen(); }); dialog::display(); }); return true; } EX bool warningprotection_hit(eMonster m) { if(m && warningprotection(XLAT("Are you sure you want to hit %the1?", m))) return true; return false; } EX bool playerInWater() { for(int i: player_indices()) if(isWatery(playerpos(i)) && !playerInBoat(i)) return true; return false; } EX int numplayers() { return multi::players; } EX vector player_positions() { vector res; for(int i=0; i player_indices() { vector res; for(int i=0; i 1) return multi::player[i].at; return singlepos(); } EX bool allPlayersInBoats() { for(cell *pc: player_positions()) if(pc->wall != waBoat) return true; return false; } EX int whichPlayerOn(cell *c) { if(singleused()) return c == singlepos() ? 0 : -1; for(int i: player_indices()) if(playerpos(i) == c) return i; return -1; } EX bool isPlayerOn(cell *c) { return whichPlayerOn(c) >= 0; } EX bool isPlayerInBoatOn(cell *c, int i) { return (playerpos(i) == c && ( c->wall == waBoat || c->wall == waStrandedBoat || (shmup::on && shmup::playerInBoat(i)) )); } EX bool playerInBoat(int i) { return isPlayerInBoatOn(playerpos(i), i); } EX bool isPlayerInBoatOn(cell *c) { for(int i=0; iland == laPower || singlepos()->land == laHalloween; for(cell *pc: player_positions()) if(pc->land == laPower || pc->land == laHalloween) return true; return false; } EX void playerMoveEffects(movei mi) { cell *c1 = mi.s; cell *c2 = mi.t; if(peace::on) items[itOrbSword] = c2->land == laBurial ? 100 : 0; changes.value_keep(sword::dir[multi::cpid]); sword::dir[multi::cpid] = sword::shift(mi, sword::dir[multi::cpid]); destroyWeakBranch(c1, c2, moPlayer); #if CAP_COMPLEX2 mine::uncover_full(c2); #endif if((c2->wall == waClosePlate || c2->wall == waOpenPlate) && normal_gravity_at(c2) && !markOrb(itOrbAether)) toggleGates(c2, c2->wall); if(c2->wall == waArrowTrap && c2->wparam == 0 && normal_gravity_at(c2) && !markOrb(itOrbAether)) activateArrowTrap(c2); if(c2->wall == waFireTrap && c2->wparam == 0 && normal_gravity_at(c2) &&!markOrb(itOrbAether)) { playSound(c2, "click"); changes.ccell(c2); c2->wparam = 1; } if(c2->wall == waReptile) c2->wparam = -1; princess::playernear(c2); if(c2->wall == waGlass && items[itOrbAether] > ORBBASE+1) { addMessage(XLAT("Your Aether powers are drained by %the1!", c2->wall)); drainOrb(itOrbAether, 2); } if(cellUnstable(c2) && !markOrb(itOrbAether)) { doesFallSound(c2); if(c2->land == laMotion && c2->wall == waChasm) c2->mondir = mi.rev_dir_or(NODIR); } if(c2->wall == waStrandedBoat && markOrb(itOrbWater)) c2->wall = waBoat; if(c2->land == laOcean && c2->wall == waBoat && c2->landparam < 30 && markOrb(itOrbWater)) c2->landparam = 40; if((c2->land == laHauntedWall || c2->land == laHaunted) && !hauntedWarning) { changes.value_set(hauntedWarning, true); addMessage(XLAT("You become a bit nervous...")); addMessage(XLAT("Better not to let your greed make you stray from your path.")); playSound(c2, "nervous"); } } EX void afterplayermoved() { pregen(); if(!racing::on) setdist(cwt.at, 7 - getDistLimit() - genrange_bonus, NULL); prairie::treasures(); if(generatingEquidistant) { printf("Warning: generatingEquidistant set to true\n"); generatingEquidistant = false; } } EX void produceGhost(cell *c, eMonster victim, eMonster who) { if(who != moPlayer && !items[itOrbEmpathy]) return; if(markOrb(itOrbUndeath) && !c->monst && isGhostable(victim)) { changes.ccell(c); c->monst = moFriendlyGhost, c->stuntime = 0; if(who != moPlayer) markOrb(itOrbEmpathy); } } EX bool swordAttack(cell *mt, eMonster who, cell *c, int bb) { eMonster m = c->monst; if(c->wall == waCavewall) markOrb(bb ? itOrbSword2: itOrbSword); if(among(c->wall, waSmallTree, waBigTree, waRose, waCTree, waVinePlant, waBigBush, waSmallBush, waSolidBranch, waWeakBranch, waShrub) || thruVine(mt, c)) { changes.ccell(c); playSound(NULL, "hit-axe"+pick123()); markOrb(bb ? itOrbSword2: itOrbSword); drawParticles(c, winf[c->wall].color, 16); addMessage(XLAT("You chop down %the1.", c->wall)); destroyHalfvine(c); c->wall = waNone; } if(c->wall == waBarrowDig) { changes.ccell(c); playSound(NULL, "hit-axe"+pick123()); markOrb(bb ? itOrbSword2: itOrbSword); drawParticles(c, winf[c->wall].color, 16); c->wall = waNone; } if(c->wall == waBarrowWall && items[itBarrow] >= 25) { changes.ccell(c); playSound(NULL, "hit-axe"+pick123()); markOrb(bb ? itOrbSword2: itOrbSword); drawParticles(c, winf[c->wall].color, 16); c->wall = waNone; } if(c->wall == waExplosiveBarrel) explodeBarrel(c); if(!peace::on && isPlayerOn(c) && whichPlayerOn(c) != multi::cpid && !markOrb(itOrbEmpathy)) killThePlayer(moPlayer, whichPlayerOn(mt), 0); if(!peace::on && mt == c && !markOrb(itOrbEmpathy)) killThePlayer(moPlayer, multi::cpid, 0); if(!peace::on && canAttack(mt, who, c, m, AF_SWORD)) { changes.ccell(c); markOrb(bb ? itOrbSword2: itOrbSword); int k = tkills(); attackMonster(c, AF_NORMAL | AF_MSG | AF_SWORD, who); if(c->monst == moShadow) c->monst = moNone; produceGhost(c, m, who); if(tkills() > k) return true; } return false; } EX void swordAttackStatic(int bb) { swordAttack(cwt.at, moPlayer, sword::pos(multi::cpid, bb), bb); } EX void swordAttackStatic() { for(int bb = 0; bb < 2; bb++) if(sword::orbcount(bb)) swordAttackStatic(bb); } EX int plague_kills; EX void spread_plague(cell *mf, cell *mt, int dir, eMonster who) { if(!items[itOrbPlague]) return; if(who != moPlayer && !items[itOrbEmpathy]) return; forCellEx(mx, mt) if(celldistance(mx, mf) > celldistance(mx, mf->modmove(dir)) && celldistance(mx, mf) <= 4) { sideAttackAt(mf, dir, mx, who, itOrbPlague, mt); } } EX void sideAttackAt(cell *mf, int dir, cell *mt, eMonster who, eItem orb, cell *pf) { eMonster m = mt->monst; flagtype f = AF_SIDE; if(orb == itOrbPlague) f |= AF_PLAGUE; if(items[itOrbSlaying]) f|= AF_CRUSH; if(!items[orb]) return; auto plague_particles = [&] { if(orb == itOrbPlague) { for(int i=0; i<16; i++) drawDirectionalParticle(pf, neighborId(pf, mt), (i&1) ? orb_auxiliary_color(orb) : iinf[orb].color); } }; if(canAttack(mf, who, mt, m, f)) { if((f & AF_CRUSH) && !canAttack(mf, who, mt, m, AF_SIDE | AF_MUSTKILL)) markOrb(itOrbSlaying); markOrb(orb); changes.ccell(mt); plague_particles(); if(who != moPlayer) markOrb(itOrbEmpathy); int kk = 0; if(orb == itOrbPlague) kk = tkills(); if(attackMonster(mt, AF_NORMAL | f | AF_MSG, who) || isAnyIvy(m)) { hit_anything = true; if(orb == itOrbPlague && kk < tkills()) plague_kills++; if(mt->monst != m) spread_plague(mf, mt, dir, who); produceGhost(mt, m, who); } } else if(mt->wall == waSmallTree) { changes.ccell(mt); plague_particles(); markOrb(orb); mt->wall = waNone; spread_plague(mf, mt, dir, who); hit_anything = true; } else if(mt->wall == waShrub && markEmpathy(itOrbSlaying)) { changes.ccell(mt); plague_particles(); markOrb(orb); mt->wall = waNone; spread_plague(mf, mt, dir, who); hit_anything = true; } else if(mt->wall == waBigTree) { changes.ccell(mt); plague_particles(); markOrb(orb); mt->wall = waSmallTree; hit_anything = true; } else if(mt->wall == waExplosiveBarrel && orb != itOrbPlague) { changes.ccell(mt); explodeBarrel(mt); hit_anything = true; } } EX void sideAttack(cell *mf, int dir, eMonster who, int bonus, eItem orb) { if(!items[orb]) return; if(who != moPlayer && !items[itOrbEmpathy]) return; for(int k: {-1, 1}) { int dir1 = dir + k*bonus; dir1 = mf->c.fix(dir1); cell *mt = mf->move(dir1); sideAttackAt(mf, dir1, mt, who, orb, mf); } } EX void sideAttack(cell *mf, int dir, eMonster who, int bonuskill) { int k = tkills(); plague_kills = 0; sideAttack(mf, dir, who, 1, itOrbSide1); sideAttack(mf, dir, who, 2, itOrbSide2); sideAttack(mf, dir, who, 3, itOrbSide3); k += plague_kills; if(who == moPlayer) { int kills = tkills() - k + bonuskill; if(kills >= 5) achievement_gain_once("MELEE5"); } } EX eMonster do_we_stab_a_friend(movei mi, eMonster who) { eMonster m = moNone; do_swords(mi, who, [&] (cell *c, int bb) { if(!peace::on && canAttack(mi.t, who, c, c->monst, AF_SWORD) && c->monst && isFriendly(c)) m = c->monst; }); for(int t=0; ttype; t++) { cell *c = mi.s->move(t); if(!c) continue; bool stabthere = false; if(logical_adjacent(mi.t, who, c)) stabthere = true; if(stabthere && canAttack(mi.t,who,c,c->monst,AF_STAB) && isFriendly(c)) return c->monst; } return m; } EX void wouldkill(const char *msg) { if(who_kills_me == moWarning) addMessage(XLAT("This move appears dangerous -- are you sure?")); else if(who_kills_me == moFireball) addMessage(XLAT("Cannot move into the current location of another player!")); else if(who_kills_me == moAirball) addMessage(XLAT("Players cannot get that far away!")); else if(who_kills_me == moTongue) addMessage(XLAT("Cannot push into another player!")); else if(who_kills_me == moCrushball) addMessage(XLAT("Cannot push into the same location!")); else addMessage(XLAT(msg, who_kills_me)); } EX void movecost(cell* from, cell *to, int phase) { if(from->land == laPower && to->land != laPower && (phase & 1)) { int n=0; for(int i=0; i= 2 && i != itOrbFire) items[i] = 2, n++; if(n) addMessage(XLAT("As you leave, your powers are drained!")); } #if CAP_TOUR if(from->land != to->land && tour::on && (phase & 2)) { changes.at_commit([to] { tour::checkGoodLand(to->land); }); } #endif if(to->land == laCrossroads4 && !geometry && (phase & 2) && !cheater) { achievement_gain_once("CR4"); changes.value_set(chaosUnlocked, true); } if(isHaunted(from->land) && !isHaunted(to->land) && (phase & 2)) { updateHi(itLotus, truelotus = items[itLotus]); if(items[itLotus] >= 1) achievement_gain_once("LOTUS1"); if(items[itLotus] >= (big_unlock ? 25 : 10)) achievement_gain_once("LOTUS2"); if(items[itLotus] >= (big_unlock ? 50 : 25)) achievement_gain_once("LOTUS3"); if(items[itLotus] >= 50 && !big_unlock) achievement_gain_once("LOTUS4"); achievement_final(false); } if(geometry == gNormal && celldist(to) == 0 && !usedSafety && gold() >= 100 && (phase & 2)) achievement_gain_once("COMEBACK"); bool tortoiseOK = to->land == from->land || to->land == laTortoise || (to->land == laDragon && from->land != laTortoise) || ls::any_chaos(); if(tortoise::seek() && !from->item && !tortoiseOK && passable(from, NULL, 0) && (phase & 2)) { changes.ccell(from); changes.map_value(tortoise::babymap, from); from->item = itBabyTortoise; tortoise::babymap[from] = tortoise::seekbits; addMessage(XLAT("You leave %the1.", itBabyTortoise)); items[itBabyTortoise]--; } } }