1import { log } from "../../src/helpers.js"; 2 3/** 4 * PlayerDataManager handles persistent player data 5 * - Generates unique player IDs 6 * - Manages localStorage for player data 7 * - Loads/saves player data to WordPress database 8 */ 9export class PlayerDataManager { 10 constructor() { 11 this.STORAGE_KEY = 'spielfieber_player_data'; 12 this.API_ENDPOINT = null; 13 14 // Initialize API endpoint 15 this._initializeApiEndpoint(); 16 } 17 18 /** 19 * Initialize API endpoint URL 20 */ 21 _initializeApiEndpoint() { 22 let baseUrl = spielfieber_vars.siteurl; 23 24 // Handle different URL structures 25 if (baseUrl.endsWith('/wp')) { 26 baseUrl = baseUrl.slice(0, -3); // Remove trailing /wp 27 } 28 29 this.API_ENDPOINT = `${baseUrl}/wp-json/spielfieber/v1/player-data`; 30 } 31 32 /** 33 * Generate a unique player ID 34 */ 35 _generatePlayerId() { 36 // Create a UUID-like string for the player ID 37 return 'player_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); 38 } 39 40 /** 41 * Generate a unique 6-character user code (format: ABC123) 42 */ 43 _generateUserCode() { 44 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 45 const nums = '0123456789'; 46 let code = ''; 47 48 // 3 letters 49 for (let i = 0; i < 3; i++) { 50 code += chars.charAt(Math.floor(Math.random() * chars.length)); 51 } 52 53 // 3 numbers 54 for (let i = 0; i < 3; i++) { 55 code += nums.charAt(Math.floor(Math.random() * nums.length)); 56 } 57 58 return code; 59 } 60 61 /** 62 * Get or create player data from localStorage 63 */ 64 getPlayerData() { 65 try { 66 const storedData = localStorage.getItem(this.STORAGE_KEY); 67 68 if (storedData) { 69 const playerData = this._migrateToTotalXp(JSON.parse(storedData)); 70 console.log("====== DEBUG getPlayerData ======"); 71 console.log("Loaded from localStorage:", playerData); 72 console.log("lastCharacterBuilderShown:", playerData.lastCharacterBuilderShown); 73 console.log("================================="); 74 log("PlayerDataManager: Loaded existing player data:", playerData); 75 return playerData; 76 } 77 } catch (error) { 78 log("PlayerDataManager: Error reading from localStorage:", error); 79 } 80 81 // Create new player data if none exists 82 const newPlayerData = this._createNewPlayerData(); 83 this.savePlayerDataToStorage(newPlayerData); 84 console.log("====== DEBUG getPlayerData ======"); 85 console.log("Created NEW player data:", newPlayerData); 86 console.log("lastCharacterBuilderShown:", newPlayerData.lastCharacterBuilderShown); 87 console.log("================================="); 88 log("PlayerDataManager: Created new player data:", newPlayerData); 89 return newPlayerData; 90 } 91 92 /** 93 * One-time localStorage migration to lifetime-total XP. 94 * 95 * Stored XP used to be the surplus within the current level, so a level 3 player with 96 * 150 held a real total of 450. Without this the stale value would be written straight 97 * back over the migrated server record on the next save, undoing it. 98 * Mirrors spielfieber_migrate_xp_to_totals() in inc/multiplayer-api.php. 99 */ 100 _migrateToTotalXp(playerData) { 101 if (!playerData || playerData.xpModel === 'total') return playerData; 102 103 const level = playerData.level || 1; 104 const oldXp = playerData.xp || 0; 105 106 playerData.xp = xpForLevelStart(level) + oldXp; 107 playerData.xpModel = 'total'; 108 109 log(`PlayerDataManager: Migrated stored XP to lifetime total (level ${level}, ${oldXp} -> ${playerData.xp})`); 110 this.savePlayerDataToStorage(playerData); 111 112 return playerData; 113 } 114 115 /** 116 * Create new player data with unique ID and user code 117 */ 118 _createNewPlayerData() { 119 return { 120 playerId: this._generatePlayerId(), 121 userCode: this._generateUserCode(), 122 name: this._generatePlayerName(), 123 xp: 0, 124 level: 1, 125 xpModel: 'total', 126 character: this._generateRandomCharacter(), 127 createdAt: Date.now(), 128 lastPlayed: Date.now(), 129 lastCharacterBuilderShown: 0 // Never shown yet 130 }; 131 } 132 133 /** 134 * Save player data to localStorage 135 */ 136 savePlayerDataToStorage(playerData) { 137 try { 138 playerData.lastPlayed = Date.now(); 139 localStorage.setItem(this.STORAGE_KEY, JSON.stringify(playerData)); 140 log("PlayerDataManager: Saved player data to localStorage"); 141 } catch (error) { 142 log("PlayerDataManager: Error saving to localStorage:", error); 143 } 144 } 145 146 /** 147 * Load player data from server by player ID 148 */ 149 async loadPlayerDataFromServer(playerId) { 150 try { 151 const response = await fetch(`${this.API_ENDPOINT}/${playerId}`, { 152 method: 'GET', 153 headers: { 154 'Content-Type': 'application/json', 155 } 156 }); 157 158 if (!response.ok) { 159 throw new Error(`HTTP error! status: ${response.status}`); 160 } 161 162 const data = await response.json(); 163 164 if (data.success && data.playerData) { 165 log("PlayerDataManager: Loaded player data from server:", data.playerData); 166 return data.playerData; 167 } 168 169 return null; 170 } catch (error) { 171 log("PlayerDataManager: Error loading player data from server:", error); 172 return null; 173 } 174 } 175 176 /** 177 * Load player data from server by user code 178 */ 179 async loadPlayerDataByUserCode(userCode) { 180 try { 181 let baseUrl = spielfieber_vars.siteurl; 182 if (baseUrl.endsWith('/wp')) { 183 baseUrl = baseUrl.slice(0, -3); 184 } 185 186 const endpoint = `${baseUrl}/wp-json/spielfieber/v1/player-data-by-code/${userCode}`; 187 188 const response = await fetch(endpoint, { 189 method: 'GET', 190 headers: { 191 'Content-Type': 'application/json', 192 } 193 }); 194 195 if (!response.ok) { 196 throw new Error(`HTTP error! status: ${response.status}`); 197 } 198 199 const data = await response.json(); 200 201 if (data.success && data.playerData) { 202 log("PlayerDataManager: Loaded player data by user code:", data.playerData); 203 return data.playerData; 204 } 205 206 return null; 207 } catch (error) { 208 log("PlayerDataManager: Error loading player data by user code:", error); 209 return null; 210 } 211 } 212 213 /** 214 * Save player data to server 215 */ 216 async savePlayerDataToServer(playerData) { 217 try { 218 const response = await fetch(this.API_ENDPOINT, { 219 method: 'POST', 220 headers: { 221 'Content-Type': 'application/json', 222 }, 223 body: JSON.stringify(playerData) 224 }); 225 226 if (!response.ok) { 227 throw new Error(`HTTP error! status: ${response.status}`); 228 } 229 230 const data = await response.json(); 231 log("PlayerDataManager: Saved player data to server:", data); 232 return data.success; 233 } catch (error) { 234 log("PlayerDataManager: ERROR saving player data to server."); 235 return false;
236 } 237 } 238 239 /** 240 * Initialize player data with server sync 241 */ 242 async initializePlayerData() { 243 // Get or create local player data 244 let playerData = this.getPlayerData(); 245 246 // Try to load updated data from server 247 const serverData = await this.loadPlayerDataFromServer(playerData.playerId); 248 249 if (serverData) { 250 // Merge server data with local data (server takes precedence for name, xp, level) 251 playerData = { 252 ...playerData, 253 name: serverData.name || playerData.name, 254 xp: serverData.xp || playerData.xp, 255 level: serverData.level || playerData.level, 256 character: serverData.character || playerData.character 257 }; 258 259 // Save updated data locally 260 this.savePlayerDataToStorage(playerData); 261 log("PlayerDataManager: Merged server data with local data"); 262 } else { 263 // Save local data to server for first time 264 await this.savePlayerDataToServer(playerData); 265 log("PlayerDataManager: Saved new player data to server"); 266 } 267 268 return playerData; 269 } 270 271 getPlayerPositionX() { 272 const data = this._read() || {}; 273 const positionX = this.scene ? this.scene.get("Scene0").multiplayerManager.getPlayerData().x : gameConfig.screenResolution.width / 2; 274 log("PlayerDataManager: Retrieved player position X:" + positionX + " alternative:" + data.lastPositionX); 275 return positionX || data.lastPositionX; 276 } 277 278 updatePlayerPosition(currentPlayer) { 279 const data = this._read() || {}; 280 data.playerId = currentPlayer.playerId || data.playerId; 281 data.x = currentPlayer.x; 282 data.y = currentPlayer.y; 283 this._write(data); 284 } 285 286 _read() { 287 try { 288 const raw = localStorage.getItem(this.STORAGE_KEY); 289 return raw ? JSON.parse(raw) : null; 290 } catch (e) { 291 return null; 292 } 293 } 294 295 _write(data) { 296 try { 297 localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data)); 298 } catch (e) { 299 console.warn('PlayerDataManager: Failed to write local storage', e); 300 } 301 } 302 303 /** 304 * Update player XP and level 305 */ 306 updatePlayerXP(currentPlayerData, xpGain) { 307 currentPlayerData.xp += xpGain; 308 309 // XP is a lifetime total: nothing is deducted here, the level just follows it. 310 // A negative xpGain lowers the total and can therefore lower the level. 311 const newLevel = levelForXp(currentPlayerData.xp); 312 if (newLevel !== currentPlayerData.level) { 313 log(`PlayerDataManager: Level ${newLevel > currentPlayerData.level ? 'up' : 'down'}! Player is now level ${newLevel}`); 314 currentPlayerData.level = newLevel; 315 } 316 317 // Save to both localStorage and server 318 this.savePlayerDataToStorage(currentPlayerData); 319 this.savePlayerDataToServer(currentPlayerData); 320 321 return currentPlayerData; 322 } 323 324 /** 325 * Update player character data 326 */ 327 updatePlayerCharacter(currentPlayerData, newCharacterData) { 328 currentPlayerData.character = newCharacterData; 329 330 // Save to both localStorage and server 331 this.savePlayerDataToStorage(currentPlayerData); 332 this.savePlayerDataToServer(currentPlayerData); 333 334 return currentPlayerData; 335 } 336 337 /** 338 * Generate a random player name 339 */ 340 _generatePlayerName() { 341 const adjectives = ['Poker', 'Smart', 'Glücks', 'Cool', 'Best', 'Crazy', 'Schneller', 'Erfolgs']; 342 const nouns = ['Spieler', 'Gamer', 'Held', 'Entdecker', 'Besucher', 'Gast', 'User', 'Freund']; 343 const adj = adjectives[Math.floor(Math.random() * adjectives.length)]; 344 const noun = nouns[Math.floor(Math.random() * nouns.length)]; 345 const number = Math.floor(Math.random() * 999) + 1; 346 return `${adj}${noun}${number}`; 347 } 348 349 /** 350 * Generate random character appearance 351 */ 352 _generateRandomCharacter() { 353 // Randomly choose gender 354 const gender = Math.random() < 0.5 ? 'male' : 'female'; 355 356 let versions; 357 if (gender === 'female') { 358 versions = { 359 skin: Math.floor(Math.random() * 5) + 1, // 1-5 360 corset: Math.floor(Math.random() * 5) + 1, // 1-5 361 hair: Math.floor(Math.random() * 5) + 1, // 1-5 362 skirt: Math.floor(Math.random() * 5) + 1, // 1-5 363 socks: Math.floor(Math.random() * 5) + 1, // 1-5 364 hat: Math.floor(Math.random() * 5) + 1 // 1-5 365 }; 366 } else { 367 versions = { 368 skin: Math.floor(Math.random() * 5) + 1, // 1-5 369 shirt: Math.floor(Math.random() * 5) + 1, // 1-5 370 hair: Math.floor(Math.random() * 5) + 1, // 1-5 371 shoes: Math.floor(Math.random() * 5) + 1, // 1-5 372 pants: Math.floor(Math.random() * 5) + 1, // 1-5 373 hat: Math.floor(Math.random() * 5) + 1 // 1-5 374 }; 375 } 376 377 return { gender, versions }; 378 } 379}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.