/** * @return StreamedViewResponse */ private function combat(Player $attacker, Player $target, $required_turns = 0, $options) { $error = ''; $stealthed_attack = false; $stealth_damage = false; $stealth_lost = false; $bounty_result = false; $rewarded_ki = false; $wrath = false; $loot = 0; $killpoints = 1; $rounds = 1; $victor = null; $loser = null; $starting_attacker = clone $attacker; $starting_target = clone $target; $turns_counter = $options['duel'] ? -1 : 1; $attacker_label = $attacker->name(); if (!$options['duel'] && $attacker->hasStatus(STEALTH)) { $stealthed_attack = true; $this->stealthStrike($attacker, $target); $gold_mod = self::STEALTH_GOLD_MOD; if ($target->health > 0) { $stealth_damage = true; } else { $attacker_label = 'a stealthed ninja'; $victor = $attacker; $loser = $target; } $attack_label = "attacked %s from the shadows"; } else { $gold_mod = $options['duel'] ? self::DUEL_GOLD_MOD : self::DEFAULT_GOLD_MOD; if ($attacker->hasStatus(STEALTH)) { $stealth_lost = true; } $attacker->subtractStatus(STEALTH); while ($turns_counter != 0 && $attacker->health > 0 && $target->health > 0) { $turns_counter--; $rounds++; $this->strike($attacker, $target, $options['blaze'], $options['deflect']); /** * Evasion effect: * * Break off the duel/attack if less than 10% health or * health is less than average of defender's strength */ if ($options['evade'] && ($attacker->health < $target->getStrength() * 0.5 || $attacker->health < $attacker->health * 0.1)) { break; } } $attacker->turns = $attacker->turns - max(0, $required_turns); $attack_label = $options['duel'] ? 'dueled %s' : 'attacked %s'; } if ($target->health > 0 && $attacker->health > 0) { $combat_msg = "%s {$attack_label} for %s damage, but they got away before you could kill them!"; Event::create($attacker->id(), $target->id(), sprintf($combat_msg, $attacker->name(), 'you', $starting_target->health - $target->health)); if ($attacker->hasStatus(STEALTH)) { $stealth_lost = true; } $attacker->subtractStatus(STEALTH); } else { if ($target->health < 1 && $attacker->health < 1) { $loot = 0; $this->win($attacker, $target, $loot, $killpoints); $this->win($target, $attacker, $loot, 1); $this->lose($attacker, $target, $loot); $this->lose($target, $attacker, $loot); } else { if ($target->health < 1) { $victor = $attacker; $loser = $target; $bounty_result = Combat::runBountyExchange($victor, $loser); $loot = floor($gold_mod * $loser->gold); if ($options['duel']) { $killpoints = Combat::killpointsFromDueling($attacker, $target); $skillListObj = new Skill(); if ($skillListObj->hasSkill('wrath', $attacker)) { // They'll regain some health for the kill, at the end. $attacker->heal(self::BASE_WRATH_REGAIN); $wrath = true; } } $reporting_victor = $victor; if ($victor->hasStatus(STEALTH)) { $reporting_victor = new Player(); $reporting_victor->uname = 'a stealthed ninja'; $reporting_victor->player_id = 0; } $this->lose($loser, $reporting_victor, $loot); $this->win($victor, $loser, $loot, $killpoints); } else { $victor = $target; $loser = $attacker; $loot = floor($gold_mod * $loser->gold); $this->lose($loser, $victor, $loot); $this->win($victor, $loser, $loot, $killpoints); } } } if ($options['duel']) { $this->logDuel($attacker, $target, $victor, $killpoints); } if ($rounds > self::EVEN_MATCH_ROUND_COUNT) { // Evenly matched battle! Reward some ki to the attacker, even if they die $rewarded_ki = self::EVEN_MATCH_KI_REWARD; $attacker->setKi($attacker->ki + $rewarded_ki); } $target->save(); $attacker->save(); return new StreamedViewResponse('Battle Status', 'attack_mod.tpl', get_defined_vars(), ['quickstat' => 'player']); }
/** * Handle Standard Abstract Npcs * * @param String $victim * @param Player $player * @param Array $npcs * @return array [$npc_template, $combat_data] */ private function attackAbstractNpc($victim, Player $player, $npcs) { $npc_stats = $npcs[$victim]; // Pull an npcs individual stats with generic fallbacks. $npco = new Npc($npc_stats); // Construct the npc object. $display_name = isset($npc_stats['name']) ? $npc_stats['name'] : ucfirst($victim); $status_effect = isset($npc_stats['status']) ? $npc_stats['status'] : null; $reward_item = isset($npc_stats['item']) && $npc_stats['item'] ? $npc_stats['item'] : null; $is_quick = (bool) ($npco->getSpeed() > $player->getSpeed()); // Beyond basic speed and they see you coming, so show that message. $is_weaker = $npco->getStrength() * 3 < $player->getStrength(); // Npc much weaker? $is_stronger = $npco->getStrength() > $player->getStrength() * 3; // Npc More than twice as strong? $image = isset($npc_stats['img']) ? $npc_stats['img'] : null; // Assume defeat... $victory = false; $received_gold = null; $received_items = null; $added_bounty = ''; $is_rewarded = null; // Gets items or gold. $statuses = null; $status_classes = null; $image_path = null; // If the image exists, set the path to it for use on the page. if ($image && file_exists(SERVER_ROOT . 'www/images/characters/' . $image)) { $image_path = IMAGE_ROOT . 'characters/' . $image; } // ******* FIGHT Logic *********** $npc_damage = $npco->damage(); $survive_fight = $player->harm($npc_damage); $kill_npc = $npco->getHealth() < $player->damage(); if ($survive_fight > 0) { // The ninja survived, they get any gold the npc has. $received_gold = $this->calcReceivedGold($npco, (bool) $reward_item); $player->setGold($player->gold + $received_gold); $received_items = array(); if ($kill_npc) { $victory = true; // Victory occurred, reward the poor sap. if ($npco->inventory()) { $inventory = new Inventory($player); foreach (array_keys($npco->inventory()) as $l_item) { $item = Item::findByIdentity($l_item); $received_items[] = $item->getName(); $inventory->add($item->identity(), 1); } } // Add bounty where applicable for npcs. if ($npco->bountyMod() > 0 && $player->level > self::MIN_LEVEL_FOR_BOUNTY && $player->level <= self::MAX_LEVEL_FOR_BOUNTY) { $added_bounty = Combat::runBountyExchange($player, $npco, $npco->bountyMod()); } } $is_rewarded = (bool) $received_gold || (bool) count($received_items); if (isset($npc_stats['status']) && null !== $npc_stats['status']) { $player->addStatus($npc_stats['status']); // Get the statuses and status classes for display. $statuses = implode(', ', Player::getStatusList()); $status_classes = implode(' ', Player::getStatusList()); } } $player->save(); return ['npc.abstract.tpl', ['victim' => $victim, 'display_name' => $display_name, 'attack_damage' => $npc_damage, 'status_effect' => $status_effect, 'display_statuses' => $statuses, 'display_statuses_classes' => $status_classes, 'received_gold' => $received_gold, 'received_display_items' => $received_items, 'is_rewarded' => $is_rewarded, 'victory' => $victory, 'survive_fight' => $survive_fight, 'kill_npc' => $kill_npc, 'image_path' => $image_path, 'npc_stats' => $npc_stats, 'is_quick' => $is_quick, 'added_bounty' => $added_bounty, 'is_villager' => $npco->hasTrait('villager'), 'race' => $npco->race(), 'is_weaker' => $is_weaker, 'is_stronger' => $is_stronger]]; }
public function testBountyAtLeastMoreThanBountyModFromNpcs() { $pc = new MockPlayer(); $pc->difficulty = 0; $pc->level = 1; $npc = new MockNpc(); $npc->data['bounty_mod'] = 20; $bounty_mess = Combat::runBountyExchange($pc, $npc, $npc->bountyMod()); // With a high powered pc, some bounty should be put on by attacking a low powered npc. $this->assertNotEquals('', $bounty_mess); }
/** * Use an item on a target * @note /use/ is aliased to useItem externally because use is a php reserved keyword */ public function useItem($give = false, $self_use = false) { // Formats are: // http://nw.local/item/self_use/amanita/ // http://nw.local/item/use/shuriken/10/ // http://nw.local/item/give/shuriken/10/ // http://nw.local/item/use/shuriken/156001/ $slugs = $this->parse_slugs($give, $self_use); // Pull the parsed slugs $link_back = $slugs['link_back']; $selfTarget = $slugs['selfTarget']; $item_in = $slugs['item_in']; // Item identifier, either it's id or internal name $in_target = $slugs['in_target']; $give = $slugs['give']; $target = $in_target; if (positive_int($in_target)) { $target_id = positive_int($target); } else { $target_id = get_char_id($target); } $give = in_array($give, array('on', 'Give')); $player = new Player(self_char_id()); $victim_alive = true; $using_item = true; $item_used = true; $stealthLost = false; $error = false; $suicide = false; $kill = false; $repeat = false; $ending_turns = null; $turns_change = null; $turns_to_take = null; $gold_mod = NULL; $result = NULL; $targetResult = NULL; // result message to send to target of item use $targetName = ''; $targetHealth = ''; $bountyMessage = ''; $resultMessage = ''; $alternateResultMessage = ''; if ($item_in == (int) $item_in && is_numeric($item_in)) { // Can be cast to an id. $item = $item_obj = getItemByID($item_in); } elseif (is_string($item_in)) { $item = $item_obj = $this->getItemByIdentity($item_in); } else { $item = null; } if (!is_object($item)) { return new RedirectResponse(WEB_ROOT . 'inventory?error=noitem'); } else { $item_count = $this->itemCount($player->id(), $item); // Check whether use on self is occurring. $self_use = $selfTarget || $target_id === $player->id(); if ($self_use) { $target = $player->name(); $targetObj = $player; } else { if ($target_id) { $targetObj = new Player($target_id); $target = $targetObj->name(); } } $starting_turns = $player->turns; $username_turns = $starting_turns; $username_level = $player->level; if ($targetObj instanceof Player && $targetObj->id()) { $targets_turns = $targetObj->turns; $targets_level = $targetObj->level; $target_hp = $targetObj->health; } else { $targets_turns = $targets_level = $target_hp = null; } $max_power_increase = 10; $level_difference = $targets_level - $username_level; $level_check = $username_level - $targets_level; $near_level_power_increase = $this->nearLevelPowerIncrease($level_difference, $max_power_increase); // Sets the page to link back to. if ($target_id && ($link_back == "" || $link_back == 'player') && $target_id != $player->id()) { $return_to = 'player'; } else { $return_to = 'inventory'; } // Exceptions to the rules, using effects. if ($item->hasEffect('wound')) { // Minor damage by default items. $item->setTargetDamage(rand(1, $item->getMaxDamage())); // DEFAULT, overwritable. // e.g. Shuriken slices, for some reason. if ($item->hasEffect('slice')) { // Minor slicing damage. $item->setTargetDamage(rand(1, max(9, $player->getStrength() - 4)) + $near_level_power_increase); } // Piercing weapon, and actually does any static damage. if ($item->hasEffect('pierce')) { // Minor static piercing damage, e.g. 1-50 plus the near level power increase. $item->setTargetDamage(rand(1, $item->getMaxDamage()) + $near_level_power_increase); } // Increased damage from damaging effects, minimum of 20. if ($item->hasEffect('fire')) { // Major fire damage $item->setTargetDamage(rand(20, $player->getStrength() + 20) + $near_level_power_increase); } } // end of wounds section. // Exclusive speed/slow turn changes. if ($item->hasEffect('slow')) { $item->setTurnChange(-1 * $this->caltropTurnLoss($targets_turns, $near_level_power_increase)); } else { if ($item->hasEffect('speed')) { $item->setTurnChange($item->getMaxTurnChange()); } } $turn_change = $item_obj->getTurnChange(); $itemName = $item->getName(); $itemType = $item->getType(); $article = self::getIndefiniteArticle($item_obj->getName()); if ($give) { $turn_cost = 1; $using_item = false; } else { $turn_cost = $item->getTurnCost(); } // Attack Legal section $attacker = $player->name(); $params = ['required_turns' => $turn_cost, 'ignores_stealth' => $item_obj->ignoresStealth(), 'self_use' => $item->isSelfUsable()]; assert(!!$selfTarget || $attacker != $target); $AttackLegal = new AttackLegal($player, $targetObj, $params); $attack_allowed = $AttackLegal->check(); $attack_error = $AttackLegal->getError(); // *** Any ERRORS prevent attacks happen here *** if (!$attack_allowed) { //Checks for error conditions before starting. $error = 1; } else { if (is_string($item) || $target == "") { $error = 2; } else { if ($item_count < 1) { $error = 3; } else { /**** MAIN SUCCESSFUL USE ****/ if ($give) { $this->giveItem($player->name(), $target, $item->getName()); $alternateResultMessage = "__TARGET__ will receive your {$item->getName()}."; } else { if (!$item->isOtherUsable()) { // If it doesn't do damage or have an effect, don't use up the item. $resultMessage = $result = 'This item is not usable on __TARGET__, so it remains unused.'; $item_used = false; $using_item = false; } else { if ($item->hasEffect('stealth')) { $targetObj->addStatus(STEALTH); $alternateResultMessage = "__TARGET__ is now stealthed."; $targetResult = ' be shrouded in smoke.'; } if ($item->hasEffect('vigor')) { if ($targetObj->hasStatus(STR_UP1)) { $result = "__TARGET__'s body cannot become more vigorous!"; $item_used = false; $using_item = false; } else { $targetObj->addStatus(STR_UP1); $result = "__TARGET__'s muscles experience a strange tingling."; } } if ($item->hasEffect('strength')) { if ($targetObj->hasStatus(STR_UP2)) { $result = "__TARGET__'s body cannot become any stronger!"; $item_used = false; $using_item = false; } else { $targetObj->addStatus(STR_UP2); $result = "__TARGET__ feels a surge of power!"; } } // Slow and speed effects are exclusive. if ($item->hasEffect('slow')) { $turns_change = $item->getTurnChange(); if ($targetObj->hasStatus(SLOW)) { // If the effect is already in play, it will have a decreased effect. $turns_change = ceil($turns_change * 0.3); $alternateResultMessage = "__TARGET__ is already moving slowly."; } else { if ($targetObj->hasStatus(FAST)) { $targetObj->subtractStatus(FAST); $alternateResultMessage = "__TARGET__ is no longer moving quickly."; } else { $targetObj->addStatus(SLOW); $alternateResultMessage = "__TARGET__ begins to move slowly..."; } } if ($turns_change == 0) { $alternateResultMessage .= " You fail to take any turns from __TARGET__."; } $targetResult = " lose " . abs($turns_change) . " turns."; $targetObj->subtractTurns($turns_change); } else { if ($item->hasEffect('speed')) { // Note that speed and slow effects are exclusive. $turns_change = $item->getTurnChange(); if ($targetObj->hasStatus(FAST)) { // If the effect is already in play, it will have a decreased effect. $turns_change = ceil($turns_change * 0.5); $alternateResultMessage = "__TARGET__ is already moving quickly."; } else { if ($targetObj->hasStatus(SLOW)) { $targetObj->subtractStatus(SLOW); $alternateResultMessage = "__TARGET__ is no longer moving slowly."; } else { $targetObj->addStatus(FAST); $alternateResultMessage = "__TARGET__ begins to move quickly!"; } } // Actual turn gain is 1 less because 1 is used each time you use an item. $targetResult = " gain {$turns_change} turns."; $targetObj->changeTurns($turns_change); // Still adding some turns. } } if ($item->getTargetDamage() > 0) { // *** HP Altering *** $alternateResultMessage .= " __TARGET__ takes " . $item->getTargetDamage() . " damage."; if ($self_use) { $result .= "You take " . $item->getTargetDamage() . " damage!"; } else { if (strlen($targetResult) > 0) { $targetResult .= " You also"; // Join multiple targetResult messages. } $targetResult .= " take " . $item->getTargetDamage() . " damage!"; } $victim_alive = $targetObj->subtractHealth($item->getTargetDamage()); // This is the other location that $victim_alive is set, to determine whether the death proceedings should occur. } if ($item->hasEffect('death')) { $targetObj->death(); $resultMessage = "The life force drains from __TARGET__ and they drop dead before your eyes!"; $victim_alive = false; $targetResult = " be drained of your life-force and die!"; $gold_mod = 0.25; //The Dim Mak takes away 25% of a targets' gold. } if ($turns_change !== null) { // Even if $turns_change is set to zero, let them know that. if ($turns_change > 0) { $resultMessage .= "__TARGET__ has gained back {$turns_change} turns!"; } else { if ($turns_change === 0) { $resultMessage .= "__TARGET__ did not lose any turns!"; } else { $resultMessage .= "__TARGET__ has lost " . abs($turns_change) . " turns!"; } if ($targetObj->turns <= 0) { // Message when a target has no more turns to remove. $resultMessage .= " __TARGET__ no longer has any turns."; } } } if (empty($resultMessage) && !empty($result)) { $resultMessage = $result; } if (!$victim_alive) { // Target was killed by the item. if (!$self_use) { // *** SUCCESSFUL KILL, not self-use of an item *** $attacker_id = $player->hasStatus(STEALTH) ? "A Stealthed Ninja" : $player->name(); if (!$gold_mod) { $gold_mod = 0.15; } $initial_gold = $targetObj->gold(); $loot = floor($gold_mod * $initial_gold); $targetObj->set_gold($initial_gold - $loot); $player->set_gold($player->gold() + $loot); $player->save(); $targetObj->save(); $player->addKills(1); $kill = true; $bountyMessage = Combat::runBountyExchange($player->name(), $target); //Rewards or increases bounty. } else { $loot = 0; $suicide = true; } // Send mails if the target was killed. $this->sendKillMails($player->name(), $target, $attacker_id, $article, $item->getName(), $loot); } else { // They weren't killed. $attacker_id = $player->name(); } if (!$self_use && $item_used) { if (!$targetResult) { error_log('Debug: Issue 226 - An attack was made using ' . $item->getName() . ', but no targetResult message was set.'); } // Notify targets when they get an item used on them. $message_to_target = "{$attacker_id} has used {$article} {$item->getName()} on you"; if ($targetResult) { $message_to_target .= " and caused you to {$targetResult}"; } else { $message_to_target .= '.'; } send_event($player->id(), $target_id, str_replace(' ', ' ', $message_to_target)); } // Unstealth if (!$item->isCovert() && !$item->hasEffect('stealth') && $player->hasStatus(STEALTH)) { //non-covert acts $player->subtractStatus(STEALTH); $stealthLost = true; } else { $stealthLost = false; } } } $targetName = $targetObj->uname; $targetHealth = $targetObj->health; $turns_to_take = 1; if ($item_used) { // *** remove Item *** removeItem($player->id(), $item->getName(), 1); // *** Decreases the item amount by 1. } if ($victim_alive && $using_item) { $repeat = true; } } } } // *** Take away at least one turn even on attacks that fail to prevent page reload spamming *** if ($turns_to_take < 1) { $turns_to_take = 1; } $ending_turns = $player->subtractTurns($turns_to_take); assert($item->hasEffect('speed') || $ending_turns < $starting_turns || $starting_turns == 0); return ['template' => 'inventory_mod.tpl', 'title' => 'Use Item', 'parts' => get_defined_vars(), 'options' => ['body_classes' => 'inventory-use', 'quickstat' => 'player']]; } // Item was not valid object }
} $attacking_player->addKills($killpoints); // Attacker gains their killpoints. $target_player->death(); if (!$simultaneousKill) { // This stuff only happens if you don't die also. $loot = floor($gold_mod * $target_player->gold()); // Add the wrath health regain to the attacker. if (isset($wrath_regain)) { $attacking_player->changeHealth($wrath_regain); } } $target_msg = "DEATH: You've been killed by {$attacker} and lost {$loot} gold!"; sendMessage($attacker, $target, $target_msg); // Stopped telling attackers when they win a duel. $bounty_result = Combat::runBountyExchange($attacker, $target); // *** Determines bounty for dueling. *** } if ($attackerHealthRemaining < 1) { // *** DEFENDER KILLS ATTACKER! *** if ($simultaneousKill = $attackerHealthRemaining < 1) { // *** If both died at the same time. *** } else { $victor = $target; $loser = $attacker; } $attacker_died = true; $defenderKillpoints = 1; if ($duel) { // *** if they were dueling when they died *** $duel_log_msg = "{$attacker} has dueled {$target} and lost at " . date("F j, Y, g:i a");
/** * Use an item on a target * * http://nw.local/item/use/shuriken/10/ * * @return Response * @note * /use/ is aliased to useItem externally because use is a php reserved keyword */ public function useItem(Container $p_dependencies) { $slugs = $this->parseSlugs(); $target = $this->findPlayer($slugs['in_target']); $player = Player::find(SessionFactory::getSession()->get('player_id')); $inventory = new Inventory($player); $had_stealth = $player->hasStatus(STEALTH); $error = false; $turns_to_take = 1; // Take away one turn even on attacks that fail to prevent page reload spamming $bounty_message = ''; $display_message = ''; $extra_message = ''; $attacker_label = $player->name(); $loot = null; try { $item = $this->findItem($slugs['item_in']); $article = $item ? self::getIndefiniteArticle($item->getName()) : ''; } catch (\InvalidArgumentException $e) { return new RedirectResponse(WEB_ROOT . 'inventory?error=noitem'); } if (empty($target)) { $error = 2; } else { if ($this->itemCount($player, $item) < 1) { $error = 3; } else { if ($target->id() === $player->id()) { return $this->selfUse(); } else { $params = ['required_turns' => $item->getTurnCost(), 'ignores_stealth' => $item->ignoresStealth()]; $attack_legal = new AttackLegal($player, $target, $params); if (!$attack_legal->check()) { $error = 1; $display_message = $attack_legal->getError(); } else { if (!$item->isOtherUsable()) { $error = 1; $display_message = 'This item cannot be used on others!'; } else { $result = $this->applyItemEffects($player, $target, $item); if ($result['success']) { $message_to_target = "{$attacker_label} has used {$article} " . $item->getName() . " on you{$result['notice']}"; Event::create($player->id(), $target->id(), str_replace(' ', ' ', $message_to_target)); $inventory->remove($item->identity(), 1); if ($target->health <= 0) { // Target was killed by the item $attacker_label = $player->hasStatus(STEALTH) ? "A Stealthed Ninja" : $player->name(); $gold_mod = $item->hasEffect('death') ? 0.25 : 0.15; $loot = floor($gold_mod * $target->gold); $target->setGold($target->gold - $loot); $player->setGold($player->gold + $loot); $player->addKills(1); $bounty_message = Combat::runBountyExchange($player, $target); //Rewards or increases bounty. $this->sendKillMails($player, $target, $attacker_label, $article, $item->getName(), $loot); } } $display_message = $result['message']; $extra_message = $result['extra_message']; } } $player->changeTurns(-1 * $turns_to_take); $target->save(); $player->save(); } } } return $this->renderUse(['action' => 'use', 'return_to' => in_array(RequestWrapper::getPostOrGet('link_back'), ['', 'player']) ? 'player' : 'inventory', 'error' => $error, 'target' => $target, 'resultMessage' => $display_message, 'alternateResultMessage' => $extra_message, 'stealthLost' => $had_stealth && $player->hasStatus(STEALTH), 'repeat' => $target->health > 0 && empty($error), 'item' => $item, 'bountyMessage' => $bounty_message, 'article' => $article, 'loot' => $loot]); }