Created
October 16, 2025 06:45
-
-
Save Waffle1434/4d7b8d08a827d5bcc1b2843a38d325bb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const vec3 = (x = 0, y = 0, z = 0) => mod.CreateVector(x, y, z); | |
| class vec2 { | |
| x: number; | |
| y: number; | |
| constructor(x: number, y: number) { | |
| this.x = x; | |
| this.y = y; | |
| } | |
| to_key(): string { return `${this.x},${this.y}`; } | |
| } | |
| enum InputDir { Left, Right, Up, Down } | |
| class Cell { | |
| box: mod.UIWidget; | |
| constructor(game: SnakeGame, pos: vec2, color: mod.Vector) { | |
| this.box = game.create_box(pos, color); | |
| } | |
| } | |
| class Apple extends Cell { | |
| goodness: number; | |
| constructor(game: SnakeGame, pos: vec2, color: mod.Vector, goodness: number) { | |
| super(game, pos, color); | |
| this.goodness = goodness; | |
| game.cells.set(pos.to_key(), this); | |
| } | |
| } | |
| class SnakeCell extends Cell { | |
| next : SnakeCell | null = null; | |
| key : string; | |
| constructor(game: SnakeGame, pos: vec2, color: mod.Vector) { | |
| super(game, pos, color); | |
| this.key = pos.to_key(); | |
| game.cells.set(this.key, this); | |
| } | |
| move(game: SnakeGame, pos: vec2) { | |
| game.cells.delete(this.key); | |
| this.key = pos.to_key(); | |
| game.cells.set(this.key, this); | |
| mod.SetUIWidgetPosition(this.box, game.cell_to_px(pos)); | |
| } | |
| } | |
| class Snake { | |
| cells : number = 0; | |
| cells_to_add : number; | |
| head_pos : vec2; | |
| color : mod.Vector; | |
| head : SnakeCell | null = null; | |
| tail : SnakeCell | null = null; | |
| constructor(game: SnakeGame, pos: vec2, cells: number, color: mod.Vector) { | |
| this.cells = cells; | |
| this.cells_to_add = cells; | |
| this.head_pos = pos; | |
| this.color = color; | |
| this.tail = this.add_head(game); | |
| } | |
| set_head(cell: SnakeCell) { | |
| if (this.head != null) | |
| this.head.next = cell; | |
| this.head = cell; | |
| } | |
| add_head(game: SnakeGame): SnakeCell { | |
| const cell = new SnakeCell(game, this.head_pos, this.color); | |
| this.set_head(cell); | |
| this.cells_to_add--; | |
| return cell; | |
| } | |
| update(game: SnakeGame, input: vec2) { | |
| this.head_pos.x += input.x; | |
| this.head_pos.y += input.y; | |
| const head_pos_key = this.head_pos.to_key(); | |
| const eat_cell = game.cells.get(head_pos_key); | |
| if (eat_cell instanceof SnakeCell || | |
| this.head_pos.x < 0 || this.head_pos.x >= game.size.x || | |
| this.head_pos.y < 0 || this.head_pos.y >= game.size.y | |
| ) { | |
| game.end(); | |
| return; | |
| } | |
| if (eat_cell instanceof Apple) { | |
| mod.DeleteUIWidget(eat_cell.box); // TODO: move instead? | |
| game.cells.delete(head_pos_key); | |
| this.cells_to_add += eat_cell.goodness; | |
| this.cells += eat_cell.goodness; | |
| game.update_score(); | |
| game.add_apple(); | |
| } | |
| if (this.cells_to_add > 0) { | |
| this.add_head(game); | |
| } | |
| else if (this.tail != null) { | |
| const head_cell = this.tail; | |
| head_cell.move(game, this.head_pos); | |
| if (this.head != this.tail) | |
| this.set_head(head_cell); | |
| if (head_cell.next != null) { | |
| this.tail = head_cell.next; | |
| head_cell.next = null; | |
| } | |
| } | |
| } | |
| } | |
| class SnakeGame { | |
| size : vec2; | |
| cell_size : number; | |
| cells : Map<string, Cell>; | |
| background : mod.UIWidget; | |
| player : mod.Player; | |
| do_score : boolean; | |
| paused : boolean = false; | |
| over : boolean = false; | |
| kill : boolean = true; | |
| cell_size_vec : mod.Vector; | |
| box_id : number = 0; | |
| snake_color : mod.Vector = vec3(1, 1, 1); | |
| apple_color : mod.Vector = vec3(1, 0, 0); | |
| snake : Snake; | |
| constructor(size: vec2, cell_size: number, root: mod.UIWidget, player: mod.Player, do_score: boolean) { | |
| this.size = size; | |
| this.cell_size = cell_size; | |
| this.cells = new Map<string, Cell>(); | |
| this.player = player; | |
| this.do_score = do_score; | |
| this.cell_size_vec = vec3(cell_size, cell_size, 0); | |
| const color_background = vec3(0, 0, 0); | |
| const background_name = "snake_background"; | |
| const background_size = vec3(size.x * cell_size, size.y * cell_size, 0); | |
| mod.AddUIContainer(background_name, vec3(0, 0, 0), background_size, mod.UIAnchor.Center, root, true, 0, color_background, 1, mod.UIBgFill.Solid, player); | |
| this.background = mod.FindUIWidgetWithName(background_name); | |
| this.snake = new Snake(this, new vec2(0, 0), 3, this.snake_color); | |
| if (this.do_score) { | |
| mod.SetScoreboardType(mod.ScoreboardType.CustomFFA); | |
| mod.SetScoreboardColumnNames(mod.Message("Snake Length")); | |
| this.update_score(); | |
| } | |
| } | |
| cell_to_px(pos: vec2): mod.Vector { | |
| return vec3(pos.x * this.cell_size, pos.y * this.cell_size, 0); | |
| } | |
| create_box(pos: vec2, color: mod.Vector): mod.UIWidget { | |
| const name = `snake_${this.box_id++}`; | |
| mod.AddUIContainer(name, this.cell_to_px(pos), this.cell_size_vec, mod.UIAnchor.BottomLeft, this.background, true, 0, color, 1, mod.UIBgFill.Solid, this.player); | |
| return mod.FindUIWidgetWithName(name); | |
| } | |
| get_random_empty_cell(): vec2 { | |
| // TODO: check if grid is full! | |
| while (true) { | |
| const rand_x = mod.RoundToInteger(mod.RandomReal(0, this.size.x - 1)); | |
| const rand_y = mod.RoundToInteger(mod.RandomReal(0, this.size.y - 1)); | |
| const rand_pos = new vec2(rand_x, rand_y); | |
| if (!this.cells.has(rand_pos.to_key())) | |
| return rand_pos; | |
| } | |
| } | |
| add_apple(): Apple { | |
| return new Apple(this, this.get_random_empty_cell(), this.apple_color, 5); | |
| } | |
| update_score() { | |
| if (this.do_score) | |
| mod.SetScoreboardPlayerValues(this.player, this.snake.cells); | |
| } | |
| update(input: InputDir) { | |
| const input_vec = new vec2( | |
| input == InputDir.Left ? -1 : input == InputDir.Right ? 1 : 0, | |
| input == InputDir.Up ? 1 : input == InputDir.Down ? -1 : 0 | |
| ); | |
| this.snake.update(this, input_vec); | |
| } | |
| end() { | |
| this.over = true; | |
| mod.AddUIText("snake_game_over", vec3(0, 0, 0), vec3(160, 32, 0), mod.UIAnchor.Center, this.background, | |
| true, 0, vec3(0, 0, 0), 1, mod.UIBgFill.Solid, mod.Message("GAME OVER"), 32, vec3(1, 1, 1), 1, mod.UIAnchor.Center); | |
| } | |
| } | |
| class Controller { | |
| player : mod.Player; | |
| input_dir : InputDir | null = null; | |
| input_dir_l : InputDir | null = null; | |
| queued_input : InputDir | null = null; | |
| button_l : mod.UIWidget | null = null; | |
| button_l_name : string | null = null; | |
| disabled_buttons : mod.UIWidget[] = []; | |
| static inverse_input = { | |
| [InputDir.Left]: InputDir.Right, | |
| [InputDir.Right]: InputDir.Left, | |
| [InputDir.Up]: InputDir.Down, | |
| [InputDir.Down]: InputDir.Up | |
| }; | |
| constructor(player: mod.Player) { | |
| this.player = player; | |
| } | |
| async create_buttons() { | |
| const button_center = this.create_button("snake_button_center", 0, 0, game.background); | |
| controller.set_button_l(button_center, "snake_button_center"); | |
| await mod.Wait(0.5); | |
| this.create_button("snake_button_l", -1, 0, game.background); | |
| this.create_button("snake_button_r", 1, 0, game.background); | |
| this.create_button("snake_button_u", 0, 1, game.background); | |
| this.create_button("snake_button_d", 0, -1, game.background); | |
| this.create_button("snake_button_ul", -1, 1, game.background); | |
| this.create_button("snake_button_ur", 1, 1, game.background); | |
| this.create_button("snake_button_dl", -1, -1, game.background); | |
| this.create_button("snake_button_dr", 1, -1, game.background); | |
| } | |
| create_button(name: string, x: number, y: number, background: mod.UIWidget): mod.UIWidget { | |
| const button_size = 16; | |
| const x_base = -64; | |
| const y_base = -64; | |
| mod.AddUIButton(name, | |
| vec3(x_base + x * button_size, y_base + y * button_size, 0), | |
| vec3(button_size, button_size, 0), | |
| mod.UIAnchor.BottomLeft, background, true, 0, | |
| vec3(1.0, 1.0, 1.0), 1.0, mod.UIBgFill.Solid, true, | |
| vec3(0.5, 0.5, 0.5), 1.0, | |
| vec3(1.0, 1.0, 1.0), 0.5, | |
| vec3(1.0, 1.0, 1.0), 1.0, | |
| vec3(0.0, 0.0, 1.0), 1.0, | |
| vec3(1.0, 0.0, 0.0), 1.0, | |
| this.player | |
| ); | |
| const button = mod.FindUIWidgetWithName(name); | |
| mod.EnableUIButtonEvent(button, mod.UIButtonEvent.FocusIn, true); | |
| mod.EnableUIButtonEvent(button, mod.UIButtonEvent.ButtonDown, true); | |
| return button; | |
| } | |
| set_input_state(enabled: boolean) { | |
| mod.EnableInputRestriction(this.player, mod.RestrictedInputs.MoveForwardBack, enabled); | |
| mod.EnableInputRestriction(this.player, mod.RestrictedInputs.MoveLeftRight, enabled); | |
| //mod.EnableInputRestriction(this.player, mod.RestrictedInputs.Jump, enabled); | |
| } | |
| set_button_l(button: mod.UIWidget, name: string) { | |
| this.button_l = button; | |
| this.button_l_name = name; | |
| } | |
| set_input(input: InputDir) { | |
| this.input_dir = input; | |
| for (const button of this.disabled_buttons) { | |
| mod.SetUIWidgetVisible(button, true); | |
| } | |
| this.disabled_buttons.length = 0; | |
| for (const name of this.get_disabled_inputs(input)) { | |
| const button = mod.FindUIWidgetWithName(name); | |
| mod.SetUIWidgetVisible(button, false); | |
| this.disabled_buttons.push(button); | |
| } | |
| } | |
| get_disabled_inputs(input: InputDir): string[] { | |
| switch (input) { | |
| case InputDir.Left: | |
| case InputDir.Right: | |
| return ["snake_button_l", "snake_button_r"]; | |
| case InputDir.Up: | |
| case InputDir.Down: | |
| return ["snake_button_u", "snake_button_d"]; | |
| } | |
| } | |
| button_to_direction(button_name: string): InputDir | null { | |
| switch (button_name) { | |
| case "snake_button_l": return InputDir.Left; | |
| case "snake_button_r": return InputDir.Right; | |
| case "snake_button_u": return InputDir.Up; | |
| case "snake_button_d": return InputDir.Down; | |
| default: | |
| if (this.input_dir_l == null || this.button_l_name == null) | |
| return null; | |
| const next_input = ({ | |
| snake_button_l: { snake_button_ul: InputDir.Up, snake_button_dl: InputDir.Down }, | |
| snake_button_r: { snake_button_ur: InputDir.Up, snake_button_dr: InputDir.Down }, | |
| snake_button_u: { snake_button_ul: InputDir.Left, snake_button_ur: InputDir.Right }, | |
| snake_button_d: { snake_button_dl: InputDir.Left, snake_button_dr: InputDir.Right }, | |
| } as any)[this.button_l_name]?.[button_name] ?? null; | |
| if (next_input == Controller.inverse_input[this.input_dir_l]) { | |
| this.queued_input = next_input; | |
| return null; | |
| } | |
| return next_input; | |
| } | |
| } | |
| async snake_OnPlayerUIButtonEvent( | |
| player: mod.Player, | |
| button: mod.UIWidget, | |
| button_event: mod.UIButtonEvent | |
| ) { | |
| const button_name = mod.GetUIWidgetName(button); | |
| if (button_name.startsWith("snake_") && this.button_l != null) { | |
| if (mod.Equals(button_event, mod.UIButtonEvent.FocusIn)) { | |
| const new_input = this.button_to_direction(button_name); | |
| mod.SetUIWidgetName(this.button_l, button_name); | |
| mod.SetUIWidgetName(button, "snake_button_center"); | |
| const pos_old = mod.GetUIWidgetPosition(this.button_l); | |
| const pos_new = mod.GetUIWidgetPosition(button); | |
| mod.SetUIWidgetPosition(button, pos_old); | |
| mod.SetUIWidgetPosition(this.button_l, pos_new); | |
| this.set_button_l(button, button_name); | |
| if (new_input != null) | |
| this.set_input(new_input); | |
| } | |
| else if (mod.Equals(button_event, mod.UIButtonEvent.ButtonDown)) | |
| game.paused = !game.paused; | |
| } | |
| } | |
| } | |
| let game : SnakeGame; | |
| let controller : Controller; | |
| function is_snake_gadget_out(player: mod.Player): boolean { | |
| return mod.IsInventorySlotActive(player, mod.InventorySlots.GadgetOne); | |
| } | |
| async function game_session(player: mod.Player) { | |
| const root = mod.GetUIRoot(); | |
| mod.AddUIContainer("snake_blur_background", vec3(0, 0, 0), vec3(8192, 8192, 0), mod.UIAnchor.Center, root, true, 0, vec3(1, 1, 1), 1, mod.UIBgFill.Blur, player); | |
| game = new SnakeGame(new vec2(16, 16), 32, root, player, true); | |
| game.add_apple(); | |
| controller = new Controller(player); | |
| await controller.create_buttons(); | |
| controller.set_input_state(true); | |
| controller.set_input(InputDir.Right); | |
| while (mod.GetSoldierState(player, mod.SoldierStateBool.IsAlive) && !game.over) { | |
| if (!is_snake_gadget_out(player)) { | |
| //paused = true; | |
| break; | |
| } | |
| if (!game.paused && controller.input_dir != null) { | |
| game.update(controller.input_dir); | |
| controller.input_dir_l = controller.input_dir; | |
| if (controller.queued_input != null) { | |
| controller.set_input(controller.queued_input); | |
| controller.queued_input = null; | |
| } | |
| if (game.over) { | |
| await mod.Wait(2); | |
| if (game.kill) | |
| mod.Kill(player); | |
| } | |
| } | |
| await mod.Wait(0.25); | |
| } | |
| mod.DeleteAllUIWidgets(); | |
| controller.set_input_state(false); | |
| } | |
| async function snake_OnPlayerDeployed(player: mod.Player) { | |
| mod.AddEquipment(player, mod.Gadgets.Deployable_EOD_Bot); | |
| //mod.ForceSwitchInventory(player, mod.InventorySlots.GadgetOne); | |
| mod.DisplayHighlightedWorldLogMessage(mod.Message("SELECT GADGET TO PLAY SNAKE! (WASD)")); | |
| await mod.Wait(2); | |
| mod.DisplayHighlightedWorldLogMessage(mod.Message("Press G in scoreboard to like!")); | |
| while (mod.GetSoldierState(player, mod.SoldierStateBool.IsAlive)) { | |
| if (is_snake_gadget_out(player)) { | |
| await mod.Wait(0.5); | |
| await game_session(player); | |
| } | |
| await mod.Wait(0.25); | |
| } | |
| } | |
| export async function OnPlayerDeployed(player: mod.Player) { | |
| await snake_OnPlayerDeployed(player); | |
| } | |
| export async function OnPlayerUIButtonEvent( | |
| player: mod.Player, | |
| button: mod.UIWidget, | |
| button_event: mod.UIButtonEvent | |
| ) { | |
| await controller.snake_OnPlayerUIButtonEvent(player, button, button_event); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment