mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-21 11:18:08 +00:00
feat: Implement NPC security guard interaction for lockpicking detection
- Added a new security guard NPC with conversation flow for lockpicking attempts. - Integrated influence system to determine NPC reactions based on player choices. - Created a new JSON scenario for the security guard's behavior and interactions. - Refactored lockpicking system to allow NPC interruptions during attempts. - Developed a test scenario to visualize NPC patrol and line-of-sight detection. - Added a debug panel for testing line-of-sight visualization in the game.
This commit is contained in:
@@ -740,6 +740,11 @@ export function update() {
|
||||
window.npcBehaviorManager.update(this.time.now, this.time.delta);
|
||||
}
|
||||
|
||||
// Update NPC LOS visualizations if enabled
|
||||
if (window.npcManager && window.npcManager.losVisualizationEnabled) {
|
||||
window.npcManager.updateLOSVisualizations(this);
|
||||
}
|
||||
|
||||
// Check for object interactions
|
||||
checkObjectInteractions.call(this);
|
||||
|
||||
|
||||
@@ -1963,6 +1963,37 @@ function createNPCSpritesForRoom(roomId, roomData) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-enable LOS visualization if any NPC has los.visualize = true
|
||||
if (window.npcManager && gameRef) {
|
||||
console.log(`👁️ Checking ${npcsInRoom.length} NPCs for LOS visualization requests...`);
|
||||
npcsInRoom.forEach(npc => {
|
||||
console.log(` NPC "${npc.id}": los=${!!npc.los}, visualize=${npc.los?.visualize}`);
|
||||
});
|
||||
|
||||
const hasVisualNPC = npcsInRoom.some(npc => npc.los?.visualize === true);
|
||||
console.log(`👁️ hasVisualNPC: ${hasVisualNPC}`);
|
||||
|
||||
if (hasVisualNPC) {
|
||||
console.log(`👁️ Auto-enabling LOS visualization for room ${roomId}`);
|
||||
console.log(` npcManager: ${!!window.npcManager}`);
|
||||
console.log(` gameRef: ${!!gameRef}`);
|
||||
|
||||
// Get the current scene - use gameRef.scene (the current scene manager) or fall back to window.game
|
||||
const currentScene = gameRef.scene || window.game?.scene?.scenes?.[0];
|
||||
console.log(` currentScene: ${!!currentScene}, key: ${currentScene?.key}`);
|
||||
|
||||
if (currentScene) {
|
||||
window.npcManager.setLOSVisualization(true, currentScene);
|
||||
} else {
|
||||
console.warn(`⚠️ Cannot get current scene for LOS visualization`);
|
||||
}
|
||||
} else {
|
||||
console.log(`👁️ No NPCs requesting LOS visualization in room ${roomId}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`👁️ Cannot auto-enable LOS: npcManager=${!!window.npcManager}, gameRef=${!!gameRef}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
104
js/main.js
104
js/main.js
@@ -184,6 +184,110 @@ function initializeGame() {
|
||||
window.addEventListener('orientationchange', handleOrientationChange);
|
||||
document.addEventListener('fullscreenchange', handleOrientationChange);
|
||||
|
||||
// Check for LOS visualization debug flag
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('debug-los') || urlParams.has('los')) {
|
||||
// Delay to ensure scene is ready
|
||||
setTimeout(() => {
|
||||
const mainScene = window.game?.scene?.scenes?.[0];
|
||||
if (mainScene && window.npcManager) {
|
||||
console.log('🔍 Enabling LOS visualization (from URL parameter)');
|
||||
window.npcManager.setLOSVisualization(true, mainScene);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Add console helper
|
||||
window.enableLOS = function() {
|
||||
console.log('🔍 enableLOS() called');
|
||||
console.log(' game:', !!window.game);
|
||||
console.log(' game.scene:', !!window.game?.scene);
|
||||
console.log(' scenes:', window.game?.scene?.scenes?.length ?? 0);
|
||||
|
||||
const mainScene = window.game?.scene?.scenes?.[0];
|
||||
console.log(' mainScene:', !!mainScene, mainScene?.key);
|
||||
console.log(' npcManager:', !!window.npcManager);
|
||||
|
||||
if (!mainScene) {
|
||||
console.error('❌ Could not get main scene');
|
||||
// Try to find any active scene
|
||||
if (window.game?.scene?.scenes) {
|
||||
for (let i = 0; i < window.game.scene.scenes.length; i++) {
|
||||
console.log(` Available scene[${i}]:`, window.game.scene.scenes[i].key, 'isActive:', window.game.scene.scenes[i].isActive());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.npcManager) {
|
||||
console.error('❌ npcManager not available');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🎯 Setting LOS visualization with scene:', mainScene.key);
|
||||
window.npcManager.setLOSVisualization(true, mainScene);
|
||||
console.log('✅ LOS visualization enabled');
|
||||
};
|
||||
|
||||
window.disableLOS = function() {
|
||||
if (window.npcManager) {
|
||||
window.npcManager.setLOSVisualization(false);
|
||||
console.log('✅ LOS visualization disabled');
|
||||
} else {
|
||||
console.error('❌ npcManager not available');
|
||||
}
|
||||
};
|
||||
|
||||
// Test graphics rendering
|
||||
window.testGraphics = function() {
|
||||
console.log('🧪 Testing graphics rendering...');
|
||||
const scene = window.game?.scene?.scenes?.[0];
|
||||
if (!scene) {
|
||||
console.error('❌ No scene found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📊 Scene:', scene.key, 'Active:', scene.isActive());
|
||||
|
||||
const test = scene.add.graphics();
|
||||
console.log('✅ Created graphics object:', {
|
||||
exists: !!test,
|
||||
hasScene: !!test.scene,
|
||||
depth: test.depth,
|
||||
alpha: test.alpha,
|
||||
visible: test.visible
|
||||
});
|
||||
|
||||
test.fillStyle(0xff0000, 0.5);
|
||||
test.fillRect(100, 100, 50, 50);
|
||||
console.log('✅ Drew red square at (100, 100)');
|
||||
console.log(' If you see a RED SQUARE on screen, graphics rendering is working!');
|
||||
console.log(' If NOT, check browser console for errors');
|
||||
|
||||
// Clean up after 5 seconds
|
||||
setTimeout(() => {
|
||||
test.destroy();
|
||||
console.log('🧹 Test graphics cleaned up');
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// Get detailed LOS status
|
||||
window.losStatus = function() {
|
||||
console.log('📡 LOS System Status:');
|
||||
console.log(' Enabled:', window.npcManager?.losVisualizationEnabled ?? 'N/A');
|
||||
console.log(' NPCs loaded:', window.npcManager?.npcs?.size ?? 0);
|
||||
console.log(' Graphics objects:', window.npcManager?.losVisualizations?.size ?? 0);
|
||||
|
||||
if (window.npcManager?.npcs?.size > 0) {
|
||||
for (const npc of window.npcManager.npcs.values()) {
|
||||
console.log(` NPC: "${npc.id}"`);
|
||||
console.log(` LOS enabled: ${npc.los?.enabled ?? false}`);
|
||||
console.log(` Position: (${npc.sprite?.x.toFixed(0) ?? 'N/A'}, ${npc.sprite?.y.toFixed(0) ?? 'N/A'})`);
|
||||
console.log(` Facing: ${npc.facingDirection ?? npc.direction ?? 'N/A'}°`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initial setup
|
||||
setTimeout(setupPixelArt, 100);
|
||||
}
|
||||
|
||||
345
js/systems/npc-los.js
Normal file
345
js/systems/npc-los.js
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* NPC LINE-OF-SIGHT (LOS) SYSTEM
|
||||
* ===============================
|
||||
*
|
||||
* Handles visibility detection for NPCs with configurable:
|
||||
* - Detection range (in pixels)
|
||||
* - Field-of-view angle (in degrees, cone shape)
|
||||
* - Facing direction (auto-calculated from NPC's current direction)
|
||||
* - Visualization (debug cone rendering)
|
||||
*
|
||||
* Used to determine if an NPC can "see" the player or events like lockpicking.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if target is within NPC's line of sight
|
||||
* @param {Object} npc - NPC object with position and direction
|
||||
* @param {Object} target - Target position { x, y } or entity with sprite
|
||||
* @param {Object} losConfig - LOS configuration { range, angle, enabled }
|
||||
* @returns {boolean} True if target is in LOS
|
||||
*/
|
||||
export function isInLineOfSight(npc, target, losConfig = {}) {
|
||||
// Default LOS config if not provided
|
||||
const {
|
||||
range = 200, // Detection range in pixels
|
||||
angle = 120, // Field of view angle in degrees (120 = 60° on each side of facing direction)
|
||||
enabled = true
|
||||
} = losConfig;
|
||||
|
||||
if (!enabled) return true; // If LOS disabled, always return true (can see everything)
|
||||
|
||||
// Get NPC position
|
||||
const npcPos = getNPCPosition(npc);
|
||||
if (!npcPos) return false;
|
||||
|
||||
// Get target position
|
||||
const targetPos = getTargetPosition(target);
|
||||
if (!targetPos) return false;
|
||||
|
||||
// Calculate distance
|
||||
const distance = Phaser.Math.Distance.Between(npcPos.x, npcPos.y, targetPos.x, targetPos.y);
|
||||
if (distance > range) {
|
||||
return false; // Target outside range
|
||||
}
|
||||
|
||||
// Get NPC's facing direction (0-360 degrees)
|
||||
const npcFacing = getNPCFacingDirection(npc);
|
||||
|
||||
// Calculate angle to target from NPC
|
||||
const angleToTarget = Phaser.Math.Angle.Between(npcPos.x, npcPos.y, targetPos.x, targetPos.y);
|
||||
const angleToTargetDegrees = Phaser.Math.RadToDeg(angleToTarget);
|
||||
|
||||
// Normalize angles to 0-360
|
||||
const normalizedFacing = normalizeAngle(npcFacing);
|
||||
const normalizedTarget = normalizeAngle(angleToTargetDegrees);
|
||||
|
||||
// Calculate angular difference (shortest arc)
|
||||
const angleDiff = shortestAngularDistance(normalizedFacing, normalizedTarget);
|
||||
const maxAngle = angle / 2; // Half angle on each side
|
||||
|
||||
return Math.abs(angleDiff) <= maxAngle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPC's current position
|
||||
*/
|
||||
function getNPCPosition(npc) {
|
||||
if (!npc) return null;
|
||||
|
||||
// If NPC has _sprite property (how it's stored: npc._sprite = sprite)
|
||||
if (npc._sprite && typeof npc._sprite.getCenter === 'function') {
|
||||
return npc._sprite.getCenter();
|
||||
}
|
||||
|
||||
// If NPC has sprite property (Phaser sprite)
|
||||
if (npc.sprite && typeof npc.sprite.getCenter === 'function') {
|
||||
return npc.sprite.getCenter();
|
||||
}
|
||||
|
||||
// If NPC has x, y properties (raw coordinates)
|
||||
if (npc.x !== undefined && npc.y !== undefined) {
|
||||
return { x: npc.x, y: npc.y };
|
||||
}
|
||||
|
||||
// If NPC has position property
|
||||
if (npc.position && npc.position.x !== undefined && npc.position.y !== undefined) {
|
||||
return { x: npc.position.x, y: npc.position.y };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get target position (handles both plain objects and sprites)
|
||||
*/
|
||||
function getTargetPosition(target) {
|
||||
if (!target) return null;
|
||||
|
||||
// If target has getCenter method (Phaser sprite)
|
||||
if (typeof target.getCenter === 'function') {
|
||||
return target.getCenter();
|
||||
}
|
||||
|
||||
// If target has x, y properties
|
||||
if (target.x !== undefined && target.y !== undefined) {
|
||||
return { x: target.x, y: target.y };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPC's current facing direction in degrees (0-360)
|
||||
* Tries multiple sources: stored direction, sprite direction, patrol direction
|
||||
*/
|
||||
export function getNPCFacingDirection(npc) {
|
||||
if (!npc) return 0;
|
||||
|
||||
// If NPC has explicit facing direction property
|
||||
if (npc.facingDirection !== undefined) {
|
||||
return npc.facingDirection;
|
||||
}
|
||||
|
||||
// Try to get direction from behavior system (most current)
|
||||
if (window.npcBehaviorManager) {
|
||||
const behavior = window.npcBehaviorManager.getBehavior?.(npc.id);
|
||||
if (behavior && behavior.direction !== undefined) {
|
||||
const directions = {
|
||||
'down': 90, // Down (south)
|
||||
'up': 270, // Up (north)
|
||||
'left': 180, // Left (west)
|
||||
'right': 0, // Right (east)
|
||||
'down-left': 225,
|
||||
'down-right': 45,
|
||||
'up-left': 225,
|
||||
'up-right': 315
|
||||
};
|
||||
return directions[behavior.direction] ?? 90;
|
||||
}
|
||||
}
|
||||
|
||||
// If NPC has _sprite (stored sprite reference) with rotation
|
||||
if (npc._sprite && npc._sprite.rotation !== undefined) {
|
||||
return Phaser.Math.RadToDeg(npc._sprite.rotation);
|
||||
}
|
||||
|
||||
// If NPC has sprite with rotation
|
||||
if (npc.sprite && npc.sprite.rotation !== undefined) {
|
||||
return Phaser.Math.RadToDeg(npc.sprite.rotation);
|
||||
}
|
||||
|
||||
// If NPC has direction property (string or numeric)
|
||||
if (npc.direction !== undefined) {
|
||||
if (typeof npc.direction === 'string') {
|
||||
const directions = {
|
||||
'down': 90,
|
||||
'up': 270,
|
||||
'left': 180,
|
||||
'right': 0,
|
||||
'down-left': 225,
|
||||
'down-right': 45,
|
||||
'up-left': 225,
|
||||
'up-right': 315
|
||||
};
|
||||
return directions[npc.direction] ?? 90;
|
||||
} else if (typeof npc.direction === 'number') {
|
||||
// Numeric: 0=down, 1=left, 2=up, 3=right
|
||||
const directions = [90, 180, 270, 0];
|
||||
return directions[npc.direction % 4] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Default: facing down (90 degrees in Phaser convention for top-down)
|
||||
return 90;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize angle to 0-360 range
|
||||
*/
|
||||
function normalizeAngle(angle) {
|
||||
let normalized = angle % 360;
|
||||
if (normalized < 0) normalized += 360;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate shortest angular distance between two angles
|
||||
* Returns signed value: positive = clockwise, negative = counter-clockwise
|
||||
*/
|
||||
function shortestAngularDistance(from, to) {
|
||||
let diff = to - from;
|
||||
|
||||
// Normalize to -180 to 180
|
||||
while (diff > 180) diff -= 360;
|
||||
while (diff < -180) diff += 360;
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw LOS cone for debugging
|
||||
* @param {Phaser.Scene} scene - Phaser scene for drawing
|
||||
* @param {Object} npc - NPC object
|
||||
* @param {Object} losConfig - LOS configuration
|
||||
* @param {number} color - Hex color for cone (default 0x00ff00)
|
||||
* @param {number} alpha - Alpha value for cone (default 0.2)
|
||||
*/
|
||||
export function drawLOSCone(scene, npc, losConfig = {}, color = 0x00ff00, alpha = 0.2) {
|
||||
const {
|
||||
range = 200,
|
||||
angle = 120,
|
||||
enabled = true
|
||||
} = losConfig;
|
||||
|
||||
if (!enabled || !scene || !scene.add) {
|
||||
console.log('🔴 Cannot draw LOS cone - missing scene or disabled');
|
||||
return null;
|
||||
}
|
||||
|
||||
const npcPos = getNPCPosition(npc);
|
||||
if (!npcPos) {
|
||||
console.log('🔴 Cannot draw LOS cone - NPC position not found', {
|
||||
npcId: npc?.id,
|
||||
hasSprite: !!npc?.sprite,
|
||||
hasX: npc?.x !== undefined,
|
||||
hasPosition: !!npc?.position
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Scale the range to 50% size
|
||||
const scaledRange = range * 0.5;
|
||||
// Set cone opacity to 20%
|
||||
const coneAlpha = 0.2;
|
||||
|
||||
console.log(`🟢 Drawing LOS cone for NPC at (${npcPos.x.toFixed(0)}, ${npcPos.y.toFixed(0)}), range: ${scaledRange}px (50% of ${range}), angle: ${angle}°`);
|
||||
|
||||
const npcFacing = getNPCFacingDirection(npc);
|
||||
console.log(` NPC facing: ${npcFacing.toFixed(0)}°`);
|
||||
|
||||
const npcFacingRad = Phaser.Math.DegToRad(npcFacing);
|
||||
const halfAngleRad = Phaser.Math.DegToRad(angle / 2);
|
||||
|
||||
// Create graphics object for the cone
|
||||
const graphics = scene.add.graphics();
|
||||
console.log(` 📊 Graphics object created - checking properties:`, {
|
||||
graphicsExists: !!graphics,
|
||||
hasScene: !!graphics.scene,
|
||||
sceneKey: graphics.scene?.key,
|
||||
canAdd: typeof graphics.add === 'function'
|
||||
});
|
||||
|
||||
// Draw outer range circle (light, semi-transparent)
|
||||
graphics.lineStyle(1, color, 0.2);
|
||||
graphics.strokeCircle(npcPos.x, npcPos.y, scaledRange);
|
||||
console.log(` ⭕ Range circle drawn at (${npcPos.x}, ${npcPos.y}) radius: ${scaledRange}`);
|
||||
|
||||
// Draw the cone fill
|
||||
graphics.fillStyle(color, coneAlpha);
|
||||
graphics.lineStyle(2, color, 0.2);
|
||||
|
||||
// Calculate cone points
|
||||
const conePoints = [];
|
||||
conePoints.push(new Phaser.Geom.Point(npcPos.x, npcPos.y)); // Center (NPC position)
|
||||
|
||||
// Left edge of cone
|
||||
const leftAngle = npcFacingRad - halfAngleRad;
|
||||
const leftPoint = new Phaser.Geom.Point(
|
||||
npcPos.x + scaledRange * Math.cos(leftAngle),
|
||||
npcPos.y + scaledRange * Math.sin(leftAngle)
|
||||
);
|
||||
conePoints.push(leftPoint);
|
||||
|
||||
// Arc from left to right (approximate with segments)
|
||||
const segments = Math.max(12, Math.floor(angle / 5));
|
||||
for (let i = 1; i < segments; i++) {
|
||||
const t = i / segments;
|
||||
const currentAngle = npcFacingRad - halfAngleRad + (angle * Math.PI / 180) * t;
|
||||
conePoints.push(new Phaser.Geom.Point(
|
||||
npcPos.x + scaledRange * Math.cos(currentAngle),
|
||||
npcPos.y + scaledRange * Math.sin(currentAngle)
|
||||
));
|
||||
}
|
||||
|
||||
// Right edge of cone
|
||||
const rightAngle = npcFacingRad + halfAngleRad;
|
||||
conePoints.push(new Phaser.Geom.Point(
|
||||
npcPos.x + scaledRange * Math.cos(rightAngle),
|
||||
npcPos.y + scaledRange * Math.sin(rightAngle)
|
||||
));
|
||||
|
||||
// Close back to center
|
||||
conePoints.push(new Phaser.Geom.Point(npcPos.x, npcPos.y));
|
||||
|
||||
// Draw the cone polygon
|
||||
graphics.fillPoints(conePoints, true);
|
||||
graphics.strokePoints(conePoints, true);
|
||||
|
||||
// Draw NPC position indicator (bright circle)
|
||||
graphics.fillStyle(color, 0.6);
|
||||
graphics.fillCircle(npcPos.x, npcPos.y, 10);
|
||||
|
||||
// Draw a line showing the facing direction (to front of cone)
|
||||
graphics.lineStyle(3, color, 0.1);
|
||||
const dirLength = scaledRange * 0.4;
|
||||
graphics.lineBetween(
|
||||
npcPos.x,
|
||||
npcPos.y,
|
||||
npcPos.x + dirLength * Math.cos(npcFacingRad),
|
||||
npcPos.y + dirLength * Math.sin(npcFacingRad)
|
||||
);
|
||||
|
||||
// Draw angle wedge markers
|
||||
graphics.lineStyle(1, color, 0.5);
|
||||
graphics.lineBetween(npcPos.x, npcPos.y, leftPoint.x, leftPoint.y);
|
||||
graphics.lineBetween(npcPos.x, npcPos.y,
|
||||
npcPos.x + scaledRange * Math.cos(rightAngle),
|
||||
npcPos.y + scaledRange * Math.sin(rightAngle)
|
||||
);
|
||||
|
||||
// Set depth on top of other objects (was -999, now 9999)
|
||||
graphics.setDepth(9999); // On top of everything
|
||||
graphics.setAlpha(1.0); // Ensure not transparent
|
||||
|
||||
console.log(`✅ LOS cone rendered successfully:`, {
|
||||
positionX: npcPos.x.toFixed(0),
|
||||
positionY: npcPos.y.toFixed(0),
|
||||
depth: graphics.depth,
|
||||
alpha: graphics.alpha,
|
||||
visible: graphics.visible,
|
||||
active: graphics.active,
|
||||
pointsCount: conePoints.length
|
||||
});
|
||||
|
||||
return graphics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up LOS visualization
|
||||
* @param {Phaser.GameObjects.Graphics} graphics - Graphics object to destroy
|
||||
*/
|
||||
export function clearLOSCone(graphics) {
|
||||
if (graphics && typeof graphics.destroy === 'function') {
|
||||
graphics.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// NPCManager with event → knot auto-mapping and conversation history
|
||||
// OPTIMIZED: InkEngine caching, event listener cleanup, debug logging
|
||||
// default export NPCManager
|
||||
import { isInLineOfSight, drawLOSCone, clearLOSCone, getNPCFacingDirection } from './npc-los.js';
|
||||
|
||||
export default class NPCManager {
|
||||
constructor(eventDispatcher, barkSystem = null) {
|
||||
this.eventDispatcher = eventDispatcher;
|
||||
@@ -18,6 +20,10 @@ export default class NPCManager {
|
||||
this.inkEngineCache = new Map(); // { npcId: inkEngine }
|
||||
this.storyCache = new Map(); // { storyPath: storyJson }
|
||||
|
||||
// LOS Visualization
|
||||
this.losVisualizations = new Map(); // { npcId: graphicsObject }
|
||||
this.losVisualizationEnabled = false; // Toggle LOS cone rendering
|
||||
|
||||
// OPTIMIZATION: Debug mode (set via window.NPC_DEBUG = true)
|
||||
this.debug = false;
|
||||
}
|
||||
@@ -112,6 +118,99 @@ export default class NPCManager {
|
||||
return this.npcs.get(id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any NPC in a room should trigger person-chat instead of lockpicking
|
||||
* Considers NPC line-of-sight and facing direction
|
||||
* Returns the NPC if one should handle lockpick_used_in_view with person-chat
|
||||
* Otherwise returns null
|
||||
*/
|
||||
shouldInterruptLockpickingWithPersonChat(roomId, playerPosition = null) {
|
||||
if (!roomId) return null;
|
||||
|
||||
console.log(`👁️ [LOS CHECK] shouldInterruptLockpickingWithPersonChat: roomId="${roomId}", playerPos=${playerPosition ? `(${playerPosition.x.toFixed(0)}, ${playerPosition.y.toFixed(0)})` : 'null'}`);
|
||||
|
||||
for (const npc of this.npcs.values()) {
|
||||
// NPC must be in the specified room and be a 'person' type NPC
|
||||
if (npc.roomId !== roomId || npc.npcType !== 'person') continue;
|
||||
|
||||
console.log(`👁️ [LOS CHECK] Checking NPC: "${npc.id}" (room: ${npc.roomId}, type: ${npc.npcType})`);
|
||||
|
||||
// Check if NPC has lockpick_used_in_view event mapping with person-chat
|
||||
if (npc.eventMappings && Array.isArray(npc.eventMappings)) {
|
||||
const lockpickMapping = npc.eventMappings.find(mapping =>
|
||||
mapping.eventPattern === 'lockpick_used_in_view' &&
|
||||
mapping.conversationMode === 'person-chat'
|
||||
);
|
||||
|
||||
if (!lockpickMapping) {
|
||||
console.log(`👁️ [LOS CHECK] ✗ NPC has no lockpick_used_in_view mapping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`👁️ [LOS CHECK] ✓ NPC has lockpick_used_in_view→person-chat mapping`);
|
||||
|
||||
// Check LOS configuration
|
||||
const losConfig = npc.los || {
|
||||
enabled: true,
|
||||
range: 300, // Default detection range
|
||||
angle: 120 // Default 120° field of view
|
||||
};
|
||||
|
||||
// If player position provided, check if player is in LOS
|
||||
if (playerPosition) {
|
||||
// Get detailed information for debugging
|
||||
// Try to get sprite from npc._sprite (how it's stored), then npc.sprite, then npc position
|
||||
const sprite = npc._sprite || npc.sprite;
|
||||
const npcPos = (sprite && typeof sprite.getCenter === 'function') ?
|
||||
sprite.getCenter() :
|
||||
{ x: npc.x ?? 0, y: npc.y ?? 0 };
|
||||
|
||||
console.log(`👁️ [LOS CHECK] npcPos: ${npcPos ? `(${npcPos.x}, ${npcPos.y})` : 'NULL'}, losConfig: range=${losConfig.range}, angle=${losConfig.angle}`);
|
||||
|
||||
// Ensure npcPos is valid before using
|
||||
if (npcPos && npcPos.x !== undefined && npcPos.y !== undefined &&
|
||||
!Number.isNaN(npcPos.x) && !Number.isNaN(npcPos.y)) {
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(playerPosition.x - npcPos.x, 2) +
|
||||
Math.pow(playerPosition.y - npcPos.y, 2)
|
||||
);
|
||||
|
||||
// Calculate angle to player
|
||||
const angleRad = Math.atan2(playerPosition.y - npcPos.y, playerPosition.x - npcPos.x);
|
||||
const angleToPlayer = (angleRad * 180 / Math.PI + 360) % 360;
|
||||
|
||||
// Get NPC facing direction for debugging
|
||||
const npcFacing = getNPCFacingDirection(npc);
|
||||
|
||||
const inLOS = isInLineOfSight(npc, playerPosition, losConfig);
|
||||
console.log(`👁️ [LOS CHECK] NPC Facing: ${npcFacing.toFixed(1)}°, Distance: ${distance.toFixed(1)}px (range: ${losConfig.range}), Angle: ${angleToPlayer.toFixed(1)}° (FOV: ${losConfig.angle}°), LOS: ${inLOS}`);
|
||||
|
||||
if (!inLOS) {
|
||||
console.log(
|
||||
`👁️ NPC "${npc.id}" CANNOT see player\n` +
|
||||
` Position: NPC(${npcPos.x.toFixed(0)}, ${npcPos.y.toFixed(0)}) → Player(${playerPosition.x.toFixed(0)}, ${playerPosition.y.toFixed(0)})\n` +
|
||||
` Distance: ${distance.toFixed(1)}px (range: ${losConfig.range}px) ${distance > losConfig.range ? '❌ TOO FAR' : '✅ in range'}\n` +
|
||||
` Angle to Player: ${angleToPlayer.toFixed(1)}° (FOV: ${losConfig.angle}°)`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
console.log(`👁️ [LOS CHECK] Position invalid, checking LOS anyway...`);
|
||||
if (!isInLineOfSight(npc, playerPosition, losConfig)) {
|
||||
// Position unavailable but still check LOS detection
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`<EFBFBD>🚫 INTERRUPTING LOCKPICKING: NPC "${npc.id}" in room "${roomId}" can see player and has person-chat mapped to lockpick event`);
|
||||
return npc;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set bark system (can be set after construction)
|
||||
setBarkSystem(barkSystem) {
|
||||
this.barkSystem = barkSystem;
|
||||
@@ -203,7 +302,8 @@ export default class NPCManager {
|
||||
once: mapping.onceOnly || mapping.once,
|
||||
cooldown: mapping.cooldown,
|
||||
condition: mapping.condition,
|
||||
maxTriggers: mapping.maxTriggers // Add max trigger limit
|
||||
maxTriggers: mapping.maxTriggers, // Add max trigger limit
|
||||
conversationMode: mapping.conversationMode // Add conversation mode (e.g., 'person-chat')
|
||||
};
|
||||
|
||||
console.log(` 📌 Registering listener for event: ${eventPattern} → ${config.knot}`);
|
||||
@@ -296,6 +396,40 @@ export default class NPCManager {
|
||||
console.log(`📍 Updated ${npcId} current knot to: ${config.knot}`);
|
||||
}
|
||||
|
||||
// Debug: Log the full config to see what we're working with
|
||||
console.log(`🔍 Event config for ${eventPattern}:`, {
|
||||
conversationMode: config.conversationMode,
|
||||
npcType: npc.npcType,
|
||||
knot: config.knot,
|
||||
fullConfig: config
|
||||
});
|
||||
|
||||
// Check if this event should trigger a full person-chat conversation
|
||||
// instead of just a bark (indicated by conversationMode: 'person-chat')
|
||||
if (config.conversationMode === 'person-chat' && npc.npcType === 'person') {
|
||||
console.log(`👤 Starting person-chat conversation for NPC ${npcId}`);
|
||||
|
||||
// Close any currently running minigame (like lockpicking) first
|
||||
if (window.MinigameFramework && window.MinigameFramework.currentMinigame) {
|
||||
console.log(`🛑 Closing currently running minigame before starting person-chat`);
|
||||
window.MinigameFramework.endMinigame(false, null);
|
||||
console.log(`✅ Closed current minigame`);
|
||||
}
|
||||
|
||||
// Start the person-chat minigame
|
||||
if (window.MinigameFramework) {
|
||||
console.log(`✅ Starting person-chat minigame for ${npcId}`);
|
||||
window.MinigameFramework.startMinigame('person-chat', null, {
|
||||
npcId: npc.id,
|
||||
startKnot: config.knot || npc.currentKnot,
|
||||
scenario: window.gameScenario
|
||||
});
|
||||
console.log(`[NPCManager] Event '${eventPattern}' triggered for NPC '${npcId}' → person-chat conversation`);
|
||||
return; // Exit early - person-chat is handling it
|
||||
} else {
|
||||
console.warn(`⚠️ MinigameFramework not available for person-chat`);
|
||||
}
|
||||
}
|
||||
// If bark text is provided, show it directly
|
||||
if (this.barkSystem && (config.bark || config.message)) {
|
||||
const barkText = config.bark || config.message;
|
||||
@@ -743,6 +877,106 @@ export default class NPCManager {
|
||||
this.storyCache.clear();
|
||||
console.log(`[NPCManager] Cleared all caches`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable LOS cone visualization for debugging
|
||||
* @param {boolean} enable - Whether to show LOS cones
|
||||
* @param {Phaser.Scene} scene - Phaser scene for drawing
|
||||
*/
|
||||
setLOSVisualization(enable, scene = null) {
|
||||
this.losVisualizationEnabled = enable;
|
||||
|
||||
if (enable && scene) {
|
||||
console.log('👁️ Enabling LOS visualization');
|
||||
this._updateLOSVisualizations(scene);
|
||||
} else if (!enable) {
|
||||
console.log('👁️ Disabling LOS visualization');
|
||||
this._clearLOSVisualizations();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update LOS visualizations for all NPCs in a scene
|
||||
* Call this from the game loop (update method) if visualization is enabled
|
||||
* @param {Phaser.Scene} scene - Phaser scene for drawing
|
||||
*/
|
||||
updateLOSVisualizations(scene) {
|
||||
if (!this.losVisualizationEnabled || !scene) return;
|
||||
|
||||
this._updateLOSVisualizations(scene);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Update or create LOS cone graphics
|
||||
*/
|
||||
_updateLOSVisualizations(scene) {
|
||||
console.log(`🎯 Updating LOS visualizations for ${this.npcs.size} NPCs`);
|
||||
let visualizedCount = 0;
|
||||
|
||||
for (const npc of this.npcs.values()) {
|
||||
// Only visualize person-type NPCs with LOS config
|
||||
if (npc.npcType !== 'person') {
|
||||
console.log(` Skip "${npc.id}" - not person type (${npc.npcType})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!npc.los || !npc.los.enabled) {
|
||||
console.log(` Skip "${npc.id}" - no LOS config or disabled`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(` Processing "${npc.id}" - has LOS config`, npc.los);
|
||||
|
||||
// Remove old visualization
|
||||
if (this.losVisualizations.has(npc.id)) {
|
||||
console.log(` Clearing old visualization for "${npc.id}"`);
|
||||
clearLOSCone(this.losVisualizations.get(npc.id));
|
||||
}
|
||||
|
||||
// Draw new cone (depth is set inside drawLOSCone)
|
||||
const graphics = drawLOSCone(scene, npc, npc.los, 0x00ff00, 0.15);
|
||||
if (graphics) {
|
||||
this.losVisualizations.set(npc.id, graphics);
|
||||
// Graphics depth is already set inside drawLOSCone to -999
|
||||
console.log(` ✅ Created visualization for "${npc.id}"`);
|
||||
visualizedCount++;
|
||||
} else {
|
||||
console.log(` ❌ Failed to create visualization for "${npc.id}"`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ LOS visualization update complete: ${visualizedCount}/${this.npcs.size} visualized`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: Clear all LOS visualizations
|
||||
*/
|
||||
_clearLOSVisualizations() {
|
||||
for (const graphics of this.losVisualizations.values()) {
|
||||
clearLOSCone(graphics);
|
||||
}
|
||||
this.losVisualizations.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup: destroy all LOS visualizations and event listeners
|
||||
*/
|
||||
destroy() {
|
||||
this._clearLOSVisualizations();
|
||||
this.stopTimedMessages();
|
||||
|
||||
// Clear all event listeners
|
||||
for (const listeners of this.eventListeners.values()) {
|
||||
listeners.forEach(({ listener }) => {
|
||||
if (this.eventDispatcher && typeof listener === 'function') {
|
||||
this.eventDispatcher.off('*', listener);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.eventListeners.clear();
|
||||
|
||||
console.log('[NPCManager] Destroyed');
|
||||
}
|
||||
}
|
||||
|
||||
// Console helper for debugging
|
||||
|
||||
@@ -108,6 +108,32 @@ export function handleUnlock(lockable, type) {
|
||||
} else if (hasLockpick) {
|
||||
// Only lockpick available - launch lockpicking minigame directly
|
||||
console.log('LOCKPICK AVAILABLE - STARTING LOCKPICKING MINIGAME');
|
||||
|
||||
// CHECK: Should any NPC interrupt with person-chat instead?
|
||||
const roomId = lockable.doorProperties?.roomId || window.currentRoomId;
|
||||
if (window.npcManager && roomId) {
|
||||
// Get player position for LOS check
|
||||
const playerPos = window.player?.sprite?.getCenter ?
|
||||
window.player.sprite.getCenter() :
|
||||
{ x: window.player?.x || 0, y: window.player?.y || 0 };
|
||||
|
||||
const interruptingNPC = window.npcManager.shouldInterruptLockpickingWithPersonChat(roomId, playerPos);
|
||||
if (interruptingNPC) {
|
||||
console.log(`🚫 LOCKPICKING INTERRUPTED: Triggering person-chat with NPC "${interruptingNPC.id}"`);
|
||||
|
||||
// Trigger the lockpick event which will start person-chat
|
||||
if (window.npcManager.eventDispatcher) {
|
||||
window.npcManager.eventDispatcher.emit('lockpick_used_in_view', {
|
||||
npcId: interruptingNPC.id,
|
||||
roomId: roomId,
|
||||
lockable: lockable,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
return; // Don't start lockpicking minigame
|
||||
}
|
||||
}
|
||||
|
||||
let difficulty = lockable.doorProperties?.difficulty || lockable.scenarioData?.difficulty || lockable.properties?.difficulty || lockRequirements.difficulty || 'medium';
|
||||
// Check for both keyPins (camelCase) and key_pins (snake_case)
|
||||
let keyPins = lockable.doorProperties?.keyPins || lockable.doorProperties?.key_pins ||
|
||||
|
||||
198
scenarios/ink/security-guard.ink
Normal file
198
scenarios/ink/security-guard.ink
Normal file
@@ -0,0 +1,198 @@
|
||||
// security-guard.ink
|
||||
// A security guard that patrols the corridor
|
||||
// Reacts when the player attempts to lockpick in their view
|
||||
// Can be persuaded to let the player off or confronted with consequences
|
||||
// Uses hub pattern for clear conversation flow
|
||||
|
||||
VAR influence = 0
|
||||
VAR caught_lockpicking = false
|
||||
VAR confrontation_attempts = 0
|
||||
VAR warned_player = false
|
||||
|
||||
=== start ===
|
||||
# speaker:security_guard
|
||||
{not warned_player:
|
||||
# display:guard-patrol
|
||||
You see the guard patrolling back and forth. They're watching the area carefully.
|
||||
~ warned_player = true
|
||||
What brings you to this corridor?
|
||||
}
|
||||
{warned_player and not caught_lockpicking:
|
||||
# display:guard-patrol
|
||||
The guard nods at you as they continue their patrol.
|
||||
What do you want?
|
||||
}
|
||||
-> hub
|
||||
|
||||
=== hub ===
|
||||
+ [I'm just passing through]
|
||||
-> passing_through
|
||||
+ [I need to access that door]
|
||||
-> request_access
|
||||
+ [Nothing, just leaving]
|
||||
#exit_conversation
|
||||
# speaker:security_guard
|
||||
Good. Stay out of trouble.
|
||||
|
||||
-> hub
|
||||
|
||||
=== on_lockpick_used ===
|
||||
# speaker:security_guard
|
||||
{caught_lockpicking < 1:
|
||||
~ caught_lockpicking = true
|
||||
~ confrontation_attempts = 0
|
||||
}
|
||||
~ confrontation_attempts++
|
||||
|
||||
# display:guard-confrontation
|
||||
{confrontation_attempts == 1:
|
||||
Hey! What do you think you're doing with that lock?
|
||||
|
||||
* [I was just... looking for something I dropped]
|
||||
-> explain_drop
|
||||
* [This is official business]
|
||||
-> claim_official
|
||||
* [I can explain...]
|
||||
-> explain_situation
|
||||
* [Mind your own business]
|
||||
-> hostile_response
|
||||
}
|
||||
{confrontation_attempts > 1:
|
||||
I already told you to stop! This is your final warning.
|
||||
|
||||
* [Okay, I'm leaving right now]
|
||||
-> back_down
|
||||
* [You can't tell me what to do]
|
||||
-> escalate_conflict
|
||||
}
|
||||
|
||||
=== explain_drop ===
|
||||
# speaker:security_guard
|
||||
{influence >= 30:
|
||||
~ influence -= 10
|
||||
Looking for something... sure. Well, I don't get paid enough to care too much.
|
||||
Just make it quick and don't let me catch you again.
|
||||
# display:guard-annoyed
|
||||
-> hub
|
||||
}
|
||||
{influence < 30:
|
||||
~ influence -= 15
|
||||
That's a pretty thin excuse. I'm going to have to report this incident.
|
||||
Move along before I call for backup.
|
||||
# display:guard-hostile
|
||||
-> END
|
||||
}
|
||||
|
||||
=== claim_official ===
|
||||
# speaker:security_guard
|
||||
{influence >= 40:
|
||||
~ influence -= 5
|
||||
Official, huh? You look like you might belong here. Fine. But I'm watching.
|
||||
# display:guard-neutral
|
||||
-> hub
|
||||
}
|
||||
{influence < 40:
|
||||
~ influence -= 20
|
||||
Official? I don't recognize your clearance. Security protocol requires me to log this.
|
||||
You're coming with me to speak with my supervisor.
|
||||
# display:guard-alert
|
||||
-> END
|
||||
}
|
||||
|
||||
=== explain_situation ===
|
||||
# speaker:security_guard
|
||||
{influence >= 25:
|
||||
~ influence -= 5
|
||||
I'm listening. Make it quick.
|
||||
|
||||
* [I need to access critical files for the investigation]
|
||||
-> explain_files
|
||||
* [I'm security testing your protocols]
|
||||
-> explain_audit
|
||||
* [Actually, just let me go]
|
||||
-> back_down
|
||||
}
|
||||
{influence < 25:
|
||||
~ influence -= 20
|
||||
No explanations. Security breach detected. This is being reported.
|
||||
# display:guard-arrest
|
||||
-> END
|
||||
}
|
||||
|
||||
=== explain_files ===
|
||||
# speaker:security_guard
|
||||
{influence >= 35:
|
||||
~ influence -= 10
|
||||
Critical files need a key. Do you have one? If not, this conversation is over.
|
||||
# display:guard-sympathetic
|
||||
-> hub
|
||||
}
|
||||
{influence < 35:
|
||||
~ influence -= 15
|
||||
Critical files are locked for a reason. You don't have the clearance.
|
||||
# display:guard-hostile
|
||||
-> END
|
||||
}
|
||||
|
||||
=== explain_audit ===
|
||||
# speaker:security_guard
|
||||
{influence >= 45:
|
||||
~ influence -= 5
|
||||
Security audit? You just exposed our weakest point. Congratulations.
|
||||
But you need to leave now before someone else sees this.
|
||||
# display:guard-amused
|
||||
-> hub
|
||||
}
|
||||
{influence < 45:
|
||||
~ influence -= 20
|
||||
An audit would be scheduled and documented. This isn't.
|
||||
# display:guard-alert
|
||||
-> END
|
||||
}
|
||||
|
||||
=== hostile_response ===
|
||||
# speaker:security_guard
|
||||
~ influence -= 30
|
||||
That's it. You just made a big mistake.
|
||||
SECURITY! CODE VIOLATION IN THE CORRIDOR!
|
||||
# display:guard-aggressive
|
||||
-> END
|
||||
|
||||
=== escalate_conflict ===
|
||||
# speaker:security_guard
|
||||
~ influence -= 40
|
||||
You've crossed the line! This is a lockdown!
|
||||
INTRUDER ALERT! INTRUDER ALERT!
|
||||
# display:guard-alarm
|
||||
-> END
|
||||
|
||||
=== back_down ===
|
||||
# speaker:security_guard
|
||||
{influence >= 15:
|
||||
~ influence -= 5
|
||||
Smart move. Now get out of here and don't come back.
|
||||
# display:guard-neutral
|
||||
}
|
||||
{influence < 15:
|
||||
Good thinking. But I've got a full description now.
|
||||
# display:guard-watchful
|
||||
}
|
||||
-> END
|
||||
|
||||
=== passing_through ===
|
||||
# speaker:security_guard
|
||||
Just passing through, huh? Keep it that way. No trouble.
|
||||
# display:guard-neutral
|
||||
-> hub
|
||||
|
||||
=== request_access ===
|
||||
# speaker:security_guard
|
||||
{influence >= 50:
|
||||
You? Access to that door? That's above your pay grade, friend.
|
||||
But I like the confidence. Not happening though.
|
||||
}
|
||||
{influence < 50:
|
||||
Access? Not without proper credentials. Nice try though.
|
||||
}
|
||||
# display:guard-skeptical
|
||||
-> hub
|
||||
1
scenarios/ink/security-guard.json
Normal file
1
scenarios/ink/security-guard.json
Normal file
File diff suppressed because one or more lines are too long
149
scenarios/npc-patrol-lockpick.json
Normal file
149
scenarios/npc-patrol-lockpick.json
Normal file
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"scenario_brief": "Test scenario for NPC patrol and lockpick detection",
|
||||
"endGoal": "Test NPC line-of-sight detection and lockpicking interruption",
|
||||
"startRoom": "patrol_corridor",
|
||||
|
||||
"player": {
|
||||
"id": "player",
|
||||
"displayName": "Agent 0x00",
|
||||
"spriteSheet": "hacker",
|
||||
"spriteTalk": "assets/characters/hacker-talk.png",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
}
|
||||
},
|
||||
|
||||
"rooms": {
|
||||
"patrol_corridor": {
|
||||
"type": "room_office",
|
||||
"connections": {
|
||||
"north": "secure_vault"
|
||||
},
|
||||
"npcs": [
|
||||
{
|
||||
"id": "patrol_with_face",
|
||||
"displayName": "Patrol + Face Player",
|
||||
"npcType": "person",
|
||||
"position": { "x": 5, "y": 5 },
|
||||
"spriteSheet": "hacker",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/security-guard.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"facePlayer": true,
|
||||
"facePlayerDistance": 96,
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"speed": 100,
|
||||
"changeDirectionInterval": 4000,
|
||||
"bounds": {
|
||||
"x": 128,
|
||||
"y": 128,
|
||||
"width": 128,
|
||||
"height": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
"los": {
|
||||
"enabled": true,
|
||||
"range": 250,
|
||||
"angle": 120,
|
||||
"visualize": true
|
||||
},
|
||||
"eventMappings": [
|
||||
{
|
||||
"eventPattern": "lockpick_used_in_view",
|
||||
"targetKnot": "on_lockpick_used",
|
||||
"conversationMode": "person-chat",
|
||||
"cooldown": 0
|
||||
}
|
||||
],
|
||||
"_comment": "Patrols normally, but stops to face player when within 3 tiles. Can see player within 250px at 120° FOV"
|
||||
},
|
||||
{
|
||||
"id": "security_guard",
|
||||
"displayName": "Security Guard",
|
||||
"npcType": "person",
|
||||
"position": { "x": 5, "y": 4 },
|
||||
"spriteSheet": "hacker-red",
|
||||
"spriteTalk": "assets/characters/hacker-red-talk.png",
|
||||
"spriteConfig": {
|
||||
"idleFrameStart": 20,
|
||||
"idleFrameEnd": 23
|
||||
},
|
||||
"storyPath": "scenarios/ink/security-guard.json",
|
||||
"currentKnot": "start",
|
||||
"behavior": {
|
||||
"patrol": {
|
||||
"enabled": true,
|
||||
"route": [
|
||||
{ "x": 2, "y": 3 },
|
||||
{ "x": 8, "y": 3 },
|
||||
{ "x": 8, "y": 6 },
|
||||
{ "x": 2, "y": 6 }
|
||||
],
|
||||
"speed": 40,
|
||||
"pauseTime": 10
|
||||
}
|
||||
},
|
||||
"los": {
|
||||
"enabled": true,
|
||||
"range": 300,
|
||||
"angle": 140,
|
||||
"visualize": true
|
||||
},
|
||||
"eventMappings": [
|
||||
{
|
||||
"eventPattern": "lockpick_used_in_view",
|
||||
"targetKnot": "on_lockpick_used",
|
||||
"conversationMode": "person-chat",
|
||||
"cooldown": 0
|
||||
}
|
||||
],
|
||||
"_comment": "Follows route patrol, detects player within 300px at 140° FOV"
|
||||
}
|
||||
],
|
||||
"objects": [
|
||||
{
|
||||
"type": "lockpick",
|
||||
"name": "Lock Pick Set",
|
||||
"takeable": true,
|
||||
"observations": "A complete lock picking set with various picks and tension wrenches"
|
||||
}
|
||||
]
|
||||
},
|
||||
"secure_vault": {
|
||||
"type": "room_office",
|
||||
"connections": {
|
||||
"south": "patrol_corridor"
|
||||
},
|
||||
"locked": true,
|
||||
"lockType": "key",
|
||||
"requires": "vault_key",
|
||||
"keyPins": [75, 30, 50, 100],
|
||||
"difficulty": "medium",
|
||||
"objects": [
|
||||
{
|
||||
"type": "notes",
|
||||
"name": "Classified Files",
|
||||
"takeable": true,
|
||||
"readable": true,
|
||||
"text": "These files contain sensitive security protocols and encryption keys. Do not leave unattended.",
|
||||
"observations": "Highly classified files stored in the secure vault"
|
||||
},
|
||||
{
|
||||
"type": "key",
|
||||
"name": "Vault Key",
|
||||
"takeable": true,
|
||||
"key_id": "vault_key",
|
||||
"keyPins": [75, 30, 50, 100],
|
||||
"observations": "A key that unlocks the secure vault door"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
116
test-los-visualization.html
Normal file
116
test-los-visualization.html
Normal file
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LOS Visualization Test</title>
|
||||
<link rel="stylesheet" href="css/main.css?v=1">
|
||||
<link rel="stylesheet" href="css/minigames-framework.css?v=1">
|
||||
<link rel="stylesheet" href="css/lockpicking.css?v=1">
|
||||
<link rel="stylesheet" href="css/password-minigame.css?v=1">
|
||||
<link rel="stylesheet" href="css/pin.css?v=1">
|
||||
<link rel="stylesheet" href="css/biometrics-minigame.css?v=1">
|
||||
<link rel="stylesheet" href="css/bluetooth-scanner.css?v=1">
|
||||
<link rel="stylesheet" href="css/phone-chat-minigame.css?v=1">
|
||||
<link rel="stylesheet" href="css/person-chat-minigame.css?v=1">
|
||||
<link rel="stylesheet" href="css/inventory.css?v=1">
|
||||
<link rel="stylesheet" href="css/notifications.css?v=1">
|
||||
<link rel="stylesheet" href="css/modals.css?v=1">
|
||||
<link rel="stylesheet" href="css/panels.css?v=1">
|
||||
<link rel="stylesheet" href="css/npc-interactions.css?v=1">
|
||||
<link rel="stylesheet" href="css/npc-barks.css?v=1">
|
||||
<link rel="stylesheet" href="css/text-file-minigame.css?v=1">
|
||||
<link rel="stylesheet" href="css/container-minigame.css?v=1">
|
||||
<link rel="stylesheet" href="css/dusting.css?v=1">
|
||||
<link rel="stylesheet" href="css/notes.css?v=1">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #1a1a1a;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
#debug-panel {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: #00ff00;
|
||||
padding: 10px;
|
||||
border: 2px solid #00ff00;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
z-index: 9999;
|
||||
max-width: 300px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
.debug-line {
|
||||
margin: 5px 0;
|
||||
padding: 2px;
|
||||
}
|
||||
button {
|
||||
background: #00ff00;
|
||||
color: #000;
|
||||
border: 2px solid #00ff00;
|
||||
padding: 8px 12px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
button:hover {
|
||||
background: #00cc00;
|
||||
border-color: #00cc00;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game-container"></div>
|
||||
|
||||
<div id="debug-panel">
|
||||
<div class="debug-line">📡 LOS Debug Panel</div>
|
||||
<div class="debug-line">
|
||||
<button onclick="window.enableLOS()">Enable LOS</button>
|
||||
<button onclick="window.disableLOS()">Disable LOS</button>
|
||||
</div>
|
||||
<div id="debug-status" class="debug-line">Loading...</div>
|
||||
</div>
|
||||
|
||||
<!-- Phaser library -->
|
||||
<script src="assets/vendor/phaser.js?v=1"></script>
|
||||
|
||||
<!-- EasyStar pathfinding -->
|
||||
<script src="assets/vendor/easystar.js?v=1"></script>
|
||||
|
||||
<!-- CyberChef -->
|
||||
<script src="assets/cyberchef/CyberChef.js?v=1"></script>
|
||||
|
||||
<!-- Ink -->
|
||||
<script src="assets/vendor/ink.js"></script>
|
||||
|
||||
<!-- Main game script (with los flag in URL) -->
|
||||
<script type="module">
|
||||
// Force LOS visualization on load
|
||||
window.location.search = '?los=1';
|
||||
import('./js/main.js?v=1').then(() => {
|
||||
console.log('✅ Game loaded');
|
||||
|
||||
// Update debug panel
|
||||
setInterval(() => {
|
||||
const status = document.getElementById('debug-status');
|
||||
if (window.npcManager) {
|
||||
const enabled = window.npcManager.losVisualizationEnabled;
|
||||
const vizCount = window.npcManager.losVisualizations?.size || 0;
|
||||
const npcCount = window.npcManager.npcs?.size || 0;
|
||||
status.innerHTML = `
|
||||
<div>Status: ${enabled ? '✅ ENABLED' : '❌ DISABLED'}</div>
|
||||
<div>NPCs: ${npcCount}</div>
|
||||
<div>Visualized: ${vizCount}</div>
|
||||
`;
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user