feat: update inventory frontend and services to current production state
This commit is contained in:
parent
7050cfb8b7
commit
fc557ab1d5
4 changed files with 1321 additions and 307 deletions
|
|
@ -1363,7 +1363,7 @@ async def process_inventory(inventory: InventoryItem):
|
|||
for table in ('item_raw_data', 'item_combat_stats', 'item_requirements',
|
||||
'item_enhancements', 'item_ratings', 'item_spells'):
|
||||
await database.execute(
|
||||
sa.text(f"DELETE FROM {table} WHERE item_id = ANY(:ids)"),
|
||||
f"DELETE FROM {table} WHERE item_id = ANY(:ids)",
|
||||
{"ids": db_ids}
|
||||
)
|
||||
|
||||
|
|
@ -1577,7 +1577,7 @@ async def upsert_inventory_item(character_name: str, item: Dict[str, Any]):
|
|||
for table in ('item_raw_data', 'item_combat_stats', 'item_requirements',
|
||||
'item_enhancements', 'item_ratings', 'item_spells'):
|
||||
await database.execute(
|
||||
sa.text(f"DELETE FROM {table} WHERE item_id = ANY(:ids)"),
|
||||
f"DELETE FROM {table} WHERE item_id = ANY(:ids)",
|
||||
{"ids": db_ids}
|
||||
)
|
||||
await database.execute(
|
||||
|
|
@ -1730,7 +1730,35 @@ async def upsert_inventory_item(character_name: str, item: Dict[str, Any]):
|
|||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
|
||||
logger.info(f"Single item upsert for {character_name}: item_id={item_game_id}, processed={processed_count}")
|
||||
return {"status": "ok", "processed": processed_count}
|
||||
|
||||
# Fetch the just-upserted item with all joins and enrich it
|
||||
enriched_item = None
|
||||
try:
|
||||
enrich_query = """
|
||||
SELECT
|
||||
i.id, i.item_id, i.name, i.icon, i.object_class, i.value, i.burden,
|
||||
i.has_id_data, i.timestamp,
|
||||
i.current_wielded_location, i.container_id, i.items_capacity,
|
||||
cs.max_damage, cs.armor_level, cs.damage_bonus, cs.attack_bonus,
|
||||
r.wield_level, r.skill_level, r.equip_skill, r.lore_requirement,
|
||||
e.material, e.imbue, e.item_set, e.tinks, e.workmanship,
|
||||
rt.damage_rating, rt.crit_rating, rt.heal_boost_rating,
|
||||
rd.int_values, rd.double_values, rd.string_values, rd.bool_values, rd.original_json
|
||||
FROM items i
|
||||
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id
|
||||
LEFT JOIN item_requirements r ON i.id = r.item_id
|
||||
LEFT JOIN item_enhancements e ON i.id = e.item_id
|
||||
LEFT JOIN item_ratings rt ON i.id = rt.item_id
|
||||
LEFT JOIN item_raw_data rd ON i.id = rd.item_id
|
||||
WHERE i.id = :db_id
|
||||
"""
|
||||
row = await database.fetch_one(enrich_query, {"db_id": db_item_id})
|
||||
if row:
|
||||
enriched_item = enrich_db_item(row)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to enrich item after upsert: {e}")
|
||||
|
||||
return {"status": "ok", "processed": processed_count, "item": enriched_item}
|
||||
|
||||
|
||||
@app.delete("/inventory/{character_name}/item/{item_id}",
|
||||
|
|
@ -1755,7 +1783,7 @@ async def delete_inventory_item(character_name: str, item_id: int):
|
|||
for table in ('item_raw_data', 'item_combat_stats', 'item_requirements',
|
||||
'item_enhancements', 'item_ratings', 'item_spells'):
|
||||
await database.execute(
|
||||
sa.text(f"DELETE FROM {table} WHERE item_id = ANY(:ids)"),
|
||||
f"DELETE FROM {table} WHERE item_id = ANY(:ids)",
|
||||
{"ids": db_ids}
|
||||
)
|
||||
|
||||
|
|
@ -1771,49 +1799,14 @@ async def delete_inventory_item(character_name: str, item_id: int):
|
|||
return {"status": "ok", "deleted": deleted_count}
|
||||
|
||||
|
||||
@app.get("/inventory/{character_name}",
|
||||
summary="Get Character Inventory",
|
||||
description="Retrieve processed inventory data for a specific character with normalized item properties.",
|
||||
tags=["Character Data"])
|
||||
async def get_character_inventory(
|
||||
character_name: str,
|
||||
limit: int = Query(1000, le=5000),
|
||||
offset: int = Query(0, ge=0)
|
||||
):
|
||||
"""Get processed inventory for a character with structured data and comprehensive translations."""
|
||||
def enrich_db_item(item) -> dict:
|
||||
"""Enrich a single DB row (from the JOIN query) into the full frontend format.
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
i.id, i.name, i.icon, i.object_class, i.value, i.burden,
|
||||
i.has_id_data, i.timestamp,
|
||||
cs.max_damage, cs.armor_level, cs.damage_bonus, cs.attack_bonus,
|
||||
r.wield_level, r.skill_level, r.equip_skill, r.lore_requirement,
|
||||
e.material, e.imbue, e.item_set, e.tinks, e.workmanship,
|
||||
rt.damage_rating, rt.crit_rating, rt.heal_boost_rating,
|
||||
rd.int_values, rd.double_values, rd.string_values, rd.bool_values, rd.original_json
|
||||
FROM items i
|
||||
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id
|
||||
LEFT JOIN item_requirements r ON i.id = r.item_id
|
||||
LEFT JOIN item_enhancements e ON i.id = e.item_id
|
||||
LEFT JOIN item_ratings rt ON i.id = rt.item_id
|
||||
LEFT JOIN item_raw_data rd ON i.id = rd.item_id
|
||||
WHERE i.character_name = :character_name
|
||||
ORDER BY i.name
|
||||
LIMIT :limit OFFSET :offset
|
||||
Takes a mapping (e.g. asyncpg Record or dict) from the items+joins query and returns
|
||||
a clean dict with translated material names, spell info, combat stats, ratings,
|
||||
workmanship text, mana display, etc. Identical logic to what get_character_inventory
|
||||
used inline — extracted here so upsert_inventory_item can reuse it.
|
||||
"""
|
||||
|
||||
items = await database.fetch_all(query, {
|
||||
"character_name": character_name,
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
})
|
||||
|
||||
if not items:
|
||||
raise HTTPException(status_code=404, detail=f"No inventory found for {character_name}")
|
||||
|
||||
# Convert to structured format with enhanced translations
|
||||
processed_items = []
|
||||
for item in items:
|
||||
processed_item = dict(item)
|
||||
|
||||
# Get comprehensive translations from original_json
|
||||
|
|
@ -2021,7 +2014,52 @@ async def get_character_inventory(
|
|||
|
||||
# Remove null values for cleaner response
|
||||
processed_item = {k: v for k, v in processed_item.items() if v is not None}
|
||||
processed_items.append(processed_item)
|
||||
return processed_item
|
||||
|
||||
|
||||
@app.get("/inventory/{character_name}",
|
||||
summary="Get Character Inventory",
|
||||
description="Retrieve processed inventory data for a specific character with normalized item properties.",
|
||||
tags=["Character Data"])
|
||||
async def get_character_inventory(
|
||||
character_name: str,
|
||||
limit: int = Query(1000, le=5000),
|
||||
offset: int = Query(0, ge=0)
|
||||
):
|
||||
"""Get processed inventory for a character with structured data and comprehensive translations."""
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
i.id, i.item_id, i.name, i.icon, i.object_class, i.value, i.burden,
|
||||
i.has_id_data, i.timestamp,
|
||||
i.current_wielded_location, i.container_id, i.items_capacity,
|
||||
cs.max_damage, cs.armor_level, cs.damage_bonus, cs.attack_bonus,
|
||||
r.wield_level, r.skill_level, r.equip_skill, r.lore_requirement,
|
||||
e.material, e.imbue, e.item_set, e.tinks, e.workmanship,
|
||||
rt.damage_rating, rt.crit_rating, rt.heal_boost_rating,
|
||||
rd.int_values, rd.double_values, rd.string_values, rd.bool_values, rd.original_json
|
||||
FROM items i
|
||||
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id
|
||||
LEFT JOIN item_requirements r ON i.id = r.item_id
|
||||
LEFT JOIN item_enhancements e ON i.id = e.item_id
|
||||
LEFT JOIN item_ratings rt ON i.id = rt.item_id
|
||||
LEFT JOIN item_raw_data rd ON i.id = rd.item_id
|
||||
WHERE i.character_name = :character_name
|
||||
ORDER BY i.name
|
||||
LIMIT :limit OFFSET :offset
|
||||
"""
|
||||
|
||||
items = await database.fetch_all(query, {
|
||||
"character_name": character_name,
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
})
|
||||
|
||||
if not items:
|
||||
raise HTTPException(status_code=404, detail=f"No inventory found for {character_name}")
|
||||
|
||||
# Convert to structured format with enhanced translations
|
||||
processed_items = [enrich_db_item(item) for item in items]
|
||||
|
||||
return {
|
||||
"character_name": character_name,
|
||||
|
|
|
|||
13
main.py
13
main.py
|
|
@ -2002,7 +2002,18 @@ async def ws_receive_snapshots(
|
|||
f"{INVENTORY_SERVICE_URL}/inventory/{char_name}/item",
|
||||
json=item
|
||||
)
|
||||
if resp.status_code >= 400:
|
||||
if resp.status_code < 400:
|
||||
# Use enriched item from inventory-service response for broadcast
|
||||
resp_json = resp.json()
|
||||
enriched_item = resp_json.get("item")
|
||||
if enriched_item:
|
||||
data = {
|
||||
"type": "inventory_delta",
|
||||
"action": action,
|
||||
"character_name": char_name,
|
||||
"item": enriched_item
|
||||
}
|
||||
else:
|
||||
logger.warning(f"Inventory service returned {resp.status_code} for delta {action}")
|
||||
|
||||
# Broadcast delta to all browser clients
|
||||
|
|
|
|||
597
static/script.js
597
static/script.js
|
|
@ -893,6 +893,131 @@ function updateStatsTimeRange(content, name, timeRange) {
|
|||
}
|
||||
|
||||
// Show or create an inventory window for a character
|
||||
/**
|
||||
* Normalize raw plugin MyWorldObject format to flat fields expected by createInventorySlot.
|
||||
* Plugin sends PascalCase computed properties: { Id, Icon, Name, Value, Burden, ArmorLevel, Material, ... }
|
||||
* Also has raw dictionaries: { IntValues: {19: value, 5: burden, ...}, StringValues: {1: name, ...} }
|
||||
* Inventory service sends flat lowercase: { item_id, icon, name, value, burden, armor_level, ... }
|
||||
*
|
||||
* MyWorldObject uses -1 as sentinel for "not set" on int/double properties.
|
||||
*/
|
||||
function normalizeInventoryItem(item) {
|
||||
if (!item) return item;
|
||||
if (item.name && item.item_id) return item;
|
||||
|
||||
// MyWorldObject uses -1 as "not set" sentinel — filter those out
|
||||
const v = (val) => (val !== undefined && val !== null && val !== -1) ? val : undefined;
|
||||
|
||||
if (!item.item_id) item.item_id = item.Id;
|
||||
if (!item.icon) item.icon = item.Icon;
|
||||
if (!item.object_class) item.object_class = item.ObjectClass;
|
||||
if (item.HasIdData !== undefined) item.has_id_data = item.HasIdData;
|
||||
|
||||
const baseName = item.Name || (item.StringValues && item.StringValues['1']) || null;
|
||||
const material = item.Material || null;
|
||||
if (material) {
|
||||
item.material = material;
|
||||
item.material_name = material;
|
||||
}
|
||||
|
||||
// Prepend material to name (e.g. "Pants" → "Satin Pants") matching inventory-service
|
||||
if (baseName) {
|
||||
if (material && !baseName.toLowerCase().startsWith(material.toLowerCase())) {
|
||||
item.name = material + ' ' + baseName;
|
||||
} else {
|
||||
item.name = baseName;
|
||||
}
|
||||
}
|
||||
|
||||
const iv = item.IntValues || {};
|
||||
if (item.value === undefined) item.value = v(item.Value) ?? v(iv['19']);
|
||||
if (item.burden === undefined) item.burden = v(item.Burden) ?? v(iv['5']);
|
||||
|
||||
// Container/equipment tracking
|
||||
if (item.container_id === undefined) item.container_id = item.ContainerId || 0;
|
||||
if (item.current_wielded_location === undefined) {
|
||||
item.current_wielded_location = v(item.CurrentWieldedLocation) ?? v(iv['10']) ?? 0;
|
||||
}
|
||||
if (item.items_capacity === undefined) item.items_capacity = v(item.ItemsCapacity) ?? v(iv['6']);
|
||||
|
||||
const armor = v(item.ArmorLevel);
|
||||
if (armor !== undefined) item.armor_level = armor;
|
||||
|
||||
const maxDmg = v(item.MaxDamage);
|
||||
if (maxDmg !== undefined) item.max_damage = maxDmg;
|
||||
|
||||
const dmgBonus = v(item.DamageBonus);
|
||||
if (dmgBonus !== undefined) item.damage_bonus = dmgBonus;
|
||||
|
||||
const atkBonus = v(item.AttackBonus);
|
||||
if (atkBonus !== undefined) item.attack_bonus = atkBonus;
|
||||
|
||||
const elemDmg = v(item.ElementalDmgBonus);
|
||||
if (elemDmg !== undefined) item.elemental_damage_vs_monsters = elemDmg;
|
||||
|
||||
const meleeD = v(item.MeleeDefenseBonus);
|
||||
if (meleeD !== undefined) item.melee_defense_bonus = meleeD;
|
||||
|
||||
const magicD = v(item.MagicDBonus);
|
||||
if (magicD !== undefined) item.magic_defense_bonus = magicD;
|
||||
|
||||
const missileD = v(item.MissileDBonus);
|
||||
if (missileD !== undefined) item.missile_defense_bonus = missileD;
|
||||
|
||||
const manaC = v(item.ManaCBonus);
|
||||
if (manaC !== undefined) item.mana_conversion_bonus = manaC;
|
||||
|
||||
const wieldLvl = v(item.WieldLevel);
|
||||
if (wieldLvl !== undefined) item.wield_level = wieldLvl;
|
||||
|
||||
const skillLvl = v(item.SkillLevel);
|
||||
if (skillLvl !== undefined) item.skill_level = skillLvl;
|
||||
|
||||
const loreLvl = v(item.LoreRequirement);
|
||||
if (loreLvl !== undefined) item.lore_requirement = loreLvl;
|
||||
|
||||
if (item.EquipSkill) item.equip_skill = item.EquipSkill;
|
||||
if (item.Mastery) item.mastery = item.Mastery;
|
||||
if (item.ItemSet) item.item_set = item.ItemSet;
|
||||
if (item.Imbue) item.imbue = item.Imbue;
|
||||
|
||||
const tinks = v(item.Tinks);
|
||||
if (tinks !== undefined) item.tinks = tinks;
|
||||
|
||||
const work = v(item.Workmanship);
|
||||
if (work !== undefined) item.workmanship = work;
|
||||
|
||||
const damR = v(item.DamRating);
|
||||
if (damR !== undefined) item.damage_rating = damR;
|
||||
|
||||
const critR = v(item.CritRating);
|
||||
if (critR !== undefined) item.crit_rating = critR;
|
||||
|
||||
const healR = v(item.HealBoostRating);
|
||||
if (healR !== undefined) item.heal_boost_rating = healR;
|
||||
|
||||
const vitalR = v(item.VitalityRating);
|
||||
if (vitalR !== undefined) item.vitality_rating = vitalR;
|
||||
|
||||
const critDmgR = v(item.CritDamRating);
|
||||
if (critDmgR !== undefined) item.crit_damage_rating = critDmgR;
|
||||
|
||||
const damResR = v(item.DamResistRating);
|
||||
if (damResR !== undefined) item.damage_resist_rating = damResR;
|
||||
|
||||
const critResR = v(item.CritResistRating);
|
||||
if (critResR !== undefined) item.crit_resist_rating = critResR;
|
||||
|
||||
const critDmgResR = v(item.CritDamResistRating);
|
||||
if (critDmgResR !== undefined) item.crit_damage_resist_rating = critDmgResR;
|
||||
|
||||
if (item.Spells && Array.isArray(item.Spells) && item.Spells.length > 0 && !item.spells) {
|
||||
item.spells = item.Spells;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single inventory slot DOM element from item data.
|
||||
* Used by both initial inventory load and live delta updates.
|
||||
|
|
@ -900,7 +1025,7 @@ function updateStatsTimeRange(content, name, timeRange) {
|
|||
function createInventorySlot(item) {
|
||||
const slot = document.createElement('div');
|
||||
slot.className = 'inventory-slot';
|
||||
slot.setAttribute('data-item-id', item.Id || item.id || item.item_id || 0);
|
||||
slot.setAttribute('data-item-id', item.item_id || item.Id || item.id || 0);
|
||||
|
||||
// Create layered icon container
|
||||
const iconContainer = document.createElement('div');
|
||||
|
|
@ -1020,9 +1145,83 @@ function createInventorySlot(item) {
|
|||
slot.addEventListener('mouseleave', hideInventoryTooltip);
|
||||
|
||||
slot.appendChild(iconContainer);
|
||||
|
||||
// Add stack count if > 1
|
||||
const stackCount = item.count || item.Count || item.stack_count || item.StackCount || 1;
|
||||
if (stackCount > 1) {
|
||||
const countEl = document.createElement('div');
|
||||
countEl.className = 'inventory-count';
|
||||
countEl.textContent = stackCount;
|
||||
slot.appendChild(countEl);
|
||||
}
|
||||
|
||||
return slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equipment slots mapping for the AC inventory layout.
|
||||
* Grid matches the real AC "Equipment Slots Enabled" paperdoll view.
|
||||
*
|
||||
* Layout (6 cols × 6 rows):
|
||||
* Col: 1 2 3 4 5 6
|
||||
* Row 1: Neck — Head Sigil(Blue) Sigil(Yellow) Sigil(Red)
|
||||
* Row 2: Trinket — ChestArmor — — Cloak
|
||||
* Row 3: Bracelet(L) UpperArmArmor AbdomenArmor — Bracelet(R) ChestWear(Shirt)
|
||||
* Row 4: Ring(L) LowerArmArmor UpperLegArmor — Ring(R) AbdomenWear(Pants)
|
||||
* Row 5: — Hands — LowerLegArmor — —
|
||||
* Row 6: Shield — — Feet Weapon Ammo
|
||||
*/
|
||||
const EQUIP_SLOTS = {
|
||||
// Row 1: Necklace, Head, 3× Aetheria/Sigil
|
||||
32768: { name: 'Neck', row: 1, col: 1 }, // EquipMask.NeckWear
|
||||
1: { name: 'Head', row: 1, col: 3 }, // EquipMask.HeadWear
|
||||
268435456: { name: 'Sigil (Blue)', row: 1, col: 5 }, // EquipMask.SigilOne
|
||||
536870912: { name: 'Sigil (Yellow)', row: 1, col: 6 }, // EquipMask.SigilTwo
|
||||
1073741824: { name: 'Sigil (Red)', row: 1, col: 7 }, // EquipMask.SigilThree
|
||||
|
||||
// Row 2: Trinket, Chest Armor, Cloak
|
||||
67108864: { name: 'Trinket', row: 2, col: 1 }, // EquipMask.TrinketOne
|
||||
2048: { name: 'Upper Arm Armor',row: 2, col: 2 }, // EquipMask.UpperArmArmor
|
||||
512: { name: 'Chest Armor', row: 2, col: 3 }, // EquipMask.ChestArmor
|
||||
134217728: { name: 'Cloak', row: 2, col: 7 }, // EquipMask.Cloak
|
||||
|
||||
// Row 3: Bracelet(L), Lower Arm Armor, Abdomen Armor, Upper Leg Armor, Bracelet(R), Shirt
|
||||
65536: { name: 'Bracelet (L)', row: 3, col: 1 }, // EquipMask.WristWearLeft
|
||||
4096: { name: 'Lower Arm Armor',row: 3, col: 2 }, // EquipMask.LowerArmArmor
|
||||
1024: { name: 'Abdomen Armor', row: 3, col: 3 }, // EquipMask.AbdomenArmor
|
||||
8192: { name: 'Upper Leg Armor',row: 3, col: 4 }, // EquipMask.UpperLegArmor
|
||||
131072: { name: 'Bracelet (R)', row: 3, col: 5 }, // EquipMask.WristWearRight
|
||||
2: { name: 'Shirt', row: 3, col: 7 }, // EquipMask.ChestWear
|
||||
|
||||
// Row 4: Ring(L), Hands, Lower Leg Armor, Ring(R), Pants
|
||||
262144: { name: 'Ring (L)', row: 4, col: 1 }, // EquipMask.FingerWearLeft
|
||||
32: { name: 'Hands', row: 4, col: 2 }, // EquipMask.HandWear
|
||||
16384: { name: 'Lower Leg Armor',row: 4, col: 4 }, // EquipMask.LowerLegArmor
|
||||
524288: { name: 'Ring (R)', row: 4, col: 5 }, // EquipMask.FingerWearRight
|
||||
4: { name: 'Pants', row: 4, col: 7 }, // EquipMask.AbdomenWear
|
||||
|
||||
// Row 5: Feet
|
||||
256: { name: 'Feet', row: 5, col: 4 }, // EquipMask.FootWear
|
||||
|
||||
// Row 6: Shield, Weapon, Ammo
|
||||
2097152: { name: 'Shield', row: 6, col: 1 }, // EquipMask.Shield
|
||||
1048576: { name: 'Melee Weapon', row: 6, col: 3 }, // EquipMask.MeleeWeapon
|
||||
4194304: { name: 'Missile Weapon', row: 6, col: 3 }, // EquipMask.MissileWeapon
|
||||
16777216: { name: 'Held', row: 6, col: 3 }, // EquipMask.Held
|
||||
33554432: { name: 'Two Handed', row: 6, col: 3 }, // EquipMask.TwoHanded
|
||||
8388608: { name: 'Ammo', row: 6, col: 7 }, // EquipMask.Ammunition
|
||||
};
|
||||
|
||||
const SLOT_COLORS = {};
|
||||
// Purple: jewelry
|
||||
[32768, 67108864, 65536, 131072, 262144, 524288].forEach(m => SLOT_COLORS[m] = 'slot-purple');
|
||||
// Blue: armor
|
||||
[1, 512, 2048, 1024, 4096, 8192, 16384, 32, 256].forEach(m => SLOT_COLORS[m] = 'slot-blue');
|
||||
// Teal: clothing/misc
|
||||
[2, 4, 134217728, 268435456, 536870912, 1073741824].forEach(m => SLOT_COLORS[m] = 'slot-teal');
|
||||
// Dark blue: weapons/combat
|
||||
[2097152, 1048576, 4194304, 16777216, 33554432, 8388608].forEach(m => SLOT_COLORS[m] = 'slot-darkblue');
|
||||
|
||||
/**
|
||||
* Handle live inventory delta updates from WebSocket.
|
||||
* Updates the inventory grid for a character if their inventory window is open.
|
||||
|
|
@ -1030,35 +1229,260 @@ function createInventorySlot(item) {
|
|||
function updateInventoryLive(delta) {
|
||||
const name = delta.character_name;
|
||||
const win = inventoryWindows[name];
|
||||
if (!win) return; // No inventory window open for this character
|
||||
if (!win || !win._inventoryState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const grid = win.querySelector('.inventory-grid');
|
||||
if (!grid) return;
|
||||
const state = win._inventoryState;
|
||||
const getItemId = (d) => {
|
||||
if (d.item) return d.item.item_id || d.item.Id || d.item.id;
|
||||
return d.item_id;
|
||||
};
|
||||
|
||||
const itemId = getItemId(delta);
|
||||
|
||||
if (delta.action === 'remove') {
|
||||
const itemId = delta.item_id || (delta.item && (delta.item.Id || delta.item.id));
|
||||
const existing = grid.querySelector(`[data-item-id="${itemId}"]`);
|
||||
if (existing) existing.remove();
|
||||
} else if (delta.action === 'add') {
|
||||
const newSlot = createInventorySlot(delta.item);
|
||||
grid.appendChild(newSlot);
|
||||
} else if (delta.action === 'update') {
|
||||
const itemId = delta.item.Id || delta.item.id || delta.item.item_id;
|
||||
const existing = grid.querySelector(`[data-item-id="${itemId}"]`);
|
||||
if (existing) {
|
||||
const newSlot = createInventorySlot(delta.item);
|
||||
existing.replaceWith(newSlot);
|
||||
state.items = state.items.filter(i => (i.item_id || i.Id || i.id) !== itemId);
|
||||
} else if (delta.action === 'add' || delta.action === 'update') {
|
||||
normalizeInventoryItem(delta.item);
|
||||
const existingIdx = state.items.findIndex(i => (i.item_id || i.Id || i.id) === itemId);
|
||||
if (existingIdx >= 0) {
|
||||
state.items[existingIdx] = delta.item;
|
||||
} else {
|
||||
const newSlot = createInventorySlot(delta.item);
|
||||
grid.appendChild(newSlot);
|
||||
state.items.push(delta.item);
|
||||
}
|
||||
}
|
||||
|
||||
// Update item count
|
||||
const countEl = win.querySelector('.inventory-count');
|
||||
if (countEl) {
|
||||
const slotCount = grid.querySelectorAll('.inventory-slot').length;
|
||||
countEl.textContent = `${slotCount} items`;
|
||||
renderInventoryState(state);
|
||||
}
|
||||
|
||||
function renderInventoryState(state) {
|
||||
// 1. Clear equipment slots
|
||||
state.slotMap.forEach((slotEl) => {
|
||||
slotEl.innerHTML = '';
|
||||
const colorClass = slotEl.className.match(/slot-\w+/)?.[0] || '';
|
||||
slotEl.className = `inv-equip-slot empty ${colorClass}`;
|
||||
delete slotEl.dataset.itemId;
|
||||
});
|
||||
|
||||
// 2. Identify containers (object_class === 10) by item_id for sidebar
|
||||
// These are packs/sacks/pouches/foci that appear in inventory as items
|
||||
// but should ONLY show in the pack sidebar, not in the item grid.
|
||||
const containers = []; // container objects (object_class=10)
|
||||
const containerItemIds = new Set(); // item_ids of containers (to exclude from grid)
|
||||
|
||||
state.items.forEach(item => {
|
||||
if (item.object_class === 10) {
|
||||
containers.push(item);
|
||||
containerItemIds.add(item.item_id);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Separate equipped items from pack items, excluding containers from grid
|
||||
let totalBurden = 0;
|
||||
const packItems = new Map(); // container_id → [items] (non-container items only)
|
||||
|
||||
// Determine the character body container_id: items with wielded_location > 0
|
||||
// share a container_id that is NOT 0 and NOT a pack's item_id.
|
||||
// We treat non-wielded items from the body container as "main backpack" items.
|
||||
let bodyContainerId = null;
|
||||
state.items.forEach(item => {
|
||||
if (item.current_wielded_location && item.current_wielded_location > 0) {
|
||||
const cid = item.container_id;
|
||||
if (cid && cid !== 0 && !containerItemIds.has(cid)) {
|
||||
bodyContainerId = cid;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.items.forEach(item => {
|
||||
totalBurden += (item.burden || 0);
|
||||
|
||||
// Skip container objects — they go in sidebar only
|
||||
if (containerItemIds.has(item.item_id)) return;
|
||||
|
||||
if (item.current_wielded_location && item.current_wielded_location > 0) {
|
||||
const mask = item.current_wielded_location;
|
||||
const isArmor = item.object_class === 2;
|
||||
|
||||
// For armor (object_class=2): render in ALL matching slots (multi-slot display)
|
||||
// For everything else (clothing, jewelry, weapons): place in first matching slot only
|
||||
if (isArmor) {
|
||||
Object.keys(EQUIP_SLOTS).forEach(m => {
|
||||
const slotMask = parseInt(m);
|
||||
if ((mask & slotMask) === slotMask) {
|
||||
const slotDef = EQUIP_SLOTS[slotMask];
|
||||
const key = `${slotDef.row}-${slotDef.col}`;
|
||||
if (state.slotMap.has(key)) {
|
||||
const slotEl = state.slotMap.get(key);
|
||||
if (!slotEl.dataset.itemId) {
|
||||
slotEl.innerHTML = '';
|
||||
const colorClass = slotEl.className.match(/slot-\w+/)?.[0] || '';
|
||||
slotEl.className = `inv-equip-slot equipped ${colorClass}`;
|
||||
slotEl.dataset.itemId = item.item_id;
|
||||
slotEl.appendChild(createInventorySlot(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Non-armor: find the first matching slot by exact mask key, then by bit overlap
|
||||
let placed = false;
|
||||
// Try exact mask match first (e.g. necklace mask=32768 matches key 32768 directly)
|
||||
if (EQUIP_SLOTS[mask]) {
|
||||
const slotDef = EQUIP_SLOTS[mask];
|
||||
const key = `${slotDef.row}-${slotDef.col}`;
|
||||
if (state.slotMap.has(key)) {
|
||||
const slotEl = state.slotMap.get(key);
|
||||
if (!slotEl.dataset.itemId) {
|
||||
slotEl.innerHTML = '';
|
||||
const colorClass = slotEl.className.match(/slot-\w+/)?.[0] || '';
|
||||
slotEl.className = `inv-equip-slot equipped ${colorClass}`;
|
||||
slotEl.dataset.itemId = item.item_id;
|
||||
slotEl.appendChild(createInventorySlot(item));
|
||||
placed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no exact match, find first matching bit in EQUIP_SLOTS
|
||||
if (!placed) {
|
||||
for (const m of Object.keys(EQUIP_SLOTS)) {
|
||||
const slotMask = parseInt(m);
|
||||
if ((mask & slotMask) === slotMask) {
|
||||
const slotDef = EQUIP_SLOTS[slotMask];
|
||||
const key = `${slotDef.row}-${slotDef.col}`;
|
||||
if (state.slotMap.has(key)) {
|
||||
const slotEl = state.slotMap.get(key);
|
||||
if (!slotEl.dataset.itemId) {
|
||||
slotEl.innerHTML = '';
|
||||
const colorClass = slotEl.className.match(/slot-\w+/)?.[0] || '';
|
||||
slotEl.className = `inv-equip-slot equipped ${colorClass}`;
|
||||
slotEl.dataset.itemId = item.item_id;
|
||||
slotEl.appendChild(createInventorySlot(item));
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-equipped, non-container → pack item. Group by container_id.
|
||||
let cid = item.container_id || 0;
|
||||
// Items on the character body (not wielded) → treat as main backpack (cid=0)
|
||||
if (bodyContainerId !== null && cid === bodyContainerId) cid = 0;
|
||||
if (!packItems.has(cid)) packItems.set(cid, []);
|
||||
packItems.get(cid).push(item);
|
||||
}
|
||||
});
|
||||
|
||||
state.burdenLabel.textContent = 'Burden';
|
||||
state.burdenFill.style.height = '0%';
|
||||
|
||||
// 4. Sort containers for stable sidebar order (by unsigned item_id)
|
||||
containers.sort((a, b) => {
|
||||
const ua = a.item_id >>> 0;
|
||||
const ub = b.item_id >>> 0;
|
||||
return ua - ub;
|
||||
});
|
||||
|
||||
// 5. Render packs in sidebar
|
||||
state.packList.innerHTML = '';
|
||||
|
||||
// Helper: compute icon URL from raw icon id
|
||||
const iconUrl = (iconRaw) => {
|
||||
if (!iconRaw) return '/icons/06001080.png';
|
||||
const hex = (iconRaw + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||
return `/icons/${hex}.png`;
|
||||
};
|
||||
|
||||
// --- Main backpack (container_id === 0, non-containers) ---
|
||||
const mainPackEl = document.createElement('div');
|
||||
mainPackEl.className = `inv-pack-icon ${state.activePack === null ? 'active' : ''}`;
|
||||
const mainPackImg = document.createElement('img');
|
||||
mainPackImg.src = '/icons/06001BB1.png';
|
||||
mainPackImg.onerror = function() { this.src = '/icons/06000133.png'; };
|
||||
|
||||
const mainFillCont = document.createElement('div');
|
||||
mainFillCont.className = 'inv-pack-fill-container';
|
||||
const mainFill = document.createElement('div');
|
||||
mainFill.className = 'inv-pack-fill';
|
||||
|
||||
// Main backpack items = container_id 0, excluding container objects
|
||||
const mainPackItems = packItems.get(0) || [];
|
||||
const mainPct = Math.min(100, (mainPackItems.length / 102) * 100);
|
||||
mainFill.style.height = `${mainPct}%`;
|
||||
|
||||
mainFillCont.appendChild(mainFill);
|
||||
mainPackEl.appendChild(mainPackImg);
|
||||
mainPackEl.appendChild(mainFillCont);
|
||||
|
||||
mainPackEl.onclick = () => {
|
||||
state.activePack = null;
|
||||
renderInventoryState(state);
|
||||
};
|
||||
state.packList.appendChild(mainPackEl);
|
||||
|
||||
// --- Sub-packs: each container object (object_class=10) ---
|
||||
containers.forEach(container => {
|
||||
const cid = container.item_id; // items inside this pack have container_id = this item_id
|
||||
const packEl = document.createElement('div');
|
||||
packEl.className = `inv-pack-icon ${state.activePack === cid ? 'active' : ''}`;
|
||||
const packImg = document.createElement('img');
|
||||
// Use the container's actual icon from the API
|
||||
packImg.src = iconUrl(container.icon);
|
||||
packImg.onerror = function() { this.src = '/icons/06001080.png'; };
|
||||
|
||||
const fillCont = document.createElement('div');
|
||||
fillCont.className = 'inv-pack-fill-container';
|
||||
const fill = document.createElement('div');
|
||||
fill.className = 'inv-pack-fill';
|
||||
|
||||
const pItems = packItems.get(cid) || [];
|
||||
const capacity = container.items_capacity || 24; // default pack capacity in AC
|
||||
const pPct = Math.min(100, (pItems.length / capacity) * 100);
|
||||
fill.style.height = `${pPct}%`;
|
||||
|
||||
fillCont.appendChild(fill);
|
||||
packEl.appendChild(packImg);
|
||||
packEl.appendChild(fillCont);
|
||||
|
||||
packEl.onclick = () => {
|
||||
state.activePack = cid;
|
||||
renderInventoryState(state);
|
||||
};
|
||||
state.packList.appendChild(packEl);
|
||||
});
|
||||
|
||||
// 6. Render item grid
|
||||
state.itemGrid.innerHTML = '';
|
||||
let itemsToShow = [];
|
||||
if (state.activePack === null) {
|
||||
// Main backpack: non-container items with container_id === 0
|
||||
itemsToShow = mainPackItems;
|
||||
state.contentsHeader.textContent = 'Contents of Backpack';
|
||||
} else {
|
||||
// Sub-pack: items with matching container_id
|
||||
itemsToShow = packItems.get(state.activePack) || [];
|
||||
// Use the container's name for the header
|
||||
const activeContainer = containers.find(c => c.item_id === state.activePack);
|
||||
state.contentsHeader.textContent = activeContainer
|
||||
? `Contents of ${activeContainer.name}`
|
||||
: 'Contents of Pack';
|
||||
}
|
||||
|
||||
const numCells = Math.max(24, Math.ceil(itemsToShow.length / 6) * 6);
|
||||
for (let i = 0; i < numCells; i++) {
|
||||
const cell = document.createElement('div');
|
||||
if (i < itemsToShow.length) {
|
||||
cell.className = 'inv-item-slot occupied';
|
||||
const itemNode = createInventorySlot(itemsToShow[i]);
|
||||
cell.appendChild(itemNode);
|
||||
} else {
|
||||
cell.className = 'inv-item-slot';
|
||||
}
|
||||
state.itemGrid.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1078,19 +1502,120 @@ function showInventoryWindow(name) {
|
|||
win.dataset.character = name;
|
||||
inventoryWindows[name] = win;
|
||||
|
||||
// Loading message
|
||||
const loading = document.createElement('div');
|
||||
loading.className = 'inventory-loading';
|
||||
loading.textContent = 'Loading inventory...';
|
||||
content.appendChild(loading);
|
||||
|
||||
// Inventory content container
|
||||
const invContent = document.createElement('div');
|
||||
invContent.className = 'inventory-content';
|
||||
invContent.style.display = 'none';
|
||||
content.appendChild(invContent);
|
||||
|
||||
// Fetch inventory data from main app (which will proxy to inventory service)
|
||||
const topSection = document.createElement('div');
|
||||
topSection.className = 'inv-top-section';
|
||||
|
||||
const equipGrid = document.createElement('div');
|
||||
equipGrid.className = 'inv-equipment-grid';
|
||||
|
||||
const slotMap = new Map();
|
||||
const createdSlots = new Set();
|
||||
|
||||
Object.entries(EQUIP_SLOTS).forEach(([mask, slotDef]) => {
|
||||
const key = `${slotDef.row}-${slotDef.col}`;
|
||||
if (!createdSlots.has(key)) {
|
||||
createdSlots.add(key);
|
||||
const slotEl = document.createElement('div');
|
||||
const colorClass = SLOT_COLORS[parseInt(mask)] || 'slot-darkblue';
|
||||
slotEl.className = `inv-equip-slot empty ${colorClass}`;
|
||||
slotEl.style.left = `${(slotDef.col - 1) * 44}px`;
|
||||
slotEl.style.top = `${(slotDef.row - 1) * 44}px`;
|
||||
slotEl.dataset.pos = key;
|
||||
equipGrid.appendChild(slotEl);
|
||||
slotMap.set(key, slotEl);
|
||||
}
|
||||
});
|
||||
|
||||
const sidebar = document.createElement('div');
|
||||
sidebar.className = 'inv-sidebar';
|
||||
|
||||
const burdenContainer = document.createElement('div');
|
||||
burdenContainer.className = 'inv-burden-bar';
|
||||
const burdenFill = document.createElement('div');
|
||||
burdenFill.className = 'inv-burden-fill';
|
||||
const burdenLabel = document.createElement('div');
|
||||
burdenLabel.className = 'inv-burden-label';
|
||||
burdenLabel.textContent = 'Burden';
|
||||
burdenContainer.appendChild(burdenLabel);
|
||||
burdenContainer.appendChild(burdenFill);
|
||||
sidebar.appendChild(burdenContainer);
|
||||
|
||||
const packList = document.createElement('div');
|
||||
packList.className = 'inv-pack-list';
|
||||
sidebar.appendChild(packList);
|
||||
|
||||
topSection.appendChild(equipGrid);
|
||||
topSection.appendChild(sidebar);
|
||||
|
||||
const bottomSection = document.createElement('div');
|
||||
bottomSection.className = 'inv-bottom-section';
|
||||
|
||||
const contentsHeader = document.createElement('div');
|
||||
contentsHeader.className = 'inv-contents-header';
|
||||
contentsHeader.textContent = 'Contents of Backpack';
|
||||
|
||||
const itemGrid = document.createElement('div');
|
||||
itemGrid.className = 'inv-item-grid';
|
||||
|
||||
bottomSection.appendChild(contentsHeader);
|
||||
bottomSection.appendChild(itemGrid);
|
||||
|
||||
invContent.appendChild(topSection);
|
||||
invContent.appendChild(bottomSection);
|
||||
|
||||
const resizeGrip = document.createElement('div');
|
||||
resizeGrip.className = 'inv-resize-grip';
|
||||
win.appendChild(resizeGrip);
|
||||
|
||||
let resizing = false;
|
||||
let startY, startH;
|
||||
|
||||
resizeGrip.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resizing = true;
|
||||
startY = e.clientY;
|
||||
startH = win.offsetHeight;
|
||||
document.body.style.cursor = 'ns-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!resizing) return;
|
||||
const newH = Math.max(400, startH + (e.clientY - startY));
|
||||
win.style.height = newH + 'px';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (!resizing) return;
|
||||
resizing = false;
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
});
|
||||
|
||||
win._inventoryState = {
|
||||
items: [],
|
||||
activePack: null,
|
||||
slotMap: slotMap,
|
||||
equipGrid: equipGrid,
|
||||
itemGrid: itemGrid,
|
||||
packList: packList,
|
||||
burdenFill: burdenFill,
|
||||
burdenLabel: burdenLabel,
|
||||
contentsHeader: contentsHeader,
|
||||
characterName: name
|
||||
};
|
||||
|
||||
fetch(`${API_BASE}/inventory/${encodeURIComponent(name)}?limit=1000`)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
|
@ -1098,24 +1623,12 @@ function showInventoryWindow(name) {
|
|||
})
|
||||
.then(data => {
|
||||
loading.style.display = 'none';
|
||||
invContent.style.display = 'block';
|
||||
invContent.style.display = 'flex';
|
||||
|
||||
// Create inventory grid
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'inventory-grid';
|
||||
data.items.forEach(i => normalizeInventoryItem(i));
|
||||
win._inventoryState.items = data.items;
|
||||
|
||||
// Render each item
|
||||
data.items.forEach(item => {
|
||||
grid.appendChild(createInventorySlot(item));
|
||||
});
|
||||
|
||||
invContent.appendChild(grid);
|
||||
|
||||
// Add item count
|
||||
const count = document.createElement('div');
|
||||
count.className = 'inventory-count';
|
||||
count.textContent = `${data.item_count} items`;
|
||||
invContent.appendChild(count);
|
||||
renderInventoryState(win._inventoryState);
|
||||
})
|
||||
.catch(err => {
|
||||
handleError('Inventory', err, true);
|
||||
|
|
|
|||
546
static/style.css
546
static/style.css
|
|
@ -709,13 +709,15 @@ body.noselect, body.noselect * {
|
|||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* ---------- inventory window styling ----------------------------- */
|
||||
/* ---------- inventory window styling (AC Layout) ----------------------------- */
|
||||
.inventory-content {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: none;
|
||||
color: var(--ac-text);
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.inventory-placeholder {
|
||||
|
|
@ -733,15 +735,18 @@ body.noselect, body.noselect * {
|
|||
position: fixed;
|
||||
top: 100px;
|
||||
left: 400px;
|
||||
width: 600px;
|
||||
height: 500px;
|
||||
background: var(--card);
|
||||
border: 1px solid #555;
|
||||
border-radius: 8px;
|
||||
width: 390px;
|
||||
height: 520px;
|
||||
background: rgba(20, 20, 20, 0.92);
|
||||
backdrop-filter: blur(2px);
|
||||
border: 2px solid var(--ac-gold);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: inset 0 0 10px #000, 0 4px 15px rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
font-family: "Palatino Linotype", "Book Antiqua", Palatino, serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.inventory-loading {
|
||||
|
|
@ -750,37 +755,229 @@ body.noselect, body.noselect * {
|
|||
justify-content: center;
|
||||
height: 100%;
|
||||
font-size: 1.1rem;
|
||||
color: #888;
|
||||
color: var(--ac-text-dim);
|
||||
}
|
||||
|
||||
/* Inventory grid layout - matches AC original */
|
||||
.inventory-grid {
|
||||
.inv-top-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 264px;
|
||||
}
|
||||
|
||||
.inv-equipment-grid {
|
||||
position: relative;
|
||||
width: 308px;
|
||||
height: 264px;
|
||||
}
|
||||
|
||||
.inv-equip-slot {
|
||||
position: absolute;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--ac-medium-stone);
|
||||
border-top: 2px solid #3d4b5f;
|
||||
border-left: 2px solid #3d4b5f;
|
||||
border-bottom: 2px solid #12181a;
|
||||
border-right: 2px solid #12181a;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.inv-equip-slot.equipped {
|
||||
border: 2px solid var(--ac-cyan);
|
||||
box-shadow: 0 0 5px var(--ac-cyan), inset 0 0 5px var(--ac-cyan);
|
||||
}
|
||||
|
||||
.inv-equip-slot.empty::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-image: url('/icons/06000133.png');
|
||||
background-size: contain;
|
||||
opacity: 0.15;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.inv-equip-slot .inventory-slot {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.inv-sidebar {
|
||||
width: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.inv-burden-bar {
|
||||
width: 16px;
|
||||
height: 40px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid var(--ac-border-light);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
margin-bottom: 2px;
|
||||
margin-top: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inv-burden-fill {
|
||||
width: 100%;
|
||||
background: var(--ac-green);
|
||||
height: 0%;
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
|
||||
.inv-burden-label {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
width: 60px;
|
||||
left: -22px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: var(--ac-gold);
|
||||
}
|
||||
|
||||
.inv-pack-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.inv-pack-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inv-pack-icon.active {
|
||||
border: 1px solid var(--ac-green);
|
||||
box-shadow: 0 0 4px var(--ac-green);
|
||||
}
|
||||
|
||||
.inv-pack-icon.active::before {
|
||||
content: "▶";
|
||||
position: absolute;
|
||||
left: -14px;
|
||||
top: 10px;
|
||||
color: var(--ac-gold);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.inv-pack-fill-container {
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: -1px;
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
background: #000;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.inv-pack-fill {
|
||||
height: 100%;
|
||||
background: var(--ac-green);
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.inv-pack-icon img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.inv-bottom-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 10px;
|
||||
margin-right: 52px;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.inv-contents-header {
|
||||
color: var(--ac-gold);
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid var(--ac-border-light);
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.inv-item-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 36px);
|
||||
gap: 0px;
|
||||
padding: 8px;
|
||||
background:
|
||||
linear-gradient(90deg, #333 1px, transparent 1px),
|
||||
linear-gradient(180deg, #333 1px, transparent 1px),
|
||||
#111;
|
||||
background-size: 36px 36px;
|
||||
max-height: 450px;
|
||||
grid-template-columns: repeat(6, 36px);
|
||||
grid-auto-rows: 36px;
|
||||
gap: 2px;
|
||||
background: var(--ac-black);
|
||||
padding: 4px;
|
||||
border: 1px solid var(--ac-border-light);
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #444;
|
||||
min-height: 0;
|
||||
align-content: start;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
/* Individual inventory slots - no borders like AC original */
|
||||
.inv-item-grid::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
.inv-item-grid::-webkit-scrollbar-track {
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
.inv-item-grid::-webkit-scrollbar-thumb {
|
||||
background: #0022cc;
|
||||
border-top: 2px solid var(--ac-gold);
|
||||
border-bottom: 2px solid var(--ac-gold);
|
||||
}
|
||||
|
||||
.inv-item-slot {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #222;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.inv-item-slot.occupied {
|
||||
background: linear-gradient(135deg, #3d007a 0%, #1a0033 100%);
|
||||
border: 1px solid #4a148c;
|
||||
}
|
||||
|
||||
/* Base slot styling used by createInventorySlot */
|
||||
.inventory-slot {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s ease;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -794,14 +991,11 @@ body.noselect, body.noselect * {
|
|||
height: 36px;
|
||||
object-fit: contain;
|
||||
image-rendering: pixelated;
|
||||
/* Improve icon appearance - make background match slot */
|
||||
border: none;
|
||||
outline: none;
|
||||
background: #1a1a1a;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Icon compositing for overlays/underlays - matches AC original */
|
||||
/* Icon compositing */
|
||||
.item-icon-composite {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
|
|
@ -827,24 +1021,13 @@ body.noselect, body.noselect * {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.icon-underlay {
|
||||
z-index: 1;
|
||||
}
|
||||
.icon-underlay { z-index: 1; }
|
||||
.icon-base { z-index: 2; }
|
||||
.icon-overlay { z-index: 3; }
|
||||
|
||||
.icon-base {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.icon-overlay {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* Item count */
|
||||
/* Item count (hidden in new AC layout, kept for compatibility) */
|
||||
.inventory-count {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Inventory tooltip */
|
||||
|
|
@ -1848,3 +2031,272 @@ table.ts-allegiance td:first-child {
|
|||
border-color: #af7a30;
|
||||
}
|
||||
|
||||
|
||||
/* ==============================================
|
||||
Inventory Window Visual Fixes - AC Game Match
|
||||
============================================== */
|
||||
|
||||
.inventory-window,
|
||||
.inventory-window * {
|
||||
font-family: "Times New Roman", Times, serif !important;
|
||||
text-shadow: 1px 1px 0 #000 !important;
|
||||
}
|
||||
|
||||
.inventory-window .chat-header {
|
||||
background: #0e0c08 !important;
|
||||
border-bottom: 1px solid #8a7a44 !important;
|
||||
color: #d4af37 !important;
|
||||
padding: 4px 6px !important;
|
||||
box-shadow: none !important;
|
||||
font-size: 11px !important;
|
||||
font-weight: bold !important;
|
||||
height: 22px !important;
|
||||
box-sizing: border-box !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.inventory-window .window-content {
|
||||
background: linear-gradient(180deg, #1a1814 0%, #0e0c0a 100%) !important;
|
||||
border: 2px solid #8a7a44 !important;
|
||||
padding: 4px !important;
|
||||
}
|
||||
|
||||
.inv-equipment-grid {
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 50%, rgba(30, 28, 25, 0.6) 0%, transparent 70%),
|
||||
radial-gradient(ellipse at 80% 30%, rgba(25, 23, 20, 0.4) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 50% 80%, rgba(35, 30, 25, 0.5) 0%, transparent 50%),
|
||||
linear-gradient(180deg, #0e0c0a 0%, #141210 50%, #0c0a08 100%) !important;
|
||||
}
|
||||
|
||||
.inv-equip-slot {
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
border-top: 1px solid #2a2a30 !important;
|
||||
border-left: 1px solid #2a2a30 !important;
|
||||
border-bottom: 1px solid #0a0a0e !important;
|
||||
border-right: 1px solid #0a0a0e !important;
|
||||
background: #14141a !important;
|
||||
}
|
||||
|
||||
.inv-equip-slot.equipped {
|
||||
border: 1px solid #222 !important;
|
||||
background: #14141a !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Equipment slot color categories - matching real AC
|
||||
Real AC uses clearly visible colored borders AND tinted backgrounds per slot type */
|
||||
.inv-equip-slot.slot-purple {
|
||||
border: 1px solid #8040a8 !important;
|
||||
background: #2a1538 !important;
|
||||
}
|
||||
.inv-equip-slot.slot-blue {
|
||||
border: 1px solid #3060b0 !important;
|
||||
background: #141e38 !important;
|
||||
}
|
||||
.inv-equip-slot.slot-teal {
|
||||
border: 1px solid #309898 !important;
|
||||
background: #0e2828 !important;
|
||||
}
|
||||
.inv-equip-slot.slot-darkblue {
|
||||
border: 1px solid #1e3060 !important;
|
||||
background: #0e1428 !important;
|
||||
}
|
||||
/* Brighter tint when equipped (item present) */
|
||||
.inv-equip-slot.equipped.slot-purple {
|
||||
border: 1px solid #9050b8 !important;
|
||||
background: #341a44 !important;
|
||||
}
|
||||
.inv-equip-slot.equipped.slot-blue {
|
||||
border: 1px solid #4070c0 !important;
|
||||
background: #1a2844 !important;
|
||||
}
|
||||
.inv-equip-slot.equipped.slot-teal {
|
||||
border: 1px solid #40a8a8 !important;
|
||||
background: #143030 !important;
|
||||
}
|
||||
.inv-equip-slot.equipped.slot-darkblue {
|
||||
border: 1px solid #283870 !important;
|
||||
background: #141a30 !important;
|
||||
}
|
||||
|
||||
.inv-equip-slot.empty::before {
|
||||
opacity: 0.15 !important;
|
||||
filter: grayscale(100%) !important;
|
||||
}
|
||||
|
||||
.inv-item-grid {
|
||||
background: #1a1208 !important;
|
||||
gap: 2px !important;
|
||||
}
|
||||
|
||||
.inv-item-slot.occupied {
|
||||
background: #442c1e !important;
|
||||
border: 1px solid #5a3c28 !important;
|
||||
}
|
||||
|
||||
.inv-item-slot {
|
||||
background: #2a1c14 !important;
|
||||
border: 1px solid #3a2818 !important;
|
||||
}
|
||||
|
||||
.inv-contents-header {
|
||||
font-size: 10px !important;
|
||||
font-family: "Times New Roman", Times, serif !important;
|
||||
color: #ffffff !important;
|
||||
border-bottom: none !important;
|
||||
text-align: center !important;
|
||||
padding-bottom: 2px !important;
|
||||
margin-bottom: 2px !important;
|
||||
text-transform: none !important;
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
|
||||
.inv-sidebar {
|
||||
width: 52px !important;
|
||||
align-items: center !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.inv-pack-icon {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
border: 1px solid #1a1a1a !important;
|
||||
margin-bottom: 2px !important;
|
||||
overflow: visible !important;
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
|
||||
.inv-pack-icon img {
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
}
|
||||
|
||||
.inv-pack-icon.active {
|
||||
border: 1px solid #8a7a44 !important;
|
||||
position: relative !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.inv-pack-icon.active::before {
|
||||
content: '' !important;
|
||||
position: absolute !important;
|
||||
left: -8px !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
border-top: 6px solid transparent !important;
|
||||
border-bottom: 6px solid transparent !important;
|
||||
border-left: 7px solid #d4af37 !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.inv-pack-fill-container {
|
||||
position: absolute !important;
|
||||
right: -6px !important;
|
||||
top: 0 !important;
|
||||
bottom: auto !important;
|
||||
left: auto !important;
|
||||
width: 4px !important;
|
||||
height: 32px !important;
|
||||
background: #000 !important;
|
||||
border: 1px solid #333 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column-reverse !important;
|
||||
}
|
||||
|
||||
.inv-pack-fill {
|
||||
width: 100% !important;
|
||||
background: #00ff00 !important;
|
||||
transition: height 0.3s ease !important;
|
||||
}
|
||||
|
||||
.inv-item-grid::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
}
|
||||
.inv-item-grid::-webkit-scrollbar-track {
|
||||
background: #0e0a04;
|
||||
border: 1px solid #8a7a44;
|
||||
}
|
||||
.inv-item-grid::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, #2244aa 0%, #1a3399 50%, #2244aa 100%);
|
||||
border: 1px solid #8a7a44;
|
||||
}
|
||||
.inv-item-grid::-webkit-scrollbar-button:vertical:start:decrement,
|
||||
.inv-item-grid::-webkit-scrollbar-button:vertical:end:increment {
|
||||
background: #8a2020;
|
||||
border: 1px solid #b89a30;
|
||||
height: 14px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inv-burden-bar {
|
||||
width: 14px !important;
|
||||
height: 40px !important;
|
||||
margin-top: 20px !important;
|
||||
}
|
||||
|
||||
.inv-burden-label {
|
||||
position: absolute !important;
|
||||
top: -20px !important;
|
||||
width: 60px !important;
|
||||
left: -22px !important;
|
||||
text-align: center !important;
|
||||
font-size: 9px !important;
|
||||
color: #fff !important;
|
||||
font-weight: normal !important;
|
||||
line-height: 1.1 !important;
|
||||
}
|
||||
|
||||
.inventory-count {
|
||||
display: block !important;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
font-size: 8px !important;
|
||||
color: #fff !important;
|
||||
background: #1a3399 !important;
|
||||
padding: 0 2px !important;
|
||||
line-height: 12px !important;
|
||||
min-width: 8px !important;
|
||||
text-align: center !important;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.inventory-window {
|
||||
border: 2px solid #8a7a44 !important;
|
||||
background: #0e0c08 !important;
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
/* Custom resize grip for inventory window */
|
||||
.inv-resize-grip {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 6px;
|
||||
cursor: ns-resize;
|
||||
z-index: 100;
|
||||
background: transparent;
|
||||
border-top: 1px solid #8a7a44;
|
||||
}
|
||||
|
||||
.inv-resize-grip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 30px;
|
||||
height: 2px;
|
||||
border-top: 1px solid #5a4a24;
|
||||
border-bottom: 1px solid #5a4a24;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue