arg === null) { return false; } if (!key_exists('destCityID', $this->arg)) { return false; } if (!key_exists($this->arg['destCityID'], CityConst::all())) { return false; } $this->arg = [ 'destCityID' => $this->arg['destCityID'] ]; return true; } protected function calcSabotageAttackProb(): float { $statType = static::$statType; $general = $this->generalObj; $nation = $this->nation; if ($statType === 'leadership') { $genScore = $general->getLeadership(); } else if ($statType === 'strength') { $genScore = $general->getStrength(); } else if ($statType === 'intel') { $genScore = $general->getIntel(); } else { throw new \sammo\MustNotBeReachedException(); } $prob = $genScore / GameConst::$sabotageProbCoefByStat; $prob = $general->onCalcDomestic('계략', 'success', $prob); return $prob; } protected function calcSabotageDefenceProb(array $destCityGeneralList): float { $statType = static::$statType; $destCity = $this->destCity; $destNation = $this->destNation; $destNationID = $destNation['nation']; $maxGenScore = 0; $probCorrection = 0; $affectGeneralCount = 0; foreach ($destCityGeneralList as $destGeneral) { /** @var General $destGeneral */ if ($destGeneral->getNationID() != $destNationID) { continue; } $affectGeneralCount++; if ($statType === 'leadership') { $genScore = $destGeneral->getLeadership(); } else if ($statType === 'strength') { $genScore = $destGeneral->getStrength(); } else if ($statType === 'intel') { $genScore = $destGeneral->getIntel(); } else { throw new \sammo\MustNotBeReachedException(); } $maxGenScore = max($maxGenScore, $genScore); $probCorrection = $destGeneral->onCalcStat($destGeneral, 'sabotageDefence', $probCorrection); } $prob = $maxGenScore / GameConst::$sabotageProbCoefByStat; $prob += $probCorrection; $prob += (log($affectGeneralCount + 1, 2) - 1.25) * GameConst::$sabotageDefenceCoefByGeneralCnt; $prob += $destCity['secu'] / $destCity['secu_max'] / 5; //최대 20%p $prob += $destCity['supply'] ? 0.1 : 0; return $prob; } protected function init() { $general = $this->generalObj; $this->setCity(); [$reqGold, $reqRice] = $this->getCost(); $this->minConditionConstraints = [ ConstraintHelper::NotBeNeutral(), ConstraintHelper::OccupiedCity(), ConstraintHelper::SuppliedCity(), ConstraintHelper::ReqGeneralGold($reqGold), ConstraintHelper::ReqGeneralRice($reqRice), ]; } protected function initWithArg() { $this->setNation(); $this->setDestCity($this->arg['destCityID']); $this->setDestNation($this->destCity['nation']); [$reqGold, $reqRice] = $this->getCost(); $this->fullConditionConstraints = [ ConstraintHelper::NotBeNeutral(), ConstraintHelper::OccupiedCity(), ConstraintHelper::SuppliedCity(), ConstraintHelper::NotOccupiedDestCity(), ConstraintHelper::NotNeutralDestCity(), ConstraintHelper::ReqGeneralGold($reqGold), ConstraintHelper::ReqGeneralRice($reqRice), ConstraintHelper::DisallowDiplomacyBetweenStatus( [7 => '불가침국입니다.'] ), ]; } public function getCommandDetailTitle(): string { $name = $this->getName(); $statTypeBase = [ 'leadership' => '통솔경험', 'strength' => '무력경험', 'intel' => '지력경험', ]; $statType = $statTypeBase[static::$statType]; [$reqGold, $reqRice] = $this->getCost(); $title = "{$name}({$statType}"; if ($reqGold > 0) { $title .= ", 자금{$reqGold}"; } if ($reqRice > 0) { $title .= ", 군량{$reqRice}"; } $title .= ')'; return $title; } public function getCost(): array { $env = $this->env; $cost = $env['develcost'] * 5; return [$cost, $cost]; } public function getPreReqTurn(): int { return 0; } public function getPostReqTurn(): int { return 0; } public function getBrief(): string { $commandName = $this->getName(); $destCityName = CityConst::byID($this->arg['destCityID'])->name; return "【{$destCityName}】에 {$commandName}실행"; } public function getFailString(): string { $commandName = $this->getName(); $failReason = $this->testFullConditionMet(); if ($failReason === null) { throw new \RuntimeException('실행 가능한 커맨드에 대해 실패 이유를 수집'); } $destCityName = CityConst::byID($this->arg['destCityID'])->name; return "{$failReason} {$destCityName}에 {$commandName} 실패."; } protected function affectDestCity(RandUtil $rng, int $injuryCount) { $general = $this->generalObj; $date = $general->getTurnTime($general::TURNTIME_HM); $logger = $general->getLogger(); $destCity = $this->destCity; $destCityName = $destCity['name']; $destCityID = $destCity['city']; $commandName = $this->getName(); $agriAmount = Util::valueFit($rng->nextRangeInt(GameConst::$sabotageDamageMin, GameConst::$sabotageDamageMax), null, $destCity['agri']); $commAmount = Util::valueFit($rng->nextRangeInt(GameConst::$sabotageDamageMin, GameConst::$sabotageDamageMax), null, $destCity['comm']); $destCity['agri'] -= $agriAmount; $destCity['comm'] -= $commAmount; DB::db()->update('city', [ 'state' => 32, 'agri' => $destCity['agri'], 'comm' => $destCity['comm'] ], 'city=%i', $destCityID); $agriAmountText = number_format($agriAmount); $commAmountText = number_format($commAmount); $josaYi = JosaUtil::pick($destCityName, '이'); $logger->pushGlobalActionLog("{$destCityName}{$josaYi} 불타고 있습니다."); $josaYi = JosaUtil::pick($commandName, '이'); $logger->pushGeneralActionLog("{$destCityName}에 {$commandName}{$josaYi} 성공했습니다. <1>$date"); $logger->pushGeneralActionLog( "도시의 농업이 {$agriAmountText}, 상업이 {$commAmountText}만큼 감소하고, 장수 {$injuryCount}명이 부상 당했습니다.", ActionLogger::PLAIN ); } public function run(\Sammo\RandUtil $rng): bool { if (!$this->hasFullConditionMet()) { throw new \RuntimeException('불가능한 커맨드를 강제로 실행 시도'); } $db = DB::db(); $env = $this->env; $general = $this->generalObj; $date = $general->getTurnTime($general::TURNTIME_HM); $destCity = $this->destCity; $destCityName = $destCity['name']; $destCityID = $destCity['city']; $destNationID = $destCity['nation']; $commandName = $this->getName(); $statType = static::$statType; $logger = $general->getLogger(); $dist = searchDistance($general->getCityID(), 5, false)[$destCityID] ?? 99; $destCityGeneralList = []; $cityGeneralID = $db->queryFirstColumn('SELECT no FROM general WHERE city = %i AND nation = %i', $destCityID, $destNationID); $destCityGeneralList = General::createObjListFromDB($cityGeneralID, ['name', 'city', 'nation', 'officer_level', 'leadership', 'horse', 'strength', 'weapon', 'intel', 'book', 'item', 'last_turn', 'injury', 'special', 'special2', 'injury', 'crewtype', 'crew', 'atmos', 'train']); foreach ($destCityGeneralList as &$destCityGeneral) { $destCityGeneral->setRawCity($this->destCity); unset($destCityGeneral); } //계략에 성공할 경우 logger를 사용해야 하므로 해야하므로, 미리 초기화한다. //실패하면 날리는거지 뭐~ $prob = GameConst::$sabotageDefaultProb + $this->calcSabotageAttackProb() - $this->calcSabotageDefenceProb($destCityGeneralList); $prob /= $dist; $prob = Util::valueFit($prob, 0, 0.5); if (!$rng->nextBool($prob)) { $josaYi = JosaUtil::pick($commandName, '이'); $logger->pushGeneralActionLog("{$destCityName}에 {$commandName}{$josaYi} 실패했습니다. <1>$date"); $exp = $rng->nextRangeInt(1, 100); $ded = $rng->nextRangeInt(1, 70); [$reqGold, $reqRice] = $this->getCost(); $general->increaseVarWithLimit('gold', -$reqGold, 0); $general->increaseVarWithLimit('rice', -$reqRice, 0); $general->addExperience($exp); $general->addDedication($ded); $general->increaseVar($statType . '_exp', 1); $this->setResultTurn(new LastTurn(static::getName(), $this->arg)); $general->checkStatChange(); $general->applyDB($db); return false; } if (static::$injuryGeneral) { $injuryCount = \sammo\SabotageInjury($rng, $destCityGeneralList, '계략'); } else { $injuryCount = 0; } $this->affectDestCity($rng, $injuryCount); $itemObj = $general->getItem(); if ($itemObj->tryConsumeNow($general, 'GeneralCommand', '계략')) { $itemName = $itemObj->getName(); $itemRawName = $itemObj->getRawName(); $josaUl = JosaUtil::pick($itemRawName, '을'); $logger->pushGeneralActionLog("{$itemName}{$josaUl} 사용!", ActionLogger::PLAIN); $general->deleteItem(); } $exp = $rng->nextRangeInt(201, 300); $ded = $rng->nextRangeInt(141, 210); [$reqGold, $reqRice] = $this->getCost(); $general->increaseVarWithLimit('gold', -$reqGold, 0); $general->increaseVarWithLimit('rice', -$reqRice, 0); $general->addExperience($exp); $general->addDedication($ded); $general->increaseVar($statType . '_exp', 1); $general->increaseRankVar(RankColumn::firenum, 1); $this->setResultTurn(new LastTurn(static::getName(), $this->arg)); StaticEventHandler::handleEvent($this->generalObj, $this->destGeneralObj, $this::class, $this->env, $this->arg ?? []); $general->checkStatChange(); $general->applyDB($db); return true; } public function exportJSVars(): array { return [ 'procRes' => [ 'cities' => \sammo\JSOptionsForCities(), 'distanceList' => \sammo\JSCitiesBasedOnDistance($this->generalObj->getCityID(), 3), ], ]; } }