diff --git a/ecs/Banca.ts b/ecs/Banca.ts
new file mode 100644
index 0000000..ef95305
--- /dev/null
+++ b/ecs/Banca.ts
@@ -0,0 +1,26 @@
+import {
+ ActionButton,
+ BoxShape,
+ Entity,
+ Material,
+ OnPointerDown,
+ Texture,
+ Transform,
+ TransformConstructorArgs,
+} from 'decentraland-ecs';
+import TicketPrompt from './TicketPrompt';
+
+export default class Banca extends Entity {
+ constructor(transformArgs: TransformConstructorArgs) {
+ super('Banca');
+ this.addComponent(new Transform(transformArgs));
+ this.addComponent(new BoxShape());
+
+ const material = new Material();
+ material.albedoTexture = new Texture('https://dummyimage.com/100x100/ffd700/000000.png&text=BET');
+ this.addComponent(material);
+
+ this.addComponent(new OnPointerDown(() => TicketPrompt.show(),
+ { hoverText: 'bet', showFeedback: true, button: ActionButton.POINTER }));
+ }
+}
diff --git a/ecs/SceneManager.ts b/ecs/SceneManager.ts
index e05a21a..2b3cb9a 100644
--- a/ecs/SceneManager.ts
+++ b/ecs/SceneManager.ts
@@ -4,7 +4,9 @@ import {
IEngine,
ISystem,
MessageBus,
+ Vector3,
} from 'decentraland-ecs';
+import Banca from './Banca';
import Bicho, { BichoType } from './Bicho';
import getUserAddress from './web3/getUserAddress';
import getUserBichos from './web3/getUserBichos';
@@ -43,6 +45,8 @@ export default class SceneManager implements ISystem {
this.address = address;
bichos?.forEach((type) => this.bus.emit('bicho', { address, type } as BichoEvent));
});
+
+ this.engine.addEntity(new Banca({ position: new Vector3(8, 1, 8) }));
}
deactivate() {
diff --git a/ecs/TicketPrompt.ts b/ecs/TicketPrompt.ts
new file mode 100644
index 0000000..63b259a
--- /dev/null
+++ b/ecs/TicketPrompt.ts
@@ -0,0 +1,139 @@
+///
+import {
+ ButtonStyles,
+ CustomPrompt,
+ CustomPromptButton,
+ CustomPromptCheckBox,
+ CustomPromptText,
+ LoadingIcon,
+ PromptStyles,
+} from '@dcl/ui-scene-utils';
+import { DecentralandInterface, Entity } from 'decentraland-ecs';
+import { BichoType } from './Bicho';
+import image from './images/ticket.jpg';
+import getBichoContract from './web3/getBichoContract';
+import getSigner from './web3/getSigner';
+
+declare const dcl: DecentralandInterface;
+
+const IMAGE_SCALE = 2 / 3;
+const IMAGE_WIDTH = 580 - 13 - 13;
+const IMAGE_HEIGHT = 864 - 104 - 31;
+
+const COLUMN_COUNT = 5;
+const COLUMN_WIDTH = IMAGE_WIDTH / COLUMN_COUNT - 1;
+const COLUMN_HEIGHT = IMAGE_HEIGHT / COLUMN_COUNT + 1;
+
+const BAR_HEIGHT = 64;
+const CONTENT_Y = BAR_HEIGHT / 2;
+const BAR_Y = -(IMAGE_HEIGHT * IMAGE_SCALE + 16) / 2;
+
+enum State {
+ Bet,
+ Waiting,
+ Result,
+}
+
+interface CustomPromptElement extends Entity {
+ hide(): void;
+ show(): void;
+}
+
+class TicketPrompt extends CustomPrompt {
+ protected bichos: CustomPromptCheckBox[];
+
+ protected submitButton: CustomPromptButton;
+
+ protected result: CustomPromptText;
+
+ protected state: State;
+
+ protected groups: { [S in State]?: CustomPromptElement[] } = {};
+
+ constructor() {
+ super(PromptStyles.LIGHT,
+ IMAGE_WIDTH * IMAGE_SCALE + 48, IMAGE_HEIGHT * IMAGE_SCALE + BAR_HEIGHT + 48);
+
+ this.result = this.addText(null, 0, CONTENT_Y);
+ this.groups[State.Result] = [this.result,
+ this.addButton('OK', 0, BAR_Y, () => this.hide(), ButtonStyles.RED)];
+
+ const loading = new LoadingIcon(null, 0, 0);
+ this.elements.push(loading);
+ this.groups[State.Waiting] = [loading];
+
+ const ticket = this.addIcon(image, 0, CONTENT_Y,
+ IMAGE_WIDTH * IMAGE_SCALE, IMAGE_HEIGHT * IMAGE_SCALE, {
+ sourceLeft: 13, sourceTop: 104, sourceWidth: IMAGE_WIDTH, sourceHeight: IMAGE_HEIGHT,
+ });
+ const cancelButton = this.addButton('Cancel', 100, BAR_Y, () => this.hide(), ButtonStyles.F);
+ this.submitButton = this.addButton('Bet', -100, BAR_Y, () => this.submit(), ButtonStyles.RED);
+ this.bichos = [...Array(BichoType.COUNT)].map((_: any, i) => {
+ const x = i % COLUMN_COUNT;
+ const y = Math.floor(i / COLUMN_COUNT);
+ const xOffset = (x - (COLUMN_COUNT - 1) / 2) * IMAGE_SCALE * COLUMN_WIDTH;
+ const yOffset = ((COLUMN_COUNT - 1) / 2 - y) * IMAGE_SCALE * COLUMN_HEIGHT + CONTENT_Y;
+ return this.addCheckbox(
+ null,
+ xOffset - 38 * IMAGE_SCALE,
+ yOffset - 33 * IMAGE_SCALE,
+ () => {
+ if (!dcl.DEBUG) this.bichos.forEach((checkbox, j) => i !== j && checkbox.uncheck());
+ this.submitButton.enable();
+ },
+ () => this.submitButton.grayOut(),
+ );
+ });
+ this.groups[State.Bet] = [ticket, cancelButton, this.submitButton, ...this.bichos];
+
+ this.hide();
+ }
+
+ reset() {
+ this.submitButton.grayOut();
+ this.bichos.forEach((checkbox) => checkbox.uncheck());
+ this.result.text.value = null;
+ this.state = State.Bet;
+ }
+
+ hide() {
+ super.hide();
+ this.reset();
+ }
+
+ show() {
+ this.background.visible = true;
+ Object.entries(this.groups).forEach(([key, entities]) => {
+ const visible = Number(key) === this.state;
+ entities.forEach((entity) => (visible ? entity.show() : entity.hide()));
+ });
+ }
+
+ async submit() {
+ try {
+ this.setState(State.Waiting);
+ const [contract, signer] = await Promise.all([getBichoContract(), getSigner()]);
+ const writableContract = contract.connect(signer);
+ let nonce = await signer.getTransactionCount();
+ await Promise.all(this.bichos.map(async (checkbox, i) => {
+ if (!checkbox.checked) return;
+ await writableContract.bet(i, { nonce: ++nonce });
+ }));
+ this.hide();
+ } catch ({ code, message }) {
+ if (code === 4001) {
+ this.hide();
+ } else {
+ this.result.text.value = `ERROR\n\n${message ?? ''}`;
+ this.setState(State.Result);
+ }
+ }
+ }
+
+ setState(state: State) {
+ this.state = state;
+ this.show();
+ }
+}
+
+export default new TicketPrompt();
diff --git a/ecs/images/ticket.jpg b/ecs/images/ticket.jpg
new file mode 100644
index 0000000..e4660fb
Binary files /dev/null and b/ecs/images/ticket.jpg differ
diff --git a/ecs/web3/getSigner.ts b/ecs/web3/getSigner.ts
new file mode 100644
index 0000000..2e7abe0
--- /dev/null
+++ b/ecs/web3/getSigner.ts
@@ -0,0 +1,15 @@
+import { JsonRpcSigner } from '@ethersproject/providers';
+import getProvider from './getProvider';
+import getUserAddress from './getUserAddress';
+
+let signer: Promise;
+
+const getSigner = async () => {
+ const [address, provider] = await Promise.all([getUserAddress(), getProvider()]);
+ return address && provider.getSigner(address);
+};
+
+export default async () => {
+ signer ??= getSigner();
+ return signer;
+};
diff --git a/webpack.config.ts b/webpack.config.ts
index 39c3952..51a284b 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -36,14 +36,17 @@ function config(_: any, { mode = 'production' }): Configuration {
Fonts: ['decentraland-ecs', 'Fonts'],
GLTFShape: ['decentraland-ecs', 'GLTFShape'],
Input: ['decentraland-ecs', 'Input'],
+ OnChanged: ['decentraland-ecs', 'OnChanged'],
OnClick: ['decentraland-ecs', 'OnClick'],
OnPointerDown: ['decentraland-ecs', 'OnPointerDown'],
+ OnTextSubmit: ['decentraland-ecs', 'OnTextSubmit'],
Quaternion: ['decentraland-ecs', 'Quaternion'],
Texture: ['decentraland-ecs', 'Texture'],
Transform: ['decentraland-ecs', 'Transform'],
UICanvas: ['decentraland-ecs', 'UICanvas'],
UIContainerRect: ['decentraland-ecs', 'UIContainerRect'],
UIImage: ['decentraland-ecs', 'UIImage'],
+ UIInputText: ['decentraland-ecs', 'UIInputText'],
UIText: ['decentraland-ecs', 'UIText'],
Vector3: ['decentraland-ecs', 'Vector3'],
engine: ['decentraland-ecs', 'engine'],