Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"test:seasonxp": "node scripts/test-season-xp.js",
"test:exit-streak": "node scripts/test-exit-streak.js",
"test:privy": "node scripts/test-privy-gate.js --serve",
"test:kills": "node scripts/test-leaderboard-writes.js"
"test:kills": "node scripts/test-leaderboard-writes.js",
"test:proploot": "node scripts/test-prop-loot.js"
},
"overrides": {
"permissionless": {
Expand Down
47 changes: 47 additions & 0 deletions public/game-engine/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -1583,6 +1583,45 @@ function applyLoot(kind,amt,x,y){
log('A weathered paper, folded shut. Tap it in your inventory to read the code.', 'reward', 'scroll');
spark(x, y-10, '#c9a86a', 26, 200);
}
else if(kind==='item' || kind==='food'){
// ── THE DROP THAT NEVER ARRIVED ────────────────────────────────────────
//
// OWNER: *"Lootingan food ko skrg kaya gaada ya?? ini serius perlu di bahas
// kenapa di hilangkan."* Nothing was removed. This branch never existed.
//
// Nine breakable props roll an `item` outcome — crate, crystal, tablet,
// statue, oak_barrel, barrel_stack, hay_pile, plaque_sword, cot — and
// applyLoot() handled hp, xp, relic, goldkey, paper and gshard. An `item`
// roll fell through every branch, played the pickup sound, and logged
// "— loot!" because the roll was not 'none'. The player got nothing. His
// own screenshot says *"Straw Bedding shattered — loot!"* over an empty
// inventory, which is this line missing.
//
// Food was never removed either: food IS items (the edible slice of the
// same library, ~16% of ids), so a broken item path took food with it.
//
// `amt` carries the tier the loot table asked for — ['item',2,30] means a
// tier-2 roll — which is how a chest yields better icons than a barrel.
const I = window.NS_ITEMS;
const it = !I ? null : (kind==='food' ? I.rollFoodDrop() : I.rollItemDrop(amt||1));
if(it){
const got = addItemToStash(it, 1);
if(got.added > 0){
const edible = !!(window.NS_MARKET && NS_MARKET.isFood(it.id));
lootText(x, y, '+' + (it.shortName || it.name), it.color || '#eafff5');
// 'heart' and 'open' because those keys exist in NS_ICON_PATHS —
// nsIcon() returns an empty string for anything else, so an invented
// name would silently ship a message with no icon.
log((edible ? 'Found food: ' : 'Found ') + it.name + (edible ? ' — eat it from the Food tab.' : ''),
'reward', edible ? 'heart' : 'open');
spark(x, y-10, it.color || '#eafff5', 22, 190);
} else {
// Stack already at its cap. Say so rather than repeating the silent
// loss this whole branch exists to fix.
lootText(x, y, it.shortName + ' (stack full)', '#c9a86a');
}
}
}
else if(kind==='gshard'){
// Phase 2 (blueprint §2.2): Glitch Shard crafting material. Tier follows
// the current act (early acts drop t1, mid t2, late t3) so materials
Expand Down Expand Up @@ -6168,6 +6207,14 @@ window.__NS = { get G(){ return G; },
spawnElite(){ if(!G) return; const a=ARCHETYPES[0];
G.enemies.push(new Enemy(a, G.player.x+120, G.player.y, G.depth, false, true)); },
addDecor(t){ if(!G) return; G.decor.push(new Decor(t||'vase', G.player.x+40, G.player.y)); },
// Debug-only, alongside addDecor above and for the same reason: driving a
// real swing into a prop from a test is all timing and hitboxes, and none of
// that is what the loot path is being asked about. Smashes prop `i` through
// the SAME smashDecor() a weapon calls, so what the test exercises is the
// production path — see scripts/test-prop-loot.js, which exists because
// applyLoot() shipped with no `item` branch and nine props dropped nothing.
smashDecorAt(i){ if(!G || !G.decor[i]) return false;
const o = G.decor[i]; o.hp = 1; smashDecor(o); return !!o.broken; },
onEnemyKilled, // debug-only: lets tests exercise act-completion without a full attack simulation
get campaignActIndex(){ return campaignActIndex; },
get campaignCycle(){ return campaignCycle; }, // Null Cycles — enemy scaling
Expand Down
26 changes: 26 additions & 0 deletions public/game-engine/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,37 @@
return best;
}

// rollFoodDrop(): an item drawn from the EDIBLE range only.
//
// Food is not a separate table — it is the slice of the same 1244-icon
// library that NS_MARKET.FOOD_RANGES marks edible (ids 321-515, about 16%).
// So a generic item roll already produces food one time in six; this exists
// for props that should reliably yield something to eat — a supply crate, a
// stocked shelf, a water pail — where "one in six" was indistinguishable
// from never.
//
// Deliberately no rarity re-roll. rollItemDrop biases toward better rarity
// for richer containers; food heals 3-5% of max HP by rarity, a spread too
// narrow to be worth the bias, and a legendary apple reads as a joke.
function rollFoodDrop(){
var ranges = (window.NS_MARKET && window.NS_MARKET.FOOD_RANGES) || [[321, 515]];
var span = 0, i;
for (i = 0; i < ranges.length; i++) span += (ranges[i][1] - ranges[i][0] + 1);
var pick = Math.floor(Math.random() * span);
for (i = 0; i < ranges.length; i++){
var len = ranges[i][1] - ranges[i][0] + 1;
if (pick < len) return getItem(ranges[i][0] + pick);
pick -= len;
}
return getItem(ranges[0][0]);
}

window.NS_ITEMS = {
ITEM_COUNT: ITEM_COUNT,
RARITIES: RARITIES,
RARITY_ORDER: RARITY_ORDER,
getItem: getItem,
rollItemDrop: rollItemDrop,
rollFoodDrop: rollFoodDrop,
};
})();
46 changes: 35 additions & 11 deletions public/game-engine/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,33 @@
facing 'left' = against the right wall (9 o'clock, side profile)
============================================================ */
const DECOR_TYPES = {
// ---- what a loot entry means -------------------------------------------
// ['hp', n, w] heal n
// ['xp', n, w] n experience
// ['item', t, w] one item from the library, rolled at TIER t (1-3)
// ['food', t, w] one EDIBLE item (NS_MARKET.FOOD_RANGES). t is unused for
// now — food has no rarity bias, see rollFoodDrop()
// ['relic'|'gshard'|'goldkey'|'paper', n, w]
// ['none', 0, w] nothing
//
// OWNER: *"Lootingan food ko skrg kaya gaada ya?"* Two separate reasons, and
// only one of them was a tuning problem:
//
// 1. applyLoot() in game.js had no `item` branch at all, so every item roll
// from a SMASHED prop (nine types) played the sound, logged "— loot!"
// and gave nothing. Food is the edible ~16% of the same item library, so
// a broken item path took food with it. Fixed there.
// 2. Even working, food arrived only as a sixth of an item roll, and the
// props a player smashes most had no item roll at all. So the props that
// hold PROVISIONS — pails, barrels, crates, shelves, bedding, the herb
// pot — now drop food directly, at ~20-26%.
//
// Weight is taken from `none` first and `xp` second. The hp/relic/gshard
// economies are deliberately untouched: this was meant to add food, not to
// quietly make every prop richer.
// ---- breakable decorations: smash them for loot ----
vase: { hp:1, w:24, h:38, label:'Glazed Vase', loot:[['hp',6,50],['xp',12,32],['relic',1,4],['none',0,14]] },
pot: { hp:1, w:30, h:30, label:'Herb Pot', loot:[['hp',8,58],['xp',10,26],['none',0,16]] },
pot: { hp:1, w:30, h:30, label:'Herb Pot', loot:[['hp',8,50],['xp',10,22],['food',1,22],['none',0,6]] },
barrel: { hp:2, w:30, h:40, label:'Old Barrel', loot:[['xp',18,45],['hp',10,35],['none',0,20]] },
// Owner, again, and about the LOOTABLE crate this time (the earlier pass at
// the same complaint only resized the ambient crate stacks at the bottom of
Expand All @@ -26,7 +50,7 @@ const DECOR_TYPES = {
// drawScale in Decor.draw(). 1.3x brings the drawn crate to ~44px, in line
// with the scenery around it, and w/h below are the matching footprint so
// the collision circle and contact shadow still fit what is on screen.
crate: { hp:2, w:44, h:44, drawScale:1.3, label:'Supply Crate', loot:[['xp',22,45],['hp',8,25],['item',1,20],['none',0,10]] },
crate: { hp:2, w:44, h:44, drawScale:1.3, label:'Supply Crate', loot:[['xp',22,36],['hp',8,20],['item',1,20],['food',1,24]] },
cabinet_s:{ hp:2, w:34, h:46, label:'Forgotten Archive', loot:[['hp',14,26],['xp',24,24],['item',2,30],['relic',1,10]], interactive:true, containerMaterial:'wood', northOnly:true }, // owner: cabinets/safes hug the NORTH wall only
wardrobe: { hp:3, w:48, h:64, label:'Rotten Armoire', loot:[['hp',32,24],['xp',55,24],['item',2,32]], interactive:true, containerMaterial:'wood_rotten', northOnly:true },
// ---- ancient ornaments: break them for XP, CELO, or rare relics ----
Expand All @@ -42,21 +66,21 @@ const DECOR_TYPES = {
chest: { hp:2, w:42, h:34, label:'Lost Cache', loot:[['relic',1,15],['item',3,35],['xp',60,15],['hp',30,10]], interactive:true, containerMaterial:'iron', northOnly:true },
// ---- v75 sprite decor (user-supplied 4-direction art, /sprites/decor2) ----
safe: { hp:2, w:40, h:44, label:'Rusted Strongbox', loot:[['relic',1,18],['item',3,32],['xp',55,18],['hp',26,12]], interactive:true, containerMaterial:'iron', northOnly:true },
table_w: { hp:1, w:36, h:30, label:'Wooden Table', loot:[['xp',14,45],['hp',8,25],['none',0,30]] },
table_w: { hp:1, w:36, h:30, label:'Wooden Table', loot:[['xp',14,40],['hp',8,22],['food',1,20],['none',0,18]] },
bench: { hp:2, w:52, h:26, label:'Waiting Bench', loot:[['xp',16,45],['hp',8,25],['none',0,30]], northOnly:true },
// ---- v80 LPC sprite props (18 new PNGs in /sprites/decor2, cut from the
// Liberated Pixel Cup contest tilesets — Sharm, Janna, Skyler R. Collady,
// Lanea Zimmerman et al., CC-BY-SA 3.0 / GPL 3.0, see decor2/CREDITS.md).
// Owner request: maps felt empty — more breakable & lootable dressing.
// ---- breakables ----
oak_barrel: { hp:2, w:30, h:40, label:'Oak Barrel', loot:[['xp',16,42],['hp',10,34],['item',1,8],['none',0,16]] },
barrel_stack: { hp:3, w:46, h:56, label:'Barrel Stack', loot:[['xp',26,42],['hp',14,30],['item',1,14],['none',0,14]] },
bucket: { hp:1, w:22, h:24, label:'Wooden Pail', loot:[['hp',6,50],['xp',8,26],['none',0,24]] },
bucket_water: { hp:1, w:22, h:24, label:'Water Pail', loot:[['hp',12,62],['xp',6,18],['none',0,20]] },
oak_barrel: { hp:2, w:30, h:40, label:'Oak Barrel', loot:[['xp',16,34],['hp',10,28],['item',1,8],['food',1,24],['none',0,6]] },
barrel_stack: { hp:3, w:46, h:56, label:'Barrel Stack', loot:[['xp',26,34],['hp',14,24],['item',1,14],['food',2,24],['none',0,4]] },
bucket: { hp:1, w:22, h:24, label:'Wooden Pail', loot:[['hp',6,44],['xp',8,22],['food',1,20],['none',0,14]] },
bucket_water: { hp:1, w:22, h:24, label:'Water Pail', loot:[['hp',12,52],['xp',6,14],['food',1,26],['none',0,8]] },
boulder: { hp:2, w:32, h:30, label:'Fallen Boulder', loot:[['xp',14,45],['hp',6,20],['relic',1,4],['gshard',1,7],['none',0,31]] },
hay_pile: { hp:1, w:44, h:50, label:'Straw Bedding', loot:[['hp',8,44],['xp',10,28],['item',1,8],['none',0,20]] },
hay_pile: { hp:1, w:44, h:50, label:'Straw Bedding', loot:[['hp',8,36],['xp',10,22],['item',1,8],['food',1,26],['none',0,8]] },
chalice: { hp:1, w:26, h:40, label:'Ritual Chalice', loot:[['relic',1,16],['xp',24,38],['hp',10,22],['gshard',1,10],['none',0,24]] },
basin: { hp:1, w:30, h:26, label:'Wash Basin', loot:[['hp',10,52],['xp',10,24],['none',0,24]] },
basin: { hp:1, w:30, h:26, label:'Wash Basin', loot:[['hp',10,46],['xp',10,20],['food',1,20],['none',0,14]] },
// Owner: this one read as a framed photograph on the wall — "delete it,
// put another lootable thing there instead". Same call they made about
// plaque_coin, and for the same reason: a picture frame is not something a
Expand Down Expand Up @@ -84,10 +108,10 @@ const DECOR_TYPES = {
// saved floor built before this change breaks on load.
plaque_coin: { hp:2, w:29, h:22, label:'Gold Chest', loot:[['relic',1,16],['item',3,34],['xp',55,20],['hp',24,14],['gshard',1,16]], interactive:true, containerMaterial:'iron', northOnly:true },
skull_heap: { hp:1, w:32, h:16, label:'Skull Heap', loot:[['xp',12,44],['relic',1,7],['hp',6,20],['gshard',1,8],['none',0,29]] },
cot: { hp:2, w:26, h:56, label:'Rotten Cot', loot:[['hp',12,36],['xp',14,30],['item',1,12],['none',0,22]], northOnly:true },
cot: { hp:2, w:26, h:56, label:'Rotten Cot', loot:[['hp',12,32],['xp',14,26],['item',1,12],['food',1,18],['none',0,12]], northOnly:true },
// ---- interactive containers (opened via OPEN button, like cabinet_s) ----
footlocker: { hp:2, w:34, h:30, label:'Iron Footlocker', loot:[['item',2,32],['xp',30,26],['hp',16,20],['relic',1,10]], interactive:true, containerMaterial:'iron', northOnly:true }, // boxy, front-view-only art: keep on the top wall so it never faces the wrong way on a side wall (owner: "lemari di W/E menghadap S/N")
shelf_stocked: { hp:2, w:52, h:52, label:'Stocked Shelf', loot:[['item',1,34],['hp',14,26],['xp',22,26],['relic',1,6]], interactive:true, containerMaterial:'wood', northOnly:true },
shelf_stocked: { hp:2, w:52, h:52, label:'Stocked Shelf', loot:[['item',1,30],['food',2,24],['hp',14,20],['xp',22,20],['relic',1,6]], interactive:true, containerMaterial:'wood', northOnly:true },
dresser: { hp:2, w:48, h:40, label:'Old Dresser', loot:[['item',1,30],['xp',20,28],['hp',12,26],['relic',1,6]], interactive:true, containerMaterial:'wood', northOnly:true },
cabinet_ornate: { hp:2, w:46, h:52, label:'Ornate Cabinet', loot:[['item',2,30],['xp',26,26],['hp',14,22],['relic',1,10]], interactive:true, containerMaterial:'wood', northOnly:true },
// ---- Bunker 5 "THE LAST LIGHT" weekly Vault door (Phase 5.5 #9C/#10) ----
Expand Down
Loading
Loading