<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>游戏存档数据实时对照工具</title>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
<link href="https://unpkg.com/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<style>
/* 基础样式 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: #f3f4f6;
color: #1f2937;
line-height: 1.5;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 头部样式 */
header {
text-align: center;
margin-bottom: 40px;
}
h1 {
font-size: clamp(1.8rem, 4vw, 2.5rem);
font-weight: 700;
margin-bottom: 10px;
color: #1e293b;
}
header p {
color: #6b7280;
font-size: 1.1rem;
}
/* 主内容区样式 */
main {
display: flex;
flex-direction: column;
gap: 30px;
}
.card {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 20px;
transition: box-shadow 0.3s ease;
}
.card:hover {
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
display: flex;
align-items: center;
}
.card-title i {
margin-right: 8px;
}
.encrypted .card-title {
color: #3b82f6;
}
.decrypted .card-title {
color: #10b981;
}
.map-preview .card-title {
color: #f59e0b;
}
/* 文本区域样式 */
textarea {
width: 100%;
min-height: 250px;
padding: 15px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-family: Consolas, Monaco, monospace;
font-size: 0.875rem;
resize: both;
overflow: auto;
transition: all 0.3s ease;
}
textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25);
}
.decrypted textarea:focus {
border-color: #10b981;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25);
}
/* 按钮样式 */
.btn-group {
display: flex;
gap: 10px;
}
button {
display: flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background-color: #3b82f6;
color: white;
}
.btn-primary:hover {
background-color: #2563eb;
}
.btn-secondary {
background-color: #10b981;
color: white;
}
.btn-secondary:hover {
background-color: #059669;
}
.btn-neutral {
background-color: #e5e7eb;
color: #1f2937;
}
.btn-neutral:hover {
background-color: #d1d5db;
}
/* 提示消息样式 */
.message {
padding: 15px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.message-success {
background-color: #dcfce7;
color: #166534;
}
.message-error {
background-color: #fee2e2;
color: #b91c1c;
}
/* 底部样式 */
footer {
text-align: center;
margin-top: 50px;
color: #6b7280;
font-size: 0.875rem;
}
/* 全屏模式样式 */
.fullscreen-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
background-color: #1a1a1a;
padding: 20px;
display: flex;
flex-direction: column;
}
.fullscreen-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
color: white;
}
.fullscreen-textarea {
flex: 1;
width: 100%;
min-height: auto !important;
background-color: #2d2d2d;
color: #f0f0f0;
border: none;
font-size: 1rem !important;
}
.fullscreen-textarea:focus {
border-color: #10b981;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25);
}
/* 地图预览样式 */
.map-preview-container {
margin-top: 15px;
}
.map-grid {
border-collapse: collapse;
margin: 0 auto;
}
.map-grid td {
width: 30px;
height: 30px;
border: 1px solid #d1d5db;
text-align: center;
vertical-align: middle;
font-size: 10px;
background-color: #f9fafb;
}
/* 提示框样式 */
.cell-tooltip {
position: fixed;
/* 改为fixed定位,相对于视口 */
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
/* 让鼠标事件穿透提示框 */
z-index: 1000;
transition: opacity 0.1s;
white-space: nowrap;
}
/* 响应式调整 */
@media (max-width: 768px) {
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.btn-group {
width: 100%;
justify-content: flex-start;
}
.map-grid td {
width: 20px;
height: 20px;
}
}
</style>
</head>
<body>
<div id="app" class="container">
<header>
<h1>游戏存档数据实时对照工具</h1>
<p>加密数据与解密数据双向实时转换(含中英文翻译)</p>
</header>
<main>
<div class="card encrypted">
<div class="card-header">
<h2 class="card-title">
<i class="fa fa-lock"></i>加密存档数据
</h2>
<button @click="copyEncrypted" class="btn-primary">
<i class="fa fa-copy"></i> 复制
</button>
</div>
<textarea v-model="encryptedData" @input="encryptToDecrypt"
placeholder="粘贴加密的游戏存档数据到这里..."></textarea>
</div>
<div class="card decrypted">
<div class="card-header">
<h2 class="card-title">
<i class="fa fa-unlock"></i>解密数据(中文)
</h2>
<div class="btn-group">
<button @click="toggleFullscreen" class="btn-neutral">
<i class="fa" :class="isFullscreen ? 'fa-compress' : 'fa-expand'"></i>
{{ isFullscreen ? '退出全屏' : '全屏编辑' }}
</button>
<button @click="copyDecrypted" class="btn-secondary">
<i class="fa fa-copy"></i> 复制
</button>
</div>
</div>
<textarea v-model="decryptedData" @input="decryptToEncrypt"
placeholder="解密后的中文数据会显示在这里,也可以直接编辑...未知字符会显示为【原字符】"></textarea>
</div>
<div class="card map-preview">
<div class="card-header">
<h2 class="card-title">
<i class="fa fa-map"></i>楼层地图预览
</h2>
<button @click="refreshMap" class="btn-neutral">
<i class="fa fa-refresh"></i> 刷新地图
</button>
</div>
<div v-if="mapData.length > 0" class="map-preview-container">
<div v-if="showTooltip" class="cell-tooltip" :style="{
left: tooltipX + 'px',
top: tooltipY + 'px'
}">
{{ tooltipValue }}
</div>
<table class="map-grid">
<tbody>
<tr v-for="(row, rowIndex) in mapData" :key="rowIndex">
<td v-for="(cell, colIndex) in row" :key="colIndex" :style="{
backgroundColor: (cell === '墙'||cell === 'X') ? 'black' :
(cell === '空' || cell === 'Y') ? 'white' : 'gray',
width: '30px',
height: '30px',
border: '1px solid #ccc',
position: 'relative'
}" @mouseenter="handleMouseEnter(cell, $event)" @mousemove="handleMouseMove($event)"
@mouseleave="handleMouseLeave">
<template v-if="(currentX+1===colIndex)&&(currentY+1===rowIndex)">
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '12px',
height: '12px',
backgroundColor: '#00e5ff',
borderRadius: '50%',
boxShadow: '0 0 10px rgba(0, 229, 255, 0.9), 0 0 20px rgba(0, 229, 255, 0.5)',
zIndex: 10
}"></span>
</template>
<template v-if="typeof cell === 'object'">
<template v-if="cell.o != null">
<template v-if="cell.o === 0">
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '15px',
height: '15px',
borderRadius: '50%',
backgroundColor: 'red',
boxShadow: '0 0 3px rgba(255, 0, 0, 0.8)'
}"></span>
</template>
<template v-else-if="cell.o === 1">
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%) rotate(45deg)',
width: '14px',
height: '14px',
backgroundColor: '#1e90ff',
boxShadow: '0 0 4px rgba(30, 144, 255, 0.8)',
border: '1px solid rgba(255, 255, 255, 0.5)'
}"></span>
</template>
<template v-else-if="cell.o === 2">
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '16px',
height: '16px',
backgroundColor: '#9932cc',
clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)',
border: '1px solid #9932cc',
zIndex: 2
}"></span>
</template>
<template v-else-if="cell.o === 3">
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: '#ffd700',
boxShadow: '0 0 3px rgba(255, 215, 0, 0.8)'
}"></span>
</template>
<template v-else-if="cell.o === 4">
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%) rotate(45deg)',
width: '14px',
height: '14px',
backgroundColor: '#ff4422',
border: '1px solid #ffaa00',
boxShadow: '0 0 6px rgba(255,68,34,0.9), 0 0 10px rgba(255,170,0,0.5)',
zIndex: 3
}"></span>
</template>
<template v-else-if="cell.o === 5">
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '18px',
height: '10px',
backgroundColor: '#8b4513',
border: '1px solid #5d2f09',
borderRadius: '2px'
}"></span>
</template>
<template v-else-if="cell.o === 6">
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '18px',
height: '18px',
backgroundColor: '#e6ccff', /* 更明亮的浅紫色 */
clipPath: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
boxShadow: '0 0 10px 3px rgba(230, 204, 255, 0.9), 0 0 15px 5px rgba(230, 204, 255, 0.6)', /* 强化静态发光 */
border: '1px solid rgba(255, 255, 255, 0.8)', /* 白色边框增加亮度 */
zIndex: 2 /* 确保在其他元素上方显示 */
}"></span>
</template>
<template v-else-if="cell.o === 7">
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 0,
height: 0,
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderBottom: '16px solid #32cd32',
boxShadow: '0 0 4px rgba(50, 205, 50, 0.8)'
}"></span>
</template>
<template v-else-if="cell.o === 8">
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '14px',
height: '18px',
backgroundColor: '#40e0d0',
clipPath: 'polygon(50% 0%, 100% 35%, 80% 100%, 20% 100%, 0% 35%)',
boxShadow: '0 0 4px rgba(64, 224, 208, 0.7)',
zIndex: 2
}"></span>
</template>
<template v-else-if="cell.o === 9">
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '18px',
height: '16px',
backgroundColor: '#ff6347',
border: '2px solid #b22222',
borderRadius: '3px',
boxShadow: '0 0 6px rgba(255, 99, 71, 0.9)',
zIndex: 2
}"></span>
</template>
<template v-else>
<span :style="{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
color: '#fff',
fontSize: '16px',
fontWeight: 'bold'
}">?</span>
</template>
</template>
<template v-else>
<span :style="{
position: 'absolute',
width: '100%',
height: '100%',
top: '0',
right: '0',
bottom: '0',
left: '0',
background: cell.w==='T'?'#000':'#fff',
fontSize: '16px',
fontWeight: 'bold'
}"></span>
</template>
</template>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="message message-error">
<i class="fa fa-exclamation-circle"></i>
<span>未找到楼层地砖数据或数据格式不正确</span>
</div>
</div>
<div v-if="message" :class="'message message-' + messageType">
<i :class="messageType === 'success' ? 'fa fa-check-circle' : 'fa fa-exclamation-circle'"></i>
<span>{{ message }}</span>
</div>
<div v-if="isFullscreen" class="fullscreen-container">
<div class="fullscreen-header">
<h2><i class="fa fa-unlock"></i> 解密数据全屏编辑</h2>
<button @click="toggleFullscreen" class="btn-neutral">
<i class="fa fa-compress"></i> 退出全屏
</button>
</div>
<textarea v-model="fullscreenContent" @input="syncFullscreenChanges" class="fullscreen-textarea"
placeholder="在此编辑解密数据..."></textarea>
</div>
</main>
<footer>
<p>游戏存档数据转换工具 © 2023</p>
</footer>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
// 加密和解密数据
encryptedData: '',
decryptedData: '',
// 存储"楼层地砖"所在行的内容
floorTilesData: '',
// 解析后的地图数据(二维数组)
mapData: [],
showTooltip: false,
tooltipValue: '',
tooltipX: 0,
tooltipY: 0,
currentX: -1, // 当前角色位置的坐标
currentY: -1, // 当前角色位置的坐标
// 全屏相关数据
isFullscreen: false,
fullscreenContent: '',
// 消息提示
message: '',
messageType: 'success',
// 替换加密的密码对应表
cipherMap: {
"h": "a",
"Q": "b",
"L": "c",
"y": "d",
"1": "e",
"l": "f",
"X": "g",
"s": "h",
"Y": "i",
"d": "j",
"n": "k",
"i": "l",
"P": "m",
"J": "n",
"r": "o",
"F": "p",
"t": "q",
"Z": "r",
"V": "s",
"f": "t",
"w": "u",
"#": "v",
"I": "w",
"u": "x",
"C": "y",
// "?": "z", // 无
"e": "A",
"v": "B",
"O": "C",
"T": "D",
"E": "E",
"j": "F",
"q": "G",
"@": "H",
"5": "I",
// "?": "J", // 无
// "?": "K", // 无
"o": "L",
"b": "M",
"4": "N",
"N": "O",
"K": "P",
"\"": "Q", // 猜测(数组中单个Q表示可见的不可达区域)
"c": "R",
"7": "S",
"p": "T",
"k": "U",
// "?": "v", // 无
"g": "W",
"R": "X", // 猜测(X32表示连续32个完全黑暗的不可达虚空)
"2": "Y", // 猜测(Y表示当前情况无法走到的道路)
"G": "Z", // 猜测(数组中单个Z表示道路)
"}": "0",
"3": "1",
"S": "2",
"0": "3",
"B": "4",
"D": "5",
"9": "6",
"a": "7",
"8": "8",
"W": "9",
"]": "{",
"[": "}",
",": "[",
"!": "]",
"z": ":",
".": "\"",
":": ",",
"{": ".",
"-": "-",
" ": " ",
},
// 英文到汉字的对照表
englishToChineseMap: {
// 基础属性
"shopsUnlocked": "已解锁商店",
"items": "物品",
"defense": "防御力",
"bestiaryMonsterIDs": "图鉴怪物ID",
"hpGemsThisRun": "本次获得的生命宝石",
"goldMultiplier": "金币倍率",
"storyProgress": "剧情进度",
"successfulRuns": "成功运行次数",
"attGems": "攻击宝石",
"defEarth": "土系防御",
"mpGemsThisRun": "本次获得的魔法宝石",
"defIce": "冰系防御",
"mpGems": "魔法宝石",
"potionDuration": "药水持续时间",
"maxMP": "最大魔法值",
"attGemsThisRun": "本次获得的攻击宝石",
"shopsFoundThisRun": "本次发现的商店",
"hp": "当前生命值",
"attack": "攻击力",
"attWind": "风系攻击",
"foundFloorObjects": "发现的楼层物品",
"runsToBeatTheGame": "通关所需运行次数",
"secretTheseFiveFloors": "这五层的秘密",
"gamepadSelectedHotkeySlot": "手柄选中的快捷键槽",
"creationTime": "创建时间",
"numFloorObjects": "楼层物品数量",
"snapshotData": "快照数据",
"gold": "金币",
"equippedItems": "已装备物品",
"skillNodesUnlocked": "已解锁技能节点",
"mp": "当前魔法值",
"nextLevelAt": "下一等级所需经验",
"level": "等级",
"magGemCost": "魔法宝石成本",
"defGems": "防御宝石",
"attEarth": "土系攻击",
"attIce": "冰系攻击",
"skillGemCost": "技能宝石成本",
"dodgeChance": "闪避几率",
"hpGems": "生命宝石",
"maxHP": "最大生命值",
"magic": "魔法值",
"defWind": "风系防御",
"critChance": "暴击几率",
"hotkeySlotItems": "快捷键槽物品",
"dropBonus": "掉落加成",
"defLight": "光系防御",
"killChance": "击杀几率",
"sellPercent": "出售百分比",
"gameSwitches": "游戏开关",
"hpGemCost": "生命宝石成本",
"skillPoints": "技能点",
"skillGems": "技能宝石",
"attLight": "光系攻击",
"hpRegen": "生命回复",
"defGemCost": "防御宝石成本",
"magGems": "魔法宝石",
"attFire": "火系攻击",
"attDark": "暗系攻击",
"mpGemCost": "魔法宝石成本",
"mpRegen": "魔法回复",
"bleed": "流血效果",
"attGemCost": "攻击宝石成本",
"skillPointsPerLevel": "每级技能点",
"defDark": "暗系防御",
"defFire": "火系防御",
"attackDelay": "攻击延迟",
"critMultiplier": "暴击倍率",
"potionEfficiency": "药水效率",
"lifeDrain": "生命吸取",
"exp": "经验值",
"expMultiplier": "经验倍率",
// 其他属性
"startPoint": "起始点",
"museumItems": "博物馆物品",
"deepestFloor": "最深楼层",
"currentFloor": "当前楼层",
"heroType": "英雄类型",
"magGemsThisRun": "本次获得的魔法宝石",
"runStartGold": "运行开始时的金币",
"itemsEnchanted": "已附魔物品数量",
"skillGemsThisRun": "本次获得的技能宝石",
"floorTiles": "楼层地砖",
"wingsFound": "已发现翅膀",
"legacySavesChecked": "已检查旧存档",
"activeEffects": "活跃效果",
"exists": "是否存在",
"sorcerorPhase": "巫师阶段",
"potionsCrafted": "已制作药水数量",
"runStartLevel": "运行开始时的等级",
"defGemsThisRun": "本次获得的防御宝石",
"canChangeHeroes": "是否可更换英雄",
"shopItems": "商店物品",
"hotkeyItems": "快捷键物品",
"wingsSoldFloor": "翅膀出售楼层",
"runStartFloor": "运行开始时的楼层",
"gameMode": "游戏模式",
"newGamePlusNum": "游戏周目",
"secretRoomsFound": "已发现的秘密房间",
"messageWins": "胜利消息",
// 地图信息
"Z": "空",
"Q": "墙",
},
// 反向映射表(用于解密到加密)
reverseCipherMap: {},
// 汉字到英文的反向对照表(用于中文编辑后重新加密)
chineseToEnglishMap: {},
// 排序后的中文键列表(按长度排序,用于正确翻译)
sortedChineseKeys: []
}
},
created() {
// 生成反向映射表
this.generateReverseMap();
// 生成中文到英文的反向对照表及排序后的键列表
this.generateChineseToEnglishMap();
},
methods: {
// 生成反向映射表(加密映射的反向)
generateReverseMap() {
for (const [key, value] of Object.entries(this.cipherMap)) {
this.reverseCipherMap[value] = key;
}
},
// 生成中文到英文的反向对照表及排序后的键列表
generateChineseToEnglishMap() {
// 创建中文到英文的映射
for (const [english, chinese] of Object.entries(this.englishToChineseMap)) {
this.chineseToEnglishMap[chinese] = english;
}
// 提取所有中文键并按长度降序排序(长词汇优先)
this.sortedChineseKeys = Object.keys(this.chineseToEnglishMap)
.sort((a, b) => b.length - a.length);
},
// 将英文转换为中文
translateToChinese(text) {
let result = text;
// 先将较长的英文词汇翻译,避免被短词汇分割
const sortedEnglishEntries = Object.entries(this.englishToChineseMap)
.sort((a, b) => b[0].length - a[0].length);
// 遍历排序后的英文-中文映射表,替换所有匹配的英文为中文
for (const [english, chinese] of sortedEnglishEntries) {
const regex = new RegExp(`${english}`, 'g');
result = result.replace(regex, chinese);
}
return result;
},
// 将中文转换为英文(用于编辑后重新加密)
translateToEnglish(text) {
let result = text;
// 使用排序后的中文键列表(长词汇优先)进行替换
for (const chinese of this.sortedChineseKeys) {
const english = this.chineseToEnglishMap[chinese];
// 使用正则表达式确保匹配整个词汇
const regex = new RegExp(`${chinese}`, 'g');
result = result.replace(regex, english);
}
return result;
},
// 处理格式化:遇到[时,在遇到]之前不换行,其他情况遇到{或}时换行
formatText(jsonStr) {
let result = '';
let indentLevel = 0; // 缩进层级
let inString = false; // 是否在字符串中
let prevChar = ''; // 上一个字符
let currentValue = ''; // 当前值缓存
const fullWidthSpace = ' '; // 中文全角空格
let openBraces = 0; // 未闭合的"{"个数
let openBrackets = 0; // 未闭合的"["个数
// 用于生成缩进的辅助函数,使用全角空格
const getIndent = () => fullWidthSpace.repeat(indentLevel * 2);
for (const char of jsonStr) {
// 处理字符串边界(考虑转义的引号)
if (char === '"' && prevChar !== '\\') {
inString = !inString;
result += char;
prevChar = char;
continue;
}
// 不在字符串中时才处理格式化
if (!inString) {
// 缓存当前值字符
if (char !== '{' && char !== '}' && char !== '[' && char !== ']' &&
char !== ',' && char !== ':' && char.trim() !== '') {
currentValue += char;
}
// 判断是否在数组内部
const inArray = openBrackets > 0;
switch (char) {
case '{':
indentLevel++;
openBraces++;
if (currentValue) {
result += currentValue;
currentValue = '';
}
result += char;
// 数组内部不添加任何空格和换行,外部则换行缩进
if (!inArray) {
result += '\n' + getIndent();
}
break;
case '[':
if (currentValue) {
result += currentValue;
currentValue = '';
}
result += char;
indentLevel++;
openBrackets++;
break;
case '}':
indentLevel = Math.max(0, indentLevel - 1);
openBraces = Math.max(0, openBraces - 1);
if (currentValue) {
result += currentValue;
currentValue = '';
}
// 数组内部不添加任何空格和换行,外部则换行缩进
if (!inArray) {
result += '\n' + getIndent() + char;
} else {
result += char;
}
break;
case ']':
indentLevel = Math.max(0, indentLevel - 1);
openBrackets = Math.max(0, openBrackets - 1);
if (currentValue) {
result += currentValue;
currentValue = '';
}
result += char;
break;
case ',':
if (currentValue) {
result += currentValue;
currentValue = '';
}
result += char;
// 数组内部不添加任何空格和换行,外部则换行缩进
if (!inArray) {
result += '\n' + getIndent();
}
break;
case ':':
if (currentValue) {
result += currentValue;
currentValue = '';
}
// 数组内部不添加空格,外部添加全角空格
result += ':' + (inArray ? '' : fullWidthSpace);
break;
case ' ':
case '\n':
case '\t':
// 忽略原有的空白字符,但如果有缓存值,先添加
if (currentValue) {
result += currentValue;
currentValue = '';
}
break;
default:
// 非特殊字符且不在字符串中,不直接添加,由缓存处理
break;
}
} else {
// 在字符串中,直接添加字符
result += char;
}
prevChar = char;
}
// 添加最后可能剩余的缓存值
if (currentValue) {
result += currentValue;
}
return result;
},
// 提取"楼层地砖"所在行的内容
extractFloorTilesData(text) {
// 查找包含"楼层地砖"的行
const lines = text.split('\n');
for (const line of lines) {
if (line.includes('楼层地砖')) {
// 提取并清理数据(去除前后空格和逗号)
this.floorTilesData = line.trim().replace(/,$/, '');
// 提取后立即解析地图数据
this.parseFloorTilesData();
break;
}
}
},
// 解析楼层地砖数据为二维数组
// 解析楼层地砖数据为二维数组,并处理全是"墙"的起始行和列
parseFloorTilesData() {
// 重置地图数据
this.mapData = [];
if (!this.floorTilesData) {
return;
}
try {
// 提取冒号后的数组部分
const colonIndex = this.floorTilesData.indexOf(':');
let dataPart = colonIndex !== -1 ? this.floorTilesData.slice(colonIndex + 1).trim() : '';
if (!dataPart) {
throw new Error("未找到有效的数组数据");
}
// 删除全部普通空格
dataPart = dataPart.replace(/\s/g, '');
// ==========新增:展开字符+数字压缩格式 X32 / X11{xxx} ==========
const expandedPart = dataPart.replace(/([a-zA-Z])(\d+)(?=\{|\D|$)/g, (match, char, numStr,
offset, srcStr) => {
const count = Number(numStr);
let out = Array(count).fill(char).join(',');
// 看匹配结束位置的下一个字符是不是 {,是则补逗号
const nextPos = offset + match.length;
if (nextPos < srcStr.length && (srcStr[nextPos] === '{' || srcStr[nextPos] ===
'Y')) {
out += ',';
}
return out;
});
// ============================================================
// 处理没有引号的字符串元素,给它们添加引号
let formattedData = expandedPart.replace(/([^,\[\]{}:]+?)(?=[,\[\]}:])/g, (match) => {
if (!/^\d+$/.test(match.trim()) && !/^["'].*["']$/.test(match.trim())) {
return `"${match.trim()}"`;
}
return match;
});
// 将处理后的字符串解析为JSON
const parsedData = JSON.parse(formattedData);
// 验证是否为二维数组
if (Array.isArray(parsedData) && parsedData.every(row => Array.isArray(row))) {
// 处理全是"墙"的行和列
let processedData = [...parsedData];
// 移除全是"墙"的起始行
while (processedData.length > 0 && processedData[0].every(cell => cell === "墙")) {
processedData.shift();
}
// 移除全是"墙"的末尾行
while (processedData.length > 0 && processedData[processedData.length - 1].every(cell =>
cell === "墙")) {
processedData.pop();
}
// 关键修改:进行矩阵转置,将列转为行
if (processedData.length > 0) {
const rows = processedData.length;
const cols = processedData[0].length;
// 初始化转置后的数组
let transposedData = [];
for (let j = 0; j < cols; j++) {
transposedData[j] = [];
for (let i = 0; i < rows; i++) {
// 原[i][j]变为新[j][i]
transposedData[j][i] = processedData[i][j];
}
}
// 移除全是"墙"的起始行
while (transposedData.length > 0 && transposedData[0].every(cell => cell === "墙")) {
transposedData.shift();
}
// 移除全是"墙"的末尾行
while (transposedData.length > 0 && transposedData[transposedData.length - 1].every(
cell => cell === "墙")) {
transposedData.pop();
}
// 新增:在最外层添加一层"墙"
if (transposedData.length > 0) {
const colsCount = transposedData[0].length;
// 创建顶部和底部的墙行(全是"墙")
const wallRow = Array(colsCount).fill("墙");
// 在顶部添加墙行
transposedData.unshift(wallRow);
// 在底部添加墙行
transposedData.push(wallRow);
// 在每行的首尾添加墙
transposedData = transposedData.map(row => {
return ["墙", ...row, "墙"];
});
}
this.mapData = transposedData;
} else {
this.mapData = processedData;
}
// 显示解析成功信息
const rowCount = this.mapData.length;
const colCount = rowCount > 0 ? this.mapData[0].length : 0;
this.showMessage(`地图解析成功,共 ${rowCount} 行 ${colCount} 列,已添加外层墙`, 'success');
} else {
throw new Error("解析的数据不是二维数组");
}
} catch (error) {
console.error("解析楼层地砖数据失败:", error);
this.showMessage(`地图解析失败: ${error.message}`, 'error');
this.mapData = [];
}
},
// 刷新地图预览
refreshMap() {
this.parseFloorTilesData();
},
// 加密数据转解密数据:先字符映射,再翻译为中文,最后格式化
encryptToDecrypt() {
// 1. 应用字符映射转换,未找到映射的字符用【】包裹
let result = '';
for (const char of this.encryptedData) {
if (this.cipherMap.hasOwnProperty(char)) {
result += this.cipherMap[char];
} else {
console.log('char', char)
// 对未知字符使用【】包裹
result += `【${char}】`;
}
}
// 2. 将结果中的英文翻译为中文
const translatedResult = this.translateToChinese(result);
// 获取当前角色的坐标--begin
const reg = /"起始点"\s*:\s*\{\s*"x"\s*:\s*(\d+)\s*,\s*"y"\s*:\s*(\d+)\s*\}/;
const match = translatedResult.match(reg);
if (match) {
const x = Number(match[1]);
const y = Number(match[2]);
this.currentX = x;
this.currentY = y;
} else {
this.currentX = -1;
this.currentY = -1;
}
// 获取当前角色的坐标--end
// 3. 应用格式化规则
const formattedResult = this.formatText(translatedResult);
// 4. 提取楼层地砖数据
this.extractFloorTilesData(formattedResult);
// 避免循环更新
if (this.decryptedData !== formattedResult) {
this.decryptedData = formattedResult;
// 如果在全屏模式下,同步更新全屏内容
if (this.isFullscreen) {
this.fullscreenContent = formattedResult;
}
}
},
// 解密数据转加密数据:先将中文翻译为英文,移除格式,再字符反向映射
decryptToEncrypt() {
this.fullscreenContent = this.decryptedData;
// 1. 先将中文翻译回英文
const englishText = this.translateToEnglish(this.decryptedData);
// 2. 移除所有换行符、制表符、中文空格
let textWithoutNewlines = englishText.replace(/[\n\r\t ]/g, '');
// 3. 处理【】包裹的未知字符,提取原始字符
// 使用正则表达式匹配【...】格式并提取内容
const unknownCharRegex = /【(.*?)】/g;
textWithoutNewlines = textWithoutNewlines.replace(unknownCharRegex, (match, p1) => p1);
// 4. 应用反向字符映射
let result = '';
for (const char of textWithoutNewlines) {
if (this.reverseCipherMap.hasOwnProperty(char)) {
result += this.reverseCipherMap[char];
} else {
// 对于解密区新增的未知字符,直接保留
console.log('char')
result += char;
}
}
// 5. 更新楼层地砖数据
this.extractFloorTilesData(this.decryptedData);
// 避免循环更新
if (this.encryptedData !== result) {
this.encryptedData = result;
}
},
// 复制加密数据
copyEncrypted() {
this.copyToClipboard(this.encryptedData, '加密数据已复制到剪贴板');
},
// 复制解密数据
copyDecrypted() {
this.copyToClipboard(this.decryptedData, '解密数据已复制到剪贴板');
},
// 复制到剪贴板通用方法
copyToClipboard(text, successMsg) {
if (!text) {
this.showMessage('没有可复制的内容', 'error');
return;
}
navigator.clipboard.writeText(text)
.then(() => {
this.showMessage(successMsg, 'success');
})
.catch(err => {
this.showMessage('复制失败,请手动复制', 'error');
console.error('复制失败:', err);
});
},
// 显示消息提示
showMessage(text, type = 'success') {
this.message = text;
this.messageType = type;
// 3秒后自动隐藏消息
setTimeout(() => {
this.message = '';
}, 3000);
},
// 切换全屏模式
toggleFullscreen() {
if (!this.isFullscreen) {
// 进入全屏模式前保存当前内容
this.fullscreenContent = this.decryptedData;
this.isFullscreen = true;
// 进入全屏后自动聚焦文本框
this.$nextTick(() => {
document.querySelector('.fullscreen-textarea').focus();
});
this.showMessage('已进入全屏编辑模式', 'success');
} else {
// 退出全屏模式时更新内容
this.decryptedData = this.fullscreenContent;
this.decryptToEncrypt(); // 同步到加密区域
this.isFullscreen = false;
this.showMessage('已退出全屏编辑模式', 'success');
}
},
// 同步全屏模式下的编辑内容
syncFullscreenChanges() {
this.decryptedData = this.fullscreenContent;
// 1. 先将中文翻译回英文
const englishText = this.translateToEnglish(this.fullscreenContent);
// 2. 移除所有换行符、制表符、中文空格
let textWithoutNewlines = englishText.replace(/[\n\r\t ]/g, '');
// 3. 处理【】包裹的未知字符,提取原始字符
const unknownCharRegex = /【(.*?)】/g;
textWithoutNewlines = textWithoutNewlines.replace(unknownCharRegex, (match, p1) => p1);
// 4. 应用反向字符映射
let result = '';
for (const char of textWithoutNewlines) {
if (this.reverseCipherMap.hasOwnProperty(char)) {
result += this.reverseCipherMap[char];
} else {
// 对于解密区新增的未知字符,直接保留
result += char;
}
}
// 5. 更新楼层地砖数据
this.extractFloorTilesData(this.fullscreenContent);
// 避免循环更新
if (this.encryptedData !== result) {
this.encryptedData = result;
}
},
// 鼠标进入单元格时显示提示
handleMouseEnter(cellValue, event) {
this.showTooltip = true;
this.updateTooltipPosition(event);
// 根据cellValue的类型和属性设置提示文字
if (typeof cellValue === 'object' && cellValue !== null) {
if ('o' in cellValue) {
switch (cellValue.o) {
case 0:
// 处理存在p属性的情况(怪物)
if ('p' in cellValue) {
switch (cellValue.p) {
case 266:
this.tooltipValue = `老鼠[${JSON.stringify(cellValue)}]`;
break;
case 273:
this.tooltipValue = `蝙蝠[${JSON.stringify(cellValue)}]`;
break;
default:
this.tooltipValue = `未知怪物[${JSON.stringify(cellValue)}]`;
}
} else
this.tooltipValue = `未知[${JSON.stringify(cellValue)}]`;
break;
case 1:
if(cellValue.c === 3)
this.tooltipValue = `魔法镐宝箱[${JSON.stringify(cellValue)}]`;
else if(cellValue.c === 2)
this.tooltipValue = `材料宝箱[${JSON.stringify(cellValue)}]`;
else
this.tooltipValue = `装备宝箱?[${JSON.stringify(cellValue)}]`;
break;
case 2:
if (cellValue.s === 0)
this.tooltipValue = `生命上限宝石[${JSON.stringify(cellValue)}]`;
else if (cellValue.s === 1)
this.tooltipValue = `魔法值上限宝石[${JSON.stringify(cellValue)}]`;
else
this.tooltipValue = `未知宝石[${JSON.stringify(cellValue)}]`;
break;
case 3:
if ('g' in cellValue) {
this.tooltipValue = `金钱: ${cellValue.g}[${JSON.stringify(cellValue)}]`;
} else
this.tooltipValue = `未知[${JSON.stringify(cellValue)}]`;
break;
case 4:
this.tooltipValue = `隐藏房间[${JSON.stringify(cellValue)}]`;
break;
case 5:
this.tooltipValue = `书柜装饰[${JSON.stringify(cellValue)}]`;
break;
case 6:
this.tooltipValue = `剧情触发点[${JSON.stringify(cellValue)}]`;
break;
case 7:
this.tooltipValue = `出口[${JSON.stringify(cellValue)}]`;
break;
case 8:
this.tooltipValue = `npc[${JSON.stringify(cellValue)}]`;
break;
case 9:
this.tooltipValue = `锻造炉[${JSON.stringify(cellValue)}]`;
break;
default:
this.tooltipValue = `未知[${JSON.stringify(cellValue)}]`;
break;
}
} else
this.tooltipValue = `未知[${JSON.stringify(cellValue)}]`;
} else { // 非JSON对象的情况(墙、空等)
this.tooltipValue = cellValue + `[${JSON.stringify(cellValue)}]`;
}
},
// 鼠标在单元格内移动时更新提示位置
handleMouseMove(event) {
this.updateTooltipPosition(event);
},
// 鼠标离开单元格时隐藏提示
handleMouseLeave() {
this.showTooltip = false;
},
// 更新提示框位置,考虑滚动偏移
updateTooltipPosition(event) {
// 使用clientX和clientY获取相对于视口的位置
// 这样即使页面滚动,提示框也能正确显示
this.tooltipX = event.clientX + 10;
this.tooltipY = event.clientY + 10;
// 可选:检测边界,确保提示框不会超出视口
const tooltipWidth = 100; // 估算提示框宽度
const tooltipHeight = 30; // 估算提示框高度
// 如果右侧超出视口,调整到左侧
if (this.tooltipX + tooltipWidth > window.innerWidth) {
this.tooltipX = event.clientX - tooltipWidth - 10;
}
// 如果底部超出视口,调整到上方
if (this.tooltipY + tooltipHeight > window.innerHeight) {
this.tooltipY = event.clientY - tooltipHeight - 10;
}
}
}
});
</script>
</body>
</html>