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:
Z. Cliffe Schreuders
2025-11-20 15:37:38 +00:00
parent 266bc7a7ca
commit 87fae7cb07

View File

@@ -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