<?php
// server_php/api/characters/list.php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    exit(0);
}

// 禁用错误显示，防止破坏 JSON 格式
ini_set('display_errors', 0);
error_reporting(E_ALL);

header('Content-Type: application/json');
const LIST_REQUIRED_SCHEMA_VERSION = 20260501;

require_once __DIR__ . '/_inventory_trade_lock.php';
$vipPolicyPath = dirname(__DIR__) . '/_vip_policy.php';
if (is_file($vipPolicyPath)) {
    require_once $vipPolicyPath;
}
$scratch_settings_file = __DIR__ . '/_scratch_settings.php';
if (is_file($scratch_settings_file)) {
    require_once $scratch_settings_file;
}

if (!function_exists('scratch_apply_desc_to_item_data')) {
    function scratch_apply_desc_to_item_data($db, $item_name, $item_data) {
        if (!is_array($item_data)) return $item_data;
        $name = trim(strval($item_name));
        if ($name === '散仙刮刮乐') $desc = '刮开有几率赢得元宝巨奖，每天限刮20个';
        else if ($name === '地仙刮刮乐') $desc = '刮开有几率赢得元宝巨奖，每天限刮30个';
        else if ($name === '天仙刮刮乐') $desc = '刮开有几率赢得元宝巨奖，每天限刮10个';
        else if ($name === '金仙刮刮乐') $desc = '刮开有几率赢得元宝巨奖，每天限刮3个';
        else return $item_data;
        $item_data['desc'] = $desc;
        $item_data['description'] = $desc;
        return $item_data;
    }
}

function _list_cfg($path, $default) {
    if (!function_exists('gameplay_var')) return $default;
    return gameplay_var($path, $default);
}

function _list_max_level_cap() {
    $cap = intval(_list_cfg('growth.breakthrough.max_level_cap', 79));
    return $cap > 0 ? $cap : 79;
}

function _is_db_locked_error($e) {
    if (!$e) return false;
    $msg = strtolower(strval($e->getMessage()));
    return strpos($msg, 'database is locked') !== false
        || strpos($msg, 'database table is locked') !== false
        || strpos($msg, 'general error:5') !== false
        || strpos($msg, 'sqlstate[hy000]') !== false;
}

function _name_matches($n, $targets) {
    $name = trim(strval($n));
    if ($name === '') return false;
    $base = preg_replace('/·.*/u', '', $name);
    foreach ($targets as $t) {
        $tt = trim(strval($t));
        if ($tt === '') continue;
        $tb = preg_replace('/·.*/u', '', $tt);
        if ($name === $tt || $name === ($tb . "·凡") || $name === ($tb . "·极") || $base === $tb) return true;
    }
    return false;
}

function _has_newbie_items($inventory, $equip) {
    $targets = array("新手布帽", "新手布衫", "新手草鞋", "新手吊坠", "新手武器");
    if (is_array($inventory)) {
        foreach ($inventory as $it) {
            if (!is_array($it)) continue;
            $n1 = isset($it['item_name']) ? $it['item_name'] : '';
            $n2 = '';
            if (isset($it['item_data']) && is_array($it['item_data']) && isset($it['item_data']['name'])) $n2 = $it['item_data']['name'];
            if (_name_matches($n1, $targets) || _name_matches($n2, $targets)) return true;
        }
    }
    if (is_array($equip)) {
        foreach ($equip as $eq) {
            if (!is_array($eq)) continue;
            $n1 = isset($eq['item_name']) ? $eq['item_name'] : '';
            $n2 = isset($eq['name']) ? $eq['name'] : '';
            if (_name_matches($n1, $targets) || _name_matches($n2, $targets)) return true;
        }
    }
    return false;
}

function _has_item_in_inventory_by_base($inventory, $target_base_name) {
    if (!is_array($inventory)) return false;
    $tb = trim(strval($target_base_name));
    if ($tb === '') return false;
    foreach ($inventory as $it) {
        if (!is_array($it)) continue;
        $n1 = isset($it['item_name']) ? strval($it['item_name']) : '';
        $n2 = '';
        if (isset($it['item_data']) && is_array($it['item_data']) && isset($it['item_data']['name'])) $n2 = strval($it['item_data']['name']);
        $b1 = preg_replace('/·.*/u', '', trim($n1));
        $b2 = preg_replace('/·.*/u', '', trim($n2));
        if ($b1 === $tb || $b2 === $tb) return true;
    }
    return false;
}

function _canonical_scratch_desc_item_data($db, $item_name, $item_data) {
    if (!is_array($item_data)) return $item_data;
    if (function_exists('scratch_apply_desc_to_item_data')) {
        $item_data = scratch_apply_desc_to_item_data($db, $item_name, $item_data);
    }
    return $item_data;
}

function mingshen_list_attr_defs() {
    return array(
        'phy_atk' => array('bonus_field' => 'mingshen_bonus_phy_atk'),
        'mag_atk' => array('bonus_field' => 'mingshen_bonus_mag_atk'),
        'phy_def' => array('bonus_field' => 'mingshen_bonus_phy_def'),
        'mag_def' => array('bonus_field' => 'mingshen_bonus_mag_def')
    );
}

function mingshen_list_track_storage_mode($db) {
    static $mode = null;
    if ($mode !== null) return $mode;
    $mode = 'runtime_file_fallback';
    try {
        $row = $db->query("SHOW TABLES LIKE 'settings'")->fetch(PDO::FETCH_ASSOC);
        if (is_array($row) && !empty($row)) $mode = 'settings';
    } catch (Throwable $e) {
        $mode = 'runtime_file_fallback';
    }
    return $mode;
}

function mingshen_list_setting_key($characterId, $attrKey) {
    return 'mingshen.training.track.' . strval(intval($characterId)) . '.' . strval($attrKey);
}

function mingshen_list_runtime_track_path($characterId, $attrKey) {
    $safeAttr = preg_replace('/[^a-z0-9_\-]/i', '_', strval($attrKey));
    return rtrim(strval(db_runtime_dir()), "\\/") . DIRECTORY_SEPARATOR . 'mingshen_training_tracks' . DIRECTORY_SEPARATOR . 'char_' . strval(intval($characterId)) . '_' . $safeAttr . '.json';
}

function mingshen_list_parse_track($raw, $attrKey) {
    $out = array(
        'attr_key' => strval($attrKey),
        'level_ups' => 0,
        'level' => 1,
        'exp' => 0,
        'need_exp' => 5000,
        'total_bonus' => 0,
        'total_bonus_percent' => 0
    );
    $s = trim(strval($raw));
    if ($s !== '') {
        $parsed = json_decode($s, true);
        if (is_array($parsed)) {
            if (isset($parsed['level_ups'])) $out['level_ups'] = max(0, intval($parsed['level_ups']));
            if (isset($parsed['exp'])) $out['exp'] = max(0, intval($parsed['exp']));
        }
    }
    $out['level'] = max(1, intval($out['level_ups']) + 1);
    $out['total_bonus_percent'] = intval($out['level_ups']);
    $out['total_bonus'] = intval($out['total_bonus_percent']);
    return $out;
}

function mingshen_list_fetch_track($db, $characterId, $attrKey) {
    if (mingshen_list_track_storage_mode($db) === 'settings') {
        try {
            $stmt = $db->prepare("SELECT `value` FROM `settings` WHERE `key` = ? LIMIT 1");
            $stmt->execute(array(mingshen_list_setting_key($characterId, $attrKey)));
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if (function_exists('db_close_cursor')) db_close_cursor($stmt);
            return mingshen_list_parse_track(isset($row['value']) ? $row['value'] : '', $attrKey);
        } catch (Throwable $e) {
            return mingshen_list_parse_track('', $attrKey);
        }
    }
    $path = mingshen_list_runtime_track_path($characterId, $attrKey);
    $raw = is_file($path) ? @file_get_contents($path) : '';
    return mingshen_list_parse_track($raw, $attrKey);
}

function mingshen_list_apply_bonus_fields($db, $char) {
    if (!is_array($char)) return $char;
    $charId = intval(isset($char['id']) ? $char['id'] : 0);
    $defs = mingshen_list_attr_defs();
    foreach ($defs as $attrKey => $cfg) {
        $bonusField = strval(isset($cfg['bonus_field']) ? $cfg['bonus_field'] : '');
        if ($bonusField === '') continue;
        $track = $charId > 0 ? mingshen_list_fetch_track($db, $charId, $attrKey) : mingshen_list_parse_track('', $attrKey);
        $char[$bonusField] = intval(isset($track['total_bonus']) ? $track['total_bonus'] : 0);
    }
    $char['mingshen_track_storage_mode'] = mingshen_list_track_storage_mode($db);
    return $char;
}

function _calc_equip_stat_point_bonus($equip, $extraPct = 0.0) {
    $out = array('strength' => 0, 'agility' => 0, 'constitution' => 0, 'spirit' => 0);
    if (!is_array($equip)) return $out;
    $items = $equip;
    $is_assoc = array_keys($equip) !== range(0, count($equip) - 1);
    if ($is_assoc) $items = array_values($equip);
    foreach ($items as $item) {
        if (!is_array($item)) continue;
        $out['strength'] += intval(isset($item['strength']) ? $item['strength'] : 0) + intval(isset($item['力量']) ? $item['力量'] : 0);
        $out['agility'] += intval(isset($item['agility']) ? $item['agility'] : 0) + intval(isset($item['敏捷']) ? $item['敏捷'] : 0);
        $out['constitution'] += intval(isset($item['constitution']) ? $item['constitution'] : 0) + intval(isset($item['体质']) ? $item['体质'] : 0);
        $out['spirit'] += intval(isset($item['spirit']) ? $item['spirit'] : 0) + intval(isset($item['灵力']) ? $item['灵力'] : 0);
        if (isset($item['基础属性']) && trim(strval($item['基础属性'])) !== '') {
            $parts = explode('-', strval($item['基础属性']));
            foreach ($parts as $p) {
                $sub = explode('+', $p);
                if (count($sub) !== 2) continue;
                $k = trim(strval($sub[0]));
                $v = intval($sub[1]);
                if ($k === '力量') $out['strength'] += $v;
                else if ($k === '敏捷') $out['agility'] += $v;
                else if ($k === '体质') $out['constitution'] += $v;
                else if ($k === '灵力') $out['spirit'] += $v;
                else if ($k === '全属' || $k === '所有属性') {
                    $out['strength'] += $v;
                    $out['agility'] += $v;
                    $out['constitution'] += $v;
                    $out['spirit'] += $v;
                }
            }
        }
        if (isset($item['extra_affixes']) && is_array($item['extra_affixes'])) {
            foreach ($item['extra_affixes'] as $af) {
                if (!is_array($af)) continue;
                $an = isset($af['name']) ? strval($af['name']) : '';
                $av = intval(isset($af['value']) ? $af['value'] : 0);
                if ($av === 0) continue;
                if ($an === '力量') $out['strength'] += $av;
                else if ($an === '敏捷') $out['agility'] += $av;
                else if ($an === '体质') $out['constitution'] += $av;
                else if ($an === '灵力') $out['spirit'] += $av;
                else if ($an === '全属' || $an === '所有属性') {
                    $out['strength'] += $av;
                    $out['agility'] += $av;
                    $out['constitution'] += $av;
                    $out['spirit'] += $av;
                }
            }
        }
    }
    $pct = max(0.0, floatval($extraPct));
    if ($pct > 0.0) {
        $factor = 1.0 + ($pct / 100.0);
        foreach (array('strength', 'agility', 'constitution', 'spirit') as $key) {
            $out[$key] = intval(ceil(floatval($out[$key]) * $factor));
        }
    }
    return $out;
}

function _calc_mount_stat_point_bonus($mount_name, $mount_level) {
    $out = array(
        'strength' => 0, 'agility' => 0, 'constitution' => 0, 'spirit' => 0,
        'pct_all' => 0, 'pct_potential' => 0
    );
    if (empty($mount_name) || $mount_level <= 0) return $out;
    
    $is_high_tier = in_array($mount_name, ['烈焰神凤', '傲世龙皇', '烈焰神凤凰']);
    
    if ($is_high_tier) {
        $out['pct_potential'] = $mount_level * 1;
        if ($mount_level >= 10) {
            $out['pct_all'] = 10;
        }
    } else {
        $out['strength'] += $mount_level * 5;
        $out['agility'] += $mount_level * 5;
        $out['constitution'] += $mount_level * 5;
        $out['spirit'] += $mount_level * 5;
        if ($mount_level >= 5) {
            $out['pct_potential'] = 5;
        }
    }
    
    return $out;
}

function _normalize_mount_compare_name($mount_name) {
    $name = trim(strval($mount_name));
    if ($name === '') return '';
    return preg_replace('/·.*/u', '', $name);
}

function _apply_low_level_empty_resource_refill_rule($level, $hp, $max_hp, $mp, $max_mp, $hp_pool_current, $mp_pool_current) {
    $lvl = intval($level);
    $cur_hp = intval($hp);
    $cur_mp = intval($mp);
    $mhp = intval($max_hp);
    $mmp = intval($max_mp);
    if ($lvl < 10 && $cur_hp <= 0) {
        if ($mhp <= 0) $mhp = 100;
        if ($mmp <= 0) $mmp = 50;
        return array('changed' => true, 'hp' => $mhp, 'mp' => $mmp);
    }
    return array('changed' => false, 'hp' => $cur_hp, 'mp' => $cur_mp);
}

function _decode_json_array_compat($v) {
    if (is_array($v)) return $v;
    if (!is_string($v) || trim($v) === '') return array();
    $j = json_decode($v, true);
    if (is_array($j)) return $j;
    if (is_string($j) && trim($j) !== '') {
        $j2 = json_decode($j, true);
        if (is_array($j2)) return $j2;
    }
    return array();
}

function _build_vip_display_name($base_name, $vip_level) {
    $name = trim(strval($base_name));
    if ($name === '') $name = '玩家';
    $vip = intval($vip_level);
    if ($vip < 0) $vip = 0;
    if ($vip > 7) $vip = 7;
    if ($vip <= 0) return $name;
    return $name . "[V" . strval($vip) . "]";
}

function _quality_id_from_text($quality_text) {
    $q = trim(strval($quality_text));
    if ($q === '极' || $q === '极品' || $q === '极品装备') return 3;
    if ($q === '精' || $q === '精品' || $q === '精品装备') return 2;
    if ($q === '良' || $q === '凡' || $q === '普通' || $q === '普通装备') return 1;
    if (is_numeric($q)) {
        $n = intval($q);
        if ($n < 1) $n = 1;
        if ($n > 3) $n = 3;
        return $n;
    }
    return 0;
}

function _quality_text_from_id($qid) {
    $n = intval($qid);
    if ($n >= 3) return '极';
    if ($n >= 2) return '精';
    return '良';
}

function _infer_quality_id_by_name($name) {
    $n = strval($name);
    if ($n === '') return 0;
    if (strpos($n, '·极') !== false || strpos($n, '极品') !== false) return 3;
    if (strpos($n, '·精') !== false || strpos($n, '精品') !== false) return 2;
    if (strpos($n, '·良') !== false || strpos($n, '·凡') !== false) return 1;
    return 0;
}

function _item_quality_id($item_name, $idata) {
    $best = _infer_quality_id_by_name($item_name);
    if (is_array($idata)) {
        if (isset($idata['quality_id'])) $best = max($best, _quality_id_from_text($idata['quality_id']));
        if (isset($idata['quality'])) $best = max($best, _quality_id_from_text($idata['quality']));
        if (isset($idata['quality_text'])) $best = max($best, _quality_id_from_text($idata['quality_text']));
        $nm = '';
        if (isset($idata['item_name'])) $nm = strval($idata['item_name']);
        if ($nm === '' && isset($idata['name'])) $nm = strval($idata['name']);
        $best = max($best, _infer_quality_id_by_name($nm));
    }
    if ($best <= 0) $best = 1;
    if ($best > 3) $best = 3;
    return $best;
}

function _normalize_warehouse_items_with_backtrace($items) {
    $changed = false;
    if (!is_array($items)) $items = array();
    $out = array();
    foreach ($items as $entry) {
        if (!is_array($entry)) continue;
        $item_name = strval(isset($entry['item_name']) ? $entry['item_name'] : '');
        $idata = isset($entry['item_data']) ? _decode_json_array_compat($entry['item_data']) : array();
        $origin = isset($entry['origin_item_data']) ? _decode_json_array_compat($entry['origin_item_data']) : array();
        $cur_q = _item_quality_id($item_name, $idata);
        $org_q = _item_quality_id($item_name, $origin);
        if ($org_q > $cur_q && !empty($origin)) {
            // Backtrace repair: prefer original backpack snapshot if current warehouse copy was downgraded.
            $idata = $origin;
            $cur_q = $org_q;
            $changed = true;
        }
        $entry['quality_id'] = intval($cur_q);
        if (!empty($idata)) {
            $idata['quality_id'] = intval($cur_q);
            $idata['quality'] = _quality_text_from_id($cur_q);
            if (!isset($idata['quality_text']) || trim(strval($idata['quality_text'])) === '') {
                $idata['quality_text'] = $idata['quality'];
            }
            $entry['item_data'] = $idata;
        }
        $out[] = $entry;
    }
    return array($out, $changed);
}

function _migrate_newbie_necklace($db, $char_id) {
    $target_name = "新手吊坠·极";

    $eqStmt = $db->prepare("SELECT equipment FROM characters WHERE id = ?");
    $eqStmt->execute(array($char_id));
    $eqRow = $eqStmt->fetch(PDO::FETCH_ASSOC);
    if ($eqRow && isset($eqRow['equipment']) && $eqRow['equipment'] !== '') {
        $equip = json_decode($eqRow['equipment'], true);
        if (is_array($equip)) {
            $changed = false;
            foreach ($equip as $k => $v) {
                if (!is_array($v)) continue;
                $n1 = isset($v['item_name']) ? strval($v['item_name']) : '';
                $n2 = isset($v['name']) ? strval($v['name']) : '';
                $is_old_newbie_necklace = false;
                if (strpos($n1, "新手") === 0 && strpos($n1, "吊坠") === false && strpos($n1, "链") !== false) $is_old_newbie_necklace = true;
                if (strpos($n2, "新手") === 0 && strpos($n2, "吊坠") === false && strpos($n2, "链") !== false) $is_old_newbie_necklace = true;
                if ($is_old_newbie_necklace) {
                    $equip[$k]['item_name'] = $target_name;
                    $equip[$k]['name'] = $target_name;
                    $equip[$k]['type'] = "项链";
                    $equip[$k]['icon_path'] = "res://img/装备/新手/新手吊坠.png";
                    $changed = true;
                }
            }
            if ($changed) {
                $db->prepare("UPDATE characters SET equipment = ? WHERE id = ?")->execute(array(json_encode($equip, JSON_UNESCAPED_UNICODE), $char_id));
            }
        }
    }

    $invStmt = $db->prepare("SELECT id, item_name, item_count, item_data FROM inventory WHERE character_id = ? AND item_name LIKE '新手%链%' AND item_name NOT LIKE '%吊坠%'");
    $invStmt->execute(array($char_id));
    $rows = $invStmt->fetchAll(PDO::FETCH_ASSOC);
    if (!is_array($rows) || count($rows) === 0) {
        return;
    }

    $targetStmt = $db->prepare("SELECT id, item_count, item_data FROM inventory WHERE character_id = ? AND item_name = ? LIMIT 1");
    $targetStmt->execute(array($char_id, $target_name));
    $targetRow = $targetStmt->fetch(PDO::FETCH_ASSOC);
    $target_id = $targetRow ? intval($targetRow['id']) : 0;
    $target_count = $targetRow ? intval($targetRow['item_count']) : 0;
    $added_count = 0;
    $final_data = null;
    foreach ($rows as $r) {
        $added_count += intval($r['item_count']);
        $decoded = null;
        if (isset($r['item_data']) && $r['item_data'] !== '') {
            $tmp = json_decode($r['item_data'], true);
            if (is_array($tmp)) $decoded = $tmp;
        }
        if (!is_array($decoded)) $decoded = array();
        $decoded['item_name'] = $target_name;
        $decoded['name'] = $target_name;
        $decoded['type'] = '项链';
        $decoded['icon_path'] = 'res://img/装备/新手/新手吊坠.png';
        if ($final_data === null) $final_data = $decoded;
    }
    if ($final_data === null) {
        $final_data = array(
            'item_name' => $target_name,
            'name' => $target_name,
            'type' => '项链',
            'quality' => '极',
            'level' => 1,
            'icon_path' => 'res://img/装备/新手/新手吊坠.png'
        );
    }
    if ($target_id > 0) {
        $db->prepare("UPDATE inventory SET item_count = ?, item_data = ? WHERE id = ?")->execute(array(
            $target_count + $added_count,
            json_encode($final_data, JSON_UNESCAPED_UNICODE),
            $target_id
        ));
        foreach ($rows as $r) {
            $rid = intval($r['id']);
            if ($rid !== $target_id) {
                $db->prepare("DELETE FROM inventory WHERE id = ?")->execute(array($rid));
            }
        }
    } else {
        $first_id = intval($rows[0]['id']);
        $db->prepare("UPDATE inventory SET item_name = ?, item_count = ?, item_data = ? WHERE id = ?")->execute(array(
            $target_name,
            $added_count,
            json_encode($final_data, JSON_UNESCAPED_UNICODE),
            $first_id
        ));
        for ($i = 1; $i < count($rows); $i++) {
            $db->prepare("DELETE FROM inventory WHERE id = ?")->execute(array(intval($rows[$i]['id'])));
        }
    }
}

function _ensure_character_equipment_column($db) {
    if (!db_table_has_column($db, 'characters', 'equipment')) {
        $db->exec("ALTER TABLE characters ADD COLUMN equipment LONGTEXT NULL");
    }
}

function _ensure_character_equipment_shadow_table($db) {
    $db->exec("CREATE TABLE IF NOT EXISTS character_equipment_shadow (
        character_id BIGINT UNSIGNED PRIMARY KEY,
        equipment_json LONGTEXT NOT NULL,
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
}

function _load_equipment_shadow($db, $char_id) {
    $stmt = $db->prepare("SELECT equipment_json FROM character_equipment_shadow WHERE character_id = ? LIMIT 1");
    $stmt->execute(array($char_id));
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    if (!$row || !isset($row['equipment_json'])) return '';
    $s = trim(strval($row['equipment_json']));
    return $s;
}

function _normalize_newbie_item_names($db, $char_id) {
    $pairs = array(
        "新手布帽" => "新手布帽·极",
        "新手头巾" => "新手布帽·极",
        "新手头巾·凡" => "新手布帽·极",
        "新手头巾·极" => "新手布帽·极",
        "新手布衫" => "新手布衫·极",
        "新手布衣" => "新手布衫·极",
        "新手布衣·凡" => "新手布衫·极",
        "新手布衣·极" => "新手布衫·极",
        "新手草鞋" => "新手草鞋·极",
        "新手吊坠" => "新手吊坠·极",
        "新手项链" => "新手吊坠·极",
        "新手项链·凡" => "新手吊坠·极",
        "新手项链·极" => "新手吊坠·极",
        "新手武器玄霄殿" => "新手武器玄霄殿·极",
        "新手武器盘丝洞" => "新手武器盘丝洞·极",
        "新手武器雷音寺" => "新手武器雷音寺·极",
        "新手武器广寒宫" => "新手武器广寒宫·极"
    );
    foreach ($pairs as $old_name => $new_name) {
        $q = $db->prepare("SELECT id, item_name, item_count, item_data FROM inventory WHERE character_id = ? AND item_name IN (?, ?) ORDER BY CASE WHEN item_name = ? THEN 0 ELSE 1 END, id ASC");
        $q->execute(array($char_id, $old_name, $new_name, $new_name));
        $rows = $q->fetchAll(PDO::FETCH_ASSOC);
        if (!is_array($rows) || count($rows) === 0) continue;
        $keep_id = intval($rows[0]['id']);
        $total_count = 0;
        $keep_data = null;
        foreach ($rows as $r) {
            $total_count += intval($r['item_count']);
            if ($keep_data === null && isset($r['item_data']) && $r['item_data'] !== '') {
                $tmp = json_decode($r['item_data'], true);
                if (is_array($tmp)) $keep_data = $tmp;
            }
        }
        if (!is_array($keep_data)) $keep_data = array();
        $keep_data['item_name'] = $new_name;
        $keep_data['name'] = $new_name;
        $db->prepare("UPDATE inventory SET item_name = ?, item_count = ?, item_data = ? WHERE id = ?")->execute(array(
            $new_name,
            $total_count,
            json_encode($keep_data, JSON_UNESCAPED_UNICODE),
            $keep_id
        ));
        for ($i = 1; $i < count($rows); $i++) {
            $db->prepare("DELETE FROM inventory WHERE id = ?")->execute(array(intval($rows[$i]['id'])));
        }
    }
}

function _normalize_equipment_item_names($db, $char_id) {
    $map = array(
        "新手布帽" => "新手布帽·极",
        "新手布衫" => "新手布衫·极",
        "新手草鞋" => "新手草鞋·极",
        "新手吊坠" => "新手吊坠·极",
        "新手武器玄霄殿" => "新手武器玄霄殿·极",
        "新手武器盘丝洞" => "新手武器盘丝洞·极",
        "新手武器雷音寺" => "新手武器雷音寺·极",
        "新手武器广寒宫" => "新手武器广寒宫·极"
    );
    $eqStmt = $db->prepare("SELECT equipment FROM characters WHERE id = ?");
    $eqStmt->execute(array($char_id));
    $eqRow = $eqStmt->fetch(PDO::FETCH_ASSOC);
    if (!$eqRow || !isset($eqRow['equipment']) || $eqRow['equipment'] === '') return;
    $faction = "玄霄殿";
    $fStmt = $db->prepare("SELECT faction FROM characters WHERE id = ? LIMIT 1");
    $fStmt->execute(array($char_id));
    $fRow = $fStmt->fetch(PDO::FETCH_ASSOC);
    if ($fRow && isset($fRow['faction']) && trim(strval($fRow['faction'])) !== '') {
        $faction = trim(strval($fRow['faction']));
    }
    $faction_weapon = "新手武器玄霄殿·极";
    if ($faction === "盘丝洞") $faction_weapon = "新手武器盘丝洞·极";
    else if ($faction === "雷音寺") $faction_weapon = "新手武器雷音寺·极";
    else if ($faction === "广寒宫") $faction_weapon = "新手武器广寒宫·极";
    $equip = json_decode($eqRow['equipment'], true);
    if (!is_array($equip)) return;
    $changed = false;
    foreach ($equip as $k => $v) {
        if (!is_array($v)) continue;
        $n1 = isset($v['item_name']) ? trim(strval($v['item_name'])) : '';
        $n2 = isset($v['name']) ? trim(strval($v['name'])) : '';
        $final = $n1 !== '' ? $n1 : $n2;
        if ($final === '') continue;
        if ($final === "新手头巾" || $final === "新手头巾·凡" || $final === "新手头巾·极") $final = "新手布帽·极";
        if ($final === "新手布衣" || $final === "新手布衣·凡" || $final === "新手布衣·极") $final = "新手布衫·极";
        if ($final === "新手项链" || $final === "新手项链·凡" || $final === "新手项链·极") $final = "新手吊坠·极";
        if ($final === "新手枪" || $final === "新手枪·凡" || $final === "新手枪·极") $final = $faction_weapon;
        if (isset($map[$final])) $final = $map[$final];
        if ($n1 !== $final || $n2 !== $final) {
            $equip[$k]['item_name'] = $final;
            $equip[$k]['name'] = $final;
            $changed = true;
        }
    }
    if ($changed) {
        $db->prepare("UPDATE characters SET equipment = ? WHERE id = ?")->execute(array(json_encode($equip, JSON_UNESCAPED_UNICODE), $char_id));
    }
}

function _purge_legacy_newbie_items($db, $char_id) {
    $blocked = array("新手头巾", "新手头巾·凡", "新手布衣", "新手布衣·凡", "新手枪", "新手枪·凡");

    $eqStmt = $db->prepare("SELECT equipment FROM characters WHERE id = ?");
    $eqStmt->execute(array($char_id));
    $eqRow = $eqStmt->fetch(PDO::FETCH_ASSOC);
    if ($eqRow && isset($eqRow['equipment']) && $eqRow['equipment'] !== '') {
        $equip = json_decode($eqRow['equipment'], true);
        if (is_array($equip)) {
            $changed = false;
            foreach ($equip as $k => $v) {
                if (!is_array($v)) continue;
                $n1 = isset($v['item_name']) ? strval($v['item_name']) : '';
                $n2 = isset($v['name']) ? strval($v['name']) : '';
                foreach ($blocked as $bn) {
                    if ($n1 === $bn || $n2 === $bn) {
                        unset($equip[$k]);
                        $changed = true;
                        break;
                    }
                }
            }
            if ($changed) {
                $db->prepare("UPDATE characters SET equipment = ? WHERE id = ?")->execute(array(json_encode($equip, JSON_UNESCAPED_UNICODE), $char_id));
            }
        }
    }

    $ph = implode(',', array_fill(0, count($blocked), '?'));
    $sql = "DELETE FROM inventory WHERE character_id = ? AND item_name IN ($ph)";
    $params = array_merge(array($char_id), $blocked);
    $stmt = $db->prepare($sql);
    $stmt->execute($params);

    // FIX DUPLICATED VALID ITEMS (From the previous open_pack bug)
    $valid_newbie_items = array(
        "新手布帽·凡", "新手布衫·凡", "新手草鞋·凡", "新手吊坠·凡", "新手武器玄霄殿·凡", "新手武器盘丝洞·凡", "新手武器雷音寺·凡", "新手武器广寒宫·凡",
        "新手布帽·极", "新手布衫·极", "新手草鞋·极", "新手吊坠·极", "新手武器玄霄殿·极", "新手武器盘丝洞·极", "新手武器雷音寺·极", "新手武器广寒宫·极"
    );
    foreach ($valid_newbie_items as $v_item) {
        $dup_stmt = $db->prepare("SELECT id, item_count FROM inventory WHERE character_id = ? AND item_name = ? ORDER BY id ASC");
        $dup_stmt->execute(array($char_id, $v_item));
        $rows = $dup_stmt->fetchAll(PDO::FETCH_ASSOC);
        if (is_array($rows) && count($rows) > 1) {
            $total_count = 0;
            $first_id = $rows[0]['id'];
            foreach ($rows as $r) {
                $total_count += intval($r['item_count']);
            }
            $db->prepare("UPDATE inventory SET item_count = ? WHERE id = ?")->execute(array($total_count, $first_id));
            for ($i = 1; $i < count($rows); $i++) {
                $db->prepare("DELETE FROM inventory WHERE id = ?")->execute(array($rows[$i]['id']));
            }
        }
    }
}

try {
    require_once '../../config/database.php';
    require_once '../../变量目录/加载玩法变量.php';

    $user_id = isset($_GET['user_id']) ? intval($_GET['user_id']) : 0;
    $server_id = isset($_GET['server_id']) ? intval($_GET['server_id']) : 0;

    if ($user_id <= 0 || $server_id <= 0) {
        echo json_encode(array('status' => 'success', 'characters' => array()));
        exit;
    }

    $dbClass = new Database();
    $db = $dbClass->getConnection();
    $dbClass->initDB(); // 确保表存在
    $schemaVersion = intval(db_schema_meta_get($db, '__schema_version__', 0));
    if ($schemaVersion < LIST_REQUIRED_SCHEMA_VERSION) {
        // 仅在低版本库上执行兜底迁移，避免每次请求重复探测 schema。
        try { if (!db_table_has_column($db, 'users', 'vip_level')) $db->exec("ALTER TABLE users ADD COLUMN vip_level INT NOT NULL DEFAULT 0"); } catch (Exception $e) {}
        try { if (!db_table_has_column($db, 'characters', 'vip_level_override')) $db->exec("ALTER TABLE characters ADD COLUMN vip_level_override INT NULL DEFAULT NULL"); } catch (Exception $e) {}
        try { if (!db_table_has_column($db, 'users', 'is_banned')) $db->exec("ALTER TABLE users ADD COLUMN is_banned TINYINT(1) NOT NULL DEFAULT 0"); } catch (Exception $e) {}
        _ensure_character_equipment_column($db);
        _ensure_character_equipment_shadow_table($db);
    }
    $banStmt = $db->prepare("SELECT is_banned FROM users WHERE id = ? LIMIT 1");
    $banStmt->execute(array($user_id));
    $banRow = $banStmt->fetch(PDO::FETCH_ASSOC);
    if ($banRow && intval(isset($banRow['is_banned']) ? $banRow['is_banned'] : 0) === 1) {
        echo json_encode(array('status' => 'error', 'message' => '因开挂该账户被封禁需联系管理员解封'));
        exit;
    }
    try {
        $fixLvStmt = $db->prepare("UPDATE characters SET level = 1 WHERE user_id = ? AND server_id = ? AND (level IS NULL OR level <= 0)");
        $fixLvStmt->execute(array($user_id, $server_id));
        $fixExpStmt = $db->prepare("UPDATE characters SET exp = 0 WHERE user_id = ? AND server_id = ? AND exp < 0");
        $fixExpStmt->execute(array($user_id, $server_id));
    } catch (PDOException $e) {
        if (!_is_db_locked_error($e)) throw $e;
        error_log('characters/list.php pre-fix write skipped due to lock: ' . $e->getMessage());
    }

    // 兼容旧数据库：动态检测列，避免 no such column
    $existingCols = db_table_columns($db, 'characters');

    $selectParts = array(
        "id", "user_id", "server_id", "name",
        "race", "faction", "gender", "level", "exp",
        "hp", "max_hp", "mp", "max_mp", "gold", "ingots",
        "(SELECT COALESCE(characters.vip_level_override, u.vip_level, 0) FROM users u WHERE u.id = characters.user_id LIMIT 1) AS vip_level"
    );

    $optionalCols = array(
        "equipment", "hp_pool_capacity", "hp_pool_current", "mp_pool_capacity", "mp_pool_current",
        "perm_str", "perm_agi", "perm_con", "perm_spi", "potential_points", "warehouse1", "warehouse2",
        "cultivation", "skill_levels_json",
        "strength", "agility", "constitution", "spirit", "level_attr_bonus_applied_level",
        "mount_name", "mount_active_until", "mount_speed_pct", "mount_escape_pct", "mount_equipped",
        "signature", "clan", "clan_name", "guild_name", "sect_name", "clan_id", "guild_id", "sect_id"
    );
    foreach ($optionalCols as $oc) {
        if (in_array($oc, $existingCols)) {
            $selectParts[] = $oc;
        }
    }
    $can_reconcile_pp = in_array("strength", $existingCols) && in_array("agility", $existingCols) && in_array("constitution", $existingCols) && in_array("spirit", $existingCols) && in_array("potential_points", $existingCols);
    $enable_reconcile_pp = false;

    if (in_array("current_map", $existingCols)) {
        $selectParts[] = "current_map as current_map_raw";
    } else if (in_array("map_name", $existingCols)) {
        $selectParts[] = "map_name as current_map_raw";
    } else {
        $selectParts[] = "'' as current_map_raw";
    }
    if (in_array("scene", $existingCols)) {
        $selectParts[] = "scene as scene_raw";
    } else {
        $selectParts[] = "'' as scene_raw";
    }

    if (in_array("x", $existingCols)) {
        $selectParts[] = "x as pos_x";
    } else if (in_array("pos_x", $existingCols)) {
        $selectParts[] = "pos_x";
    } else {
        $selectParts[] = "0 as pos_x";
    }

    if (in_array("y", $existingCols)) {
        $selectParts[] = "y as pos_y";
    } else if (in_array("pos_y", $existingCols)) {
        $selectParts[] = "pos_y";
    } else {
        $selectParts[] = "0 as pos_y";
    }

    $sql = "SELECT " . implode(", ", $selectParts) . " FROM characters WHERE user_id = ? AND server_id = ? ORDER BY id DESC";
    $stmt = $db->prepare($sql);
    $stmt->execute(array($user_id, $server_id));
    $chars = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // 处理每个角色
    foreach ($chars as &$char) {
        $char = mingshen_list_apply_bonus_fields($db, $char);
        $char['map_name'] = db_resolve_character_map_name(
            $db,
            isset($char['current_map_raw']) ? $char['current_map_raw'] : '',
            isset($char['scene_raw']) ? $char['scene_raw'] : '',
            '清水村'
        );
        try {
            _migrate_newbie_necklace($db, $char['id']);
            _normalize_newbie_item_names($db, $char['id']);
            _normalize_equipment_item_names($db, $char['id']);
            _purge_legacy_newbie_items($db, $char['id']);
        } catch (PDOException $e) {
            if (!_is_db_locked_error($e)) throw $e;
            error_log('characters/list.php migration write skipped due to lock: char_id=' . intval($char['id']) . ' ' . $e->getMessage());
        }
        $eqReloadStmt = $db->prepare("SELECT equipment FROM characters WHERE id = ? LIMIT 1");
        $eqReloadStmt->execute(array($char['id']));
        $eqReloadRow = $eqReloadStmt->fetch(PDO::FETCH_ASSOC);
        $equip_source = 'none';
        if ($eqReloadRow && array_key_exists('equipment', $eqReloadRow)) {
            $char['equipment'] = $eqReloadRow['equipment'];
            $equip_source = 'characters';
        }
        $shadow_eq = _load_equipment_shadow($db, $char['id']);
        if ($shadow_eq !== '') {
            $eq_cur = isset($char['equipment']) ? trim(strval($char['equipment'])) : '';
            if ($eq_cur === '' || $eq_cur === '{}' || $eq_cur === '[]') {
                $char['equipment'] = $shadow_eq;
                $equip_source = 'shadow';
            }
        }
        // 确保字段存在
        if (!isset($char['race'])) $char['race'] = '人族';
        if (!isset($char['gender'])) $char['gender'] = '男';
        if (!isset($char['name'])) $char['name'] = '';
        $char['vip_level'] = intval(isset($char['vip_level']) ? $char['vip_level'] : 0);
        if ($char['vip_level'] < 0) $char['vip_level'] = 0;
        if ($char['vip_level'] > 7) $char['vip_level'] = 7;
        $vipPolicy = function_exists('vip_get_policy') ? vip_get_policy($char['vip_level']) : array();
        $char['vip_extra_bag_slots'] = max(0, intval(isset($vipPolicy['extra_bag_slots']) ? $vipPolicy['extra_bag_slots'] : 0));
        $char['vip_set_bonus_add_pct'] = max(0.0, floatval(isset($vipPolicy['set_bonus_add_pct']) ? $vipPolicy['set_bonus_add_pct'] : 0.0));
        $char['vip_chat_world_unlimited'] = intval(isset($vipPolicy['chat_world_unlimited']) ? $vipPolicy['chat_world_unlimited'] : 0) === 1 ? 1 : 0;
        $char['vip_map_fly_free'] = intval(isset($vipPolicy['map_fly_free']) ? $vipPolicy['map_fly_free'] : 0) === 1 ? 1 : 0;
        $char['inventory_max_slots'] = 40 + intval($char['vip_extra_bag_slots']);
        $char['display_name'] = _build_vip_display_name($char['name'], $char['vip_level']);
        if (!isset($char['equipment'])) $char['equipment'] = '';
        if (!isset($char['clan'])) $char['clan'] = '';
        if (trim(strval($char['clan'])) === '') {
            foreach (array('clan_name', 'guild_name', 'sect_name') as $ck) {
                if (isset($char[$ck]) && trim(strval($char[$ck])) !== '') {
                    $char['clan'] = trim(strval($char[$ck]));
                    break;
                }
            }
        }
        if (!isset($char['warehouse1'])) $char['warehouse1'] = '[]';
        if (!isset($char['warehouse2'])) $char['warehouse2'] = '[]';
        if (!isset($char['skill_levels_json'])) $char['skill_levels_json'] = '{}';
        if (!isset($char['cultivation'])) $char['cultivation'] = 0;
        if (!isset($char['strength'])) $char['strength'] = 10;
        if (!isset($char['agility'])) $char['agility'] = 10;
        if (!isset($char['constitution'])) $char['constitution'] = 10;
        if (!isset($char['spirit'])) $char['spirit'] = 10;
        if (!isset($char['level_attr_bonus_applied_level'])) $char['level_attr_bonus_applied_level'] = intval($char['level']);
        if (!isset($char['potential_points'])) $char['potential_points'] = 0;
        if (!isset($char['hp_pool_current'])) $char['hp_pool_current'] = 0;
        if (!isset($char['mp_pool_current'])) $char['mp_pool_current'] = 0;
        if (!isset($char['max_hp'])) $char['max_hp'] = 100;
        if (!isset($char['max_mp'])) $char['max_mp'] = 50;
        if (!isset($char['hp'])) $char['hp'] = 0;
        if (!isset($char['mp'])) $char['mp'] = 0;
        $raw_level = intval(isset($char['level']) ? $char['level'] : 1);
        $raw_level = min($raw_level, _list_max_level_cap());
        $raw_exp = intval(isset($char['exp']) ? $char['exp'] : 0);
        $char['level'] = $raw_level;
        if ($char['level'] <= 0) $char['level'] = 1;
        $char['exp'] = $raw_exp;
        if ($char['exp'] < 0) $char['exp'] = 0;
        $char['cultivation'] = intval($char['cultivation']);
        if ($char['cultivation'] < 0) $char['cultivation'] = 0;
        $raw_pp = intval($char['potential_points']);
        if ($can_reconcile_pp && $enable_reconcile_pp) {
            $str = intval($char['strength']);
            $agi = intval($char['agility']);
            $con = intval($char['constitution']);
            $spi = intval($char['spirit']);
            $auto_attr = max(0, intval($char['level_attr_bonus_applied_level']) - 1);
            $spent = max(0, $str - 10 - $auto_attr) + max(0, $agi - 10 - $auto_attr) + max(0, $con - 10 - $auto_attr) + max(0, $spi - 10 - $auto_attr);
            $min_pp = max(0, intval($char['level']) * 4 - $spent);
            if ($raw_pp < $min_pp) $char['potential_points'] = $min_pp;
        }
        if ($char['level'] != $raw_level || $char['exp'] != $raw_exp) {
            try {
                $fixStmt = $db->prepare("UPDATE characters SET level = ?, exp = ? WHERE id = ?");
                $fixStmt->execute(array($char['level'], $char['exp'], $char['id']));
            } catch (PDOException $e) {
                if (!_is_db_locked_error($e)) throw $e;
                error_log('characters/list.php level/exp fix skipped due to lock: char_id=' . intval($char['id']) . ' ' . $e->getMessage());
            }
        }
        $refill = _apply_low_level_empty_resource_refill_rule(
            $char['level'],
            $char['hp'],
            $char['max_hp'],
            $char['mp'],
            $char['max_mp'],
            $char['hp_pool_current'],
            $char['mp_pool_current']
        );
        if ($refill['changed']) {
            $char['hp'] = intval($refill['hp']);
            $char['mp'] = intval($refill['mp']);
            try {
                $fixResStmt = $db->prepare("UPDATE characters SET hp = ?, mp = ? WHERE id = ?");
                $fixResStmt->execute(array($char['hp'], $char['mp'], $char['id']));
            } catch (PDOException $e) {
                if (!_is_db_locked_error($e)) throw $e;
                error_log('characters/list.php hp/mp refill write skipped due to lock: char_id=' . intval($char['id']) . ' ' . $e->getMessage());
            }
        }
        if (intval($char['potential_points']) != $raw_pp) {
            try {
                $fixPPStmt = $db->prepare("UPDATE characters SET potential_points = ? WHERE id = ?");
                $fixPPStmt->execute(array(intval($char['potential_points']), $char['id']));
            } catch (PDOException $e) {
                if (!_is_db_locked_error($e)) throw $e;
                error_log('characters/list.php potential_points write skipped due to lock: char_id=' . intval($char['id']) . ' ' . $e->getMessage());
            }
        }
        
        // Decode warehouses
        $char['warehouse1'] = isset($char['warehouse1']) ? json_decode($char['warehouse1'], true) : array();
        $char['warehouse2'] = isset($char['warehouse2']) ? json_decode($char['warehouse2'], true) : array();
        if (!is_array($char['warehouse1'])) $char['warehouse1'] = array();
        if (!is_array($char['warehouse2'])) $char['warehouse2'] = array();
        list($w1_norm, $w1_changed) = _normalize_warehouse_items_with_backtrace($char['warehouse1']);
        list($w2_norm, $w2_changed) = _normalize_warehouse_items_with_backtrace($char['warehouse2']);
        $char['warehouse1'] = $w1_norm;
        $char['warehouse2'] = $w2_norm;
        if ($w1_changed || $w2_changed) {
            try {
                $updW = $db->prepare("UPDATE characters SET warehouse1 = ?, warehouse2 = ? WHERE id = ?");
                $updW->execute(array(
                    json_encode($char['warehouse1'], JSON_UNESCAPED_UNICODE),
                    json_encode($char['warehouse2'], JSON_UNESCAPED_UNICODE),
                    $char['id']
                ));
            } catch (PDOException $e) {
                if (!_is_db_locked_error($e)) throw $e;
                error_log('characters/list.php warehouse backtrace repair skipped due to lock: char_id=' . intval($char['id']) . ' ' . $e->getMessage());
            }
        }
        
        // Avatars
        $char['avatars'] = array();
        $aStmt = $db->prepare("SELECT filename FROM avatars WHERE character_id = ?");
        $aStmt->execute(array($char['id']));
        while ($row = $aStmt->fetch(PDO::FETCH_ASSOC)) {
            $char['avatars'][] = $row['filename'];
        }
        
        // Inventory - IMPORTANT: Fetch all columns needed
        $char['inventory'] = array();
        $inventory_rows = array();
        $iStmt = $db->prepare("SELECT id, item_name, item_count, item_data FROM inventory WHERE character_id = ?");
        $iStmt->execute(array($char['id']));
        while ($row = $iStmt->fetch(PDO::FETCH_ASSOC)) {
            $item_entry = array(
                'id' => intval($row['id']),
                'item_name' => $row['item_name'],
                'item_count' => $row['item_count'],
                'item_data' => null
            );
            
            if (isset($row['item_data']) && $row['item_data'] !== '') {
                $decoded = json_decode($row['item_data'], true);
                if (is_array($decoded)) {
                    $item_entry['item_data'] = $decoded;
                }
            }
            // If item_data is missing name, polyfill it
            if (is_array($item_entry['item_data']) && !isset($item_entry['item_data']['name'])) {
                 $item_entry['item_data']['name'] = $row['item_name'];
            }
            if (is_array($item_entry['item_data'])) {
                $item_entry['item_data'] = _canonical_scratch_desc_item_data($db, $row['item_name'], $item_entry['item_data']);
            }

            $inventory_rows[] = $item_entry;
        }
        if (function_exists('db_close_cursor')) db_close_cursor($iStmt);
        $inventory_locked_counts = function_exists('inv_trade_locked_counts') ? inv_trade_locked_counts($db, $char['id']) : array();
        $char['inventory'] = function_exists('inv_apply_trade_locks_to_inventory_rows')
            ? inv_apply_trade_locks_to_inventory_rows($inventory_rows, $inventory_locked_counts)
            : $inventory_rows;

        // Equipment (JSON stored on character row)
        $equip_raw = isset($char['equipment']) ? $char['equipment'] : '';
        $decodedEquip = array();
        if ($equip_raw !== '') {
            $tmp = json_decode($equip_raw, true);
            if (is_array($tmp)) {
                $decodedEquip = $tmp;
            }
        }

        if (!is_array($decodedEquip)) $decodedEquip = array();

        $needs_regrant = false;
        try {
            $logCheck = $db->prepare("SELECT COUNT(1) FROM shop_logs WHERE character_id = ? AND item_name = '新手礼包' AND purchase_time >= COALESCE((SELECT created_at FROM characters WHERE id = ?), '1970-01-01 00:00:00')");
            $logCheck->execute(array($char['id'], $char['id']));
            $bought_pack = intval($logCheck->fetchColumn()) > 0;
            $grantCheck = $db->prepare("SELECT COUNT(1) FROM shop_logs WHERE character_id = ? AND item_name = '系统补发_新手装备'");
            $grantCheck->execute(array($char['id']));
            $already_regranted = intval($grantCheck->fetchColumn()) > 0;
            $has_unopened_pack = _has_item_in_inventory_by_base($char['inventory'], '新手礼包');
            if ($bought_pack && !$already_regranted && !$has_unopened_pack && !_has_newbie_items($char['inventory'], $decodedEquip)) {
                $needs_regrant = true;
            }
        } catch (Exception $e) {
            $needs_regrant = false;
        }

        if ($needs_regrant) {
            $faction = isset($char['faction']) ? $char['faction'] : '玄霄殿';
            $weapon_name = "新手武器玄霄殿·极";
            $weapon_type = "枪";
            $weapon_icon = "res://img/装备/20级装备/新手武器玄霄殿.png";
            if ($faction === "盘丝洞") {
                $weapon_name = "新手武器盘丝洞·极";
                $weapon_type = "爪";
                $weapon_icon = "res://img/装备/20级装备/新手武器盘丝洞.png";
            } elseif ($faction === "雷音寺") {
                $weapon_name = "新手武器雷音寺·极";
                $weapon_type = "杖";
                $weapon_icon = "res://img/装备/20级装备/新手武器雷音寺.png";
            } elseif ($faction === "广寒宫") {
                $weapon_name = "新手武器广寒宫·极";
                $weapon_type = "剑";
                $weapon_icon = "res://img/装备/20级装备/新手武器广寒宫.png";
            }
            $mul = 1.0 + 0.20 * 12.0;
            $mk_full = function($base_assoc) use ($mul) {
                $out = array();
                foreach ($base_assoc as $k => $v) $out[$k] = intval(round(floatval($v) * $mul));
                return $out;
            };
            $helmet_base = array('hp_max' => 520, 'phy_def' => 18);
            $armor_base = array('phy_def' => 42);
            $boots_base = array('dodge' => 55, 'speed' => 55);
            $neck_base = array('hp_max' => 260, 'damage_bonus' => 40);
            $weapon_base = array('damage_bonus' => 98);
            $common = array('quality' => '极', 'level' => 1, 'enhance_stars' => 6.0, 'enhance_half_stars' => 12, 'enhance_success_count' => 12, 'enhance_version' => 2);
            $grant_items = array(
                array("item_name" => "新手布帽·极", "item_data" => array_merge(array("name" => "新手布帽·极", "item_name" => "新手布帽·极", "type" => "头盔", "icon_path" => "res://img/装备/新手/新手布帽.png", "enhance_base_stats" => $helmet_base, "extra_affixes" => array(array("name"=>"体质","value"=>30), array("name"=>"物理命中","value"=>120))), $common, $mk_full($helmet_base))),
                array("item_name" => "新手布衫·极", "item_data" => array_merge(array("name" => "新手布衫·极", "item_name" => "新手布衫·极", "type" => "衣服", "icon_path" => "res://img/装备/新手/新手布衫.png", "enhance_base_stats" => $armor_base, "extra_affixes" => array(array("name"=>"体质","value"=>28), array("name"=>"伤害","value"=>85))), $common, $mk_full($armor_base))),
                array("item_name" => "新手草鞋·极", "item_data" => array_merge(array("name" => "新手草鞋·极", "item_name" => "新手草鞋·极", "type" => "鞋子", "icon_path" => "res://img/装备/新手/新手草鞋.png", "enhance_base_stats" => $boots_base, "extra_affixes" => array(array("name"=>"敏捷","value"=>24), array("name"=>"速度","value"=>65))), $common, $mk_full($boots_base))),
                array("item_name" => "新手吊坠·极", "item_data" => array_merge(array("name" => "新手吊坠·极", "item_name" => "新手吊坠·极", "type" => "项链", "icon_path" => "res://img/装备/新手/新手吊坠.png", "enhance_base_stats" => $neck_base, "extra_affixes" => array(array("name"=>"伤害","value"=>95), array("name"=>"体质","value"=>22))), $common, $mk_full($neck_base))),
                array("item_name" => $weapon_name, "item_data" => array_merge(array("name" => $weapon_name, "item_name" => $weapon_name, "type" => "武器", "weapon_type" => $weapon_type, "icon_path" => $weapon_icon, "enhance_base_stats" => $weapon_base, "extra_affixes" => array(array("name"=>"伤害","value"=>180), array("name"=>"物理命中","value"=>150), array("name"=>"速度","value"=>40))), $common, $mk_full($weapon_base)))
            );
            try {
                foreach ($grant_items as $gi) {
                    // Newbie equipment must be granted as isolated instances (never stack into existing row).
                    $db->prepare("INSERT INTO inventory (character_id, item_name, item_count, item_data) VALUES (?, ?, 1, ?)")->execute(array(
                        $char['id'],
                        $gi['item_name'],
                        json_encode($gi['item_data'], JSON_UNESCAPED_UNICODE)
                    ));
                    $char['inventory'][] = array(
                        'item_name' => $gi['item_name'],
                        'item_count' => 1,
                        'item_data' => $gi['item_data']
                    );
                }
                try {
                    $db->prepare("INSERT INTO shop_logs (character_id, item_name) VALUES (?, '系统补发_新手装备')")->execute(array($char['id']));
                } catch (Exception $e) {
                }
            } catch (PDOException $e) {
                if (!_is_db_locked_error($e)) throw $e;
                error_log('characters/list.php newbie grant write skipped due to lock: char_id=' . intval($char['id']) . ' ' . $e->getMessage());
            }
        }

        $char['equipment'] = $decodedEquip;
        $char['equip_stat_point_bonus'] = _calc_equip_stat_point_bonus($decodedEquip, floatval($char['vip_set_bonus_add_pct']));
        
        $mount_equipped = isset($char['mount_equipped']) ? intval($char['mount_equipped']) : 0;
        $mount_active_until = isset($char['mount_active_until']) ? intval($char['mount_active_until']) : 0;
        $mount_name = isset($char['mount_name']) ? strval($char['mount_name']) : '';
        $char['mount_equipped'] = $mount_equipped;
        $char['mount_active_until'] = $mount_active_until;
        $char['mount_name'] = $mount_name;

        $mount_level = 0;
        if ($mount_equipped === 1 && $mount_active_until > time()) {
            foreach ($char['inventory'] as $inv_item) {
                if (isset($inv_item['item_data'])) {
                    $idata = is_string($inv_item['item_data']) ? json_decode($inv_item['item_data'], true) : $inv_item['item_data'];
                    if (is_array($idata)) {
                        $m_name = isset($idata['mount_name']) ? strval($idata['mount_name']) : (isset($idata['name']) ? strval($idata['name']) : strval($inv_item['item_name']));
                        $m_compare = _normalize_mount_compare_name($m_name);
                        $char_mount_compare = _normalize_mount_compare_name($mount_name);
                        if ($m_compare !== '' && $m_compare === $char_mount_compare) {
                            $ml = isset($idata['mount_level']) ? intval($idata['mount_level']) : 0;
                            if ($ml > $mount_level) {
                                $mount_level = $ml;
                            }
                        }
                    }
                }
            }
        }
        $char['mount_stat_point_bonus'] = _calc_mount_stat_point_bonus($mount_name, $mount_level);
        
        $char['_equip_source'] = $equip_source;
    }

    echo json_encode(array(
        'status' => 'success',
        'characters' => $chars,
        'debug' => array(
            'db_backend' => defined('DB_BACKEND') ? DB_BACKEND : 'unknown'
        )
    ));

} catch (PDOException $e) {
    // header("HTTP/1.1 500 Internal Server Error"); // 客户端无法解析500响应体，改为返回200但包含错误信息
    $msg = _is_db_locked_error($e) ? '服务器繁忙，请稍后重试' : ('Database Error: ' . $e->getMessage());
    echo json_encode(array('status' => 'error', 'message' => $msg));
} catch (Exception $e) {
    // header("HTTP/1.1 500 Internal Server Error");
    $msg = _is_db_locked_error($e) ? '服务器繁忙，请稍后重试' : $e->getMessage();
    echo json_encode(array('status' => 'error', 'message' => $msg));
}
?>
