1, 'warnum' => 1, 'killnum' => 1, 'deathnum' => 1, 'killcrew' => 1, 'deathcrew' => 1, 'ttw' => 1, 'ttd' => 1, 'ttl' => 1, 'ttg' => 1, 'ttp' => 1, 'tlw' => 1, 'tld' => 1, 'tll' => 1, 'tlg' => 1, 'tlp' => 1, 'tsw' => 1, 'tsd' => 1, 'tsl' => 1, 'tsg' => 1, 'tsp' => 1, 'tiw' => 1, 'tid' => 1, 'til' => 1, 'tig' => 1, 'tip' => 1, 'betwin' => 1, 'betgold' => 1, 'betwingold' => 1, 'killcrew_person' => 1, 'deathcrew_person' => 1, 'occupied' => 1, ]; const INHERITANCE_KEY = [ 'previous' => [true, 1, '기존 포인트'], 'lived_month' => [true, 1, '생존'], 'max_belong' => [false, 10, '최대 임관년 수'], 'max_domestic_critical' => [true, 1, '최대 연속 내정 성공'], //'snipe_combat' => [true, 10, '병종 상성 우위 횟수'], 'combat' => [['rank', 'warnum'], 5, '전투 횟수'], 'sabotage' => [['rank', 'firenum'], 20, '계략 성공 횟수'], 'unifier' => [true, 1, '천통 기여'], 'dex' => [false, 0.001, '숙련도'], 'tournament' => [true, 1, '토너먼트'], 'betting' => [false, 10, '베팅 당첨'], ]; const TURNTIME_FULL_MS = -1; const TURNTIME_FULL = 0; const TURNTIME_HMS = 1; const TURNTIME_HM = 2; protected $calcCache = []; protected static $prohibitedDirectUpdateVars = [ //Reason: iAction 'leadership' => 1, 'power' => 1, 'intel' => 1, 'nation' => 2, 'officer_level' => 1, //NOTE: officerLevelObj로 인해 국가의 '레벨'이 바뀌는 것도 조심해야 하나, 국가 레벨의 변경은 월 초/말에만 일어남. 'special' => 1, 'special2' => 1, 'personal' => 1, 'horse' => 1, 'weapon' => 1, 'book' => 1, 'item' => 1 ]; /** * @param array $raw DB row값. * @param null|array $city DB city 테이블의 row값 * @param int|null $year 게임 연도 * @param int|null $month 게임 월 * @param bool $fullConstruct iAction, 및 ActionLogger 초기화 여부, false인 경우 no, name, city, nation, officer_level 정도로 초기화 가능 */ public function __construct(array $raw, ?array $rawRank, ?array $city, ?array $nation, ?int $year, ?int $month, bool $fullConstruct = true) { //TODO: 밖에서 가져오도록 하면 버그 확률이 높아짐. 필요한 raw 값을 직접 구해야함. if ($nation === null) { $nation = getNationStaticInfo($raw['nation']); } $this->raw = $raw; $this->rawCity = $city; $this->resultTurn = new LastTurn(); if (key_exists('last_turn', $this->raw)) { $this->lastTurn = LastTurn::fromJson($this->raw['last_turn']); $this->resultTurn = $this->lastTurn->duplicate(); } if ($year !== null && $month !== null) { $this->initLogger($year, $month); } if ($rawRank) { $this->rankVarRead = $rawRank; } if (!$fullConstruct) { return; } $this->nationType = buildNationTypeClass($nation['type']); $this->officerLevelObj = new TriggerOfficerLevel($this->raw, $nation['level']); $this->specialDomesticObj = buildGeneralSpecialDomesticClass($raw['special']); $this->specialWarObj = buildGeneralSpecialWarClass($raw['special2']); $this->personalityObj = buildPersonalityClass($raw['personal']); $this->itemObjs['horse'] = buildItemClass($raw['horse']); $this->itemObjs['weapon'] = buildItemClass($raw['weapon']); $this->itemObjs['book'] = buildItemClass($raw['book']); $this->itemObjs['item'] = buildItemClass($raw['item']); if (key_exists('aux', $this->raw)) { $rawInheritBuff = $this->getAuxVar('inheritBuff'); if ($rawInheritBuff !== null) { $this->inheritBuffObj = new TriggerInheritBuff($rawInheritBuff); } } } function initLogger(int $year, int $month) { $this->logger = new ActionLogger( $this->getVar('no'), $this->getVar('nation'), $year, $month, false ); } function getTurnTime(int $short = self::TURNTIME_FULL_MS) { return [ self::TURNTIME_FULL_MS => function ($turntime) { return $turntime; }, self::TURNTIME_FULL => function ($turntime) { return substr($turntime, 0, 19); }, self::TURNTIME_HMS => function ($turntime) { return substr($turntime, 11, 8); }, self::TURNTIME_HM => function ($turntime) { return substr($turntime, 11, 5); }, ][$short]($this->getVar('turntime')); } function setItem(string $itemKey = 'item', ?string $itemCode) { if ($itemCode === null) { $this->deleteItem($itemKey); } $this->setVar($itemKey, $itemCode); $this->itemObjs[$itemKey] = buildItemClass($itemCode); } function deleteItem(string $itemKey = 'item') { $this->setVar($itemKey, 'None'); $this->itemObjs[$itemKey] = new ActionItem\None(); } function getItem(string $itemKey = 'item'): BaseItem { return $this->itemObjs[$itemKey]; } function getNPCType(): int { return $this->raw['npc']; } /** @return BaseItem[] */ function getItems(): array { return $this->itemObjs; } function getPersonality(): iAction { return $this->personalityObj; } function getSpecialDomestic(): iAction { return $this->specialDomesticObj; } function getSpecialWar(): iAction { return $this->specialWarObj; } function getLastTurn(): LastTurn { return $this->lastTurn; } function _setResultTurn(LastTurn $resultTurn) { $this->resultTurn = $resultTurn; } function getResultTurn(): LastTurn { return $this->resultTurn; } function clearActivatedSkill() { foreach ($this->activatedSkill as $skillName => $state) { if (!$state) { continue; } if (!key_exists($skillName, $this->logActivatedSkill)) { $this->logActivatedSkill[$skillName] = 1; } else { $this->logActivatedSkill[$skillName] += 1; } } $this->activatedSkill = []; } function getActivatedSkillLog(): array { return $this->logActivatedSkill; } function hasActivatedSkill(string $skillName): bool { return $this->activatedSkill[$skillName] ?? false; } function activateSkill(...$skillNames) { foreach ($skillNames as $skillName) { $this->activatedSkill[$skillName] = true; } } function deactivateSkill(...$skillNames) { foreach ($skillNames as $skillName) { $this->activatedSkill[$skillName] = false; } } function getName(): string { return $this->raw['name']; } function getInfo(): string { //iAction용 info로는 적절하지 않음 return ''; } function getID(): int { return $this->raw['no']; } function getRawCity(): ?array { return $this->rawCity; } function setRawCity(?array $city) { $this->rawCity = $city; } function getCityID(): int { return $this->raw['city']; } function getNationID(): int { return $this->raw['nation']; } function getStaticNation(): array { return getNationStaticInfo($this->raw['nation']); } function getLogger(): ?ActionLogger { return $this->logger; } public function getNationTypeObj(): iAction { return $this->nationType; } public function getOfficerLevelObj(): iAction { return $this->officerLevelObj; } function getCrewTypeObj(): GameUnitDetail { $crewType = GameUnitConst::byID($this->getVar('crewtype')); if ($crewType === null) { throw new \InvalidArgumentException('Invalid CrewType:' . $this->getVar('crewtype')); } return $crewType; } function calcRecentWarTurn(int $turnTerm): int { $cacheKey = "recent_war_turn_{$turnTerm}"; if (key_exists($cacheKey, $this->calcCache)) { return $this->calcCache[$cacheKey]; } if (!$this->getVar('recent_war')) { $result = 12 * 1000; $this->calcCache[$cacheKey] = $result; return $result; } $recwar = new \DateTimeImmutable($this->getVar('recent_war')); $turnNow = new \DateTimeImmutable($this->getVar('turntime')); $secDiff = TimeUtil::DateIntervalToSeconds($recwar->diff($turnNow)); if ($secDiff <= 0) { $this->calcCache[$cacheKey] = 0; return 0; } $result = intdiv(Util::toInt($secDiff), 60 * $turnTerm); $this->calcCache[$cacheKey] = $result; return $result; } function getReservedTurn(int $turnIdx, array $env): GeneralCommand { $db = DB::db(); $rawCmd = $db->queryFirstRow('SELECT * FROM general_turn WHERE general_id = %i AND turn_idx = %i', $this->getID(), $turnIdx); if (!$rawCmd) { return buildGeneralCommandClass(null, $this, $env); } return buildGeneralCommandClass($rawCmd['action'], $this, $env, Json::decode($rawCmd['arg'] ?? null)); } /** * @param General[] $generalList * @param int $turnIdxFrom [$turnIdxFrom, $turnIdxTo) * @param int $turnIdxTo [$turnIdxFrom, $turnIdxTo) * @param array $env * @return GeneralCommand[] */ public function getReservedTurnList(int $turnIdxFrom, int $turnIdxTo, array $env) { if ($turnIdxFrom < 0 || $turnIdxFrom >= GameConst::$maxTurn) { throw new \OutOfRangeException('$turnIdxFrom 범위 초과' . $turnIdxFrom); } if ($turnIdxTo <= $turnIdxFrom || GameConst::$maxTurn < $turnIdxTo) { throw new \OutOfRangeException('$turnIdxTo 범위 초과' . $turnIdxTo); } $db = DB::db(); $generalID = $this->getID(); $result = []; $rawCmds = $db->queryFirstRow('SELECT * FROM general_turn WHERE general_id = %i AND %i <= turn_idx AND turn_idx < %i ORDER BY turn_idx ASC', $generalID, $turnIdxFrom, $turnIdxTo); if (!$rawCmds) { foreach (Util::range($turnIdxFrom, $turnIdxTo) as $turnIdx) { $result[$turnIdx] = buildGeneralCommandClass(null, $this, $env); } return $result; } foreach ($rawCmds as $turnIdx => $rawCmd) { $result[$turnIdx] = buildGeneralCommandClass($rawCmd['action'], $this, $env, Json::decode($rawCmd['arg'] ?? null)); } return $result; } /** * 장수의 스탯을 계산해옴 * * @param string $statName 스탯값, leadership, strength, intel 가능 * @param bool $withInjury 부상값 사용 여부 * @param bool $withIActionObj 아이템, 특성, 성격 등 보정치 적용 여부 * @param bool $withStatAdjust 능력치간 보정 사용 여부 * @param bool $useFloor 내림 사용 여부, false시 float 값을 반환할 수도 있음 * * @return int|float 계산된 능력치 */ protected function getStatValue(string $statName, $withInjury = true, $withIActionObj = true, $withStatAdjust = true, $useFloor = true): float { $cKey = "{$statName}_{$withInjury}_{$withIActionObj}_{$withStatAdjust}"; if (key_exists($cKey, $this->calcCache)) { $statValue = $this->calcCache[$cKey]; if ($useFloor) { return Util::toInt($statValue); } return $statValue; } $statValue = $this->getVar($statName); if ($withInjury) { $statValue *= (100 - $this->getVar('injury')) / 100; } if ($withStatAdjust) { if ($statName === 'strength') { $statValue += Util::round($this->getStatValue('intel', $withInjury, $withIActionObj, false, false) / 4); } else if ($statName === 'intel') { $statValue += Util::round($this->getStatValue('strength', $withInjury, $withIActionObj, false, false) / 4); } } if ($withIActionObj) { foreach ([ $this->nationType, $this->officerLevelObj, $this->specialDomesticObj, $this->specialWarObj, $this->personalityObj, $this->inheritBuffObj, ] as $actionObj) { if ($actionObj !== null) { $statValue = $actionObj->onCalcStat($this, $statName, $statValue); } } foreach ($this->itemObjs as $actionObj) { if ($actionObj !== null) { $statValue = $actionObj->onCalcStat($this, $statName, $statValue); } } } $this->calcCache[$cKey] = $statValue; if ($useFloor) { return Util::toInt($statValue); } return $statValue; } function getLeadership($withInjury = true, $withIActionObj = true, $withStatAdjust = true, $useFloor = true): float { return $this->getStatValue('leadership', $withInjury, $withIActionObj, $withStatAdjust, $useFloor); } function getStrength($withInjury = true, $withIActionObj = true, $withStatAdjust = true, $useFloor = true): float { return $this->getStatValue('strength', $withInjury, $withIActionObj, $withStatAdjust, $useFloor); } function getIntel($withInjury = true, $withIActionObj = true, $withStatAdjust = true, $useFloor = true): float { return $this->getStatValue('intel', $withInjury, $withIActionObj, $withStatAdjust, $useFloor); } function getDex(GameUnitDetail $crewType) { $armType = $crewType->armType; if ($armType == GameUnitConst::T_CASTLE) { $armType = GameUnitConst::T_SIEGE; } return $this->getVar("dex{$armType}"); } function addDex(GameUnitDetail $crewType, float $exp, bool $affectTrainAtmos = false) { $armType = $crewType->armType; if ($armType == GameUnitConst::T_CASTLE) { $armType = GameUnitConst::T_SIEGE; } if ($armType < 0) { return; } if ($armType == GameUnitConst::T_WIZARD) { $exp *= 0.9; } else if ($armType == GameUnitConst::T_SIEGE) { $exp *= 0.9; } if ($affectTrainAtmos) { $exp *= ($this->getVar('train') + $this->getVar('atmos')) / 200; } $dexType = "dex{$armType}"; $this->increaseVar($dexType, $exp); } function addExperience(float $experience, bool $affectTrigger = true) { if ($affectTrigger) { $experience = $this->onCalcStat($this, 'experience', $experience); } $this->increaseVar('experience', $experience); $nextExpLevel = getExpLevel($this->getVar('experience')); $comp = $nextExpLevel <=> $this->getVar('explevel'); if ($comp === 0) { return; } $this->updateVar('explevel', $nextExpLevel); $josaRo = JosaUtil::pick($nextExpLevel, '로'); if ($comp > 0) { $this->getLogger()->pushGeneralActionLog("Lv {$nextExpLevel}{$josaRo} 레벨업!", ActionLogger::PLAIN); } else { $this->getLogger()->pushGeneralActionLog("Lv {$nextExpLevel}{$josaRo} 레벨다운!", ActionLogger::PLAIN); } } function addDedication(float $dedication, bool $affectTrigger = true) { if ($affectTrigger) { $dedication = $this->onCalcStat($this, 'dedication', $dedication); } $this->increaseVar('dedication', $dedication); $nextDedLevel = getDedLevel($this->getVar('dedication')); $comp = $nextDedLevel <=> $this->getVar('dedlevel'); if ($comp === 0) { return; } $this->updateVar('dedlevel', $nextDedLevel); $dedLevelText = getDedLevelText($nextDedLevel); $billText = number_format(getBillByLevel($nextDedLevel)); $josaRoDed = JosaUtil::pick($dedLevelText, '로'); $josaRoBill = JosaUtil::pick($billText, '로'); if ($comp > 0) { $this->getLogger()->pushGeneralActionLog("{$dedLevelText}{$josaRoDed} 승급하여 봉록이 {$billText}{$josaRoBill} 상승했습니다!", ActionLogger::PLAIN); } else { $this->getLogger()->pushGeneralActionLog("{$dedLevelText}{$josaRoDed} 강등되어 봉록이 {$billText}{$josaRoBill} 하락했습니다!", ActionLogger::PLAIN); } } function updateVar(string $key, $value) { if (($this->raw[$key] ?? null) === $value) { return; } if (!key_exists($key, $this->updatedVar)) { $this->updatedVar[$key] = true; } $this->raw[$key] = $value; $this->calcCache = []; } /** * @param \MeekroDB $db */ function kill($db, bool $sendDyingMessage = true, ?string $dyingMessage = null) { $generalID = $this->getID(); $logger = $this->getLogger(); $generalName = $this->getName(); //유산포인트 관련 항목 환불 if ($this->getNPCType() < 2) { $refundPoint = 0; $userID = $this->getVar('owner'); $userLogger = new UserLogger($userID); if ($this->getAuxVar('inheritRandomUnique')) { $this->setAuxVar('inheritRandomUnique', null); $userLogger->push(sprintf("사망으로 랜덤 유니크 구입 %d 포인트 반환", GameConst::$inheritItemRandomPoint), "inheritPoint"); $refundPoint += GameConst::$inheritItemRandomPoint; } $itemTrials = $this->getAuxVar('inheritUniqueTrial') ?? []; foreach (array_keys($itemTrials) as $itemKey) { $trialStor = KVStorage::getStorage($db, "ut_{$itemKey}"); $ownTrial = $trialStor->getValue("u{$userID}"); $itemObj = buildItemClass($itemKey); $itemName = $itemObj->getName(); if (!$ownTrial) { continue; } [,, $amount] = $ownTrial; $trialStor->deleteValue("u{$userID}"); $userLogger->push("사망으로 {$itemName} 입찰에 사용한 {$amount} 포인트 반환", "inheritPoint"); $refundPoint += $amount; } if($this->getAuxVar('inheritSpecificSpecialWar')){ $this->setAuxVar('inheritSpecificSpecialWar', null); $userLogger->push(sprintf("사망으로 전투 특기 지정 %d 포인트 반환", GameConst::$inheritSpecificSpecialPoint), "inheritPoint"); $refundPoint += GameConst::$inheritSpecificSpecialPoint; } if ($refundPoint > 0) { $this->increaseInheritancePoint('previous', $refundPoint); } $this->mergeTotalInheritancePoint(); } // 군주였으면 유지 이음 $officerLevel = $this->getVar('officer_level'); if ($officerLevel == 12) { nextRuler($this); $this->setVar('officer_level', 1); } // 부대 처리 $troopLeaderID = $this->getVar('troop'); if ($troopLeaderID == $generalID) { //부대장일 경우 // 모두 탈퇴 $db->update('general', [ 'troop' => 0 ], 'troop=%i', $troopLeaderID); // 부대 삭제 $db->delete('troop', 'troop_leader=%i', $troopLeaderID); } if ($sendDyingMessage) { if ($dyingMessage) { $logger->pushGlobalActionLog($dyingMessage); } else { $dyingMessageObj = new TextDecoration\DyingMessage($this); $logger->pushGlobalActionLog($dyingMessageObj->getText()); } $logger->flush(); } $db->update('select_pool', [ 'general_id' => null, 'owner' => null, 'reserved_until' => null, ], 'general_id=%i', $generalID); $db->delete('general', 'no=%i', $generalID); $db->delete('general_turn', 'general_id=%i', $generalID); $db->delete('rank_data', 'general_id=%i', $generalID); $db->delete('betting', 'general_id=%i', $generalID); $this->updatedVar = []; $db->update('nation', [ 'gennum' => $db->sqleval('gennum - 1') ], 'nation=%i', $this->getVar('nation')); } function rebirth() { $logger = $this->getLogger(); $generalName = $this->getName(); $ownerID = $this->getVar('owner'); if ($ownerID) { $this->mergeTotalInheritancePoint(); resetInheritanceUser($ownerID, true); } $this->multiplyVarWithLimit('leadership', 0.85, 10); $this->multiplyVarWithLimit('strength', 0.85, 10); $this->multiplyVarWithLimit('intel', 0.85, 10); $this->setVar('injury', 0); $this->multiplyVar('experience', 0.5); $this->multiplyVar('dedication', 0.5); $this->setVar('age', 20); $this->setVar('specage', 0); $this->setVar('specage2', 0); $this->multiplyVar('dex1', 0.5); $this->multiplyVar('dex2', 0.5); $this->multiplyVar('dex3', 0.5); $this->multiplyVar('dex4', 0.5); $this->multiplyVar('dex5', 0.5); foreach (array_keys(static::RANK_COLUMN) as $rankKey) { $this->setRankVar($rankKey, 0); } $josaYi = JosaUtil::pick($generalName, '이'); $logger->pushGlobalActionLog("{$generalName}{$josaYi} 은퇴하고 그 자손이 유지를 이어받았습니다."); $logger->pushGeneralActionLog('나이가 들어 은퇴하고 자손에게 자리를 물려줍니다.', ActionLogger::PLAIN); $logger->pushGeneralHistoryLog('나이가 들어 은퇴하고, 자손에게 관직을 물려줌'); } function increaseRankVar(string $key, int $value) { if (!key_exists($key, static::RANK_COLUMN)) { throw new \InvalidArgumentException('올바르지 않은 인자 :' . $key); } if (key_exists($key, $this->rankVarSet)) { $this->rankVarSet[$key] += $value; return; } if (key_exists($key, $this->rankVarRead)) { $this->rankVarSet[$key] = $this->rankVarRead[$key] + $value; unset($this->rankVarRead[$key]); return; } if (key_exists($key, $this->rankVarIncrease)) { $this->rankVarIncrease[$key] += $value; return; } $this->rankVarIncrease[$key] = $value; } function setRankVar(string $key, int $value) { if (!key_exists($key, static::RANK_COLUMN)) { throw new \InvalidArgumentException('올바르지 않은 인자 :' . $key); } if (key_exists($key, $this->rankVarRead)) { unset($this->rankVarRead[$key]); } else if (key_exists($key, $this->rankVarIncrease)) { unset($this->rankVarIncrease[$key]); } $this->rankVarSet[$key] = $value; } function getRankVar(string $key): int { if (!key_exists($key, static::RANK_COLUMN)) { throw new \InvalidArgumentException('올바르지 않은 인자 :' . $key); } if (key_exists($key, $this->rankVarSet)) { return $this->rankVarSet[$key]; } if (!key_exists($key, $this->rankVarRead)) { throw new \RuntimeException('인자가 없음 : ' . $key); } return $this->rankVarRead[$key]; } /** * @param \MeekroDB $db */ function applyDB($db): bool { if ($this->lastTurn && $this->getLastTurn() != $this->getResultTurn()) { $this->setVar('last_turn', $this->getResultTurn()->toJson()); } $updateVals = $this->getUpdatedValues(); $generalID = $this->getID(); $result = false; if ($updateVals) { $db->update('general', $updateVals, 'no=%i', $generalID); $result = $result || $db->affectedRows() > 0; if (key_exists('nation', $updateVals)) { $db->update('rank_data', [ 'nation_id' => $updateVals['nation'] ], 'general_id = %i', $generalID); $result = true; } $this->flushUpdateValues(); } if ($this->rankVarIncrease) { foreach ($this->rankVarIncrease as $rankKey => $rankVal) { $db->update('rank_data', [ 'value' => $db->sqleval('value + %i', $rankVal) ], 'general_id = %i AND type = %s', $generalID, $rankKey); } $result = true; } if ($this->rankVarSet) { foreach ($this->rankVarSet as $rankKey => $rankVal) { $db->update('rank_data', [ 'value' => $rankVal ], 'general_id = %i AND type = %s', $generalID, $rankKey); $this->rankVarRead[$rankKey] = $rankVal; } $result = true; } $this->rankVarIncrease = []; $this->rankVarSet = []; $this->getLogger()->flush(); return $result; } function checkStatChange(): bool { $logger = $this->getLogger(); $limit = GameConst::$upgradeLimit; $table = [ ['통솔', 'leadership'], ['무력', 'strength'], ['지력', 'intel'], ]; $result = false; foreach ($table as [$statNickName, $statName]) { $statExpName = $statName . '_exp'; if ($this->getVar($statExpName) < 0) { $logger->pushGeneralActionLog("{$statNickName}이 1 떨어졌습니다!", ActionLogger::PLAIN); $this->increaseVar($statExpName, $limit); $this->increaseVar($statName, -1); $result = true; } else if ($this->getVar($statExpName) >= $limit) { $logger->pushGeneralActionLog("{$statNickName}이 1 올랐습니다!", ActionLogger::PLAIN); $this->increaseVar($statExpName, -$limit); $this->increaseVar($statName, 1); $result = true; } } return $result; } public function getPreTurnExecuteTriggerList(General $general): ?GeneralTriggerCaller { $caller = new GeneralTriggerCaller(); foreach (array_merge([ $this->nationType, $this->officerLevelObj, $this->specialDomesticObj, $this->specialWarObj, $this->personalityObj, $this->getCrewTypeObj(), $this->inheritBuffObj, ], $this->itemObjs) as $iObj) { if (!$iObj) { continue; } /** @var iAction $iObj */ $caller->merge($iObj->getPreTurnExecuteTriggerList($general)); } return $caller; } public function onCalcDomestic(string $turnType, string $varType, float $value, $aux = null): float { foreach (array_merge([ $this->nationType, $this->officerLevelObj, $this->specialDomesticObj, $this->specialWarObj, $this->personalityObj, $this->getCrewTypeObj(), $this->inheritBuffObj, ], $this->itemObjs) as $iObj) { if (!$iObj) { continue; } /** @var iAction $iObj */ $value = $iObj->onCalcDomestic($turnType, $varType, $value, $aux); } return $value; } public function onCalcStat(General $general, string $statName, $value, $aux = null) { //xxx: $general? foreach (array_merge([ $this->nationType, $this->officerLevelObj, $this->specialDomesticObj, $this->specialWarObj, $this->personalityObj, $this->getCrewTypeObj(), $this->inheritBuffObj, ], $this->itemObjs) as $iObj) { if (!$iObj) { continue; } /** @var iAction $iObj */ $value = $iObj->onCalcStat($this, $statName, $value, $aux); } return $value; } public function onCalcOpposeStat(General $general, string $statName, $value, $aux = null) { //xxx: $general? foreach (array_merge([ $this->nationType, $this->officerLevelObj, $this->specialDomesticObj, $this->specialWarObj, $this->personalityObj, $this->getCrewTypeObj(), $this->inheritBuffObj, ], $this->itemObjs) as $iObj) { if (!$iObj) { continue; } /** @var iAction $iObj */ $value = $iObj->onCalcOpposeStat($this, $statName, $value, $aux); } return $value; } public function onCalcStrategic(string $turnType, string $varType, $value) { foreach (array_merge([ $this->nationType, $this->officerLevelObj, $this->specialDomesticObj, $this->specialWarObj, $this->personalityObj, $this->getCrewTypeObj(), $this->inheritBuffObj, ], $this->itemObjs) as $iObj) { if (!$iObj) { continue; } /** @var iAction $iObj */ $value = $iObj->onCalcStrategic($turnType, $varType, $value); } return $value; } public function onCalcNationalIncome(string $type, $amount) { foreach (array_merge([ $this->nationType, $this->officerLevelObj, $this->specialDomesticObj, $this->specialWarObj, $this->personalityObj, $this->getCrewTypeObj(), $this->inheritBuffObj, ], $this->itemObjs) as $iObj) { if (!$iObj) { continue; } /** @var iAction $iObj */ $amount = $iObj->onCalcNationalIncome($type, $amount); } return $amount; } public function getWarPowerMultiplier(WarUnit $unit): array { //xxx:$unit $att = 1; $def = 1; foreach (array_merge([ $this->nationType, $this->officerLevelObj, $this->specialDomesticObj, $this->specialWarObj, $this->personalityObj, $this->getCrewTypeObj(), $this->inheritBuffObj, ], $this->itemObjs) as $iObj) { if (!$iObj) { continue; } /** @var iAction $iObj */ [$attV, $defV] = $iObj->getWarPowerMultiplier($unit); $att *= $attV; $def *= $defV; } return [$att, $def]; } public function getBattleInitSkillTriggerList(WarUnit $unit): ?WarUnitTriggerCaller { $caller = new WarUnitTriggerCaller(); foreach (array_merge([ $this->nationType, $this->officerLevelObj, $this->specialDomesticObj, $this->specialWarObj, $this->personalityObj, $this->getCrewTypeObj(), $this->inheritBuffObj, ], $this->itemObjs) as $iObj) { if (!$iObj) { continue; } /** @var iAction $iObj */ $caller->merge($iObj->getBattleInitSkillTriggerList($unit)); } return $caller; } public function getBattlePhaseSkillTriggerList(WarUnit $unit): ?WarUnitTriggerCaller { $caller = new WarUnitTriggerCaller( new WarUnitTrigger\che_필살시도($unit), new WarUnitTrigger\che_필살발동($unit), new WarUnitTrigger\che_회피시도($unit), new WarUnitTrigger\che_회피발동($unit), new WarUnitTrigger\che_계략시도($unit), new WarUnitTrigger\che_계략발동($unit), new WarUnitTrigger\che_계략실패($unit) ); foreach (array_merge([ $this->nationType, $this->officerLevelObj, $this->specialDomesticObj, $this->specialWarObj, $this->personalityObj, $this->getCrewTypeObj(), $this->inheritBuffObj, ], $this->itemObjs) as $iObj) { if (!$iObj) { continue; } /** @var iAction $iObj */ $caller->merge($iObj->getBattlePhaseSkillTriggerList($unit)); } return $caller; } static public function mergeQueryColumn(?array $reqColumns = null, int $constructMode = 2): array { $minimumColumn = ['no', 'name', 'city', 'nation', 'officer_level', 'officer_city']; $defaultEventColumn = [ 'no', 'name', 'owner', 'city', 'nation', 'officer_level', 'officer_city', 'special', 'special2', 'personal', 'horse', 'weapon', 'book', 'item', 'last_turn', 'aux', ]; $fullColumn = [ 'no', 'name', 'owner', 'owner_name', 'picture', 'imgsvr', 'nation', 'city', 'troop', 'injury', 'affinity', 'leadership', 'leadership_exp', 'strength', 'strength_exp', 'intel', 'intel_exp', 'weapon', 'book', 'horse', 'item', 'experience', 'dedication', 'officer_level', 'officer_city', 'gold', 'rice', 'crew', 'crewtype', 'train', 'atmos', 'turntime', 'makelimit', 'killturn', 'block', 'dedlevel', 'explevel', 'age', 'startage', 'belong', 'personal', 'special', 'special2', 'defence_train', 'tnmt', 'npc', 'npc_org', 'deadyear', 'npcmsg', 'dex1', 'dex2', 'dex3', 'dex4', 'dex5', 'betray', 'recent_war', 'last_turn', 'myset', 'specage', 'specage2', 'con', 'connect', 'aux', 'lastrefresh', ]; if ($reqColumns === null) { return [$fullColumn, array_keys(static::RANK_COLUMN)]; } $rankColumn = []; $subColumn = []; foreach ($reqColumns as $column) { if (key_exists($column, static::RANK_COLUMN)) { $rankColumn[] = $column; } else { $subColumn[] = $column; } } if ($constructMode > 1) { return [array_unique(array_merge($defaultEventColumn, $subColumn)), $rankColumn]; } return [array_unique(array_merge($minimumColumn, $subColumn)), $rankColumn]; } /** * @param ?int[] $generalIDList * @param null|string[] $column * @param int $constructMode * @return \sammo\General[] * @throws MustNotBeReachedException */ static public function createGeneralObjListFromDB(?array $generalIDList, ?array $column = null, int $constructMode = 2): array { if ($generalIDList === []) { return []; } $db = DB::db(); if ($constructMode > 0) { $gameStor = KVStorage::getStorage($db, 'game_env'); [$year, $month] = $gameStor->getValuesAsArray(['year', 'month']); } else { $year = null; $month = null; } [$column, $rankColumn] = static::mergeQueryColumn($column, $constructMode); if ($generalIDList === null) { $rawGenerals = Util::convertArrayToDict( $db->query('SELECT %l FROM general WHERE 1', Util::formatListOfBackticks($column)), 'no' ); $generalIDList = array_keys($rawGenerals); } else { $rawGenerals = Util::convertArrayToDict( $db->query('SELECT %l FROM general WHERE no IN %li', Util::formatListOfBackticks($column), $generalIDList), 'no' ); } $rawRanks = []; if ($rankColumn) { $rawValue = $db->queryAllLists( 'SELECT `general_id`, `type`, `value` FROM rank_data WHERE general_id IN %li AND `type` IN %ls', $generalIDList, $rankColumn ); foreach ($rawValue as [$generalID, $rankType, $rankValue]) { if (!key_exists($generalID, $rawRanks)) { $rawRanks[$generalID] = []; } $rawRanks[$generalID][$rankType] = $rankValue; } } $result = []; foreach ($generalIDList as $generalID) { if (!key_exists($generalID, $rawGenerals)) { $result[$generalID] = new DummyGeneral($constructMode > 0); continue; } if (key_exists($generalID, $rawRanks) && count($rawRanks[$generalID] ?? []) !== count($rankColumn)) { throw new \RuntimeException('column의 수가 일치하지 않음 : ' . $generalID); } $result[$generalID] = new static($rawGenerals[$generalID], $rawRanks[$generalID] ?? null, null, null, $year, $month, $constructMode > 1); } return $result; } static public function createGeneralObjFromDB(int $generalID, ?array $column = null, int $constructMode = 2): self { $db = DB::db(); if ($constructMode > 0) { $gameStor = KVStorage::getStorage($db, 'game_env'); [$year, $month] = $gameStor->getValuesAsArray(['year', 'month']); } else { $year = null; $month = null; } [$column, $rankColumn] = static::mergeQueryColumn($column, $constructMode); $rawGeneral = $db->queryFirstRow('SELECT %l FROM general WHERE no = %i', Util::formatListOfBackticks($column), $generalID); if (!$rawGeneral) { return new DummyGeneral($constructMode > 0); } $rawRankValues = []; if ($rankColumn) { $rawValue = $db->queryAllLists('SELECT `type`, `value` FROM rank_data WHERE general_id = %i AND `type` IN %ls', $generalID, $rankColumn); foreach ($rawValue as [$rankType, $rankValue]) { $rawRankValues[$rankType] = $rankValue; } } $general = new static($rawGeneral, $rawRankValues, null, null, $year, $month, $constructMode > 1); return $general; } /** * @param General[] $generalList * @param int $turnIdx * @param array $env * @return GeneralCommand[] */ static public function getReservedTurnByGeneralList(array $generalList, int $turnIdx, array $env) { if (!$generalList) { return []; } $generalIDList = array_map(function (General $general) { return $general->getID(); }, $generalList); $db = DB::db(); $result = []; $rawCmds = Util::convertArrayToDict($db->query('SELECT * FROM general_turn WHERE general_id IN %li AND turn_idx = %i', $generalIDList, $turnIdx), 'general_id'); foreach ($generalList as $general) { $generalID = $general->getID(); if (!key_exists($generalID, $rawCmds)) { $result[$generalID] = buildGeneralCommandClass(null, $general, $env); continue; } $rawCmd = $rawCmds[$generalID]; $result[$generalID] = buildGeneralCommandClass($rawCmd['action'], $general, $env, Json::decode($rawCmd['arg'])); } return $result; } /** * @param General[] $generalList * @param int $turnIdxFrom [$turnIdxFrom, $turnIdxTo) * @param int $turnIdxTo [$turnIdxFrom, $turnIdxTo) * @param array $env * @return GeneralCommand[][] */ static public function getReservedTurnListByGeneralList(array $generalList, int $turnIdxFrom, int $turnIdxTo, array $env) { //XXX: static인데 return값이 General이 아니라고?? GeneralCommandHelper같은게 있어야하지 않을까? if (!$generalList) { return []; } if ($turnIdxFrom < 0 || $turnIdxFrom >= GameConst::$maxTurn) { throw new \OutOfRangeException('$turnIdxFrom 범위 초과' . $turnIdxFrom); } if ($turnIdxTo <= $turnIdxFrom || GameConst::$maxTurn < $turnIdxTo) { throw new \OutOfRangeException('$turnIdxTo 범위 초과' . $turnIdxTo); } $generalIDList = array_map(function (General $general) { return $general->getID(); }, $generalList); $db = DB::db(); $rawCmds = $db->queryFirstRow('SELECT * FROM general_turn WHERE general_id IN %i AND %i <= turn_idx AND turn_idx < %i ORDER BY general_id ASC, turn_idx ASC', $generalIDList, $turnIdxFrom, $turnIdxTo); $orderedRawCmds = []; foreach ($rawCmds as $rawCmd) { $generalID = $rawCmd['general_id']; $turnIdx = $rawCmd['turn_idx']; if (!key_exists($generalID, $orderedRawCmds)) { $orderedRawCmds[$generalID] = []; } $orderedRawCmds[$generalID][$turnIdx] = $rawCmd; } $result = []; foreach ($generalList as $general) { $generalID = $general->getID(); $result[$generalID] = []; if (!key_exists($generalID, $orderedRawCmds)) { foreach (Util::range($turnIdxFrom, $turnIdxTo) as $turnIdx) { $result[$generalID][$turnIdx] = buildGeneralCommandClass(null, $general, $env); } continue; } foreach ($orderedRawCmds[$generalID] as $turnIdx => $rawCmd) { $result[$generalID][$turnIdx] = buildGeneralCommandClass($rawCmd['action'], $general, $env, Json::decode($rawCmd['arg'])); } } return $result; } /** * @return int|float */ public function getInheritancePoint(string $key, &$aux = null, bool $forceCalc = false) { $inheritType = static::INHERITANCE_KEY[$key] ?? null; if ($inheritType === null) { throw new \OutOfRangeException("{$key}는 유산 타입이 아님"); } $ownerID = $this->getVar('owner'); if (!$ownerID) { return 0; } if ($this->getVar('npc') != 0) { return 0; } [$storeType, $multiplier,] = $inheritType; $gameStor = KVStorage::getStorage(DB::db(), 'game_env'); if ($storeType === true || ($gameStor->isunited != 0 && !$forceCalc)) { $inheritStor = KVStorage::getStorage(DB::db(), "inheritance_{$ownerID}"); [$value, $aux] = $inheritStor->getValue($key); return $value; } if (is_array($storeType)) { [$storSubType, $storSubKey] = $storeType; if ($storSubType === 'rank') { return $this->getRankVar($storSubKey) * $multiplier; } if ($storSubType === 'raw') { return $this->getVar($storSubKey) * $multiplier; } if ($storSubType === 'aux') { return ($this->getAuxVar($storSubKey) ?? 0) * $multiplier; } throw new \InvalidArgumentException("{$storSubType}은 참조 할 수 없는 유산 세부키임"); } if ($storeType !== false) { throw new \InvalidArgumentException("{$storeType}은 올바르지 않은 유산 키임"); } $extractFn = function () { return [0, null]; }; switch ($key) { case 'dex': $extractFn = function () use ($multiplier) { $totalDex = 0; foreach (array_keys(GameUnitConst::allType()) as $armType) { $totalDex += $this->getVar("dex{$armType}"); } return [$totalDex * $multiplier, null]; }; break; case 'betting': $extractFn = function () use ($multiplier) { $betWin = $this->getRankVar('betwin'); $betWinRate = $this->getRankVar('betwingold') / max(1, $this->getRankVar('betgold')); return [$betWin * $multiplier * pow($betWinRate, 2), null]; }; break; case 'max_belong': $extractFn = function () use ($multiplier) { $maxBelong = max($this->getVar('belong'), $this->getAuxVar('max_belong') ?? 0); return [$maxBelong * $multiplier, null]; }; break; default: throw new \InvalidArgumentException("{$key}는 유산 추출기를 보유하고 있지 않음"); } [$value, $aux] = ($extractFn)(); return $value; } public function setInheritancePoint(string $key, $value, $aux = null) { if (!is_int($value) && !is_float($value)) { throw new \InvalidArgumentException("{$value}는 숫자가 아님"); } $inheritType = static::INHERITANCE_KEY[$key] ?? null; if ($inheritType === null) { throw new \OutOfRangeException("{$key}는 유산 타입이 아님"); } [$storeType, $multiplier,] = $inheritType; if ($storeType !== true) { throw new \InvalidArgumentException("{$key}는 직접 저장형 유산 포인트가 아님"); } if ($multiplier != 1 && $value != 0) { throw new \InvalidArgumentException("{$key}는 1:1 유산 포인트가 아님"); } $ownerID = $this->getVar('owner'); if (!$ownerID) { return; } if ($this->getVar('npc') != 0) { return; } $gameStor = KVStorage::getStorage(DB::db(), 'game_env'); if ($gameStor->isunited != 0) { return; } $inheritStor = KVStorage::getStorage(DB::db(), "inheritance_{$ownerID}"); $inheritStor->setValue($key, [$value, $aux]); } public function increaseInheritancePoint(string $key, $value, $aux = null) { if (!is_int($value) && !is_float($value)) { throw new \InvalidArgumentException("{$value}는 숫자가 아님"); } $inheritType = static::INHERITANCE_KEY[$key] ?? null; if ($inheritType === null) { throw new \OutOfRangeException("{$key}는 유산 타입이 아님"); } [$storeType, $multiplier,] = $inheritType; if ($storeType !== true) { throw new \InvalidArgumentException("{$key}는 직접 저장형 유산 포인트가 아님"); } $ownerID = $this->getVar('owner'); if (!$ownerID) { return; } if ($this->getVar('npc') != 0) { return; } $gameStor = KVStorage::getStorage(DB::db(), 'game_env'); if ($gameStor->isunited != 0) { return; } $inheritStor = KVStorage::getStorage(DB::db(), "inheritance_{$ownerID}"); [$oldValue, $oldAux] = $inheritStor->getValue($key) ?? [0, null]; if ($oldAux !== $aux) { $oldValue = 0; } $newValue = $oldValue + $value * $multiplier; $inheritStor->setValue($key, [$newValue, $aux]); } public function mergeTotalInheritancePoint() { $ownerID = $this->getVar('owner'); if (!$ownerID) { return; } if ($this->getVar('npc') != 0) { return; } $inheritStor = KVStorage::getStorage(DB::db(), "inheritance_{$ownerID}"); $inheritStor->cacheAll(); foreach (static::INHERITANCE_KEY as $key => [$storType,,]) { $aux = null; $point = $this->getInheritancePoint($key, $aux, true); if ($storType === true) { continue; } $inheritStor->setValue($key, [$point, $aux]); } $oldInheritStor = KVStorage::getStorage(DB::db(), "inheritance_result"); $gameStor = KVStorage::getStorage(DB::db(), 'game_env'); $serverID = UniqueConst::$serverID; $year = $gameStor->year; $month = $gameStor->month; $oldInheritStor->setValue("{$serverID}_{$ownerID}_{$this->getID()}_{$year}_{$month}", $inheritStor->getAll(true)); } }