Enhance office room layout and interactions

- Updated room_office2.tmj to adjust object positions and dimensions for better alignment.
- Added new objects and refined existing ones to improve gameplay experience.
- Modified TiledItemPool in rooms.js to support regular and table items separately, ensuring proper item matching and prioritization.
- Improved interaction handling for swivel chairs, allowing them to be kicked and spin upon collision with walls.
- Updated object physics to handle swivel chair collisions dynamically, enhancing realism.
- Revised scenario details in cybok_heist.json for clarity and improved narrative flow.
This commit is contained in:
Z. Cliffe Schreuders
2025-10-24 09:59:11 +01:00
parent a2a9359a02
commit 6f4cfd43c8
8 changed files with 758 additions and 225 deletions

View File

@@ -81,6 +81,18 @@
"width":78,
"x":118,
"y":146.666666666667
},
{
"gid":541,
"height":41,
"id":98,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":253.333333333333,
"y":176
}],
"opacity":1,
"type":"objectgroup",
@@ -493,7 +505,7 @@
"y":0
}],
"nextlayerid":11,
"nextobjectid":98,
"nextobjectid":99,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.2",

View File

@@ -89,6 +89,18 @@
"width":78,
"x":118,
"y":146.666666666667
},
{
"gid":541,
"height":41,
"id":98,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":253.333333333333,
"y":176
}],
"opacity":1,
"type":"objectgroup",
@@ -501,7 +513,7 @@
"y":0
}],
"nextlayerid":11,
"nextobjectid":98,
"nextobjectid":99,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.2",

View File

@@ -47,12 +47,12 @@
{
"data":[0, 101, 0, 0, 0, 0, 0, 0, 101, 0,
0, 107, 0, 0, 0, 0, 0, 0, 107, 0,
420, 0, 0, 0, 0, 0, 0, 0, 0, 420,
444, 0, 0, 0, 0, 0, 0, 0, 0, 444,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
420, 0, 0, 0, 0, 0, 0, 0, 0, 420,
444, 0, 0, 0, 0, 0, 0, 0, 0, 444,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
@@ -210,8 +210,8 @@
"type":"",
"visible":true,
"width":16,
"x":92,
"y":121
"x":234.936288088643,
"y":179.725761772853
},
{
"gid":198,
@@ -246,8 +246,8 @@
"type":"",
"visible":true,
"width":9,
"x":254.5,
"y":171.5
"x":246.005078485688,
"y":171.869344413666
},
{
@@ -283,8 +283,8 @@
"type":"",
"visible":true,
"width":17,
"x":209.954755309326,
"y":170.630655586334
"x":208.108033240997,
"y":172.477377654663
},
{
"gid":205,
@@ -319,8 +319,8 @@
"type":"",
"visible":true,
"width":15,
"x":241.5,
"y":132.5
"x":244.454755309326,
"y":132.869344413666
},
{
"gid":219,
@@ -392,8 +392,8 @@
"type":"",
"visible":true,
"width":24,
"x":258,
"y":180.5
"x":252.82917820868,
"y":179.391966759003
},
{
"gid":338,
@@ -487,8 +487,8 @@
"type":"",
"visible":true,
"width":16,
"x":217.25,
"y":183
"x":218.358033240997,
"y":183.738688827331
}],
"opacity":1,
"type":"objectgroup",
@@ -562,64 +562,77 @@
"y":66
},
{
"gid":412,
"gid":404,
"height":32,
"id":144,
"id":152,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":209.669436749769,
"y":216.923361034164
},
{
"gid":411,
"height":32,
"id":145,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":29.0600184672207,
"y":170.755309325946
"x":99.2354570637119,
"y":228.373037857802
},
{
"gid":405,
"height":32,
"id":146,
"id":153,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":242.54108956602,
"x":101.082179132041,
"y":114.245614035088
},
{
"gid":403,
"height":32,
"id":154,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":181.599261311173,
"y":91.7156048014774
},
{
"gid":407,
"height":32,
"id":155,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":211.146814404432,
"y":223.940904893813
},
{
"gid":409,
"height":32,
"id":157,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":217.795013850416,
"y":172.602031394275
},
{
"gid":412,
"height":32,
"id":158,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":27.9519852262234,
"y":172.971375807941
},
{
"gid":402,
"height":32,
"id":148,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":181.229916897507,
"y":79.8965835641736
},
{
"gid":400,
"height":32,
"id":149,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":75.5974145891043,
"y":222.46352723915
}],
"opacity":1,
"type":"objectgroup",
@@ -776,6 +789,30 @@
"width":26,
"x":34.2760849492151,
"y":216.662049861496
},
{
"gid":261,
"height":17,
"id":160,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":23,
"x":114.946445060018,
"y":66.617728531856
},
{
"gid":264,
"height":17,
"id":161,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":22,
"x":184.752539242844,
"y":66.9870729455217
}],
"opacity":1,
"type":"objectgroup",
@@ -795,7 +832,7 @@
"y":0
}],
"nextlayerid":11,
"nextobjectid":151,
"nextobjectid":162,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.2",
@@ -892,7 +929,7 @@
"margin":0,
"name":"objects",
"spacing":0,
"tilecount":278,
"tilecount":302,
"tileheight":359,
"tiles":[
{
@@ -2589,12 +2626,159 @@
"image":"..\/objects\/smartscreen.png",
"imageheight":34,
"imagewidth":48
},
{
"id":297,
"image":"..\/objects\/chair-white-1.aseprite",
"imageheight":32,
"imagewidth":32
},
{
"id":298,
"image":"..\/objects\/fingerprint_kit.png",
"imageheight":24,
"imagewidth":10
},
{
"id":299,
"image":"..\/objects\/fingerprint_small.png",
"imageheight":24,
"imagewidth":18
},
{
"id":300,
"image":"..\/objects\/key-ring.png",
"imageheight":27,
"imagewidth":18
},
{
"id":301,
"image":"..\/objects\/notes.png",
"imageheight":14,
"imagewidth":27
},
{
"id":302,
"image":"..\/objects\/notes5.png",
"imageheight":32,
"imagewidth":25
},
{
"id":303,
"image":"..\/objects\/pc.png",
"imageheight":24,
"imagewidth":36
},
{
"id":304,
"image":"..\/objects\/pin-cracker-large.png",
"imageheight":32,
"imagewidth":32
},
{
"id":305,
"image":"..\/objects\/pin-cracker.png",
"imageheight":13,
"imagewidth":12
},
{
"id":306,
"image":"..\/objects\/plant-large11-top-ani1.png",
"imageheight":75,
"imagewidth":64
},
{
"id":307,
"image":"..\/objects\/plant-large11-top-ani2.png",
"imageheight":75,
"imagewidth":64
},
{
"id":308,
"image":"..\/objects\/plant-large11-top-ani3.png",
"imageheight":75,
"imagewidth":64
},
{
"id":309,
"image":"..\/objects\/plant-large11-top-ani4.png",
"imageheight":75,
"imagewidth":64
},
{
"id":310,
"image":"..\/objects\/plant-large12-top-ani1.png",
"imageheight":75,
"imagewidth":64
},
{
"id":311,
"image":"..\/objects\/plant-large12-top-ani2.png",
"imageheight":75,
"imagewidth":64
},
{
"id":312,
"image":"..\/objects\/plant-large12-top-ani3.png",
"imageheight":75,
"imagewidth":64
},
{
"id":313,
"image":"..\/objects\/plant-large12-top-ani4.png",
"imageheight":75,
"imagewidth":64
},
{
"id":314,
"image":"..\/objects\/plant-large12-top-ani5.png",
"imageheight":75,
"imagewidth":64
},
{
"id":315,
"image":"..\/objects\/plant-large13-top-ani1.png",
"imageheight":88,
"imagewidth":64
},
{
"id":316,
"image":"..\/objects\/plant-large13-top-ani2.png",
"imageheight":88,
"imagewidth":64
},
{
"id":317,
"image":"..\/objects\/plant-large13-top-ani3.png",
"imageheight":88,
"imagewidth":64
},
{
"id":318,
"image":"..\/objects\/plant-large13-top-ani4.png",
"imageheight":88,
"imagewidth":64
},
{
"id":319,
"image":"..\/objects\/text_file.png",
"imageheight":16,
"imagewidth":16
},
{
"id":320,
"image":"..\/objects\/workstation.png",
"imageheight":18,
"imagewidth":24
}],
"tilewidth":221
},
{
"columns":6,
"firstgid":420,
"firstgid":444,
"image":"..\/tiles\/door_side_sheet_32.png",
"imageheight":32,
"imagewidth":192,
@@ -2607,7 +2791,7 @@
},
{
"columns":10,
"firstgid":426,
"firstgid":450,
"image":"..\/tiles\/rooms\/room14.png",
"imageheight":320,
"imagewidth":320,
@@ -2620,7 +2804,7 @@
},
{
"columns":10,
"firstgid":526,
"firstgid":550,
"image":"..\/tiles\/rooms\/room18.png",
"imageheight":320,
"imagewidth":320,

View File

@@ -55,12 +55,12 @@
{
"data":[0, 101, 0, 0, 0, 0, 0, 0, 101, 0,
0, 107, 0, 0, 0, 0, 0, 0, 107, 0,
420, 0, 0, 0, 0, 0, 0, 0, 0, 420,
444, 0, 0, 0, 0, 0, 0, 0, 0, 444,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
420, 0, 0, 0, 0, 0, 0, 0, 0, 420,
444, 0, 0, 0, 0, 0, 0, 0, 0, 444,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
@@ -218,8 +218,8 @@
"type":"",
"visible":true,
"width":16,
"x":92,
"y":121
"x":234.936288088643,
"y":179.725761772853
},
{
"gid":198,
@@ -254,8 +254,8 @@
"type":"",
"visible":true,
"width":9,
"x":254.5,
"y":171.5
"x":246.005078485688,
"y":171.869344413666
},
{
@@ -291,8 +291,8 @@
"type":"",
"visible":true,
"width":17,
"x":209.954755309326,
"y":170.630655586334
"x":208.108033240997,
"y":172.477377654663
},
{
"gid":205,
@@ -327,8 +327,8 @@
"type":"",
"visible":true,
"width":15,
"x":241.5,
"y":132.5
"x":244.454755309326,
"y":132.869344413666
},
{
"gid":219,
@@ -400,8 +400,8 @@
"type":"",
"visible":true,
"width":24,
"x":258,
"y":180.5
"x":252.82917820868,
"y":179.391966759003
},
{
"gid":338,
@@ -495,8 +495,8 @@
"type":"",
"visible":true,
"width":16,
"x":217.25,
"y":183
"x":218.358033240997,
"y":183.738688827331
}],
"opacity":1,
"type":"objectgroup",
@@ -570,64 +570,77 @@
"y":66
},
{
"gid":412,
"gid":404,
"height":32,
"id":144,
"id":152,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":209.669436749769,
"y":216.923361034164
},
{
"gid":411,
"height":32,
"id":145,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":29.0600184672207,
"y":170.755309325946
"x":99.2354570637119,
"y":228.373037857802
},
{
"gid":405,
"height":32,
"id":146,
"id":153,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":242.54108956602,
"x":101.082179132041,
"y":114.245614035088
},
{
"gid":403,
"height":32,
"id":154,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":181.599261311173,
"y":91.7156048014774
},
{
"gid":407,
"height":32,
"id":155,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":211.146814404432,
"y":223.940904893813
},
{
"gid":409,
"height":32,
"id":157,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":217.795013850416,
"y":172.602031394275
},
{
"gid":412,
"height":32,
"id":158,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":27.9519852262234,
"y":172.971375807941
},
{
"gid":402,
"height":32,
"id":148,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":181.229916897507,
"y":79.8965835641736
},
{
"gid":400,
"height":32,
"id":149,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":75.5974145891043,
"y":222.46352723915
}],
"opacity":1,
"type":"objectgroup",
@@ -784,6 +797,30 @@
"width":26,
"x":34.2760849492151,
"y":216.662049861496
},
{
"gid":261,
"height":17,
"id":160,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":23,
"x":114.946445060018,
"y":66.617728531856
},
{
"gid":264,
"height":17,
"id":161,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":22,
"x":184.752539242844,
"y":66.9870729455217
}],
"opacity":1,
"type":"objectgroup",
@@ -803,7 +840,7 @@
"y":0
}],
"nextlayerid":11,
"nextobjectid":151,
"nextobjectid":162,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.2",
@@ -842,7 +879,7 @@
"margin":0,
"name":"objects",
"spacing":0,
"tilecount":278,
"tilecount":302,
"tileheight":359,
"tiles":[
{
@@ -2539,19 +2576,166 @@
"image":"..\/objects\/smartscreen.png",
"imageheight":34,
"imagewidth":48
},
{
"id":297,
"image":"..\/objects\/chair-white-1.aseprite",
"imageheight":32,
"imagewidth":32
},
{
"id":298,
"image":"..\/objects\/fingerprint_kit.png",
"imageheight":24,
"imagewidth":10
},
{
"id":299,
"image":"..\/objects\/fingerprint_small.png",
"imageheight":24,
"imagewidth":18
},
{
"id":300,
"image":"..\/objects\/key-ring.png",
"imageheight":27,
"imagewidth":18
},
{
"id":301,
"image":"..\/objects\/notes.png",
"imageheight":14,
"imagewidth":27
},
{
"id":302,
"image":"..\/objects\/notes5.png",
"imageheight":32,
"imagewidth":25
},
{
"id":303,
"image":"..\/objects\/pc.png",
"imageheight":24,
"imagewidth":36
},
{
"id":304,
"image":"..\/objects\/pin-cracker-large.png",
"imageheight":32,
"imagewidth":32
},
{
"id":305,
"image":"..\/objects\/pin-cracker.png",
"imageheight":13,
"imagewidth":12
},
{
"id":306,
"image":"..\/objects\/plant-large11-top-ani1.png",
"imageheight":75,
"imagewidth":64
},
{
"id":307,
"image":"..\/objects\/plant-large11-top-ani2.png",
"imageheight":75,
"imagewidth":64
},
{
"id":308,
"image":"..\/objects\/plant-large11-top-ani3.png",
"imageheight":75,
"imagewidth":64
},
{
"id":309,
"image":"..\/objects\/plant-large11-top-ani4.png",
"imageheight":75,
"imagewidth":64
},
{
"id":310,
"image":"..\/objects\/plant-large12-top-ani1.png",
"imageheight":75,
"imagewidth":64
},
{
"id":311,
"image":"..\/objects\/plant-large12-top-ani2.png",
"imageheight":75,
"imagewidth":64
},
{
"id":312,
"image":"..\/objects\/plant-large12-top-ani3.png",
"imageheight":75,
"imagewidth":64
},
{
"id":313,
"image":"..\/objects\/plant-large12-top-ani4.png",
"imageheight":75,
"imagewidth":64
},
{
"id":314,
"image":"..\/objects\/plant-large12-top-ani5.png",
"imageheight":75,
"imagewidth":64
},
{
"id":315,
"image":"..\/objects\/plant-large13-top-ani1.png",
"imageheight":88,
"imagewidth":64
},
{
"id":316,
"image":"..\/objects\/plant-large13-top-ani2.png",
"imageheight":88,
"imagewidth":64
},
{
"id":317,
"image":"..\/objects\/plant-large13-top-ani3.png",
"imageheight":88,
"imagewidth":64
},
{
"id":318,
"image":"..\/objects\/plant-large13-top-ani4.png",
"imageheight":88,
"imagewidth":64
},
{
"id":319,
"image":"..\/objects\/text_file.png",
"imageheight":16,
"imagewidth":16
},
{
"id":320,
"image":"..\/objects\/workstation.png",
"imageheight":18,
"imagewidth":24
}],
"tilewidth":221
},
{
"firstgid":420,
"firstgid":444,
"source":"..\/..\/..\/assets\/rooms\/door_side_sheet_32.tsx"
},
{
"firstgid":426,
"firstgid":450,
"source":"room14.tsx"
},
{
"firstgid":526,
"firstgid":550,
"source":"room18.tsx"
}],
"tilewidth":32,

View File

@@ -103,11 +103,12 @@ let gameRef = null;
*/
class TiledItemPool {
constructor(objectsByLayer, map) {
this.itemsByType = {}; // Regular items indexed by type
this.itemsByType = {}; // Regular items (non-table) indexed by type
this.tableItemsByType = {}; // Regular table items indexed by type
this.conditionalItemsByType = {}; // Conditional items indexed by type
this.conditionalTableItemsByType = {}; // Conditional table items indexed by type
this.reserved = new Set(); // Track reserved items to prevent reuse
this.map = map; // Store map for tileset lookups
this.reserved = new Set(); // Track reserved items to prevent reuse
this.map = map; // Store map for tileset lookups
this.populateFromLayers(objectsByLayer);
}
@@ -147,9 +148,16 @@ class TiledItemPool {
/**
* Populate pool from Tiled object layers
* Indexes items by their base type for efficient lookup
*
* Priority order for matching:
* 1. Regular items (non-table)
* 2. Regular table items
* 3. Conditional items (non-table)
* 4. Conditional table items
*/
populateFromLayers(objectsByLayer) {
this.itemsByType = this.indexByType(objectsByLayer.items || []);
this.tableItemsByType = this.indexByType(objectsByLayer.table_items || []);
this.conditionalItemsByType = this.indexByType(objectsByLayer.conditional_items || []);
this.conditionalTableItemsByType = this.indexByType(objectsByLayer.conditional_table_items || []);
}
@@ -175,15 +183,28 @@ class TiledItemPool {
/**
* Find best matching item for a scenario object
* Searches in priority order: regular → conditional → conditional table items
* Skips reserved items to prevent reuse
* Searches in strict priority order:
* 1. Regular items (items layer)
* 2. Regular table items (table_items layer)
* 3. Conditional items (conditional_items layer)
* 4. Conditional table items (conditional_table_items layer)
*
* This ensures unconditional items are ALWAYS used before conditional items.
* For multiple requests of the same type, exhausts each layer before moving to next.
*
* Skips reserved items to prevent reuse.
* Returns the matched item or null if no match found
*/
findMatchFor(scenarioObj) {
const searchType = scenarioObj.type;
// Search priority: regular items first, then conditional, then table items
const searchOrder = [this.itemsByType, this.conditionalItemsByType, this.conditionalTableItemsByType];
// Search priority: unconditional layers first, then conditional layers
const searchOrder = [
this.itemsByType, // Regular items
this.tableItemsByType, // Regular table items (NEW - CRITICAL FIX)
this.conditionalItemsByType, // Conditional items
this.conditionalTableItemsByType // Conditional table items
];
for (const indexedItems of searchOrder) {
const candidates = indexedItems[searchType] || [];
@@ -217,10 +238,12 @@ class TiledItemPool {
}
/**
* Get all unreserved items across all layers
* Used to process background decoration items
* NOTE: Only returns regular items, NOT conditional items
* Conditional items should ONLY be created when explicitly requested by scenario
* Get all unreserved items across all regular (unconditional) layers
* Used to process background decoration items that weren't used by scenario
*
* NOTE: Returns BOTH regular items AND regular table items, but NOT conditional items.
* Conditional items should ONLY be created when explicitly requested by scenario.
* This ensures conditional items stay hidden until the scenario needs them.
*/
getUnreservedItems() {
const unreserved = [];
@@ -235,8 +258,10 @@ class TiledItemPool {
});
};
// Only process regular items - conditional items should NOT be auto-created
// Process both regular items and regular table items
// Exclude conditional items - they should only appear when scenario explicitly requests them
collectUnreserved(this.itemsByType);
collectUnreserved(this.tableItemsByType);
return unreserved;
}
@@ -904,44 +929,9 @@ export function createRoom(roomId, roomData, position) {
tableGroups.push(group);
});
// Process table items and assign them to groups
if (objectsByLayer.table_items) {
objectsByLayer.table_items.forEach(obj => {
const processedObj = processObject(obj, position, roomId, 'table_item', map);
if (processedObj) {
// Find the closest table
const closestTable = findClosestTable(processedObj.sprite, tableObjects);
if (closestTable) {
const group = tableGroups.find(g => g.table === closestTable);
if (group) {
group.items.push(processedObj);
}
}
}
});
}
// Conditional table items are now handled by scenario matching system
// Set z-index ordering for each group (table first, then items from north to south)
tableGroups.forEach(group => {
// Table is already at the correct depth
console.log(`Setting up group for table at depth ${group.baseDepth}`);
// Sort items from north to south (lower Y values first)
group.items.sort((a, b) => a.sprite.y - b.sprite.y);
// Set items to share the same base depth as the table
group.items.forEach((item, index) => {
// Table items don't need elevation - they're grouped with the table
const itemDepth = group.baseDepth + (index + 1) * 0.01; // Slight offset for proper ordering
item.sprite.setDepth(itemDepth);
// No elevation for table items
item.sprite.elevation = 0;
console.log(`Set item ${item.sprite.name} to depth ${itemDepth} (north to south order, no elevation)`);
});
});
// NOTE: Table items (both regular and conditional) are now processed through the item pool
// in processScenarioObjectsWithConditionalMatching(). They will be handled there
// with proper priority ordering (regular table items before conditional ones).
// Build a set of inventory items that should NOT be created as sprites
const inventoryItemTypes = new Set();
@@ -957,8 +947,9 @@ export function createRoom(roomId, roomData, position) {
// Process scenario objects with conditional item matching first
const usedItems = processScenarioObjectsWithConditionalMatching(roomId, position, objectsByLayer, map);
// Process all non-conditional items (chairs, plants, etc.)
// Give them default properties if not used in scenario
// Process all non-conditional items (chairs, plants, lamps, PCs, etc.)
// These should ALWAYS be visible (not conditional)
// They get default properties if not customized by scenario
if (objectsByLayer.items) {
objectsByLayer.items.forEach(obj => {
const imageName = getImageNameFromObjectWithMap(obj, map);
@@ -975,12 +966,19 @@ export function createRoom(roomId, roomData, position) {
return;
}
// Skip if this base type was used by scenario objects
if (imageName && (usedItems.has(imageName) || usedItems.has(baseType))) {
console.log(`Skipping regular item ${imageName} (baseType: ${baseType}) - used by scenario object`);
// Skip if this exact item was used by scenario objects
// BUT: Create it anyway if we haven't used ALL items of this type
if (imageName && usedItems.has(imageName)) {
console.log(`Skipping regular item ${imageName} (exact item used by scenario)`);
return;
}
processObject(obj, position, roomId, 'item', map);
// Process the item and store it
const result = processObject(obj, position, roomId, 'item', map);
if (result && result.sprite) {
// Store unconditional items in the objects collection so they're revealed
rooms[roomId].objects[result.sprite.objectId] = result.sprite;
}
});
}
@@ -1021,22 +1019,28 @@ export function createRoom(roomId, roomData, position) {
usedItem = itemPool.findMatchFor(scenarioObj);
if (usedItem) {
// Check if this is a table item by searching which layer it came from
// Check which layer this item came from to determine if it's a table item
const imageName = itemPool.getImageNameFromObject(usedItem);
const baseType = itemPool.extractBaseTypeFromImageName(imageName);
// Determine if item came from table items layer
if (itemPool.conditionalTableItemsByType[baseType] &&
itemPool.conditionalTableItemsByType[baseType].includes(usedItem)) {
// Determine source layer and log appropriately
let sourceLayer = 'unknown';
if (itemPool.itemsByType[baseType] && itemPool.itemsByType[baseType].includes(usedItem)) {
sourceLayer = 'items (regular)';
isTableItem = false;
} else if (itemPool.tableItemsByType[baseType] && itemPool.tableItemsByType[baseType].includes(usedItem)) {
sourceLayer = 'table_items (regular)';
isTableItem = true;
} else if (itemPool.conditionalItemsByType[baseType] && itemPool.conditionalItemsByType[baseType].includes(usedItem)) {
sourceLayer = 'conditional_items';
isTableItem = false;
} else if (itemPool.conditionalTableItemsByType[baseType] && itemPool.conditionalTableItemsByType[baseType].includes(usedItem)) {
sourceLayer = 'conditional_table_items';
isTableItem = true;
console.log(`Using conditional table item for ${objType}`);
} else if (itemPool.conditionalItemsByType[baseType] &&
itemPool.conditionalItemsByType[baseType].includes(usedItem)) {
console.log(`Using conditional item for ${objType}`);
} else {
console.log(`Using regular item for ${objType}`);
}
console.log(`Using ${objType} from ${sourceLayer} layer`);
// Create sprite from matched item
sprite = createSpriteFromMatch(usedItem, scenarioObj, position, roomId, index, map);
@@ -1059,7 +1063,7 @@ export function createRoom(roomId, roomData, position) {
// No elevation for table items
sprite.elevation = 0;
group.items.push({ sprite, type: 'conditional_table_item' });
group.items.push({ sprite, type: sourceLayer });
// Store table items in objects collection so interaction system can find them
rooms[roomId].objects[sprite.objectId] = sprite;
@@ -1078,8 +1082,73 @@ export function createRoom(roomId, roomData, position) {
setDepthAndStore(sprite, position, roomId, false);
}
});
// Re-sort table groups after adding scenario items to maintain north-to-south order
// 3. Process unreserved Tiled items (existing background decoration items)
// These are unconditional items that were not used by any scenario object
const unreservedItems = itemPool.getUnreservedItems();
// Separate table items from regular items for special processing
const unreservedTableItems = [];
const unreservedRegularItems = [];
unreservedItems.forEach(tiledItem => {
const imageName = itemPool.getImageNameFromObject(tiledItem);
// Skip if this exact item was already used by scenario objects
if (usedItems.has(imageName)) {
return;
}
// Check if this is a table item by seeing if it's in tableItemsByType
const baseType = itemPool.extractBaseTypeFromImageName(imageName);
if (itemPool.tableItemsByType[baseType] &&
itemPool.tableItemsByType[baseType].includes(tiledItem)) {
unreservedTableItems.push(tiledItem);
} else {
unreservedRegularItems.push(tiledItem);
}
});
// Process regular unreserved items (chairs, lamps, etc.)
unreservedRegularItems.forEach(tiledItem => {
const imageName = itemPool.getImageNameFromObject(tiledItem);
// Use processObject to create sprite with all properties (collision, animation, etc.)
const result = processObject(tiledItem, position, roomId, 'item', map);
if (result && result.sprite) {
// Store unreserved items so they're revealed
rooms[roomId].objects[result.sprite.objectId] = result.sprite;
console.log(`Added unreserved item ${imageName} to room objects`);
}
});
// Process unreserved table items - need to group them with tables and set depth
unreservedTableItems.forEach(tiledItem => {
const imageName = itemPool.getImageNameFromObject(tiledItem);
// Use processObject to create sprite with all properties
const result = processObject(tiledItem, position, roomId, 'table_item', map);
if (result && result.sprite) {
// Find the closest table to group this item with
if (tableObjects.length > 0) {
const closestTable = findClosestTable(result.sprite, tableObjects);
if (closestTable) {
const group = tableGroups.find(g => g.table === closestTable);
if (group) {
group.items.push(result);
console.log(`Added unreserved table item ${imageName} to table group`);
}
}
} else {
// No tables, just store it as a regular item
rooms[roomId].objects[result.sprite.objectId] = result.sprite;
console.log(`Added unreserved table item ${imageName} to room objects (no tables to group with)`);
}
}
});
// Final re-sort and depth assignment for all table groups
// (includes both scenario and unreserved table items)
tableGroups.forEach(group => {
// Sort items from north to south (lower Y values first)
group.items.sort((a, b) => a.sprite.y - b.sprite.y);
@@ -1092,30 +1161,13 @@ export function createRoom(roomId, roomData, position) {
// No elevation for table items
item.sprite.elevation = 0;
console.log(`Re-sorted item ${item.sprite.name} to depth ${itemDepth} (north to south order, no elevation)`);
console.log(`Final depth: table item ${item.sprite.name} to depth ${itemDepth} (position ${index + 1} of ${group.items.length})`);
});
});
// 3. Process unreserved Tiled items (existing background decoration items)
const unreservedItems = itemPool.getUnreservedItems();
unreservedItems.forEach(tiledItem => {
const imageName = itemPool.getImageNameFromObject(tiledItem);
const baseType = itemPool.extractBaseTypeFromImageName(imageName);
// Skip if this base type was already used by scenario objects
if (!usedItems.has(imageName) && !usedItems.has(baseType)) {
const sprite = gameRef.add.sprite(
Math.round(position.x + tiledItem.x),
Math.round(position.y + tiledItem.y - tiledItem.height),
imageName
);
// Apply Tiled properties for unreserved items too
applyTiledProperties(sprite, tiledItem);
// Set depth and store
setDepthAndStore(sprite, position, roomId, false);
}
// Store all group items in room objects
group.items.forEach(item => {
rooms[roomId].objects[item.sprite.objectId] = item.sprite;
});
});
// Log summary of item usage
@@ -1123,6 +1175,9 @@ export function createRoom(roomId, roomData, position) {
Object.entries(itemPool.itemsByType).forEach(([baseType, items]) => {
console.log(`Regular items for ${baseType}: ${items.length} available`);
});
Object.entries(itemPool.tableItemsByType).forEach(([baseType, items]) => {
console.log(`Regular table items for ${baseType}: ${items.length} available`);
});
Object.entries(itemPool.conditionalItemsByType).forEach(([baseType, items]) => {
console.log(`Conditional items for ${baseType}: ${items.length} available`);
});
@@ -1399,6 +1454,13 @@ export function createRoom(roomId, roomData, position) {
console.log(`Applied default properties to ${type} ${imageName} -> ${cleanName}`);
}
// Make swivel chairs interactable but don't highlight them
if (sprite.isSwivelChair) {
sprite.interactable = true;
sprite.noInteractionHighlight = true;
console.log(`Marked swivel chair ${sprite.objectId} as interactable (no highlight)`);
}
// Note: Click handling is now done by the main scene's pointerdown handler
// which checks for all objects at the clicked position

View File

@@ -65,6 +65,11 @@ export function checkObjectInteractions() {
return;
}
// Skip highlighting for objects marked with noInteractionHighlight (like swivel chairs)
if (obj.noInteractionHighlight) {
return;
}
// Skip objects outside viewport for performance (if viewport bounds available)
if (viewBounds && (
obj.x < viewBounds.left ||
@@ -262,7 +267,45 @@ export function handleObjectInteraction(sprite) {
scenarioData: sprite.scenarioData
});
if (!sprite || !sprite.scenarioData) {
if (!sprite) {
console.warn('Invalid sprite');
return;
}
// Handle swivel chair interaction - send it flying!
if (sprite.isSwivelChair && sprite.body) {
const player = window.player;
if (player) {
// Calculate direction from player to chair
const dx = sprite.x - player.x;
const dy = sprite.y - player.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize the direction vector
const dirX = dx / distance;
const dirY = dy / distance;
// Apply a strong kick velocity
const kickForce = 600; // Pixels per second
sprite.body.setVelocity(dirX * kickForce, dirY * kickForce);
// Trigger spin direction calculation for visual rotation
if (window.calculateChairSpinDirection) {
window.calculateChairSpinDirection(player, sprite);
}
// Show feedback message
console.log('SWIVEL CHAIR KICKED', {
chairName: sprite.name,
velocity: { x: dirX * kickForce, y: dirY * kickForce }
});
}
}
return;
}
if (!sprite.scenarioData) {
console.warn('Invalid sprite or missing scenario data');
return;
}

View File

@@ -56,7 +56,14 @@ export function setupChairCollisions(chair) {
if (room.wallCollisionBoxes) {
room.wallCollisionBoxes.forEach(wallBox => {
if (wallBox.body) {
game.physics.add.collider(chair, wallBox);
// Add collision callback for swivel chairs to modify spin on wall hit
if (chair.isSwivelChair) {
game.physics.add.collider(chair, wallBox, () => {
handleChairWallCollision(chair);
});
} else {
game.physics.add.collider(chair, wallBox);
}
}
});
}
@@ -68,7 +75,14 @@ export function setupChairCollisions(chair) {
room.doorSprites.forEach(doorSprite => {
// Only collide with closed doors (doors that haven't been opened)
if (doorSprite.body && doorSprite.body.immovable) {
game.physics.add.collider(chair, doorSprite);
// Add collision callback for swivel chairs to modify spin on wall hit
if (chair.isSwivelChair) {
game.physics.add.collider(chair, doorSprite, () => {
handleChairWallCollision(chair);
});
} else {
game.physics.add.collider(chair, doorSprite);
}
}
});
}
@@ -133,6 +147,28 @@ export function setupExistingChairsWithNewRoom(roomId) {
console.log(`Set up chair collisions for room ${roomId} with ${window.chairs.length} existing chairs`);
}
// Handle collision between swivel chair and wall - modify spin on impact
function handleChairWallCollision(chair) {
if (!chair.isSwivelChair) return;
// When chair hits a wall, reverse the spin direction and give it a speed boost
// This creates a dynamic "bounce" effect
if (chair.spinDirection !== 0) {
chair.spinDirection *= -1; // Reverse spin
// Give spin animation a nudge - speed it up temporarily
// Add a boost to the rotation speed (up to 30% faster, but cap at max)
const speedBoost = 1.3;
chair.rotationSpeed = Math.min(chair.rotationSpeed * speedBoost, chair.maxRotationSpeed);
console.log('Chair hit wall - spin reversed with boost', {
newDirection: chair.spinDirection,
newRotationSpeed: chair.rotationSpeed,
maxRotationSpeed: chair.maxRotationSpeed
});
}
}
// Calculate chair spin direction based on contact point
export function calculateChairSpinDirection(player, chair) {
if (!chair.isSwivelChair) return;

View File

@@ -1,5 +1,5 @@
{
"scenario_brief": "You are a cyber security student tasked with recovering the Professor's backup of the CyBOK LaTeX source files for the CyBOK 1.1 release. According to legend, the HDD is stored in a safe in the Professor's office. Follow the clues scattered around the department to find the safe code. Time to put your physical security skills to the test! Good luck, and try not to get caught... or expelled.",
"scenario_brief": "You are a cyber security student tasked with recovering the Professor's backup of the CyBOK LaTeX source files for the CyBOK 1.1 release. According to legend, the HDD is stored in a safe in the Professor's office. Follow the clues scattered around the department office to find the safe code. Time to put your physical security skills to the test! Good luck, and try not to get caught... or expelled.",
"endGoal": "Recover the CyBOK LaTeX source HDD",
"startRoom": "reception",
"rooms": {
@@ -24,12 +24,12 @@
"name": "Lockpicking Hint Notice",
"takeable": true,
"readable": true,
"text": "DEPARTMENT NOTICE:\n\nMany doors use pin tumbler locks.\n\nTip: Apply tension to the lock while manipulating the pins with a pick.\n\nDo NOT attempt this in front of colleagues.",
"text": "DEPARTMENT NOTICE:\n\nMany doors use pin tumbler locks.\n\nTip: Using a lockpick set, apply tension to the lock while manipulating the pins with a pick.\n\nDo NOT attempt this in front of colleagues.",
"observations": "A cautionary notice about lockpicking techniques"
},
{
"type": "bag",
"name": "Lockpicking Backpack",
"name": "Heist Gear Backpack",
"contents": [
{
"type": "lockpick",
@@ -60,10 +60,10 @@
},
"objects": [
{
"type": "bag1",
"name": "Old Messenger Bag",
"type": "briefcase",
"name": "Professor's briefcase",
"takeable": false,
"observations": "A worn messenger bag sitting in the corner, partially open",
"observations": "A worn briefcase sitting in the corner, partially open",
"contents": [
{
"type": "notes",
@@ -122,7 +122,7 @@
"name": "Using Cyber Chef to Decode Base64",
"takeable": true,
"readable": true,
"text": "CYBER CHEF TIPS:\n\nTo decode Base64 strings, use Cyber Chef's 'From Base64' operation.\n\nExample:\nInput: Y3lib2syMDI0\nOperation: From Base64\nOutput: cybok2024\n\nCyber Chef is a powerful tool for all your encoding and decoding needs!",
"text": "CYBER CHEF TIPS:\n\nTo decode Base64 strings, use Cyber Chef's 'From Base64' operation.\n\nExample:\nInput: Y3lib2syMDI0\nOperation: From Base64\nOutput: cybok2024\n\nCyber Chef is a powerful tool for all your encoding and decoding needs!\n\nA good clue that a string is Base64 is if it ends with '=' or '=='.\n\nHappy decoding!",
"observations": "A quick reference guide for using Cyber Chef to decode Base64"
},
{