diff --git a/content.cpp b/content.cpp index ee9513cd..eae65d14 100644 --- a/content.cpp +++ b/content.cpp @@ -1729,6 +1729,8 @@ MONSTER('d', 0x901020, "Angry Die", moAngryDie, ZERO, RESERVED, moAnimatedDie, "You have made a die unhappy. Taste the revenge! This one won't forgive you, no matter what you do." ) +ITEM('}', 0xFFFF80, "Crossbow", itCrossbow, IC_NAI, ZERO, RESERVED, osNone, "Your crossbow.") + //shmupspecials MONSTER( '@', 0xC0C0C0, "Rogue", moPlayer, CF_FACE_UP | CF_PLAYER, RESERVED, moNone, "In the Shoot'em Up mode, you are armed with thrown Knives.") MONSTER( '*', 0xC0C0C0, "Knife", moBullet, ZERO | CF_BULLET, RESERVED, moNone, "A simple, but effective, missile, used by rogues.") diff --git a/crossbow.cpp b/crossbow.cpp new file mode 100644 index 00000000..ec4b1ce6 --- /dev/null +++ b/crossbow.cpp @@ -0,0 +1,194 @@ +// Hyperbolic Rogue +// Copyright (C) 2011-2019 Zeno Rogue + +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +/** \file crossbow.cpp + * \brief implementation of the crossbow mode + */ + +#include "hyper.h" + +namespace hr { + +EX namespace bow { + +#if HDR +enum eWeapon { wBlade, wCrossbow }; +enum eCrossbowStyle { cbBull, cbGeodesic }; +#endif + +EX eWeapon weapon; +EX eCrossbowStyle style; +EX bool bump_to_shoot = true; + +EX bool crossbow_mode() { return weapon == wCrossbow; } + +#if HDR +struct bowpoint { + int total; + movei last, next, lastun; + cell *con; + bowpoint() : last(nullptr), next(nullptr), lastun(nullptr) { total = 0; } + }; +#endif + +EX vector bowpath; + +EX vector last_bowpath; + +EX map target_at; + +EX int loading_time() { + return style == cbBull ? 3 : 4; + } + +EX bool blocks(cell *c) { + if(isWall(c) && c->wall != waMirrorWall) return true; + return false; + } + +EX int create_path() { + map scores; + scores[cwt.at] = bowpoint(); + scores[cwt.at].last = movei(cwt.at, STAY); + + int best_score = -1; cell* best_score_at = cwt.at; + + for(cell *c: dcal) { + cell *c1 = target_at[c->cpdist]; + if(c1 && c != c1) continue; + if(c == c1) { best_score = -1; } + bowpoint best; + best.total = -1; + forCellIdEx(c1, i, c) if(c1->cpdist < c->cpdist && scores.count(c1)) { + auto last = scores[c1]; + int ntotal = last.total; + auto cw2 = cellwalker(c, i); + if(inmirror(c)) cw2 = mirror::reflect(cw2); + if(blocks(cw2.peek())) continue; + if(cw2.at->monst) { ntotal += 10000; ntotal += 1280 >> c->cpdist; } + ntotal += 2; + if(c->cpdist > 1) { + int d = abs(szgmod(last.lastun.d - c->c.spin(i), c1->type)); + if(d != c1->type / 2) { + if(style == cbGeodesic) ntotal--; + if(style == cbBull) continue; + } + } + if(ntotal > best.total) { best.total = ntotal; best.lastun = movei(c, i); best.last = movei(cw2.at, cw2.spin); best.con = c1; best.next = movei(cw2.at, STAY); } + best.total = max(best.total, ntotal); + } + if(best.total > best_score) { best_score = best.total; best_score_at = c; } + if(best.total > -1) scores[c] = best; + } + + bowpath.clear(); + if(best_score == -1) return best_score; + while(best_score_at != cwt.at) { + bowpath.push_back(scores[best_score_at]); best_score_at = bowpath.back().con; scores[best_score_at].next = bowpath.back().last.rev(); + } + bowpath.push_back(scores[best_score_at]); + + reverse(bowpath.begin(), bowpath.end()); + return best_score; + } + +EX bool auto_path() { + target_at = {}; + target_at[1] = cwt.cpeek(); + return create_path() >= 10000; + } + +EX bool fire_mode; + +EX void switch_fire_mode() { + if(!crossbow_mode()) { addMessage(XLAT("You fire an angry glance at your enemies.")); return; } + if(items[itCrossbow]) { addMessage(XLAT("You need more time to reload your crossbow!")); return; } + if(!fire_mode) { + addMessage(XLAT("Double-click tile to fire.")); + fire_mode = true; + last_bowpath = {}; + targets = {}; + } + else if(fire_mode) { + addMessage(XLAT("Firing cancelled.")); + fire_mode = false; + last_bowpath = {}; + } + } + +EX void add_fire(cell *c) { + bool emp = target_at.empty(); + auto& t = target_at[c->cpdist]; + if(t == c) { + println(hlog, "done"); + bow::last_bowpath.clear(); + checked_move_issue = miVALID; + pcmove pcm; + pcm.checkonly = false; + changes.init(false); + bool b = pcm.try_shooting(false); + println(hlog, "b = ", b); + if(!b) changes.rollback(); + fire_mode = false; + } + else { + t = c; + int res = create_path(); + if(res == -1) { + if(!emp) { + target_at = {}; + add_fire(c); + } + else addMessage(XLAT("No way to hit this place.")); + } + bow::last_bowpath = bow::bowpath; + } + } + +EX void shoot() { + flagtype attackflags = AF_NORMAL; + if(items[itOrbSpeed]&1) attackflags |= AF_FAST; + if(items[itOrbSlaying]) attackflags |= AF_CRUSH; + if(items[itCurseWeakness]) attackflags |= AF_WEAK; + + for(auto& m: bowpath) { + cell *c = m.next.s; + if(!c) continue; + changes.ccell(c); + if(c->monst) attackMonster(c, attackflags | AF_MSG, moPlayer); + } + last_bowpath = bowpath; + } + +EX void showMenu() { + cmode = sm::SIDE | sm::MAYDARK; + gamescreen(); + dialog::init(XLAT("weapon selection")); + add_edit(weapon); + if(crossbow_mode()) { + add_edit(style); + add_edit(bump_to_shoot); + } + else dialog::addBreak(200); + dialog::addBack(); + dialog::display(); + } + +EX } + +} + diff --git a/hyper.cpp b/hyper.cpp index 0ae2b0c2..a17812ea 100644 --- a/hyper.cpp +++ b/hyper.cpp @@ -126,6 +126,7 @@ #include "inforder.cpp" #include "vr.cpp" #include "intra.cpp" +#include "crossbow.cpp" #if CAP_ROGUEVIZ #include "rogueviz/rogueviz-all.cpp" diff --git a/orbs.cpp b/orbs.cpp index 59bc0750..09427a07 100644 --- a/orbs.cpp +++ b/orbs.cpp @@ -216,6 +216,7 @@ EX void reduceOrbPowers() { whirlwind::calcdirs(cwt.at); items[itStrongWind] = !items[itOrbAether] && whirlwind::qdirs == 1; items[itWarning] = 0; + if(items[itCrossbow]) items[itCrossbow]--; } eWall orig_wall; diff --git a/pcmove.cpp b/pcmove.cpp index 29ee04f0..6c439500 100644 --- a/pcmove.cpp +++ b/pcmove.cpp @@ -219,6 +219,7 @@ struct pcmove { 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) {} @@ -241,6 +242,46 @@ EX bool movepcto(int d, int subdir IS(1), bool checkonly IS(false)) { return b; } +bool pcmove::try_shooting(bool auto_target) { + 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; + } + } + items[itCrossbow] = bow::loading_time(); + bow::shoot(); + + if(items[itOrbGravity]) { + gravity_state = get_static_gravity(cwt.at); + if(gravity_state) markOrb(itOrbGravity); + } + lastmovetype = lmAttack; lastmove = NULL; + if(checkNeedMove(checkonly, false)) + return false; + swordAttackStatic(); + nextmovetype = lmAttack; + + mi = movei(cwt.at, STAY); + if(last_gravity_state && !gravity_state) + playerMoveEffects(mi); + + if(monstersnear_add_pmi(mi)) { + if(vmsg(miTHREAT)) 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); @@ -281,10 +322,19 @@ bool pcmove::movepcto() { fmsActivate = forcedmovetype == fmSkip || forcedmovetype == fmActivate; changes.init(checkonly); + changes.value_keep(bow::last_bowpath); + bow::last_bowpath.clear(); bool b = (d >= 0) ? actual_move() : stay(); if(checkonly || !b) { changes.rollback(); if(!checkonly) flipplayer = false; + + if(!b && items[itCrossbow] == 0 && bow::crossbow_mode() && !bow::fire_mode) { + changes.init(checkonly); + changes.value_keep(bow::last_bowpath); + b = try_shooting(true); + if(checkonly || !b) changes.rollback(); + } } else if(changes.on) { println(hlog, "error: not commited!"); @@ -803,6 +853,8 @@ void pcmove::tell_why_cannot_attack() { 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() && items[itCrossbow]) + addMessage(XLAT("Your crossbow is still reloading!")); else addMessage(XLAT("For some reason... cannot attack!")); } @@ -999,7 +1051,7 @@ bool pcmove::attack() { if(items[itOrbSlaying]) attackflags |= AF_CRUSH; if(items[itCurseWeakness]) attackflags |= AF_WEAK; - bool ca =canAttack(cwt.at, moPlayer, c2, c2->monst, attackflags); + bool ca = bow::crossbow_mode() ? false : canAttack(cwt.at, moPlayer, c2, c2->monst, attackflags); if(!ca) { if(forcedmovetype == fmAttack) {