Adding a Header to Window_Command subclass

nathanlink169

Game Dev by Career, no idea what I'm doing
Veteran
Joined
Aug 15, 2013
Messages
72
Reaction score
32
First Language
English
Primarily Uses
RMMV
Hello all!

TL-DR - I'm looking to have an unselectable "Header" inside of a Window_Command subclass.

I'm working on a custom quest system in the game. I've written a script to populate some quest data, and now I'm writing the code to show the quests the player can currently go on. I would like to split this list out into two separate lists. At the top will be the active quests. At the bottom will be the completed quests. Above the active quests, I'm looking to add an "Active Quests" header: I hope to make this text either bigger than the selectable texts, or at least a different colour. Does anyone know how to go about doing something like this? I've attached my code at the bottom

(please don't judge the formatting too harshly, it's horrible - I'm a c++ developer who's only about 10 development hours into Javascript!)

Code:
//-----------------------------------------------------------------------------
// Window_MenuQuestList
//
// The window for selecting a quest and travelling.

function Window_MenuQuestList() {
    this.initialize.apply(this, arguments);
}

Window_MenuQuestList.prototype = Object.create(Window_Command.prototype);
Window_MenuQuestList.prototype.constructor = Window_MenuQuestList;

Window_MenuQuestList.prototype.initialize = function (x, y) {
    Window_Command.prototype.initialize.call(this, x, y);
    this.selectLast();

    this._previousIndex = -10; // set this to something that index() will never be set to
};

Window_MenuQuestList._lastCommandSymbol = null;

Window_MenuQuestList.initCommandPosition = function () {
    this._lastCommandSymbol = null;
};

Window_MenuQuestList.prototype.windowWidth = function () {
    return 240;
};

Window_MenuQuestList.prototype.windowHeight = function () {
    return Graphics.boxHeight;
};

Window_MenuQuestList.prototype.numVisibleRows = function () {
    return this.maxItems();
};

Window_MenuQuestList.prototype.update = function () {
    Window_Command.prototype.update.call(this);

    if (this.index() != this._previousIndex) {
        this._previousIndex = this.index();

        var key = this._questKeyArray[this.index()];
        this._helpWindow._text = $gamePlayer._questInformation[key]["description"];
        this._helpWindow.refresh();
    }
    console.log(this.index());
};


Window_MenuQuestList.prototype.makeCommandList = function () {
    this.addMainCommands();
};

Window_MenuQuestList.prototype.addMainCommands = function () {
    var questArray = $gamePlayer._questInformation;

    this._questKeyArray = {};

    var index = 0;

    // TODO: Insert "Active Quests" Header here

    for (var key in questArray) {
        var value = questArray[key];
        if (value["unlocked"] === true) {
            this.addCommand(value["title"], undefined, true, undefined);

            this._questKeyArray[index] = key;
            index = index + 1;
        }
    }

    // TODO: Show Completed Quests Here
};

Window_MenuQuestList.prototype.processOk = function () {
    Window_MenuQuestList._lastCommandSymbol = this.currentSymbol();
    Window_Command.prototype.processOk.call(this);
};

Window_MenuQuestList.prototype.selectLast = function () {
    this.selectSymbol(Window_MenuQuestList._lastCommandSymbol);
};
 

Ossra

Formerly Exhydra
Veteran
Joined
Aug 21, 2013
Messages
1,076
Reaction score
848
First Language
English
Primarily Uses
RMMV
@nathanlink169 What you will need to do in order to create a header is add the height of a single line (or perhaps two if you want to use larger text) to the base rect of each item. You can do this by hijacking the 'itemRect' function in your window and adding in the value returned from the 'lineHeight' function (usually 36 pixels). The following is from code I used on a command window I wanted to have a header on :

Code:
Window_GameNew.prototype.itemRect = function(index) {
  var rect = Window_Command.prototype.itemRect.call(this, index);
  rect.y  += this.lineHeight();
 
  return rect;
};

Window_GameNew.prototype.refresh = function() {
  Window_Command.prototype.refresh.call(this);
 
  this.drawText('My Header', 0, 0, this.contentsWidth(), 'center');
};


EDIT: Here is the rest of the code I used for the window. I added in some code that deals with a larger font size for the header :

Code:
function Window_GameNew() {
    this.initialize.apply(this, arguments);
}

Window_GameNew.prototype = Object.create(Window_Command.prototype);
Window_GameNew.prototype.constructor = Window_GameNew;

Window_GameNew.prototype.initialize = function() {
    Window_Command.prototype.initialize.call(this, 0, 0);
    this.updatePlacement();
    this.openness = 0;
};

Window_GameNew.prototype.windowWidth = function() {
    return 250;
};

Window_GameNew.prototype.itemTextAlign = function() {
  return 'center';
};

Window_GameNew.prototype.numVisibleRows = function() {
    return 3;
};

Window_GameNew.prototype.itemRect = function(index) {
  var rect = Window_Command.prototype.itemRect.call(this, index);
  rect.y  += this.lineHeight() + 20;
 
  return rect;
};

Window_GameNew.prototype.updatePlacement = function() {
    this.x = (Graphics.boxWidth - this.width) / 2;
    this.y = (Graphics.boxHeight - this.height) / 2;
};

Window_GameNew.prototype.makeCommandList = function() {
    this.addCommand('Yes', 'newGame');
    this.addCommand('No',  'cancel');
};

Window_GameNew.prototype.refresh = function() {
  Window_Command.prototype.refresh.call(this);

  this.contents.fontSize = 56;
  this.drawText('Start the game?', 0, 5, this.contentsWidth(), 'center');
};

Window_GameNew.prototype.contentsHeight = function() {
  var height = Window_Command.prototype.contentsHeight.call(this);

  return height + 20;
};

Window_GameNew.prototype.fittingHeight = function(numLines) {
  var height = Window_Command.prototype.fittingHeight.call(this, numLines);

  return height + 20;
};

To accommodate the larger text at the top of the window, I changed the 'y' value in drawText to move the text down, then added some more buffer space in itemRect as well as contentsHeight, and fittingHeight. If you keep the text normal sized, then you will not have to fiddle and tinker as much.
 
Last edited:

nathanlink169

Game Dev by Career, no idea what I'm doing
Veteran
Joined
Aug 15, 2013
Messages
72
Reaction score
32
First Language
English
Primarily Uses
RMMV
@nathanlink169 What you will need to do in order to create a header is add the height of a single line (or perhaps two if you want to use larger text) to the base rect of each item. You can do this by hijacking the 'itemRect' function in your window and adding in the value returned from the 'lineHeight' function (usually 36 pixels). The following is from code I used on a command window I wanted to have a header on :
Hey Ossra, thanks for the reply! I got it a good chunk of the way through with your help, so I really appreciate it!

I'm running into another issue. I lied a little bit - I actually want to add two headers: one for the available quests, and one for the completed quests. I've managed to get everything in place, and it looks fairly good so far!

upload_2019-10-3_21-56-52.png

Unfortunately I'm running into a bit of an issue with the placement of the selection box. It starts off in the right place, but when I move the selection down, it jerks down one space, most likely due to the extra space I jammed in the middle of the list.

upload_2019-10-3_22-1-7.png
Notice how the text description is describing the fourth quest - the selection box has moved down one, despite the fact the fourth quest is actually the one selected.

Do you have any idea why this is happening or how to fix it? I'm attaching my code at the bottom of this post once again. Thank you again so much for your help!
Code:
//-----------------------------------------------------------------------------
// Window_MenuQuestList
//
// The window for selecting a quest and travelling.

function Window_MenuQuestList() {
    this.initialize.apply(this, arguments);
}

Window_MenuQuestList.prototype = Object.create(Window_Command.prototype);
Window_MenuQuestList.prototype.constructor = Window_MenuQuestList;

Window_MenuQuestList.prototype.initialize = function (x, y) {
    Window_Command.prototype.initialize.call(this, x, y);
    this.selectLast();

    this._previousIndex = -10;
    this._completedIndex = -10;
};

Window_MenuQuestList._lastCommandSymbol = null;

Window_MenuQuestList.initCommandPosition = function () {
    this._lastCommandSymbol = null;
};

Window_MenuQuestList.prototype.windowWidth = function () {
    return 240;
};

Window_MenuQuestList.prototype.windowHeight = function () {
    return Graphics.boxHeight;
};

Window_MenuQuestList.prototype.numVisibleRows = function () {
    return this.maxItems();
};

Window_MenuQuestList.prototype.refresh = function () {
    Window_Command.prototype.refresh.call(this);

    this.changePaintOpacity(true);
    this.drawText('Available', 0, 0, this.contentsWidth(), 'center');
    this.drawText('Completed', 0, (this._completedIndex + 1) * this.lineHeight(), this.contentsWidth(), 'center');
};

Window_MenuQuestList.prototype.update = function () {
    Window_Command.prototype.update.call(this);

    if (this.index() != this._previousIndex) {
        this._previousIndex = this.index();

        var key = this._questKeyArray[this.index()];
        this._helpWindow._text = $gamePlayer._questInformation[key]["description"];
        this._helpWindow.refresh();
    }
    console.log(this.index());
};

Window_MenuQuestList.prototype.itemRect = function (index) {
    var rect = Window_Command.prototype.itemRect.call(this, index);
    rect.y += this.lineHeight();

    if (index >= this._completedIndex) {
        rect.y += this.lineHeight();
    }

    return rect;
};


Window_MenuQuestList.prototype.makeCommandList = function () {
    this.addMainCommands();
};

Window_MenuQuestList.prototype.addMainCommands = function () {
    var questArray = $gamePlayer._questInformation;

    this._questKeyArray = {};

    var index = 0;

    // Active Quests

    for (var key in questArray) {
        var value = questArray[key];
        if (value["unlocked"] === true && value["complete"] === false) {
            this.addCommand(value["title"], undefined, true, undefined);

            this._questKeyArray[index] = key;
            index = index + 1;
        }
    }

    this._completedIndex = index;

    // Completed Quests

    for (var key in questArray) {
        var value = questArray[key];
        if (value["unlocked"] === true && value["complete"] === true) {
            this.addCommand(value["title"], undefined, false, undefined);

            this._questKeyArray[index] = key;
            index = index + 1;
        }
    }
};

Window_MenuQuestList.prototype.processOk = function () {
    Window_MenuQuestList._lastCommandSymbol = this.currentSymbol();
    Window_Command.prototype.processOk.call(this);
};

Window_MenuQuestList.prototype.selectLast = function () {
    this.selectSymbol(Window_MenuQuestList._lastCommandSymbol);
};
 

Ossra

Formerly Exhydra
Veteran
Joined
Aug 21, 2013
Messages
1,076
Reaction score
848
First Language
English
Primarily Uses
RMMV
Code:
if (index >= this._completedIndex) {
        rect.y += this.lineHeight();
}
@nathanlink169 Shouldn't that if statement just be 'index > this._completedIndex' ? I think the extra lineHeight is being added because it equals the index of the last active quest.
 

nathanlink169

Game Dev by Career, no idea what I'm doing
Veteran
Joined
Aug 15, 2013
Messages
72
Reaction score
32
First Language
English
Primarily Uses
RMMV
Code:
if (index >= this._completedIndex) {
        rect.y += this.lineHeight();
}
@nathanlink169 Shouldn't that if statement just be 'index > this._completedIndex' ? I think the extra lineHeight is being added because it equals the index of the last active quest.
That's what I thought originally too. It's a bit of a weird thing that's going on. It starts on the first item (in this case, "Third Quest")

As soon as I move either up or down, the entire selection box is permanently one below what it should be at. So, after moving the cursor once, highlighting Fourth Quest will actually select the third quest, highlighting Completed will show the Fourth Quest, and highlighting First Quest will show the first quest. I cannot move the cursor back to highlight Third Quest, even though it was originally there.

If you would like, I can put together a sample project to send which may show this behaviour a little better.
 

Ossra

Formerly Exhydra
Veteran
Joined
Aug 21, 2013
Messages
1,076
Reaction score
848
First Language
English
Primarily Uses
RMMV
@nathanlink169 Hmm, yeah, a sample project would be much easier to troubleshoot.
 

nathanlink169

Game Dev by Career, no idea what I'm doing
Veteran
Joined
Aug 15, 2013
Messages
72
Reaction score
32
First Language
English
Primarily Uses
RMMV
@nathanlink169 Hmm, yeah, a sample project would be much easier to troubleshoot.
I've attached it here - I didn't include the Audio or Img folders, just due to file size - if you just throw the default audio and image folders in there it should work!
 

Attachments

Ossra

Formerly Exhydra
Veteran
Joined
Aug 21, 2013
Messages
1,076
Reaction score
848
First Language
English
Primarily Uses
RMMV
@nathanlink169 Ah, I found the issue. When the Window_Command 'initialize' function is being called, the 'addMainCommands' function is run and '_completedIndex' is set to the index of the last active quest. Unfortunately, right after the 'initialize' function is completed, both '_previousIndex' and '_completedIndex' are set to '-10'. Therefore, whenever 'itemRect' checks to see whether the 'index' argument is greater than '_completedIndex', it is always true since no index less than '-10'. The effect is that the cursor is bumped down two lines.

To correct the issue, place the two variables before the Window_Command 'initialize' is called :

Code:
Window_MenuQuestList.prototype.initialize = function (x, y) {
    this._previousIndex = -10;
    this._completedIndex = -10;
 
    Window_Command.prototype.initialize.call(this, x, y);

    this.selectLast();
};

Also, I created a partial re-write of the current code in order to use an array as the quest container instead of an object to simply several processes :

Code:
//-----------------------------------------------------------------------------
// Scene_GetQuest
//-----------------------------------------------------------------------------

function Scene_GetQuest() {
  this.initialize.apply(this, arguments);
}

Scene_GetQuest.prototype = Object.create(Scene_MenuBase.prototype);
Scene_GetQuest.prototype.constructor = Scene_GetQuest;

Scene_GetQuest.prototype.initialize = function () {
  Scene_MenuBase.prototype.initialize.call(this);
};

Scene_GetQuest.prototype.create = function () {
  Scene_MenuBase.prototype.create.call(this);

  this.createHelpWindow();
  this.createListWindow();
};

// 'Scene_MenuBase' changed to 'Scene_GetQuest' to keep issues from arising down the road
Scene_GetQuest.prototype.createHelpWindow = function () {
  this._helpWindow = new Window_QuestHelp();
  this.addWindow(this._helpWindow);
};

Scene_GetQuest.prototype.createListWindow = function () {
  this._listWindow = new Window_MenuQuestList(0, 0);
  // Removed un-needed handlers
  this._listWindow.setHandler('cancel', this.popScene.bind(this));
  // Added helpWindow to listWindow via built-in helper function
  this._listWindow.setHelpWindow(this._helpWindow);
  this.addWindow(this._listWindow);
};

//-----------------------------------------------------------------------------
// Window_MenuQuestList
//-----------------------------------------------------------------------------

function Window_MenuQuestList() {
  this.initialize.apply(this, arguments);
}

Window_MenuQuestList.prototype = Object.create(Window_Command.prototype);
Window_MenuQuestList.prototype.constructor = Window_MenuQuestList;

Window_MenuQuestList.prototype.initialize = function (x, y) {
  Window_Command.prototype.initialize.call(this, x, y);
  this.selectLast();

  // No need for 'previousIndex' and 'completedIndex'
};

// Changed 'lastCommandSymbol' to 'lastCommandId' to return cursor to last selected index
Window_MenuQuestList._lastCommandId = 0;

Window_MenuQuestList.initCommandPosition = function () {
  this._lastCommandSymbol = 0;
};

Window_MenuQuestList.prototype.windowWidth = function () {
  return 240;
};

Window_MenuQuestList.prototype.windowHeight = function () {
  return Graphics.boxHeight;
};

Window_MenuQuestList.prototype.numVisibleRows = function () {
  return this.maxItems();
};

Window_MenuQuestList.prototype.refresh = function () {
  Window_Command.prototype.refresh.call(this);

  // Get number of completed quests to determine position of 'Completed' header text
  var completed = $gamePlayer._questInformation.filter(function(quest) {
    return !quest.completed;
  });

  this.changePaintOpacity(true);
  this.drawText('Available', 0, 0, this.contentsWidth(), 'center');
  this.drawText('Completed', 0, (completed.length - 1) * this.lineHeight(), this.contentsWidth(), 'center');
};

// Over-write built-in helpWindow update function
Window_MenuQuestList.prototype.updateHelp = function() {
  // ext of each command contains the index of the quest in the $gamePlayer._questInformation array
  var ext  = this.currentExt();
  var text = $gamePlayer._questInformation[ext].description;

  this._helpWindow.setText(text);
  this._helpWindow.refresh();
};

Window_MenuQuestList.prototype.itemRect = function (index) {
  var rect = Window_Command.prototype.itemRect.call(this, index);
  // Base Y value bump for first header
  rect.y += this.lineHeight();

  // If current item is not enabled, item is completed quest
  if (!this.isCommandEnabled(index)) {
    rect.y += this.lineHeight();
  }

  return rect;
};

Window_MenuQuestList.prototype.makeCommandList = function () {
  this.addMainCommands();
};

// Simplified adding commands
Window_MenuQuestList.prototype.addMainCommands = function () {
  var questList = $gamePlayer._questInformation;

  for (var i = 0; i < questList.length; i++) {
    var quest = questList[i];

    if (quest.unlocked) {
      // Title, Symbol, Enabled, Ext (questInformation index)
      this.addCommand(quest.title, 'quest', !quest.complete, i);
    }
  }
};

Window_MenuQuestList.prototype.processOk = function () {
  // Set lastCommandId to current index on 'Ok'
  Window_MenuQuestList._lastCommandId = this.index();

  Window_Command.prototype.processOk.call(this);
};

Window_MenuQuestList.prototype.selectLast = function () {
  this.select(Window_MenuQuestList._lastCommandId);
};

//-----------------------------------------------------------------------------
// Window_QuestHelp
//-----------------------------------------------------------------------------

function Window_QuestHelp() {
  this.initialize.apply(this, arguments);
}

Window_QuestHelp.prototype = Object.create(Window_Base.prototype);
Window_QuestHelp.prototype.constructor = Window_QuestHelp;

Window_QuestHelp.prototype.initialize = function () {
  var width = Graphics.boxWidth - 240;
  var height = this.fittingHeight(2);

  Window_Base.prototype.initialize.call(this, 240, 0, width, height);
  this._text = '';
};

Window_QuestHelp.prototype.setText = function (text) {
  if (this._text !== text) {
    this._text = text;
    this.refresh();
  }
};

Window_QuestHelp.prototype.clear = function () {
  this.setText('');
};

Window_QuestHelp.prototype.setItem = function (item) {
  this.setText(item ? item.description : '');
};

Window_QuestHelp.prototype.refresh = function () {
  this.contents.clear();

  this.drawTextEx(this._text, this.textPadding(), 0);
};

//-----------------------------------------------------------------------------
// Game_Player
//-----------------------------------------------------------------------------

// Add a function 'alias'
var NP_GamePlayer_initMembers = Game_Player.prototype.initMembers;

Game_Player.prototype.initMembers = function() {
  // Call old function to set player variables
  NP_GamePlayer_initMembers.call(this);

  // Set 'questInformation' variable so we do not have to set it on the fly
  this._questInformation = [];
};

Game_Player.prototype.addQuest = function (title, description, unlocked, complete) {
  var quest = {
    "title": title,
    "description": description,
    "unlocked": typeof unlocked !== 'undefined' ? unlocked : true,
    "complete": typeof complete !== 'undefined' ? complete : false
  };

  // Add quest object to array
  this._questInformation.push(quest);

  // Sort array by complete status
  // Completed quests at bottom of list to simplify printing process in quest list window
  this._questInformation.sort(function(x, y) {
    return (y.complete === x.complete) ? 0 : y.complete ? -1 : 1;
  });
};

// updateQuest function accepts single property,value pair or an array
// $gamePlayer.updateQuest('First Quest', 'complete', false);
// $gamePlayer.updateQuest('First Quest', ['complete', 'unlocked'], [false, true]);
Game_Player.prototype.updateQuest = function (title, property, value) {
  for (var i = 0; i < this._questInformation; i++) {
    var quest = this._questInformation[i];

    if (quest.title === title) {
      if (Array.isArray(property)) {
        if (property.length !== value.length) break;
        property.forEach(function(prop, index) {
          quest[prop] = value[index];
        });
      } else {
        quest[property] = value;
      }
      break;
    }
  }
};
Adding quests via script calls :

Code:
◆Script:$gamePlayer.addQuest(
:      :"First Quest",
:      :"This is the first quest description",
:      :true,
:      :true);
◆Script:$gamePlayer.addQuest(
:      :"Second Quest",
:      :"This is the second quest description",
:      :false,
:      :false);
◆Script:$gamePlayer.addQuest(
:      :"Third Quest",
:      :"This is the third quest description");
◆Script:$gamePlayer.addQuest(
:      :"Fourth Quest",
:      :"This is the fourth quest description");
 

nathanlink169

Game Dev by Career, no idea what I'm doing
Veteran
Joined
Aug 15, 2013
Messages
72
Reaction score
32
First Language
English
Primarily Uses
RMMV
@nathanlink169 Ah, I found the issue. When the Window_Command 'initialize' function is being called, the 'addMainCommands' function is run and '_completedIndex' is set to the index of the last active quest. Unfortunately, right after the 'initialize' function is completed, both '_previousIndex' and '_completedIndex' are set to '-10'. Therefore, whenever 'itemRect' checks to see whether the 'index' argument is greater than '_completedIndex', it is always true since no index less than '-10'. The effect is that the cursor is bumped down two lines.
Ohhh, this makes sense. Thank you so much for your help! I appreciate it a lot.

Would you mind if I direct messaged you some questions about your re-write of the code? It's off topic for this forum post, but I have a couple more questions, if you wouldn't mind.
 

Users Who Are Viewing This Thread (Users: 0, Guests: 1)

Latest Threads

Latest Profile Posts

Less than a week and I can move into my new apartment and out of this bad situation. I'm so excited! Then game making power will increase. :kaoluv:
Enter the password in letters. (English subtitles)
I really tried to pull off something for halloween, but I don't feel like going on. I feel like the plot I was building was too generic, and I couldn't connect to it as I do with other projects. On the bright side, I've been working on my cosmic-puzzle project, so far I think the core mechanics are working properly, so I'll be creating some large test maps to see how it works out.
People2_5 & SF_Monster1 added!

Ami
--- X Costume ---

M.Healer: I'm wearing M.Mage's Costume.
M.Mage: I'm wearing M.Healer's Costume.
M.Knight: Why not using the Scary Costume like me,The Scary Frankenstein?
M.Healer: Err…Because we have low of Budget.
M.Mage: Yeah,right.

Forum statistics

Threads
104,602
Messages
1,007,487
Members
136,084
Latest member
TikeTenGamesOffical
Top