Grid inventory - take 2

Exactly two months on from my musings about this subject - as seen here; http://textadventures.co.uk/forum/quest/topic/ncyrvbqk90ifgqmx6ev0_a/visual-player-inventory

I'm back on trying to implement it, after relaxing my time constraints and deciding that the current unsortable list-based system is an obstacle to a user-friendly large-scale RPG game.

As my knowledge on js frankly wouldn't fill one side of a sticky note, I'm starting from the beginning - making a grid - and moving on from there. I have the grid made, I have the dimensions of everything set - 40px columns/rows, 2px grid gap, 3px padding, totally 214px in width and height, where 214 px is the current size of my panes. Grid items have a border of 1px with modifiable colours, which I can probably use to highlight the item last clicked on, or colour code items by their type - consumable, armour, weapon, etc.

Next up is figuring out how to append objects and thumbnails to those grid items, and how to get them to drag and drop around the grid, along with swapping.

... Easier said than done with no knowledge of the topic at hand, but what better time to learn than during a lockdown! If anyone thinks they can help, I'd more than welcome it - ideally I'd like to make this a library available for anyone to use, as I think it's something Quest could sorely use!

Goals -

  • 5x5 grid inventory system, support for drag and drop reorganizing, support for item swapping on dropping on a filled space.

  • Support for tying existing game objects to the new inventory system - appending small images to each item.

  • Support for the game recording and displaying this information on game save/load.

  • Stacking of consumable items - e.g rather than 2 Health Potions taking up 2 slots, they should take up 1 slot with a '2' in the corner.

  • Clicking on an item in the inventory will display its verbs as normal, but must also display the item's alias. Hovering over an item should display the item's name above the cursor.

  • Optional but desired - Support for item 'weight', wherein weight is a number of slots. Displayed as a number in the bottom left opposite stack count, grays out a grid slot starting from the last row and column and moving down the row for each weight above '1'. Automatically moves items caught in these greyed slots into an open slot or, if there aren't enough open slots, cancels and action and informs the player they must make room.

  • Optional - A sort button, sorting by item type/class first, then by alphabetical order. (eg; Consumable, Weapon, Armour, Outfit. Apple, Banana, Orange, Dagger, Sword, Leather Armour, Steel Plate, Noble Gown, Villager Rags.)

  • Not desired - Items taking up different grid sizes/shapes. Too complicated, as fond as my memories of Deus Ex inventory Tetris are.


For images, I'd suggest using filenames of the form inventory_(objectname).png - this system will need to be mostly written in javascript, and JS code only has access to an object's name, alias, and current verbs, so using the name avoids any need to have a turnscript passing filenames to the javascript every time you pick up an item.

To link your script to the inventory, you want to look at the existing updateList function:

function updateList(listName, listData) {
    var listElement = "";
    var buttonPrefix = "";

    if (listName == "inventory") {
        listElement = "#lstInventory";
        inventoryVerbs = new Array();
        buttonPrefix = "cmdInventory";
    }

    if (listName == "placesobjects") {
        listElement = "#lstPlacesObjects";
        placesObjectsVerbs = new Array();
        buttonPrefix = "cmdPlacesObjects";
    }

    var previousSelectionText = "";
    var previousSelectionKey = "";
    var foundPreviousSelection = false;

    var $selected = $(listElement + " .ui-selected");
    if ($selected.length > 0) {
        previousSelectionText = $selected.first().text();
        previousSelectionKey = $selected.first().data("key");
    }

    $(listElement).empty();
    var count = 0;
    $.each(listData, function (key, value) {
        var data = JSON.parse(value);
        var objectDisplayName = data["Text"];
        var verbsArray, idPrefix;

        if (listName == "inventory") {
            verbsArray = inventoryVerbs;
            idPrefix = "cmdInventory";
        } else {
            verbsArray = placesObjectsVerbs;
            idPrefix = "cmdPlacesObjects";
        }

        verbsArray.push(data);

        if (listName == "inventory" || $.inArray(objectDisplayName, _compassDirs) == -1) {
            var $newItem = $("<li/>").data("key", key).data("elementid", data["ElementId"]).data("elementname", data["ElementName"]).data("index", count).html(objectDisplayName);
            if (objectDisplayName == previousSelectionText && key == previousSelectionKey) {
                $newItem.addClass("ui-selected");
                foundPreviousSelection = true;
                updateVerbButtons($newItem, verbsArray, idPrefix);
            }
            $(listElement).append($newItem);
            count++;
        }
    });

    var selectSize = count;
    if (selectSize < 3) selectSize = 3;
    if (selectSize > 12) selectSize = 12;
    $(listElement).attr("size", selectSize);

    if (!foundPreviousSelection) {
        for (var i = 1; i <= verbButtonCount; i++) {
            var target = $("#" + buttonPrefix + i);
            target.hide();
        }
    }
}

This is the code responsible for the current panes.

I presume that you would modify this to behave differently when listName is "inventory"; in this case listData is a plain object whose members are objects with the keys "ElementId" (Quest name), "ElementName" (alias), and "Text" (listalias).

You will need to ensure that any objects in this data that aren't already in the inventory are displayed, and any objects in the inventory that aren't in this list are removed.


Just the person I wanted to hear from, haha. That should come in handy when I get that far, thank you!

For now I'm continuing to tackle things in order, which means now I'm trying to apply Sortable functionality to my existing grid. The sample code for JQuery's swappable, unfortunately, pulls from existing stylesheets for most of the setup, making it kind of... difficult to learn from. That's going slowly. Might take a break for now, then come back.

At the bottom is what I have so far, which simply generates a grid with the correct dimensions I need (and some placeholder colours). Only 3 of the items are actually filled. Now to figure out where to insert the Sortable functionality, and what I need to rewrite to get it to all point in the right directions. (Can you tell I'm new to this?!)

Trying to learn that from the example code found here; https://jqueryui.com/sortable/#display-grid
But, as I said, it pulls from existing style sheets, which complicates things a bit.

(Oh, and this isn't going anywhere near Quest itself, yet. Gunna wrap my head around setting it up before I complicate things.)

<html>
<head>
<style>
.grid-container {
  display: inline-grid;
  grid-gap: 2px;
  grid-template-columns: 40px 40px 40px 40px 40px;
  grid-template-rows: 40px 40px 40px 40px 40px;
  background-color: black;
  padding: 3px;
}

.grid-item {
  background-color: rgba(255, 100, 255, 0.8);
  border: 1px solid rgba(0, 0, 0, 0.8);
  padding: 0px;
  font-size: 36px;
  text-align: center;
}
</style>
</head>
<body>

<h1>The grid-gap Property:</h1>

<div class="grid-container">
  <div class="grid-item">1</div>
  <div class="grid-item">2</div>
  <div class="grid-item">3</div>  
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>  
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div> 
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
</div>


</body>
</html>

The forum isn't letting me update my post, not sure what's going on there. (Big red 'You can't post that here' shows up after captcha verification.)

Scrapped the above code. After a day of screaming and cannibalizing a dozen different examples from across the web, I have my grid, I have a placeholder image pulled from a placeholder image website, and I have the ability to drag it about into different positions in said grid. It's a start. Going to bed, but I did notice that if I made a second fill item, dragging it would reposition the first one, instead. A puzzle for tomorrow...

Update; After a lot of help from a new friend, the grid now accepts multiple objects, supports dragging and dropping them, and even supports swapping them in place. I've also started using a codepen for ease of sharing it with people - https://codepen.io/Pykrete/pen/ExjqawG
Though, it seems codepen likes to preview zoomed in slightly, which makes the formatting look a little... off. The same code in other previews looks fine and to scale at 214px wide, though.

(OUTDATED)

<div class="grid-container">
  <div class="grid-item">
  <div class="fill" draggable="true"> </div>
  </div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
</div>

//CSS//
body {
  background: black;
}

.grid-container {
  display: grid;
  grid-gap: 2px;
  grid-template-columns: 40px 40px 40px 40px 40px;
  grid-template-rows: 40px 40px 40px 40px 40px;
  background-color: black;
  padding: 3px;
}

.grid-item {
  background-color: rgba(255, 100, 255, 0.8);
  border: 1px solid rgba(0, 0, 0, 0.8);
  padding: 0px;
  font-size: 36px;
  text-align: center;
}

.fill {
  background-image: url("https://source.unsplash.com/random/150x150");
  position: relative;
  height: 38px;
  width: 38px;
  top: 0px;
  left: 0px;
  cursor: pointer;
}

.hold {
  border: solid 2px #ccc;
}

.hovered {
  background: #f4f4f4;
  border-style: dashed;
}

//JS//

const fill = document.querySelector('.fill');
const empties = document.querySelectorAll('.grid-item');

// Fill listeners
fill.addEventListener('dragstart', dragStart);
fill.addEventListener('dragend', dragEnd);

// Loop through empty boxes and add listeners
for (const empty of empties) {
  empty.addEventListener('dragover', dragOver);
  empty.addEventListener('dragenter', dragEnter);
  empty.addEventListener('dragleave', dragLeave);
  empty.addEventListener('drop', dragDrop);
}

// Drag Functions

function dragStart() {
  this.className += ' hold';
  setTimeout(() => (this.className = 'invisible'), 0);
}

function dragEnd() {
  this.className = 'fill';
}

function dragOver(e) {
  e.preventDefault();
}

function dragEnter(e) {
  e.preventDefault();
  this.className += ' hovered';
}

function dragLeave() {
  this.className = 'grid-item';
}

function dragDrop() {
  this.className = 'grid-item';
  this.append(fill);
}```

Took a break, back at it again. Current functionality creates a grid of a set size (currently 25, in rows/columns of 5), creates some placeholder items which can be moved around and swapped, and has a button to simulate picking up additional items, dropping them into the first available slot. Currently trying to upgrade this functionality to run both ways - i.e., if a heavy object is picked up which would remove end slots, and one of those end slots is filled, the object occupying it would be moved back into the last available slot instead. Those newly added items currently don't sort/drag properly, either, need to set up the listeners for them.

I've only gotten this far thanks to a very patient person helping me out, heh.

https://www.w3schools.com/code/tryit.asp?filename=GDVYFBANV8P2


looks great. made a change. To conceptualize the end result

https://www.w3schools.com/code/tryit.asp?filename=GDWFOSN5LRN4


Yup, that's the idea. 32x32 icons with, iirc, a 3 pixel border which changes colour based on item type, for quick visual identification. Gold for precious sellable treasure items, red for weapons, etc.

Hovering over an item should display the name of it above the cursor, hopefully that can just directly draw from the object's .alias. The existing portion of the inventory screen where when you click on an object and the verbs pop up below, might port that intact - why fix what isn't broken? Make it so the last clicked-on item's border changes to something obvious, like giving it a dash effect.

Mind you, I've still not a clue how I'm going to handle removing items from the grid, heh. Everything should have a unique ID based off of the object's .name , but past that... (Did I mention I have no idea what I'm doing?)


Wow! It is really awesome! I have been wondering how visually integrating it to a Quest game.


one step further: https://www.w3schools.com/code/tryit.asp?filename=GDY0P53GWXNS


Great job! But when I did spawn more swords the trash can just disappeared.


Oh, fancy stuff! Though Deckrect is right, there's something up with the Delete trash can, but I couldn't replicate it disappearing with item spawning. What I -did- notice is that if you drag an item over the trash can, then drag it off without dropping it, the trash can disappears and the grid square it was occupying becomes a normal slot.


These are only small problems https://www.w3schools.com/code/tryit.asp?filename=GDZ7V9SIDDL9
The real problem is to integrate this into Quest and this does not work with a tablet


The real problem is to integrate this into Quest

I'd suggest modifying UpdateObjectLinks rather than relying on updateList. Or using a turnscript that passes a stringdictionary of icon URIs every turn.

The JS can then $.each over this dictionary, pseudocode something like:

$.each(data, function (name, uri) {
  if (object with this name isn't visible) {
    create it
  }
  set object's image to uri
  set an 'updated' flag on the object
});
remove any objects without the 'updated' flag
remove the 'updated' flag from all objects

and this does not work with a tablet

If you use jQueryUI's draggable function, there is a library available which patches it to work with most mobile devices. I'm surprised to see that this apparently isn't standard yet.


The trash button is cute, but it's not really something I think the system needs. My hope is that clicking on an item will bring up its verbs as it works in the current system, and that should include the ability to drop an item - a trash icon with no 'are you sure?' warning sounds like a recipe for frustration. That, and it would require increasing the default size of the inventory pane to accommodate for it, while part of the focus of this project is to provide an inventory system that can handle a large inventory without creating a dizzyingly long inventory list.

Regardless, currently fiddling with the code, trying to combine it with my existing one.

As for JQuery, given that I have no experience, I thought it would be best to start with basic vanilla JS, get everything figured out in terms of basic functionality, then see what I can replace/rewrite to utilize JQuery, as I remember you telling me that Quest utilizes it, Angel.

https://www.w3schools.com/code/tryit.asp?filename=GDZI80QS835O -- Current progress, newly added items have their listeners set up, now. I think next I'll play around with setting up a hover that displays the item name.


I think it might be easier learning jQuery; it does quite a bit of stuff for you, meaning it takes less code to do the same thing.

Here's a quick jQueryUI version, tweaked to do the same thing. I defined the 'grid-item's to be "droppable" targets, and the 'fill's to be "draggable"

https://www.w3schools.com/code/tryit.asp?filename=GDZSS5077UBE


Oh! Well, that's a start, thank you! The problem I ran into with trying to learn through JQuery is precisely the part where it does so much for you, heh. If that makes sense? Maybe I'm at the point where I can transition over, now.



I had this in my head when I went to bed last night, and I couldn't sleep. So here's a chunk of script off the top of my head; as yet untested, and written when sleep deprived.

First, the stylesheet (mostly yours, just with class names changed):

#inventoryAccordion {
  display: grid;
  grid-gap: 2px;
  grid-template-columns: 40px 40px 40px 40px 40px;
  grid-template-rows: 40px 40px 40px 40px 40px;
  background-color: black;padding: 3px;
}
.igrid-slot {
  background-color: rgba(255, 100, 255, 0.8);
  border: 1px solid rgba(0, 0, 0, 0.8);
  padding: 0px; text-align: center;
}
.igrid-item {
  background-image: url('inventory_icons.png');
  height: 38px;
  width: 38px;
  cursor: pointer;
}
.igrid-active {
  border: solid 2px #ccc;
}
.ui-droppable-hover {
  background: #f4f4f4;
  border-style: dashed;
}

The UI initialisation script (note that you need to pass it a stringlist containing the stylesheet above. Probably stored in a string attribute somewhere):

JS.initialiseGridInventory(styles)

A turnscript to update the inventory (though it would be better to put this into the FinishTurn or UpdateObjectLinks functions so that it still runs when turnscripts are suppressed):

icons = NewStringDictionary()
foreach (obj, ScopeInventory()) {
  if (not HasString (obj, "inventoryicon")) obj.inventoryicon = "0,0"
  dictionary add (icons, obj.name, obj.inventoryicon)
  JS.updateInventoryIcons (icons)
}

And a chunk of javascript. You can probably tell how tired I was from the illegibility:

initialiseGridInventory = (style) => $.getScript('https://mrangel.info/jquery.ui.touch-punch.min.js', function () {
  var layout = $('#outputData').data('igrid-layout');
  if (layout) {
    $.each(layout.split(':'), (i, val) => addItem.apply(this, val.split('=')).appendTo(addCells(1)));
  } else {
    addCells(25, $('#inventoryAccordion').empty());
  }
  $('<style>').appendTo('head').text(style.join("\n"));
});

$(function () {
  updateInventoryIcons = function (data) {
    var oldItems = $('#inventoryAccordion .igrid-item');
    var pending = $();
    $.each(data, function (id, icon) {
      var element = $('#igrid-item-'+id);
      if (element.length) {
        oldItems = oldItems.not(element);
        element.css({backgroundImage: icon});
      } else {
        pending = pending.add(addItem('id', 'icon'));
      }
    });
    oldItems.remove();
    pending.each(function (index, item) {
      var slot = $('#inventoryAccordion .grid-item:empty');
      if (slot.length == 0) {
        slot = addCells(1);
      }
      slot.first().append(item);
    });
  };

  function addCells (number, container) {
    var prevCount = container.find('.grid-item').length;
    return $($.map(Array(number),
      num => $('<div>', {id: 'gridcellcell'+(prevCount+num), class: 'igrid-slot'}).appendTo(container).droppable({
        drop: function (ev, ui) {
          $(this).children().appendTo(ui.draggable.parent());
          $(ui.draggable).appendTo(this);
          var serialised = $('#inventoryAccordion .grid-slot').map(
            (i, slot) => $(slot).children().map(
              (j, item) => ($(item).data('ElementId') + '=' + $(item).css('background-image'))
            ).get()
          ).get().join(':');
          $('#outputData').data('igrid-layout', serialised).attr('data-igrid-layout', serialised);
        }
      }).get()
    )).appendTo(container);
  }

  function addItem(id, icon) {
    if (!item) { return $(); }
    var cell = $('#inventoryAccordion .grid-item:empty').first();
    var item = $('<div>', {class: 'igrid-item cmdlink elementmenu', id: 'igrid-item-'+id}).data('ElementId', 'id').draggable({
      containment: cell.parent(),
      helper: 'clone',
      zIndex: 100,
      revert: 'invalid',
      start: () => o.addClass('igrid-active'),
      stop: () => o.removeClass('igrid-active')
    }).attr('style', icon.replace(/^(\d+)[,\s]*(\d*)(?!\w)/, (match, x, y) => ('background-position: -'+(x*40)+'px -'+((y||0)*40)+'px;'))).appendTo(cell);
    return (cell.length ? $() : item);
  }

  var originalUpdateList = updateList;
  updateList = function (name, listdata) {
    if (name == 'inventory') {
      $.each(listData, function (key, value) {
        var objdata = JSON.parse(value);
        var item = $('#igrid-item-'+objdata['ElementId']);
        if (objdata['Text']) {
          item.prop('title', objdata['Text']);
        }
        if (objdata['Verbs']) {
          item.removeClass('disabled').data('verbs', objdata['Verbs']);
        } else {
          item.addClass('disabled');
        }
      });
    } else {
      originalUpdateList(name, listdata);
    }
  };
});

If you wanted to put this all in the UI Initialisation script (for example if you're using the web editor) it would look like:

JS.eval("initialiseGridInventory=a=>$.getScript('https://mrangel.info/jquery.ui.touch-punch.min.js',function(){var b=$('#outputData').data('igrid-layout');b?$.each(b.split(':'),(a,b)=>addItem.apply(this,b.split('=')).appendTo(addCells(1))):addCells(25,$('#inventoryAccordion').empty()),$('<style>').appendTo('head').text(a)}),$(function(){function a(a,b){var c=b.find('.grid-item').length;return $($.map(Array(a),a=>$('<div>',{id:'gridcellcell'+(c+a),class:'igrid-slot'}).appendTo(b).droppable({drop:function(a,b){$(this).children().appendTo(b.draggable.parent()),$(b.draggable).appendTo(this);var c=$('#inventoryAccordion .grid-slot').map((a,b)=>$(b).children().map((a,b)=>$(b).data('ElementId')+'='+$(b).css('background-image')).get()).get().join(':');$('#outputData').data('igrid-layout',c).attr('data-igrid-layout',c)}}).get())).appendTo(b)}function b(a,b){return $();var c=$('#inventoryAccordion .grid-item:empty').first(),d=$('<div>',{class:'igrid-item cmdlink elementmenu',id:'igrid-item-'+a}).data('ElementId','id').draggable({containment:c.parent(),helper:'clone',zIndex:100,revert:'invalid',start:()=>o.addClass('igrid-active'),stop:()=>o.removeClass('igrid-active')}).attr('style',b.replace(/^(\\d+)[,\\s]*(\\d*)(?!\\w)/,(a,b,c)=>'background-position: -'+40*b+'px -'+40*(c||0)+'px;')).appendTo(c)}updateInventoryIcons=function(c){var d=$('#inventoryAccordion .igrid-item'),e=$();$.each(c,function(a,c){var f=$('#igrid-item-'+a);f.length?(d=d.not(f),f.css({backgroundImage:c})):e=e.add(b('id','icon'))}),d.remove(),e.each(function(b,c){var d=$('#inventoryAccordion .grid-item:empty');0==d.length&&(d=a(1)),d.first().append(c)})};var c=updateList;updateList=function(a,b){'inventory'==a?$.each(listData,function(a,b){var c=JSON.parse(b),d=$('#igrid-item-'+c.ElementId);c.Text&&d.prop('title',c.Text),c.Verbs?d.removeClass('disabled').data('verbs',c.Verbs):d.addClass('disabled')}):c(a,b)}});")
JS.initialiseGridInventory("#inventoryAccordion{display:grid;grid-gap:2px;grid-template-columns:40px 40px 40px 40px 40px;grid-template-rows:40px 40px 40px 40px 40px;background-color:#000;padding:3px}.igrid-slot{background-color:rgba(255,100,255,.8);border:1px solid rgba(0,0,0,.8);padding:0;text-align:center}.igrid-item{background-image:url('"+GetFileURL("inventory_icons.png")+"');height:38px;width:38px;cursor:pointer}.igrid-active{border:solid 2px #ccc}.ui-droppable-hover{background:#f4f4f4;border-style:dashed}")
create turnscript ("igrid_update")
SetTurnScript (GetObject ("igrid_update")) {
  icons = NewStringDictionary()
  foreach (obj, ScopeInventory()) {
    if (not HasString (obj, "inventoryicon")) obj.inventoryicon = "0,0"
    dictionary add (icons, obj.name, obj.inventoryicon)
    JS.updateInventoryIcons (icons)
  }
}

In theory, this expects the game to contain a single image inventory_icons.png which contains a 40×40 grid of icons. Each item which can be collected should have an attribute inventoryicon, which is a string like "3,5" specifying which image to choose from the grid. This avoids lag when a new item is added, because cutting an item out of an already-downloaded image can't be delayed by momentary network latency.


Oh wow, ok, I need to read through this. Is this the start of what I'd need to integrate the system into Quest?!

The icon grid thing is a solid shout, I know a lot of pixel-based games utilize that method. 40x40 should be plenty, too. I was thinking of creating borders for each item based on their 'class' - key item, weapon, etc - but come to think of it, it would be easier and more sensible to just make that border on the icon itself.

Edit; Oh, wait, as in a grid of icons that are 40px in size, or a grid of icons that is 40 columns by 40 rows?


Are you familiar with the latest version of javascript? I've used arrow notation quite a lot, which is a fairly new addition.

If you've not come across it yet, it basically means that:

var a = b => c;

is shorthand for:

function a (b) {
  return c;
}

I've also abused scope frames somewhat to make the code smaller, which is kind of a bad habit (in JS, a function defined inside another function can access its parent function's local variables).

If there's any parts that don't make sense (or that don't work), I can try to explain.

Other parts likely to confuse a sane coder:

addItem.apply(this, val.split('='))

is a slightly faster way of doing:

var params = val.split('=');
addItem(params[0], params[1]);

I used the data-igrid-layout attribute of #outputData to preserve the layout in a saved game.


I'll be honest with you buddy, I'm not familiar with any version of javascript, ahah. But, good to know, thank you!

Um, I popped everything into UI Initialisation in a fresh project (offline editor), and while no errors are being tossed up it also doesn't seem to have changed anything. Inventory pane is present and normal. Is it reliant on anything outside of the UI Ini script that I glossed over above?


Could still be errors; I wrote this on my phone when I should have been sleeping.

I'll take a look; but I've got a couple of books free on Kindle that I haven't done any promotion for yet, so I really need to find some time to work today.


No pressure! This is certainly a start, so thank you! <3


OK… couple of dumb mistakes.

Either addCells and addItem need to be global functions (in which case I should probably rename them to avoid colliding with other functions), or the definition of intialiseGridInventory needs to be inside the same function.

I've got a typo; listData and listdata aren't the same.

Now it displays; and the handlers all seem to be attached. But it's not displaying items :S


Ah… I was testing it in the javascript console, so the turnscript wasn't running.

I'll have to create an actual game to test further; and I don't think I have time right now. Will get back to you later :) But the JS seems to be mostly working.

Also, after renaming the class fill to igrid-item, and grid-item to igrid-slot (which makes more sense to me), I missed a few of them in the code.


No rush at all, I'm a bit swamped with work myself but I'm trying to fiddle with this project at least once a day to force myself to get at least a basic handle on JS, as well as force myself to get back to sprite art practice (gonna need to make those icons, after all). And god, ok, that's something I can relate to. "Wait, why did I call this 'x'? 'y' makes much more sense!" Entire Quest project breaks

As for not displaying items, I assume it wouldn't until it has icons to draw from... right? If it's trying to pull from a sheet that doesn't exist, would that not interrupt the creation process? EDIT -- nevermind, didn't focus on the second post!

Aaah I'm stuck in something but I'm excited!


Well, this works as far as I can tell. Haven't properly tested the Quest integration (just pasted it into the JS console on an existing game), but it seems to work.

So many typos, or getting my i and j confused…

Updated javascript:

$(function () {
  initialiseGridInventory = (style) => $.getScript('https://mrangel.info/jquery.ui.touch-punch.min.js', function () {
    var layout = $('#outputData').data('igrid-layout');
    if (layout) {
      $.each(layout, function(i, val) {
        if (val.length) {
          addItem.apply(this, val);
          $('#igrid-item-'+val[0]).appendTo(addCells(1, $('#inventoryAccordion')));
        } else {
            addCells(1, $('#inventoryAccordion'));
        }
      });
    } else {
      addCells(25, $('#inventoryAccordion').empty());
    }
    $('<style>').appendTo('head').text(style);
  });

  var grid_icons = {};
  updateInventoryIcons = function (data) {
    $.each(data, function (id, icon) {
      $('#igrid-item-'+id).attr('style', icon.replace(/^(\d+)[,\s]*(\d*)(?!\w)/, (match, x, y) => ('background-position: -'+(x*40)+'px -'+((y||0)*40)+'px;')))
    });
    grid_icons[id] = icon;
  };

  function addCells (number, container) {
    var count = container.find('.igrid-slot').length;
    return $($.map(Array(number),
      num => $('<div>', {id: 'igrid-cell-'+(count++), class: 'igrid-slot'}).appendTo(container).droppable({
        drop: function (ev, ui) {
          $(this).children().appendTo(ui.draggable.parent());
          $(ui.draggable).appendTo(this);
          $('#outputData').attr('data-igrid-layout', JSON.stringify($('#inventoryAccordion .igrid-slot').map(
            (i, slot) => [[$(slot).children().map(
              (j, item) => ($(item).data('ElementId'), $(item).attr('style'))
            )]]
          )));
        }
      })
    ));
  }

  function addItem(id, icon) {
    if (!id) { return $(); }
    var cell = $('#inventoryAccordion .igrid-slot:empty').first();
    var item = $('<div>', {class: 'igrid-item', id: 'igrid-item-'+id}).data('ElementId', 'id').appendTo(cell).draggable({
      containment: cell.parent(),
      helper: 'clone',
      zIndex: 100,
      revert: 'invalid',
      start: () => item.addClass('igrid-active'),
      stop: () => item.removeClass('igrid-active')
    }).click(function (event) {
      if (!item.hasClass("disabled")) {
        var metadata = {};
        var alias = (item.prop('title') || id).toLowerCase();
        metadata[alias] = id;
        event.preventDefault();
        event.stopPropagation();
        item.blur().jjmenu_popup($.map(item.data('verbs'), verb => {return {
          title: verb,
          action: {callback: i => sendCommand(verb.toLowerCase() + ' ' + alias, metadata)}
        }}));
        return false;
      }
    }).attr('style', icon.replace(/^(\d+)[,\s]*(\d*)(?!\w)/, (match, x, y) => ('background-position: -'+(x*40)+'px -'+((y||0)*40)+'px;')));
    return item;
  }

  updateInventoryItem = function (id, alias, verbs) {
    var item = $('#igrid-item-'+id);
    if (alias) {
      item.prop('title', alias);
    }
    if (verbs) {
      item.data('verbs', verbs);
    }
  };

  var originalUpdateList = updateList;
  updateList = function (name, listdata) {
    if (name == 'inventory') {
      var oldItems = $('#inventoryAccordion .igrid-item');
      var pending = $();
      $.each(listdata, function (key, value) {
        var objdata = JSON.parse(value);
        var id = objdata['ElementId'];
        var item = $('#igrid-item-'+id);
        if (item.length) {
          oldItems = oldItems.not(item);
        } else {
          item = addItem(id, grid_icons[id] || '0,0');
          // if we failed to add the item (grid is full), then try again after removing items that have been removed from inventory
          if (item.parent().length == 0) {
            pending = pending.add(item);
          }
        }
        if (objdata['Text']) {
          item.prop('title', objdata['Text']);
        }
        if (objdata['Verbs']) {
          item.data('verbs', objdata['Verbs']).removeClass('disabled');
        } else {
          item.addClass('disabled');
        }
      });
      oldItems.remove();
      pending.each(function (index, item) {
        var slot = $('#inventoryAccordion .igrid-slot:empty');
        if (slot.length == 0) {
          slot = addCells(1);
        }
        slot.first().append(item);
      });  
    } else {
      originalUpdateList(name, listdata);
    }
  };
});

Or compressed for Quest:

JS.eval("$(function(){function a(a,b){var c=b.find('.igrid-slot').length;return $($.map(Array(a),()=>$('<div>',{id:'igrid-cell-'+c++,class:'igrid-slot'}).appendTo(b).droppable({drop:function(a,b){$(this).children().appendTo(b.draggable.parent()),$(b.draggable).appendTo(this),$('#outputData').attr('data-igrid-layout',JSON.stringify($('#inventoryAccordion .igrid-slot').map((a,b)=>[[$(b).children().map((a,b)=>($(b).data('ElementId'),$(b).attr('style')))]])))}})))}function b(a,b){if(!a)return $();var c=$('#inventoryAccordion .igrid-slot:empty').first(),d=$('<div>',{class:'igrid-item',id:'igrid-item-'+a}).data('ElementId','id').appendTo(c).draggable({containment:c.parent(),helper:'clone',zIndex:100,revert:'invalid',start:()=>d.addClass('igrid-active'),stop:()=>d.removeClass('igrid-active')}).click(function(b){if(!d.hasClass('disabled')){var c={},e=(d.prop('title')||a).toLowerCase();return c[e]=a,b.preventDefault(),b.stopPropagation(),d.blur().jjmenu_popup($.map(d.data('verbs'),a=>({title:a,action:{callback:()=>sendCommand(a.toLowerCase()+' '+e,c)}}))),!1}}).attr('style',b.replace(/^(\\d+)[,\\s]*(\\d*)(?!\\w)/,(a,b,c)=>'background-position: -'+40*b+'px -'+40*(c||0)+'px;'));return d}initialiseGridInventory=c=>$.getScript('https://mrangel.info/jquery.ui.touch-punch.min.js',function(){var d=$('#outputData').data('igrid-layout');d?$.each(d,function(c,d){d.length?(b.apply(this,d),$('#igrid-item-'+d[0]).appendTo(a(1,$('#inventoryAccordion')))):a(1,$('#inventoryAccordion'))}):a(25,$('#inventoryAccordion').empty()),$('<style>').appendTo('head').text(c)});var c={};updateInventoryIcons=function(a){$.each(a,function(a,b){$('#igrid-item-'+a).attr('style',b.replace(/^(\\d+)[,\\s]*(\\d*)(?!\\w)/,(a,b,c)=>'background-position: -'+40*b+'px -'+40*(c||0)+'px;'))}),c[id]=icon},updateInventoryItem=function(a,b,c){var d=$('#igrid-item-'+a);b&&d.prop('title',b),c&&d.data('verbs',c)};var d=updateList;updateList=function(e,f){if('inventory'==e){var g=$('#inventoryAccordion .igrid-item'),h=$();$.each(f,function(a,d){var e=JSON.parse(d),f=e.ElementId,i=$('#igrid-item-'+f);i.length?g=g.not(i):(i=b(f,c[f]||'0,0'),0==i.parent().length&&(h=h.add(i))),e.Text&&i.prop('title',e.Text),e.Verbs?i.data('verbs',e.Verbs).removeClass('disabled'):i.addClass('disabled')}),g.remove(),h.each(function(b,c){var d=$('#inventoryAccordion .igrid-slot:empty');0==d.length&&(d=a(1)),d.first().append(c)})}else d(e,f)}});")

Can confirm:

  • Clicking an object and using its verbs works
  • Dragging objects works
  • Objects are added and removed when they would be for the default inventory pane
    • Note: This unfortunately means that it's not guaranteed to update immediately if an object's listalias or inventoryverbs changes. If this is a problem, you can use JS.updateInventoryItem (name, alias, verbs) to make sure an item is displayed correctly.
  • Object's listalias is used when hovering the mouse over an object; and in the echoed command when using the verbs menu.

As for not displaying items, I assume it wouldn't until it has icons to draw from... right? If it's trying to pull from a sheet that doesn't exist, would that not interrupt the creation process?

I haven't tested the icons yet. If the icon sheet doesn't exist at all, the items appear as a draggable transparent box. Maybe I should have added a background-color: yellow to the .igrid-item class in the stylesheet for testing purposes.

Once the icon sheet exists, any object which doesn't give itself a specific icon will display icon 0,0 (the top left one) instead; so it might be good to use that space as a generic placeholder.

If you want items with the same icon but a different background (for example to distinguish a common sword from a rare sword), you could set the inventoryicon property to a string like 1,4 background-color: red - it does a search and replace to turn 1,4 at the beginning into background-position: -40px -160px; and then applies it to the item's "style" attribute.

Hope that's helpful.


Hmm... Well, I opened up that fresh game and tried out the code, and nothing is appearing on my end - either with items already in inventory, or starting with it empty and picking up items anew, it's still the original inventory panel.

https://i.gyazo.com/b6047d90d41c9e69e739413cd53126a6.png
^ Here's how it looks in the advanced scripts tab of game, that's all the javascript compressed on one line (directly copied as above, from eval to else d(e,f)}});") -- is there something I was meant to place elsewhere, like a rewritten core inventory function, or turning off the inventory panel in settings?

Thank you again!


Have you got the stylesheet line? I don't see it in that screenshot.

It should be:

JS.initialiseGridInventory("#inventoryAccordion{display:grid;grid-gap:2px;grid-template-columns:40px 40px 40px 40px 40px;grid-template-rows:40px 40px 40px 40px 40px;background-color:#000;padding:3px}.igrid-slot{background-color:rgba(255,100,255,.8);border:1px solid rgba(0,0,0,.8);padding:0;text-align:center}.igrid-item{background-image:url('"+GetFileURL("inventory_icons.png")+"');height:38px;width:38px;cursor:pointer}.igrid-active{border:solid 2px #ccc}.ui-droppable-hover{background:#f4f4f4;border-style:dashed}")

That's the line that actually switches to the grid layout (as well as containing all the style information). It should come after the eval. You can also change the filename if necessary.

If you're using the desktop editor, you might also want to remove the link to a library on my webserver so that the game can be played offline.
I believe the desktop editor allows you to add javascript files to a game, so you could include the non-compressed version of the javascript code and change the line

  initialiseGridInventory = (style) => $.getScript('https://mrangel.info/jquery.ui.touch-punch.min.js', function () {

to

  initialiseGridInventory = (function (style) {

if the file jquery.ui.touch-punch.min.js is in your project. (The touch-punch library just makes jQuery's draggable work neatly with touchscreen devices, by generating artificial mouse movement events when it receives touch movement events)


I didn't have the stylesheet line, no! But now it's in after the eval chunk as a new JS line, but it's still not displaying anything different. I have a placeholder image in place named inventory_icons.png, too.

And oh, god, yeah, I want this to be played offline. I had people complaining to me about my Gamebooks not being stable online, I don't wanna think about my text RPGs having that reliance. Quest can add javascript files? I had no idea!... nor do I know how to do that, argh. I know images is as simple as dropping the image in the game's project folder - same deal?

Will changing that line in the compressed version work, or no?

... Actually, when you say play offline, do you mean this code currently won't work at all in the editor/player? That would certainly explain why it's still not working!


Will changing that line in the compressed version work, or no?

It should work. But making the draggable functionality work with touchscreens will need either the $.getScript call, or the file jquery.ui.touch-punch.min.js included in your project. (It's basically a library that waits for touch movement events, and changes their type to mouse movement events within jQuery's event stack, so that draggable sees them)


I can't work out what's different; the script as posted is certainly working for me.

Are there any errors in the javascript console? (Ctrl+Shift+J if you're playing in a browser; not sure if that will work in the desktop player or not)


Uh... well. I uploaded the test 'game' and, upon opening it online...
https://i.gyazo.com/33d6bf6100bb3058d42be8940c671b4d.png

There's the grid. It's completely janked out of the pane, but there it is. That isn't appearing -at all- in the editor, as so;

https://i.gyazo.com/5fe841d452109235189c64406cf908f0.png

Aaaand if I open the published file in the offline player, same result. So, currently it only works in online web mode.

Now comes the relief of 'oh, I'm not missing something blindingly obvious', mixed with the despair of 'but whyyyyy??'

Also, no js errors of relevance to the game, just one of my extensions that I need to remove that no longer functions, I think. The ctrl-shift-j shortcut doesn't work in the game editor/player.

edit; heading to bed for now, but feeling positive.


I'll have to look up what version of Chromium is bundled with Quest… arrow notation has been supported since version 45, grid layout since 57, and draggable since 81.

Had a poke around, but I can't find the version number, so not sure how much of the code will need changing. We might end up needing to arrange the grid using a table, or handle dragging by catching individual mousebutton press/release events (but I think jQuery did that before 'draggable' was a thing)


Yikes... that's not good. Sounds like Quest is due for an update in that department :S That also leaves me even further out of my depth than before.


Looks like the desktop player is using Cefsharp 39.0.1 … so there'll be a few changes required.


2015, huh. I was hobbling my way with guides and a lot of help from others, but 'don't implement anything introduced in the last 5 years' is a bit too much of a hurdle, heh. So- I'm assuming a greater reliance on HTML, at least for the grid?


That's a 2015 browser… so no HTML5 draggable, no arrow functions, and no display: grid.

Draggable: until HTML5 introduced the draggable attribute, jQuery implemented the draggable and droppable types by listening for mousedown, mouseup, and mousemoved events. Quest includes jQuery UI 1.11, so this should just work.

Grid: We can put the items in a table. Or, because we've got a container for each slot in the inventory, we could just make .igrid-slot a fixed size and rely on the browser to arrange them.

Arrow functions: This is just a syntax change; it'll take a little time to update.

I really should have thought about this, because I had a similar issue when I used position: sticky in a game. (Check out the game 'Circus' on my profile; see the difference on web/desktop. That needs Chrome 57 or later to display properly)


Honestly, this might go some ways to explaining some issues I had a while back with js, where things simply weren't working until I separated the whole block line by line, at which point it all inexplicably started showing up. I never even thought to test it online, maybe it worked just fine there!

It might be worth seeing if we can get in touch with Pixie about updating things, because being stuck in 2015 is surely going to cause problems at some point. Mind you, I've no idea how much work that would entail, let alone how much it might break...

When you say rely on the browser to arrange them, how do you mean?


When you say rely on the browser to arrange them, how do you mean?

If you have a bunch of <div>s with display: inline-block, they will be arranged like text characters. For example:

<div style="background-color:yellow; display:block; width:160px; line-height: 0px;">
  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>
  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>
  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>
  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>

  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>
  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>
  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>
  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>

  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>
  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>
  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>
  <div style="display: inline-block; background-color: purple; border:2px solid black; height:40px; width:40px"/>
</div>

Displays like:

Each div has a fixed height and width, and the container has a fixed width, so they'll just wrap to having 5 on each line. You can't easily put spacing between them (you'd need to use a table for that), but I think it would work well enough for this.

If one of the icons was too big, it would mess up positioning of all the ones after it… but in our case, that isn't going to happen. You really only need to use grid if there's a chance of text being longer than you expected, or user stylesheets causing a problem.


Edited to make it display correctly (the forum doesn't like <div/> so I had to change them all to <div></div> syntax. It seems the forum still parses HTML as SGML-style rather than XML-style.


Honestly, if it works, it works! So, in the example above, if the width was changed to 200px instead of 160px, would they start displaying in lines of 5 instead of 4? (Probably blitheringly obvious, but...)


Not tested yet (on my phone), but here's an updated version. A few tweaks; as I'd changed the way it works while debugging.

The CSS:

#inventoryAccordion {
  text-align: center;
  line-height: 0px;
}
.igrid-slot {
  display: inline-block;
  background-color: purple;
  border: 1px solid black;
  padding: 0px;
  width: 40px;
  height: 40px;
  margin: 0px;
}

.igrid-item {
  background-image: url('inventory_icons.png');
  background-color: yellow;
  height: 38px;
  width: 38px;
  cursor: pointer;
}

.igrid-active {
  border: solid 2px #ccc;
}

.ui-droppable-hover {
  background: #f4f4f4;
  border-style: dashed;
}

The start script:

style = "#inventoryAccordion {text-align: center; line-height: 0px;} .igrid-slot {display: inline-block; background-color: purple; border: 1px solid black; padding: 0px; width: 40px; height: 40px; margin: 0px;}
.igrid-item {background-image: url('"+GetFileURL("inventory_icons.png")+"'); background-color: yellow; height: 38px; width: 38px; cursor: pointer;}
.igrid-active {border: solid 2px #ccc; background-color: green;}
.ui-droppable-hover {background: #f4f4f4; border-style: dashed; background-color: red}"

JS.initialiseGridInventory(style)

The turnscript:

icons = NewStringDictionary()
foreach (obj, ScopeInventory()) {
  if (HasString (obj, "inventoryicon")) {
    dictionary add (icons, obj.name, obj.inventoryicon)
  }
}
JS.updateInventoryIcons (icons)

The javascript:

$(function() {
  initialiseGridInventory = function(style) {
    $.getScript('https://mrangel.info/jquery.ui.touch-punch.min.js', function() {
      var layout = $('#outputData')
        .data('igrid-layout');
      if (layout) {
        $.each(layout, function(i, val) {
          if (val.length) {
            addItem.apply(this, val);
            $('#igrid-item-' + val[0])
              .appendTo(addCells(1, $('#inventoryAccordion')));
          } else {
            addCells(1, $('#inventoryAccordion'));
          }
        });
      } else {
        addCells(25, $('#inventoryAccordion')
          .empty());
      }
      $('<style>')
        .appendTo('head')
        .text(style);
    })
  };

  var grid_icons = {};
  updateInventoryIcons = function(data) {
    $.each(data, function(id, icon) {
      $('#igrid-item-' + id)
        .attr('style', icon.replace(/^(\d+)[,\s]*(\d*)(?!\w)/, function(match, x, y) {
          return ('background-position: -' + (x * 40) + 'px -' + ((y || 0) * 40) + 'px;')
        }))
    });
    grid_icons[id] = icon;
  };

  function addCells(number, container) {
    var count = container.find('.igrid-slot')
      .length;
    return $($.map(Array(number), function() {
      return $('<div>', {
          id: 'igrid-cell-' + (count++),
          class: 'igrid-slot'
        })
        .appendTo(container)
        .droppable({
          drop: function(ev, ui) {
            $(this)
              .children()
              .appendTo(ui.draggable.parent());
            $(ui.draggable)
              .appendTo(this);
            $('#outputData')
              .attr('data-igrid-layout', JSON.stringify($('#inventoryAccordion .igrid-slot')
                .map(
                  function(i, slot) {
                    return [
                      [$(slot)
                        .children()
                        .map(
                          function(j, item) {
                            return ($(item)
                              .data('ElementId'), $(item)
                              .attr('style'));
                          }
                        )
                      ]
                    ];
                  }
                )));
          }
        });
    }));
  }

  function addItem(id, icon) {
    if (!id) {
      return $();
    }
    var cell = $('#inventoryAccordion .igrid-slot:empty')
      .first();
    var item = $('<div>', {
        class: 'igrid-item',
        id: 'igrid-item-' + id
      })
      .data('ElementId', 'id')
      .appendTo(cell)
      .draggable({
        containment: cell.parent(),
        helper: 'clone',
        zIndex: 100,
        revert: 'invalid',
        start: function() {
          item.addClass('igrid-active');
        },
        stop: function() {
          item.removeClass('igrid-active');
        }
      })
      .click(function(event) {
        if (!item.hasClass("disabled")) {
          var metadata = {};
          var alias = (item.prop('title') || id)
            .toLowerCase();
          metadata[alias] = id;
          event.preventDefault();
          event.stopPropagation();
          item.blur()
            .jjmenu_popup($.map(item.data('verbs'), function(verb) {
              return {
                title: verb,
                action: {
                  callback: function() {
                    sendCommand(verb.toLowerCase() + ' ' + alias, metadata);
                  }
                }
              };
            }));
          return false;
        }
      })
      .attr('style', icon.replace(/^(\d+)[,\s]*(\d*)(?!\w)/, function(match, x, y) {
        return ('background-position: -' + (x * 40) + 'px -' + ((y || 0) * 40) + 'px;')
      }));
    return item;
  }

  updateInventoryItem = function(id, alias, verbs) {
    var item = $('#igrid-item-' + id);
    if (alias) {
      item.prop('title', alias);
    }
    if (verbs) {
      item.data('verbs', verbs);
    }
  };

  var originalUpdateList = updateList;
  updateList = function(name, listdata) {
    if (name == 'inventory') {
      var oldItems = $('#inventoryAccordion .igrid-item');
      var pending = $();
      $.each(listdata, function(key, value) {
        var objdata = JSON.parse(value);
        var id = objdata['ElementId'];
        var item = $('#igrid-item-' + id);
        if (item.length) {
          oldItems = oldItems.not(item);
        } else {
          item = addItem(id, grid_icons[id] || '0,0');
          // if we failed to add the item (grid is full), then try again after removing items that have been removed from inventory
          if (item.parent()
            .length == 0) {
            pending = pending.add(item);
          }
        }
        if (objdata['Text']) {
          item.prop('title', objdata['Text']);
        }
        if (objdata['Verbs']) {
          item.data('verbs', objdata['Verbs'])
            .removeClass('disabled');
        } else {
          item.addClass('disabled');
        }
      });
      oldItems.remove();
      pending.each(function(index, item) {
        var slot = $('#inventoryAccordion .igrid-slot:empty');
        if (slot.length == 0) {
          slot = addCells(1);
        }
        slot.first()
          .append(item);
      });
    } else {
      originalUpdateList(name, listdata);
    }
  };
});

All in one script for web-editor UI Initialisation:

JS.eval("$(function(){initialiseGridInventory=function(n){$.getScript('https://mrangel.info/jquery.ui.touch-punch.min.js',function(){var t=$('#outputData').data('igrid-layout');t?$.each(t,function(t,n){n.length?(c.apply(this,n),$('#igrid-item-'+n[0]).appendTo(e(1,$('#inventoryAccordion')))):e(1,$('#inventoryAccordion'))}):e(25,$('#inventoryAccordion').empty()),$('<style>').appendTo('head').text(n)})};var d={};function e(t,n){var i=n.find('.igrid-slot').length;return $($.map(Array(t),function(){return $('<div>',{id:'igrid-cell-'+i++,class:'igrid-slot'}).appendTo(n).droppable({drop:function(t,n){$(this).children().appendTo(n.draggable.parent()),$(n.draggable).appendTo(this),$('#outputData').attr('data-igrid-layout',JSON.stringify($('#inventoryAccordion .igrid-slot').map(function(t,n){return[[$(n).children().map(function(t,n){return $(n).data('ElementId'),$(n).attr('style')})]]})))}})}))}function c(e,t){if(!e)return $();var n=$('#inventoryAccordion .igrid-slot:empty').first(),r=$('<div>',{class:'igrid-item',id:'igrid-item-'+e}).data('ElementId','id').appendTo(n).draggable({containment:n.parent(),helper:'clone',zIndex:100,revert:'invalid',start:function(){r.addClass('igrid-active')},stop:function(){r.removeClass('igrid-active')}}).click(function(t){if(!r.hasClass('disabled')){var n={},i=(r.prop('title')||e).toLowerCase();return n[i]=e,t.preventDefault(),t.stopPropagation(),r.blur().jjmenu_popup($.map(r.data('verbs'),function(t){return{title:t,action:{callback:function(){sendCommand(t.toLowerCase()+' '+i,n)}}}})),!1}}).attr('style',t.replace(/^(\\d+)[,\\s]*(\\d*)(?!\\w)/,function(t,n,i){return'background-position: -'+40*n+'px -'+40*(i||0)+'px;'}));return r}updateInventoryIcons=function(t){$.each(t,function(t,n){$('#igrid-item-'+t).attr('style',n.replace(/^(\\d+)[,\\s]*(\\d*)(?!\\w)/,function(t,n,i){return'background-position: -'+40*n+'px -'+40*(i||0)+'px;'}))}),d[id]=icon},updateInventoryItem=function(t,n,i){var e=$('#igrid-item-'+t);n&&e.prop('title',n),i&&e.data('verbs',i)};var i=updateList;updateList=function(t,n){if('inventory'==t){var a=$('#inventoryAccordion .igrid-item'),o=$();$.each(n,function(t,n){var i=JSON.parse(n),e=i.ElementId.replace(/\\s/g,'-'),r=$('#igrid-item-'+e);r.length?a=a.not(r):0==(r=c(e,d[e]||'0,0')).parent().length&&(o=o.add(r)),i.Text&&r.prop('title',i.Text),i.Verbs?r.data('verbs',i.Verbs).removeClass('disabled'):r.addClass('disabled')}),a.remove(),o.each(function(t,n){var i=$('#inventoryAccordion .igrid-slot:empty');0==i.length&&(i=e(1)),i.first().append(n)})}else i(t,n)}});")

create turnscript ("igrid_update")
SetTurnScript (GetObject ("igrid_update")) {
  icons = NewStringDictionary()
  foreach (obj, ScopeInventory()) {
    if (HasString (obj, "inventoryicon")) {
      dictionary add (icons, obj.name, obj.inventoryicon)
    }
  }
  JS.updateInventoryIcons (icons)
}

style = "#inventoryAccordion{line-height:0px;} .igrid-slot {display: inline-block; background-color: purple; border: 1px solid black; padding: 0px; width: 40px; height: 40px; margin: 0px;} .igrid-item {background-image: url('"+GetFileURL("inventory_icons.png")+"'); background-color: yellow; height: 38px; width: 38px; cursor: pointer;} .igrid-active {border: solid 2px #ccc;} .ui-droppable-hover {background: #f4f4f4; border-style: dashed;}"

JS.initialiseGridInventory(style)

No dice on the all-in-one UI in script, but then I remembered the issues I mentioned earlier, when last I tried to utilize js in my project. The bizarre workaround, in the end, was to do every line as a separate entry - style = "line one", style = style + "line two", etc.

so, I fiddled around with that, and...

https://i.gyazo.com/e910b741164035dfba152e8cee6ee54a.png

Well, not exactly 'bingo', but it's showing up, at least. Unfortunately, picking up an item has done nothing - it disappears from the room, but doesn't appear in the new inventory.

style = ".igrid-slot {display: inline-block; background-color: purple; border: 1px solid black; padding: 1px; width: 40px; height: 40px; margin: 0px;})"
style = style + ".igrid-item {background-image: url('"+GetFileURL("inventory_icons.png")+"'); height: 38px; width: 38px; cursor: pointer;}"
style = style + ".igrid-active {border: solid 2px #ccc;}"
style = style + ".ui-droppable-hover {background: #f4f4f4; border-style: dashed;}"

Removing the padding (or, reducing it to 0px) got me here;
https://i.gyazo.com/0ba4f88a803b94835f527d95f833ee4b.png

Trying the style = stuff without spacing it out into separate lines just threw a nasty error at me in preview on the editor, while trying to implement the code into the online editor threw a generic 'an error has occured' popup, froze the tab for about a minute, then rebooted itself back blank.


I think in the online editor it would be a real pain, because code view throws a wobbler trying to save a string with a < in it.

I'll try debugging again and see if I've introduced any new errors when removing the arrows.

If you add #inventoryAccordion {line-height: 38px;} to the style sheet, it should remove the additional space between rows of cells.


I eventually figured out how to do that, but 38px is just making the gaps increase in size dramatically.

Lowering it down to, say, 0, eliminates the gaps, but then makes the grid 4/4/4/4/4/4/1 instead of 5/5/5/5/5.

At the original 38px; https://i.gyazo.com/3273046e784016cb489e70cd0f98801e.png

At 0; https://i.gyazo.com/32f63dea51db418c006be790e016b99c.png


No dice on the all-in-one UI in script, but then I remembered the issues I mentioned earlier, when last I tried to utilize js in my project. The bizarre workaround, in the end, was to do every line as a separate entry - style = "line one", style = style + "line two", etc.

Is that a specific problem in the desktop editor? Because using one big line works for me.

OK… a new game created on the web editor, using the "UI Initialisation" short form (the most recent copy I posted above).





I notice a small bug when playing with this: items that are initially in your inventory aren't shown until you pick something up. I guess that the first call to updateList is done from c# code before handing off to Quest's InitInterface.

I didn't notice this when I was pasting the script into the Javascript Console on someone else's game (quickest way to test JS) because that game has an "intro room" before the game starts, after which the initial items are moved to your inventory by script. I'll have to add a function which scans the original inventory list to get the verbs for any items you start with. Maybe tomorrow.

But at least it's working :)


I use, and only use, the desktop editor, because we're told time and time again on this site to use the desktop editor due to issues with the online editor. (I have no idea why it's working for you and not for me, I just tried again and it immediately threw this; https://i.gyazo.com/fb8e48143d460258f43d58000ac3beba.png then crash-reset the tab.)

Tried uploading the game to see what would happen, and playing online it... almost works, except the white space is still visible in between rows.

Trying to play the game through the desktop application, which was and remains to be my intended method of play due to a single internet hiccup booting you off of the online version (unless that got fixed since I started using Quest), results in... well.

https://i.gyazo.com/02104029e5c83075e9cfb24a7577e473.png -- For some reason the first item picked up (nothing starting in the inventory) is dropped into slot 2, and moving it anywhere results in the drop function never quite finishing, the item becoming completely non-interactive.

I'm assuming you've made edits since the last code you posted, given the drastic differences. At least it's working for someone!... But unless it works in desktop...

-- Edit --
Fresh game, created the js.eval, left it blank, copied in everything else, THEN added in the js.eval portion straight into the box instead of in code view.

Running and playing the game online works - it works almost perfectly. Only error I found is that if you drop an item into -any- of the bottom 5 slots in the final row, then pick it up against, the css freaks out and turns into that awful 4/4/4/4/4/4/1 grid again until you drop it.

Taking the same project file into the offline editor and then playing it just breaks everything, again, as described above. Starts in slot 2, breaks after a single movement.


(I have no idea why it's working for you and not for me, I just tried again and it immediately threw this; https://i.gyazo.com/fb8e48143d460258f43d58000ac3beba.png then crash-reset the tab.)

That's a known bug in the web editor when switching from code view to GUI. To get around it I included a line like JS.eval("placeholder") and then paste the actual minified javascript into the box in GUI view.

Not ideal, but usable.

I've been going through the code, checking all the functions I've used and when they were added; but can't find anything that should shut it down. I can imagine it possibly failing on the getScript if it takes too long to load, possibly? Maybe I could include the file directly?

Would be something like:

//
// jQuery UI Touch Punch 0.2.3
//
// Copyright 2011–2014, Dave Furfero
// Dual licensed under the MIT or GPL Version 2 licenses.
//
//Depends:
//  jquery.ui.widget.js
//  jquery.ui.mouse.js
//
JS.eval("!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent('MouseEvents');d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch='ontouchend'in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,'mouseover'),f(a,'mousemove'),f(a,'mousedown'))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,'mousemove'))},b._touchEnd=function(a){e&&(f(a,'mouseup'),f(a,'mouseout'),this._touchMoved||f(a,'click'),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,'_touchStart'),touchmove:a.proxy(b,'_touchMove'),touchend:a.proxy(b,'_touchEnd')}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,'_touchStart'),touchmove:a.proxy(b,'_touchMove'),touchend:a.proxy(b,'_touchEnd')}),d.call(b)}}}(jQuery);")

JS.eval("$(function(){initialiseGridInventory=function(t){var n=$('#outputData').data('igrid-layout');n?$.each(n,function(t,n){n.length?(c.apply(this,n),$('#igrid-item-'+n[0]).appendTo(e(1,$('#inventoryAccordion')))):e(1,$('#inventoryAccordion'))}):e(25,$('#inventoryAccordion').empty()),$('<style>').appendTo('head').text(t)};var d={};function e(t,n){var i=n.find('.igrid-slot').length;return $($.map(Array(t),function(){return $('<div>',{id:'igrid-cell-'+i++,class:'igrid-slot'}).appendTo(n).droppable({drop:function(t,n){$(this).children().appendTo(n.draggable.parent()),$(n.draggable).appendTo(this),$('#outputData').attr('data-igrid-layout',JSON.stringify($('#inventoryAccordion .igrid-slot').map(function(t,n){return[[$(n).children().map(function(t,n){return $(n).data('ElementId'),$(n).attr('style')})]]})))}})}))}function c(e,t){if(!e)return $();var n=$('#inventoryAccordion .igrid-slot:empty').first(),r=$('<div>',{class:'igrid-item',id:'igrid-item-'+e}).data('ElementId','id').appendTo(n).draggable({containment:n.parent(),helper:'clone',zIndex:100,revert:'invalid',start:function(){r.addClass('igrid-active')},stop:function(){r.removeClass('igrid-active')}}).click(function(t){if(!r.hasClass('disabled')){var n={},i=(r.prop('title')||e).toLowerCase();return n[i]=e,t.preventDefault(),t.stopPropagation(),r.blur().jjmenu_popup($.map(r.data('verbs'),function(t){return{title:t,action:{callback:function(){sendCommand(t.toLowerCase()+' '+i,n)}}}})),!1}}).attr('style',t.replace(/^(\\d+)[,\\s]*(\\d*)(?!\\w)/,function(t,n,i){return'background-position: -'+40*n+'px -'+40*(i||0)+'px;'}));return r}updateInventoryIcons=function(t){$.each(t,function(t,n){$('#igrid-item-'+t).attr('style',n.replace(/^(\\d+)[,\\s]*(\\d*)(?!\\w)/,function(t,n,i){return'background-position: -'+40*n+'px -'+40*(i||0)+'px;'}))}),d[id]=icon},updateInventoryItem=function(t,n,i){var e=$('#igrid-item-'+t);n&&e.prop('title',n),i&&e.data('verbs',i)};var i=updateList;updateList=function(t,n){if('inventory'==t){var a=$('#inventoryAccordion .igrid-item'),o=$();$.each(n,function(t,n){var i=JSON.parse(n),e=i.ElementId,r=$('#igrid-item-'+e);r.length?a=a.not(r):0==(r=c(e,d[e]||'0,0')).parent().length&&(o=o.add(r)),i.Text&&r.prop('title',i.Text),i.Verbs?r.data('verbs',i.Verbs).removeClass('disabled'):r.addClass('disabled')}),a.remove(),o.each(function(t,n){var i=$('#inventoryAccordion .igrid-slot:empty');0==i.length&&(i=e(1)),i.first().append(n)})}else i(t,n)}});")

I'm running out of ideas now.
Do you know if the desktop version gives you any way to access the console, or to see javascript errors?


Yeah, that's what I did after I posted, as shown in the edit.

Throwing in the direct javascript rather than linking to it didn't break anything any worse, but didn't fix things either.

https://i.gyazo.com/3f2e48faf917217c8103a8bc9a3d63bc.png

Here's a screenshot which shows what tools I have, along with an example of the behaviour offline - it drops the item into the right square, but the dragged image remains overlaid on top of it. Doing anything to the original object - using, etc - resets it to the original position afterwards.

Debugger sounds exciting, but that's purely for looking into existing scripts, variables, etc. Log I've never known what the hell it is, as it's blank. HTML tools is, I hope, what you're looking for... here's a shot.

https://i.gyazo.com/c02f5afb19e027b78d2927456120a6ed.png

EDIT:

https://i.gyazo.com/a6952f3c011e6dca292d5596e092b335.png found the damn console. This is what happens when an item is dragged and dropped offline, even if it's into the same square.


Aha, I think we're getting somewhere.

After moving an object, it generates a string representation of the state of the grid, which is saved as an attribute of an invisible element in the output so that it can be loaded by a saved game. I assume that if that's generating an error, it's preventing the 'drop' from finishing.

It looks like I missed out a .get() somewhere in the script, so it's attempting to serialise a jQuery array… and the version of JSON.stringify running on desktop can't handle that.

I'll tidy up that bit of the code.


What a runaround! Sounds like Quest is definitely due for an update in this regard :S

I would never have figured any of this out alone, god...


OK… have managed to replicate the error on my end (by turning a "not supported by this old version of jQuery" into an actual typo), and confirmed that a bug in that function causes exactly the behaviour you described.

Now I just have to fix that bug.


OK… does this version fix that?

JS.eval("$(function(){initialiseGridInventory=function(t){var e=$('#outputData').data('igrid-layout');e&&e.length?$.each(e,function(t,e){var n=i(1,$('#inventoryAccordion'));e.id&&(updateInventoryItem(e.id,e.alias,e.verbs),c(e.id,e.icon).appendTo(n))}):i(25,$('#inventoryAccordion').empty()),$('<style>').appendTo('head').text(t)};var d={};function i(t,e){var n=e.find('.igrid-slot').length;return $($.map(Array(t),function(){return $('<div>',{id:'igrid-cell-'+n++,class:'igrid-slot'}).appendTo(e).droppable({drop:function(t,e){$(this).children().appendTo(e.draggable.parent()),$(e.draggable).appendTo(this),r()}})}))}function r(){$('#outputData').attr('data-igrid-layout',JSON.stringify($('#inventoryAccordion .igrid-slot').map(function(t,e){var n=$(e).children();return n.length&&!n.hasClass('ui-draggable-dragging')?{id:$(n).data('ElementId'),icon:d[$(n).data('ElementId').replace(/\\s/g,'-')],alias:$(n).attr('title'),verbs:$(n).data('verbs')}:{}}).get()))}function c(i,t){var r=i.replace(/\\s/g,'-');if(!r)return $();t&&(d[r]=t);var e=$('#inventoryAccordion .igrid-slot:empty').first(),a=$('<div>',{class:'igrid-item',id:'igrid-item-'+r}).data('ElementId',i).appendTo(e).draggable({containment:e.parent(),helper:'clone',zIndex:100,revert:'invalid',start:function(){a.addClass('igrid-active')},stop:function(){a.removeClass('igrid-active')}}).click(function(t){if(!a.hasClass('disabled')){var e={},n=(a.prop('title')||r).toLowerCase();return e[n]=i||r,t.preventDefault(),t.stopPropagation(),a.blur().jjmenu_popup($.map(a.data('verbs'),function(t){return{title:t,action:{callback:function(){sendCommand(t.toLowerCase()+' '+n,e)}}}})),!1}}).attr('style',t.replace(/^(\\d+)[,\\s]*(\\d*)(?!\\w)/,function(t,e,n){return'background-position: -'+40*e+'px -'+40*(n||0)+'px;'}));return a}updateInventoryIcons=function(t){$.each(t,function(t,e){t=t.replace(/\\s/g,'-'),$('#igrid-item-'+t).attr('style',e.replace(/^(\\d+)[,\\s]*(\\d*)(?!\\w)/,function(t,e,n){return'background-position: -'+40*e+'px -'+40*n+'px;'})),d[t]=e})},updateInventoryItem=function(t,e,n){t=t.replace(/\\s/g,'-');var i=$('#igrid-item-'+t);e&&i.prop('title',e),n&&i.data('verbs',n)};var n=updateList;updateList=function(t,e){if('inventory'==t){var a=$('#inventoryAccordion .igrid-item'),o=$();$.each(e,function(t,e){var n=JSON.parse(e),i=n.ElementId.replace(/\\s/g,'-'),r=$('#igrid-item-'+i);r.length?a=a.not(r):0==(r=c(i,d[i]||'0,0')).parent().length&&(o=o.add(r)),n.Text&&r.prop('title',n.Text),n.Verbs?r.data('verbs',n.Verbs).removeClass('disabled'):r.addClass('disabled')}),a.remove(),o.each(function(t,e){var n=$('#inventoryAccordion .igrid-slot:empty');0==n.length&&(n=i(5,$('#inventoryAccordion'))),n.first().append(e)}),r()}else n(t,e)}});")

Still need to work out how to get the initial inventory items… might have to have the initialisation script search for them manually :S
Haven't tested the save/load functionality yet, but think it should work.


ALMOST!

https://i.gyazo.com/d236bad182c59a188ef820106163677f.png

Still got the massive white space between rows (which can be eliminated by turning line-height to 0, but then the grid turns weird as mentioned), and there's this new error in the transcript, but things seemed to be functioning fine regardless?...

Error running script: Error compiling expression 'turnscript': RootExpressionElement: Cannot convert type 'Object' to expression result of 'Element'


I've got line-height set to 0, and it seems to work fine.

If the layout's changing when you drag things, it could be because it's overflowing? Like, it makes the whole grid a pixel taller, which causes a scrollbar to appear, pushing all the rightmost boxes onto the next line…

Try adding height: 200px; overflow: hidden;on the #inventoryAccordion style?


Oh, ok, I thought were were still going with line height 38 as originally posted. Set that to 0, then added the height and overflow settings to the accordion, now tweaking those around... Almost perfect, just got a little bit of an irritating border around the bottom and right of the pane, which if I can centralize the squares will make a nice even border instead, as I've set the pane's background colour to black.

Is that doable? Setting the 'grid''s style to centralize within the pane?

https://i.gyazo.com/1d05ac9b0702d29ca55859ae8a8fb8d3.png

-- Also, still getting that error message, but stuff still works! Getting rid of Overflow was the right call, I knew it was probably something like that but didn't have the jargon to hand to put it in words.

Looking at the icons being slightly out of alignment, I'm just going to make the igrid-items 40x40 and do their border/background in the tile art, I think. Setting them to 40x40 looks snug as hell~.

Also, thank you so much for all your help! There's still more I want to do with the system, but I have enough 'overcomplex but functional hacks' knowledge to implement most of them in a way I'll understand, albeit non-optimally. Eheh.


That should be doable in CSS. Setting text-align: center; on the accordion will center the cells within it. Vertically, you can tweak the numbers as necessary.

On my test game I'm currently using:

style = "#inventoryAccordion{line-height:0px; text-align: center; overflow: hidden;} .igrid-slot {display: inline-block; background-color: purple; border: 1px solid black; padding: 0px; width: 40px; height: 40px; margin: 0px; overflow: hidden;} .igrid-item {background-image: url('http://icons.iconarchive.com/icons/pixture/board-game/icons-390.jpg'); background-color: yellow; height: 38px; width: 38px; cursor: pointer;} .igrid-active {border: solid 2px #ccc;} .ui-droppable-hover {background: #f4f4f4; border-style: dashed;}"

(I'm just using a random image from the web for testing purposes; as I'm in no way artistic)

I'm not that good at visual design; but I hope that at least the coding side of this is working. I'll take a look at the initial objects when I've got my current video uploaded; would be good if you could let me know if load/save works.


text-align center worked just dandy, after some more height tweaking!

I'll test save/load now! As for initial objects, I do have a hacky workaround for that... executing the items pick up verbs upon entering the first room in the game, clearing the screen, then running the introduction/ using ShowRoomDescription. So, if we can't figure that one out, not the end of the world!

For the actual image, it's a grid of 40x40 icons, yes? does that need to be a specific size, or can it just be any multiple of 40 in height and width?

--- alright, testing save/load. The good news is, items in the inventory are saved! On load, they still appear in the inventory, and everything appears to remain functional.

Bad news is, items are reordered from first slot onwards in the order they were picked up! Not alphabetical, as I originally thought. They also won't all crowd into one cell, thankfully.

Also, you reminded me of the icons thing. I gave both items a different inventoryicon string (3,5 and 1,2 respectively), and both icons generated are 0,0 from my test image. (File name is correct, I believe.)


Looks like save/load isn't working properly, then. I'm not sure what order different things are executed; it should save the position of objects in the grid when a game is saved. I haven't tested that because on the web editor, you can't save a game without publishing it.

The icon selection should work. It sounds like your turnscript isn't running; have you changed that code at all?
Can you check that the turnscript is enabled? (I assume the debugger thing lets you test stuff like that).

Edit: Looks like I missed a line when pasting the code here. igrid_update.enabled = true.

If you're creating the turnscript normally in the editor, you can just tick the "enabled" box.


Hmm, it didn't like me trying to enable it that way. I scrapped the UI ini turn script generation code and just made a turn script (called igrid_update -- I noticed the other one was called GetObject (igrid_update), was that important?

The original error message is gone now, but when an item is in the player inventory it starts throwing an error every turn.

https://i.imgur.com/W5PuVtP.png

Unrecognized dictionary type must, I assume, refer to;
dictionary add (icons, obj.name, obj.inventoryicon)

Also, I noticed that the turn script's code changed a few iterations back, was that intentional? It used to be;

icons = NewStringDictionary()
foreach (obj, ScopeInventory()) {
if (not HasString (obj, "inventoryicon")) obj.inventoryicon = "0,0"
dictionary add (icons, obj.name, obj.inventoryicon)
JS.updateInventoryIcons (icons)
}

and became

icons = NewStringDictionary()
foreach (obj, ScopeInventory()) {
if (HasString (obj, "inventoryicon")) {
dictionary add (icons, obj.name, obj.inventoryicon)
}
}
JS.updateInventoryIcons (icons)


I noticed the other one was called GetObject (igrid_update), was that important?

The hazards of coding at 4am. GetObject there is completely unnecessary, no idea why it was there. I think I removed it from later versions.

The original error message is gone now, but when an item is in the player inventory it starts throwing an error every turn.

Unrecognised dictionary type most often means you're trying to pass a scriptdictionary to a JS. function (it only understands stringdictionary and objectdictionary).
I'm not sure why that would be failing; I can't see any way for the dictionary to end up as another type there.
I'm really hoping there isn't some dumb restriction like the playercontroller not handling dictionaries properly. We could serialise the data manually, but that's going to be tedious and time-consuming.

Also, I noticed that the turn script's code changed a few iterations back, was that intentional?

Yes. When I changed the script to work with Chrome versions before 45, it ended up being simpler to check for an object with no icon specified in the JS. So it's no longer necessary for the turnscript to repeat the same job; it can just send over data for the objects that have the attribute.


Right, right. Well, I'm as stumped as you, then. :S

... Well, almost. There's one thing that's caught my eye. The current code sets icons = new string list...

Should that be = new string dictionary? https://i.imgur.com/qBecj1e.png

Update; I changed it to New String Dictionary, and... error messages gone! Grid icons are now generated based on the item's string (3,5, 2,1 both gave different sections!)

Upon saving and loading, the items are still shunted back to positions 1 and 2, and they also both change to 0,0 icon. A single action will kick the turn script into action and return the correct icons.


... Well, almost. There's one thing that's caught my eye. The current code sets icons = new string list...

No, it says NewStringDictionary().

It said List in the first post; careless typo. I changed it, and mentioned that I'd changed it.
I did think that you might have missed that post, but the code that you posted 45 minutes ago has NewStringDictionary, so I assumed that was the one you're using.

Upon saving and loading, the items are still shunted back to positions 1 and 2, and they also both change to 0,0 icon. A single action will kick the turn script into action and return the correct icons.

You can put do (igrid_update, "script") at the end of the UI Initialisation script if you want, to force it to run immediately when loading a saved game. But I'm not sure why it isn't saving the layout. I'll take another look at that.


-- At one point Quest did crash, and I think I might have mixed up current code with an older version as a result. Sorry for any confusion that may have caused!

And alright, that makes sense, forces it to immediately trigger on start and load.

If the layout thing can be figured out, then all basic functionality is in!

-- Put the code in at the end of UI ini, got this.
Error running script: Error evaluating expression 'GetAllChildObjects(game.pov)': GetAllChildObjects function expected object parameter but was passed 'null' Error running script: Cannot foreach over '' as it is not a list

However, on load, it does as advertized without throwing up a new error. From glancing at it -- sorry, getting distracted in the meat space irl - I'm assuming it's freaking out over not finding anything in the player's inventory in the first turn?


OK… I've got things out of order. The touchpunch library (to make it work with touchscreen) is slowing everything down.

The script deleting the default inventory pane and replacing it with an empty grid is running after the initial items are rendered.
Trying to rearrange it now


Testing version: https://textadventures.co.uk/games/view/ddgrfg_iq0mp1ag9b_mwkq/ui-playground

fingers crossed

Just wish I could test it without cloudflare errors that make me lose track of what I was doing.


OK…

On web version:

  • Hovering over item shows its listalias
  • Clicking an item shows a verb menu
    • If an object's inventryverbs are changed while it is in the inventory, game needs to call JS.updateInventoryItem(name, alias, verbs) to get it to update.
  • When saving a game, each item's location, alias, verbs, and appearance are saved
  • When loading a saved game, each item's location, alias, verbs, and appearance are correctly restored
  • New items are placed in the first available slot. If there aren't enough slots, new ones are created
    • To avoid this, use Quest's inventory limiting feature to ensure that the player can't carry too many items.

On mobile:

  • Everything seems to work correctly :)

How does it work on desktop?

Latest JS code (really ugly because I didn't keep the indentation in line when I was fixing bugs):
$(function() {
    $.getScript('https://mrangel.info/jquery.ui.touch-punch.min.js');
    initialiseGridInventory = function(style) {
        addCells(25, $('#inventoryAccordion')
          .empty());
      $('<style>')
        .appendTo('head')
        .text(style);
  };

  loadHtml = function(content) {
        console.log('Restoring save data:');
    $('#divOutput').html(content);  
      var layout = $('#outputData').data('igrid-layout');
      if (layout && layout.length) {
      console.log('Found layout data:'+$('#outputData').attr('data-igrid-layout'));
      // keeps track of any items in cells which we're moving an item into
      var pending = $();
        $.each(layout, function(i, val) {
              updateInventoryItem(val['id'], val['alias'], val['verbs']);
            var item = addItem(val['id'], val['icon']);
            console.log('Moving item '+val['alias']+' from '+item.parent().attr('id')+' to '+val['parent']+'; setting verbs to '+val['verbs']+' and icon to '+val['icon']);
              var cell = $('#'+val['parent']);
              pending = pending.add(cell.children());
            cell.append(item);
            pending = pending.not(item);
        });
        if (pending.length) {
          pending.each(function () {
              console.log ("WARNING! Cell "+pending.parent().attr('id')+" contains multiple elements!");
             $(this).appendTo($('#inventoryAccordion .igrid-slot:empty').last());
          });
        }
      } else {
       console.log("No saved layout data?");   
      }
  };

  var grid_icons = {};
  updateInventoryIcons = function(data) {
    $.each(data, function(id, icon) {
        id = id.replace(/\s/g, '-');
      $('#igrid-item-' + id)
        .attr('style', icon.replace(/^(\d+)[,\s]*(\d*)(?!\w)/, function(match, x, y) {
          return ('background-position: -' + (x * 40) + 'px -' + (y * 40) + 'px;')
        }))
      grid_icons[id] = icon;
    });
  };

  function addCells(number, container) {
    var count = container.find('.igrid-slot')
      .length;
    return $($.map(Array(number), function() {
      return $('<div>', {
          id: 'igrid-cell-' + (count++),
          class: 'igrid-slot'
        })
        .appendTo(container)
        .droppable({
          drop: function(ev, ui) {
            $(this)
              .children()
              .appendTo(ui.draggable.parent());
            $(ui.draggable)
              .appendTo(this);
            saveGridData();
          }
        });
    }));
  }

  function saveGridData() {
   $('#outputData')
              .attr('data-igrid-layout', JSON.stringify($('#inventoryAccordion .igrid-item:not(.ui-draggable-dragging)')
                .map(
                  function(i, item) {
                      if ($(item).data('ElementId')) {
                    return {
                              id: $(item).data('ElementId'),
                              icon: grid_icons[$(item).data('ElementId').replace(/\s/g, '-')],
                              alias: $(item).attr('title'),
                              verbs: $(item).data('verbs'),
                     parent: $(item).parent().attr('id')
                            };
                  } else {
                   console.log('Storing an element with no ID?');
                   console.log(item);
                   console.log($(item).data());
                   return null;
                  }
                  }
                ).get()));   
  }

  function setIcon(icon, item) {
        return item.attr('style', icon.replace(/^(\d+)[,\s]*(\d*)(?!\w)/, function(match, x, y) {
        return ('background-position: -' + (x * 40) + 'px -' + ((y || 0) * 40) + 'px;')
      }));
  }
  
  function addItem(rawid, icon) {
        var id = rawid.replace(/\s/g, '-');
    if (!id) {
      return $();
    }
    if ($('#inventoryAccordion .igrid-slot').length == 0) {
        // grid hasn't been drawn yet
        setTimeout(function () {
            addItem (rawid, icon);
        }, 100);
        return $();
    }
    if (icon) {
      grid_icons[id] = icon;  
    } else {
      icon = grid_icons[id];
    }
    if ($('#igrid-item-'+id).length) {
      // already exists
        // this should only happen when restoring a save, so we make sure the icon is set correctly before returning it
    return setIcon(icon, $('#igrid-item-'+id));
    }
    var cell = $('#inventoryAccordion .igrid-slot:empty')
      .first();
    var item = $('<div>', {
        class: 'igrid-item',
        id: 'igrid-item-' + id
      })
      .data('ElementId', rawid)
      .appendTo(cell)
      .draggable({
        containment: cell.parent(),
        helper: 'clone',
        zIndex: 100,
        revert: 'invalid',
        start: function() {
          item.addClass('igrid-active');
        },
        stop: function() {
          item.removeClass('igrid-active');
        }
      })
      .click(function(event) {
        if (!item.hasClass("disabled")) {
          var metadata = {};
          var alias = (item.prop('title') || id)
            .toLowerCase();
          metadata[alias] = rawid || id;
          event.preventDefault();
          event.stopPropagation();
          item.blur()
            .jjmenu_popup($.map(item.data('verbs'), function(verb) {
              return {
                title: verb,
                action: {
                  callback: function() {
                    sendCommand(verb.toLowerCase() + ' ' + alias, metadata);
                  }
                }
              };
            }));
          return false;
        }
      });
    return setIcon (icon, item);
  }

  updateInventoryItem = function(id, alias, verbs) {
        id = id.replace(/\s/g, '-');
    var item = $('#igrid-item-' + id);
    if (alias) {
      item.prop('title', alias);
    }
    if (verbs) {
      item.data('verbs', verbs);
    }
  };

  var originalUpdateList = updateList;
  updateList = function(name, listdata) {
    if (name == 'inventory') {
      var oldItems = $('#inventoryAccordion .igrid-item');
      var pending = $();
      $.each(listdata, function(key, value) {
        var objdata = JSON.parse(value);
        var id = objdata['ElementId'].replace(/\s/g, '-');
        var item = $('#igrid-item-' + id);
        if (item.length) {
          oldItems = oldItems.not(item);
        } else {
          item = addItem(id, grid_icons[id] || '0,0');
          // if we failed to add the item (grid is full), then try again after removing items that have been removed from inventory
          if (item.parent()
            .length == 0) {
            pending = pending.add(item);
          }
        }
        if (objdata['Text']) {
          item.prop('title', objdata['Text']);
        }
        if (objdata['Verbs']) {
          item.data('verbs', objdata['Verbs'])
            .removeClass('disabled');
        } else {
          item.addClass('disabled');
        }
      });
      oldItems.remove();
      pending.each(function(index, item) {
        var slot = $('#inventoryAccordion .igrid-slot:empty');
        if (slot.length == 0) {
          slot = addCells(5, $('#inventoryAccordion'));
        }
        slot.first()
          .append(item);
      });
      saveGridData();
    } else {
      originalUpdateList(name, listdata);
    }
  };
});
Compressed version (Including touchpunch inline. I've included the copyright/licensing comments as Quest comments, which I believe is required by the licenses)

If you're in a hurry, you should be able to past all of this into a UI Initialisation script.
But really it would be better to create the turnscript in the editor (make sure it's initialised!), and then call RunTurnScripts() at the end of your start script.

If you're on the desktop editor, it would be better to put the JS above into a file, the CSS into a file, and add those and a copy of jquery.ui.touch-punch.min.js to your project.

//
// jQuery UI Touch Punch 0.2.3
//
// Copyright 2011–2014, Dave Furfero
// Dual licensed under the MIT or GPL Version 2 licenses.
//
//Depends:
//  jquery.ui.widget.js
//  jquery.ui.mouse.js
//
JS.eval("!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent('MouseEvents');d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch='ontouchend'in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,'mouseover'),f(a,'mousemove'),f(a,'mousedown'))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,'mousemove'))},b._touchEnd=function(a){e&&(f(a,'mouseup'),f(a,'mouseout'),this._touchMoved||f(a,'click'),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,'_touchStart'),touchmove:a.proxy(b,'_touchMove'),touchend:a.proxy(b,'_touchEnd')}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,'_touchStart'),touchmove:a.proxy(b,'_touchMove'),touchend:a.proxy(b,'_touchEnd')}),d.call(b)}}}(jQuery);")
// MIT / GPL licensed code above this point

JS.eval("$(function(){initialiseGridInventory=function(t){i(25,$('#inventoryAccordion').empty()),$('<style>').appendTo('head').text(t)},loadHtml=function(t){console.log('Restoring save data:'),$('#divOutput').html(t);var e=$('#outputData').data('igrid-layout');if(e&&e.length){console.log('Found layout data:'+$('#outputData').attr('data-igrid-layout'));var a=$();$.each(e,function(t,e){updateInventoryItem(e.id,e.alias,e.verbs);var n=l(e.id,e.icon);console.log('Moving item '+e.alias+' from '+n.parent().attr('id')+' to '+e.parent+'; setting verbs to '+e.verbs+' and icon to '+e.icon);var i=$('#'+e.parent);a=a.add(i.children()),i.append(n),a=a.not(n)}),a.length&&a.each(function(){console.log('WARNING! Cell '+a.parent().attr('id')+' contains multiple elements!'),$(this).appendTo($('#inventoryAccordion .igrid-slot:empty').last())})}else console.log('No saved layout data?')};var d={};function i(t,e){var n=e.find('.igrid-slot').length;return $($.map(Array(t),function(){return $('<div>',{id:'igrid-cell-'+n++,class:'igrid-slot'}).appendTo(e).droppable({drop:function(t,e){$(this).children().appendTo(e.draggable.parent()),$(e.draggable).appendTo(this),a()}})}))}function a(){$('#outputData').attr('data-igrid-layout',JSON.stringify($('#inventoryAccordion .igrid-item:not(.ui-draggable-dragging)').map(function(t,e){return $(e).data('ElementId')?{id:$(e).data('ElementId'),icon:d[$(e).data('ElementId').replace(/\\s/g,'-')],alias:$(e).attr('title'),verbs:$(e).data('verbs'),parent:$(e).parent().attr('id')}:(console.log('Storing an element with no ID?'),console.log(e),console.log($(e).data()),null)}).get()))}function n(t,e){return e.attr('style',t.replace(/^(\\d+)[,\\s]*(\\d*)(?!\\w)/,function(t,e,n){return'background-position: -'+40*e+'px -'+40*(n||0)+'px;'}))}function l(i,t){var a=i.replace(/\\s/g,'-');if(!a)return $();if(0==$('#inventoryAccordion .igrid-slot').length)return setTimeout(function(){l(i,t)},100),$();if(t?d[a]=t:t=d[a],$('#igrid-item-'+a).length)return n(t,$('#igrid-item-'+a));var e=$('#inventoryAccordion .igrid-slot:empty').first(),r=$('<div>',{class:'igrid-item',id:'igrid-item-'+a}).data('ElementId',i).appendTo(e).draggable({containment:e.parent(),helper:'clone',zIndex:100,revert:'invalid',start:function(){r.addClass('igrid-active')},stop:function(){r.removeClass('igrid-active')}}).click(function(t){if(!r.hasClass('disabled')){var e={},n=(r.prop('title')||a).toLowerCase();return e[n]=i||a,t.preventDefault(),t.stopPropagation(),r.blur().jjmenu_popup($.map(r.data('verbs'),function(t){return{title:t,action:{callback:function(){sendCommand(t.toLowerCase()+' '+n,e)}}}})),!1}});return n(t,r)}updateInventoryIcons=function(t){$.each(t,function(t,e){t=t.replace(/\\s/g,'-'),$('#igrid-item-'+t).attr('style',e.replace(/^(\\d+)[,\\s]*(\\d*)(?!\\w)/,function(t,e,n){return'background-position: -'+40*e+'px -'+40*n+'px;'})),d[t]=e})},updateInventoryItem=function(t,e,n){t=t.replace(/\\s/g,'-');var i=$('#igrid-item-'+t);e&&i.prop('title',e),n&&i.data('verbs',n)};var c=updateList;updateList=function(t,e){if('inventory'==t){var r=$('#inventoryAccordion .igrid-item'),o=$();$.each(e,function(t,e){var n=JSON.parse(e),i=n.ElementId.replace(/\\s/g,'-'),a=$('#igrid-item-'+i);a.length?r=r.not(a):0==(a=l(i,d[i]||'0,0')).parent().length&&(o=o.add(a)),n.Text&&a.prop('title',n.Text),n.Verbs?a.data('verbs',n.Verbs).removeClass('disabled'):a.addClass('disabled')}),r.remove(),o.each(function(t,e){var n=$('#inventoryAccordion .igrid-slot:empty');0==n.length&&(n=i(5,$('#inventoryAccordion'))),n.first().append(e)}),a()}else c(t,e)}});")

style = "#inventoryAccordion{line-height:0px; text-align: center; overflow: hidden;} .igrid-slot {display: inline-block; background-color: purple; border: 1px solid black; padding: 0px; width: 40px; height: 40px; margin: 0px; overflow: hidden;} .igrid-item {background-image: url('"+GetFileURL("inventory_icons.png")+"'); background-color: yellow; height: 38px; width: 38px; cursor: pointer;} .igrid-active {border: solid 2px #ccc;} .ui-droppable-hover {background: #f4f4f4; border-style: dashed;}"

JS.initialiseGridInventory(style)

// only do this once.
// This should really be in the 'start' script, not the UI Initialisation script, but I put it here for the benefit of anyone who wants a one-click installation of the grid inventory
if (GetObject ("igrid_update") = null) {
  create turnscript("igrid_update")
  SetTurnScript (igrid_update) {
    icons = NewStringDictionary()
    foreach (obj, ScopeInventory()) {
      if (HasString (obj, "inventoryicon")) {
        dictionary add (icons, obj.name, obj.inventoryicon)
      }
    }
    JS.updateInventoryIcons (icons)
  }
  igrid_update.enabled = true
  if (HasObject (game, "pov")) {
    do (igrid_update, "script")
  }
}

I've been tinkering about with the CSS, but that's not really my field.


Had a play around, downloaded it, messed around with it in every bugtesting way I could think of, and!...

No bugs I could find! Everything worked! I'll have a play with adding the JS into my own project when I've woken up a bit more!


Alright! The game -really- doesn't like do (igrid_update, "script") being placed in the UI ini script, for some reason. (Also, the turnscript creation in the compressed code is set to create a stringlist again, not a dictionary. With that quick change, and with moving do (igrid_update, "script") into a later script that UI ini, everything works fine. It seems that the UI Ini script fires off before the turnscript is properly initialized, even if made seperately in the editor beforehand. Moving the 'do' to a later script bypasses this error, which would appear on load, too. Of course, that won't fire off on a load, though...

... so my extremely hacky answer? do (igrid_update, "script") in the 'after entering room first time' of the first room, then set the same do (igrid_update, "script") in the ui ini script behind an if check with a simple 'player.started' flag, set that to True in the first room, then it fires off on load without complaining.


lright! The game -really- doesn't like do (igrid_update, "script") being placed in the UI ini script, for some reason.

Just tried it. It works for me.

It seems that the UI Ini script fires off before the turnscript is properly initialized

No, the turnscript is initialised fine. The only error it could give is one about game.pov not being defined.

If you haven't chosen a player object on the game's "Player" tab, it defaults to any object named "player" - but this happens after UI initialisation, so if you rely on it your UI initialisation script can't look at the player's inventory.

Go to the "Player" tab and choose a player object, and it works fine. I edited the script above so that it will check whether player is defined before running the turnscript.

Also, the turnscript creation in the compressed code is set to create a stringlist again, not a dictionary.

Ooops, must have pasted in an old one. Edited.

Moving the 'do' to a later script bypasses this error, which would appear on load, too.

No it wouldn't. On loading, the player has already been initialised (but of course, the turnscript already exists; which causes a different error - now fixed by checking for the turnscript's existence before creating it)

so my extremely hacky answer? do (igrid_update, "script") in the 'after entering room first time' of the first room

That works. Though I think the game start script would be slightly more logical. Either way, it works.

hen set the same do (igrid_update, "script") in the ui ini script behind an if check with a simple 'player.started' flag, set that to True in the first room, then it fires off on load without complaining.

Should be unnecessary. The loadHtml hack in the latest version of the javascript should be able to find the right icons for any objects that were in the inventory during a saved game.


Oh. Erm, setting the player object to player rather than letting the system figure that out itself has fixed that issue, yes. So, no need for the workaround. My bad!

Well, with that, basic functionality has been achieved!

The two optional features I'd like to implement would be the use of HTML tooltips when hovering over a grid item to display the name of it immediately in a nice little black background white text bubble, as opposed to the current 'wait a sec for alt text to kick in', and having the grid size change in response to a weight limit variable (allowing it to be increased or decreased with stats, special gear/milestones (e.g backpack, satchel), heavy objects decreasing available slots, etc.), though I'm just thrilled all the core features are working!

I guess the questions there are;

  • Would HTML tooltips be something introduced in the last 5 years, thus incompatible as standard with Quest's offline browser?
  • The grid was created with a set size, could it be changed during gameplay or would it require a restructuring? At this point, I'm not sure it's worth the hassle.

Off the top of my head (untested):

The use of HTML tooltips when hovering over a grid item to display the name of it immediately in a nice little black background white text bubble, as opposed to the current 'wait a sec for alt text to kick in',

function setTooltip (item, name) {
  if (item.children('.floaty-tooltip').length) {
    item.children('.floaty-tooltip').text(name);
  } else {
    item.css({overflow: 'visible', position: 'relative'});
    var tooltip = $('<div>').appendTo(item).addClass('floaty-tooltip').css({position: 'absolute', top: '20px', right: '20px', padding: 7, backgroundColor: 'black', color: 'white', border: '5px double white'}).text(name).hide();
    item.hover (
      function () { tooltip.show();},
      function () { tooltip.hide();}
    );
    });
  }
}

(probably calling this from within the updateList function; or anywhere else the 'title' attribute is changed)


having the grid size change in response to a weight limit variable

You'd still need to have Quest check the weight limit before taking an item, but…

setGridSize = function (number) {
  if (number > $('.igrid-slot').length) {
    addCells (number - $('.igrid-slot').length, $('#inventoryAccordion'));
  } else {
    var dropped = [];
    while( $('.igrid-slot').length > number) {
      // find the last slot in the grid
      var slot = $('.igrid-slot').last();
      // if it has any children, move them to the previous empty slot
      if (slot.children().length) {
        $('.igrid-slot:empty').last().append(slot.children());
      }
      // If it's still not empty, tell Quest that the player is dropping stuff
      slot.children(':not('.ui-draggable-dragging')').each(function() {
        dropped.push($(this).data('ElementId'));
      });
      slot.remove()
    }
    if (dropped.length) {
      ASLEvent ('GridDroppedItems', dropped);
    }
  }
};

I think that should just let you do JS.setGridSize(15) or whatever. If the grid grows, more cells are added. If the grid shrinks, items at the bottom are pushed up to the next empty cell. If the grid shrinks and not everything will fit, items will be removed from the bottom and the Quest function GridDroppedItems will be called, its single parameter being a stringlist containing the names of the items that were dropped.


Yeah, introducing weight would require use of the built-in weight function, or replacing it. But hey, this looks promising! I'll have a play around when work finishes.


I've been thinking again about making items take up more than one space as a way of representing weight. It might make the calculations a bit of a headache, but I think you could set it up by calculating a bitmask representing the state of the various cells in the grid; and the squares taken up by any given item. Bitwise and will give a quick test for overlaps; and you can give the droppable a reference to a function which tests the permissibility of dropping something there.

I'm not going to start experimenting with the coding now; I haven't done any work yet today. But it's nice to realise that something like this could be done in a simpler way than I expected.


Mm! It's not something I'm personally interested in pursuing, but the more ways this new inventory system can be used, the better!

Oh, there was one other functionality I thought of - multiple tabs for the inventory. Part of the idea of this was to save space, after all, in games with large inventories - letting the grid expand too far would defeat that purpose. To use Final Fantasy 14's inventory system as an example;

https://media.discordapp.net/attachments/696878380682838106/701470357487353957/06a7c408e7903672d9c43a6a23868140.png

Tabs at the top to change the currently displayed page, with any slots past a grid size of 25 starting and expanding a new grid on page 2. Likewise, if that number dropped below 26, it would remove or gray out the 2nd tab until it's needed again. I imagine this would be bloody tricky, though.

Could have a tab dedicated to Key Items, too, for separate storage. Important gear that shouldn't leave the character, quest objects, etc.

Come to think of it... maybe that could be as simple as constraining things so only 25 cells are shown at once, scroll is locked, and hitting tab 2 scrolls it down to the next position, equally spaced? I dunno if I'm explaining that well.


Alrighty! Looking to test the two new functions, but up until now I've been using the UI Ini compressed scripts (except for the turn script creation, which I've done separately). I'd need to compress the new scripts in the same way, so...

Alternately, you advised adding the js into a file, the css into a file and the Jquery touchpunch thing and adding it to the game, instead of doing it through the UI ini script. I'll have to do that to insert the two new functions, but I'm still not clear how you do that. Is it as simple as having the .js files available in the folder, then calling them as normal with the transcript and JS.initialiseGridInventory (style)? Probably a blitheringly obvious question, but if Quest has documentation on using external JS I've not found it.


I don't actually know. I have to use the web editor because the desktop one doesn't run on linux; and the web editor also doesn't allow external .js files.

I know I've seen it mentioned on the forum before that you can add JS files in the desktop editor, but the relevant parts of the documentation seem to be 404 right now.

Another way to do it, if you prefer, is to add an attribute to the game containing the code, so you can just do JS.eval(game.initjs) or similar. Text attributes are allowed to contain line breaks, so you can include the full script that way without needing to compress it. (JS.eval has no problem with values containing multiple lines; it's only the script editor that doesn't like them).


I would have the take the full code and replace every break with a <br>, then? Am I understanding correctly? Strings don't let you copy in existing line breaks and such, from my experience.


(Edit: missed a backtick)
It's been on my mind how to make an object take up more than one space. And I think that giving it a bitmask attribute is the obvious way to do it. Generating that attribute would be a little fiddly, but not too hard.

I didn't expect this code to come out so big… So… tweaking 'intersect' so that when you're dragging an item that covers more than one slot in the grid, it triggers the 'drop' function on the top left square it covers (with a small offset):
var original_intersect = $.ui.intersect;
$.ui.intersect = function (item, slot, mode) {
  if (mode == "topleft") {
    var offsetx = $(item).offset().left + 12 - $(slot).offset().left;
    var offsety = $(item).offset().top + 12 - $(slot).offset().top;
    return (offsetx >= 0 && offsetx < $(slot).width() && offsety >= 0 && offsety < $(slot).height());
  } else {
    original_intersect.call(this, item, slot, mode);
  }
}

Then when we set up the droppable, we can do:

tolerance: "topleft",
accept: function (ui) {
  var item = ui.draggable;
  // if dropping the item here would have part of it going off the edge of the grid, reject it
  if (item.data('grid-width') + (slot.data('slotnumber') % 5) >= 6) { return false; }
  if (item.data('grid-height') + (slot.data('slotnumber) / 5) >= 6) { return false; }
  // Otherwise:
  var targetcells = item.data('grid-layout') << slot.data('slotnumber');
  var occupiedcells;
  var fixedcells'
  // loop over all *other* items in the inventory.
  $('.igrid-item').not(ui.draggable).each(function () {
    // if found item takes up more than one space
    if ($(this).data('grid-shape') > 1) {
      // if the found item is overlapped by the potential drop position
      if (($(this).data('grid-layout') << $(this).parent('.igrid-slot').data('slotnumber')) & targetcells) {
        occupiedcells = occupiedcells | ($(this).data('grid-layout') << $(this).parent('.igrid-slot').data('slotnumber'));
      } else {
        fixedcells = fixedcells | ($(this).data('grid-layout') << $(this).parent('.igrid-slot').data('slotnumber'));
      }
    }
  });
  // occupiedcells is now the grid of cells occupied by items that we would have to move in order to drop here
  // and fixedcells is the grid of cells occupied by other objects
  // So: Could we move all of those items back to where the dragged item came from?
  if (occupiedcells) {
    var moveby = item.parent('.igrid-cell').data('slotnumber') - slot.data('slotnumber');

    if (moveby > 0 && occupiedcells >= (1 << (25-moveby))) { return false; } // move off the bottom of the grid
    if (moveby < 0 && occupiedcells % (1 << -moveby)) { return false; } // move off the top of the grid

    // Check if any of those objects would collide with some other object:
    if ((occupiedcells << moveby) & fixedcells) { return false; }

    // Or if this new position collides with the item we originally dragged:
    if ((occupiedcells << moveby) & (item.data('grid-shape') << slot.data('slotnumber'))) { return false; }

    // if it's moving horizontally, check we're not sliding over the edge
    if (moveby % 5) {
      // quickly make a mask for the first column of the grid - is there a quicker way?
      my column; for (i=0 ; i<5 ; i++) { column = column + 32**i; }
      if (((occupiedcells << moveby) & column) && ((occupiedcells << moveby) & (column << 4))) {return false;}
    }
  }
  // if we haven't returned yet, it's okay to drag that item here
  return true;
},
drop: function(ev, ui) {
  var item = ui.draggable;
  var oldpos = item.parent('.igrid-slot').data('slotnumber');
  var newpos = slot.data('slotnumber');
  item.appendTo(slot);
  var dropped_into = item.data('grid-shape') << newpos;
  var remaining = $();
  // find items that overlap the one we just moved;
  $('.igrid-item').not(item).each (function () {
    var pos = $(this).parent('.igrid-slot').data('slotnumber');
    if (($(this).data('grid-shape') << pos) & dropped_into) {
      $(this).appendTo($('#igrid-cell-'+(pos + oldpos - newpos)));
    }
  });

  // After moving, we need to make sure there are no overlapping items. Because of the 'accept' script, we know that this will only be an issue for single-square items
  var fullcells = 0;
  var searchpos = 0;
  $('.igrid-item').filter(function () {
    if ($(this).data('grid-shape') == 1) {
      return true;
    } else {
      fullcells = fullcells | ($(this).data('grid-shape') << $(this).parent('igrid-slot').data('slotnumber'));
      return false;
    }
  }).filter(function() {
    if (fullcells & (1 << $(this).parent('.igrid-slot').data('slotnumber'))) {
      return true;
    } else {
      fullcells = fullcells | (1 << $(this).parent('igrid-slot').data('slotnumber'));
      return false;
    }
  }).each(function () {
    // find the first empty cell
    while((1 << searchpos) & fullcells) { searchpos++; }
    fullcells = fullcells | (1 << searchpos);
    $(this).appendTo('#igrid-slot-'+searchpos);
  });
} 

(assuming that items have data items grid-width, grid-height, and grid-shape; and that slots have data slotnumber)

You would need to have all items absolutely positioned within their cells, and cells set to overflow: visible, but that shouldn't cause any issues.

And yes, I know I have way too many parentheses in there. I can never remember the operator precedence.

(When you try to drag an item, any items overlapping its new position will move to overlap its old position instead. If this doesn't work, it'll try moving any 1×1 items out of the way, and put them back in the first empty space after moving everything else. This works because moving items around doesn't increase the number of spaces in use, so there will always be space for those 1×1s)

With something like this, you could have large/heavy items take up more than one space in the grid… and still allow players to rearrange them without it being a total pain.


I would have the take the full code and replace every break with a <br>, then? Am I understanding correctly? Strings don't let you copy in existing line breaks and such, from my experience.

As I understand it, strings are quite capable of containing line breaks. The only problem with them is:

  • msg(somestring) will not show line breaks - because all whitespace (tabs, line breaks, spaces) is treated as a single space when a web browser displays it. If you wanted to display the code to the player you'd need to add <br/>s to it, but not for running it.
  • The web editor doesn't allow you to add line breaks in an attribute, except for a room description (in which case it converts them to <br/> automatically, mangling your code). I don't know if the desktop editor also has this limitation. If it does, you'd have to use full code view to put the script into an attribute. (I know that this will work, because I've seen it done this way when looking at the source of other games)
    • In full code view, any attribute (string or script) which contains a < or & character must start with <![CDATA[ and end with ]]>. These aren't part of the attribute; they're just to tell the XML parser that this is character data and not XML.

Ah! See, I tried that earlier; I made an attribute, then tried to put the whole script into said attribute in code view, and it threw an error at me about unexpected characters, shunting me back to code view until I removed it. But adding in the <![CDATA[ -code- ]]> means it's no longer screaming at me. Alright, now to try and actually call it up as before, just with JS.eval(game.inventoryinitialize)... (That's what I called mine.)

... Hmm, no dice. I must be missing something. The full code now displays in the attribute's string, with line breaks, so that's a start...

edit; sorry, to be more useful than 'no dice', nothing initialized. The inventory panel remains as vanilla.

(Alternately, if you know where to input the Tooltip and Gridsetsize in compressed form into the existing compressed code, that would work. I'm not sure if you did that compression by hand, or fed it through something like uglifyjs?)


Does it give you any errors on the console? I'm curious why that isn't working.

In the past, I've set up a system so that I can easily put javascript in Quest using the web editor. I set it up so that any rooms with the alias "JAVASCRIPT" would have their descriptions treated as JS.
The UI Initialisation script to do that was:

foreach (room, FilterByAttribute (AllObjects(), "alias", "JAVASCRIPT")) {
  code = room.description
  code = Replace (code, "<br>", Chr(13))
  code = Replace (code, "<br/>", Chr(13))
  code = Replace (code, "«", "<")
  code = Replace (code, "»", ">")
  code = Replace (code, "§", "&")
  fixedcode = ""
  foreach (line, Split(code, Chr(13))) {
    pos = Instr (line, "//")
    if (pos = 0) {
      fixedcode = fixedcode + line
    }
    else {
      fixedcode = fixedcode + Left (line, pos - 1)
    }
  }
  JS.eval (fixedcode)
}

That's a rather convoluted way to do it; but makes it easier to do small changes while testing something. Just means that I have to change <, >, and & to «, », and § respectively to stop the web editor thinking they're HTML when saving the description.

(Alternately, if you know where to input the Tooltip and Gridsetsize in compressed form into the existing compressed code, that would work. I'm not sure if you did that compression by hand, or fed it through something like uglifyjs?)

I use jscompress; I expect they're all pretty similar. There's nothing to stop you editing the uncompressed code and then compressing it yourself. (Just remember to search and replace if you're going to put it into a Quest script; any " and \ characters in the compressed output will need to be turned into \" and \\ respectively.


Let's see, errors... I got;
Uncaught SyntaxError: Unexpected end of input (UI:1)
Uncaught ReferenceError: updateInventoryIcons is not defined (about:blank:1)

And ah, ok, that makes the prospect far less daunting. Going to try that now!


-- Trying to compress

function setTooltip (item, name) {
  if (item.children('.floaty-tooltip').length) {
    item.children('.floaty-tooltip').text(name);
  } else {
    item.css({overflow: 'visible', position: 'relative'});
    var tooltip = $('<div>').appendTo(item).addClass('floaty-tooltip').css({position: 'absolute', top: '20px', right: '20px', padding: 7, backgroundColor: 'black', color: 'white', border: '5px double white'}).text(name).hide();
    item.hover (
      function () { tooltip.show();},
      function () { tooltip.hide();}
    );
    });
  }
}

alone, throws up; Unexpected token: punc «)» (line: 11, col: 5). Similar thing with the gridset script, so I imagine this is the result of these being snippets intended for insertion into larger scripts with prerequisites already implemented. You said that the tooltip one needs to go into the update list, so... where exactly would I place it within this section of the uncompressed code?

var originalUpdateList = updateList;
  updateList = function(name, listdata) {
    if (name == 'inventory') {
      var oldItems = $('#inventoryAccordion .igrid-item');
      var pending = $();
      $.each(listdata, function(key, value) {
        var objdata = JSON.parse(value);
        var id = objdata['ElementId'].replace(/\s/g, '-');
        var item = $('#igrid-item-' + id);
        if (item.length) {
          oldItems = oldItems.not(item);
        } else {
          item = addItem(id, grid_icons[id] || '0,0');
          // if we failed to add the item (grid is full), then try again after removing items that have been removed from inventory
          if (item.parent()
            .length == 0) {
            pending = pending.add(item);
          }
        }
        if (objdata['Text']) {
          item.prop('title', objdata['Text']);
        }
        if (objdata['Verbs']) {
          item.data('verbs', objdata['Verbs'])
            .removeClass('disabled');
        } else {
          item.addClass('disabled');
        }
      });
      oldItems.remove();
      pending.each(function(index, item) {
        var slot = $('#inventoryAccordion .igrid-slot:empty');
        if (slot.length == 0) {
          slot = addCells(5, $('#inventoryAccordion'));
        }
        slot.first()
          .append(item);
      });
      saveGridData();
    } else {
      originalUpdateList(name, listdata);
    }
  };
});

Yep, looks like I've got an extra bracket left over. Presumably I miscounted them when I was typing (as these fragments were written in the forum, off the top of my head). Line 11 (});) should be removed, I think.

I'd put it next to the other top level functions (addItem, addCells, etc.).

Then call it from updateList, adding a line setTooltip (item, objdata['Text']); after the item.prop('title', objdata['Text']);.

If you're seeing the custom tooltip as well as the default one (I didn't check), then you'll want to change all instances of .prop('title') to .data('title'). (can't just remove them, because the onclick function uses that as the object's name when you click on a verb).

Similar thing with the gridset script,

Looks like I got an extra set of quotes in the selector :not(.ui-draggable-dragging)… any more errors with that one?

One of the reasons I like jscompress is that if I have a syntax error in my code (missing brackets, missing quotes, or similar), it'll quite often show me where.


Alright! So, going one addition at a time for troubleshooting purposes... I inserted setTooltip in after addCells, and called it from updateList with the line you provided.

The result is promising, but needs tweaking! Namely; https://i.imgur.com/ZdP8mrN.png

The name appears nice and visible, but it's confined to within the grid item! Upon clicking on an item to drag it, it's freed from that prison to display properly.


The name appears nice and visible, but it's confined to within the grid item!

I thought I'd already commented on that, but I can't see it above.
The CSS needs to include overflow: visible for the item, the cell, and possibly for #inventoryAccordion.

In case you haven't come across it yet, the overflow attribute controls how a display element behaves when something inside it goes outside its borders.

  • overflow: scroll - might attempt to resize the object or move its borders; or shrink the inner item to fit, or display a scroll bar, depending on the type of element.
    scroll scroll
  • overflow: hidden - hide anything that goes out out bounds
    hidden hidden
  • overflow: visible - display the overflowing content anyway
    visible visible

Initially I suggested overflow:hidden on the accordion, to stop it developing scroll bars (which only left enough space for 4 cells per row) when an item was dragged right up to the edge. But with the tooltips, it needs to be visible instead; and the cells will need to have the same.

Upon clicking on an item to drag it, it's freed from that prison to display properly.

I assume that's because the item is cloned including the tooltip, moving it outside its cell. But that makes me wonder if there's another issue here. I'm not sure if clicking on the tooltip rather than the item itself would allow you to drag it, or launch the verb menu. And if so, whether the tooltip would fail to disappear when the drag or menu ends.


Fear not, if you click on the tooltip it picks up the item as normal for dragging. You can click on the tooltip to activate the verb menu, too, but it doesn't seem to cause any abnormal behaviour. I might see about moving the tooltip display either hovering below or above the item so that it can't be interacted with, just in case.

setCss ("#inventoryAccordion", "overflow: visible;") is back on for the #inventoryAccordian, as the pane's been resized a bit to accommodate for it now anyway.

JS.setCss ("#igrid-slot", "overflow: visible")
JS.setCss ("#igrid-item-", "overflow: visible")
-- aren't working, but I no doubt have the names wrong. Picked them out through guesswork from the main code.

NEVERMIND!

JS.setCss (".igrid-slot", "overflow: visible") was the ticket! Looks like grid item isn't needed, either. Gunna let me food digest, then I'll fiddle with adding the next step, the resize function.

Edit; Ah. ONE issue I just realized... it was fine with a short .alias, but the box can't expand past the current set size! So a long name gets horribly crushed inside that tiny tooltip. I know there's a command for making it as big as it needs to be, uh... rmm...

EDIT EDIT; I stand corrected! It's not really long alias', it alias' with spaces in them. All the words jumble into the center of the box, which is only as long as the longest word in the string.


Looks like grid item isn't needed, either.

The setTooltip function already sets the item it is attaching the tip on to overflow: visible.

EDIT EDIT; I stand corrected! It's not really long alias', it alias' with spaces in them. All the words jumble into the center of the box, which is only as long as the longest word in the string.

My second guess would be to use non-breaking spaces. Yhe sequence &nbsp; is rendered as a space, but is considered to be letter for the purpose of word-wrapping. So in setTooltip (before the rest of the function), I think you could add a line name = name.replace(/\s+/, "&nbsp;"); - although you would then have to change both uses of .text(name) within that function to .html(name).

(My first guess was that you can control wrapping with CSS. But there's a bunch of different attributes for it, I can't remember which is which, and I'm not sure if some of them were added in the last 5 years)


Abusing &nbsp;!... half worked.

https://i.imgur.com/xw4k3ce.png

It only works on the first space in a string. The second space doesn't work, at which point things start to wrap back around... so I might need to throw in a word-wrap css thing.

https://i.imgur.com/ILd0n9B.png

Also, I just realized that upon doing anything with an object - look, use, etc - the tooltips of an item render in the top right of the grid window for some reason, also pictured. However, this inexplicably fixes the spacing issue!?

So, the turnscript fires, fixes the tooltips, but also glues their position up to the top right of the grid.

Just tested the prior code - same thing. The tooltip is fixed, spacing and word-wrap wise, by the turnscript... but it moves and glues the position. They'll still disappear and trigger as normal when hovering over a grid item, but they'll appear up in the top right.

Disabling the turnscript means the spacing fix no longer happens on movement/action, but the top-right glueing thing doesn't happen.


Ooops, I missed the g (global) modifier out of .replace(/\s+/g, "&nbsp;").

I'm in the dark about the second issue; will have to try it myself and take a look at what's happening.


Also, I just realized that upon doing anything with an object - look, use, etc - the tooltips of an item render in the top right of the grid window for some reason, also pictured. However, this inexplicably fixes the spacing issue!?

OK… as originally posted, the code places the "tooltip" inside the item. So it wraps the text as well as it can to make it fit in there, even if it's displayed outside it.

But, the tooltip's coordinates are relative to the "closest positioned ancestor" - and for some reason the items are ceasing to be positioned. (I can't actually work out why they were positioned to start with, but…)

To fix your two issues, I modified the style string to add:

.floaty-tooltip {white-space: nowrap;}
.igrid-item {position: relative; top: 0px;}

This forces the icon to be positioned 0 pixels lower than where normal text flow would put it - and it now counts as a "positioned ancestor" when the tooltip is working out which object its coordinates are relative to.

white-space: nowrap seems to work for the spacing thing, a neater solution than the <br> method.

But in my tests, tooltips are getting cut off at the left edge of the grid; #inventoryAccordion keeps switching to overflow:hidden and I can't find where it's changing.

Does that work for you?


My latest version (fiddled with a few more things, fixed typos):
CSS:

#inventoryAccordion{
     line-height: 0px;
     overflow: visible;
}
.igrid-slot {
     display: inline-block;
     overflow: visible;
     background-color: purple;
     border: 1px solid black;
     padding: 0px;
     width: 40px;
     height: 40px;
     margin: 0px;
}
.ui-droppable-hover {
     background-color: #f4f4f4;
     border-style: dashed;
     magic: hover;
}
.ui-droppable-hover .igrid-item {
     opacity: 0.6;
}
.igrid-item {
     position: relative;
     top: 1px;
     background-image: url('" + GetFileURL("oooh do animations work?.gif") + "');
     background-color: yellow;
     height: 38px;
     width: 38px;
     cursor: pointer;
}
.igrid-active {
     border-color: #ccc;
}
.floaty-tooltip {
     white-space: nowrap;
}

And JS:

$(function () {
  $.getScript('https://mrangel.info/jquery.ui.touch-punch.min.js');
  initialiseGridInventory = function (style) {
    addCells(25, $('#inventoryAccordion')
      .empty());
    var styleBlock = $('<style>')
      .appendTo('head')
      .text(style);
    changeGridHoverStyle = function (hover) {
      styleBlock.text(style.replace(/magic:\s*hover;?/, hover));
    };
  };
  loadHtml = function (content) {
    console.log('Restoring save data:');
    $('#divOutput').html(content);
    var layout = $('#outputData').data('igrid-layout');
    if (layout && layout.length) {
      console.log('Found layout data:' + $('#outputData').attr('data-igrid-layout'));
      // keeps track of any items in cells which we're moving an item into
      var pending = $();
      $.each(layout, function (i, val) {
        updateInventoryItem(val['id'], val['alias'], val['verbs']);
        var item = addItem(val['id'], val['icon']);
        console.log('Moving item ' + val['alias'] + ' from ' + item.parent().attr('id') + ' to ' + val['parent'] + '; setting verbs to ' + val['verbs'] + ' and icon to ' + val['icon']);
        var cell = $('#' + val['parent']);
        pending = pending.add(cell.children());
        cell.append(item);
        pending = pending.not(item);
      });
      if (pending.length) {
        pending.each(function () {
          console.log("WARNING! Cell " + pending.parent().attr('id') + " contains multiple elements!");
          $(this).appendTo($('#inventoryAccordion .igrid-slot:empty').last());
        });
      }
    } else {
      console.log("No saved layout data?");
    }
  };
  var grid_icons = {};
  updateInventoryIcons = function (data) {
    $.each(data, function (id, icon) {
      id = id.replace(/\s/g, '-');
      $('#igrid-item-' + id)
        .attr('style', icon.replace(/^(\d+)[,\s]*(\d*)(?!\w)/, function (match, x, y) {
          return ('background-position: -' + (x * 40) + 'px -' + (y * 40) + 'px;')
        }))
      grid_icons[id] = icon;
    });
  };

  function addCells(number, container) {
    var count = container.find('.igrid-slot')
      .length;
    return $($.map(Array(number), function () {
      return $('<div>', {
          id: 'igrid-cell-' + (count++),
          class: 'igrid-slot'
        })
        .appendTo(container)
        .droppable({
          hoverClass: 'ui-droppable-hover',
          drop: function (ev, ui) {
            $(this)
              .children()
              .appendTo(ui.draggable.parent());
            $(ui.draggable)
              .appendTo(this);
            saveGridData();
          }
        });
    }));
  }

  function saveGridData() {
    $('#outputData')
      .attr('data-igrid-layout', JSON.stringify($('#inventoryAccordion .igrid-item:not(.ui-draggable-dragging)')
        .map(
          function (i, item) {
            if ($(item).data('ElementId')) {
              return {
                id: $(item).data('ElementId'),
                icon: grid_icons[$(item).data('ElementId').replace(/\s/g, '-')],
                alias: $(item).data('title'),
                verbs: $(item).data('verbs'),
                parent: $(item).parent().attr('id')
              };
            } else {
              console.log('Storing an element with no ID?');
              console.log(item);
              console.log($(item).data());
              return null;
            }
          }
        ).get()));
  }

  function setIcon(icon, item) {
    return item.attr('style', icon.replace(/^(\d+)[,\s]*(\d*)(?!\w)/, function (match, x, y) {
      return ('background-position: -' + (x * 40) + 'px -' + ((y || 0) * 40) + 'px;')
    }));
  }

  function setTooltip(item, name) {
    if (item.children('.floaty-tooltip').length) {
      item.children('.floaty-tooltip').text(name);
    } else {
      item.css({ overflow: 'visible', position: 'relative' });
      var tooltip = $('<div>').appendTo(item).addClass('floaty-tooltip').css({ position: 'absolute', top: '20px', right: '20px', padding: 7, backgroundColor: 'black', color: 'white', border: '5px double white' }).text(name).hide();
      item.hover(
        function () { tooltip.show(); },
        function () { tooltip.hide(); }
      );
    }
  }

  function addItem(rawid, icon) {
    var id = rawid.replace(/\s/g, '-');
    if (!id) {
      return $();
    }
    if ($('#inventoryAccordion .igrid-slot').length == 0) {
      // grid hasn't been drawn yet
      setTimeout(function () {
        addItem(rawid, icon);
      }, 100);
      return $();
    }
    if (icon) {
      grid_icons[id] = icon;
    } else {
      icon = grid_icons[id];
    }
    if ($('#igrid-item-' + id).length) {
      // already exists
      // this should only happen when restoring a save, so we make sure the icon is set correctly before returning it
      return setIcon(icon, $('#igrid-item-' + id));
    }
    var cell = $('#inventoryAccordion .igrid-slot:empty')
      .first();
    var item = $('<div>', {
        class: 'igrid-item',
        id: 'igrid-item-' + id
      })
      .data('ElementId', rawid)
      .appendTo(cell)
      .draggable({
        containment: cell.parent(),
        helper: 'clone',
        zIndex: 100,
        revert: 'invalid',
        start: function () {
          item.addClass('igrid-active');
          if (item.css('background-image')) {
            changeGridHoverStyle('background-image:' + item.css('background-image') + '; background-position:' + item.css('background-position') + ';');
          }
        },
        stop: function () {
          item.removeClass('igrid-active');
        }
      })
      .click(function (event) {
        if (!item.hasClass("disabled")) {
          var metadata = {};
          var alias = (item.data('title') || id)
            .toLowerCase();
          metadata[alias] = rawid || id;
          event.preventDefault();
          event.stopPropagation();
          item.blur()
            .jjmenu_popup($.map(item.data('verbs'), function (verb) {
              return {
                title: verb,
                action: {
                  callback: function () {
                    sendCommand(verb.toLowerCase() + ' ' + alias, metadata);
                  }
                }
              };
            }));
          return false;
        }
      });
    return setIcon(icon, item);
  }
  updateInventoryItem = function (id, alias, verbs) {
    id = id.replace(/\s/g, '-');
    var item = $('#igrid-item-' + id);
    if (alias) {
      item.data('title', alias);
      setTooltip(item, alias);
    }
    if (verbs) {
      item.data('verbs', verbs);
    }
  };
  var originalUpdateList = updateList;
  updateList = function (name, listdata) {
    if (name == 'inventory') {
      var oldItems = $('#inventoryAccordion .igrid-item');
      var pending = $();
      $.each(listdata, function (key, value) {
        var objdata = JSON.parse(value);
        var id = objdata['ElementId'].replace(/\s/g, '-');
        var item = $('#igrid-item-' + id);
        if (item.length) {
          oldItems = oldItems.not(item);
        } else {
          item = addItem(id, grid_icons[id] || '0,0');
          // if we failed to add the item (grid is full), then try again after removing items that have been removed from inventory
          if (item.parent()
            .length == 0) {
            pending = pending.add(item);
          }
        }
        if (objdata['Text']) {
          item.data('title', objdata['Text']);
          setTooltip(item, objdata['Text']);
        }
        if (objdata['Verbs']) {
          item.data('verbs', objdata['Verbs'])
            .removeClass('disabled');
        } else {
          item.addClass('disabled');
        }
      });
      oldItems.remove();
      pending.each(function (index, item) {
        var slot = $('#inventoryAccordion .igrid-slot:empty');
        if (slot.length == 0) {
          slot = addCells(5, $('#inventoryAccordion'));
        }
        slot.first()
          .append(item);
      });
      saveGridData();
    } else {
      originalUpdateList(name, listdata);
    }
  };
});

Check out my sample game to see how it looks in practice


Played around with your sample game;
http://play2.textadventures.co.uk/Play.aspx?id=ddgrfg_iq0mp1ag9b_mwkq

(Relinking it because WOW this thread got long). Everything seems to work fine... though, it remains to be seen if it plays nicely with the offline player. I'll try implementing it once my work finishes up for the day.


Probably worth mentioning that in the stylesheet, magic: hover will now be replaced by the 'background-image' and 'background-position' attributes of the item being dragged. Can be combined with opacity to display a transparent ghost image of the dragged item on the cell it will be dropped into.

I also added the parameter hoverClass: 'ui-droppable-hover', to the droppable() call. That's done by default in jQuery UI 1.12, but Quest comes with 1.11 for some reason.


Alright, fiddled around with things! top: 1px is, from my testing, not needed. With white space word wrap in the tooltip css and absolute positioning in the grid items, which I believe I was missing before, things seem almost fine now!

Almost, because I found a new issue. And it's present in your version, too, just a bit difficult to notice.

https://i.imgur.com/2q9Q6Ip.png
When two items are on different rows, if the tooltip intersects with an icon below it (and possibly above, haven't tested) then it will be behind the icon - you can just about see this by the bottom of the border being cut off.

https://i.imgur.com/PJ5nBMF.png
However, when they're on the same row, the tooltip is presented from and center.

Thankfully, I managed to figure something out for myself, for once! I added in z-index: 1; into the floaty tooltip's css, and now it always renders above everything else.

Outside of positioning - I'm thinking I might have the tags show up to the left of the icon instead of below/above, as it seems easier - that's the tooltips fully implemented now, I believe! Oh, also, I've set the css for the tooltips to display at 41px below the icons, so they look attached but can't actually be clicked on - trying to move down means the cursor goes over that 1px gap and shuts the tooltip off before it can be interacted with.


Ah… I added in z-index, but it looks like I only did it on my testing version (editing the compressed code in the web editor) and didn't copy the change back to the master copy.


God, the amount of fuckups I almost reported because I was adding tweaks to old code by mistake... Mood.

After some more playing, not found any issues. So, I guess up next is the next addition, the setgridsize additional function.

Throwing

setGridSize = function (number) {
  if (number > $('.igrid-slot').length) {
    addCells (number - $('.igrid-slot').length, $('#inventoryAccordion'));
  } else {
    var dropped = [];
    while( $('.igrid-slot').length > number) {
      // find the last slot in the grid
      var slot = $('.igrid-slot').last();
      // if it has any children, move them to the previous empty slot
      if (slot.children().length) {
        $('.igrid-slot:empty').last().append(slot.children());
      }
      // If it's still not empty, tell Quest that the player is dropping stuff
      slot.children(':not('.ui-draggable-dragging')').each(function() {
        dropped.push($(this).data('ElementId'));
      });
      slot.remove()
    }
    if (dropped.length) {
      ASLEvent ('GridDroppedItems', dropped);
    }
  }
};

Through simplify threw up an error; Unexpected token: string «)», expected: punc «,» (line: 14, col: 49)

I remembered you saying that slot.children(':not('.ui-draggable-dragging')').each(function() { had an extra pair of quotes, I changed it to slot.children(':not(.ui-draggable-dragging)').each(function() {and it compressed. That seem about right?

EDIT; Went ahead and tested it like that, and... complete success! JS.setGridSize(15) works flawlessly! I stacked all the items up at the last 3 slots, and they were moved into slots 13, 14 and 15 of the shrunken grid without any errors.

However, growing the grid has issues with current css settings. Or, mine, anyway.
https://i.gyazo.com/0bfa04850d31e45eba5c3eac8ee27f99.png

Brings me back to my musings about the possibility of creating 'tabs', or the illusion of them, for any amount over 25...


That should work, I think :)

You can call it from Quest when the inventory limit changes; JS.setGridSize(26) or whatever. If the player already has too many items to fit in that size, the Quest function GridDroppedItems will be called. Its parameter should be a stringlist, containing the names of items which will be dropped. It's up to your game to remove those from the inventory; otherwise the grid will be enlarged again to fit them.

I'd suggest something like:

<function name="GridDroppedItems" parameters="itemnames">
  items = NewObjectList()
  dropped = NewObjectList()
  foreach (name, itemnames) {
    list add (items, GetObject (name))
  }
  foreach (object, items) {
    if (ListContains (ScopeReachableInventory(), object) and GetBoolean (object, "drop")) {
      object.parent = game.pov.parent
    }
    else {
      // That item isn't droppable; so find something we *can* drop instead
      object = PickOneObject (ListExclude (FilterByAttribute (ScopeReachableInventory(), "drop", true), items))
      object.parent = game.pov.parent
      list add (dropped, object)
    }
    msg ("You find that you can no longer hold onto "+FormatList(dropped, ", ", ", and", "everything")+"!")
  }
</function>

(Note: this doesn't account for objects which have a 'drop' script; but it does account for dropping something else instead if the items selected by the JS can't be dropped)


Good stuff, though I intend on making sure that this scenario doesn't come to pass to begin with through carefully gating everything behind weight scripts and such. Still, I may have a play around with it later!

Also, I just thought to test something about the grid - does it automatically update when an item's alias changes? (which will be vital with wearables). The answer is 'yes'! I also just remembered you talking about inventory/display verb lists updating if changed earlier, did your latest code include something to cover that? I can't test that right at the minute, going to get to it later if I can. I have two verbs, 'modify' and 'sell' that appear on weapons/armour when entering a workshop-capable room, and on anything sellable when entering a shop, respectively.

Edit; Easiest solution to the grid spilling out of the pane was to set the accordian's width to the same as it is currently, but set the height to auto. Now to shrinks and expands with the grid while keeping it in rows of 5 max!

Oh, and question. How difficult would it be to get the gridsize editing to read quest attributes for the purposes of maths? Lets say I have a situation where I pick up a backpack as an upgrade - it adds five slots. But, it's a sandbox game, so they could have suffered a penalty already, or gained other bonuses, or carry weight is lowering the grid size. How would I go about doing something like a normal quest expression, something like player.weight = player.weight + 5 when getting a backpack upgrade, then do JS.setGridSize (player.weight)?

EDIT EDIT; nevermind! Tested it myself. Set 'player.weight' to 10, did JS.setGridSize (player.weight), and it worked like a charm! If I do the calculations outside of js then just set with the final result, sorted!


(sorry this post took so long. I typed it, then the captcha errored out, so I tried refreshing the page, and it looks like the site was down)

Also, I just thought to test something about the grid - does it automatically update when an item's alias changes? (which will be vital with wearables). The answer is 'yes'! I also just remembered you talking about inventory/display verb lists updating if changed earlier, did your latest code include something to cover that? I can't test that right at the minute, going to get to it later if I can. I have two verbs, 'modify' and 'sell' that appear on weapons/armour when entering a workshop-capable room, and on anything sellable when entering a shop, respectively.

The answer seems to be that items whose alias or displayverbs changes will sometimes be updated in the sidebar. This is done in the Quest core, and the code there is a little beyond me (I don't know C# so well).

Usually it does the right thing, but I've seen a few situations where that doesn't happen, and I haven't been able to work out why.

I know that the inventory is sent over to the JS as a single dictionary; so if an item moves in or out of the inventory, you can rely on it updating the alias and verbs of anything else. In other circumstances, it usually seems to do the right thing; but sometimes it doesn't.

I would suggest that if you have any code that changes the listalias or inventoryverbs of an object that might be held (such as the 'wear' command, or the script you mention), it would be safer to add a line like:

JS.updateInventoryItem (object.name, object.listalias, object.inventoryverbs)

(using the appropriate object name, of course)

Theoretically you could put this inside the turnscript, to make sure it's always up-to-date. But as these data items don't change frequently, I think it makes more sense to add a line to the script that's changing them.


Hmm... Alright! I'll keep an eye out to see if it ever crops up in testing, and if it does, I'll add this to my wear, remove, shop check and mod check turn scripts.


(Completely unnecessary; this is just me thinking about alternate ways to do things that might be more efficient)

If I was changing the verbs of a whole lot of objects at once, I'd start wondering if there was a way to handle it in the menu-generation script. Like… modify the script that draws the popup menu so that it skips the "buy" and "sell" verbs unless the player is in a shop.

Like, in the turnscript you could have something like:

JS.maskVerb ("buy/sell", some_expression_that_returns_true_if_the_player_is_in_a_shop)
JS.maskVerb ("modify", some_expression_that_returns_true_if_you_can_modify_equipment_here)

Then it would be fairly simple to modify the script that draws the popup menu so that it will miss out those verbs.

For object links in the text:

var maskedVerbs = {};
maskVerb = function (verb, enabled) {
  $.each($.isArray(verb)) ? verb : verb.split(/[\/;]/), function (v) {
    maskedVerbs[v.toLowerCase()] = !enabled;
  };
};

var original_buildMenuOptions = buildMenuOptions;
buildMenuOptions = function (verbs, text, elementId) {
  return original_buildMenuOptions(verbs, text, elementId).map (function (data) {
    return maskedVerbs[data.title.toLowerCase()] ? null : data;
  });
};

and for the inventory pane, a couple of lines in addItem:

            .jjmenu_popup($.map(item.data('verbs'), function (verb) {
              return {

would change to:

            .jjmenu_popup($.map(item.data('verbs'), function (verb) {
              return maskedVerbs[verb] ? null : {

So instead of having to go over every object and add or remove verbs, you can just add or remove those verbs to a list of "verbs to skip when drawing the popup menus".

I know that's a weird way to go about it :-p Just seems more efficient to me.


It's not wholly related to the topic at hand, but I have a new issue. Namely, I can't seem to get Padding-Right in Quest's Game > Interface tab to actually do anything.

The inventory grid is currently large enough at default that, in addition to my status pane, it knocks my map (which is contained in a side pane) off the bottom of the screen. I want to expand the game display width and move some of the panes onto the left, so they flank the text and I can have my status pane and inventory windows be as large as I want.

Problem is, I can't do that while padding-right refuses to cooperate... setting in enough padding-left to make room for the panes has the text spilling through the panes on the right.

EDIT; while I'm still curious, I did figure out a workaround. Setting the correct padding on left - the size of the panes, and then a little more - and then setting the #gameContent's width to an appropriate amount to make sure the text aligns up nicely in the center of the two pane positions.


Here's a thought I had.

How difficult do you think it would be to replicate this system to apply to Places and Objects/This room contains, too? I'm thrilled to bits with the inventory system and have begun integrating it into my framework - no problems so far! - though I'm wondering if it can be extended to another grid...

Swapping between grids would be difficult, I think, but also not particularly needed; drop and pick up would simply swap items between the two grids. Having that consistency would look nice, not to mention provide opportunities for sorting in safe locations for keeping treasure troves of junk and resources, heh.

https://i.gyazo.com/af19050dd78745504120c4ddeedfb901.png

The obvious, immediate issue seems, to me, that it would effectively be changing in every single location you went into. And grid size consistency... would it be better to make it simply match the number of items inside of it, or would it be better to have it a set large size per room?


I was thinking about making it work for both panes initially; that's why addCells takes a parameter for the parent object. But I may have made assumptions about the layout when tweaking bits trying to get stuff working.

In theory, it should be quite simple to have two grids, working interchangeably.

The 'drop' event would be (pseudocode):

if (slot.parent().contains(item)) {
  move slot's children to item's parent (as before)
  move item to slot
} else {
  leave the helper (the mobile clone of a dragged icon) where it is
  add some kind of 'wait pointer' over the helper
  store the identity of the object being dragged, and the cells it was being dragged to/from
  generate a "drop" or "take" command
}

And in updateList, inside the loop for each item:

if (this was an item with a wait pointer over it) {
  remove the wait pointer
  select either the "from" or "to" cells, based on which list is being updated
  animate the helper towards the chosen cell
    when finished: remove the helper, add the actual item to the cell
} else {
  normal behaviour
}

At the end of updateList, if there's a pending take/drop that hasn't shown up in either list, remove the helper. (for example, if the player dropping an item causes it to smash)

The obvious, immediate issue seems, to me, that it would effectively be changing in every single location you went into. And grid size consistency... would it be better to make it simply match the number of items inside of it, or would it be better to have it a set large size per room?

I would say that it's better to give it a small, sane default size. Maybe 3 rows; but something that could be configured for any given game.
I think it would likely be best to have the turnscript also notify the JS what the current room is.

If the room has changed, remove all cells from placesObjects and draw a new row of cells. But I'd give placesObjects a 'dummy' column at the right and a dummy row at the bottom, which aren't allowed to contain cells. Dropping an item onto the dummy area will add a new row of cells to put it in (and if too many rows are added, the dummy column will be partially covered by the scrollbar, avoiding any items being obscured.

The JS can save the layout per room as long as it knows the room name. And a new row will be added automatically if necessary to hold items.


In Pykrete's example:

https://i.gyazo.com/af19050dd78745504120c4ddeedfb901.png

How did you guys place some of the right-side panes on the left side, and then center the whole lot?

Could you share the code please?


That would be on me, and it's some really hacky, awful implementation, eheh.

I couldn't figure out how to do it whilst retaining the normal functionality of the panes repositioning when accordions are closed, as I used position: absolute; in the css for the panes. As a result, while the panes on the right will still shift up and down when panes above them are closed/opened, the ones on the left will remain where they are. This is why the panes on the left are ones that won't resize in-game, while I left the two prone to resizing on the right, where they would maintain their normal functionality.

(That, and the rule of western eye-order. We read left to right, thus, the top left corner should contain the most important information, in my opinion.)

JS.setCss ("#gameContent", "width: 1000px;")
// ---
JS.setCss ("#customStatusPane", "position: absolute;")
JS.setCss ("#customStatusPane", "right: 1281px;")
JS.setCss ("#customStatusPane", "top: 0px;")
JS.setCss ("#customStatusPane", "width: 208px;")
JS.setCss ("#compassLabel", "width: 200px;")
JS.setCss ("#compassAccordion", "position: absolute;")
JS.setCss ("#compassAccordion", "right: 1280px;")
JS.setCss ("#compassAccordion", "top: 275px;")
JS.setCss ("#compassAccordion", "width: 214px;")
JS.setCss ("#compassLabel", "position: absolute;")
JS.setCss ("#compassLabel", "right: 1280px;")
JS.setCss ("#compassLabel", "width: 200px;")
JS.setCss ("#compassLabel", "top: 236px;")

As mentioned above, the padding options didn't seem to want to work for what I wanted to do, so I set a custom width to #gameContent, then padded it substantially left in the Interface tab of Quest.

Please note that the px numbers input above will not work for your game's display width unless it matches mine! Mine is 1500.

Also, the map in its own accordion is entirely separate code, another MrAngel miracle if I remember correctly.

JS.eval ("$('<h3 id=\"mapLabel\" class=\"ui-accordion-header ui-helper-reset ui-state-active ui-corner-top\"><span class=\"ui-icon ui-icon-triangle-1-s\"></span><span class=\"accordion-header-text\">Map</span></h3><div id=\"mapAccordion\" class=\"ui-accordion-content-active ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom\"></div>').insertAfter(\"#compassAccordion\");$('#gridPanel').hide();$('#gamePanelSpacer').height('0');var size = $(\"#mapAccordion\").innerWidth();$('#gridCanvas').width(size).height(size).appendTo($('#mapAccordion'));paper.view.viewSize.width = size;paper.view.viewSize.height = size;$(\"#gridPanel\").hide();$(\"#mapLabel\").bind(\"click\", $._data($(\"#inventoryLabel\")[0]).events['click'][0]['handler']);$(\"#mapLabel\").bind(\"mouseover\", $._data($(\"#inventoryLabel\")[0]).events['mouseover'][0]['handler']);$(\"#mapLabel\").bind(\"mouseout\", $._data($(\"#inventoryLabel\")[0]).events['mouseout'][0]['handler']);")

^ All one line, in UI Initialization. Also, I have to do my js.setCss in seperate chunks, as in the first code snippet, for some reason - or Quest throws a wobbly at me.

My code is gross, sorry.


Thanks Pykrete. Let me play around with this code...


Just be aware that there are undoubtedly better ways of doing it.


couldn't figure out how to do it whilst retaining the normal functionality of the panes repositioning when accordions are closed,

The accordions are a mess anyway. The multiopenaccordion plugin doesn't allow you to add extra panes once it's started. I found a really hacky way to do it for the map pane, but there should be a better approach.

I suspect that if you want panes on the left, the ideal method would be to create two containers, gamePanesLeft and gamePanesRight which they can be added to. If you want to, you could wrap a container div around each label/accordion pair, so that you can just do $('#gamePanesLeft,#gamePanesRight').sortable({handle: 'h3', connectWith: '#gamePanesLeft,#gamePanesRight'}); to let the player drag panes around.


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums