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'],