ThePix / QuestJS

A major re-write of Quest that is written in JavaScript and will run in the browser.
MIT License
66 stars 13 forks source link

Development propositions: draw exits in maps and setting to display map on entering a zone #19

Closed Kln95130 closed 3 years ago

Kln95130 commented 3 years ago

Edit: I added the code to take into account the fact that an exit is locked.

So, after getting my hands into the zone library, I have one proposition for the map feature, which is to allow the developer to mark the exits on the map in a simple manner.

First, we will use two properties to the exits of a zone, mapX and mapY. They represent the position of the exit on the map. They will be calculated by a function that I call getMapxAndY().

res.getMapXAndY = function(exit) {
    switch (exit.dir) {
      case "north":
        return {mapX: exit.x, mapY: exit.y + 1};
      case "northeast":
        return {mapX: exit.x + 1, mapY: exit.y + 1};
      case "east":
        return {mapX: exit.x + 1, mapY: exit.y};
      case "southeast":
        return {mapX: exit.x + 1, mapY: exit.y - 1};
      case "south":
        return {mapX: exit.x, mapY: exit.y - 1};
      case "southwest":
        return {mapX: exit.x - 1, mapY: exit.y - 1};
      case "west":
        return {mapX: exit.x - 1, mapY: exit.y};
      case "northwest":
        return {mapX: exit.x - 1, mapY: exit.y + 1};
      // Same coordinates than the exit because the room is located outside of the map's plane
      case "up":
      case "down":
      case "in":
      case "out":
        return {mapX: exit.x, mapY: exit.y};
      //In case of unknown direction, we return a decimal value to ensure getExitAt will always be false when comparing with the cell
      default:
        return {mapX: 1.1, mapY: 1.1};
    }
  }

Second, we add three elements we will make use of: a variable for the exit colour, a function to check if the cell is qualified as an "exit cell", and an edit to drawMap() to check for valid exits before borders. To the occasion, we could add a "hidden" property so that the exit is not drawn on the map.

  res.openExitColour = "green";
  res.lockedExitColour = "red";

  res.getExitAt = function(x, y) {
    for (let exit of this.exits) {
      //We do the condition here so that we spare the calculation if we do not need to do it
      if (typeof exit.isHidden !== 'function' || !exit.isHidden()) {
        let cell = this.getMapXAndY(exit);
        if (x === cell.mapX && y === cell.mapY) {
          return {isLocked: exit.isLocked};
        }
      }
    }
    return false
  }

  res.drawMap = function() {
    if (this.size === undefined) return false
    const cells = []
    const features = []
    const labels = []
    for (let x = -this.size; x <= this.size; x++) {
      for (let y = -this.size; y <= this.size; y++) {
        const x2 = (this.size + x) * this.cellSize
        const y2 = (this.size - y) * this.cellSize

        let exit = this.getExitAt(x, y);
        if (exit) {
          let cellColour = (exit.isLocked) ?  this.lockedExitColour : this.openExitColour;
          cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + cellColour + '"/>');
        }
        // ELSE IF because we do not want to colour the cell as a border over it
        else if (this.getBorderAt(x, y)) {
          cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + this.outsideColour + '"/>')
        }

        else {
          cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + this.insideColour + '"/>')
        }
        const feature = this.getFeatureAt(x, y)
        if (feature) {
          const colour = feature.zoneColour || this.featureColour
          features.push('<circle cx="' + (x2+this.cellSize/2) + '" cy="' + (y2+this.cellSize/2) + '" r="' + (this.cellSize/2 - 1) + '" stroke="none" fill="' + colour + '"/>')
          if (feature.zoneMapName) labels.push('<text x="' + (x2+this.cellSize) + '" y="' + (y2+5) + '" style="font: ' + this.mapFont + '; fill: black;">' + feature.zoneMapName + '</text>')
        }
      }
    }

In a next update, this could lead to allow the player to draw simple (as in quadrilateral) rooms on the map using the coordinates of each angle.

I will keep on experimenting. If I make any substantial progress, I will update this ticket.

Finally, I think it could be a useful option to have a setting, that would run w[game.player.loc].drawMap(); each time the player starts the turn in a zone, instead of using a turn script. That, or have a permanent map display in the page whenever the player is in a zone.

ThePix commented 3 years ago

If you look at lang.exit_list in lang.js (line 748), you will see x and y values are already in there. That should mean you do not need the getMapxAndY() function. You can access the object in lang.exit_list usinbg the data attribute of the exit, so if you are in a function that belongs to an exit, you can do this.data.x to get the x displacement.

With regards to an updating map, that would be cool, and should not be too difficult. It is something Quest 6 needs more generally, and ideally would be integrated with that.

Kln95130 commented 3 years ago

Okay, this seems to work:

 for (let exit of this.exits) {
      if (typeof exit.isHidden !== 'function' || !exit.isHidden()) {
        let newX = exit.x + this[exit.dir].data.x;
        let newY = exit.y + this[exit.dir].data.y;
        if (x == newX && y == newY) {
          return {isLocked: exit.isLocked};
        }
      }
    }
Kln95130 commented 3 years ago

I think I found a way, albeit it relies on the developer positioning the rooms himself, and the rooms being square:

add rooms: [] property to ZONE(), as an array of generic objects. Each generic object will have four properties: minX, maxX, minY, maxY

  rooms:[
    {minX: 0, maxX: 1, minY: 1, maxY: 2}
  ]

Then, add a function to check if the cell's x and y are within the room's limits. The room could have the option to be hidden (for instance if unexplored.

res.roomColour = "black";

  res.getRoomAt=function(x , y) {
    for (let room of this.rooms) {
      if ((typeof room.isHidden !== 'function' || !room.isHidden()) && x >= room.minX && x <= room.maxX && y >= room.minY && y <=room.maxY) {
        return true;
      }
    }
    return false;
  }

Then add the control in drawMap:

 if (this.getRoomAt(x, y)) {
          cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + this.roomColour + '"/>')
        }
        else {
          let exit = this.getExitAt(x, y);
          if (exit) {
            let cellColour = (exit.isLocked) ?  this.lockedExitColour : this.openExitColour;
            cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + cellColour + '"/>');
          }
          else if (this.getBorderAt(x, y)) {
            cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + this.outsideColour + '"/>')
          }
          else {
            cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + this.insideColour + '"/>')
          }
          const feature = this.getFeatureAt(x, y)
          if (feature) {
            const colour = feature.zoneColour || this.featureColour
            features.push('<circle cx="' + (x2+this.cellSize/2) + '" cy="' + (y2+this.cellSize/2) + '" r="' + (this.cellSize/2 - 1) + '" stroke="none" fill="' + colour + '"/>')
            if (feature.zoneMapName) labels.push('<text x="' + (x2+this.cellSize) + '" y="' + (y2+5) + '" style="font: ' + this.mapFont + '; fill: black;">' + feature.zoneMapName + '</text>')
          }
        }

You know your code better than me, so you will probably find a more elegant solution. But that should get you started.

Kln95130 commented 3 years ago

I've edited drawMap a little, so that the exit cell gets the exit colour on top of the room's colour.

    for (let x = -this.size; x <= this.size; x++) {
      for (let y = -this.size; y <= this.size; y++) {
        const x2 = (this.size + x) * this.cellSize
        const y2 = (this.size - y) * this.cellSize

        let exit = this.getExitAt(x, y);
        if (exit) {
          let cellColour = (exit.isLocked) ?  this.lockedExitColour : this.openExitColour;
          cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + cellColour + '"/>');
        }
        else if (this.getRoomAt(x, y)) {
          cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + this.roomColour + '"/>')
        }
        else if (this.getBorderAt(x, y)) {
          cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + this.outsideColour + '"/>')
        }
        else {
          cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + this.insideColour + '"/>')
        }
        const feature = this.getFeatureAt(x, y)
        if (feature) {
          const colour = feature.zoneColour || this.featureColour
          features.push('<circle cx="' + (x2+this.cellSize/2) + '" cy="' + (y2+this.cellSize/2) + '" r="' + (this.cellSize/2 - 1) + '" stroke="none" fill="' + colour + '"/>')
          if (feature.zoneMapName) labels.push('<text x="' + (x2+this.cellSize) + '" y="' + (y2+5) + '" style="font: ' + this.mapFont + '; fill: black;">' + feature.zoneMapName + '</text>')
        }

      }
Kln95130 commented 3 years ago

Coming for one last dev proposition (for the moment ;)):

Label your exits with a letter, to help the player. This could tie in, in the future, with the addition of a legend to the map.

exits:[
    {x:0, y:0, dir:'north', dest:'playerCabin', msg:'You enter your cabin.', letter: 'E'},
    {x:-1, y:0, dir:'south', dest:'captainCabin', msg:"You enter the captain's cabin.", isLocked: true, lockedmsg: "The captain has locked her door. You will need her permission to enter.", letter: 'D'},
    {x:-3, y:0, dir:'north', dest:'briefingRoom', msg:'You enter the briefing room.', letter: 'B'},
    {x:-3, y:0, dir:'west', dest:'commandRoom', msg:'You enter the command room.', letter: 'A'},
    {x:-3, y:0, dir:'south', dest:'shipAirlock', msg:'You enter the airlock.', isLocked: true, lockedmsg: "The airlock is locked during space travel, for obvious safety reasons.", letter: 'C'},
    {x:2, y:0, dir:"south", dest: "shipElevator", msg: "You call the ship's elevator, then step inside.", letter: 'G'},
    {x:2, y:1, dir: "down", dest: "jollyRiderMidBridge", msg: "You hop into the hole. The magtube makes you glide down gently one floor below.", letter: 'F'}
  ],
es.getExitAt = function(x, y) {

    for (let exit of this.exits) {
      //We do the condition here so that we spare the calculation if we do not need to do it
      if (typeof exit.isHidden !== 'function' || !exit.isHidden()) {
        //let cell = this.getMapXAndY(exit);
        let newX = exit.x + this[exit.dir].data.x;
        let newY = exit.y + this[exit.dir].data.y;
        if (x == newX && y == newY) {
          return {letter: exit.letter, isLocked: exit.isLocked};
        }
      }
    }
    return false
  }
let exit = this.getExitAt(x, y);
        if (exit) {
          let cellColour = (exit.isLocked) ?  this.lockedExitColour : this.openExitColour;
          cells.push('<g>');
          cells.push('<rect x="' + x2 + '" y="' + y2 + '" width="' + this.cellSize + '" height="' + this.cellSize + '" stroke="none" fill="' + cellColour + '"/>');
          if (exit.letter) {
             cells.push('<text x="' + (x2 + this.cellSize / 5) + '" y="' + (y2 + this.cellSize * (3/4)) + '" style="font: ' + this.mapFont + '" fill="black">' + exit.letter + '</text>');
          }
          cells.push('</g>');
        }

image

ThePix commented 3 years ago

It looks like you are colouring a cell that has an exit going into it, green if unlocked, red if locked. I am concerned that will not work in some situations. For example, where there are two routes to a room, the player can reach either side of an exit; how does it know which side to colour red/green?

Maybe that is not an issue for you, but I would have to consider a more general solution. My feeling is that a better way would be a smaller box over the join, and colour that.

Kln95130 commented 3 years ago

I did not think of that. As I said in an earlier message, you must have a better vision of things.

My main goal while experimenting was: "when the player uses the map, they must know where their intended destination as quickly as possible". Hence why I sought to add a simple colour code and labels.

Using another criterium, like if the room was visited, might be better if your game has room with exits in different states of locking. Alternatively, you could change exit_list's x and y, so that the mapper could draw a line pointing in the direction, then the exit square in the next cell, and the line itself would be coloured.

ThePix commented 3 years ago

"Using another criterium, like if the room was visited, might be better if your game has room with exits in different states of locking."

Yes, doors locked from one side only will be a problem...

ThePix commented 3 years ago

Maps have now been fully implemented so I think this has now been done.