mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
refactor: Simplify unlock loading UI dramatically
User correctly pointed out the loading UI was over-engineered. ## Simplifications: ### Before (over-complicated): - Complex timeline management - Success/failure flash effects (green/red) - Spinner alternatives - Stored references on sprites - Timeline cleanup logic - ~150 lines of code ### After (simple): - startThrob(sprite) - Blue tint + pulsing alpha - stopThrob(sprite) - Kill tweens, reset - ~20 lines of code ## Why This Works: 1. **Door sprites get removed anyway** when they open 2. **Container items transition** to next state automatically 3. **Game already shows alerts** for success/error 4. **Only need feedback** during the ~100-300ms API call ## API Changes: - showUnlockLoading() → startThrob() - clearUnlockLoading() → stopThrob() - No success/failure parameter needed - No stored references to clean up ## Result: From 150+ lines down to ~30 lines total. Same UX, much simpler implementation. User feedback: "Just set the door or item to throb, and remove when the loading finishes (the door sprite is removed anyway), and if it's a container, just follow the unlock with a removal of the animation."
This commit is contained in:
@@ -936,9 +936,9 @@ const inkScript = await ApiClient.getNPCScript(npc.id);
|
|||||||
|
|
||||||
**CRITICAL:** This section handles the conversion from client-side to server-side unlock validation. The unlock system is in `js/systems/unlock-system.js` and is called after minigames succeed.
|
**CRITICAL:** This section handles the conversion from client-side to server-side unlock validation. The unlock system is in `js/systems/unlock-system.js` and is called after minigames succeed.
|
||||||
|
|
||||||
#### 9.5.1 Create Unlock Loading UI Helper
|
#### 9.5.1 Create Simple Unlock Loading Helper
|
||||||
|
|
||||||
**Create a new file for visual feedback during async unlock validation:**
|
**Create a minimal helper for throbbing effect during server validation:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
vim public/break_escape/js/utils/unlock-loading-ui.js
|
vim public/break_escape/js/utils/unlock-loading-ui.js
|
||||||
@@ -950,150 +950,53 @@ vim public/break_escape/js/utils/unlock-loading-ui.js
|
|||||||
/**
|
/**
|
||||||
* UNLOCK LOADING UI
|
* UNLOCK LOADING UI
|
||||||
* =================
|
* =================
|
||||||
*
|
* Simple throbbing effect for doors/objects during server unlock validation.
|
||||||
* Provides visual feedback during async server unlock validation.
|
|
||||||
* Shows a throbbing tint effect on doors/objects being unlocked.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply throbbing tint effect to a Phaser sprite
|
* Start throbbing effect on sprite
|
||||||
* @param {Phaser.GameObjects.Sprite} sprite - The door or object sprite
|
* @param {Phaser.GameObjects.Sprite} sprite - The door or object sprite
|
||||||
* @param {number} duration - How long to show the effect (ms)
|
|
||||||
* @returns {Promise} Resolves when animation completes
|
|
||||||
*/
|
*/
|
||||||
export function showUnlockLoading(sprite) {
|
export function startThrob(sprite) {
|
||||||
if (!sprite || !sprite.scene) {
|
if (!sprite || !sprite.scene) return;
|
||||||
console.warn('showUnlockLoading: Invalid sprite');
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store original tint
|
// Blue tint + pulsing alpha
|
||||||
const originalTint = sprite.tint || 0xffffff;
|
sprite.setTint(0x4da6ff);
|
||||||
|
|
||||||
// Create throbbing animation (blue tint pulsing)
|
sprite.scene.tweens.add({
|
||||||
const scene = sprite.scene;
|
|
||||||
|
|
||||||
// Add blue tint and start pulsing
|
|
||||||
sprite.setTint(0x4da6ff); // Light blue
|
|
||||||
|
|
||||||
// Create timeline for throbbing effect
|
|
||||||
const timeline = scene.tweens.createTimeline();
|
|
||||||
|
|
||||||
// Pulse darker blue
|
|
||||||
timeline.add({
|
|
||||||
targets: sprite,
|
targets: sprite,
|
||||||
alpha: { from: 1.0, to: 0.7 },
|
alpha: { from: 1.0, to: 0.7 },
|
||||||
duration: 300,
|
duration: 300,
|
||||||
ease: 'Sine.easeInOut'
|
ease: 'Sine.easeInOut',
|
||||||
|
yoyo: true,
|
||||||
|
repeat: -1 // Loop forever
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pulse back to lighter
|
|
||||||
timeline.add({
|
|
||||||
targets: sprite,
|
|
||||||
alpha: { from: 0.7, to: 1.0 },
|
|
||||||
duration: 300,
|
|
||||||
ease: 'Sine.easeInOut'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Loop the pulse
|
|
||||||
timeline.loop = -1; // Infinite loop
|
|
||||||
timeline.play();
|
|
||||||
|
|
||||||
// Store timeline reference on sprite for cleanup
|
|
||||||
sprite._unlockLoadingTimeline = timeline;
|
|
||||||
|
|
||||||
return timeline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear unlock loading effect
|
* Stop throbbing effect
|
||||||
* @param {Phaser.GameObjects.Sprite} sprite - The door or object sprite
|
|
||||||
* @param {boolean} success - Whether unlock succeeded
|
|
||||||
*/
|
|
||||||
export function clearUnlockLoading(sprite, success = true) {
|
|
||||||
if (!sprite || !sprite.scene) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop and remove timeline
|
|
||||||
if (sprite._unlockLoadingTimeline) {
|
|
||||||
sprite._unlockLoadingTimeline.stop();
|
|
||||||
sprite._unlockLoadingTimeline.remove();
|
|
||||||
sprite._unlockLoadingTimeline = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove tint with quick flash
|
|
||||||
const scene = sprite.scene;
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// Success: Quick green flash then clear
|
|
||||||
sprite.setTint(0x00ff00); // Green
|
|
||||||
scene.tweens.add({
|
|
||||||
targets: sprite,
|
|
||||||
alpha: { from: 1.0, to: 1.0 },
|
|
||||||
duration: 200,
|
|
||||||
onComplete: () => {
|
|
||||||
sprite.clearTint();
|
|
||||||
sprite.setAlpha(1.0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Failure: Quick red flash then clear
|
|
||||||
sprite.setTint(0xff0000); // Red
|
|
||||||
scene.tweens.add({
|
|
||||||
targets: sprite,
|
|
||||||
alpha: { from: 1.0, to: 1.0 },
|
|
||||||
duration: 200,
|
|
||||||
onComplete: () => {
|
|
||||||
sprite.clearTint();
|
|
||||||
sprite.setAlpha(1.0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show loading spinner near sprite (alternative visual)
|
|
||||||
* @param {Phaser.GameObjects.Sprite} sprite - The door or object sprite
|
* @param {Phaser.GameObjects.Sprite} sprite - The door or object sprite
|
||||||
*/
|
*/
|
||||||
export function showLoadingSpinner(sprite) {
|
export function stopThrob(sprite) {
|
||||||
if (!sprite || !sprite.scene) {
|
if (!sprite || !sprite.scene) return;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scene = sprite.scene;
|
// Kill all tweens on this sprite
|
||||||
|
sprite.scene.tweens.killTweensOf(sprite);
|
||||||
|
|
||||||
// Create simple rotating circle as spinner
|
// Reset appearance
|
||||||
const spinner = scene.add.graphics();
|
sprite.clearTint();
|
||||||
spinner.lineStyle(3, 0x4da6ff, 1);
|
sprite.setAlpha(1.0);
|
||||||
spinner.arc(sprite.x, sprite.y - 30, 10, 0, Math.PI * 1.5);
|
|
||||||
spinner.setDepth(1000); // Always on top
|
|
||||||
|
|
||||||
// Rotate continuously
|
|
||||||
scene.tweens.add({
|
|
||||||
targets: spinner,
|
|
||||||
angle: 360,
|
|
||||||
duration: 1000,
|
|
||||||
repeat: -1
|
|
||||||
});
|
|
||||||
|
|
||||||
sprite._unlockLoadingSpinner = spinner;
|
|
||||||
|
|
||||||
return spinner;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear loading spinner
|
|
||||||
* @param {Phaser.GameObjects.Sprite} sprite - The door or object sprite
|
|
||||||
*/
|
|
||||||
export function clearLoadingSpinner(sprite) {
|
|
||||||
if (sprite && sprite._unlockLoadingSpinner) {
|
|
||||||
sprite._unlockLoadingSpinner.destroy();
|
|
||||||
sprite._unlockLoadingSpinner = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**That's it! No complex timeline management, no success/failure flashes, no stored references.**
|
||||||
|
|
||||||
|
**Why this is simpler:**
|
||||||
|
- Door sprite gets removed anyway when it opens
|
||||||
|
- Container items automatically transition to next state
|
||||||
|
- Game already shows success/error alerts
|
||||||
|
- Just need visual feedback during the ~100-300ms API call
|
||||||
|
|
||||||
**Save and close**
|
**Save and close**
|
||||||
|
|
||||||
#### 9.5.2 Update unlock-system.js for Server Validation
|
#### 9.5.2 Update unlock-system.js for Server Validation
|
||||||
@@ -1108,7 +1011,7 @@ vim public/break_escape/js/systems/unlock-system.js
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { ApiClient } from '../api-client.js';
|
import { ApiClient } from '../api-client.js';
|
||||||
import { showUnlockLoading, clearUnlockLoading } from '../utils/unlock-loading-ui.js';
|
import { startThrob, stopThrob } from '../utils/unlock-loading-ui.js';
|
||||||
```
|
```
|
||||||
|
|
||||||
**Find the `unlockTarget` function (around line 468) and wrap it with server validation:**
|
**Find the `unlockTarget` function (around line 468) and wrap it with server validation:**
|
||||||
@@ -1140,8 +1043,8 @@ export function unlockTarget(lockable, type, layer) {
|
|||||||
export async function unlockTarget(lockable, type, layer, attempt = null, method = null) {
|
export async function unlockTarget(lockable, type, layer, attempt = null, method = null) {
|
||||||
console.log('🔓 unlockTarget called:', { type, lockable, attempt, method });
|
console.log('🔓 unlockTarget called:', { type, lockable, attempt, method });
|
||||||
|
|
||||||
// Show loading UI
|
// Start throbbing
|
||||||
showUnlockLoading(lockable);
|
startThrob(lockable);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get target ID
|
// Get target ID
|
||||||
@@ -1153,8 +1056,8 @@ export async function unlockTarget(lockable, type, layer, attempt = null, method
|
|||||||
console.log('🔓 Validating unlock with server...', { targetId, type, method });
|
console.log('🔓 Validating unlock with server...', { targetId, type, method });
|
||||||
const result = await ApiClient.unlock(type, targetId, attempt, method);
|
const result = await ApiClient.unlock(type, targetId, attempt, method);
|
||||||
|
|
||||||
// Clear loading UI (success)
|
// Stop throbbing (whether success or failure)
|
||||||
clearUnlockLoading(lockable, true);
|
stopThrob(lockable);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('✅ Server validated unlock');
|
console.log('✅ Server validated unlock');
|
||||||
@@ -1173,12 +1076,6 @@ export async function unlockTarget(lockable, type, layer, attempt = null, method
|
|||||||
lockType: doorProps.lockType
|
lockType: doorProps.lockType
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update room data if server provided it
|
|
||||||
if (result.roomData) {
|
|
||||||
// Merge server room data with client state
|
|
||||||
console.log('🔓 Received room data from server:', result.roomData);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Handle item unlocking
|
// Handle item unlocking
|
||||||
if (lockable.scenarioData) {
|
if (lockable.scenarioData) {
|
||||||
@@ -1196,10 +1093,9 @@ export async function unlockTarget(lockable, type, layer, attempt = null, method
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-launch container minigame
|
// Auto-launch container minigame (throb already stopped)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (window.handleContainerInteraction) {
|
if (window.handleContainerInteraction) {
|
||||||
console.log('Auto-launching container minigame after unlock');
|
|
||||||
window.handleContainerInteraction(lockable);
|
window.handleContainerInteraction(lockable);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -1219,7 +1115,6 @@ export async function unlockTarget(lockable, type, layer, attempt = null, method
|
|||||||
lockable.layer.remove(lockable);
|
lockable.layer.remove(lockable);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Collected item:', lockable.scenarioData);
|
|
||||||
window.gameAlert(`Collected ${lockable.scenarioData?.name || 'item'}`,
|
window.gameAlert(`Collected ${lockable.scenarioData?.name || 'item'}`,
|
||||||
'success', 'Item Collected', 3000);
|
'success', 'Item Collected', 3000);
|
||||||
}
|
}
|
||||||
@@ -1229,8 +1124,8 @@ export async function unlockTarget(lockable, type, layer, attempt = null, method
|
|||||||
window.gameAlert(result.message || 'Unlock failed', 'error', 'Unlock Failed', 3000);
|
window.gameAlert(result.message || 'Unlock failed', 'error', 'Unlock Failed', 3000);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Clear loading UI (failure)
|
// Stop throbbing on error
|
||||||
clearUnlockLoading(lockable, false);
|
stopThrob(lockable);
|
||||||
|
|
||||||
console.error('❌ Unlock validation failed:', error);
|
console.error('❌ Unlock validation failed:', error);
|
||||||
window.gameAlert('Failed to validate unlock with server', 'error', 'Network Error', 4000);
|
window.gameAlert('Failed to validate unlock with server', 'error', 'Network Error', 4000);
|
||||||
@@ -1244,6 +1139,12 @@ export function unlockTargetClientSide(lockable, type, layer) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Key simplifications:**
|
||||||
|
- Just `startThrob()` at the beginning
|
||||||
|
- Just `stopThrob()` when done (success, failure, or error)
|
||||||
|
- No need to track success/failure differently - the game alerts handle that
|
||||||
|
- Door/container transitions handle removing the sprite
|
||||||
|
|
||||||
**Save and close**
|
**Save and close**
|
||||||
|
|
||||||
#### 9.5.3 Update Minigame Callbacks
|
#### 9.5.3 Update Minigame Callbacks
|
||||||
|
|||||||
Reference in New Issue
Block a user