In-world hologram UI - In world inventory (Frontend) (Part 4)
1/9/2025
Kaloyan Geshev
In the final tutorial of this series, we will design the UI for the inventory menu, allowing the player to interact with the items.
You can find the rest Hologram UI series here.
Showcase Overview
This part will focus on creating the UI for the inventory.
We will achieve this by:
- Using Gameface data binding to generate and render the inventory items.
- Applying
CSS
transforms to create the screens from the image. - Utilizing the progress bar Gameface component to display weapon stats.
- Utilizing the interaction manager to allow the player to navigate through the items list using the keyboard.
Getting started - UI layout overview
To create the inventory UI, we will divide the screens into three parts:
We will create a main wrapper element for all UI parts using display flex in a row direction. Inside this wrapper, we will have two elements that together occupy 100% of the wrapper’s width.
The first element will display the weapon stats and preview screen.
The second element will use display flex in a column direction and contain two children:
- The first child will display the inventory items.
- The second child will display the currencies.
Setting the wrappers
The HTML
representation of the above layout is as follows:
1<div class='menu'>2 <div class="menu-left">3 <div class="submenu-top"></div>4 </div>5 <div class="menu-right">6 <div class="submenu-top"></div>7 <div class="submenu-bottom"></div>8 </div>9</div>
The main wrapper element is menu
. Inside it, we have menu-left
and menu-right
to split the screen into two parts as described. Inside them, we add submenu-top
and submenu-bottom
elements. Since the left menu only shows weapon stats, we omit submenu-bottom
. The right menu’s submenu-top
shows the inventory, and submenu-bottom
shows the currencies.
The wrappers will have the following styles:
1.menu {2 background-color: rgb(0, 0, 0, 0);3 color: white;4 width: 100vw;5 height: 100vh;6 position: absolute;7 display: flex;8 align-items: center;9 justify-content: center;10 perspective: 1000px;11 opacity: 1;12}13
14.menu-left,15.menu-right {16 height: 100%;17 padding: 1rem;18}19
27 collapsed lines
20.menu-left {21 margin-left: 1rem;22 flex: 1 1 30%;23 transform: rotateY(20deg) skewY(-3deg) translateY(-1rem);24 transform-origin: center left;25}26
27.menu-right {28 transform: translateX(-5rem);29 flex: 1 1 70%;30 perspective: 1000px;31}32
33.submenu-top {34 transform: scale(0.9) rotateY(-4deg);35 transform-origin: top right;36 height: 70%;37 position: relative;38}39
40.submenu-bottom {41 width: 97.5%;42 transform: scale(0.9) rotateY(-4deg) rotateX(10deg) skewY(0deg) skewX(-10deg) translateX(5%) translateY(-5%);43 transform-origin: center;44 height: 25%;45 position: relative;46}
As you can see, we also set the transformations for each screen. To achieve a 3D effect, we use the perspective
CSS property on the menu
wrapper. For the other elements, we use different transform properties to achieve the effect shown in the video or images.
To simplify the process, we used our Inspector to transform the wrappers, allowing temporary updates to the UI that are immediately visible in our standalone application called the Player
. Once satisfied with the result, you can copy the CSS from the Styles
tab and paste it into your CSS file.
Note: Since the Gameface view in the game created in the previous tutorial also has transformations, it is advisable to check the final result directly in the game. You can open the inspector while the game is running and use the same approach to temporarily edit the elements as with the standalone application.
Creating the holographic background for menus
To achieve the holographic effect for the menu background, follow these steps:
- First, create a border as a PNG image for the element. Then, use the
border-image
CSS property to apply a nine-slice scaling technique, ensuring the border’s edges maintain their ratio when the wrapper element is scaled. We will use the following image: - Next, add the noise animation created earlier in the series. To ensure it stays within the border image boundaries and acts as a background, use the
border-radius
CSS property. - Finally, translate and scale the wrapper slightly to create an offset behind the menu wrapper, producing a depth effect.
For each submenu, add this element that is positioned absolutely to the menu. Here is an example in our HTML page:
1<div class='menu'>2 <div class="menu-left">3 <div class="submenu-top">4 <div class="border-bg border-bg-weapon-preview">5 <div class="overlay-lines border-radius-lines"></div>6 </div>7 </div>8 </div>9 <div class="menu-right">10 <div class="submenu-top">11 <div class="border-bg border-bg-inventory">12 <div class="overlay-lines border-radius-lines"></div>13 </div>14 </div>15 <div class="submenu-bottom">16 <div class="border-bg border-bg-currencies">17 <div class="overlay-lines border-radius-lines"></div>18 </div>19 </div>20 </div>21</div>
Each background is tagged with a specific class to apply different transition animations. For example, the weapon preview background is tagged with the border-bg-weapon-preview
class.
The styles are defined as follows:
1.overlay,2.overlay-lines,3.overlay-radial {4 pointer-events: none;5 position: absolute;6 width: 100%;7 height: 100%;8 top: 0;9 left: 0;38 collapsed lines
10 right: 0;11 bottom: 0;12 z-index: 999;13}14
15.overlay-lines {16 background-image: linear-gradient(rgba(0, 200, 228, 0.1) 0%, rgba(0, 200, 228, 0.1) 30%, rgba(255, 255, 255, 0) 30%, rgba(255, 255, 255, 0) 100%);17 background-size: 100% 0.5rem;18 animation: overlay-anim 3s forwards linear infinite;19}20
21@keyframes overlay-anim {22 from {23 background-position: 0% 0%;24 }25
26 to {27 background-position: 0% -10%;28 }29}30
31.border-radius-lines {32 border-radius: 4rem;33}34
35.border-bg {36 position: absolute;37 width: 108%;38 height: 106%;39 border-image-source: url(./assets/border.png);40 border-image-slice: 60;41 border-image-width: 3.5rem;42 border-image-outset: 0;43 border-image-repeat: stretch;44 z-index: -1;45 transform: translateX(-2rem) scale(1);46 top: 1rem;47}
This will result in a similar effect:
Weapon preview and stats menu
To create the weapon preview menu, we will design a separate submenu wrapper element that displays its children in a column layout.
Next, we will create two reusable elements within the submenu wrapper: one for the menu item title and one for the menu item container. This will help us define the layout based on images and videos.
Weapon preview
We will use the following structure in our HTML file:
1<div class='menu'>2 <div class="menu-left">3 <div class="submenu-top">4 <div class="border-bg border-bg-weapon-preview">5 <div class="overlay-lines border-radius-lines"></div>6 </div>7 <div class="submenu-wrapper weapon-preview-wrapper">8 <div class="menu-item-title">Weapon preview</div>9 <div class="menu-selected-item-preview">10 <div11 class="selected-item-image"12 data-bind-style-background-image-url="{{activeState.selectedItem.image}}"13 ></div>14 </div>
We use the menu-item-title
to display the text “Weapon preview” and the menu-selected-item-preview
to show a preview image of the currently selected weapon. The data-bind-style-background-image-url
attribute ensures that the image updates when the selected weapon changes. We will later set up all data models using mock models.
To match the submenu with the previously created background, we will set a border radius and overflow to hidden on the submenu-wrapper
:
1.submenu-wrapper {2 border-radius: 4rem;3 overflow: hidden;4 height: 100%;5}
Progress bar initialization
To use the progress bar in our UI, run npm i coherent-gameface-progress-bar
inside the Content/uiresources/HologramUI/
folder and then import its styles and JavaScript into the page.
1<head>2 <link rel="stylesheet" href="node_modules/coherent-gameface-progress-bar/coherent-gameface-components-theme.css">3 <link rel="stylesheet" href="node_modules/coherent-gameface-progress-bar/style.css">4 ...5</head>6<body>7 ...8 <script src="./node_modules/coherent-gameface-progress-bar/dist/progress-bar.production.min.js"></script>9</body>
Weapon stats
For the weapon stats, we will add a menu-item-title
with the text “Stats” and use the menu-item-container
to display all the stats. Each stat will be wrapped in a menu-item-stat
element, with menu-item-stat-label
displaying the stat name and menu-item-stat-value
showing the stat value using the gameface progress bar or simple text via the data-bind-value
attribute.
Here is a snippet demonstrating how different stats are structured in the HTML page:
1<div class="submenu-wrapper weapon-preview-wrapper">2 ...3 <div class="menu-item-title">Stats</div>4 <div class="menu-item-container">5 <div class="menu-item-stats-wrapper">6 <div class="menu-item-stat">7 <div class="menu-item-stat-label">Name</div>8 <div9 class="menu-item-stat-value"10 data-bind-value="{{activeState.selectedItem.name}}"11 ></div>12 </div>13 ...14 <div class="menu-item-stat">15 <div class="menu-item-stat-label">Firepower</div>16 <div class="menu-item-stat-value">17 <gameface-progress-bar18 animation-duration="500"19 data-bind-progress="{{activeState.selectedItem.firepower}}"20 ></gameface-progress-bar>21 <span22 class="stat-text-value"23 data-bind-value="{{activeState.selectedItem.firepower}}+'%'"24 ></span>25 </div>26 </div>27 ...28 </div>29 </div>30</div>
We apply values from the active item in our model via data binding attributes, which we will set up later. To dynamically change the progress bar value, we will use the data-bind-progress
custom attribute, which we will define later as well.
Inventory menu
To build the inventory, we will reuse some elements from the weapon stats. We’ll create a new submenu-top
inside the menu-right
wrapper, adding the border-bg
and submenu-wrapper
elements.
For displaying all items, we’ll use a grid-like layout. Each item from our data model will be rendered using the data-bind-for
attribute.
1<div class="menu-right">2 <div class="submenu-top">3 <div class="border-bg border-bg-inventory">4 <div class="overlay-lines border-radius-lines"></div>5 </div>6 <div class="submenu-wrapper inventory-wrapper">7 <div class="menu-item-title">Inventory (6)</div>8 <div class="menu-item-container">9 <div10 class="player-item-wrapper"11 data-bind-for="item:{{player.items}}"12 >13 <div14 class="player-item"15 data-bind-mouseover="onItemFocused(event,this,{{item}})"16 data-bind-focus="onItemFocused(event,this,{{item}})"17 >18 <div19 data-bind-class-toggle="player-item-selected:{{item}}==={{activeState.selectedItem}}"20 class="player-item-background"21 ></div>22 <div23 data-bind-if="{{item}}==={{activeState.selectedItem}}"24 class="overlay"25 >26 <div class="overlay-lines"></div>27 <div class="overlay-radial"></div>28 </div>29 <div30 class="player-item-image"31 data-bind-style-background-image-url="{{item.image}}"32 ></div>33 </div>34 </div>35 </div>36 </div>37 </div>
Each player-item
has data-bind-mouseover
and data-bind-focus
events. These events mark the item as active when the player hovers over or clicks on it. The onItemFocused
function, defined later in our JavaScript file, handles these events.
The player-item-selected
class styles the active item by changing its background. This class is toggled using the data-bind-class-toggle
attribute.
When an item is active, an overlay is added to highlight it. The overlay is shown only when the item is active using the data-bind-if
attribute.
The player-item-image
element displays the item image, setting the background image using the data-bind-style-background-image-url
attribute.
Currencies menu
The currencies menu uses the same structure as the inventory menu. We’ll create a new submenu-bottom
inside the menu-right
wrapper, adding the border-bg
and submenu-wrapper
elements.
To dynamically show the active item’s price, we’ll use the data-bind-value
attribute.
1<div class="submenu-bottom">2 <div class="border-bg border-bg-currencies">3 <div class="overlay-lines border-radius-lines"></div>4 </div>5 <div class="submenu-wrapper currencies-wrapper">6 <div class="menu-item-title">Currencies</div>7 <div class="menu-item-container">8 <div class="menu-item-stat">9 <div class="menu-item-stat-label">10 <div class="icon icon-money"></div>11 </div>12 <div13 class="menu-item-stat-value currency"14 data-bind-value="{{activeState.selectedItem.gold}}"15 ></div>16 </div>17 <div class="menu-item-stat">18 <div class="menu-item-stat-label">19 <div class="icon icon-currency"></div>20 </div>21 <div class="menu-item-stat-value currency">155 230 500</div>22 </div>23 </div>24 </div>25</div>
Showing menu controls hints
To guide the player on navigating through the items, we’ll add hints at the bottom of the screen, positioned after the currencies wrapper.
1<div class="submenu-bottom">22 collapsed lines
2 <div class="border-bg border-bg-currencies">3 <div class="overlay-lines border-radius-lines"></div>4 </div>5 <div class="submenu-wrapper currencies-wrapper">6 <div class="menu-item-title">Currencies</div>7 <div class="menu-item-container">8 <div class="menu-item-stat">9 <div class="menu-item-stat-label">10 <div class="icon icon-money"></div>11 </div>12 <div13 class="menu-item-stat-value currency"14 data-bind-value="{{activeState.selectedItem.gold}}"15 ></div>16 </div>17 <div class="menu-item-stat">18 <div class="menu-item-stat-label">19 <div class="icon icon-currency"></div>20 </div>21 <div class="menu-item-stat-value currency">155 230 500</div>22 </div>23 </div>24 </div>25 <div class="controls">26 <div class="arrow">27 <div class="arrow-icon up"></div>28 <div>Move up</div>29 </div>30 <div class="arrow">31 <div class="arrow-icon down"></div>32 <div>Move down</div>33 </div>34 <div class="arrow">35 <div class="arrow-icon left"></div>36 <div>Move left</div>37 </div>38 <div class="arrow">39 <div class="arrow-icon right"></div>40 <div>Move right</div>41 </div>42 <div class="arrow">43 <div class="arrow-icon p">P</div>44 <div>Close menu</div>45 </div>46 </div>47</div>
The arrow icon is an SVG that will be rotated using the transform: rotate
CSS property based on the arrow direction.
1.arrow {2 width: 8rem;3 height: 5rem;4 display: flex;5 flex-direction: row;6 align-items: center;7}8
9.arrow-icon {28 collapsed lines
10 background-image: url(./assets/arrow.svg);11 background-size: 100% 100%;12 width: 1.5rem;13 height: 1.5rem;14 margin-right: 0.5rem;15}16
17.arrow-icon.up {18 transform: rotate(-90deg);19}20
21.arrow-icon.down {22 transform: rotate(90deg);23}24
25.arrow-icon.left {26 transform: rotate(-180deg);27}28
29.arrow-icon.p {30 background-image: none;31 display: flex;32 align-items: center;33 justify-content: center;34 font-weight: bold;35 font-size: 1.5rem;36 color: rgba(0, 201, 228, 1);37}
Setting up the data model
In this tutorial, we will mock our data directly in JavaScript for faster iteration. However, it is necessary to create all the models directly in the game and use the Gameface data binding to display them in the UI.
To set up the mock data model, first, we will import the cohtml.js
library and the player-inventory.js
file in our player-inventory.html
file.
1<body>2 ...3 <script src="cohtml.js"></script>4 <script src="./js/player-inventory.js"></script>5</body>
After that, we can proceed with the data model setup:
1engine.whenReady.then(() => {2 const NO_ACHIEVEMENT_TITLE = "No achievements unlocked";3 const FIRST_GENERATION_DESCRIPTION = "First generation";4
5 const TYPE_LABEL_ENUM = {6 Shuko: "Shuko",7 Ucolos: "Ucolos",8 Arachod: "Arachod"9 };10
11 const QUALITY_LABEL_ENUM = {12 Poor: "Poor",13 Common: "Common",14 Rare: "Rare",15 Unique: "Unique"16 };17
18 const GUNS_DATA = [19 {107 collapsed lines
20 name: "Pistol",21 description: FIRST_GENERATION_DESCRIPTION,22 gold: 1000123,23 image: "assets/weapon1.png",24 quality: QUALITY_LABEL_ENUM.Poor,25 type: TYPE_LABEL_ENUM.Ucolos,26 grade: 5,27 achievementTitle: NO_ACHIEVEMENT_TITLE,28 achievementValue: " ",29 dmgRating: 15,30 rof: 300,31 rounds: 24,32 firepower: "5",33 reload: "25",34 accuracy: "10",35 recoil: "8"36 },37 {38 name: "Rifle",39 description: FIRST_GENERATION_DESCRIPTION,40 gold: 2015017,41 image: "assets/weapon2.png",42 quality: QUALITY_LABEL_ENUM.Poor,43 type: TYPE_LABEL_ENUM.Shuko,44 grade: 25,45 achievementTitle: "Cold Marksman",46 achievementValue: "120% Critical Damage",47 dmgRating: 35,48 rof: 600,49 rounds: 90,50 firepower: "25",51 reload: "55",52 accuracy: "50",53 recoil: "60"54 },55 {56 name: "Blaster",57 description: FIRST_GENERATION_DESCRIPTION,58 gold: 199999,59 image: "assets/weapon3.png",60 quality: QUALITY_LABEL_ENUM.Poor,61 type: TYPE_LABEL_ENUM.Arachod,62 grade: 30,63 achievementTitle: NO_ACHIEVEMENT_TITLE,64 achievementValue: " ",65 dmgRating: 35,66 rof: 900,67 rounds: 60,68 firepower: "55",69 reload: "55",70 accuracy: "5",71 recoil: "60"72 },73 {74 name: "Knife",75 description: FIRST_GENERATION_DESCRIPTION,76 gold: 501010,77 image: "assets/weapon4.png",78 quality: QUALITY_LABEL_ENUM.Poor,79 type: TYPE_LABEL_ENUM.Shuko,80 grade: 15,81 achievementTitle: NO_ACHIEVEMENT_TITLE,82 achievementValue: " ",83 dmgRating: 25,84 rof: 0,85 rounds: 0,86 firepower: "0",87 reload: "0",88 accuracy: "0",89 recoil: "0"90 },91 {92 name: "RPG",93 description: FIRST_GENERATION_DESCRIPTION,94 gold: 3012343,95 image: "assets/weapon5.png",96 quality: QUALITY_LABEL_ENUM.Poor,97 type: TYPE_LABEL_ENUM.Ucolos,98 grade: 70,99 achievementTitle: NO_ACHIEVEMENT_TITLE,100 achievementValue: " ",101 dmgRating: 60,102 rof: 250,103 rounds: 5,104 firepower: "60",105 reload: "25",106 accuracy: "80",107 recoil: "30"108 },109 {110 name: "AWP",111 description: FIRST_GENERATION_DESCRIPTION,112 gold: 4300201,113 image: "assets/weapon6.png",114 quality: QUALITY_LABEL_ENUM.Poor,115 type: TYPE_LABEL_ENUM.Arachod,116 grade: 75,117 achievementTitle: NO_ACHIEVEMENT_TITLE,118 achievementValue: " ",119 dmgRating: 60,120 rof: 250,121 rounds: 35,122 firepower: "60",123 reload: "50",124 accuracy: "80",125 recoil: "80"126 },127 ];128 engine.createJSModel('player', {129 items: GUNS_DATA130 });131
132 engine.synchronizeModels();133});
Active item selection
To retrieve the active item, we will use an observable model that will hold the currently selected item. We will create a new model called activeState
and set the selectedItem
to the first item in the player.items
array.
1engine.createJSModel('player', {2 items: GUNS_DATA3});4
5engine.createObservableModel("activeState");6activeState.selectedItem = player.items[0];7
8engine.synchronizeModels();
Focusing the items
To focus the items, we will create a function called onItemFocused
that will set the activeState.selectedItem
to the item that is currently focused.
1function onItemFocused(event, element, item) {2 activeState.selectedItem = item;3 engine.synchronizeModels();4}
Updating the progress bar value
To dynamically update the progress bar value, we will create a new custom data binding attribute called data-bind-progress
.
1engine.whenReady.then(() => {2 class Progress {3 init(element, value) {4 element.targetValue = value;5 }6
7 update(element, value) {8 element.targetValue = value;9 }10 }11
12 engine.registerDataBinding('progress', Progress);13});
We will modify the progress bar value using the targetValue property of the element whenever the model’s value changes.
Inventory items navigation
To enable the player to navigate through the items, install the interaction manager library by running npm i coherent-gameface-interaction-manager
inside the Content/uiresources/HologramUI/
folder and then import it:
1<script src="./node_modules/coherent-gameface-interaction-manager/dist/interaction-manager.min.js"></script>
Once the library is imported, initialize spatial navigation and set the focusable elements to the items in the inventory.
1 interactionManager.spatialNavigation.init(['.player-item']);2 interactionManager.spatialNavigation.focusFirst();
We also focus the first item in the inventory so the player can start navigating through the items when the menu opens.
Enabling entry animations for the menus when the player opens the inventory
When the player presses the P
button in the game, it will trigger a JS event from the game to the UI as set in the previous tutorial. To handle this event and start the opening animations of our menu, we will subscribe to the openMenu
engine event in the player-inventory.js
file.
1engine.on('openMenu', () => {2 menu.classList.toggle('hide-screen', false);3});
When the event is triggered, we remove the hide-screen
class from the menu
element so the menu becomes visible and animations start.
Closing the menu
As mentioned in the previous tutorial, closing the menu will be handled from the UI side. We will add a keypress
event responsible for this operation. When the player presses the P
button, we will close the menu by adding the hide-screen
class and enabling the closing animations. Also, we will trigger an event to the engine indicating that the menu is closed.
1document.addEventListener('keypress', (event) => {2 if (event.key === 'p') {3 engine.trigger('closeMenu');4 menu.classList.toggle('hide-screen', true);5 }6});
Animations for the menu
For the closing and opening animations of our menu, we will use transitions. We will change the transform
property for each screen so they are opened or closed based on whether the hide-screen
class is set on the menu
element.
1.border-bg-weapon-preview {2 transition: transform 500ms;3}4
5.border-bg-inventory {6 transition: transform 300ms;7}8
9.border-bg-currencies {10 transition: transform 400ms;11}12
13.hide-screen .border-bg-weapon-preview {14 transform: translateX(-2rem) scale(0);15 transition: transform 500ms 300ms;16}17
18.hide-screen .border-bg-inventory {19 transform: translateX(-2rem) scale(0);56 collapsed lines
20 transition: transform 300ms 500ms;21}22
23.hide-screen .border-bg-currencies {24 transform: translateX(-2rem)scale(0);25 transition: transform 400ms 400ms;26}27
28.weapon-preview-wrapper {29 transform-origin: top;30 transform: scale(1);31 transition: transform 500ms 500ms;32}33
34.hide-screen .weapon-preview-wrapper {35 transform: scaleY(0);36 transition: transform 500ms;37}38
39.inventory-wrapper {40 transform-origin: right;41 transform: scale(1);42 transition: transform 500ms 500ms;43}44
45.hide-screen .inventory-wrapper {46 transform: scaleX(0);47 transition: transform 300ms;48}49
50.currencies-wrapper {51 transform-origin: bottom;52 transform: scale(1);53 transition: transform 500ms 500ms;54}55
56.hide-screen .currencies-wrapper {57 transform: scaleY(0);58 transition: transform 400ms;59}60
61.controls {62 display: flex;63 flex-direction: row;64 align-items: center;65 width: 100%;66 margin-top: 1.5rem;67 transform-origin: bottom;68 transform: scale(1);69 transition: transform 500ms 500ms;70}71
72.hide-screen .controls {73 transform: scaleY(0);74 transition: transform 400ms;75}
As you can see, we added a small delay between the animations so they do not all start at the same time. This will give a better visual effect when the menu is opened or closed.
Full source code
1<!DOCTYPE html>2<html lang="en">3
4<head>5 <link6 rel="stylesheet"7 href="node_modules/coherent-gameface-progress-bar/coherent-gameface-components-theme.css"8 >9 <link188 collapsed lines
10 rel="stylesheet"11 href="node_modules/coherent-gameface-progress-bar/style.css"12 >13 <link rel="stylesheet" href="player-inventory-styles.css">14</head>15
16<body>17 <div class='menu'>18 <div class="menu-left">19 <div class="submenu-top">20 <div class="border-bg border-bg-weapon-preview">21 <div class="overlay-lines border-radius-lines"></div>22 </div>23 <div class="submenu-wrapper weapon-preview-wrapper">24 <div class="menu-item-title">Weapon preview</div>25 <div class="menu-selected-item-preview">26 <div27 class="selected-item-image"28 data-bind-style-background-image-url="{{activeState.selectedItem.image}}"29 ></div>30 </div>31 <div class="menu-item-title">Stats</div>32 <div class="menu-item-container">33 <div class="menu-item-stats-wrapper">34 <div class="menu-item-stat">35 <div class="menu-item-stat-label">Name</div>36 <div37 class="menu-item-stat-value"38 data-bind-value="{{activeState.selectedItem.name}}"39 ></div>40 </div>41 <div class="menu-item-stat">42 <div class="menu-item-stat-label">Description</div>43 <div44 class="menu-item-stat-value"45 data-bind-value="{{activeState.selectedItem.description}}"46 ></div>47 </div>48 <div class="menu-item-stat">49 <div class="menu-item-stat-label">Firepower</div>50 <div class="menu-item-stat-value">51 <gameface-progress-bar52 animation-duration="500"53 data-bind-progress="{{activeState.selectedItem.firepower}}"54 ></gameface-progress-bar>55 <span56 class="stat-text-value"57 data-bind-value="{{activeState.selectedItem.firepower}}+'%'"58 ></span>59 </div>60 </div>61 <div class="menu-item-stat">62 <div class="menu-item-stat-label">Reload</div>63 <div class="menu-item-stat-value">64 <gameface-progress-bar65 animation-duration="500"66 data-bind-progress="{{activeState.selectedItem.reload}}"67 ></gameface-progress-bar>68 <span69 class="stat-text-value"70 data-bind-value="{{activeState.selectedItem.reload}}+'%'"71 ></span>72 </div>73 </div>74 <div class="menu-item-stat">75 <div class="menu-item-stat-label">Accuracy</div>76 <div class="menu-item-stat-value">77 <gameface-progress-bar78 animation-duration="500"79 data-bind-progress="{{activeState.selectedItem.accuracy}}"80 ></gameface-progress-bar>81 <span82 class="stat-text-value"83 data-bind-value="{{activeState.selectedItem.accuracy}}+'%'"84 ></span>85 </div>86 </div>87 <div class="menu-item-stat">88 <div class="menu-item-stat-label">Recoil</div>89 <div class="menu-item-stat-value">90 <gameface-progress-bar91 animation-duration="500"92 data-bind-progress="{{activeState.selectedItem.recoil}}"93 ></gameface-progress-bar>94 <span95 class="stat-text-value"96 data-bind-value="{{activeState.selectedItem.recoil}}+'%'"97 ></span>98 </div>99 </div>100 </div>101 </div>102 </div>103 </div>104 </div>105 <div class="menu-right">106 <div class="submenu-top">107 <div class="border-bg border-bg-inventory">108 <div class="overlay-lines border-radius-lines"></div>109 </div>110 <div class="submenu-wrapper inventory-wrapper">111 <div class="menu-item-title">Inventory (6)</div>112 <div class="menu-item-container">113 <div114 class="player-item-wrapper"115 data-bind-for="item:{{player.items}}"116 >117 <div118 class="player-item"119 data-bind-mouseover="onItemFocused(event,this,{{item}})"120 data-bind-focus="onItemFocused(event,this,{{item}})"121 >122 <div123 data-bind-class-toggle="player-item-selected:{{item}}==={{activeState.selectedItem}}"124 class="player-item-background"125 ></div>126 <div127 data-bind-if="{{item}}==={{activeState.selectedItem}}"128 class="overlay"129 >130 <div class="overlay-lines"></div>131 <div class="overlay-radial"></div>132 </div>133 <div134 class="player-item-image"135 data-bind-style-background-image-url="{{item.image}}"136 ></div>137 </div>138 </div>139 </div>140 </div>141 </div>142 <div class="submenu-bottom">143 <div class="border-bg border-bg-currencies">144 <div class="overlay-lines border-radius-lines"></div>145 </div>146 <div class="submenu-wrapper currencies-wrapper">147 <div class="menu-item-title">Currencies</div>148 <div class="menu-item-container">149 <div class="menu-item-stat">150 <div class="menu-item-stat-label">151 <div class="icon icon-money"></div>152 </div>153 <div154 class="menu-item-stat-value currency"155 data-bind-value="{{activeState.selectedItem.gold}}"156 ></div>157 </div>158 <div class="menu-item-stat">159 <div class="menu-item-stat-label">160 <div class="icon icon-currency"></div>161 </div>162 <div class="menu-item-stat-value currency">155 230 500</div>163 </div>164 </div>165 </div>166 <div class="controls">167 <div class="arrow">168 <div class="arrow-icon up"></div>169 <div>Move up</div>170 </div>171 <div class="arrow">172 <div class="arrow-icon down"></div>173 <div>Move down</div>174 </div>175 <div class="arrow">176 <div class="arrow-icon left"></div>177 <div>Move left</div>178 </div>179 <div class="arrow">180 <div class="arrow-icon right"></div>181 <div>Move right</div>182 </div>183 <div class="arrow">184 <div class="arrow-icon p">P</div>185 <div>Close menu</div>186 </div>187 </div>188 </div>189 </div>190 </div>191 <script src="../javascript/cohtml.js"></script>192 <script src="./node_modules/coherent-gameface-progress-bar/dist/progress-bar.production.min.js"></script>193 <script src="./node_modules/coherent-gameface-interaction-manager/dist/interaction-manager.min.js"></script>194 <script src="./js/player-inventory.js"></script>195</body>196
197</html>
1html {2 font-size: 1.5vh;3}4
5body {6 width: 100vw;7 height: 100vh;8 margin: 0;9}427 collapsed lines
10
11.overlay,12.overlay-lines,13.overlay-radial {14 pointer-events: none;15 position: absolute;16 width: 100%;17 height: 100%;18 top: 0;19 left: 0;20 right: 0;21 bottom: 0;22 z-index: 999;23}24
25.overlay {26 mask-image: radial-gradient(circle, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.5327380952380952) 65%, rgba(255, 255, 255, 0) 100%);27 mask-size: 90% 90%;28 mask-position: center;29}30
31.overlay-lines {32 background-image: linear-gradient(rgba(0, 200, 228, 0.1) 0%, rgba(0, 200, 228, 0.1) 30%, rgba(255, 255, 255, 0) 30%, rgba(255, 255, 255, 0) 100%);33 background-size: 100% 0.5rem;34 animation: overlay-anim 3s forwards linear infinite;35}36
37@keyframes overlay-anim {38 from {39 background-position: 0% 0%;40 }41
42 to {43 background-position: 0% -10%;44 }45}46
47.overlay-radial {48 background-image: radial-gradient(circle, rgba(0, 200, 228, 0.2) 0%, rgba(255, 255, 255, 0) 90%, rgba(255, 255, 255, 0) 100%);49 background-size: 200%;50 background-position: center;51 background-repeat: no-repeat;52 opacity: 0;53 animation: overlay-radial-anim 3s forwards linear infinite;54 z-index: 998;55}56
57@keyframes overlay-radial-anim {58 0% {59 opacity: 0;60 }61
62 1% {63 opacity: 1;64 }65
66 5% {67 opacity: 0;68 }69
70 19% {71 opacity: 0;72 }73
74 20% {75 opacity: 1;76 }77
78 25% {79 opacity: 0;80 }81
82 59% {83 opacity: 0;84 }85
86 60% {87 opacity: 1;88 }89
90 65% {91 opacity: 0;92 }93
94 100% {95 opacity: 0;96 }97}98
99.menu {100 background-color: rgb(0, 0, 0, 0);101 color: white;102 width: 100vw;103 height: 100vh;104 position: absolute;105 display: flex;106 align-items: center;107 justify-content: center;108 display: flex;109 perspective: 1000px;110 opacity: 1;111}112
113.menu-left,114.menu-right {115 height: 100%;116 padding: 1rem;117}118
119.menu-left {120 margin-left: 1rem;121 flex: 1 1 30%;122 transform: rotateY(20deg) skewY(-3deg) translateY(-1rem);123 transform-origin: center left;124}125
126.menu-right {127 transform: translateX(-5rem);128 flex: 1 1 70%;129 perspective: 1000px;130}131
132.submenu-top {133 transform: scale(0.9) rotateY(-4deg);134 transform-origin: top right;135 height: 70%;136 position: relative;137}138
139.submenu-bottom {140 width: 97.5%;141 transform: scale(0.9) rotateY(-4deg) rotateX(10deg) skewY(0deg) skewX(-10deg) translateX(5%) translateY(-5%);142 transform-origin: center;143 height: 25%;144 position: relative;145}146
147.menu-item-title {148 background-color: rgba(0, 121, 137, 0.9);149 display: flex;150 font-size: 2rem;151 padding: 1rem 3rem 1rem 3rem;152}153
154.menu-item-container {155 background-color: rgba(0, 39, 44, 0.5);156 display: flex;157 flex-wrap: wrap;158 align-content: flex-start;159 flex: 1 0 auto;160 padding: 1rem 3rem 1rem 3rem;161
162}163
164.menu-selected-item-preview {165 background-color: rgba(0, 39, 44, 0.9);166 display: flex;167 align-content: center;168 justify-content: center;169 width: 100%;170 height: 30%;171 padding: 1rem 3rem 1rem 3rem;172}173
174.player-item-wrapper {175 z-index: 1001;176}177
178.player-item {179 margin: 1rem;180 position: relative;181 width: 14rem;182 height: 14rem;183 display: flex;184 justify-content: center;185 align-items: center;186 z-index: 2001;187}188
189.player-item-background {190 pointer-events: none;191 width: 100%;192 height: 100%;193 position: absolute;194 background-color: rgb(0, 59, 67);195 border: 0.2rem solid rgba(255, 255, 255, 0.133);196 transition: background-color 0.3s, border 0.3s;197 z-index: 1;198 border-radius: 1.5rem;199
200}201
202.player-item-selected {203 background-color: rgba(0, 83, 94, 0.9);204 border: 0.2rem solid rgba(255, 255, 255, 0.8);205}206
207.player-item-image {208 pointer-events: none;209 width: 60%;210 height: 60%;211 background-size: contain;212 background-repeat: no-repeat;213 background-position: center;214 z-index: 1;215}216
217.menu-bottom {218 transform: rotateX(-4deg);219}220
221.h-30 {222 height: 30%;223}224
225.h-60 {226 height: 60%;227}228
229.h-70 {230 height: 70%;231}232
233
234.selected-item-image {235 width: 90%;236 height: 90%;237 background-size: contain;238 background-repeat: no-repeat;239 background-position: center;240}241
242.menu-item-stats-wrapper {243 display: flex;244 flex-direction: column;245 width: 100%;246 height: 100%;247}248
249.menu-item-stat {250 display: flex;251 flex-direction: row;252 align-items: center;253 width: 100%;254 padding: 1rem;255 font-size: 1.3rem;256}257
258.menu-item-stat-label {259 flex-basis: 8rem;260}261
262.menu-item-stat-value {263 height: 1.2rem;264 flex: 1 0 0;265 display: flex;266 flex-direction: row;267 align-items: center;268}269
270gameface-progress-bar {271 flex: 1 0 0;272}273
274.stat-text-value {275 width: 5%;276 margin-left: 1rem;277 display: flex;278 justify-content: center;279}280
281.icon {282 background-repeat: no-repeat;283 background-position: left center;284 background-size: contain;285 height: 3rem;286}287
288.icon-currency {289 background-image: url(./assets/iconCurrency.png);290}291
292.icon-money {293 background-image: url(./assets/money.png);294}295
296.currency {297 font-size: 1.6rem;298}299
300.border-bg {301 position: absolute;302 width: 108%;303 height: 106%;304 border-image-source: url(./assets/border.png);305 border-image-slice: 60;306 border-image-width: 3.5rem;307 border-image-outset: 0;308 border-image-repeat: stretch;309 z-index: -1;310 transform: translateX(-2rem) scale(1);311 top: 1rem;312}313
314.border-bg-weapon-preview {315 transition: transform 500ms;316}317
318.border-bg-inventory {319 transition: transform 300ms;320}321
322.border-bg-currencies {323 transition: transform 400ms;324}325
326.hide-screen .border-bg-weapon-preview {327 transform: translateX(-2rem) scale(0);328 transition: transform 500ms 300ms;329}330
331.hide-screen .border-bg-inventory {332 transform: translateX(-2rem) scale(0);333 transition: transform 300ms 500ms;334}335
336.hide-screen .border-bg-currencies {337 transform: translateX(-2rem)scale(0);338 transition: transform 400ms 400ms;339}340
341.border-radius-lines {342 border-radius: 4rem;343}344
345.submenu-wrapper {346 border-radius: 4rem;347 overflow: hidden;348 height: 100%;349}350
351.weapon-preview-wrapper {352 transform-origin: top;353 transform: scale(1);354 transition: transform 500ms 500ms;355}356
357.hide-screen .weapon-preview-wrapper {358 transform: scaleY(0);359 transition: transform 500ms;360}361
362.inventory-wrapper {363 transform-origin: right;364 transform: scale(1);365 transition: transform 500ms 500ms;366}367
368.hide-screen .inventory-wrapper {369 transform: scaleX(0);370 transition: transform 300ms;371}372
373.currencies-wrapper {374 transform-origin: bottom;375 transform: scale(1);376 transition: transform 500ms 500ms;377}378
379.hide-screen .currencies-wrapper {380 transform: scaleY(0);381 transition: transform 400ms;382}383
384.controls {385 display: flex;386 flex-direction: row;387 align-items: center;388 width: 100%;389 margin-top: 1.5rem;390 transform-origin: bottom;391 transform: scale(1);392 transition: transform 500ms 500ms;393}394
395.hide-screen .controls {396 transform: scaleY(0);397 transition: transform 400ms;398}399
400.arrow {401 width: 8rem;402 height: 5rem;403 display: flex;404 flex-direction: row;405 align-items: center;406}407
408.arrow-icon {409 background-image: url(./assets/arrow.svg);410 background-size: 100% 100%;411 width: 1.5rem;412 height: 1.5rem;413 margin-right: 0.5rem;414}415
416.arrow-icon.up {417 transform: rotate(-90deg);418}419
420.arrow-icon.down {421 transform: rotate(90deg);422}423
424.arrow-icon.left {425 transform: rotate(-180deg);426}427
428.arrow-icon.p {429 background-image: none;430 display: flex;431 align-items: center;432 justify-content: center;433 font-weight: bold;434 font-size: 1.5rem;435 color: rgba(0, 201, 228, 1);436}
1const menu = document.querySelector('.menu');2
3engine.whenReady.then(() => {4 class Progress {5 init(element, value) {6 element.targetValue = value;7 }8
9 update(element, value) {10 element.targetValue = value;11 }12 }13 engine.registerBindingAttribute('progress', Progress);14 engine.on('openMenu', () => {15 menu.classList.toggle('hide-screen', false);16 });17
18 document.addEventListener('keypress', (event) => {19 if (event.key === 'p') {148 collapsed lines
20 engine.trigger('closeMenu');21 menu.classList.toggle('hide-screen', true);22 }23 });24
25 const NO_ACHIEVEMENT_TITLE = "No achievements unlocked";26 const FIRST_GENERATION_DESCRIPTION = "First generation";27
28 const TYPE_LABEL_ENUM = {29 Shuko: "Shuko",30 Ucolos: "Ucolos",31 Arachod: "Arachod"32 };33
34 const QUALITY_LABEL_ENUM = {35 Poor: "Poor",36 Common: "Common",37 Rare: "Rare",38 Unique: "Unique"39 };40
41 const GUNS_DATA = [42 {43 name: "Pistol",44 description: FIRST_GENERATION_DESCRIPTION,45 gold: 1000123,46 image: "assets/weapon1.png",47 quality: QUALITY_LABEL_ENUM.Poor,48 type: TYPE_LABEL_ENUM.Ucolos,49 grade: 5,50 achievementTitle: NO_ACHIEVEMENT_TITLE,51 achievementValue: " ",52 dmgRating: 15,53 rof: 300,54 rounds: 24,55 firepower: "5",56 reload: "25",57 accuracy: "10",58 recoil: "8"59 },60 {61 name: "Rifle",62 description: FIRST_GENERATION_DESCRIPTION,63 gold: 2015017,64 image: "assets/weapon2.png",65 quality: QUALITY_LABEL_ENUM.Poor,66 type: TYPE_LABEL_ENUM.Shuko,67 grade: 25,68 achievementTitle: "Cold Marksman",69 achievementValue: "120% Critical Damage",70 dmgRating: 35,71 rof: 600,72 rounds: 90,73 firepower: "25",74 reload: "55",75 accuracy: "50",76 recoil: "60"77 },78 {79 name: "Blaster",80 description: FIRST_GENERATION_DESCRIPTION,81 gold: 199999,82 image: "assets/weapon3.png",83 quality: QUALITY_LABEL_ENUM.Poor,84 type: TYPE_LABEL_ENUM.Arachod,85 grade: 30,86 achievementTitle: NO_ACHIEVEMENT_TITLE,87 achievementValue: " ",88 dmgRating: 35,89 rof: 900,90 rounds: 60,91 firepower: "55",92 reload: "55",93 accuracy: "5",94 recoil: "60"95 },96 {97 name: "Knife",98 description: FIRST_GENERATION_DESCRIPTION,99 gold: 501010,100 image: "assets/weapon4.png",101 quality: QUALITY_LABEL_ENUM.Poor,102 type: TYPE_LABEL_ENUM.Shuko,103 grade: 15,104 achievementTitle: NO_ACHIEVEMENT_TITLE,105 achievementValue: " ",106 dmgRating: 25,107 rof: 0,108 rounds: 0,109 firepower: "0",110 reload: "0",111 accuracy: "0",112 recoil: "0"113 },114 {115 name: "RPG",116 description: FIRST_GENERATION_DESCRIPTION,117 gold: 3012343,118 image: "assets/weapon5.png",119 quality: QUALITY_LABEL_ENUM.Poor,120 type: TYPE_LABEL_ENUM.Ucolos,121 grade: 70,122 achievementTitle: NO_ACHIEVEMENT_TITLE,123 achievementValue: " ",124 dmgRating: 60,125 rof: 250,126 rounds: 5,127 firepower: "60",128 reload: "25",129 accuracy: "80",130 recoil: "30"131 },132 {133 name: "AWP",134 description: FIRST_GENERATION_DESCRIPTION,135 gold: 4300201,136 image: "assets/weapon6.png",137 quality: QUALITY_LABEL_ENUM.Poor,138 type: TYPE_LABEL_ENUM.Arachod,139 grade: 75,140 achievementTitle: NO_ACHIEVEMENT_TITLE,141 achievementValue: " ",142 dmgRating: 60,143 rof: 250,144 rounds: 35,145 firepower: "60",146 reload: "50",147 accuracy: "80",148 recoil: "80"149 },150 ];151 engine.createJSModel('player', {152 items: GUNS_DATA153 });154
155 engine.createObservableModel("activeState");156 activeState.selectedItem = player.items[0];157
158 engine.synchronizeModels();159
160 interactionManager.spatialNavigation.init(['.player-item']);161 interactionManager.spatialNavigation.focusFirst();162});163
164function onItemFocused(event, element, item) {165 activeState.selectedItem = item;166 engine.synchronizeModels();167}