diff --git a/app/controllers/break_escape/games_controller.rb b/app/controllers/break_escape/games_controller.rb index 6ea54fd..a996b94 100644 --- a/app/controllers/break_escape/games_controller.rb +++ b/app/controllers/break_escape/games_controller.rb @@ -545,10 +545,23 @@ module BreakEscape new_npcs = npc_ids - @game.player_state['encounteredNPCs'] return if new_npcs.empty? + # Log detailed information about each new NPC encountered + new_npcs.each do |npc_id| + npc_data = room_data['npcs'].find { |npc| npc['id'] == npc_id } + if npc_data + display_name = npc_data['displayName'] || npc_id + npc_type = npc_data['npcType'] || 'unknown' + Rails.logger.info "[BreakEscape] 🎭 NPC ENCOUNTERED: #{display_name} (#{npc_id}) - Type: #{npc_type} - Room: #{room_id}" + else + Rails.logger.info "[BreakEscape] 🎭 NPC ENCOUNTERED: #{npc_id} - Room: #{room_id}" + end + end + @game.player_state['encounteredNPCs'] = (@game.player_state['encounteredNPCs'] + new_npcs).uniq @game.save! - Rails.logger.debug "[BreakEscape] Tracked NPC encounters: #{new_npcs.join(', ')}" + total_encountered = @game.player_state['encounteredNPCs'].length + Rails.logger.info "[BreakEscape] ✅ Tracked #{new_npcs.length} new NPC encounter(s) in room #{room_id}. Total NPCs encountered: #{total_encountered}" rescue => e Rails.logger.error "[BreakEscape] Error tracking NPC encounters: #{e.message}\n#{e.backtrace.first(5).join("\n")}" # Continue without tracking to avoid breaking room loading @@ -744,6 +757,17 @@ module BreakEscape end end end + + # Priority 3: Items held by NPCs in this room + room_data['npcs']&.each do |npc| + next unless npc['itemsHeld'].present? + + npc['itemsHeld'].each do |held_item| + if held_item['type'] == item_type && (held_item['key_id'] == item_id || held_item['id'] == item_id || held_item['name'] == item_name || held_item['name'] == item_id) + return { item: held_item, location: { type: 'npc', npc_id: npc['id'], room_id: room_id } } + end + end + end end nil diff --git a/app/models/break_escape/game.rb b/app/models/break_escape/game.rb index 468ea55..e5fac11 100644 --- a/app/models/break_escape/game.rb +++ b/app/models/break_escape/game.rb @@ -102,8 +102,24 @@ module BreakEscape # NPC tracking def encounter_npc!(npc_id) player_state['encounteredNPCs'] ||= [] - player_state['encounteredNPCs'] << npc_id unless player_state['encounteredNPCs'].include?(npc_id) - save! + unless player_state['encounteredNPCs'].include?(npc_id) + player_state['encounteredNPCs'] << npc_id + + # Try to get NPC display name from scenario for better logging + npc_display_name = npc_id + if scenario_data && scenario_data['rooms'] + scenario_data['rooms'].each do |_room_id, room_data| + npc_data = room_data['npcs']&.find { |npc| npc['id'] == npc_id } + if npc_data && npc_data['displayName'] + npc_display_name = npc_data['displayName'] + break + end + end + end + + Rails.logger.info "[BreakEscape] 🎭 NPC ENCOUNTERED (via encounter_npc!): #{npc_display_name} (#{npc_id})" + save! + end end # Global variables (synced with client) diff --git a/public/break_escape/assets/objects/id_badge.png b/public/break_escape/assets/objects/id_badge.png new file mode 100644 index 0000000..a30fd69 Binary files /dev/null and b/public/break_escape/assets/objects/id_badge.png differ diff --git a/public/break_escape/js/systems/npc-behavior.js b/public/break_escape/js/systems/npc-behavior.js index 7f58cff..f2baac0 100644 --- a/public/break_escape/js/systems/npc-behavior.js +++ b/public/break_escape/js/systems/npc-behavior.js @@ -306,8 +306,8 @@ class NPCBehavior { return; } - const roomWorldX = roomData.worldX || 0; - const roomWorldY = roomData.worldY || 0; + const roomWorldX = roomData.position?.x ?? roomData.worldX ?? 0; + const roomWorldY = roomData.position?.y ?? roomData.worldY ?? 0; const validWaypoints = []; diff --git a/public/break_escape/js/systems/npc-sprites.js b/public/break_escape/js/systems/npc-sprites.js index d6593d3..57e0166 100644 --- a/public/break_escape/js/systems/npc-sprites.js +++ b/public/break_escape/js/systems/npc-sprites.js @@ -112,8 +112,9 @@ export function calculateNPCWorldPosition(npc, roomData) { // Support grid coordinates (tile-based positioning) if (position.x !== undefined && position.y !== undefined) { - const roomWorldX = roomData.worldX || 0; - const roomWorldY = roomData.worldY || 0; + // Room position is stored in roomData.position (from room loading system) + const roomWorldX = roomData.position?.x ?? roomData.worldX ?? 0; + const roomWorldY = roomData.position?.y ?? roomData.worldY ?? 0; return { x: roomWorldX + (position.x * TILE_SIZE), @@ -935,8 +936,8 @@ function handleNPCCollision(npcSprite, otherNPC) { const roomData = window.rooms?.[roomId]; if (roomData) { - const roomWorldX = roomData.worldX || 0; - const roomWorldY = roomData.worldY || 0; + const roomWorldX = roomData.position?.x ?? roomData.worldX ?? 0; + const roomWorldY = roomData.position?.y ?? roomData.worldY ?? 0; // Check if avoidance waypoint is on walkable tile let isWalkable = true; @@ -1096,8 +1097,8 @@ function handleNPCPlayerCollision(npcSprite, player) { const roomData = window.rooms?.[roomId]; if (roomData) { - const roomWorldX = roomData.worldX || 0; - const roomWorldY = roomData.worldY || 0; + const roomWorldX = roomData.position?.x ?? roomData.worldX ?? 0; + const roomWorldY = roomData.position?.y ?? roomData.worldY ?? 0; // Check if avoidance waypoint is on walkable tile let isWalkable = true; diff --git a/scenarios/m01_first_contact/FIXES_APPLIED.md b/scenarios/m01_first_contact/FIXES_APPLIED.md index 4be4669..a96dfb7 100644 --- a/scenarios/m01_first_contact/FIXES_APPLIED.md +++ b/scenarios/m01_first_contact/FIXES_APPLIED.md @@ -90,8 +90,8 @@ **Problem:** NPCs' items used wrong `type` field values - #give_item tags reference `type`, not `id` **User Feedback:** -- "NPC sarah_martinez doesn't have visitor_badge. An NPC must hold the items they give away" -- "Still says sarah_martinez doesn't hold visitor_badge -- maybe it needs to be specified in the give by the type 'keycard'" +- "NPC sarah_martinez doesn't have id_badge. An NPC must hold the items they give away" +- "Still says sarah_martinez doesn't hold id_badge -- maybe it needs to be specified in the give by the type 'keycard'" **Root Cause:** Misunderstood how #give_item tags work: - Items should NOT have `id` fields @@ -101,7 +101,7 @@ **Fix Applied:** - Removed ALL `id` fields from NPC items - Changed item types to match #give_item tag parameters: - - Sarah: `visitor_badge` - changed type from "keycard" to "visitor_badge" + - Sarah: `id_badge` - changed type from "keycard" to "id_badge" - Kevin: `lockpick` - type already correct - Kevin: `rfid_cloner` - changed type from "tool" to "rfid_cloner" - Updated SCENARIO_JSON_FORMAT_GUIDE.md with correct pattern @@ -114,7 +114,7 @@ // ✅ CORRECT - Item type matches Ink tag parameter "itemsHeld": [ { - "type": "visitor_badge", // Matches #give_item:visitor_badge + "type": "id_badge", // Matches #give_item:id_badge "name": "Visitor Badge", "takeable": true } @@ -123,14 +123,14 @@ ```ink // In Ink script -#give_item:visitor_badge // References item type! +#give_item:id_badge // References item type! ``` ```json // ❌ INCORRECT - Don't add id field "itemsHeld": [ { - "id": "visitor_badge", // DON'T DO THIS + "id": "id_badge", // DON'T DO THIS "type": "keycard", // Wrong - doesn't match tag "name": "Visitor Badge" } diff --git a/scenarios/m01_first_contact/ink/m01_npc_sarah.ink b/scenarios/m01_first_contact/ink/m01_npc_sarah.ink index 5a77f98..5113fdb 100644 --- a/scenarios/m01_first_contact/ink/m01_npc_sarah.ink +++ b/scenarios/m01_first_contact/ink/m01_npc_sarah.ink @@ -48,7 +48,7 @@ VAR asked_about_kevin = false === receive_badge === ~ has_badge = true -#give_item:visitor_badge +#give_item:id_badge #complete_task:meet_reception Sarah: Here you go. This gets you into public areas. diff --git a/scenarios/m01_first_contact/ink/m01_npc_sarah.json b/scenarios/m01_first_contact/ink/m01_npc_sarah.json index 3785b44..dceea9d 100644 --- a/scenarios/m01_first_contact/ink/m01_npc_sarah.json +++ b/scenarios/m01_first_contact/ink/m01_npc_sarah.json @@ -1 +1 @@ -{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",{"VAR?":"met_sarah"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",true,"/ev",{"VAR=":"met_sarah","re":true},"ev",{"VAR?":"influence"},2,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Hi! You must be the IT contractor. I'm Sarah, the receptionist.","\n","^Sarah: Let me get you checked in.","\n",{"->":"first_checkin"},{"->":"start.5"},null]}],"nop","\n","ev",{"VAR?":"met_sarah"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Sarah: Hey, need anything else?","\n",{"->":"hub"},{"->":"start.11"},null]}],"nop","\n",null],"first_checkin":[["ev","str","^Thanks. I'm here to audit your network security","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Just point me to IT and I'll get started","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Oh good! Kevin mentioned you'd be coming.","\n","^Sarah: Let me print your visitor badge.","\n",{"->":"receive_badge"},null],"c-1":["\n","^Sarah: Sure thing. Let me get your badge first.","\n",{"->":"receive_badge"},null]}],null],"receive_badge":["ev",true,"/ev",{"VAR=":"has_badge","re":true},"#","^give_item:visitor_badge","/#","#","^complete_task:meet_reception","/#","^Sarah: Here you go. This gets you into public areas.","\n","^Sarah: Restricted areas need keycard access or you'll need to ask Kevin.","\n",{"->":"hub"},null],"hub":[["ev","str","^Where can I find Kevin?","/str",{"VAR?":"asked_about_kevin"},"!","/ev",{"*":".^.c-0","flg":5},"ev","str","^Can you tell me about the office layout?","/str",{"VAR?":"asked_about_office"},"!","/ev",{"*":".^.c-1","flg":5},"ev","str","^Anyone working late I should know about?","/str",{"VAR?":"asked_about_derek"},"!",{"VAR?":"influence"},3,">=","&&","/ev",{"*":".^.c-2","flg":5},"ev","str","^Thanks, I'll get started","/str","/ev",{"*":".^.c-3","flg":4},{"c-0":["\n",{"->":"ask_kevin_location"},null],"c-1":["\n",{"->":"ask_office_layout"},null],"c-2":["\n",{"->":"ask_late_workers"},null],"c-3":["\n","#","^exit_conversation","/#","^Sarah: Good luck with the audit!","\n",{"->":"hub"},null]}],null],"ask_kevin_location":[["ev",true,"/ev",{"VAR=":"asked_about_kevin","re":true},"ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Kevin's desk is in the main office area—can't miss it. Covered in monitors and coffee cups.","\n","^Sarah: He's usually there this time of day.","\n","ev","str","^What's he like?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Thanks","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"kevin_personality"},null],"c-1":["\n",{"->":"hub"},null]}],null],"kevin_personality":["ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Super helpful, kind of overworked. The company relies on him way too much.","\n","^Sarah: He'll appreciate having someone competent help out.","\n",{"->":"hub"},null],"ask_office_layout":[["ev",true,"/ev",{"VAR=":"asked_about_office","re":true},"ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Main office is through there—hot-desking setup. Conference room on the west side, break room to the east.","\n","^Sarah: Server room is behind main office, but you'll need Kevin's access for that.","\n","ev","str","^What about executive offices?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Got it, thanks","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"ask_executive_offices"},null],"c-1":["\n",{"->":"hub"},null]}],null],"ask_executive_offices":["ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Derek's office is off the main area—he's our Senior Marketing Manager. Usually locks his door when he's out.","\n","^Sarah: Most people just have desk space, but Derek got an office because of client confidentiality stuff.","\n",{"->":"hub"},null],"ask_late_workers":[["ev",true,"/ev",{"VAR=":"asked_about_derek","re":true},"ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Derek's usually here late. Like, really late. Sometimes I leave at 6 and he's still working.","\n","^Sarah: He says it's because of client timezones, but...","\n","ev","str","^But what?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Dedication, I guess","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"derek_suspicion"},null],"c-1":["\n",{"->":"hub"},null]}],null],"derek_suspicion":[["ev",{"VAR?":"influence"},2,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: I don't know. It just seems weird, you know? He's marketing, not IT.","\n","^Sarah: And I've seen him in the server room a couple times. Told me he was checking on campaign servers.","\n","ev","str","^That does seem odd","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Maybe he's just thorough","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Right? But I'm just the receptionist. What do I know?","\n",{"->":"hub"},null],"c-1":["\n","^Sarah: Maybe. Anyway, Kevin would know more about the technical stuff.","\n",{"->":"hub"},null]}],null],"global decl":["ev",0,{"VAR=":"influence"},false,{"VAR=":"met_sarah"},false,{"VAR=":"has_badge"},false,{"VAR=":"asked_about_derek"},false,{"VAR=":"asked_about_office"},false,{"VAR=":"asked_about_kevin"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"start":["ev",{"VAR?":"met_sarah"},"!","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",true,"/ev",{"VAR=":"met_sarah","re":true},"ev",{"VAR?":"influence"},2,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Hi! You must be the IT contractor. I'm Sarah, the receptionist.","\n","^Sarah: Let me get you checked in.","\n",{"->":"first_checkin"},{"->":"start.5"},null]}],"nop","\n","ev",{"VAR?":"met_sarah"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Sarah: Hey, need anything else?","\n",{"->":"hub"},{"->":"start.11"},null]}],"nop","\n",null],"first_checkin":[["ev","str","^Thanks. I'm here to audit your network security","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Just point me to IT and I'll get started","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Oh good! Kevin mentioned you'd be coming.","\n","^Sarah: Let me print your visitor badge.","\n",{"->":"receive_badge"},null],"c-1":["\n","^Sarah: Sure thing. Let me get your badge first.","\n",{"->":"receive_badge"},null]}],null],"receive_badge":["ev",true,"/ev",{"VAR=":"has_badge","re":true},"#","^give_item:id_badge","/#","#","^complete_task:meet_reception","/#","^Sarah: Here you go. This gets you into public areas.","\n","^Sarah: Restricted areas need keycard access or you'll need to ask Kevin.","\n",{"->":"hub"},null],"hub":[["ev","str","^Where can I find Kevin?","/str",{"VAR?":"asked_about_kevin"},"!","/ev",{"*":".^.c-0","flg":5},"ev","str","^Can you tell me about the office layout?","/str",{"VAR?":"asked_about_office"},"!","/ev",{"*":".^.c-1","flg":5},"ev","str","^Anyone working late I should know about?","/str",{"VAR?":"asked_about_derek"},"!",{"VAR?":"influence"},3,">=","&&","/ev",{"*":".^.c-2","flg":5},"ev","str","^Thanks, I'll get started","/str","/ev",{"*":".^.c-3","flg":4},{"c-0":["\n",{"->":"ask_kevin_location"},null],"c-1":["\n",{"->":"ask_office_layout"},null],"c-2":["\n",{"->":"ask_late_workers"},null],"c-3":["\n","#","^exit_conversation","/#","^Sarah: Good luck with the audit!","\n",{"->":"hub"},null]}],null],"ask_kevin_location":[["ev",true,"/ev",{"VAR=":"asked_about_kevin","re":true},"ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Kevin's desk is in the main office area—can't miss it. Covered in monitors and coffee cups.","\n","^Sarah: He's usually there this time of day.","\n","ev","str","^What's he like?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Thanks","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"kevin_personality"},null],"c-1":["\n",{"->":"hub"},null]}],null],"kevin_personality":["ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Super helpful, kind of overworked. The company relies on him way too much.","\n","^Sarah: He'll appreciate having someone competent help out.","\n",{"->":"hub"},null],"ask_office_layout":[["ev",true,"/ev",{"VAR=":"asked_about_office","re":true},"ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Main office is through there—hot-desking setup. Conference room on the west side, break room to the east.","\n","^Sarah: Server room is behind main office, but you'll need Kevin's access for that.","\n","ev","str","^What about executive offices?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Got it, thanks","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"ask_executive_offices"},null],"c-1":["\n",{"->":"hub"},null]}],null],"ask_executive_offices":["ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Derek's office is off the main area—he's our Senior Marketing Manager. Usually locks his door when he's out.","\n","^Sarah: Most people just have desk space, but Derek got an office because of client confidentiality stuff.","\n",{"->":"hub"},null],"ask_late_workers":[["ev",true,"/ev",{"VAR=":"asked_about_derek","re":true},"ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Derek's usually here late. Like, really late. Sometimes I leave at 6 and he's still working.","\n","^Sarah: He says it's because of client timezones, but...","\n","ev","str","^But what?","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Dedication, I guess","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n",{"->":"derek_suspicion"},null],"c-1":["\n",{"->":"hub"},null]}],null],"derek_suspicion":[["ev",{"VAR?":"influence"},2,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: I don't know. It just seems weird, you know? He's marketing, not IT.","\n","^Sarah: And I've seen him in the server room a couple times. Told me he was checking on campaign servers.","\n","ev","str","^That does seem odd","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Maybe he's just thorough","/str","/ev",{"*":".^.c-1","flg":4},{"c-0":["\n","ev",{"VAR?":"influence"},1,"+",{"VAR=":"influence","re":true},"/ev","^Sarah: Right? But I'm just the receptionist. What do I know?","\n",{"->":"hub"},null],"c-1":["\n","^Sarah: Maybe. Anyway, Kevin would know more about the technical stuff.","\n",{"->":"hub"},null]}],null],"global decl":["ev",0,{"VAR=":"influence"},false,{"VAR=":"met_sarah"},false,{"VAR=":"has_badge"},false,{"VAR=":"asked_about_derek"},false,{"VAR=":"asked_about_office"},false,{"VAR=":"asked_about_kevin"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/scenarios/m01_first_contact/scenario.json.erb b/scenarios/m01_first_contact/scenario.json.erb index fe915fd..8959233 100644 --- a/scenarios/m01_first_contact/scenario.json.erb +++ b/scenarios/m01_first_contact/scenario.json.erb @@ -57,7 +57,7 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A "id": "briefing_cutscene", "displayName": "Agent 0x99", "npcType": "person", - "position": { "x": 5, "y": 5 }, + "position": { "x": 500, "y": 500 }, "spriteSheet": "hacker", "spriteConfig": { "idleFrameStart": 20, @@ -75,7 +75,7 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A "id": "sarah_martinez", "displayName": "Sarah Martinez", "npcType": "person", - "position": { "x": 3, "y": 4 }, + "position": { "x": 4, "y": 1.5 }, "spriteSheet": "hacker-red", "spriteTalk": "assets/characters/hacker-red-talk.png", "spriteConfig": { @@ -86,7 +86,7 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A "currentKnot": "start", "itemsHeld": [ { - "type": "visitor_badge", + "type": "id_badge", "name": "Visitor Badge", "takeable": true, "observations": "Temporary visitor badge for office access" @@ -121,7 +121,7 @@ password_hints = "Common passwords: Marketing123, Campaign2024, Viral_Dynamics_A "id": "kevin_park", "displayName": "Kevin Park", "npcType": "person", - "position": { "x": 10, "y": 7 }, + "position": { "x": 8, "y": 7 }, "spriteSheet": "hacker", "spriteConfig": { "idleFrameStart": 20,