skills.php

main 320 lines · 10.8 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2/**
3 * Title: Character Skills
4 * Icon: fa-bolt
5 * Group: Players
6 * Order: 30
7 * Description: Read and rewrite a character's level, vocation and skills.
8 */
9
10if (!defined('ACP_ROOT')) {
11 http_response_code(403);
12 die('Direct access denied.');
13}
14
15function acp_skill_value(?array $skills, int $index): int {
16 return (is_array($skills) && isset($skills[$index]['value'])) ? (int)$skills[$index]['value'] : 0;
17}
18
19$isTfs10 = (znote_server_adapter()->normalizedEngine() === 'TFS_10');
20
21// ---------------------------------------------------------------------------
22// Save
23// ---------------------------------------------------------------------------
24if (isset($_POST['pid']) && intv($_POST['pid']) > 0) {
25
26 $pid = intv($_POST['pid']);
27 $backToName = trim((string)($_POST['name'] ?? ''));
28
29 $isOnline = $isTfs10 ? user_is_online_10($pid) : user_is_online($pid);
30
31 if ($isOnline) {
32 acp_flash_error(t('acp.skl.must_offline'));
33 } else {
34 $level = intv($_POST['level'] ?? 1);
35 $vocation = intv($_POST['vocation'] ?? 0);
36
37 // Base stats, derived exactly like character creation does.
38 $playercnf = $config['player'];
39 $statgain = $config['vocations_gain'][$vocation] ?? ['hp' => 0, 'mp' => 0, 'cap' => 0];
40
41 $levelsFromBase = $level - intv($playercnf['base']['level']);
42
43 $newHp = intv($playercnf['base']['health'] + ($statgain['hp'] * $levelsFromBase));
44 $newMp = intv($playercnf['base']['mana'] + ($statgain['mp'] * $levelsFromBase));
45 $newCap = intv($playercnf['base']['cap'] + ($statgain['cap'] * $levelsFromBase));
46
47 $fist = intv($_POST['fist'] ?? 0);
48 $club = intv($_POST['club'] ?? 0);
49 $sword = intv($_POST['sword'] ?? 0);
50 $axe = intv($_POST['axe'] ?? 0);
51 $dist = intv($_POST['dist'] ?? 0);
52 $shield = intv($_POST['shield'] ?? 0);
53 $fish = intv($_POST['fish'] ?? 0);
54 $magic = intv($_POST['magic'] ?? 0);
55
56 $db = db();
57 if (!$db->beginTransaction()) {
58 acp_flash_error('Could not start the database transaction.');
59 acp_redirect('skills', $backToName !== '' ? ['name' => $backToName] : []);
60 }
61
62 $ok = true;
63 if (!$isTfs10) {
64 // TFS 0.x keeps skills in their own table.
65 foreach ([0 => $fist, 1 => $club, 2 => $sword, 3 => $axe, 4 => $dist, 5 => $shield, 6 => $fish] as $skillId => $skillValue) {
66 $ok = $ok && $db->execute(
67 "UPDATE `player_skills` SET `value` = ? WHERE `player_id` = ? AND `skillid` = ? LIMIT 1;",
68 [$skillValue, $pid, $skillId]
69 );
70 }
71
72 $ok = $ok && $db->execute("
73 UPDATE `players`
74 SET `maglevel` = ?,
75 `vocation` = ?,
76 `level` = ?,
77 `experience` = ?,
78 `health` = ?,
79 `healthmax` = ?,
80 `mana` = ?,
81 `manamax` = ?,
82 `cap` = ?
83 WHERE `id` = ?
84 LIMIT 1;
85 ", [$magic, $vocation, $level, level_to_experience($level), $newHp, $newHp, $newMp, $newMp, $newCap, $pid]);
86 } else {
87 $ok = $db->execute("
88 UPDATE `players`
89 SET `vocation` = ?,
90 `level` = ?,
91 `experience` = ?,
92 `health` = ?,
93 `healthmax` = ?,
94 `mana` = ?,
95 `manamax` = ?,
96 `cap` = ?,
97 `skill_fist` = ?,
98 `skill_club` = ?,
99 `skill_sword` = ?,
100 `skill_axe` = ?,
101 `skill_dist` = ?,
102 `skill_shielding` = ?,
103 `skill_fishing` = ?,
104 `maglevel` = ?
105 WHERE `id` = ?
106 LIMIT 1;
107 ", [$vocation, $level, level_to_experience($level), $newHp, $newHp, $newMp, $newMp, $newCap, $fist, $club, $sword, $axe, $dist, $shield, $fish, $magic, $pid]);
108 }
109
110 if ($ok) {
111 $db->commit();
112 acp_log('player.update_skills', $backToName, [
113 'level' => $level, 'vocation' => $vocation, 'magic' => $magic,
114 'fist' => $fist, 'club' => $club, 'sword' => $sword, 'axe' => $axe,
115 'dist' => $dist, 'shield' => $shield, 'fish' => $fish,
116 ]);
117 acp_flash_success(t('acp.skl.updated'));
118 } else {
119 $db->rollback();
120 acp_flash_error('Could not update character skills.');
121 }
122 }
123
124 acp_redirect('skills', $backToName !== '' ? ['name' => $backToName] : []);
125}
126
127// ---------------------------------------------------------------------------
128// Load
129// ---------------------------------------------------------------------------
130$name = isset($_GET['name']) ? trim((string)$_GET['name']) : '';
131$skills = null;
132$pid = 0;
133
134if ($name !== '') {
135 if (!user_character_exist($name)) {
136 acp_flash_error(t('acp.skl.not_found', ['name' => '<strong>' . h($name) . '</strong>']));
137 acp_redirect('skills');
138 }
139
140 $pid = (int)user_character_id($name);
141
142 if (!$isTfs10) {
143 $rows = db()->fetchAll("
144 SELECT `value` FROM `player_skills`
145 WHERE `player_id` = ?
146 ORDER BY `skillid` ASC
147 LIMIT 7;
148 ", [$pid]);
149
150 // The character can exist without skill rows yet.
151 $skills = is_array($rows) ? $rows : [];
152
153 $player = db()->fetchOne("
154 SELECT `maglevel`, `level`, `vocation`
155 FROM `players`
156 WHERE `id` = ?
157 LIMIT 1;
158 ", [$pid]);
159 if (!is_array($player)) {
160 $player = ['maglevel' => 0, 'level' => 0, 'vocation' => 0];
161 }
162
163 // Pad missing skill rows so indexes 0-6 always exist.
164 while (count($skills) < 7) {
165 $skills[] = ['value' => 0];
166 }
167
168 $skills[] = ['value' => $player['maglevel']];
169 $skills[] = ['value' => $player['level']];
170 $skills[] = ['value' => $player['vocation']];
171 } else {
172 $p = db()->fetchOne("
173 SELECT `skill_fist`, `skill_club`, `skill_sword`, `skill_axe`,
174 `skill_dist`, `skill_shielding`, `skill_fishing`,
175 `maglevel`, `level`, `vocation`
176 FROM `players`
177 WHERE `id` = ?
178 LIMIT 1;
179 ", [$pid]);
180 if (!is_array($p)) {
181 $p = [];
182 }
183
184 $skills = [
185 ['value' => $p['skill_fist'] ?? 0],
186 ['value' => $p['skill_club'] ?? 0],
187 ['value' => $p['skill_sword'] ?? 0],
188 ['value' => $p['skill_axe'] ?? 0],
189 ['value' => $p['skill_dist'] ?? 0],
190 ['value' => $p['skill_shielding'] ?? 0],
191 ['value' => $p['skill_fishing'] ?? 0],
192 ['value' => $p['maglevel'] ?? 0],
193 ['value' => $p['level'] ?? 0],
194 ['value' => $p['vocation'] ?? 0],
195 ];
196 }
197}
198
199$loaded = ($name !== '');
200?>
201
202<?php if (!$loaded): ?>
203
204 <section class="acp-card" style="max-width:520px;">
205 <header class="acp-card-head">
206 <h2><?= t('acp.skl.lookup_title') ?></h2>
207 <p><?= t('acp.skl.lookup_sub') ?></p>
208 </header>
209 <div class="acp-card-body">
210 <form method="get">
211 <input type="hidden" name="p" value="skills">
212 <div class="acp-field">
213 <label class="acp-label" for="name"><?= t('acp.skl.char_name') ?></label>
214 <input class="acp-input" id="name" name="name" placeholder="<?= h(t('acp.skl.char_name')) ?>" autofocus required>
215 </div>
216 <div class="acp-actions">
217 <button class="acp-btn" type="submit"><i class="fa fa-search"></i> <?= t('acp.skl.fetch') ?></button>
218 </div>
219 </form>
220 </div>
221 </section>
222
223<?php else: ?>
224
225 <div class="acp-toolbar">
226 <div>
227 <strong><?= h($name) ?></strong>
228 <span class="acp-pill acp-pill--grey">#<?= (int)$pid ?></span>
229 <a href="<?= h(acp_site('characterprofile.php?name=' . urlencode($name))) ?>" target="_blank" rel="noopener"><?= t('acp.skl.view_profile') ?></a>
230 </div>
231 <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h(acp_url('skills')) ?>">
232 <i class="fa fa-refresh"></i> <?= t('acp.skl.search_another') ?>
233 </a>
234 </div>
235
236 <form method="post" data-confirm="<?= h(t('acp.skl.confirm_overwrite')) ?>">
237 <?= acp_csrf_field() ?>
238 <input type="hidden" name="pid" value="<?= (int)$pid ?>">
239 <input type="hidden" name="name" value="<?= h($name) ?>">
240
241 <div class="acp-grid acp-grid--2">
242 <section class="acp-card">
243 <header class="acp-card-head"><h2><?= t('acp.skl.character_title') ?></h2></header>
244 <div class="acp-card-body">
245 <div class="acp-field">
246 <label class="acp-label" for="vocation"><?= t('acp.skl.vocation') ?></label>
247 <select class="acp-select" id="vocation" name="vocation">
248 <?php foreach (($config['vocations'] ?? []) as $vid => $v): ?>
249 <option value="<?= (int)$vid ?>" <?= (int)$vid === acp_skill_value($skills, 9) ? 'selected' : '' ?>>
250 <?= h($v['name']) ?>
251 </option>
252 <?php endforeach; ?>
253 </select>
254 </div>
255 <div class="acp-row">
256 <div class="acp-field">
257 <label class="acp-label" for="level"><?= t('acp.skl.level') ?></label>
258 <input class="acp-input" id="level" name="level" type="number" min="1" value="<?= acp_skill_value($skills, 8) ?>">
259 </div>
260 <div class="acp-field">
261 <label class="acp-label" for="magic"><?= t('acp.skl.magic_level') ?></label>
262 <input class="acp-input" id="magic" name="magic" type="number" min="0" value="<?= acp_skill_value($skills, 7) ?>">
263 </div>
264 </div>
265 <p class="acp-hint">
266 <?= t('acp.skl.recalc_hint', [
267 'base' => '<code>$config[\'player\'][\'base\']</code>',
268 'gain' => '<code>$config[\'vocations_gain\']</code>',
269 ]) ?>
270 </p>
271 </div>
272 </section>
273
274 <section class="acp-card">
275 <header class="acp-card-head"><h2><?= t('acp.skl.skills_title') ?></h2></header>
276 <div class="acp-card-body">
277 <div class="acp-row">
278 <div class="acp-field">
279 <label class="acp-label" for="fist"><?= t('acp.skl.fist') ?></label>
280 <input class="acp-input" id="fist" name="fist" type="number" min="0" value="<?= acp_skill_value($skills, 0) ?>">
281 </div>
282 <div class="acp-field">
283 <label class="acp-label" for="club"><?= t('acp.skl.club') ?></label>
284 <input class="acp-input" id="club" name="club" type="number" min="0" value="<?= acp_skill_value($skills, 1) ?>">
285 </div>
286 <div class="acp-field">
287 <label class="acp-label" for="sword"><?= t('acp.skl.sword') ?></label>
288 <input class="acp-input" id="sword" name="sword" type="number" min="0" value="<?= acp_skill_value($skills, 2) ?>">
289 </div>
290 </div>
291 <div class="acp-row">
292 <div class="acp-field">
293 <label class="acp-label" for="axe"><?= t('acp.skl.axe') ?></label>
294 <input class="acp-input" id="axe" name="axe" type="number" min="0" value="<?= acp_skill_value($skills, 3) ?>">
295 </div>
296 <div class="acp-field">
297 <label class="acp-label" for="dist"><?= t('acp.skl.dist') ?></label>
298 <input class="acp-input" id="dist" name="dist" type="number" min="0" value="<?= acp_skill_value($skills, 4) ?>">
299 </div>
300 <div class="acp-field">
301 <label class="acp-label" for="shield"><?= t('acp.skl.shield') ?></label>
302 <input class="acp-input" id="shield" name="shield" type="number" min="0" value="<?= acp_skill_value($skills, 5) ?>">
303 </div>
304 </div>
305 <div class="acp-field" style="max-width:200px;">
306 <label class="acp-label" for="fish"><?= t('acp.skl.fish') ?></label>
307 <input class="acp-input" id="fish" name="fish" type="number" min="0" value="<?= acp_skill_value($skills, 6) ?>">
308 </div>
309 </div>
310 </section>
311 </div>
312
313 <div class="acp-actions">
314 <button class="acp-btn acp-btn--green" type="submit"><i class="fa fa-save"></i> <?= t('acp.skl.save') ?></button>
315 <span class="acp-hint"><?= t('acp.skl.offline_hint') ?></span>
316 </div>
317 </form>
318
319<?php endif; ?>
320
Top