// Hyperbolic Rogue -- orbs // Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details /** \file orbs.cpp * \brief Implementation of various Orb effects, and their properties such as default and maximum charges */ #include "hyper.h" namespace hr { EX array orbused; EX array lastorbused; EX bool markOrb(eItem it) { if(!items[it]) return false; orbused[it] = true; return true; } EX bool markEmpathy(eItem it) { if(!items[itOrbEmpathy]) return false; if(!markOrb(it)) return false; markOrb(itOrbEmpathy); return true; } EX bool markEmpathy2(eItem it) { if(items[itOrbEmpathy] < 2) return false; if(!markOrb2(it)) return false; markOrb2(itOrbEmpathy); return true; } EX bool markOrb2(eItem it) { return markOrb(it); /* if(!items[it]) return false; orbused[it] = true; return items[it] > 1; */ } EX int fixpower(int qty, bool checking) { if(checking ? items[itOrbEnergy] : markOrb(itOrbEnergy)) qty = (qty+1)/2; return qty; } EX void useupOrb(eItem it, int qty) { items[it] -= fixpower(qty, false); if(items[it] < 0) items[it] = 0; } EX void drainOrb(eItem it, int target IS(0)) { if(items[it] > target) useupOrb(it, items[it] - target); } EX void empathyMove(const movei& mi) { if(!items[itOrbEmpathy]) return; if(items[itOrbFire]) { invismove = false; if(makeflame(mi.s, 10, false)) markEmpathy(itOrbFire); } if(items[itCurseWater]) { invismove = false; if(makeshallow(mi.s, 10, false)) markEmpathy(itCurseWater); } if(items[itOrbDigging]) { if(mi.proper() && earthMove(mi)) markEmpathy(itOrbDigging), invismove = false; } if(items[itOrbWinter] && isIcyLand(mi.s) && mi.s->wall == waNone) { invismove = false; mi.s->wall = waIcewall; markEmpathy(itOrbWinter); } } EX int intensify(int val) { return inv::on ? 2 * val : val * 6 / 5; } EX bool reduceOrbPower(eItem it, int cap) { if(items[it] && markOrb(itCurseDraining)) { items[it] -= (markOrb(itOrbEnergy) ? 1 : 2) * multi::activePlayers(); if(items[it] < 0) items[it] = 0; if(items[it] == 0 && it == itOrbLove) princess::bringBack(); } if(items[it] && (lastorbused[it] || (it == itOrbShield && items[it]>3) || !markOrb(itOrbTime))) { items[it] -= multi::activePlayers(); if(isHaunted(cwt.at->land)) fail_survivalist(); if(items[it] < 0) items[it] = 0; if(items[it] > cap && markOrb(itOrbIntensity)) cap = intensify(cap); if(items[it] > cap && timerghost) items[it] = cap; if(items[it] == 0 && it == itOrbLove) princess::bringBack(); return true; } if(items[it] > cap && timerghost) items[it] = cap; return false; } EX void reduceOrbPowerAlways(eItem it) { if(items[it]) { items[it] -= multi::activePlayers(); if(items[it] < 0) items[it] = 0; } } EX void reverse_curse(eItem curse, eItem orb, bool cancel) { if(items[curse] && markOrb(itOrbPurity)) { items[orb] += items[curse]; if(curse == itCurseWater) items[itOrbWinter] += items[curse]; items[curse] = 0; } if(cancel && items[curse] && items[orb]) { int m = min(items[curse], items[orb]); items[curse] -= m; items[orb] -= m; } } EX void reduceOrbPowers() { reverse_curse(itCurseWeakness, itOrbSlaying, true); reverse_curse(itCurseFatigue, itOrbSpeed, false); // OK reverse_curse(itCurseRepulsion, itOrbMagnetism, true); // OK reverse_curse(itCurseWater, itOrbFire, true); // OK reverse_curse(itCurseDraining, itOrbTime, false); // OK reverse_curse(itCurseGluttony, itOrbChoice, true); // OK if(haveMount()) markOrb(itOrbDomination); for(int i=0; iland == laCaribbean ? 777 : 150); if(invismove && !invisfish) markOrb(itOrbInvis); reduceOrbPower(itOrbLightning, 777); reduceOrbPower(itOrbSpeed, 67); reduceOrbPower(itOrbShield, 77); reduceOrbPower(itOrbShell, 150); reduceOrbPower(itOrbFlash, 777); reduceOrbPower(itOrbWinter, 77); reduceOrbPower(itOrbFire, 77); reduceOrbPower(itOrbIllusion, 111); reduceOrbPower(itOrbDragon, 111); reduceOrbPower(itOrbPsi, 111); reduceOrbPower(itOrbInvis, 77); reduceOrbPower(itOrbAether, 77); reduceOrbPower(itOrbWoods, 100); reduceOrbPower(itOrbDigging, 100); reduceOrbPower(itOrbTeleport, 200); reduceOrbPower(itOrbSpace, 150); reduceOrbPower(itOrbMagnetism, 150); reduceOrbPowerAlways(itOrbSafety); reduceOrbPower(itOrbThorns, 150); reduceOrbPower(itOrbWater, 150); reduceOrbPower(itOrbAir, 150); reduceOrbPower(itOrbFrog, 77); reduceOrbPower(itOrbDash, 77); reduceOrbPower(itOrbPhasing, 77); reduceOrbPower(itOrbDiscord, 67); reduceOrbPower(itOrbSummon, 333); reduceOrbPower(itOrbMatter, 333); reduceOrbPower(itOrbFish, 57 + 20 * multi::activePlayers()); if(!items[itSavedPrincess]) items[itOrbLove] = 0; reduceOrbPower(itOrbLove, 777); reduceOrbPower(itOrbStunning, 100); reduceOrbPower(itOrbLuck, 333); reduceOrbPower(itOrbUndeath, 77); reduceOrbPower(itOrbFreedom, 77); reduceOrbPower(itOrbEmpathy, 77); markOrb(itOrb37); reduceOrbPower(itOrb37, 333); reduceOrbPower(itOrbBeauty, 77); reduceOrbPower(itOrbEnergy, 77); reduceOrbPower(itOrbDomination, 120); reduceOrbPower(itOrbSword, 100 + 20 * multi::activePlayers()); reduceOrbPower(itOrbSword2, 100 + 20 * multi::activePlayers()); reduceOrbPower(itOrbStone, 120); reduceOrbPower(itOrbNature, 120); reduceOrbPower(itOrbRecall, 77); reduceOrbPower(itOrbBull, 120); reduceOrbPower(itOrbHorns, 77); reduceOrbPower(itOrbLava, 80); reduceOrbPower(itOrbMorph, 80); reduceOrbPower(itOrbSlaying, 120); reduceOrbPower(itOrbGravity, 120); reduceOrbPower(itOrbChoice, 120); reduceOrbPower(itOrbIntensity, 120); reduceOrbPower(itOrbImpact, 120); reduceOrbPower(itOrbChaos, 120); reduceOrbPower(itOrbPlague, 120); reduceOrbPower(itOrbSide1, 120); reduceOrbPower(itOrbSide2, 120); reduceOrbPower(itOrbSide3, 120); if(cwt.at->land != laWildWest) reduceOrbPower(itRevolver, 6); reduceOrbPower(itOrbPurity, 30); reduceOrbPower(itCurseWeakness, 199); reduceOrbPower(itCurseDraining, 199); reduceOrbPower(itCurseWater, 199); reduceOrbPower(itCurseFatigue, 199); reduceOrbPower(itCurseRepulsion, 199); reduceOrbPower(itCurseGluttony, 199); #if CAP_COMPLEX2 mine::auto_teleport_charges(); #endif whirlwind::calcdirs(cwt.at); items[itStrongWind] = !items[itOrbAether] && whirlwind::qdirs == 1; items[itWarning] = 0; if(items[itCrossbow]) { if(lastmovetype == lmSkip && isWarped(cwt.at)) addMessage(XLAT("The warped space distracts you from reloading while staying in place!")); else items[itCrossbow]--; } } eWall orig_wall; EX void flashAlchemist(cell *c) { if(isAlch(c) && isAlch(orig_wall)) { record_spillinfo(c, orig_wall); c->wall = orig_wall; } } EX void flashCell(cell *c, eMonster killer, flagtype flags) { changes.ccell(c); eWall ow = c->wall; flashAlchemist(c); if((flags & AF_MSG) && c->monst && !isWorm(c) && c->monst != moShadow) addMessage(XLAT("%The1 is destroyed by the Flash.", c->monst)); if(c->monst || isPlayerOn(c)) if(canAttack(nullptr, killer, c, c->monst, flags)) attackMonster(c, flags, killer); if(isIcyLand(c)) HEAT(c) += 2; if(c->land == laDryForest) c->landparam += 2; if(c->wall == waCavewall) c->wall = waCavefloor; if(c->wall == waDeadTroll) c->wall = waCavefloor; if(c->wall == waDeadTroll2) c->wall = waNone; if(c->wall == waPetrified) c->wall = waNone; if(c->wall == waDeadfloor2) c->wall = waDeadfloor; if(c->wall == waGargoyleFloor) c->wall = waChasm; if(c->wall == waGargoyleBridge) placeWater(c, c); if(c->wall == waGargoyle) c->wall = waNone; if(c->wall == waTerraWarrior) c->wall = waNone, kills[moTerraWarrior]++; if(c->wall == waPlatform) c->wall = waNone; if(c->wall == waStone) c->wall = waNone, destroyTrapsAround(c); if(c->wall == waRubble) c->wall = waNone; if(c->wall == waDeadwall) c->wall = waDeadfloor2; if(c->wall == waGiantRug) c->wall = waNone; if(c->wall == waMirror) c->wall = waNone; if(c->wall == waCloud) c->wall = waNone; if(c->wall == waExplosiveBarrel) explodeBarrel(c); if(c->wall == waRuinWall) c->wall = waNone; if(c->wall == waDune) c->wall = waNone; if(c->wall == waSaloon) c->wall = waNone; if(c->wall == waSandstone) c->wall = waNone; if(c->wall == waAncientGrave) c->wall = waNone; if(c->wall == waFreshGrave) c->wall = waNone; if(c->wall == waColumn) c->wall = waNone; if(c->wall == waGlass) c->wall = waNone; if(c->wall == waBigTree || c->wall == waSmallTree) c->wall = waNone; if(c->wall == waBigStatue) c->wall = waNone; if(c->wall == waCTree) c->wall = waCIsland2; if(c->wall == waPalace) c->wall = waRubble; if(c->wall == waRose) c->wall = waNone; if(c->wall == waOpenGate || c->wall == waClosedGate) { eWall w = c->wall; c->wall = waNone; for(int i=0; itype; i++) if(c->move(i) && c->move(i)->wall == w) flashCell(c->move(i), killer, flags); } if(c->wall == waRed1) c->wall = waNone; else if(c->wall == waRed2) c->wall = waRed1; else if(c->wall == waRed3) c->wall = waRed2; if(c->wall == waBarrowWall) c->wall = waBarrowDig; else if(c->wall == waBarrowDig) c->wall = waNone; if(c->wall != ow && ow) drawParticles(c, winf[ow].color, 16); if(hasTimeout(c) && c->wparam < 77) c->wparam = 77; if(isActivable(c)) activateActiv(c, false); } EX void activateFlashFrom(cell *cf, eMonster who, flagtype flags) { drawFlash(cf); orig_wall = cwt.at->wall; playSound(cf, "storm"); for(int i=0; itype; t++) for(int u=0; utype; u++) if(c->move(t) == cf->move(u) && c->move(t) != NULL) { flashCell(c, who, flags); goto nexti; } nexti: ; } } EX bool distanceBound(cell *c1, cell *c2, int d) { if(!c1 || !c2) return false; if(d == 0) return c1 == c2; for(int i=0; itype; i++) if(distanceBound(c1, c2->move(i), d-1)) return true; return false; } EX void checkFreedom(cell *cf) { manual_celllister cl; dynamicval d(orbused); cl.add(cf); for(int i=0; icpdist >= 5) return; for(int i=0; itype; i++) { cell *c2 = c->move(i); // todo leader if(cl.listed(c2)) continue; if(!passable(c2, c, P_ISPLAYER | P_MIRROR | P_LEADER)) continue; if(againstRose(c, c2) && !scentResistant()) continue; if(c2->wall == waArrowTrap && c2->wparam == 2) continue; bool monsterhere = false; for(int j=0; jtype; j++) { cell *c3 = c2->move(j); if(c3 && c3->monst && !isFriendly(c3)) monsterhere = true; } if(!monsterhere) cl.add(c2); } } addMessage(XLAT("Your %1 activates!", itOrbFreedom)); drainOrb(itOrbFreedom); orig_wall = cwt.at->wall; for(cell *pc: player_positions()) drawBigFlash(pc); for(int i=0; icpdist > 5) break; flashCell(c, moPlayer, AF_MAGIC); } } EX void activateFlash() { int tk = tkills(); for(cell *pc: player_positions()) drawFlash(pc); addMessage(XLAT("You activate the Flash spell!")); orig_wall = cwt.at->wall; playSound(cwt.at, "storm"); drainOrb(itOrbFlash); for(int i=0; icpdist > 2) break; flashCell(c, moPlayer, AF_MAGIC); } achievement_count("FLASH", tkills(), tk); } EX bool reflectingBarrierAt(cell *c) { return c->wall == waBarrier || c->wall == waCamelot || c->wall == waPalace || c->wall == waPlatform || c->wall == waTempWall || c->wall == waWarpGate || c->wall == waBarrowDig || c->wall == waBarrowWall; } EX bool reflectingBarrierAt(cellwalker& c, int d) { if(d >= 3) return true; if(d <= -3) return true; d = gmod(c.spin + d, c.at->type); if(!c.at->move(d)) return true; return reflectingBarrierAt(c.at->move(d)); // WAS: // if(c.at->move(d)->wall == waBarrier) return true; // THEN: // if(c.at->move(d)->land == laBarrier || c.at->move(d)->land == laOceanWall || // c.at->move(d)->land == laHauntedWall || // c.at->move(d)->land == laElementalWall) ; // return false; } EX void killAdjacentSharks(cell *c) { for(int i=0; itype; i++) { cell *c2 = c->move(i); if(!c2) continue; if(isShark(c2->monst)) { changes.ccell(c2); c2->ligon = true; killMonster(c2, moLightningBolt); killAdjacentSharks(c2); } if(isKraken(c2->monst) && isWatery(c2)) { changes.ccell(c2); cell *c3 = kraken::head(c2); changes.ccell(c3); c3->ligon = true; forCellEx(c4, c3) changes.ccell(c4), killMonster(c4, moLightningBolt); // kill-all forCellEx(c4, c3) if(isWatery(c4)) { c4->ligon = true; killAdjacentSharks(c4); } } } } EX void castLightningBolt(cellwalker lig) { int bnc = 0; int counter = 1000; while(true) { counter--; if(counter < 0) break; // printf("at: %p i=%d d=%d\n", lig.c, i, lig.spin); killAdjacentSharks(lig.at); if(lig.peek() == NULL) break; lig += wstep; if(inmirror(lig)) lig = mirror::reflect(lig); cell *c = lig.at; changes.ccell(c); eWall ow = c->wall; flashAlchemist(c); if(c->monst == moMetalBeast2 && !c->item) c->item = itFulgurite; if(c->monst) if(canAttack(nullptr, moPlayer, c, c->monst, AF_MAGIC)) attackMonster(c, AF_MAGIC, moLightningBolt); if(isIcyLand(c)) HEAT(c) += 2; if(c->land == laDryForest) c->landparam += 2; bool first = !c->ligon; c->ligon = 1; bool brk = false, spin = false; if(c->wall == waGargoyle) brk = true; if(c->wall == waExplosiveBarrel) explodeBarrel(c), brk = true; if(c->wall == waCavewall) c->wall = waCavefloor, brk = true; if(c->wall == waDeadTroll) c->wall = waCavefloor, brk = true; if(c->wall == waDeadTroll2)c->wall = waNone, brk = true; if(c->wall == waPetrified) c->wall = waNone, brk = true; if(c->wall == waDeadfloor2)c->wall = waDeadfloor; if(c->wall == waRubble) c->wall = waNone; if(c->wall == waDeadwall) c->wall = waDeadfloor2, brk = true; if(c->wall == waGlass) c->wall = waNone, spin = true; if(c->wall == waDune) c->wall = waNone, brk = true; if(c->wall == waRuinWall) c->wall = waNone; if(c->wall == waIcewall) c->wall = waNone, brk = true; if(c->wall == waAncientGrave) c->wall = waNone, spin = true; if(c->wall == waFreshGrave) c->wall = waNone, spin = true; if(c->wall == waFreshGrave) c->wall = waNone, spin = true; if(c->wall == waBigStatue) c->wall = waNone, spin = true; if(c->wall == waColumn) c->wall = waNone, spin = true; if(c->wall == waStone) c->wall = waNone, brk = true, destroyTrapsAround(c); if(c->wall == waArrowTrap) activateArrowTrap(c); if(c->wall == waTerraWarrior) c->wall = waNone, kills[moTerraWarrior]++; if(c->wall == waCanopy || c->wall == waTrunk || c->wall == waBigBush || c->wall == waSmallBush) { makeflame(c, 12, false); brk = true; } if(c->wall == waGrounded) brk = true; if(c->wall == waFan) spin = true; if(c->wall == waMetal) c->wall = waCharged, brk = true; if(c->wall == waSandstone) { c->wall = waNone, brk = true; if(c->land == laStorms) c->item = itFulgurite; } if(c->wall == waCharged && first) { for(int i=0; itype; i++) // do not do strange things in horocyclic spires if(c->move(i) && c->move(i)->wall != waCharged) { cellwalker lig2(c, i); castLightningBolt(lig2); } brk = true; } if(c->wall == waBoat && c != cwt.at) become_water(c), spin = true; if(c->wall == waStrandedBoat && c !=cwt.at) c->wall = waNone, spin = true; if((c->wall == waNone || c->wall == waSea) && c->land == laLivefjord) c->wall = eWall(c->wall ^ waSea ^ waNone); if(c->wall == waRed1) c->wall = waNone; if(c->wall == waRed2) c->wall = waRed1; if(c->wall == waRed3) c->wall = waRed2, brk = true; if(isActivable(c)) activateActiv(c, false); if(c->wall == waBigTree || c->wall == waSmallTree || c->wall == waVinePlant || c->wall == waSaloon) { makeflame(c, 4, false); brk = true; } if(c->wall == waDock) makeflame(c, 5, false); if(c->wall == waCTree) makeflame(c, 12, false); if(c->wall == waRose) makeflame(c, 60, false); if(cellHalfvine(c) && c->wall == lig.peek()->wall) { destroyHalfvine(c, waPartialFire, 4); brk = true; } if(c->wall != ow && ow) drawParticles(c, winf[ow].color, 16); if(c == cwt.at) {bnc++; if(bnc > 10) break; } if(spin) lig += hrand(lig.at->type); if(brk) break; if(reflectingBarrierAt(c)) { int left = -1; int right = 1; while(!reflectingBarrierAt(lig, left)) left--; while(!reflectingBarrierAt(lig, right)) right++; lig += right + left; if(c->wall == waBarrowWall) c->wall = waBarrowDig; else if(c->wall == waBarrowDig) c->wall = waNone; bnc++; if(bnc > 10) break; } else lig += rev; if(lig.at->wall == waCloud) { lig.at->wall = waNone; mirror::createMirages(lig, mirror::LIGHTNING); } if(lig.at->wall == waMirror) { lig.at->wall = waNone; mirror::createMirrors(lig, mirror::LIGHTNING); } } } EX void castLightningBoltFrom(cell *c) { orig_wall = c->wall; for(int i=0; itype; i++) castLightningBolt(cellwalker(c, i)); } EX void activateLightning() { int tk = tkills(); drawLightning(); addMessage(XLAT("You activate the Lightning spell!")); for(int i=0; iligon = 0; drainOrb(itOrbLightning); for(cell *pc: player_positions()) castLightningBoltFrom(pc); elec::afterOrb = true; elec::act(); elec::afterOrb = false; achievement_count("LIGHTNING", tkills(), tk); playSound(cwt.at, "storm"); } // roCheck: return orb type if successful, 0 otherwise // roMouse/roKeyboard: // return orb type if successful, eItem(-1) if do nothing, 0 otherwise EX bool haveRangedTarget() { if(!haveRangedOrb()) return false; for(int i=0; i 1 && multi::activePlayers() == 1) multi::checklastmove(); if(multi::players == 1) checkmove(); } int teleportAction() { // normal teleport if(shmup::on || numplayers() == 1) return 1; // multi-player, but all in -- do nothing else if(numplayers() == multi::activePlayers()) return 0; // otherwise teleport to the game else return 2; } EX void teleportTo(cell *dest) { playSound(dest, "other-teleport"); if(dest->monst) { cwt.at->monst = dest->monst; cwt.at->stuntime = dest->stuntime; dest->monst = moNone; } if(teleportAction() == 2) { bool b = multiRevival(dest, NULL); if(b) { killFriendlyIvy(); drainOrb(itOrbTeleport); movecost(cwt.at, dest, 3); playerMoveEffects(movei(cwt.at, dest, TELEPORT)); afterplayermoved(); bfs(); advance_tides(); } return; } addMessage(XLAT("You teleport to a new location!")); killFriendlyIvy(); cell *from = cwt.at; movecost(from, dest, 1); playerMoveEffects(movei(cwt.at, dest, TELEPORT)); fix_whichcopy(dest); cwt.at = dest; cwt.spin = hrand(dest->type); flipplayer = !!(hrand(2)); drainOrb(itOrbTeleport); mirror::destroyAll(); afterplayermoved(); bfs(); advance_tides(); sword::reset(); items[itOrbSword2] = 0; if(shmup::on) shmup::teleported(); else checkmoveO(); movecost(from, dest, 2); #if CAP_COMPLEX2 mine::auto_teleport_charges(); #endif } /* calls changes.rollback or changes.commit */ EX bool jumpTo(orbAction a, cell *dest, eItem byWhat, int bonuskill IS(0), eMonster dashmon IS(moNone), cell *phasecell IS(nullptr)) { if(byWhat != itStrongWind) playSound(dest, "orb-frog"); cell *from = cwt.at; changes.value_keep(cwt); changes.ccell(dest); changes.ccell(cwt.at); if(byWhat == itOrbFrog) { useupOrb(itOrbFrog, 5); addMessage(XLAT("You jump!")); } if(byWhat == itOrbDash) { useupOrb(itOrbDash, 5); if(dashmon) addMessage(XLAT("You vault over %the1!", dashmon)); } if(byWhat == itOrbPhasing) { useupOrb(itOrbPhasing, 5); if(phasecell->monst) addMessage(XLAT("You phase through %the1!", phasecell->monst)); else addMessage(XLAT("You phase through %the1!", phasecell->wall)); } movecost(from, dest, 1); killFriendlyIvy(); cell *c1 = cwt.at; animateMovement(match(cwt.at, dest), LAYER_SMALL); cwt.at = dest; forCellIdEx(c2, i, dest) if(c2->cpdist < dest->cpdist) { cwt.spin = i; flipplayer = true; } sword::reset(); auto mi = movei(c1, dest, JUMP); stabbingAttack(mi, moPlayer, bonuskill); playerMoveEffects(mi); if(itemclass(byWhat) == IC_ORB) apply_impact(dest); // do not apply movecost later, when from no longer exists if(cwt.at->item == itOrbSafety) { movecost(from, dest, 2); from = NULL; } if(cwt.at->item != itOrbYendor && cwt.at->item != itHolyGrail) { auto c = collectItem(cwt.at, from, true); if(c) { return true; } } mirror::destroyAll(); if(monstersnearO(a, dest)) { changes.rollback(); return false; } if(isCheck(a)) { changes.rollback(); return true; } changes.commit(); fix_whichcopy(dest); countLocalTreasure(); for(int i=9; i>=0; i--) setdist(cwt.at, i, NULL); if(from) movecost(from, dest, 2); createNoise(1); bow::bowpath_map.clear(); if(shmup::on) shmup::teleported(); else monstersTurn(); return true; } /* calls changes.rollback or changes.commit */ bool growIvyTo(orbAction a, const movei& mi) { auto& dest = mi.t; flagtype f = AF_NORMAL | AF_MSG; if(items[itOrbEmpathy] && items[itOrbSlaying]) { if(!canAttack(mi.s, moFriendlyIvy, mi.t, mi.t->monst, AF_MUSTKILL)) { markOrb(itOrbEmpathy); markOrb(itOrbSlaying); } f |= AF_CRUSH; } changes.ccell(dest); if(dest->monst) attackMonster(dest, f, moFriendlyIvy); else { dest->monst = moFriendlyIvy; dest->mondir = mi.rev_dir_or(NODIR); animateMovement(mi, LAYER_BIG); moveEffect(mi, moFriendlyIvy); empathyMove(mi); } apply_impact(dest); markOrb(itOrbNature); if(monstersnearO(a, cwt.at)) { changes.rollback(); return false; } if(isCheck(a)) { changes.rollback(); return true; } changes.commit(); createNoise(1); monstersTurn(); return true; } pair spacedrain(cell *c) { int d = c->cpdist; bool usemagnet = items[itOrbMagnetism] && d > 0; if(usemagnet) d--; return {d * d, usemagnet}; } void telekinesis(cell *dest) { auto cost = spacedrain(dest); if(dest->land == laAlchemist && isAlchAny(dest) && isAlchAny(cwt.at)) dest->wall = cwt.at->wall; if(dest->land == laPower && cwt.at->land != laPower && dest->item != itOrbFire && dest->item != itOrbLife) { if(itemclass(dest->item) != IC_ORB) items[dest->item] ++; else items[dest->item] += 2; addMessage(XLAT("The Orb loses its power as it leaves the Land of Power!")); dest->item = itNone; } if(dest->wall == waGlass) { drainOrb(itOrbSpace); addMessage(XLAT("Your power is drained by %the1!", dest->wall)); } moveItem(dest, cwt.at, true); eItem it = cwt.at->item; bool saf = it == itOrbSafety; collectItem(cwt.at, cwt.at, true); if(cwt.at->item == it) animateMovement(match(dest, cwt.at), LAYER_BOAT); else if(!saf) animate_item_throw(dest, cwt.at, it); useupOrb(itOrbSpace, cost.first); if(cost.second) markOrb(itOrbMagnetism); createNoise(3); checkSwitch(); bfs(); if(!shmup::on) checkmoveO(); } EX eMonster summonedAt(cell *dest) { if(dest->monst) return moNone; if(dest->wall == waVineHalfA || dest->wall == waVineHalfB || dest->wall == waVinePlant) return moVineSpirit; if(dest->wall == waCTree) return moParrot; if(dest->wall == waLake) return moGreaterShark; if(dest->wall == waAncientGrave || dest->wall == waFreshGrave) return moGhost; if(dest->wall == waClosePlate || dest->wall == waOpenPlate) return dest->land == laPalace ? moPalace : moBat; if(dest->wall == waFloorA || dest->wall == waFloorB) return moSlime; if(dest->wall == waCavefloor) return moTroll; if(dest->wall == waDeadfloor) return moEarthElemental; if(dest->wall == waDeadfloor2) return moMiner; if(dest->wall == waMineOpen || dest->wall == waMineMine || dest->wall == waMineUnknown) return moBomberbird; if(dest->wall == waRichDie) return moAnimatedDie; if(dest->wall == waHappyDie) return moAngryDie; if(dest->wall == waTrapdoor) return dest->land == laPalace ? moFatGuard : moOrangeDog; if(dest->land == laFrog && dest->wall == waNone) { forCellEx(c1, dest) if(c1->wall == waShrub) return moVaulter; forCellEx(c1, dest) if(c1->wall == waDeepWater) return moFrog; forCellEx(c1, dest) if(c1->wall == waStone) return moPhaser; } if(dest->wall == waSea) return isElemental(dest->land) ? moWaterElemental : dest->land == laLivefjord ? moViking : dest->land == laKraken ? moViking : dest->land == laWarpCoast ? moRatling : dest->land == laDocks ? moWaterElemental : moPirate; if(isReptile(dest->wall)) return moReptile; if(dest->wall == waChasm) return moAirElemental; if(isFire(dest) || dest->wall == waMagma) return moFireElemental; if(dest->wall == waCavewall || dest->wall == waDeadwall) return moSeep; if(dest->wall == waRed1 || dest->wall == waRed2 || dest->wall == waRed3) return moRedTroll; if(dest->wall == waFrozenLake) return moFireElemental; if(dest->wall == waCIsland || dest->wall == waCIsland2) return moWaterElemental; if(dest->wall == waRubble || dest->wall == waGargoyleFloor || dest->wall == waGargoyleBridge || dest->wall == waLadder) return moGargoyle; if(dest->wall == waStrandedBoat) return moWaterElemental; if(dest->wall == waBoat) return moAirElemental; if(dest->wall == waStone || dest->wall == waRuinWall) return moEarthElemental; if(dest->wall == waGiantRug) return moVizier; if(dest->wall == waNone) { if(dest->land == laHunting) return moAirElemental; if(dest->land == laBull) return moRagingBull; if(dest->land == laPrairie) return moAirElemental; if(dest->land == laZebra) return moAirElemental; if(dest->land == laMirror) return moAirElemental; if(dest->land == laMountain) return moAirElemental; // unfortunately Ivies are too large if(dest->land == laDungeon) return moBat; if(dest->land == laIce) return moFireElemental; if(dest->land == laDesert) return moEarthElemental; if(dest->land == laJungle) return moWaterElemental; if(dest->land == laGraveyard) return moZombie; if(dest->land == laRlyeh || dest->land == laTemple) return moPyroCultist; if(dest->land == laHell) return moWaterElemental; if(dest->land == laPower) return moWitchFire; if(dest->land == laWineyard) return moVineBeast; if(dest->land == laEmerald) return moMiner; if(dest->land == laHive) return dest->type == 7 ? moBug1 : moBug0; if(dest->land == laRedRock) return moRedTroll; if(dest->land == laOcean) return moEarthElemental; if(dest->land == laDryForest) return moFireFairy; if(dest->land == laLivefjord) return moFjordTroll; if(dest->land == laStorms) return moStormTroll; if(dest->land == laOvergrown) return moForestTroll; if(dest->land == laIvoryTower) return moAirElemental; if(dest->land == laEndorian) return moAirElemental; if(dest->land == laEAir) return moAirElemental; if(dest->land == laEWater) return moWaterElemental; if(dest->land == laEEarth) return moEarthElemental; if(dest->land == laEFire) return moFireElemental; if(dest->land == laMotion) return moRunDog; if(dest->land == laWildWest) return moOutlaw; if(dest->land == laClearing) return moForestTroll; if(dest->land == laWhirlwind) return moAirElemental; if(dest->land == laWarpCoast) return moRatling; if(dest->land == laRose) return moRoseLady; if(dest->land == laDragon) return moFireElemental; if(dest->land == laTortoise) return moTortoise; if(dest->land == laBurial) return moEarthElemental; if(dest->land == laVolcano) return moFireElemental; if(dest->land == laBlizzard) return moAirElemental; if(dest->land == laTerracotta) return moEarthElemental; if(dest->land == laRuins) return moEarthElemental; if(dest->land == laSwitch) return passive_switch; if(dest->land == laSnakeNest) return moEarthElemental; if(dest->land == laBrownian) return moAcidBird; if(dest->land == laVariant) return moFireElemental; if(dest->land == laWestWall) return moAirElemental; if(isHaunted(dest->land)) return moGhost; } return moNone; } void summonAt(cell *dest) { playSound(dest, "orb-ranged"); dest->monst = summonedAt(dest); dest->stuntime = 3; if(dest->monst == moPirate || dest->monst == moViking || (dest->monst == moRatling && dest->wall == waSea)) dest->wall = waBoat, dest->item = itNone; if(dest->monst == moAnimatedDie) dest->wall = waNone; if(dest->monst == moAngryDie) dest->wall = waNone; if(dest->monst == moViking && dest->land == laKraken) dest->item = itOrbFish; if(dest->wall == waStrandedBoat) dest->wall = waBoat; else if(dest->monst == moWaterElemental) placeWater(dest, dest); if(dest->wall == waStone) dest->wall = waNone; if(dest->monst == moFireElemental && isFire(dest)) dest->wall = waNone; if(dest->monst == moTortoise) tortoise::emap[dest] = getBits(dest), dest->hitpoints = 3; addMessage(XLAT("You summon %the1!", dest->monst)); moveEffect(movei(dest, FALL), dest->monst); if(dest->wall == waClosePlate || dest->wall == waOpenPlate) toggleGates(dest, dest->wall); if(hasHitpoints(dest->monst)) dest->hitpoints = palaceHP(); useupOrb(itOrbSummon, 20); createNoise(2); bfs(); checkmoveO(); } bool tempWallPossibleAt(cell *dest) { if(dest->land == laWestWall) return false; if(dest->monst || (dest->item && !itemHidden(dest))) return false; return dest->wall == waChasm || isWatery(dest) || dest->wall == waNone || dest->wall == waTempBridge; } void tempWallAt(cell *dest) { if(dest->wall == waChasm) dest->wall = waTempFloor; else if(dest->wall == waNone) dest->wall = waTempWall; else if(dest->wall == waTempBridge) { dest->wall = waTempBridgeBlocked; return; } else if(isWatery(dest)) dest->wall = waTempBridge; int len = (items[itOrbMatter]+1) / 2; dest->wparam = len; useupOrb(itOrbMatter, len); dest->item = itNone; // underwater items are destroyed by this createNoise(2); bfs(); checkmoveO(); } void psi_attack(cell *dest) { playSound(dest, "other-mind"); if(isNonliving(dest->monst)) addMessage(XLAT("You destroy %the1 with a mental blast!", dest->monst)); else if(isDragon(dest->monst) || isKraken(dest->monst)) addMessage(XLAT("You damage %the1 with a mental blast!", dest->monst)); else addMessage(XLAT("You kill %the1 with a mental blast!", dest->monst)); // note: psi attack works with Petrify! attackMonster(dest, AF_PSI, moPlayer); useupOrb(itOrbPsi, 30); createNoise(2); bfs(); checkmoveO(); } bool gun_attack(orbAction a, cell *dest) { playSound(dest, "orb-ranged"); addMessage(XLAT("You shoot %the1!", dest->monst)); changes.ccell(dest); attackMonster(dest, AF_GUN, moNone); apply_impact(dest); if(monstersnearO(a, cwt.at)) { changes.rollback(); return false; } if(isCheck(a)) { changes.rollback(); return true; } changes.commit(); items[itRevolver] --; bfs(); checkmoveO(); createNoise(5); monstersTurn(); return true; } EX void checkStunKill(cell *dest) { if(isBird(dest->monst)) { moveEffect(movei(dest, FALL), moDeadBird); doesFall(dest); if(isWatery(dest) || dest->wall == waChasm || isFire(dest)) { addMessage(XLAT("%The1 falls!", dest->monst)); fallMonster(dest); return; } } if(dest->monst == moShadow) { addMessage(XLAT("%The1 is destroyed!", dest->monst)); killMonster(dest, moNone); } /* if(!isPermanentFlying(dest->monst) && cellEdgeUnstable(dest)) { addMessage(XLAT("%The1 falls!", dest->monst)); fallMonster(dest); } */ } void stun_attack(cell *dest) { playSound(dest, "orb-ranged"); addMessage(XLAT("You stun %the1!", dest->monst)); dest->stuntime += 5; checkStunKill(dest); useupOrb(itOrbStunning, 10); createNoise(3); bfs(); checkmoveO(); } void poly_attack(cell *dest) { playSound(dest, "orb-ranged"); eMonster orig = dest->monst; auto polymonsters = { moYeti, moRunDog, moRanger, moMonkey, moCultist, moFallingDog, moVariantWarrior, moFamiliar, moOrangeDog, moRedFox, moFalsePrincess, moResearcher, moNarciss, }; int ssf = 0; eMonster target = *(polymonsters.begin() + hrand(isize(polymonsters))); for(eMonster m: polymonsters) if(kills[m] && m != dest->monst) { ssf += kills[m]; if(hrand(ssf) < kills[m]) target = m; } addMessage(XLAT("You polymorph %the1 into %the2!", dest->monst, target)); dest->monst = target; if(!dest->stuntime) dest->stuntime = 1; if(orig == moPair) { cell *dest2 = dest->move(dest->mondir); if(dest2->monst == moPair) { dest2->monst = dest->monst; if(!dest2->stuntime) dest2->stuntime = 1; } } checkStunKill(dest); useupOrb(itOrbMorph, 3); createNoise(3); bfs(); checkmoveO(); } void placeIllusion(cell *c) { c->monst = moIllusion; c->stuntime = 0; useupOrb(itOrbIllusion, 5); addMessage(XLAT("You create an Illusion!")); bfs(); checkmoveO(); } void blowoff(const movei& mi) { auto& cf = mi.s; auto& ct = mi.t; bool die = cf->wall == waRichDie; playSound(ct, "orb-ranged"); if(cf->monst == moVoidBeast) addMessage(XLAT("You blow %the1 closer!", cf->monst)); else if(cf->monst) addMessage(XLAT("You blow %the1 away!", cf->monst)); if(cf->wall == waThumperOff) activateActiv(cf, false); if(isPushable(cf->wall) || cf->wall == waBigStatue) pushThumper(mi); else if(isBoat(cf) && !cf->monst) { bool was_stranded = cf->wall == waStrandedBoat; bool willbe_stranded = ct->wall == waNone; if(was_stranded) cf->wall = waBoat; if(willbe_stranded) become_water(ct); moveBoat(mi); if(was_stranded) cf->wall = waNone; if(willbe_stranded) ct->wall = waStrandedBoat; } else pushMonster(mi); if(cf->item == itBabyTortoise) { if(ct->item) ct->item = itNone; moveItem(cf, ct, true); } #if CAP_COMPLEX2 if(ct->monst == moAnimatedDie && dice::data[ct].happy() > 0) { ct->monst = moNone; ct->wall = waHappyDie; if(ct->land == laDice && cf->land == laDice) { cf->item = itDice; addMessage(XLAT("The die is now happy, and you are rewarded!")); } } if(die && ct->wall == waHappyDie) { /* pushMonster already awarded us -- place the reward on cf instead */ cf->item = itDice; items[itDice]--; } if(ct->wall == waHappyDie && dice::data[ct].happy() <= 0) { ct->monst = moAngryDie; ct->wall = waNone; ct->stuntime = 5; addMessage(XLAT("You have made a Happy Die angry!")); } #endif items[itOrbAir]--; createNoise(2); bfs(); checkmoveO(); } void useOrbOfDragon(cell *c) { makeflame(c, 20, false); playSound(c, "fire"); addMessage(XLAT("You throw fire!")); useupOrb(itOrbDragon, 5); createNoise(3); bfs(); checkmoveO(); } EX bool monstersnearO(orbAction a, cell *c) { // printf("[a = %d] ", a); if(shmup::on) return false; if(a == roCheck && multi::players > 1) return true; else if(a == roMultiCheck) return false; else return monstersnear(c, moPlayer); } EX bool isCheck(orbAction a) { return a == roCheck || a == roMultiCheck; } EX bool isWeakCheck(orbAction a) { return a == roCheck || a == roMultiCheck || a == roMouse; } EX movei blowoff_destination_dir(cell *c, int& di, int rev) { int d = 0; for(; dtype; d++) if(c->move(d) && rev*c->move(d)->cpdist < rev*c->cpdist) break; if(dtype) for(int e=d; etype; e++) { int di = e % c->type; cell *c2 = c->move(di); #if CAP_COMPLEX2 if(dice::on(c) && !dice::can_roll(movei(c, di))) continue; #endif if(c2 && rev*c2->cpdist > rev*c->cpdist && passable(c2, c, P_BLOW)) return movei(c, c2, di); } return movei(c, c, NO_SPACE); } EX movei blowoff_destination(cell *c, int& di) { int rev = c->monst == moVoidBeast ? -1 : 1; return blowoff_destination_dir(c, di, rev); } EX int check_jump(cell *cf, cell *ct, flagtype flags, cell*& jumpthru) { int partial = 1; forCellCM(c2, cf) { if(isNeighbor(c2, ct)) { jumpthru = c2; if(passable(c2, cf, flags | P_JUMP1)) { partial = 2; if(passable(ct, c2, flags | P_JUMP2)) { return 3; } } } } return partial; } EX int check_phase(cell *cf, cell *ct, flagtype flags, cell*& jumpthru) { int partial = 1; forCellCM(c2, cf) { if(isNeighbor(c2, ct) && !nonAdjacent(cf, c2) && !nonAdjacent(c2, ct)) { jumpthru = c2; if(passable(ct, cf, flags | P_PHASE)) { partial = 2; if(c2->monst || (isWall(c2) && c2->wall != waShrub)) { return 3; } } } } return partial; } EX cell *common_neighbor(cell *cf, cell *ct) { forCellCM(cc, cf) { if(isNeighbor(cc, ct)) return cc; } return nullptr; } EX void apply_impact(cell *c) { if(markOrb(itOrbImpact)) forCellEx(c1, c) { if(!c1->monst) continue; if(c1->monst == moMimic) continue; if(isMultitile(c1->monst)) continue; addMessage(XLAT("You stun %the1!", c1->monst)); changes.ccell(c1); c1->stuntime = min(c1->stuntime + 5, 7); checkStunKill(c1); } } EX int check_vault(cell *cf, cell *ct, flagtype flags, cell*& jumpthru) { cell *c2 = NULL, *c3 = NULL; forCellCM(cc, cf) { if(isNeighbor(cc, ct) && c2 != cc) c3 = c2, c2 = cc; } jumpthru = c2; if(!c2) return 0; if(c3) return 1; bool cutwall = among(c2->wall, waShrub, waExplosiveBarrel, waSmallTree, waBigTree); if(!c2->monst && !cutwall) return 2; bool for_monster = !(flags & P_ISPLAYER); if(for_monster && c2->monst && frog_power(c2->monst) && !items[itOrbDiscord]) return 2; if(!cutwall && !passable(c2, cwt.at, flags | P_JUMP1 | P_MONSTER)) return 3; if(!passable(ct, c2, flags | P_JUMP2)) return 4; if(!cutwall && !canAttack(cwt.at, moPlayer, c2, c2->monst, 0)) return 5; return 6; } EX bool disable_orb_range = false; EX eItem targetRangedOrb(cell *c, orbAction a) { if(!haveRangedOrb()) { if(!isWeakCheck(a)) addMessage(XLAT("You have no ranged Orbs!")); return itNone; } if(rosedist(cwt.at) == 1) { int r = rosemap[cwt.at]; int r2 = rosemap[c]; if(r2 <= r && !markOrb(itOrbBeauty)) { if(a == roKeyboard || a == roMouseForce) addMessage(XLAT("Those roses smell too nicely. You can only target cells closer to them!")); return itNone; } } // (-2) shmup variants eItem shmupEffect = shmup::targetRangedOrb(a); if(shmupEffect) return shmupEffect; // (-1) distance if(!disable_orb_range && !mine::in_minesweeper()) { if(c == cwt.at || isNeighbor(cwt.at, c)) { if(!isWeakCheck(a)) addMessage(XLAT("You cannot target that close!")); return itNone; } if(c->cpdist > 7) { if(!isWeakCheck(a)) addMessage(XLAT("You cannot target that far away!")); return itNone; } } vector orb_error_messages; #define CHKV(b, v) ((b) ? true : (v, false)) #define CHK(b, s) CHKV(b, orb_error_messages.push_back(s)) #define INI (changes.init(isCheck(a)), true) #define CHK_ROLLBACK(b, s) CHKV(b, (orb_error_messages.push_back(s), changes.rollback())) bool wouldkill_there = false; bool wouldkill_here = false; // (0-) strong wind if(items[itStrongWind] && CHK(c->cpdist == 2 && cwt.at == whirlwind::jumpFromWhereTo(c, true), XLAT("Strong wind can only take you to a specific place!"))) { changes.init(isCheck(a)); if(jumpTo(a, c, itStrongWind)) return itStrongWind; } // (0x) control if(items[itOrbDomination] && haveMount() && dragon::whichturn != turncount) { if(!isCheck(a)) { dragon::target = c; dragon::whichturn = turncount; addMessage(XLAT("Commanded %the1!", haveMount())); checkmoveO(); } return itOrbDomination; } // (0) telekinesis if(items[itOrbSpace] && CHK(c->item && !itemHiddenFromSight(c), XLAT("%The1 can only be used on items!", itOrbSpace)) && CHK(!cwt.at->item, XLAT("Cannot use %the1 here!", itOrbSpace)) && CHK(!saved_tortoise_on(c), XLAT("No, that would be heartless!")) && CHK(items[itOrbSpace] >= fixpower(spacedrain(c).first, isCheck(a)), XLAT("Not enough power for telekinesis!")) && CHK(!cantGetGrimoire(c, !isCheck(a)), XLAT("Cannot use %the1 here!", itOrbSpace)) && CHK(c->item != itBarrow, XLAT("%The1 is protected from this kind of magic!", c->item)) ) { if(!isCheck(a)) { bool saf = c->item == itOrbSafety; telekinesis(c); if(!saf) apply_impact(c); } return itOrbSpace; } // (0') air blow if(items[itOrbAir] && CHK(isBlowableMonster(c->monst) || isPushable(c->wall) || c->wall == waBigStatue || isBoat(c), c->monst ? XLAT("%The1 is immune to wind!", c->monst) : XLAT("Nothing to blow here!")) ) { int di = NODIR; movei mi = blowoff_destination(c, di); auto& c2 = mi.t; bool nowhereToBlow = false; if(!mi.op()) nowhereToBlow = true; else if(isBoat(c) && !isWatery(c2) && c2->wall != waNone) nowhereToBlow = true; else if(c->wall == waBigStatue && !canPushStatueOn(c2, P_BLOW)) nowhereToBlow = true; else { if(!isCheck(a)) blowoff(mi), apply_impact(c); return itOrbAir; } CHK(!nowhereToBlow, isBlowableMonster(c->monst) ? XLAT("Nowhere to blow %the1!", c->monst) : XLAT("Nowhere to blow %the1!", c->wall)); } // nature if(items[itOrbNature] && CHK(numplayers() == 1, XLAT("Cannot be used in multiplayer")) && CHK(c->monst != moFriendlyIvy, XLAT("You cannot grow on yourself!")) ) { changes.init(isCheck(a)); vector dirs; forCellIdCM(cf, d, c) if(cf->monst == moFriendlyIvy) { flagtype f = 0; if(items[itOrbEmpathy] && items[itOrbSlaying]) f |= AF_CRUSH; if(c->monst ? ( CHK(canAttack(cf, moFriendlyIvy, c, c->monst, f), XLAT("Cannot attack there!")) ) : ( CHK(passable(c, cf, P_ISFRIEND | P_MONSTER), XLAT("Cannot grow there!")) && CHK(!strictlyAgainstGravity(c, cf, false, MF_IVY), XLAT("Cannot grow against gravity!")) )) dirs.push_back(d); } int di = hrand_elt(dirs, -1); if(CHK(di != -1, XLAT("You cannot grow there from any adjacent cell!"))) { if(growIvyTo(a, movei(c, di).rev())) return itOrbNature; else wouldkill_here = true; } else changes.rollback(); } // (0'') jump cell *jumpthru = NULL; // orb of vaulting if(!shmup::on && items[itOrbDash] && CHK(c->cpdist == 2, XLAT("Cannot vault that far!"))) { changes.init(isCheck(a)); int vaultstate = check_vault(cwt.at, c, P_ISPLAYER, jumpthru); if(vaultstate == 0) orb_error_messages.push_back(XLAT("ERROR: No common neighbor to vault through!")); if(vaultstate == 1) orb_error_messages.push_back(XLAT("Can only vault in a roughly straight line!")); if(vaultstate == 2) orb_error_messages.push_back(XLAT("Nothing to vault over!")); if(vaultstate == 3) orb_error_messages.push_back(XLAT("Cannot pass through %the1 while vaulting!", jumpthru->wall)); if(vaultstate == 4 && c->monst) orb_error_messages.push_back(XLAT("Cannot vault onto %the1!", c->monst)); if(vaultstate == 4 && !c->monst) orb_error_messages.push_back(XLAT("Cannot vault to %the1!", c->wall)); if(vaultstate == 5) orb_error_messages.push_back(XLAT("Cannot attack %the1 while vaulting!", jumpthru->monst)); if(vaultstate == 6) { int k = tkills(); eMonster m = jumpthru->monst; if(jumpthru->wall == waShrub) { addMessage(XLAT("You chop down the shrub.")); jumpthru->wall = waNone; } if(jumpthru->wall == waSmallTree) { addMessage(XLAT("You chop down the tree.")); jumpthru->wall = waNone; } if(jumpthru->wall == waBigTree) { addMessage(XLAT("You start chopping down the tree.")); jumpthru->wall = waSmallTree; } if(jumpthru->wall == waExplosiveBarrel) { explodeBarrel(jumpthru); } if(m) attackMonster(jumpthru, AF_NORMAL | AF_MSG, moPlayer); k = tkills() - k; if(jumpTo(a, c, itOrbDash, k, m)) vaultstate = 7; else wouldkill_there = true; if(vaultstate == 7) return itOrbDash; } else changes.rollback(); } if(items[itOrbFrog] && CHK(c->cpdist == 2, XLAT("Cannot jump that far!"))) { changes.init(isCheck(a)); int frogstate = check_jump(cwt.at, c, P_ISPLAYER, jumpthru); if(frogstate == 1 && jumpthru && jumpthru->monst) { orb_error_messages.push_back(XLAT("Cannot jump through %the1!", jumpthru->monst)); } else if(frogstate == 1 && jumpthru) { orb_error_messages.push_back(XLAT("Cannot jump through %the1!", jumpthru->wall)); } else if(frogstate == 2 && c->monst) { orb_error_messages.push_back(XLAT("Cannot jump onto %the1!", c->monst)); } else if(frogstate == 2 && c->wall) { orb_error_messages.push_back(XLAT("Cannot jump on %the1!", c->wall)); } if(frogstate == 3) { if(jumpTo(a, c, itOrbFrog)) frogstate = 4; else wouldkill_there = true; if(frogstate == 4) return itOrbFrog; } else changes.rollback(); } if(items[itOrbPhasing] && CHK(c->cpdist == 2, XLAT("Cannot phase that far!"))) { changes.init(isCheck(a)); if(shmup::on) shmup::pushmonsters(); int phasestate = check_phase(cwt.at, c, P_ISPLAYER, jumpthru); if(phasestate == 1 && c->monst) { orb_error_messages.push_back(XLAT("Cannot phase onto %the1!", c->monst)); } else if(phasestate == 1) { orb_error_messages.push_back(XLAT("Cannot phase to %the1!", c->wall)); } else if(phasestate == 2) { orb_error_messages.push_back(XLAT("Nothing to phase through!")); } if(phasestate == 3) { if(jumpTo(a, c, itOrbPhasing, 0, moNone, jumpthru)) phasestate = 4; else wouldkill_there = true; } else changes.rollback(); if(shmup::on) shmup::popmonsters(); if(phasestate == 4) return itOrbPhasing; } // (1) switch with an illusion if(items[itOrbTeleport] && c->monst == moIllusion && CHK(!cwt.at->monst, XLAT("You need to move to give space to %the1!", cwt.at->monst)) && CHK(teleportAction() == 1, XLAT("All players are in the game!"))) { if(!isCheck(a)) teleportTo(c), apply_impact(c); return itOrbTeleport; } // (2) place illusion if(!shmup::on && items[itOrbIllusion] && CHK(c->monst == moNone, XLAT("Cannot cast illusion on a monster!")) && CHK(c->item == itNone || itemHidden(c), XLAT("Cannot cast illusion on an item!")) && INI && CHK_ROLLBACK(passable(c, NULL, P_MIRROR), XLAT("Cannot cast illusion here!"))) { if(!isCheck(a)) { changes.commit(), placeIllusion(c), apply_impact(c); } else changes.rollback(); return itOrbIllusion; } // (3) teleport if(items[itOrbTeleport] && CHK(c->monst == moNone, XLAT("Cannot teleport on a monster!")) && CHK(c->item == itNone || itemHidden(c), XLAT("Cannot teleport on an item!")) && CHK(teleportAction(), XLAT("All players are in the game!")) && shmup::verifyTeleport() && INI && CHK_ROLLBACK(passable(c, NULL, P_ISPLAYER | P_TELE), XLAT("Cannot teleport here!")) ) { if(!isCheck(a)) { changes.commit(), teleportTo(c), apply_impact(c), println(hlog, "commiting teleport ", items[itOrbTeleport]); } else changes.rollback(); return itOrbTeleport; } // (4) remove an illusion if(!shmup::on && items[itOrbIllusion] && c->monst == moIllusion) { if(!isCheck(a)) { addMessage(XLAT("You take the Illusion away.")); items[itOrbIllusion] += 3; // 100% effective with the Orb of Energy! apply_impact(c); c->monst = moNone; } return itOrbIllusion; } // (4a) colt if(items[itRevolver] && !shmup::on && CHK(c->monst, XLAT("Can only use %the1 on a monster!", itRevolver)) && CHK(canAttack(cwt.at, moPlayer, c, c->monst, AF_GUN), XLAT("%The1 is immune to %the2!", c->monst, itRevolver)) ) { bool inrange = false; for(cell *c1: gun_targets(cwt.at)) if(c1 == c) inrange = true; if(inrange) { changes.init(isCheck(a)); if(gun_attack(a, c)) return itRevolver; } } // (5) psi blast (non-shmup variant) if(!shmup::on && items[itOrbPsi] && CHK(c->monst, XLAT("Can only use %the1 on a monster!", itOrbPsi)) && CHK((isDragon(c->monst) || !isWorm(c)) && c->monst != moShadow && c->monst != moKrakenH, XLAT("%The1 is immune to mental blasts!", c->monst)) ) { if(!isCheck(a)) psi_attack(c), apply_impact(c); return itOrbPsi; } // (5a) summoning if(items[itOrbSummon] && CHK(summonedAt(c), c->monst ? XLAT("Cannot summon on a monster!") : XLAT("No summoning possible here!")) ) { if(!isCheck(a)) summonAt(c), apply_impact(c); return itOrbSummon; } // (5b) matter if(items[itOrbMatter] && CHK(tempWallPossibleAt(c), c->monst ? XLAT("Cannot create temporary matter on a monster!") : c->item ? XLAT("Cannot create temporary matter on an item!") : XLAT("Cannot create temporary matter here!")) ) { if(!isCheck(a)) tempWallAt(c), apply_impact(c); return itOrbMatter; } // (5c) stun if(items[itOrbStunning] && !shmup::on && CHK(c->monst, XLAT("%The1 can only be used on monsters.", itOrbStunning)) && CHK(!isMultitile(c->monst),XLAT("%The1 cannot be used on big monsters.", itOrbStunning)) && CHK(c->monst != moMimic, XLAT("%The1 cannot be used on %the2.", itOrbStunning, c->monst)) && CHK(c->stuntime < 3, XLAT("%The1 is already stunned!", c->monst)) ) { if(!isCheck(a)) stun_attack(c), apply_impact(c); return itOrbStunning; } // (5d) poly if(items[itOrbMorph] && !shmup::on && CHK(c->monst, XLAT("%The1 can only be used on monsters.", itOrbMorph)) && CHK(!isMultitile(c->monst), XLAT("%The1 cannot be used on big monsters.", itOrbMorph)) ) { if(!isCheck(a)) poly_attack(c), apply_impact(c); return itOrbMorph; } // (6) place fire (non-shmup variant) if(items[itOrbDragon] && !shmup::on && CHK(makeflame(c, 20, true), XLAT("Cannot throw fire there!")) ) { if(!isCheck(a)) useOrbOfDragon(c), apply_impact(c); return itOrbDragon; } if(isWeakCheck(a)) return itNone; for(string s: orb_error_messages) addMessage(s); if(wouldkill_there) wouldkill("%The1 would get you there!"); if(wouldkill_here) wouldkill("%The1 would get you if you grew there!"); #undef CHK #undef CHKV #undef CHK_ROLLBACK #undef INI return itNone; } bool isValentines() { const time_t now = time(NULL); const struct tm *datetime = localtime(&now); // 0-indexed tm_mon, 1-indexed tm_mday // So this is February (2nd month), and the 14th day. return datetime->tm_mon == 1 && datetime->tm_mday == 14; } EX int orbcharges(eItem it) { switch(it) { case itRevolver: //pickup-key return 6 - items[itRevolver]; case itOrbShield: return inv::on ? 30 : 20; case itOrbDiscord: return inv::on ? 46 : 23; case itOrbLove: return isValentines() ? 31 : 30; case itOrbUndeath: case itOrbSpeed: //"pickup-speed"); case itOrbInvis: case itOrbAether: return 30; case itOrbGravity: return 45; case itOrbChoice: return 60; case itOrbIntensity: return inv::on ? 150 : 50; case itOrbWinter: // "pickup-winter" return inv::on ? 45 : 30; case itOrbBeauty: case itOrbEmpathy: case itOrbFreedom: return 40; case itOrbFrog: case itOrbDash: case itOrbPhasing: case itOrbWoods: return 45; case itOrb37: case itOrbEnergy: return 50; case itOrbRecall: case itOrbNature: case itOrbStone: case itOrbStunning: case itOrbLuck: return 60; case itOrbWater: case itOrbMatter: case itOrbHorns: case itOrbBull: case itOrbShell: case itOrbAir: case itOrbSlaying: return 66; case itOrbTime: case itOrbSpace: case itOrbThorns: case itOrbLightning: case itOrbFlash: case itOrbIllusion: case itOrbPsi: case itOrbDigging: case itOrbTeleport: case itOrbMagnetism: return 77; case itOrbDomination: return 90; case itOrbSummon: return 120; case itOrbSword: return 60 + 30 * multi::activePlayers(); case itOrbSword2: return 40 + 20 * multi::activePlayers(); case itOrbFish: return 20 + 10 * multi::activePlayers(); case itOrbFire: return sphere ? 3 : 30; case itOrbDragon: return sphere ? 10 : 77; case itOrbMorph: return 60; case itOrbLava: return 50; case itOrbSide1: case itOrbSide2: case itOrbSide3: return 50; case itOrbImpact: return 50; case itOrbChaos: return 60; case itOrbPlague: return 30; case itOrbPurity: return 15; case itCurseWeakness: return 20; case itCurseDraining: return 30; case itCurseWater: return 20; case itCurseFatigue: return 30; case itCurseRepulsion: return 30; case itCurseGluttony: return 30; default: return 0; } } EX bool isShmupLifeOrb(eItem it) { return it == itOrbLife || it == itOrbFriend || it == itOrbNature || it == itOrbEmpathy || it == itOrbUndeath || it == itOrbLove || it == itOrbDomination || it == itOrbGravity; } EX void makelava(cell *c, int i) { if(!pseudohept(c)) return; if(isPlayerOn(c)) return; if(c->wall == waFire && c->wparam < i) c->wparam = i, orbused[itOrbLava] = true; else if(makeflame(c, i, false)) orbused[itOrbLava] = true; } EX void orboflava(int i) { if(items[itOrbLava]) for(cell *c: dcal) { if(c->cpdist > 5) break; if(!c->monst || isFriendly(c) || survivesFire(c->monst)) continue; forCellEx(c2, c) makelava(c2, i); } } }