# 05-keypad

Start with 05-keypad-playground

Resources

# Displaying UI

Inside of game.ts create a new UICanvas called gameCanvas:

const gameCanvas = new UICanavs();

new BaseScene();
CreateRoom5(gameCanvas);

Switch over to room5.ts and add the canvas as a parameter and add in the door:

export function CreateRoom5(gameCanvas: UICanvas): void {
  const door = new Door(
  	resources.models.door5,
  	{ position: new Vector3(19.5141, 5.54709, 25.676) },
  	resources.sounds.doorSqueak
  );
}

Create a new GameObject inside of the gameObject folder called model.ts:

export class Model extends Entity {
  constructor(model: GLTFShape, transform: TranformConstructorArgs) {
  	super();
  	engine.addEntity(this);
  
  	this.addComponent(new Transform(transform));
  	this.addComponent(model);
  }
}

Now in room5.ts add a new Model for the picture frame:

const painting = new Model(resources.models.pictureFrame,
{ position: new Vector3(22.2283, 7.60325, 20.9326) });

In the src folder create a new folder called ui and add a file called imageHint.ts:

import resources from "../resources";

export class ImageHint {
  // Expose the container for changing visibility
  public container: UIContainerRect;

  constructor(gameCanvas: UICanvas, texture: Texture) {
    this.container = new UIContainerRect(gameCanvas);
    this.container.width = "100%";
    this.container.height = "100%";

    // Add the primary image
    const hintImage = new UIImage(this.container, texture);
    hintImage.sourceWidth = 512;
    hintImage.sourceHeight = 512;
    hintImage.width = 512;
    hintImage.height = 512;

    // And a close button to the top right
    const close = new UIImage(
      this.container,
      resources.textures.closeHintButton
    );
    close.sourceWidth = 92;
    close.sourceHeight = 92;
    close.width = 46;
    close.height = 46;
    close.positionX = 256;
    close.positionY = 256;

    // UI has a different way of registering OnClick support
    close.onClick = new OnClick((): void => {
      this.container.visible = false;
    });
  }
}

Go back to room5.ts and add a new ImageHint for the picture frame and add an OnClick to enable the HintImage:

const painting = new Model(resources.models.pictureFrame,
{ position: new Vector3(22.2283, 7.60325, 20.9326) });

const paintingHint = new ImageHint(gameCanvase, resources.textures.fernHint);
paintingHint.container.visible = false;

painting.addComponent(new OnClick((): void =>{
  paintingHint.container.visible = true;
}));

Now add a rotateableEntity to add a carpet that will hide the second hint:

// And a carpet which covers a postit note
const carpet = new RotatableEntity(
  resources.models.carpet,
  {
    position: new Vector3(20.7079, 5.50579, 24.6273),
    rotation: Quaternion.Identity
  },
  undefined,
  Quaternion.Euler(0, -10, 0)
);
carpet.addComponent(
  new OnClick((): void => {
    carpet.getComponent(utils.ToggleComponent).toggle();
  })
);

Then add a second hint, hidden under the carpet with an OnClick similar to the picture frame:

// The postit contains the second hint
const postit = new Model(resources.models.postit, {
  position: new Vector3(21.571, 5.50857, 25.9534)
});

const postitHint = new ImageHint(gameCanvas, resources.textures.postitHint);
postitHint.container.visible = false;
postit.addComponent(
  new OnClick((): void => {
    postitHint.container.visible = true;
  })
);

# Creating a Number Pad

Inside of the gameObject folder add a new file called numPadLock.ts:

import resources from "../resources";

export class NumPadLock extends Entity {
  constructor(model: GLTFShape) {
    super();
    engine.addEntity(this);

    this.addComponent(model);
  }

  public playButtonPressed(): void {
    const clip = this.addComponentOrReplace(
      new AudioSource(resources.sounds.button)
    );
    clip.playOnce();
  }

  public playAccessGranted(): void {
    const clip = this.addComponentOrReplace(
      new AudioSource(resources.sounds.accessGranted)
    );
    clip.playOnce();
  }

  public playAccessDenied(): void {
    const clip = this.addComponentOrReplace(
      new AudioSource(resources.sounds.accessDenied)
    );
    clip.playOnce();
  }
}

Inside of the ui folder create a new file called keypad.ts:

import resources from "../resources";

// Constants for positioning
const panelPosition = new Vector2(12, -24);
const buttonSize = new Vector2(55, 55);
const buttonSpace = new Vector2(5, 5);

export class Keypad {
  // Expose the container for changing visibility
  public container: UIContainerRect;

  private panelInputs: UIText[];

  /**
   * Called when a value key is pressed.
   */
  public onInput: (value: number) => void;

  /**
   * Called when the reset button is pressed.
   */
  public onReset: () => void;

  /**
   * Called when the submit button is pressed.
   */
  public onSubmit: () => void;

  constructor(parent: UIShape) {
    this.container = new UIContainerRect(parent);
    this.container.positionX = -50;
    this.container.positionY = 50;
    this.container.width = "100%";
    this.container.height = "100%";

    // Display an image in the background for the keypad UI
    const panelBackground = new UIImage(
      this.container,
      resources.textures.panelBackground
    );
    panelBackground.sourceWidth = 918;
    panelBackground.sourceHeight = 1300;
    panelBackground.width = 310;
    panelBackground.height = 420;
    panelBackground.positionX = 70;
    panelBackground.positionY = -55;

    // Add a close button near the top right
    const closeImage = new UIImage(
      this.container,
      resources.textures.closeButton
    );
    closeImage.sourceWidth = 92;
    closeImage.sourceHeight = 92;
    closeImage.width = 32;
    closeImage.height = 32;
    closeImage.positionX = 194;
    closeImage.positionY = 108;

    // When close is clicked, hide the UI
    closeImage.onClick = new OnClick((): void => {
      this.container.visible = false;
    });

    // 3 boxes to show the entered code or current message
    this.panelInputs = [];
    for (let i = 0; i < 3; i++) {
      const inputImage = new UIImage(
        this.container,
        resources.textures.inputBox
      );
      const inputSlot = new UIText(this.container);
      inputImage.sourceWidth = 173;
      inputImage.sourceHeight = 173;
      inputImage.width = inputSlot.width = buttonSize.x;
      inputImage.height = inputSlot.height = buttonSize.y;
      inputImage.positionX = inputSlot.positionX =
        i * (buttonSpace.x + buttonSize.x) + 5;
      inputImage.positionY = inputSlot.positionY = 45;
      inputSlot.fontAutoSize = true;
      inputSlot.hTextAlign = "center";
      this.panelInputs.push(inputSlot);
    }

    // User input buttons
    for (let col = 0; col < 3; col++) {
      for (let row = 0; row < 4; row++) {
        // The value this button represents
        let value: number;
        if (col == 1 && row == 3) {
          // The 0 button is a special case
          value = 0;
        } else {
          value = row * 3 + col + 1;
        }

        // Create the button and its event
        let buttonImage: UIImage = null;
        if (col == 0 && row == 3) {
          // The clear button in the bottom left
          buttonImage = new UIImage(
            this.container,
            resources.textures.clearButton
          );

          // Call onReset when clicked
          buttonImage.onClick = new OnClick((): void => {
            this.onReset();
          });
        } else if (col == 2 && row == 3) {
          // The enter button in the bottom right
          buttonImage = new UIImage(
            this.container,
            resources.textures.enterButton
          );

          // Call onSubmit when clicked
          buttonImage.onClick = new OnClick((): void => {
            this.onSubmit();
          });
        } else {
          // A number value button
          buttonImage = new UIImage(
            this.container,
            resources.textures.numberButton
          );

          const numberText = new UIText(buttonImage);
          numberText.isPointerBlocker = false;
          numberText.positionX = -23;
          numberText.fontAutoSize = true;
          numberText.hTextAlign = "center";
          numberText.value = value.toString();

          // Call onInput when clicked
          buttonImage.onClick = new OnClick((): void => {
            this.onInput(value);
          });
        }

        // Configure button image
        buttonImage.sourceWidth = 171;
        buttonImage.sourceHeight = 171;
        buttonImage.width = buttonSize.x;
        buttonImage.height = buttonSize.y;
        buttonImage.positionX =
          panelPosition.x + col * (buttonSpace.x + buttonSize.x);
        buttonImage.positionY =
          panelPosition.y - row * (buttonSpace.y + buttonSize.y);
      }
    }
  }

  // Display a message above the keypad, up to 3 characters
  public display(message: string, color: Color4 = Color4.White()): void {
    for (let i = 0; i < this.panelInputs.length; i++) {
      const character = message.length > i ? message[i] : "";
      this.panelInputs[i].value = character;
      this.panelInputs[i].color = color;
    }
  }
}

Going back to room5.ts add the keypad ui and the numpad object:

const keypad = new Keypad(gameCanvas);
keypad.container.visible = false;

const numPadLock = new NumPadLock(resources.models.numpad1);
numPadLock.addComponent(new OnClick((): void =>{
	keypad.container.visible = true;
}));

Now create a variable to store the Player's input and add an event to record it:

// Wire up the keypad logic
let currentInput = "";
keypad.onInput = (value: number): void => {
  currentInput += value;
  keypad.display(currentInput);
  numPadLock.playButtonPressed();
};

Underneath that an events for resetting the input and submitting the input:

keypad.onReset = (): void => {
    currentInput = "";
    keypad.display(currentInput);
    numPadLock.playButtonPressed();
};
keypad.onSubmit = (): void => {
  if (currentInput == "155") {
    // Correct!
    keypad.display("OK!", Color4.Green());
    numPadLock.playAccessGranted();
    numPadLock.addComponentOrReplace(
      new utils.ExpireIn(2000, (): void => {
        keypad.container.visible = false;
        door.openDoor();
      })
    );
  } else {
    // The password is incorrect
    keypad.display("Err", Color4.Red());
    numPadLock.playAccessDenied();
    currentInput = "";
  }
};