Create public/index.html

#1
Files changed (1) hide show
  1. public/index.html +576 -0
public/index.html ADDED
@@ -0,0 +1,576 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Super City β€” Character Editor</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg: #0a0c10;
11
+ --panel: #0f1318;
12
+ --border: #1e2530;
13
+ --border-bright: #2e3d50;
14
+ --accent: #00c8ff;
15
+ --accent2: #ff6b35;
16
+ --accent-dim: rgba(0,200,255,0.12);
17
+ --accent2-dim: rgba(255,107,53,0.12);
18
+ --text: #c8d8e8;
19
+ --text-dim: #5a7080;
20
+ --text-bright: #eaf4ff;
21
+ --civilian: #3ddc97;
22
+ --super: #f0a500;
23
+ --civilian-dim: rgba(61,220,151,0.1);
24
+ --super-dim: rgba(240,165,0,0.1);
25
+ --danger: #ff4455;
26
+ }
27
+ * { box-sizing: border-box; margin: 0; padding: 0; }
28
+ body {
29
+ background: var(--bg);
30
+ color: var(--text);
31
+ font-family: 'Rajdhani', sans-serif;
32
+ font-size: 15px;
33
+ min-height: 100vh;
34
+ }
35
+ body::before {
36
+ content: '';
37
+ position: fixed;
38
+ inset: 0;
39
+ background-image:
40
+ linear-gradient(rgba(0,200,255,0.03) 1px, transparent 1px),
41
+ linear-gradient(90deg, rgba(0,200,255,0.03) 1px, transparent 1px);
42
+ background-size: 40px 40px;
43
+ pointer-events: none;
44
+ z-index: 0;
45
+ }
46
+ .app { position: relative; z-index: 1; max-width: 1100px; margin: 0 auto; padding: 24px 20px 60px; }
47
+
48
+ header { display: flex; align-items: center; gap: 16px; margin-bottom: 32px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
49
+ .logo-icon { width: 44px; height: 44px; background: var(--accent-dim); border: 1px solid var(--accent); display: flex; align-items: center; justify-content: center; font-size: 22px; clip-path: polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%); }
50
+ h1 { font-size: 28px; font-weight: 700; letter-spacing: 3px; text-transform: uppercase; color: var(--text-bright); }
51
+ h1 span { color: var(--accent); }
52
+ .header-sub { font-family: 'Share Tech Mono', monospace; font-size: 11px; color: var(--text-dim); letter-spacing: 2px; margin-top: 2px; }
53
+
54
+ /* STEPS */
55
+ .steps { display: flex; gap: 8px; margin-bottom: 24px; }
56
+ .step { flex: 1; padding: 8px 12px; background: var(--panel); border: 1px solid var(--border); border-radius: 4px; font-size: 11px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; color: var(--text-dim); display: flex; align-items: center; gap: 8px; transition: all 0.3s; }
57
+ .step.active { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); }
58
+ .step.done { border-color: var(--civilian); color: var(--civilian); }
59
+ .step-num { font-family: 'Share Tech Mono', monospace; font-size: 14px; }
60
+
61
+ /* UPLOAD */
62
+ .upload-zone { border: 2px dashed var(--border-bright); border-radius: 4px; padding: 50px 20px; text-align: center; cursor: pointer; transition: all 0.2s; background: var(--panel); position: relative; overflow: hidden; }
63
+ .upload-zone::before { content: ''; position: absolute; inset: 0; background: linear-gradient(135deg, var(--accent-dim) 0%, transparent 60%); opacity: 0; transition: opacity 0.3s; }
64
+ .upload-zone:hover, .upload-zone.drag-over { border-color: var(--accent); }
65
+ .upload-zone:hover::before, .upload-zone.drag-over::before { opacity: 1; }
66
+ .upload-icon { font-size: 40px; margin-bottom: 12px; }
67
+ .upload-title { font-size: 20px; font-weight: 700; letter-spacing: 2px; color: var(--text-bright); text-transform: uppercase; }
68
+ .upload-sub { font-family: 'Share Tech Mono', monospace; font-size: 11px; color: var(--text-dim); margin-top: 8px; letter-spacing: 1px; }
69
+ #apkInput { display: none; }
70
+
71
+ /* PROGRESS */
72
+ .progress-wrap { margin-top: 16px; display: none; }
73
+ .progress-label { font-family: 'Share Tech Mono', monospace; font-size: 12px; color: var(--accent); margin-bottom: 6px; letter-spacing: 1px; }
74
+ .progress-bar-outer { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
75
+ .progress-bar-inner { height: 100%; background: var(--accent); border-radius: 2px; width: 0%; transition: width 0.3s; }
76
+
77
+ /* EDITOR LAYOUT */
78
+ .editor-layout { display: grid; grid-template-columns: 280px 1fr; gap: 20px; margin-top: 24px; }
79
+ .sidebar { display: flex; flex-direction: column; gap: 12px; }
80
+ .panel { background: var(--panel); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
81
+ .panel-header { padding: 10px 14px; background: rgba(0,200,255,0.06); border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 700; letter-spacing: 3px; text-transform: uppercase; color: var(--accent); display: flex; align-items: center; gap: 8px; }
82
+ .panel-header .count { margin-left: auto; font-family: 'Share Tech Mono', monospace; font-size: 12px; color: var(--text-dim); font-weight: 400; letter-spacing: 0; }
83
+ .search-wrap { padding: 10px; border-bottom: 1px solid var(--border); }
84
+ .search-input { width: 100%; background: var(--bg); border: 1px solid var(--border-bright); color: var(--text-bright); font-family: 'Rajdhani', sans-serif; font-size: 14px; padding: 8px 10px 8px 30px; border-radius: 3px; outline: none; transition: border-color 0.2s; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='%235a7080' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.099zm-5.242 1.656a5.5 5.5 0 1 1 0-11 5.5 5.5 0 0 1 0 11z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: 10px center; }
85
+ .search-input:focus { border-color: var(--accent); }
86
+ .search-input::placeholder { color: var(--text-dim); }
87
+ .char-list { max-height: 520px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--border-bright) transparent; }
88
+ .char-item { padding: 10px 14px; border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.15s; }
89
+ .char-item:hover { background: rgba(0,200,255,0.05); }
90
+ .char-item.active { background: var(--accent-dim); border-left: 3px solid var(--accent); }
91
+ .char-name { font-weight: 700; font-size: 14px; color: var(--text-bright); }
92
+ .char-alias { font-family: 'Share Tech Mono', monospace; font-size: 11px; color: var(--text-dim); }
93
+
94
+ /* MAIN PANEL */
95
+ .main-panel { display: flex; flex-direction: column; gap: 16px; }
96
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 300px; color: var(--text-dim); gap: 12px; }
97
+ .empty-state-icon { font-size: 48px; opacity: 0.3; }
98
+ .empty-state-text { font-size: 16px; letter-spacing: 2px; text-transform: uppercase; }
99
+ .char-header-panel { padding: 20px; background: var(--panel); border: 1px solid var(--border); border-radius: 4px; display: flex; align-items: center; gap: 20px; }
100
+ .char-avatar { width: 60px; height: 60px; background: var(--accent-dim); border: 2px solid var(--accent); border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 26px; flex-shrink: 0; clip-path: polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%); }
101
+ .char-info { flex: 1; }
102
+ .char-display-name { font-size: 26px; font-weight: 700; color: var(--text-bright); }
103
+ .char-display-alias { font-family: 'Share Tech Mono', monospace; font-size: 13px; color: var(--accent); letter-spacing: 2px; margin-top: 2px; }
104
+ .char-index { font-family: 'Share Tech Mono', monospace; font-size: 11px; color: var(--text-dim); margin-top: 4px; }
105
+
106
+ .stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
107
+ .stat-group { background: var(--panel); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
108
+ .stat-group-header { padding: 12px 16px; font-size: 12px; font-weight: 700; letter-spacing: 3px; text-transform: uppercase; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid var(--border); }
109
+ .stat-group.civilian .stat-group-header { background: var(--civilian-dim); color: var(--civilian); border-bottom-color: rgba(61,220,151,0.2); }
110
+ .stat-group.super .stat-group-header { background: var(--super-dim); color: var(--super); border-bottom-color: rgba(240,165,0,0.2); }
111
+ .stat-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
112
+ .civilian .stat-dot { background: var(--civilian); box-shadow: 0 0 6px var(--civilian); }
113
+ .super .stat-dot { background: var(--super); box-shadow: 0 0 6px var(--super); }
114
+ .stat-fields { padding: 16px; display: flex; flex-direction: column; gap: 14px; }
115
+ .stat-field { display: flex; flex-direction: column; gap: 5px; }
116
+ .stat-label { font-size: 11px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; color: var(--text-dim); display: flex; justify-content: space-between; }
117
+ .stat-bar-wrap { height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; margin-bottom: 2px; }
118
+ .stat-bar { height: 100%; border-radius: 2px; transition: width 0.3s; }
119
+ .civilian .stat-bar { background: var(--civilian); }
120
+ .super .stat-bar { background: var(--super); }
121
+ .stat-input-wrap { position: relative; }
122
+ .stat-input { width: 100%; background: var(--bg); border: 1px solid var(--border-bright); color: var(--text-bright); font-family: 'Share Tech Mono', monospace; font-size: 20px; padding: 8px 48px 8px 12px; border-radius: 3px; outline: none; transition: border-color 0.2s, box-shadow 0.2s; -moz-appearance: textfield; }
123
+ .stat-input::-webkit-outer-spin-button, .stat-input::-webkit-inner-spin-button { -webkit-appearance: none; }
124
+ .civilian .stat-input:focus { border-color: var(--civilian); box-shadow: 0 0 0 2px rgba(61,220,151,0.15); }
125
+ .super .stat-input:focus { border-color: var(--super); box-shadow: 0 0 0 2px rgba(240,165,0,0.15); }
126
+ .stat-input.invalid { border-color: var(--danger) !important; box-shadow: 0 0 0 2px rgba(255,68,85,0.15) !important; }
127
+ .stat-spinner { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 2px; }
128
+ .spin-btn { width: 18px; height: 18px; background: var(--border); border: none; cursor: pointer; color: var(--text-dim); font-size: 10px; border-radius: 2px; display: flex; align-items: center; justify-content: center; transition: background 0.15s; }
129
+ .spin-btn:hover { background: var(--border-bright); color: var(--text-bright); }
130
+
131
+ /* ACTIONS */
132
+ .actions-bar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
133
+ .btn { padding: 10px 22px; border: none; cursor: pointer; font-family: 'Rajdhani', sans-serif; font-size: 14px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; border-radius: 3px; transition: all 0.2s; display: flex; align-items: center; gap: 6px; }
134
+ .btn-primary { background: var(--accent); color: #000; }
135
+ .btn-primary:hover { background: #30dfff; box-shadow: 0 0 20px rgba(0,200,255,0.4); }
136
+ .btn-secondary { background: transparent; color: var(--text); border: 1px solid var(--border-bright); }
137
+ .btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
138
+ .btn-export { background: var(--accent2-dim); color: var(--accent2); border: 1px solid rgba(255,107,53,0.3); margin-left: auto; }
139
+ .btn-export:hover { background: rgba(255,107,53,0.2); box-shadow: 0 0 16px rgba(255,107,53,0.3); }
140
+ .btn-export:disabled { opacity: 0.4; cursor: not-allowed; box-shadow: none; }
141
+ .unsaved-badge { font-family: 'Share Tech Mono', monospace; font-size: 11px; background: rgba(255,107,53,0.15); color: var(--accent2); border: 1px solid rgba(255,107,53,0.3); padding: 3px 8px; border-radius: 2px; letter-spacing: 1px; }
142
+
143
+ /* FILE INFO BAR */
144
+ .file-info { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: rgba(61,220,151,0.06); border: 1px solid rgba(61,220,151,0.2); border-radius: 4px; font-family: 'Share Tech Mono', monospace; font-size: 12px; color: var(--civilian); margin-top: 12px; }
145
+ .file-info-name { color: var(--text-bright); font-weight: 600; }
146
+
147
+ /* TOOL STATUS */
148
+ .tool-status { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
149
+ .tool-badge { font-family: 'Share Tech Mono', monospace; font-size: 11px; padding: 4px 10px; border-radius: 3px; letter-spacing: 1px; }
150
+ .tool-badge.ok { background: rgba(61,220,151,0.1); color: var(--civilian); border: 1px solid rgba(61,220,151,0.3); }
151
+ .tool-badge.missing { background: rgba(255,68,85,0.1); color: var(--danger); border: 1px solid rgba(255,68,85,0.3); }
152
+
153
+ /* TOAST */
154
+ .toast { position: fixed; bottom: 30px; right: 30px; background: var(--panel); border: 1px solid var(--accent); color: var(--text-bright); padding: 12px 20px; border-radius: 4px; font-size: 14px; font-weight: 600; letter-spacing: 1px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); transform: translateY(80px); opacity: 0; transition: all 0.3s cubic-bezier(0.34,1.56,0.64,1); z-index: 1000; }
155
+ .toast.show { transform: translateY(0); opacity: 1; }
156
+ .toast.error { border-color: var(--danger); }
157
+
158
+ ::-webkit-scrollbar { width: 5px; }
159
+ ::-webkit-scrollbar-track { background: transparent; }
160
+ ::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 3px; }
161
+ @media (max-width: 700px) { .editor-layout { grid-template-columns: 1fr; } .stats-grid { grid-template-columns: 1fr; } .char-list { max-height: 220px; } }
162
+ </style>
163
+ </head>
164
+ <body>
165
+ <div class="app">
166
+
167
+ <header>
168
+ <div class="logo-icon">⚑</div>
169
+ <div>
170
+ <h1>SUPER CITY <span>EDITOR</span></h1>
171
+ <div class="header-sub">MDICKIE CHARACTER STATS EDITOR // APK PATCHER</div>
172
+ </div>
173
+ </header>
174
+
175
+ <!-- Steps -->
176
+ <div class="steps">
177
+ <div class="step active" id="step1"><span class="step-num">01</span> Upload APK</div>
178
+ <div class="step" id="step2"><span class="step-num">02</span> Edit Characters</div>
179
+ <div class="step" id="step3"><span class="step-num">03</span> Export Patched APK</div>
180
+ </div>
181
+
182
+ <!-- Tool status -->
183
+ <div class="tool-status" id="toolStatus"></div>
184
+
185
+ <!-- Upload -->
186
+ <div class="upload-zone" id="uploadZone">
187
+ <div class="upload-icon">πŸ“¦</div>
188
+ <div class="upload-title">Drop SuperCity.apk here</div>
189
+ <div class="upload-sub">or click to browse Β· .apk files only Β· max 200 MB</div>
190
+ <input type="file" id="apkInput" accept=".apk">
191
+ </div>
192
+ <div class="progress-wrap" id="progressWrap">
193
+ <div class="progress-label" id="progressLabel">Uploading…</div>
194
+ <div class="progress-bar-outer"><div class="progress-bar-inner" id="progressBar"></div></div>
195
+ </div>
196
+ <div id="fileInfoBar" style="display:none;"></div>
197
+
198
+ <!-- Editor -->
199
+ <div class="editor-layout" id="editorLayout" style="display:none;">
200
+ <div class="sidebar">
201
+ <div class="panel">
202
+ <div class="panel-header">Characters <span class="count" id="charCount">0</span></div>
203
+ <div class="search-wrap">
204
+ <input class="search-input" type="text" id="searchInput" placeholder="Search name or alias…">
205
+ </div>
206
+ <div class="char-list" id="charList"></div>
207
+ </div>
208
+ </div>
209
+ <div class="main-panel" id="mainPanel">
210
+ <div class="empty-state" id="emptyState">
211
+ <div class="empty-state-icon">🎯</div>
212
+ <div class="empty-state-text">Select a character</div>
213
+ </div>
214
+ <div id="editorArea" style="display:none; flex-direction:column; gap:16px;">
215
+ <div class="char-header-panel" id="charHeaderPanel"></div>
216
+ <div class="stats-grid">
217
+ <div class="stat-group civilian">
218
+ <div class="stat-group-header"><div class="stat-dot"></div>Civilian Mode</div>
219
+ <div class="stat-fields">
220
+ <div class="stat-field">
221
+ <div class="stat-label">Power <span id="civPowerVal">β€”</span></div>
222
+ <div class="stat-bar-wrap"><div class="stat-bar" id="civPowerBar" style="width:0%"></div></div>
223
+ <div class="stat-input-wrap">
224
+ <input class="stat-input" type="number" id="civPower" min="0" max="99999">
225
+ <div class="stat-spinner"><button class="spin-btn" onclick="stepStat('civPower',1)">β–²</button><button class="spin-btn" onclick="stepStat('civPower',-1)">β–Ό</button></div>
226
+ </div>
227
+ </div>
228
+ <div class="stat-field">
229
+ <div class="stat-label">Defence <span id="civDefVal">β€”</span></div>
230
+ <div class="stat-bar-wrap"><div class="stat-bar" id="civDefBar" style="width:0%"></div></div>
231
+ <div class="stat-input-wrap">
232
+ <input class="stat-input" type="number" id="civDef" min="0" max="99999">
233
+ <div class="stat-spinner"><button class="spin-btn" onclick="stepStat('civDef',1)">β–²</button><button class="spin-btn" onclick="stepStat('civDef',-1)">β–Ό</button></div>
234
+ </div>
235
+ </div>
236
+ <div class="stat-field">
237
+ <div class="stat-label">Speed <span id="civSpdVal">β€”</span></div>
238
+ <div class="stat-bar-wrap"><div class="stat-bar" id="civSpdBar" style="width:0%"></div></div>
239
+ <div class="stat-input-wrap">
240
+ <input class="stat-input" type="number" id="civSpd" min="0" max="99999">
241
+ <div class="stat-spinner"><button class="spin-btn" onclick="stepStat('civSpd',1)">β–²</button><button class="spin-btn" onclick="stepStat('civSpd',-1)">β–Ό</button></div>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ <div class="stat-group super">
247
+ <div class="stat-group-header"><div class="stat-dot"></div>Super Mode</div>
248
+ <div class="stat-fields">
249
+ <div class="stat-field">
250
+ <div class="stat-label">Power <span id="supPowerVal">β€”</span></div>
251
+ <div class="stat-bar-wrap"><div class="stat-bar" id="supPowerBar" style="width:0%"></div></div>
252
+ <div class="stat-input-wrap">
253
+ <input class="stat-input" type="number" id="supPower" min="0" max="99999">
254
+ <div class="stat-spinner"><button class="spin-btn" onclick="stepStat('supPower',1)">β–²</button><button class="spin-btn" onclick="stepStat('supPower',-1)">β–Ό</button></div>
255
+ </div>
256
+ </div>
257
+ <div class="stat-field">
258
+ <div class="stat-label">Defence <span id="supDefVal">β€”</span></div>
259
+ <div class="stat-bar-wrap"><div class="stat-bar" id="supDefBar" style="width:0%"></div></div>
260
+ <div class="stat-input-wrap">
261
+ <input class="stat-input" type="number" id="supDef" min="0" max="99999">
262
+ <div class="stat-spinner"><button class="spin-btn" onclick="stepStat('supDef',1)">β–²</button><button class="spin-btn" onclick="stepStat('supDef',-1)">β–Ό</button></div>
263
+ </div>
264
+ </div>
265
+ <div class="stat-field">
266
+ <div class="stat-label">Speed <span id="supSpdVal">β€”</span></div>
267
+ <div class="stat-bar-wrap"><div class="stat-bar" id="supSpdBar" style="width:0%"></div></div>
268
+ <div class="stat-input-wrap">
269
+ <input class="stat-input" type="number" id="supSpd" min="0" max="99999">
270
+ <div class="stat-spinner"><button class="spin-btn" onclick="stepStat('supSpd',1)">β–²</button><button class="spin-btn" onclick="stepStat('supSpd',-1)">β–Ό</button></div>
271
+ </div>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ <div class="actions-bar">
277
+ <button class="btn btn-primary" onclick="saveCharacter()">βœ” Apply Changes</button>
278
+ <button class="btn btn-secondary" onclick="resetCharacter()">β†Ί Reset</button>
279
+ <span class="unsaved-badge" id="unsavedBadge" style="display:none;">● UNSAVED</span>
280
+ <button class="btn btn-export" id="exportBtn" onclick="exportAPK()">⬇ Export Patched APK</button>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </div>
286
+
287
+ <div class="toast" id="toast"></div>
288
+
289
+ <script>
290
+ // ── STATE ────────────────────────────────────────────────────���─────
291
+ let characters = [];
292
+ let activeIndex = -1;
293
+ let sessionToken = null;
294
+ let originalRaw = '';
295
+
296
+ const STAT_MAP = {
297
+ civPower: 9, civDef: 10, civSpd: 11,
298
+ supPower: 13, supDef: 14, supSpd: 15,
299
+ };
300
+
301
+ // ── TOOL STATUS ────────────────────────────────────────────────────
302
+ fetch('/tools').then(r => r.json()).then(tools => {
303
+ const bar = document.getElementById('toolStatus');
304
+ const items = [
305
+ ['keytool', 'keytool'],
306
+ ['jarsigner', 'jarsigner'],
307
+ ['zipalign', 'zipalign'],
308
+ ['apksigner', 'apksigner'],
309
+ ];
310
+ bar.innerHTML = items.map(([k, label]) =>
311
+ `<span class="tool-badge ${tools[k] ? 'ok' : 'missing'}">${tools[k] ? 'βœ”' : '✘'} ${label}</span>`
312
+ ).join('');
313
+
314
+ const canSign = tools.apksigner || tools.jarsigner;
315
+ if (!canSign) {
316
+ showToast('⚠ No signing tool found β€” install JDK or Android SDK build-tools', true);
317
+ }
318
+ }).catch(() => {});
319
+
320
+ // ── UPLOAD ─────────────────────────────────────────────────────────
321
+ const uploadZone = document.getElementById('uploadZone');
322
+ const apkInput = document.getElementById('apkInput');
323
+
324
+ uploadZone.addEventListener('click', () => apkInput.click());
325
+ uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('drag-over'); });
326
+ uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over'));
327
+ uploadZone.addEventListener('drop', e => { e.preventDefault(); uploadZone.classList.remove('drag-over'); if (e.dataTransfer.files[0]) uploadAPK(e.dataTransfer.files[0]); });
328
+ apkInput.addEventListener('change', () => { if (apkInput.files[0]) uploadAPK(apkInput.files[0]); });
329
+
330
+ function uploadAPK(file) {
331
+ if (!file.name.endsWith('.apk')) { showToast('Only .apk files', true); return; }
332
+
333
+ const wrap = document.getElementById('progressWrap');
334
+ const bar = document.getElementById('progressBar');
335
+ const label = document.getElementById('progressLabel');
336
+ wrap.style.display = 'block';
337
+ bar.style.width = '0%';
338
+ label.textContent = 'Uploading…';
339
+
340
+ const form = new FormData();
341
+ form.append('apk', file);
342
+
343
+ const xhr = new XMLHttpRequest();
344
+ xhr.open('POST', '/upload');
345
+
346
+ xhr.upload.onprogress = e => {
347
+ if (e.lengthComputable) {
348
+ const pct = Math.round(e.loaded / e.total * 80);
349
+ bar.style.width = pct + '%';
350
+ label.textContent = `Uploading… ${pct}%`;
351
+ }
352
+ };
353
+
354
+ xhr.onload = () => {
355
+ bar.style.width = '100%';
356
+ label.textContent = 'Parsing characters…';
357
+ if (xhr.status === 200) {
358
+ const data = JSON.parse(xhr.responseText);
359
+ sessionToken = data.token;
360
+ originalRaw = data.characters;
361
+ parseCharacters(data.characters);
362
+ wrap.style.display = 'none';
363
+ document.getElementById('fileInfoBar').style.display = 'flex';
364
+ document.getElementById('fileInfoBar').innerHTML = `
365
+ <div class="file-info">πŸ“¦ <span class="file-info-name">${file.name}</span>
366
+ &nbsp;Β·&nbsp; ${characters.length} characters loaded &nbsp;Β·&nbsp; session: ${sessionToken.split('_')[0]}</div>`;
367
+ document.getElementById('editorLayout').style.display = 'grid';
368
+ setStep(2);
369
+ showToast(`βœ” Loaded ${characters.length} characters`);
370
+ } else {
371
+ wrap.style.display = 'none';
372
+ const err = JSON.parse(xhr.responseText);
373
+ showToast('Error: ' + err.error, true);
374
+ }
375
+ };
376
+
377
+ xhr.onerror = () => { wrap.style.display = 'none'; showToast('Upload failed', true); };
378
+ xhr.send(form);
379
+ }
380
+
381
+ // ── PARSE ──────────────────────────────────────────────────────────
382
+ function parseCharacters(raw) {
383
+ characters = [];
384
+ const allFields = raw.split('|');
385
+
386
+ function isNameToken(s) {
387
+ s = s.trim();
388
+ return s.length > 1 && /[A-Za-z]/.test(s) && !/^-?[\d.]+$/.test(s);
389
+ }
390
+ function isNumeric(s) { return /^-?[\d.]+$/.test(s.trim()); }
391
+
392
+ const boundaries = [];
393
+ for (let i = 1; i < allFields.length - 1; i++) {
394
+ if (isNameToken(allFields[i]) && isNumeric(allFields[i + 1]) && isNameToken(allFields[i - 1])) {
395
+ boundaries.push(i);
396
+ }
397
+ }
398
+
399
+ for (let b = 0; b < boundaries.length; b++) {
400
+ const start = boundaries[b];
401
+ const end = b + 1 < boundaries.length ? boundaries[b + 1] - 1 : allFields.length;
402
+ const fields = allFields.slice(start, end);
403
+ characters.push({
404
+ index: b,
405
+ name: allFields[start - 1].trim(),
406
+ alias: fields[0].trim(),
407
+ fields: fields,
408
+ original: [...fields],
409
+ startIdx: start,
410
+ endIdx: end,
411
+ });
412
+ }
413
+
414
+ document.getElementById('charCount').textContent = characters.length;
415
+ renderList(characters);
416
+ }
417
+
418
+ // ── LIST ──────────────────────────────────���────────────────────────
419
+ function renderList(list) {
420
+ const el = document.getElementById('charList');
421
+ if (!list.length) { el.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim);font-size:13px;">No results</div>'; return; }
422
+ el.innerHTML = list.map(ch => `
423
+ <div class="char-item ${ch.index === activeIndex ? 'active' : ''}" onclick="selectChar(${ch.index})">
424
+ <div class="char-name">${esc(ch.name)}</div>
425
+ <div class="char-alias">${esc(ch.alias)}</div>
426
+ </div>`).join('');
427
+ }
428
+
429
+ document.getElementById('searchInput').addEventListener('input', function () {
430
+ const q = this.value.toLowerCase();
431
+ renderList(q ? characters.filter(c => c.name.toLowerCase().includes(q) || c.alias.toLowerCase().includes(q)) : characters);
432
+ });
433
+
434
+ // ── SELECT ─────────────────────────────────────────────────────────
435
+ function selectChar(idx) {
436
+ activeIndex = idx;
437
+ const ch = characters[idx];
438
+ document.getElementById('emptyState').style.display = 'none';
439
+ const ea = document.getElementById('editorArea');
440
+ ea.style.display = 'flex';
441
+
442
+ document.getElementById('charHeaderPanel').innerHTML = `
443
+ <div class="char-avatar">⚑</div>
444
+ <div class="char-info">
445
+ <div class="char-display-name">${esc(ch.name)}</div>
446
+ <div class="char-display-alias">${esc(ch.alias)}</div>
447
+ <div class="char-index">CHARACTER #${idx + 1}</div>
448
+ </div>`;
449
+
450
+ Object.keys(STAT_MAP).forEach(key => {
451
+ const v = parseInt(ch.fields[STAT_MAP[key]], 10) || 0;
452
+ document.getElementById(key).value = v;
453
+ document.getElementById(key).oninput = () => onStatChange(key);
454
+ updateBar(key, v);
455
+ });
456
+ document.getElementById('unsavedBadge').style.display = 'none';
457
+
458
+ const q = document.getElementById('searchInput').value.toLowerCase();
459
+ renderList(q ? characters.filter(c => c.name.toLowerCase().includes(q) || c.alias.toLowerCase().includes(q)) : characters);
460
+ }
461
+
462
+ function onStatChange(key) {
463
+ const el = document.getElementById(key);
464
+ const v = parseInt(el.value, 10);
465
+ const ok = !isNaN(v) && v >= 0 && v <= 99999;
466
+ el.classList.toggle('invalid', !ok);
467
+ if (ok) updateBar(key, v);
468
+ document.getElementById('unsavedBadge').style.display = '';
469
+ }
470
+
471
+ function updateBar(key, val) {
472
+ const bar = document.getElementById(key + 'Bar');
473
+ const lbl = document.getElementById(key + 'Val');
474
+ if (bar) bar.style.width = Math.min(100, (val / 99999) * 100) + '%';
475
+ if (lbl) lbl.textContent = val;
476
+ }
477
+
478
+ function stepStat(key, delta) {
479
+ const el = document.getElementById(key);
480
+ let v = Math.max(0, Math.min(99999, (parseInt(el.value, 10) || 0) + delta));
481
+ el.value = v;
482
+ el.classList.remove('invalid');
483
+ updateBar(key, v);
484
+ document.getElementById('unsavedBadge').style.display = '';
485
+ }
486
+
487
+ function saveCharacter() {
488
+ if (activeIndex < 0) return;
489
+ const ch = characters[activeIndex];
490
+ let valid = true;
491
+ Object.keys(STAT_MAP).forEach(key => {
492
+ const v = parseInt(document.getElementById(key).value, 10);
493
+ if (isNaN(v) || v < 0 || v > 99999) { valid = false; document.getElementById(key).classList.add('invalid'); return; }
494
+ ch.fields[STAT_MAP[key]] = String(v);
495
+ updateBar(key, v);
496
+ });
497
+ if (!valid) { showToast('Fix invalid values first', true); return; }
498
+ document.getElementById('unsavedBadge').style.display = 'none';
499
+ showToast(`βœ” ${ch.name} saved`);
500
+ setStep(3);
501
+ }
502
+
503
+ function resetCharacter() {
504
+ if (activeIndex < 0) return;
505
+ characters[activeIndex].fields = [...characters[activeIndex].original];
506
+ selectChar(activeIndex);
507
+ showToast('β†Ί Reset to original');
508
+ }
509
+
510
+ // ── EXPORT ─────────────────────────────────────────────────────────
511
+ function exportAPK() {
512
+ if (!sessionToken) { showToast('No APK session β€” re-upload', true); return; }
513
+
514
+ // Rebuild raw text from all character fields
515
+ const allFields = originalRaw.split('|');
516
+ characters.forEach(ch => {
517
+ ch.fields.forEach((val, j) => { allFields[ch.startIdx + j] = val; });
518
+ });
519
+ const rebuiltChars = allFields.join('|');
520
+
521
+ const btn = document.getElementById('exportBtn');
522
+ btn.disabled = true;
523
+ btn.textContent = '⏳ Building APK…';
524
+ showToast('Patching & signing APK, please wait…');
525
+
526
+ fetch('/patch', {
527
+ method: 'POST',
528
+ headers: { 'Content-Type': 'application/json' },
529
+ body: JSON.stringify({ token: sessionToken, characters: rebuiltChars }),
530
+ })
531
+ .then(res => {
532
+ if (!res.ok) return res.json().then(e => { throw new Error(e.error); });
533
+ return res.blob();
534
+ })
535
+ .then(blob => {
536
+ const url = URL.createObjectURL(blob);
537
+ const a = document.createElement('a');
538
+ a.href = url;
539
+ a.download = 'SuperCity_modded.apk';
540
+ a.click();
541
+ URL.revokeObjectURL(url);
542
+ btn.disabled = false;
543
+ btn.textContent = '⬇ Export Patched APK';
544
+ showToast('βœ” Patched APK downloaded β€” install with "Unknown sources" enabled');
545
+ setStep(3);
546
+ })
547
+ .catch(err => {
548
+ btn.disabled = false;
549
+ btn.textContent = '⬇ Export Patched APK';
550
+ showToast('Error: ' + err.message, true);
551
+ });
552
+ }
553
+
554
+ // ── HELPERS ────────────────────────────────────────────────────────
555
+ function setStep(n) {
556
+ [1,2,3].forEach(i => {
557
+ const el = document.getElementById('step' + i);
558
+ el.className = 'step' + (i < n ? ' done' : i === n ? ' active' : '');
559
+ });
560
+ }
561
+
562
+ let toastTimer;
563
+ function showToast(msg, isError = false) {
564
+ const t = document.getElementById('toast');
565
+ t.textContent = msg;
566
+ t.className = 'toast show' + (isError ? ' error' : '');
567
+ clearTimeout(toastTimer);
568
+ toastTimer = setTimeout(() => t.className = 'toast', 3200);
569
+ }
570
+
571
+ function esc(s) {
572
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
573
+ }
574
+ </script>
575
+ </body>
576
+ </html>