|
| 1 | +// Copyright 2020 Las Venturas Playground. All rights reserved. |
| 2 | +// Use of this source code is governed by the MIT license, a copy of which can |
| 3 | +// be found in the LICENSE file. |
| 4 | + |
| 5 | +import { Color } from 'base/color.js'; |
| 6 | +import { Rectangle } from 'components/text_draw/rectangle.js'; |
| 7 | +import { TextDraw } from 'components/text_draw/text_draw.js'; |
| 8 | + |
| 9 | +// Highlight colour to display on the cancel button when hovering over it. |
| 10 | +const kCancelButtonHighlightColor = Color.fromRGBA(0xB0, 0xBE, 0xC5, 0xFF); |
| 11 | + |
| 12 | +// Height and width of the individual color rectangles. |
| 13 | +const kColorRectangleMarginX = 9; |
| 14 | +const kColorRectangleMarginY = 0.5; |
| 15 | + |
| 16 | +const kColorRectangleHeight = 20.0; |
| 17 | +const kColorRectangleWidth = 16.0; |
| 18 | + |
| 19 | +// Height to reserve for the picker's header, indicating the phase of selection. |
| 20 | +const kHeaderHeight = 13.0; |
| 21 | + |
| 22 | +// The background color for the color picker, a dark, slightly transparent black. |
| 23 | +const kPickerBackgroundColor = Color.fromRGBA(0, 0, 0, 0x85); |
| 24 | + |
| 25 | +// Offset of the picker itself on the player screens. Based on GTA's canonical screen resolution. |
| 26 | +const kPickerOffsetX = 32.0; |
| 27 | +const kPickerOffsetY = 154.0; |
| 28 | + |
| 29 | +// Compound values of the above definitions, to avoid having complicated calculations inline. |
| 30 | +const kPickerWidth = 7 * kColorRectangleMarginX + 6 * kColorRectangleWidth; |
| 31 | +const kPickerHeight = 7 * kColorRectangleMarginY + 6 * kColorRectangleHeight + kHeaderHeight + 15; |
| 32 | + |
| 33 | +// After how many seconds do we automatically time out the colour picker? |
| 34 | +const kPickerTimeoutMs = 5 * 1000; |
| 35 | + |
| 36 | +// Displays a color picker for the |player| with the given |colors|, which must be an array with 36 |
| 37 | +// instances of the Color object. Will return a Color instance when selected, or NULL when aborted. |
| 38 | +export async function displayColorPicker(player, title, colors) { |
| 39 | + let resolver = null; |
| 40 | + |
| 41 | + const promise = new Promise(resolve => resolver = resolve); |
| 42 | + const elements = [ |
| 43 | + createBackgroundElement(), |
| 44 | + createTitleElement(title), |
| 45 | + createCancelButton(), |
| 46 | + createCancelButtonLabel(resolver.bind(/* thisArg= */ null, /* color= */ null)), |
| 47 | + ]; |
| 48 | + |
| 49 | + // Create elements for all the |colors| that can be selected by the player. |
| 50 | + for (const color of colors) { |
| 51 | + elements.push(createColorElement({ |
| 52 | + index: elements.length - 4, |
| 53 | + color: color, |
| 54 | + |
| 55 | + // Invoke the |resolver| with the given |color| when clicked on by the |player|. |
| 56 | + listener: resolver.bind(/* thisArg= */ null, color), |
| 57 | + })); |
| 58 | + } |
| 59 | + |
| 60 | + // (1) Display the color picker to the |player|. |
| 61 | + for (const element of elements) |
| 62 | + element.displayForPlayer(player); |
| 63 | + |
| 64 | + // (2) Schedule a timer for |kPickerTimeoutMs| to automatically time out the picker, if needed. |
| 65 | + wait(kPickerTimeoutMs).then(() => { |
| 66 | + if (resolver) |
| 67 | + resolver(/* color= */ null); |
| 68 | + }); |
| 69 | + |
| 70 | + // (3) Start selecting for the |player| and wait for the picker to complete, either by timeout, |
| 71 | + // cancellation or selection. We clean up our state immediately after. |
| 72 | + pawnInvoke('SelectTextDraw', 'ii', player.id, kCancelButtonHighlightColor.toNumberRGBA()); |
| 73 | + |
| 74 | + const color = await promise; |
| 75 | + |
| 76 | + pawnInvoke('CancelSelectTextDraw', 'i', player.id); |
| 77 | + |
| 78 | + resolver = null; // avoid double-resolving |
| 79 | + |
| 80 | + // (4) Remove all the |elements| for the player, as the folow has completed |
| 81 | + for (const element of elements) |
| 82 | + element.hideForPlayer(player); |
| 83 | + |
| 84 | + // (5) And return the selected |color| (which may be NULL) to the caller. |
| 85 | + return color; |
| 86 | +} |
| 87 | + |
| 88 | +// Creates an element for the picker's background for the |player|. An adjustment will be applied in |
| 89 | +// the element's height as something's wrong with the Rectangle calculation. |
| 90 | +function createBackgroundElement() { |
| 91 | + return new Rectangle( |
| 92 | + /* x= */ kPickerOffsetX, |
| 93 | + /* y= */ kPickerOffsetY, |
| 94 | + /* width= */ kPickerWidth, |
| 95 | + /* height= */ kPickerHeight, |
| 96 | + /* color= */ kPickerBackgroundColor); |
| 97 | +} |
| 98 | + |
| 99 | +// Creates an element to represent the picker's title, which helps the player understand where in |
| 100 | +// the color selection flow they are. Real complicated with two steps. |
| 101 | +function createTitleElement(title) { |
| 102 | + return new TextDraw({ |
| 103 | + text: title, |
| 104 | + position: [ |
| 105 | + kPickerOffsetX + 0.666 * kColorRectangleMarginX, |
| 106 | + kPickerOffsetY + 1, |
| 107 | + ], |
| 108 | + |
| 109 | + font: TextDraw.FONT_SANS_SERIF, |
| 110 | + letterSize: [ 0.27, 0.90 ], |
| 111 | + shadowSize: 0, |
| 112 | + }); |
| 113 | +} |
| 114 | + |
| 115 | +// Creates a cancel button that will invoke the |listener| once clicked upon. This helps players |
| 116 | +// when they change their minds, and actually don't want to change colors at all. |
| 117 | +function createCancelButton() { |
| 118 | + return new Rectangle( |
| 119 | + /* x= */ kPickerOffsetX, |
| 120 | + /* y= */ kPickerOffsetY + kPickerHeight - /* arbitrary? */ 9.0, |
| 121 | + /* width= */ kPickerWidth, |
| 122 | + /* height= */ kColorRectangleHeight, |
| 123 | + /* color= */ kPickerBackgroundColor); |
| 124 | +} |
| 125 | + |
| 126 | +// Creates the text label to draw on the cancel button. |
| 127 | +function createCancelButtonLabel(listener) { |
| 128 | + return new ClickableTextDraw(listener, { |
| 129 | + text: 'CANCEL', |
| 130 | + position: [ |
| 131 | + kPickerOffsetX + kPickerWidth / 2, |
| 132 | + kPickerOffsetY + kPickerHeight - /* arbitrary? */ 5.5, |
| 133 | + ], |
| 134 | + |
| 135 | + alignment: TextDraw.ALIGN_CENTER, |
| 136 | + font: TextDraw.FONT_SANS_SERIF, |
| 137 | + letterSize: [ 0.39, 0.90 ], |
| 138 | + shadowSize: 0, |
| 139 | + selectable: true, |
| 140 | + }); |
| 141 | +} |
| 142 | + |
| 143 | +// Creates an element to represent the given |color|, at the given |index| on the 6x6 grid. The |
| 144 | +// |listener| will be called when the color has been selected. |
| 145 | +function createColorElement({ index, color, listener } = {}) { |
| 146 | + const elementRow = Math.floor(index / 6); |
| 147 | + const elementColumn = index % 6; |
| 148 | + |
| 149 | + const elementOffsetX = |
| 150 | + kPickerOffsetX + kColorRectangleMarginX + |
| 151 | + elementColumn * (kColorRectangleMarginX + kColorRectangleWidth); |
| 152 | + |
| 153 | + const elementOffsetY = |
| 154 | + kPickerOffsetY + kColorRectangleMarginY + kHeaderHeight + |
| 155 | + elementRow * (kColorRectangleMarginY + kColorRectangleHeight); |
| 156 | + |
| 157 | + return new ClickableTextDraw(listener, { |
| 158 | + position: [ elementOffsetX + kColorRectangleWidth / 2, elementOffsetY ], |
| 159 | + text: '_', |
| 160 | + |
| 161 | + alignment: TextDraw.ALIGN_CENTER, |
| 162 | + letterSize: [ 0.0, 1.775 ], |
| 163 | + textSize: [ kColorRectangleWidth, kColorRectangleHeight ], |
| 164 | + selectable: true, |
| 165 | + |
| 166 | + boxColor: color, |
| 167 | + useBox: true, |
| 168 | + }); |
| 169 | +} |
| 170 | + |
| 171 | +// Implementation of the TextDraw class that listens to click events from the player. |
| 172 | +class ClickableTextDraw extends TextDraw { |
| 173 | + #listener_ = null; |
| 174 | + |
| 175 | + constructor(listener, ...params) { |
| 176 | + super(...params); |
| 177 | + |
| 178 | + this.#listener_ = listener; |
| 179 | + } |
| 180 | + |
| 181 | + // Called when the |player| has clicked on this text draw. Invokes the listener. |
| 182 | + onClick(player) { this.#listener_.call(null); } |
| 183 | +} |
0 commit comments