From b8a761a448fa514a97ac2fd2817110cb30e174aa Mon Sep 17 00:00:00 2001 From: Ben Carter Date: Thu, 14 Mar 2024 23:45:04 -0400 Subject: [PATCH] Added changes for AIO Advanced topic --- docs/_sidebar.md | 3 +- docs/ets/AdvancedTopics.md | 51 ----- docs/ets/BuildingCustomUIs.md | 84 ++++++++ docs/examples/aio.slotmachine.md | 316 +++++++++++++++++++++++++++++++ 4 files changed, 402 insertions(+), 52 deletions(-) delete mode 100644 docs/ets/AdvancedTopics.md create mode 100644 docs/ets/BuildingCustomUIs.md create mode 100644 docs/examples/aio.slotmachine.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 86eba3d..a87c78f 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -4,7 +4,8 @@ - [Configuration](./ets/Configuration.md) - [Building Modules](./ets/Modules.md) - [VSCode Integration](./ets/VSCodeIntegration.md) - - [Advanced Topics](./ets/AdvancedTopics.md) + - Advanced Topics + - [Building Custom UIs](./ets/BuildingCustomUIs.md) - Eluna Classes - [Achievement](./classes/Achievement.md) - [Aura](./classes/Aura.md) diff --git a/docs/ets/AdvancedTopics.md b/docs/ets/AdvancedTopics.md deleted file mode 100644 index b678f0f..0000000 --- a/docs/ets/AdvancedTopics.md +++ /dev/null @@ -1,51 +0,0 @@ - - -# Advanced Topics - -For developers looking to dive deeper into the capabilities of ETS-Cli, this section explores advanced features and integrations, including using Rochet2's AIO with WoW API for creating complex server-served addons. - -## Using Rochet2's AIO with WoW API - -Rochet2's AIO system is a powerful tool for creating intricate addons that can be served directly from the server to the client, enhancing gameplay with new features and interactions. - -### Installation and Setup - -Before incorporating AIO into your modules, you must first install and configure it on your server. Visit the [AIO GitHub page](https://github.com/Rochet2/AIO) for detailed installation instructions. - -### Example: Creating a Gambling Game - -This example demonstrates how to use AIO alongside the WoW API to build AIO UI elements into the client without requiring user patching or installing plugins. - -**Server-side Script (aio.server.ts):** -```typescript -/** @ts-expect-error */ -let aio: AIO = {}; - -const SLOT_GAME_OBJECT = 750001; - -const ShowGambler: player_event_on_command = (event: number, player: Player, command: string): boolean => { - if (command === 'gamble') { - aio.Handle(player, 'GamblerMain', 'ShowFrame'); - return false; - } - return true; -}; -``` -**Client-side Script (aio.client.ts)** -```typescript -/** @ts-expect-error */ -let aio: AIO = {}; - -if (!aio.AddAddon()) { - const gamblerHandlers = aio.AddHandlers('GamblerMain', {}); - - function ShowSlots(player: Player) { - const GamblerMainFrame = CreateFrame("Frame", "GamblerMainFrame", UIParent, "UIPanelDialogTemplate"); - GamblerMainFrame.SetSize(512, 324); - GamblerMainFrame.SetMovable(false); - GamblerMainFrame.SetPoint("CENTER"); - GamblerMainFrame.EnableMouse(true); - GamblerMainFrame.EnableKeyboard(true); - GamblerMainFrame.Hide(); - } -``` \ No newline at end of file diff --git a/docs/ets/BuildingCustomUIs.md b/docs/ets/BuildingCustomUIs.md new file mode 100644 index 0000000..798fd7e --- /dev/null +++ b/docs/ets/BuildingCustomUIs.md @@ -0,0 +1,84 @@ + + +# Building Custom UIs + +For developers looking to dive deeper into the capabilities of ETS-Cli, this section explores advanced features and integrations, including using Rochet2's AIO with WoW API for creating complex server-served addons. + +## Using Rochet2's AIO with WoW API + +Rochet2's AIO system is a powerful tool for creating intricate addons that can be served directly from the server to the client, enhancing gameplay with new features and interactions. + +### Installation and Setup + +Before incorporating AIO into your modules, you must first install and configure it on your server. Visit the [AIO GitHub page](https://github.com/Rochet2/AIO) for detailed installation instructions. + +### Building AIO Modules +Designing Addons that will be served through AIO works the same as developing addons for World of Warcraft retail. You leverate the [WoWAPI](https://wowpedia.fandom.com/wiki/World_of_Warcraft_API) to build your UI. This site will have all methods that are currently available up to 10+. This means you will need to review commands to make sure they were available for the client you are supporting. For instance if you are building for AzerothCore you will need to make sure the API call is available for 3.3.5. + +There are some key differences when building AIO modules vs AddOns +- Everything **MUST** be scripted you can not use XML +- There is not a TOC file to declare dependencies +- The Addons are shipped to the same global namespace so you need to be mindful of how you set global variables. +- If you do not use Eluna Typescript, then you are also limited to not being able to use require as it is not an available command. + +### Using WoW API in Modules +ETS comes bundle with declarations for many of the WoWAPI calls that are available in 3.3.5a. (If a method is not present please submit an issue and it will get added). This enables easier IDE support for creating custom UI's faster. If you are a GitHub Co-pilot user you will find code completion very helpful as you build. + +You can see a full example below of WoWAPI calls, here is a really basic example: +```Typescript +if(!aio.AddAddon()) { + const myHandlers = aio.AddHandlers('AIOTest', {}); + const MainFrame = CreateFrame("Frame", "MainFrame", UIParent, "UIPanelDialogTemplate"); + let frame = MainFrame; + + frame.SetSize(800,600); + frame.SetMovable(true); + frame.RegisterForDrag("LeftButton"); + frame.SetPoint("CENTER", 0, 20); + frame.EnableMouse(true); + frame.Hide(); + + + frame.SetScript("OnDragStart", frame.StartMoving); + frame.SetScript("OnHide", frame.StopMovingOrSizing); + frame.SetScript("OnDragStop", frame.StopMovingOrSizing); + + frame.show(); +} +``` +### Project Structure +Naming convention, due to the nature of how the TypeScriptToLua plugin transpiles code with the plugin you **MUST** identify your client file. module.```client```.ts The client portion is required or your transpile will not work correctly. + +**Basic structure** +```file +module/ +├── module.client.ts +├── module.server.ts +``` + +**Advanced Structure with shared common and local libraries** +```file +common/ +├── account.ts +└── ui.utils.ts +module/ +├── module.client.ts +├── module.server.ts +└── libs/ + └── module.functions.ts +``` + +In order to use this functionality you use typical imports. **This is only referencing client files, server files imports can be used however, as eluna supports "require" method call where the WoW Client does not. +```typescript +// ---- File: module.client.ts +// common import +import { Colorize } from '../common/ui.utils.ts'; +// local import +import {MyFunction} from './libs/module.functions.ts'; +``` + +!> **Time** There is a known issue with multi-layered dependency resolution and the issue is posted. This means only 1 level of dependencies in client file is allowed. + +Review the full example provided to see how the server and client code communicate using ```AIO.AddHandlers``` and the ```AIO.Handle``` methods. + +[Slot Machine Full Example](../examples/aio.slotmachine.md) \ No newline at end of file diff --git a/docs/examples/aio.slotmachine.md b/docs/examples/aio.slotmachine.md new file mode 100644 index 0000000..529a34d --- /dev/null +++ b/docs/examples/aio.slotmachine.md @@ -0,0 +1,316 @@ +## Slot Machine Example +This example demonstrates how to use AIO alongside the WoW API to build AIO UI elements into the client without requiring user patching or installing plugins. + +**Server-side Script (aio.server.ts):** +```typescript +/** @ts-expect-error */ +let aio: AIO = {}; + +/** + * Gambler - Slot Machine + * This is the server side code used to add gambling games to the server. + */ + + +/** + * Game OBject that will start the slot machine up + */ +const SLOT_GAME_OBJECT = 750001; + + +const ShowGambler: player_event_on_command = (event: number,player: Player, command: string): boolean => { + if(command == 'gamble') { + aio.Handle(player, 'GamblerMain', 'ShowFrame'); + return false; + } + return true; +}; + +/** + * @noSelf + */ +function PayForSpin(this:void, player: Player, cost: number): void { + const money = player.GetCoinage(); + if(money >= cost) { + player.ModifyMoney(cost * -1); + aio.Handle(player, 'GamblerMain', 'StartSpin'); + } else { + player.SendNotification("You don't have enough money to spin the slots!"); + player.PlayDirectSound(8959, player); + } +} + +function AwardSlotWin(this:void, player: Player, gold: number, tokens: number): void { + player.ModifyMoney(gold*10000); + if(tokens > 0) { + player.AddItem(910001, tokens); + } + + if(tokens > 75) { + player.SendChatMessageToPlayer(ChatMsg.CHAT_MSG_GUILD, 0, `|cff1eff00I HIT THE JACKPOT! I won ${gold} gold and ${tokens} tokens!`, player); + } else { + if(tokens > 0) { + player.SendChatMessageToPlayer(ChatMsg.CHAT_MSG_GUILD, 0, `|cff1eff00I won ${gold} gold and ${tokens} tokens!`, player); + } else { + player.SendChatMessageToPlayer(ChatMsg.CHAT_MSG_GUILD, 0, `|cff1eff00I won ${gold} gold`, player); + } + } + +} + +const SendSlotStart: gameobject_event_on_use = (event: number, gameobject: GameObject, player: Player): boolean => { + aio.Handle(player, 'GamblerMain', 'ShowFrame'); + return true; +} + +const gamblerHandlers = aio.AddHandlers('GamblerMain', { + PayForSpin, + AwardSlotWin +}); + +RegisterPlayerEvent( + PlayerEvents.PLAYER_EVENT_ON_COMMAND, + (...args) => ShowGambler(...args) +); + +RegisterGameObjectEvent(SLOT_GAME_OBJECT, GameObjectEvents.GAMEOBJECT_EVENT_ON_USE, (...args) => SendSlotStart(...args)); + +``` +**Client-side Script (aio.client.ts)** +```typescript +/** @ts-expect-error */ +let aio: AIO = {}; +if(!aio.AddAddon()) { + +const gamblerHandlers = aio.AddHandlers('GamblerMain', {}); + +const classImages = [ + "Interface/Gambler/druid", + "Interface/Gambler/deathknight", + "Interface/Gambler/hunter", + "Interface/Gambler/mage", + "Interface/Gambler/paladin", + "Interface/Gambler/priest", + "Interface/Gambler/rogue", + "Interface/Gambler/shaman", + "Interface/Gambler/warlock", + "Interface/Gambler/warrior", +]; + +let slotSpin = []; +let multiplier = 1; + +// this function will randomly select a class image from the array above +function getRandomClassImage() { + const spinIndex = Math.floor(Math.random() * classImages.length); + slotSpin.push(spinIndex); + + return classImages[spinIndex]; +} + +// reset the spin +function resetSpin () { + slotSpin = []; +} + + +function determineWin(): number { + let win = 0; + let gold = 0; + let tokens = 0; + + // Jackpot is all 3 slots as deathknight arthas + if(slotSpin[0] == 1 && slotSpin[1] == 1 && slotSpin[2] == 1) { + + if(multiplier == 3) { + tokens = 100; + } + gold = multiplier * 2000; + win = 2; + } + + if(slotSpin[0] == slotSpin[1] && slotSpin[1] == slotSpin[2]) { + if(multiplier == 3) { + tokens = 50; + } + gold = multiplier * 500; + win = 1; + } + + // Deathknights are considered wild cards + if( + (slotSpin[0] == slotSpin[1] && slotSpin[2] === 1) || + (slotSpin[0] == slotSpin[2] && slotSpin[1] === 1) || + (slotSpin[1] == slotSpin[2] && slotSpin[0] === 1) || + (slotSpin[0] == 1 && slotSpin[1] === 1) || + (slotSpin[0] == 1 && slotSpin[2] === 1) || + (slotSpin[1] == 1 && slotSpin[2] === 1) + + ) { + if(multiplier == 3) { + tokens = 3; + } + gold = multiplier * 300; + win = 1; + } + + // handle two of the same class in a row + if((slotSpin[0] == slotSpin[1]) && win == 0) { + gold = multiplier * 150; + win = 1; + + if(slotSpin[1] == 1) { + if(multiplier == 3) { + tokens = 3; + } + gold = multiplier * 150; + win = 1; + } + } + + // Return money on any lich king wild + if((slotSpin[0] == 1 || slotSpin[1] == 1 || slotSpin[2] == 1) && win == 0) { + if(multiplier == 3) { + tokens = 0; + gold = 100; + } else { + tokens = 0; + gold = 20; + } + + win = 1; + } + + if(win > 0) { + PlaySoundFile("Sound\\Interface\\LootCoinLarge.wav", "Master"); + aio.Handle("GamblerMain", "AwardSlotWin", gold, tokens); + } + + return win; +} + +function SpinSlots(SlotFrame: WoWAPI.Frame, Slot: WoWAPI.Texture[]) { + let timer = 1; + let counter = 1; + + PlaySoundFile("Sound\\Doodad\\GnomeMachine02StandLoop.wav", "Master"); + SlotFrame.SetScript("OnUpdate", (frame, elapsed) => { + timer = timer + elapsed; + if(timer > 0.20) { + counter = counter + 1; + + resetSpin(); + timer = 0; + Slot[0].SetTexture(getRandomClassImage()); + Slot[1].SetTexture(getRandomClassImage()); + Slot[2].SetTexture(getRandomClassImage()); + + if(counter > 22) { + frame.SetScript("OnUpdate", null); + + determineWin(); + } + } + }); +} + +function ShowSlots(player: Player) { + + const GamblerMainFrame = CreateFrame("Frame", "GamblerMainFrame", UIParent, "UIPanelDialogTemplate"); + + GamblerMainFrame.SetSize(512,324); + GamblerMainFrame.SetMovable(false); + GamblerMainFrame.SetPoint("CENTER"); + GamblerMainFrame.EnableMouse(true); + GamblerMainFrame.EnableKeyboard(true); + GamblerMainFrame.Hide(); + + const Title = GamblerMainFrame.CreateFontString("TitleFrame", "OVERLAY", "GameFontHighlight"); + Title.SetPoint("TOPLEFT", 15, -10); + Title.SetText("Heros Slots"); + Title.SetFont("Fonts\\FRIZQT__.TTF", 10); + + // Slots Display Window + const Slots = CreateFrame("Frame", "SlotsFrame", GamblerMainFrame); + Slots.SetSize(420,160); + Slots.SetPoint("CENTER", 0, 25); + Slots.SetFrameLevel(1); + Slots.SetBackdrop({ + bgFile: "Interface/DialogFrame/UI-DialogBox-Background", + edgeFile: "Interface/DialogFrame/UI-DialogBox-Border", + tile: true, + tileSize: 32, + edgeSize: 32, + insets: { + left: 11, + right: 12, + top: 12, + bottom: 11 + } + }); + + // Slot Columns 1 - 3 + const Slot1 = Slots.CreateTexture("Slot1Texture", null, Slots); + Slot1.SetSize(128,128); + Slot1.SetAlpha(0.85); + Slot1.SetPoint("TOPLEFT", 13, -16); + Slot1.SetTexture(getRandomClassImage()); + + let [ Slot1Point, Slot1Region, Slot1RelPoint, x1offset, y1offset ] = Slot1.GetPoint(); + + const Slot2 = Slots.CreateTexture("Slot2Texture", null, Slots); + Slot2.SetSize(128,128); + Slot2.SetAlpha(0.85); + Slot2.SetPoint("TOPLEFT", Slot1Region, Slot1RelPoint, x1offset + 128 + 5, y1offset); + Slot2.SetTexture(getRandomClassImage()); + + let [ Slot2Point, Slot2Region, Slot2RelPoint, x2offset, y2offset ] = Slot2.GetPoint(); + + const Slot3 = Slots.CreateTexture("Slot3Texture", null, Slots); + Slot3.SetSize(128,128); + Slot3.SetAlpha(0.85); + Slot3.SetPoint("TOPLEFT", Slot2Region, Slot2RelPoint, x2offset + 128 + 5, y2offset); + Slot3.SetTexture(getRandomClassImage()); + + // Low bet button. + const SpinButton = CreateFrame("Button", "SpinButtonLow", GamblerMainFrame, "UIPanelButtonTemplate"); + SpinButton.SetSize(128,32); + SpinButton.SetPoint("CENTER", -80, -80); + SpinButton.SetText("Bet 20g Spin"); + SpinButton.SetFrameLevel(2); + SpinButton.SetScript("OnClick", (frame, mouse, button) => { + resetSpin(); + multiplier = 1; + aio.Handle("GamblerMain", "PayForSpin", 20*10000); + }); + + const SpinButtonHigh = CreateFrame("Button", "SpinButtonHigh", GamblerMainFrame, "UIPanelButtonTemplate"); + SpinButtonHigh.SetSize(128,32); + SpinButtonHigh.SetPoint("CENTER", 80, -80); + SpinButtonHigh.SetText("Bet 100g Spin"); + SpinButtonHigh.SetFrameLevel(2); + SpinButtonHigh.SetScript("OnClick", (frame, mouse, button) => { + resetSpin(); + multiplier = 3; + aio.Handle("GamblerMain", "PayForSpin", 100*10000); + }); + + gamblerHandlers.StartSpin = (player: Player) => { + SpinSlots(Slots, [Slot1, Slot2, Slot3]); + } + + GamblerMainFrame.Show(); + + return GamblerMainFrame; +} + +gamblerHandlers.ShowFrame = (player: Player) => { + ShowSlots(player); +} + +} +``` +1. This client script create a new frame and displays 3 images randomly chosen. +2. On bet, money is removed from the player and a 24 frame animation is started changing the images out. +3. At the same time a doodad sound effect that lasts the same amount of time as the 24 frames is played. +4. Upon completion of the frame spin determinations of wins is made and money rewarded. \ No newline at end of file