fix: block S100 inheritance stat resets
This commit is contained in:
@@ -4,6 +4,7 @@ namespace sammo\API\InheritAction;
|
||||
|
||||
use sammo\Session;
|
||||
use DateTimeInterface;
|
||||
use sammo\CentennialAllStarGrowthService;
|
||||
use sammo\DB;
|
||||
use sammo\Enums\APIRecoveryType;
|
||||
use sammo\Enums\RankColumn;
|
||||
@@ -98,6 +99,10 @@ class ResetStat extends \sammo\BaseAPI
|
||||
return 'NPC는 능력치 초기화를 할 수 없습니다.';
|
||||
}
|
||||
|
||||
if (!CentennialAllStarGrowthService::isStatResetAllowed()) {
|
||||
return '100기 올스타 장수는 능력치 초기화를 사용할 수 없습니다.';
|
||||
}
|
||||
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
@@ -19,6 +19,11 @@ final class CentennialAllStarGrowthService
|
||||
return GameConst::$targetGeneralPool === self::POOL_CLASS;
|
||||
}
|
||||
|
||||
public static function isStatResetAllowed(): bool
|
||||
{
|
||||
return !self::isActive();
|
||||
}
|
||||
|
||||
public static function initialAux(array $targetInfo, ?array $userInitialStats = null): array
|
||||
{
|
||||
$granted = array_fill_keys(array_merge(self::STAT_KEYS, self::DEX_KEYS), 0);
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
<BButton class="col-6 offset-6" variant="primary" @click="checkOwner"> 소유자 찾기 </BButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-lg-4 col-sm-6 col-12 py-2">
|
||||
<div v-if="canResetStat" class="col col-lg-4 col-sm-6 col-12 py-2">
|
||||
<div class="row px-4">
|
||||
<div class="a-right col-6 align-self-center">능력치 초기화</div>
|
||||
<div class="col-6">
|
||||
@@ -222,6 +222,17 @@
|
||||
<BButton class="col-6 offset-6" variant="primary" @click="resetStat"> 능력치 초기화</BButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="col col-lg-4 col-sm-6 col-12 py-2">
|
||||
<div class="row px-4">
|
||||
<div class="a-right col-6 align-self-center">능력치 초기화</div>
|
||||
<div class="col-6 align-self-center">사용 불가</div>
|
||||
</div>
|
||||
<div class="a-right">
|
||||
<small class="form-text text-muted">
|
||||
100기 올스타 장수는 장수 전환 시 능력치 성장 기록을 보존하기 위해 능력치 초기화를 사용할 수 없습니다.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
@@ -297,6 +308,7 @@ declare const staticValues: {
|
||||
}
|
||||
>;
|
||||
availableTargetGeneral: Record<number, string>;
|
||||
canResetStat: boolean;
|
||||
currentStat: {
|
||||
leadership: number;
|
||||
strength: number;
|
||||
@@ -447,6 +459,7 @@ const {
|
||||
availableSpecialWar,
|
||||
availableUnique,
|
||||
availableTargetGeneral,
|
||||
canResetStat,
|
||||
currentStat
|
||||
} = staticValues;
|
||||
|
||||
@@ -739,4 +752,4 @@ async function getMoreLog(): Promise<void> {
|
||||
.tnum {
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -105,6 +105,7 @@ $lastInheritPointLogs = $db->query('SELECT id, server_id, year, month, date, tex
|
||||
'availableUnique' => $availableUnique,
|
||||
'lastInheritPointLogs' => $lastInheritPointLogs,
|
||||
'availableTargetGeneral' => $availableTargetGeneral,
|
||||
'canResetStat' => CentennialAllStarGrowthService::isStatResetAllowed(),
|
||||
'currentStat' => [
|
||||
'leadership' => Util::clamp($me->getVar('leadership'), GameConst::$defaultStatMin, GameConst::$defaultStatMax),
|
||||
'strength' => Util::clamp($me->getVar('strength'), GameConst::$defaultStatMin, GameConst::$defaultStatMax),
|
||||
@@ -120,4 +121,4 @@ $lastInheritPointLogs = $db->query('SELECT id, server_id, year, month, date, tex
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -25,6 +25,20 @@ final class CentennialAllStarGrowthTest extends TestCase
|
||||
self::assertSame(1.0, CentennialAllStarGrowth::progress(180, 210, 1));
|
||||
}
|
||||
|
||||
public function testStatResetIsDisabledOnlyForCentennialAllStarPool(): void
|
||||
{
|
||||
$previousPool = GameConst::$targetGeneralPool;
|
||||
try {
|
||||
GameConst::$targetGeneralPool = CentennialAllStarGrowthService::POOL_CLASS;
|
||||
self::assertFalse(CentennialAllStarGrowthService::isStatResetAllowed());
|
||||
|
||||
GameConst::$targetGeneralPool = 'RandomNameGeneral';
|
||||
self::assertTrue(CentennialAllStarGrowthService::isStatResetAllowed());
|
||||
} finally {
|
||||
GameConst::$targetGeneralPool = $previousPool;
|
||||
}
|
||||
}
|
||||
|
||||
public function testMAndGGeneralsAdvanceAtNinetyPercentProgress(): void
|
||||
{
|
||||
self::assertSame(
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import fs from 'node:fs';
|
||||
import {createHash} from 'node:crypto';
|
||||
import {chromium} from 'playwright';
|
||||
|
||||
const baseURL = process.env.REF_BROWSER_URL;
|
||||
const screenshotPath = process.env.REF_SCREENSHOT_PATH;
|
||||
const resultPath = process.env.REF_RESULT_PATH;
|
||||
const username = process.env.REF_USER_ID ?? 's100user01';
|
||||
const password = fs.readFileSync('/run/secrets/test_user_password', 'utf8').trim();
|
||||
const expectedReason = '100기 올스타 장수는 능력치 초기화를 사용할 수 없습니다.';
|
||||
const expectedWarning = '100기 올스타 장수는 장수 전환 시 능력치 성장 기록을 보존하기 위해 능력치 초기화를 사용할 수 없습니다.';
|
||||
|
||||
if (!baseURL || !screenshotPath || !resultPath) {
|
||||
throw new Error('REF_BROWSER_URL, REF_SCREENSHOT_PATH, and REF_RESULT_PATH are required');
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({headless: true});
|
||||
const context = await browser.newContext({
|
||||
viewport: {width: 1280, height: 960},
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const browserMessages = [];
|
||||
page.on('console', message => {
|
||||
browserMessages.push({type: message.type(), text: message.text()});
|
||||
});
|
||||
page.on('pageerror', error => {
|
||||
browserMessages.push({type: 'pageerror', text: error.message});
|
||||
});
|
||||
|
||||
await page.goto(baseURL, {waitUntil: 'domcontentloaded', timeout: 60_000});
|
||||
const salt = await page.locator('#global_salt').inputValue();
|
||||
const passwordHash = createHash('sha512')
|
||||
.update(salt + password + salt)
|
||||
.digest('hex');
|
||||
const loginResponse = await page.request.post(
|
||||
new URL('api.php?path=Login/LoginByID', baseURL).href,
|
||||
{data: {username, password: passwordHash}},
|
||||
);
|
||||
const loginResult = await loginResponse.json();
|
||||
if (!loginResponse.ok() || loginResult.result !== true) {
|
||||
throw new Error(`login failed: ${String(loginResult.reason ?? loginResponse.status())}`);
|
||||
}
|
||||
|
||||
const gameLoginResponse = await page.request.get(
|
||||
new URL('hwe/api.php?path=Global/GetConst', baseURL).href,
|
||||
);
|
||||
const gameLoginResult = await gameLoginResponse.json();
|
||||
if (!gameLoginResponse.ok() || gameLoginResult.result !== true) {
|
||||
throw new Error(`game login failed: ${JSON.stringify(gameLoginResult)}`);
|
||||
}
|
||||
|
||||
await page.goto(
|
||||
new URL('hwe/v_inheritPoint.php', baseURL).href,
|
||||
{waitUntil: 'networkidle', timeout: 60_000},
|
||||
);
|
||||
const bodyText = await page.locator('body').innerText();
|
||||
if (bodyText.includes('#0 /var/www/html/')) {
|
||||
throw new Error('PHP stack trace was rendered in the inheritance page');
|
||||
}
|
||||
if (!bodyText.includes(expectedWarning)) {
|
||||
await page.screenshot({path: screenshotPath, fullPage: true});
|
||||
fs.writeFileSync(
|
||||
resultPath,
|
||||
`${JSON.stringify({
|
||||
username,
|
||||
url: page.url(),
|
||||
bodyText: bodyText.slice(0, 4000),
|
||||
browserMessages,
|
||||
}, null, 2)}\n`,
|
||||
{mode: 0o600},
|
||||
);
|
||||
throw new Error('S100 stat-reset restriction was not rendered');
|
||||
}
|
||||
if (await page.getByRole('button', {name: '능력치 초기화'}).count() !== 0) {
|
||||
throw new Error('S100 stat-reset button must not be rendered');
|
||||
}
|
||||
|
||||
const warning = page.getByText(expectedWarning, {exact: true});
|
||||
await warning.waitFor({state: 'visible', timeout: 60_000});
|
||||
const warningGeometry = await warning.evaluate(element => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return {
|
||||
rect: {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
},
|
||||
style: {
|
||||
color: style.color,
|
||||
fontFamily: style.fontFamily,
|
||||
fontSize: style.fontSize,
|
||||
lineHeight: style.lineHeight,
|
||||
},
|
||||
};
|
||||
});
|
||||
const beforeState = await page.evaluate(() => ({
|
||||
currentStat: staticValues.currentStat,
|
||||
previousPoint: staticValues.items.previous,
|
||||
}));
|
||||
|
||||
const resetResponse = await page.request.put(
|
||||
new URL('hwe/api.php?path=InheritAction/ResetStat', baseURL).href,
|
||||
{
|
||||
data: {
|
||||
leadership: 55,
|
||||
strength: 55,
|
||||
intel: 55,
|
||||
},
|
||||
},
|
||||
);
|
||||
const resetResult = await resetResponse.json();
|
||||
if (!resetResponse.ok()
|
||||
|| resetResult.result !== false
|
||||
|| resetResult.reason !== expectedReason
|
||||
) {
|
||||
throw new Error(`unexpected ResetStat response: ${JSON.stringify(resetResult)}`);
|
||||
}
|
||||
|
||||
await page.reload({waitUntil: 'networkidle', timeout: 60_000});
|
||||
const afterState = await page.evaluate(() => ({
|
||||
currentStat: staticValues.currentStat,
|
||||
previousPoint: staticValues.items.previous,
|
||||
}));
|
||||
if (JSON.stringify(afterState) !== JSON.stringify(beforeState)) {
|
||||
throw new Error(`ResetStat rejection changed state: ${JSON.stringify({beforeState, afterState})}`);
|
||||
}
|
||||
|
||||
await page.screenshot({path: screenshotPath, fullPage: true});
|
||||
fs.writeFileSync(
|
||||
resultPath,
|
||||
`${JSON.stringify({
|
||||
username,
|
||||
url: page.url(),
|
||||
viewport: {width: 1280, height: 960, deviceScaleFactor: 1},
|
||||
warningGeometry,
|
||||
resetResult,
|
||||
beforeState,
|
||||
afterState,
|
||||
browserMessages,
|
||||
}, null, 2)}\n`,
|
||||
{mode: 0o600},
|
||||
);
|
||||
|
||||
await browser.close();
|
||||
console.log(`S100 inheritance stat-reset guard verified: ${screenshotPath}`);
|
||||
Reference in New Issue
Block a user