Skip to content

Commit 04ab72c

Browse files
committed
BXT: Battle Tower Table adjustments
- Renamed the bxt_battle_tower_room_leaders table back to bxt_battle_tower_trainers - No longer cull the bxt_battle_tower_honor_roll list on each addition - Now the bxt_battle_tower_honor_roll table removes trainers of the same room and level after >30 trainers exist. - pokemon/pokemon decode columns have been added to bxt_battle_tower_honor_roll
1 parent 6e40595 commit 04ab72c

File tree

6 files changed

+350
-13
lines changed

6 files changed

+350
-13
lines changed

app/pokemon-battle/index.js

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ async function updateContent() {
6767
async function updateContentForRegion(region, connection) {
6868
// Rebuild trainer pool for this region from scratch each run.
6969
await connection.execute(
70-
"DELETE FROM bxt_battle_tower_room_leaders WHERE game_region = ?",
70+
"DELETE FROM bxt_battle_tower_room_trainers WHERE game_region = ?",
7171
[region]
7272
);
7373

@@ -134,40 +134,71 @@ async function updateContentForRegion(region, connection) {
134134

135135
// 3) Maintain honor roll (leaders table) for this region/level/room.
136136
// We do NOT delete records or trainers here; records are trimmed globally.
137-
await connection.execute(
138-
"DELETE FROM bxt_battle_tower_honor_roll " +
139-
"WHERE game_region = ? AND level = ? AND room = ?",
140-
[region, level, room]
141-
);
137+
// Honor roll is append-only: do not delete existing rows.
142138

143139
if (selectedTrainers.length > 0) {
144140
const leader = selectedTrainers[0];
145141

146142
// Insert leader with decoded name and level.
147143
await connection.execute(
148144
"INSERT INTO bxt_battle_tower_honor_roll " +
149-
"(game_region, player_name, player_name_decode, `class`, `class_decode`, message_start, message_start_decode, room, level, level_decode) " +
150-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
145+
"(game_region, player_name, player_name_decode, `class`, `class_decode`, " +
146+
"pokemon1, pokemon1_decode, pokemon2, pokemon2_decode, pokemon3, pokemon3_decode, " +
147+
"message_start, message_start_decode, room, level, level_decode) " +
148+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
151149
[
152150
region,
153151
leader.player_name,
154152
leader.player_name_decode || null,
155153
leader.class || null,
156154
leader.class_decode || null,
155+
(leader.pokemon1 ?? null),
156+
leader.pokemon1_decode || null,
157+
(leader.pokemon2 ?? null),
158+
leader.pokemon2_decode || null,
159+
(leader.pokemon3 ?? null),
160+
leader.pokemon3_decode || null,
157161
(leader.message_start ?? null),
158162
leader.message_start_decode || null,
159163
room,
160164
level,
161165
leader.level_decode || null,
162166
]
163167
);
168+
169+
// Keep honor roll capped at 30 entries per (game_region, room, level),
170+
// deleting the oldest by timestamp first (ties broken by id).
171+
const [hrCountRows] = await connection.execute(
172+
"SELECT COUNT(*) AS cnt " +
173+
"FROM bxt_battle_tower_honor_roll " +
174+
"WHERE game_region = ? AND room = ? AND level = ?",
175+
[region, room, level]
176+
);
177+
const hrCount = Number(hrCountRows?.[0]?.cnt ?? 0);
178+
if (hrCount > 30) {
179+
const excess = hrCount - 30;
180+
await connection.execute(
181+
"DELETE FROM bxt_battle_tower_honor_roll " +
182+
"WHERE id IN (" +
183+
" SELECT id FROM (" +
184+
" SELECT id " +
185+
" FROM bxt_battle_tower_honor_roll " +
186+
" WHERE game_region = ? AND room = ? AND level = ? " +
187+
" ORDER BY `timestamp` ASC, id ASC " +
188+
" LIMIT ?" +
189+
" ) AS t" +
190+
")",
191+
[region, room, level, excess]
192+
);
193+
}
194+
164195
}
165196

166-
// 4) Populate trainer pool table (bxt_battle_tower_room_leaders) for this region.
197+
// 4) Populate trainer pool table (bxt_battle_tower_room_trainers) for this region.
167198
// Deduplicate on (region, trainer_id, secret_id, account_id) to match composite PK.
168199
if (selectedTrainers.length > 0) {
169200
const insertTrainerSql =
170-
"INSERT INTO bxt_battle_tower_room_leaders " +
201+
"INSERT INTO bxt_battle_tower_room_trainers " +
171202
"(game_region, room, level, level_decode, no, trainer_id, secret_id, player_name, player_name_decode, " +
172203
" `class`, `class_decode`, " +
173204
" pokemon1, pokemon1_decode, " +
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Phinx\Migration\AbstractMigration;
6+
7+
/**
8+
* Adds the following columns to bxt_battle_tower_honor_roll, positioned after class_decode:
9+
* | class_decode | pokemon1 | pokemon1_decode | pokemon2 | pokemon2_decode | pokemon3 | pokemon3_decode | message_start | message_start_decode |
10+
*
11+
* Column definitions are copied from bxt_battle_tower_records when available.
12+
* If message_start/message_start_decode already exist, they are re-positioned to the right of pokemon3_decode.
13+
*
14+
* Phinx 0.16.x compatible (no AbstractMigration::hasColumn()).
15+
* Uses INFORMATION_SCHEMA checks; safe to run multiple times.
16+
*/
17+
final class BattleTowerAddPokemonColsToHonorRoll extends AbstractMigration
18+
{
19+
private function tableExists(string $table): bool
20+
{
21+
$pdo = $this->getAdapter()->getConnection();
22+
$row = $this->fetchRow(
23+
"SELECT 1 AS ok
24+
FROM INFORMATION_SCHEMA.TABLES
25+
WHERE TABLE_SCHEMA = DATABASE()
26+
AND TABLE_NAME = " . $pdo->quote($table) . "
27+
LIMIT 1"
28+
);
29+
return (bool)$row;
30+
}
31+
32+
private function columnExists(string $table, string $column): bool
33+
{
34+
$pdo = $this->getAdapter()->getConnection();
35+
$row = $this->fetchRow(
36+
"SELECT 1 AS ok
37+
FROM INFORMATION_SCHEMA.COLUMNS
38+
WHERE TABLE_SCHEMA = DATABASE()
39+
AND TABLE_NAME = " . $pdo->quote($table) . "
40+
AND COLUMN_NAME = " . $pdo->quote($column) . "
41+
LIMIT 1"
42+
);
43+
return (bool)$row;
44+
}
45+
46+
/**
47+
* @return array<string, mixed>|null
48+
*/
49+
private function getColumnSpec(string $table, string $column): ?array
50+
{
51+
$pdo = $this->getAdapter()->getConnection();
52+
$row = $this->fetchRow(
53+
"SELECT
54+
COLUMN_NAME,
55+
COLUMN_TYPE,
56+
DATA_TYPE,
57+
IS_NULLABLE,
58+
COLUMN_DEFAULT,
59+
EXTRA,
60+
COLUMN_COMMENT,
61+
CHARACTER_SET_NAME,
62+
COLLATION_NAME
63+
FROM INFORMATION_SCHEMA.COLUMNS
64+
WHERE TABLE_SCHEMA = DATABASE()
65+
AND TABLE_NAME = " . $pdo->quote($table) . "
66+
AND COLUMN_NAME = " . $pdo->quote($column) . "
67+
LIMIT 1"
68+
);
69+
70+
return $row ?: null;
71+
}
72+
73+
/**
74+
* Builds a column definition fragment:
75+
* `col` <COLUMN_TYPE> [CHARSET/COLLATE] [NULL/NOT NULL] [DEFAULT ...] [EXTRA] [COMMENT ...]
76+
*/
77+
private function buildColumnDefinitionSql(array $spec, string $newName): string
78+
{
79+
$pdo = $this->getAdapter()->getConnection();
80+
81+
$type = (string)$spec['COLUMN_TYPE']; // includes length + unsigned etc.
82+
$dataType = strtolower((string)$spec['DATA_TYPE']);
83+
84+
$sql = "`{$newName}` {$type}";
85+
86+
// Preserve charset/collation for character types when present.
87+
if (!empty($spec['CHARACTER_SET_NAME']) && !empty($spec['COLLATION_NAME'])) {
88+
$charset = (string)$spec['CHARACTER_SET_NAME'];
89+
$collation = (string)$spec['COLLATION_NAME'];
90+
$sql .= " CHARACTER SET {$charset} COLLATE {$collation}";
91+
}
92+
93+
$nullable = ((string)$spec['IS_NULLABLE'] === 'YES');
94+
$sql .= $nullable ? " NULL" : " NOT NULL";
95+
96+
// DEFAULT handling
97+
$default = $spec['COLUMN_DEFAULT']; // can be null
98+
if ($default === null) {
99+
if ($nullable) {
100+
$sql .= " DEFAULT NULL";
101+
}
102+
} else {
103+
$defaultStr = (string)$default;
104+
105+
$upper = strtoupper($defaultStr);
106+
$isFuncDefault = in_array($upper, ['CURRENT_TIMESTAMP', 'CURRENT_TIMESTAMP()', 'NOW()', 'LOCALTIME', 'LOCALTIMESTAMP'], true);
107+
108+
if ($isFuncDefault) {
109+
$sql .= " DEFAULT {$upper}";
110+
} else {
111+
$numericTypes = [
112+
'tinyint','smallint','mediumint','int','integer','bigint',
113+
'decimal','numeric','float','double','real',
114+
'bit','bool','boolean',
115+
];
116+
117+
if (in_array($dataType, $numericTypes, true)) {
118+
$sql .= " DEFAULT {$defaultStr}";
119+
} else {
120+
$sql .= " DEFAULT " . $pdo->quote($defaultStr);
121+
}
122+
}
123+
}
124+
125+
if (!empty($spec['EXTRA'])) {
126+
$extra = trim((string)$spec['EXTRA']);
127+
if ($extra !== '' && stripos($extra, 'auto_increment') === false) {
128+
$sql .= " {$extra}";
129+
}
130+
}
131+
132+
if (isset($spec['COLUMN_COMMENT']) && (string)$spec['COLUMN_COMMENT'] !== '') {
133+
$sql .= " COMMENT " . $pdo->quote((string)$spec['COLUMN_COMMENT']);
134+
}
135+
136+
return $sql;
137+
}
138+
139+
private function addColumnLike(string $targetTable, string $newCol, ?array $sourceSpec, string $afterCol, string $fallbackSql): void
140+
{
141+
if ($this->columnExists($targetTable, $newCol)) {
142+
return;
143+
}
144+
145+
$colSql = $sourceSpec
146+
? $this->buildColumnDefinitionSql($sourceSpec, $newCol)
147+
: "`{$newCol}` {$fallbackSql}";
148+
149+
$this->execute("ALTER TABLE `{$targetTable}` ADD COLUMN {$colSql} AFTER `{$afterCol}`");
150+
}
151+
152+
private function modifyColumnPosition(string $targetTable, string $col, string $afterCol, ?array $spec, string $fallbackSql): void
153+
{
154+
if (!$this->columnExists($targetTable, $col)) {
155+
return;
156+
}
157+
158+
$colSql = $spec
159+
? $this->buildColumnDefinitionSql($spec, $col)
160+
: "`{$col}` {$fallbackSql}";
161+
162+
$this->execute("ALTER TABLE `{$targetTable}` MODIFY COLUMN {$colSql} AFTER `{$afterCol}`");
163+
}
164+
165+
public function up(): void
166+
{
167+
$target = 'bxt_battle_tower_honor_roll';
168+
if (!$this->tableExists($target)) {
169+
return;
170+
}
171+
172+
// Anchor after class_decode (or class if decode isn't present).
173+
$anchor = $this->columnExists($target, 'class_decode') ? 'class_decode'
174+
: ($this->columnExists($target, 'class') ? 'class' : null);
175+
176+
if ($anchor === null) {
177+
// Unexpected schema; avoid making a wrong assumption.
178+
return;
179+
}
180+
181+
// Source specs from records table (preferred).
182+
$source = 'bxt_battle_tower_records';
183+
$srcTableOk = $this->tableExists($source);
184+
185+
$specP1 = $srcTableOk ? $this->getColumnSpec($source, 'pokemon1') : null;
186+
$specP1d = $srcTableOk ? $this->getColumnSpec($source, 'pokemon1_decode') : null;
187+
$specP2 = $srcTableOk ? $this->getColumnSpec($source, 'pokemon2') : null;
188+
$specP2d = $srcTableOk ? $this->getColumnSpec($source, 'pokemon2_decode') : null;
189+
$specP3 = $srcTableOk ? $this->getColumnSpec($source, 'pokemon3') : null;
190+
$specP3d = $srcTableOk ? $this->getColumnSpec($source, 'pokemon3_decode') : null;
191+
192+
// Reasonable fallbacks if records isn't available:
193+
// - pokemon# as BLOB nullable
194+
// - pokemon#_decode as varchar(50) nullable (mirrors typical *_decode pattern)
195+
$this->addColumnLike($target, 'pokemon1', $specP1, $anchor, "blob NULL");
196+
$this->addColumnLike($target, 'pokemon1_decode', $specP1d, 'pokemon1', "varchar(50) NULL DEFAULT NULL");
197+
$this->addColumnLike($target, 'pokemon2', $specP2, 'pokemon1_decode', "blob NULL");
198+
$this->addColumnLike($target, 'pokemon2_decode', $specP2d, 'pokemon2', "varchar(50) NULL DEFAULT NULL");
199+
$this->addColumnLike($target, 'pokemon3', $specP3, 'pokemon2_decode', "blob NULL");
200+
$this->addColumnLike($target, 'pokemon3_decode', $specP3d, 'pokemon3', "varchar(50) NULL DEFAULT NULL");
201+
202+
// If message_start columns already exist (from the prior migration), ensure they sit AFTER pokemon3_decode.
203+
if ($this->columnExists($target, 'message_start')) {
204+
$specMs = $this->getColumnSpec($target, 'message_start');
205+
$this->modifyColumnPosition($target, 'message_start', 'pokemon3_decode', $specMs, "int(10) unsigned NULL DEFAULT NULL");
206+
}
207+
208+
if ($this->columnExists($target, 'message_start_decode')) {
209+
$specMsd = $this->getColumnSpec($target, 'message_start_decode');
210+
$after = $this->columnExists($target, 'message_start') ? 'message_start' : 'pokemon3_decode';
211+
$fallback = "varchar(50) NULL DEFAULT NULL";
212+
$this->modifyColumnPosition($target, 'message_start_decode', $after, $specMsd, $fallback);
213+
}
214+
}
215+
216+
public function down(): void
217+
{
218+
$target = 'bxt_battle_tower_honor_roll';
219+
if (!$this->tableExists($target)) {
220+
return;
221+
}
222+
223+
$t = $this->table($target);
224+
225+
foreach (['pokemon3_decode','pokemon3','pokemon2_decode','pokemon2','pokemon1_decode','pokemon1'] as $col) {
226+
if ($this->columnExists($target, $col)) {
227+
$t->removeColumn($col);
228+
}
229+
}
230+
231+
$t->update();
232+
233+
// Optional: re-anchor message_start back after class_decode/class if it exists.
234+
$anchor = $this->columnExists($target, 'class_decode') ? 'class_decode'
235+
: ($this->columnExists($target, 'class') ? 'class' : null);
236+
237+
if ($anchor !== null) {
238+
if ($this->columnExists($target, 'message_start')) {
239+
$specMs = $this->getColumnSpec($target, 'message_start');
240+
$this->modifyColumnPosition($target, 'message_start', $anchor, $specMs, "int(10) unsigned NULL DEFAULT NULL");
241+
}
242+
if ($this->columnExists($target, 'message_start_decode')) {
243+
$specMsd = $this->getColumnSpec($target, 'message_start_decode');
244+
$after = $this->columnExists($target, 'message_start') ? 'message_start' : $anchor;
245+
$this->modifyColumnPosition($target, 'message_start_decode', $after, $specMsd, "varchar(50) NULL DEFAULT NULL");
246+
}
247+
}
248+
}
249+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Phinx\Migration\AbstractMigration;
6+
7+
/**
8+
* Rename:
9+
* bxt_battle_tower_room_leaders -> bxt_battle_tower_trainers
10+
*
11+
* Idempotent: if already renamed, does nothing.
12+
*/
13+
final class BattleTowerRenameRoomLeadersToTrainers extends AbstractMigration
14+
{
15+
private function tableExists(string $table): bool
16+
{
17+
$pdo = $this->getAdapter()->getConnection();
18+
$row = $this->fetchRow(
19+
"SELECT 1 AS ok
20+
FROM INFORMATION_SCHEMA.TABLES
21+
WHERE TABLE_SCHEMA = DATABASE()
22+
AND TABLE_NAME = " . $pdo->quote($table) . "
23+
LIMIT 1"
24+
);
25+
return (bool)$row;
26+
}
27+
28+
public function up(): void
29+
{
30+
$old = 'bxt_battle_tower_room_leaders';
31+
$new = 'bxt_battle_tower_trainers';
32+
33+
if ($this->tableExists($new)) {
34+
return; // already renamed
35+
}
36+
if (!$this->tableExists($old)) {
37+
return; // nothing to rename
38+
}
39+
40+
$this->table($old)->rename($new)->save();
41+
}
42+
43+
public function down(): void
44+
{
45+
$old = 'bxt_battle_tower_room_leaders';
46+
$new = 'bxt_battle_tower_trainers';
47+
48+
if ($this->tableExists($old)) {
49+
return; // already rolled back
50+
}
51+
if (!$this->tableExists($new)) {
52+
return; // nothing to roll back
53+
}
54+
55+
$this->table($new)->rename($old)->save();
56+
}
57+
}

0 commit comments

Comments
 (0)