mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-08-22 19:47:47 +01:00
game collection (#19179)
* game collection * no phased usage * . * spellcheck * chesskers * fixes runtime during del --------- Co-authored-by: Cameron Lennox <killer65311@gmail.com>
This commit is contained in:
co-authored by
Cameron Lennox
parent
db0636c8d1
commit
0fc8ccc50e
@@ -0,0 +1,10 @@
|
||||
#define GAME_SETUP 0 // Used across all datums in the same way, please keep further steps per game as they differ
|
||||
|
||||
#define GAME_SWEEPER "VoreSweeper"
|
||||
#define GAME_FOUR_ROW "Four in a row"
|
||||
#define GAME_SPACE_BATTLE "SpaceBattle"
|
||||
#define GAME_RGP_DICE "RPG dice"
|
||||
#define GAME_CHESS "Chess"
|
||||
#define GAME_CHECKERS "Checkers"
|
||||
#define GAME_NINE_MENS_MORRIS "Nine men's morris"
|
||||
#define GAME_TIC_TAC_TOE "Tic-Tac-Toe"
|
||||
@@ -0,0 +1,87 @@
|
||||
/obj/structure/casino_table/board_game
|
||||
name = "board game table"
|
||||
desc = "A collection of various board games."
|
||||
icon_state = "gamble_preview"
|
||||
var/datum/board_game/game_ui
|
||||
var/static/list/possible_games = list(
|
||||
GAME_SWEEPER = /datum/board_game/four_row,
|
||||
GAME_FOUR_ROW = /datum/board_game/vore_sweeper,
|
||||
GAME_SPACE_BATTLE = /datum/board_game/space_battle,
|
||||
GAME_RGP_DICE = /datum/board_game/rpg_dice,
|
||||
GAME_CHESS = /datum/board_game/chess,
|
||||
GAME_CHECKERS = /datum/board_game/checkers,
|
||||
GAME_NINE_MENS_MORRIS = /datum/board_game/nine_mens,
|
||||
GAME_TIC_TAC_TOE = /datum/board_game/four_row/tic_tac_toe
|
||||
)
|
||||
|
||||
/obj/structure/casino_table/board_game/Initialize(mapload)
|
||||
. = ..()
|
||||
if(ispath(game_ui))
|
||||
game_ui = new game_ui(src)
|
||||
|
||||
/obj/structure/casino_table/board_game/Destroy()
|
||||
if(game_ui)
|
||||
game_ui.parent = null
|
||||
QDEL_NULL(game_ui)
|
||||
. = ..()
|
||||
|
||||
/obj/structure/casino_table/board_game/attack_hand(mob/user)
|
||||
. = ..()
|
||||
if(isliving(user))
|
||||
if(!game_ui)
|
||||
pick_game(user)
|
||||
game_ui.tgui_interact(user)
|
||||
|
||||
/obj/structure/casino_table/board_game/click_alt(mob/user)
|
||||
pick_game(user)
|
||||
|
||||
/obj/structure/casino_table/board_game/proc/pick_game(mob/user)
|
||||
if(game_ui?.game_state != GAME_SETUP)
|
||||
return
|
||||
var/datum/board_game/new_game = tgui_input_list(user, "Pick the game to play", "Choose Game", possible_games)
|
||||
if(!new_game)
|
||||
return
|
||||
new_game = possible_games[new_game]
|
||||
if(game_ui)
|
||||
QDEL_NULL(game_ui)
|
||||
game_ui = new new_game(src)
|
||||
icon_state = game_ui.table_icon
|
||||
|
||||
/datum/board_game
|
||||
var/name
|
||||
var/atom/parent
|
||||
var/game_state = GAME_SETUP
|
||||
var/table_icon = "gamble_preview"
|
||||
|
||||
/datum/board_game/tgui_state(mob/user)
|
||||
return GLOB.tgui_board_game_state
|
||||
|
||||
/datum/board_game/New(atom/holder)
|
||||
. = ..()
|
||||
parent = holder
|
||||
|
||||
/datum/board_game/Destroy(force)
|
||||
parent = null
|
||||
. = ..()
|
||||
|
||||
/datum/board_game/tgui_host(mob/user)
|
||||
return parent
|
||||
|
||||
/datum/board_game/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("invite_player")
|
||||
var/list/possible_mobs = ui.user.living_mobs_in_view(1, TRUE, TRUE)
|
||||
for(var/obj/belly/our_belly in ui.user.vore_organs)
|
||||
for(var/mob/living/prey in our_belly.contents)
|
||||
if(prey.client)
|
||||
possible_mobs += prey
|
||||
var/mob/living/new_player = tgui_input_list(ui.user, "Invite a nearby player to the game.", "Invite Player", possible_mobs)
|
||||
if(!new_player)
|
||||
return FALSE
|
||||
tgui_interact(new_player)
|
||||
return TRUE
|
||||
return FALSE
|
||||
@@ -0,0 +1,523 @@
|
||||
#define GAME_PLAYER_ONE 1
|
||||
#define GAME_PLAYER_TWO 2
|
||||
#define GAME_OVER 3
|
||||
#define GAME_OVER_DRAW 4
|
||||
#define GRID_SIZE 8
|
||||
|
||||
#define GAME_ACTION_NONE 0
|
||||
#define GAME_ACTION_SELECT 1
|
||||
#define GAME_ACTION_END_TURN 2
|
||||
|
||||
/obj/structure/casino_table/board_game/checkers
|
||||
name = GAME_CHECKERS
|
||||
desc = "A classic checkers game."
|
||||
icon_state = "gamble_chess"
|
||||
game_ui = /datum/board_game/checkers
|
||||
|
||||
/datum/board_game/checkers
|
||||
name = GAME_CHECKERS
|
||||
table_icon = "gamble_chess"
|
||||
var/datum/weakref/player_one
|
||||
var/datum/weakref/player_two
|
||||
var/player_one_time = 0
|
||||
var/player_two_time = 0
|
||||
var/static/list/default_board = list(
|
||||
list(null, "bM", null, "bM", null, "bM", null, "bM"),
|
||||
list("bM", null, "bM", null, "bM", null, "bM", null),
|
||||
list(null, "bM", null, "bM", null, "bM", null, "bM"),
|
||||
list(null, null, null, null, null, null, null, null),
|
||||
list(null, null, null, null, null, null, null, null),
|
||||
list("wM", null, "wM", null, "wM", null, "wM", null),
|
||||
list(null, "wM", null, "wM", null, "wM", null, "wM"),
|
||||
list("wM", null, "wM", null, "wM", null, "wM", null)
|
||||
)
|
||||
var/list/current_board = list()
|
||||
var/list/valid_moves = list()
|
||||
var/list/selected_figure = list()
|
||||
var/list/possible_jumps = list()
|
||||
var/turn_start_time = 0
|
||||
var/winner
|
||||
|
||||
/datum/board_game/checkers/Destroy(force)
|
||||
player_one = null
|
||||
player_two = null
|
||||
. = ..()
|
||||
|
||||
/datum/board_game/checkers/tgui_interact(mob/user, datum/tgui/ui)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "ChessCheckers", name)
|
||||
ui.open()
|
||||
|
||||
/datum/board_game/checkers/tgui_static_data(mob/user)
|
||||
return list("game_type" = "checkers")
|
||||
|
||||
/datum/board_game/checkers/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
|
||||
var/mob/player_one_mob = player_one?.resolve()
|
||||
var/mob/player_two_mob = player_two?.resolve()
|
||||
|
||||
return list(
|
||||
"player_one" = player_one_mob,
|
||||
"player_two" = player_two_mob,
|
||||
"player_one_time" = player_one_time + (game_state == GAME_PLAYER_ONE ? world.time - turn_start_time : 0),
|
||||
"player_two_time" = player_two_time + (game_state == GAME_PLAYER_TWO ? world.time - turn_start_time : 0),
|
||||
"current_board" = current_board,
|
||||
"selected_figure" = selected_figure,
|
||||
"valid_moves" = valid_moves,
|
||||
"game_state" = game_state,
|
||||
"winner" = winner,
|
||||
"has_won" = winner == ui.user.name,
|
||||
"possible_jumps" = possible_jumps
|
||||
)
|
||||
|
||||
/datum/board_game/checkers/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("be_player_one")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_one?.resolve() == ui.user)
|
||||
player_one = null
|
||||
return TRUE
|
||||
player_one = WEAKREF(ui.user)
|
||||
return TRUE
|
||||
if("be_player_two")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_two?.resolve() == ui.user)
|
||||
player_two = null
|
||||
return TRUE
|
||||
player_two = WEAKREF(ui.user)
|
||||
return TRUE
|
||||
if("swap_players")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
var/datum/weakref/temp_player = player_one
|
||||
player_one = player_two
|
||||
player_two = temp_player
|
||||
if("clear_game")
|
||||
if(game_state == GAME_SETUP)
|
||||
return FALSE
|
||||
reset(TRUE)
|
||||
return TRUE
|
||||
if("start_game")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
current_board = get_defaultboard()
|
||||
game_state = GAME_PLAYER_ONE
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if("play_again")
|
||||
if(game_state < GAME_OVER)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
reset()
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if("play_again_swapped")
|
||||
if(game_state < GAME_OVER)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
reset()
|
||||
var/datum/weakref/temp_player = player_one
|
||||
player_one = player_two
|
||||
player_two = temp_player
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if("game_action")
|
||||
if(ui.user == player_one?.resolve() && game_state == GAME_PLAYER_ONE)
|
||||
var/game_action = player_actions(params["action"], params["data"], ui.user, "w")
|
||||
if(game_action)
|
||||
if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN)
|
||||
game_state = GAME_PLAYER_TWO
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if(ui.user == player_two?.resolve() && game_state == GAME_PLAYER_TWO)
|
||||
var/game_action = player_actions(params["action"], params["data"], ui.user, "b")
|
||||
if(game_action)
|
||||
if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN)
|
||||
game_state = GAME_PLAYER_ONE
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/checkers/proc/reset(full)
|
||||
winner = null
|
||||
player_one_time = 0
|
||||
player_two_time = 0
|
||||
selected_figure.Cut()
|
||||
valid_moves.Cut()
|
||||
if(full)
|
||||
current_board.Cut()
|
||||
game_state = GAME_SETUP
|
||||
player_one = null
|
||||
player_two = null
|
||||
else
|
||||
current_board = get_defaultboard()
|
||||
game_state = GAME_PLAYER_ONE
|
||||
|
||||
/datum/board_game/checkers/proc/player_actions(action, list/params, mob/user, active_color)
|
||||
switch(action)
|
||||
if("select_figure")
|
||||
var/list/validated_data = validate_coords(params)
|
||||
if (!validated_data)
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
var/piece = current_board[validated_data[2]][validated_data[1]]
|
||||
if(!piece || piece[1] != active_color)
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
if(has_available_jumps(active_color))
|
||||
var/list/jumps = generate_valid_moves(validated_data[1], validated_data[2], TRUE)
|
||||
if (length(jumps) == 0)
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
selected_figure = validated_data
|
||||
update_valid_moves()
|
||||
return GAME_ACTION_SELECT
|
||||
|
||||
if("move_figure")
|
||||
var/list/coords = validate_coords(params)
|
||||
if(!coords || !selected_figure)
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
var/to_x = coords[1]
|
||||
var/to_y = coords[2]
|
||||
|
||||
if(!valid_move(to_x, to_y))
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
var/from_x = selected_figure[1]
|
||||
var/from_y = selected_figure[2]
|
||||
var/moving_piece = current_board[from_y][from_x]
|
||||
|
||||
if(moving_piece[2] == "M" && ((moving_piece[1] == "w" && to_y == 1) || (moving_piece[1] == "b" && to_y == GRID_SIZE)))
|
||||
moving_piece = moving_piece[1] + "K"
|
||||
|
||||
current_board[to_y][to_x] = moving_piece
|
||||
current_board[from_y][from_x] = null
|
||||
|
||||
var/did_jump = FALSE
|
||||
if(abs(to_x - from_x) >= 2 && abs(to_y - from_y) >= 2)
|
||||
var/step_x = (to_x - from_x) / abs(to_x - from_x)
|
||||
var/step_y = (to_y - from_y) / abs(to_y - from_y)
|
||||
var/x = from_x + step_x
|
||||
var/y = from_y + step_y
|
||||
|
||||
while(x != to_x && y != to_y)
|
||||
var/p = current_board[y][x]
|
||||
if(p && p[1] != moving_piece[1])
|
||||
current_board[y][x] = null
|
||||
did_jump = TRUE
|
||||
break
|
||||
x += step_x
|
||||
y += step_y
|
||||
|
||||
if(did_jump && piece_can_jump_again(to_x, to_y))
|
||||
selected_figure = list(to_x, to_y)
|
||||
update_valid_moves()
|
||||
validate_victory(active_color)
|
||||
possible_jumps = get_mandatory_jumps(active_color)
|
||||
return GAME_ACTION_SELECT
|
||||
|
||||
var/turn_duration = world.time - turn_start_time
|
||||
if(game_state == GAME_PLAYER_ONE)
|
||||
player_one_time += turn_duration
|
||||
else if(game_state == GAME_PLAYER_TWO)
|
||||
player_two_time += turn_duration
|
||||
|
||||
selected_figure.Cut()
|
||||
update_valid_moves()
|
||||
validate_victory(active_color)
|
||||
if(game_state != GAME_OVER)
|
||||
possible_jumps = get_mandatory_jumps(active_color == "w" ? "b" : "w")
|
||||
else
|
||||
possible_jumps.Cut()
|
||||
return GAME_ACTION_END_TURN
|
||||
|
||||
/datum/board_game/checkers/proc/get_mandatory_jumps(active_color)
|
||||
var/list/jumping_pieces = list()
|
||||
if(!has_available_jumps(active_color))
|
||||
return jumping_pieces
|
||||
|
||||
for(var/row = 1 to GRID_SIZE)
|
||||
for(var/col = 1 to GRID_SIZE)
|
||||
var/piece = current_board[row][col]
|
||||
if(piece && piece[1] == active_color)
|
||||
var/list/jumps = generate_valid_moves(col, row, TRUE)
|
||||
if(length(jumps) > 0)
|
||||
UNTYPED_LIST_ADD(jumping_pieces, list(col, row))
|
||||
return jumping_pieces
|
||||
|
||||
/datum/board_game/checkers/proc/update_valid_moves()
|
||||
valid_moves.Cut()
|
||||
if(length(selected_figure))
|
||||
var/x = selected_figure[1]
|
||||
var/y = selected_figure[2]
|
||||
valid_moves = generate_valid_moves(x, y)
|
||||
|
||||
/datum/board_game/checkers/proc/piece_can_jump_again(x, y)
|
||||
var/piece = current_board[y][x]
|
||||
if(!piece)
|
||||
return FALSE
|
||||
|
||||
for(var/list/move in generate_valid_moves(x, y, TRUE))
|
||||
if(abs(move[1] - x) >= 2 && abs(move[2] - y) >= 2)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/checkers/proc/valid_move(to_x, to_y)
|
||||
for(var/list/move in valid_moves)
|
||||
if(move[1] == to_x && move[2] == to_y)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/checkers/proc/validate_victory(active_color)
|
||||
var/opponent_color = active_color == "w" ? "b" : "w"
|
||||
var/any_opponent_pieces = FALSE
|
||||
var/opponent_has_moves = FALSE
|
||||
|
||||
var/opponent_has_jumps = has_available_jumps(opponent_color)
|
||||
|
||||
for(var/row = 1 to GRID_SIZE)
|
||||
for(var/col = 1 to GRID_SIZE)
|
||||
var/piece = current_board[row][col]
|
||||
if(piece && piece[1] == opponent_color)
|
||||
any_opponent_pieces = TRUE
|
||||
|
||||
var/list/moves
|
||||
if(opponent_has_jumps)
|
||||
moves = generate_valid_moves(col, row, TRUE)
|
||||
else
|
||||
moves = generate_valid_moves(col, row)
|
||||
|
||||
if(length(moves) > 0)
|
||||
opponent_has_moves = TRUE
|
||||
break
|
||||
if(opponent_has_moves)
|
||||
break
|
||||
|
||||
if(!any_opponent_pieces)
|
||||
var/mob/player_one_mob = player_one?.resolve()
|
||||
var/mob/player_two_mob = player_two?.resolve()
|
||||
winner = active_color == "w" ? player_one_mob?.name : player_two_mob?.name
|
||||
game_state = GAME_OVER
|
||||
return
|
||||
|
||||
if(!opponent_has_moves)
|
||||
if(any_opponent_pieces)
|
||||
winner = null
|
||||
game_state = GAME_OVER_DRAW
|
||||
return
|
||||
|
||||
var/mob/player_one_mob = player_one?.resolve()
|
||||
var/mob/player_two_mob = player_two?.resolve()
|
||||
winner = active_color == "w" ? player_one_mob?.name : player_two_mob?.name
|
||||
game_state = GAME_OVER
|
||||
|
||||
/datum/board_game/checkers/proc/generate_valid_moves(x, y, jump_only)
|
||||
var/piece = current_board[y][x]
|
||||
var/list/moves = list()
|
||||
|
||||
if (!piece)
|
||||
return moves
|
||||
|
||||
var/directions = list()
|
||||
|
||||
switch(piece[2])
|
||||
if("M")
|
||||
if(piece[1] == "w")
|
||||
directions = list(list(1, -1), list(-1, -1))
|
||||
else
|
||||
directions = list(list(1, 1), list(-1, 1))
|
||||
if("K")
|
||||
directions = list(list(1, 1), list(-1, 1), list(1, -1), list(-1, -1))
|
||||
|
||||
var/list/jump_moves = list()
|
||||
add_jumps(directions, x, y, jump_moves)
|
||||
|
||||
if(length(jump_moves) || jump_only)
|
||||
return jump_moves
|
||||
|
||||
switch(piece[2])
|
||||
if("M")
|
||||
for(var/list/direction in directions)
|
||||
var/new_x = x + direction[1]
|
||||
var/new_y = y + direction[2]
|
||||
if(field_check(new_x, new_y) && current_board[new_y][new_x] == null)
|
||||
UNTYPED_LIST_ADD(moves, list(new_x, new_y))
|
||||
if("K")
|
||||
for(var/list/direction in directions)
|
||||
var/step = 1
|
||||
while(TRUE)
|
||||
var/new_x = x + direction[1] * step
|
||||
var/new_y = y + direction[2] * step
|
||||
if(!field_check(new_x, new_y))
|
||||
break
|
||||
if(current_board[new_y][new_x])
|
||||
break
|
||||
UNTYPED_LIST_ADD(moves, list(new_x, new_y))
|
||||
step += 1
|
||||
return moves
|
||||
|
||||
/datum/board_game/checkers/proc/has_available_jumps(active_color)
|
||||
for (var/row = 1 to GRID_SIZE)
|
||||
for (var/col = 1 to GRID_SIZE)
|
||||
var/piece = current_board[row][col]
|
||||
if (piece && piece[1] == active_color)
|
||||
var/list/jumps = generate_valid_moves(col, row, TRUE)
|
||||
if (length(jumps) > 0)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/checkers/proc/already_visited(x, y, list/visited_list)
|
||||
for(var/list/coord in visited_list)
|
||||
if(coord[1] == x && coord[2] == y)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/checkers/proc/add_jumps(var/list/directions, start_x, start_y, list/jump_moves, list/visited = list())
|
||||
var/piece = current_board[start_y][start_x]
|
||||
if(!piece)
|
||||
return
|
||||
|
||||
for(var/list/direction in directions)
|
||||
switch(piece[2])
|
||||
if("M")
|
||||
var/tx = start_x + direction[1] * 2
|
||||
var/ty = start_y + direction[2] * 2
|
||||
|
||||
if(!field_check(tx, ty) || already_visited(tx, ty, visited))
|
||||
continue
|
||||
|
||||
if(can_jump_piece(start_x, start_y, tx, ty))
|
||||
var/list/new_visited = visited.Copy()
|
||||
UNTYPED_LIST_ADD(new_visited, list(tx, ty))
|
||||
UNTYPED_LIST_ADD(jump_moves, list(tx, ty))
|
||||
add_jumps(directions, tx, ty, jump_moves, new_visited)
|
||||
if("K")
|
||||
var/step = 1
|
||||
while(TRUE)
|
||||
var/tx = start_x + direction[1] * step
|
||||
var/ty = start_y + direction[2] * step
|
||||
|
||||
if(!field_check(tx, ty))
|
||||
break
|
||||
|
||||
if(already_visited(tx, ty, visited))
|
||||
step++
|
||||
continue
|
||||
|
||||
if(can_jump_piece(start_x, start_y, tx, ty))
|
||||
var/list/new_visited = visited.Copy()
|
||||
UNTYPED_LIST_ADD(new_visited, list(tx, ty))
|
||||
UNTYPED_LIST_ADD(jump_moves, list(tx, ty))
|
||||
add_jumps(directions, tx, ty, jump_moves, new_visited)
|
||||
|
||||
step++
|
||||
|
||||
/datum/board_game/checkers/proc/can_jump_piece(from_x, from_y, to_x, to_y)
|
||||
var/piece = current_board[from_y][from_x]
|
||||
if(!piece)
|
||||
return FALSE
|
||||
|
||||
var/dx = to_x - from_x
|
||||
var/dy = to_y - from_y
|
||||
var/color = piece[1]
|
||||
|
||||
switch(piece[2])
|
||||
if("M")
|
||||
if(abs(dx) != 2 || abs(dy) != 2)
|
||||
return FALSE
|
||||
var/mid_x = from_x + dx / 2
|
||||
var/mid_y = from_y + dy / 2
|
||||
var/mid_piece = current_board[mid_y][mid_x]
|
||||
if(mid_piece && mid_piece[1] != color && current_board[to_y][to_x] == null)
|
||||
return TRUE
|
||||
return FALSE
|
||||
if("K")
|
||||
if(abs(dx) != abs(dy))
|
||||
return FALSE
|
||||
|
||||
var/step_x = dx / abs(dx)
|
||||
var/step_y = dy / abs(dy)
|
||||
var/x = from_x + step_x
|
||||
var/y = from_y + step_y
|
||||
|
||||
var/jumped_piece_found = FALSE
|
||||
var/jumped_x = 0
|
||||
var/jumped_y = 0
|
||||
|
||||
while(x != to_x && y != to_y)
|
||||
var/piece_at_square = current_board[y][x]
|
||||
if(piece_at_square)
|
||||
if(piece_at_square[1] == color)
|
||||
return FALSE
|
||||
if(jumped_piece_found)
|
||||
return FALSE
|
||||
jumped_piece_found = TRUE
|
||||
jumped_x = x
|
||||
jumped_y = y
|
||||
x += step_x
|
||||
y += step_y
|
||||
|
||||
if(!jumped_piece_found)
|
||||
return FALSE
|
||||
|
||||
if(current_board[to_y][to_x] != null)
|
||||
return FALSE
|
||||
|
||||
x = jumped_x + step_x
|
||||
y = jumped_y + step_y
|
||||
|
||||
while(x != to_x || y != to_y)
|
||||
if(current_board[y][x] != null)
|
||||
return FALSE
|
||||
x += step_x
|
||||
y += step_y
|
||||
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/checkers/proc/field_check(new_x_location, new_y_location)
|
||||
if(new_x_location < 1 || new_x_location > GRID_SIZE)
|
||||
return FALSE
|
||||
if(new_y_location < 1 || new_y_location > GRID_SIZE)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/checkers/proc/validate_coords(list/params)
|
||||
var/x_loc = text2num(params["loc_x"])
|
||||
var/y_loc = text2num(params["loc_y"])
|
||||
|
||||
if(!isnum(x_loc) || !isnum(y_loc))
|
||||
return null
|
||||
|
||||
if(!field_check(x_loc, y_loc))
|
||||
return null
|
||||
|
||||
return list(x_loc, y_loc)
|
||||
|
||||
/datum/board_game/checkers/proc/get_defaultboard()
|
||||
var/list/deep_copy = list()
|
||||
for(var/list/row in default_board)
|
||||
UNTYPED_LIST_ADD(deep_copy, row.Copy())
|
||||
return deep_copy
|
||||
|
||||
#undef GAME_PLAYER_ONE
|
||||
#undef GAME_PLAYER_TWO
|
||||
#undef GAME_OVER
|
||||
#undef GAME_OVER_DRAW
|
||||
#undef GRID_SIZE
|
||||
|
||||
#undef GAME_ACTION_NONE
|
||||
#undef GAME_ACTION_SELECT
|
||||
#undef GAME_ACTION_END_TURN
|
||||
@@ -0,0 +1,566 @@
|
||||
#define GAME_PLAYER_ONE 1
|
||||
#define GAME_PLAYER_TWO 2
|
||||
#define GAME_OVER 3
|
||||
#define GAME_OVER_DRAW 4
|
||||
#define GRID_SIZE 8
|
||||
#define GAME_FLAG_CHECK_PONE 0x1
|
||||
#define GAME_FLAG_CHECK_PTWO 0x2
|
||||
#define GAME_FLAG_CASTLING_USED_PONE 0x4
|
||||
#define GAME_FLAG_CASTLING_USED_PTWO 0x8
|
||||
|
||||
#define GAME_ACTION_NONE 0
|
||||
#define GAME_ACTION_SELECT 1
|
||||
#define GAME_ACTION_END_TURN 2
|
||||
|
||||
/obj/structure/casino_table/board_game/chess
|
||||
name = GAME_CHESS
|
||||
desc = "A classic chess game."
|
||||
icon_state = "gamble_chess"
|
||||
game_ui = /datum/board_game/chess
|
||||
|
||||
/datum/board_game/chess
|
||||
name = GAME_CHESS
|
||||
table_icon = "gamble_chess"
|
||||
var/datum/weakref/player_one
|
||||
var/datum/weakref/player_two
|
||||
var/player_one_time = 0
|
||||
var/player_two_time = 0
|
||||
var/static/list/default_board = list(
|
||||
list("bR","bN","bB","bQ","bK","bB","bN","bR"),
|
||||
list("bP","bP","bP","bP","bP","bP","bP","bP"),
|
||||
list(null,null,null,null,null,null,null,null),
|
||||
list(null,null,null,null,null,null,null,null),
|
||||
list(null,null,null,null,null,null,null,null),
|
||||
list(null,null,null,null,null,null,null,null),
|
||||
list("wP","wP","wP","wP","wP","wP","wP","wP"),
|
||||
list("wR","wN","wB","wQ","wK","wB","wN","wR")
|
||||
)
|
||||
var/list/current_board = list()
|
||||
var/list/valid_moves = list()
|
||||
var/list/selected_figure = list()
|
||||
var/list/last_double_pawn_move = list()
|
||||
var/game_flags = NONE
|
||||
var/turn_start_time = 0
|
||||
var/winner
|
||||
|
||||
/datum/board_game/chess/Destroy(force)
|
||||
player_one = null
|
||||
player_two = null
|
||||
. = ..()
|
||||
|
||||
/datum/board_game/chess/tgui_interact(mob/user, datum/tgui/ui)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "ChessCheckers", name)
|
||||
ui.open()
|
||||
|
||||
/datum/board_game/chess/tgui_static_data(mob/user)
|
||||
return list("game_type" = "chess")
|
||||
|
||||
/datum/board_game/chess/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
|
||||
var/mob/player_one_mob = player_one?.resolve()
|
||||
var/mob/player_two_mob = player_two?.resolve()
|
||||
|
||||
return list(
|
||||
"player_one" = player_one_mob,
|
||||
"player_two" = player_two_mob,
|
||||
"player_one_time" = player_one_time + (game_state == GAME_PLAYER_ONE ? world.time - turn_start_time : 0),
|
||||
"player_two_time" = player_two_time + (game_state == GAME_PLAYER_TWO ? world.time - turn_start_time : 0),
|
||||
"current_board" = current_board,
|
||||
"selected_figure" = selected_figure,
|
||||
"valid_moves" = valid_moves,
|
||||
"game_state" = game_state,
|
||||
"winner" = winner,
|
||||
"has_won" = winner == ui.user.name,
|
||||
"game_flags" = game_flags
|
||||
)
|
||||
|
||||
/datum/board_game/chess/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("be_player_one")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_one?.resolve() == ui.user)
|
||||
player_one = null
|
||||
return TRUE
|
||||
player_one = WEAKREF(ui.user)
|
||||
return TRUE
|
||||
if("be_player_two")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_two?.resolve() == ui.user)
|
||||
player_two = null
|
||||
return TRUE
|
||||
player_two = WEAKREF(ui.user)
|
||||
return TRUE
|
||||
if("swap_players")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
var/datum/weakref/temp_player = player_one
|
||||
player_one = player_two
|
||||
player_two = temp_player
|
||||
if("clear_game")
|
||||
if(game_state == GAME_SETUP)
|
||||
return FALSE
|
||||
reset(TRUE)
|
||||
return TRUE
|
||||
if("start_game")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
current_board = get_defaultboard()
|
||||
game_state = GAME_PLAYER_ONE
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if("play_again")
|
||||
if(game_state < GAME_OVER)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
reset()
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if("play_again_swapped")
|
||||
if(game_state < GAME_OVER)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
reset()
|
||||
var/datum/weakref/temp_player = player_one
|
||||
player_one = player_two
|
||||
player_two = temp_player
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if("game_action")
|
||||
if(ui.user == player_one?.resolve() && game_state == GAME_PLAYER_ONE)
|
||||
var/game_action = player_actions(params["action"], params["data"], ui.user, "w")
|
||||
if(game_action)
|
||||
if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN)
|
||||
game_state = GAME_PLAYER_TWO
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if(ui.user == player_two?.resolve() && game_state == GAME_PLAYER_TWO)
|
||||
var/game_action = player_actions(params["action"], params["data"], ui.user, "b")
|
||||
if(game_action)
|
||||
if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN)
|
||||
game_state = GAME_PLAYER_ONE
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/chess/proc/reset(full)
|
||||
winner = null
|
||||
player_one_time = 0
|
||||
player_two_time = 0
|
||||
selected_figure.Cut()
|
||||
valid_moves.Cut()
|
||||
last_double_pawn_move.Cut()
|
||||
game_flags = NONE
|
||||
if(full)
|
||||
current_board.Cut()
|
||||
game_state = GAME_SETUP
|
||||
player_one = null
|
||||
player_two = null
|
||||
else
|
||||
current_board = get_defaultboard()
|
||||
game_state = GAME_PLAYER_ONE
|
||||
|
||||
/datum/board_game/chess/proc/player_actions(action, list/params, mob/user, active_color)
|
||||
switch(action)
|
||||
if("select_figure")
|
||||
var/list/validated_data = validate_coords(params)
|
||||
if(!validated_data)
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
var/piece = current_board[validated_data[2]][validated_data[1]]
|
||||
if(!piece)
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
if(piece[1] != active_color)
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
selected_figure = validated_data
|
||||
update_valid_moves()
|
||||
return GAME_ACTION_SELECT
|
||||
if("move_figure")
|
||||
var/list/coords = validate_coords(params)
|
||||
if(!coords || !selected_figure)
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
var/from_x = selected_figure[1]
|
||||
var/from_y = selected_figure[2]
|
||||
var/to_x = coords[1]
|
||||
var/to_y = coords[2]
|
||||
|
||||
if(!valid_move(to_x, to_y))
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
var/moving_piece = current_board[from_y][from_x]
|
||||
var/target_piece = current_board[to_y][to_x]
|
||||
|
||||
var/turn_duration = world.time - turn_start_time
|
||||
if(game_state == GAME_PLAYER_ONE)
|
||||
player_one_time += turn_duration
|
||||
else if(game_state == GAME_PLAYER_TWO)
|
||||
player_two_time += turn_duration
|
||||
|
||||
if(moving_piece[2] == "K" && abs(to_x - from_x) == 2)
|
||||
if(!handle_castling(active_color, from_x, from_y, to_x, to_y))
|
||||
return GAME_ACTION_NONE
|
||||
do_castling(active_color, from_x, from_y, to_x, to_y)
|
||||
|
||||
if(moving_piece[2] == "P" && abs(to_x - from_x) == 1 && !target_piece)
|
||||
if(can_en_passant(from_x, from_y, to_x, to_y))
|
||||
current_board[from_y][to_x] = null
|
||||
|
||||
if(!(moving_piece[2] == "K" && abs(to_x - from_x) == 2))
|
||||
current_board[to_y][to_x] = moving_piece
|
||||
current_board[from_y][from_x] = null
|
||||
|
||||
if(moving_piece[2] == "P" && abs(to_y - from_y) == 2)
|
||||
last_double_pawn_move = list(to_x, to_y, moving_piece[1])
|
||||
else
|
||||
last_double_pawn_move.Cut()
|
||||
|
||||
if(moving_piece[2] == "P")
|
||||
if((moving_piece[1] == "w" && to_y == 1) || (moving_piece[1] == "b" && to_y == GRID_SIZE))
|
||||
var/promotion = params["promotion_choice"]
|
||||
if(!(promotion in list("Q", "R", "B", "N")))
|
||||
promotion = "Q"
|
||||
current_board[to_y][to_x] = moving_piece[1] + promotion
|
||||
|
||||
selected_figure.Cut()
|
||||
update_valid_moves()
|
||||
validate_victory(active_color)
|
||||
|
||||
return GAME_ACTION_END_TURN
|
||||
|
||||
/datum/board_game/chess/proc/valid_move(to_x, to_y)
|
||||
for(var/list/move in valid_moves)
|
||||
if(move[1] == to_x && move[2] == to_y)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/chess/proc/handle_castling(active_color, from_x, from_y, to_x, to_y)
|
||||
if(active_color == "w" && (game_flags & (GAME_FLAG_CASTLING_USED_PONE|GAME_FLAG_CHECK_PONE)))
|
||||
return FALSE
|
||||
if(active_color == "b" && (game_flags & (GAME_FLAG_CASTLING_USED_PTWO|GAME_FLAG_CHECK_PTWO)))
|
||||
return FALSE
|
||||
|
||||
var/king = current_board[from_y][from_x]
|
||||
|
||||
if(king != "wK" && king != "bK")
|
||||
return FALSE
|
||||
|
||||
if(abs(to_x - from_x) != 2)
|
||||
return FALSE
|
||||
|
||||
var/row = from_y
|
||||
var/rook_x
|
||||
var/rook
|
||||
var/col_start
|
||||
var/col_end
|
||||
|
||||
if(to_x > from_x)
|
||||
rook_x = GRID_SIZE
|
||||
rook = current_board[row][rook_x]
|
||||
if(rook != "wR" && rook != "bR")
|
||||
return FALSE
|
||||
col_start = from_x + 1
|
||||
col_end = to_x
|
||||
else
|
||||
rook_x = 1
|
||||
rook = current_board[row][rook_x]
|
||||
if(rook != "wR" && rook != "bR")
|
||||
return FALSE
|
||||
col_start = to_x
|
||||
col_end = from_x - 1
|
||||
|
||||
for(var/c = col_start to col_end)
|
||||
if(current_board[row][c] != null && c != from_x)
|
||||
return FALSE
|
||||
|
||||
var/opponent_color = active_color == "w" ? "b" : "w"
|
||||
if(!opponent_color)
|
||||
return FALSE
|
||||
|
||||
if(square_under_attack(opponent_color, from_x, from_y))
|
||||
return FALSE
|
||||
|
||||
var/step = (to_x > from_x ? 1 : -1)
|
||||
var/x = from_x
|
||||
while(x != to_x + step)
|
||||
if(x != from_x && x != to_x && square_under_attack(opponent_color, x, row))
|
||||
return FALSE
|
||||
x += step
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/chess/proc/do_castling(active_color, from_x, from_y, to_x, to_y)
|
||||
var/row = from_y
|
||||
var/king = current_board[from_y][from_x]
|
||||
var/rook_x
|
||||
var/rook
|
||||
var/rook_target_x
|
||||
|
||||
if(to_x > from_x)
|
||||
rook_x = GRID_SIZE
|
||||
rook = current_board[row][rook_x]
|
||||
rook_target_x = to_x - 1
|
||||
else
|
||||
rook_x = 1
|
||||
rook = current_board[row][rook_x]
|
||||
rook_target_x = to_x + 1
|
||||
|
||||
current_board[row][to_x] = king
|
||||
current_board[row][from_x] = null
|
||||
|
||||
current_board[row][rook_target_x] = rook
|
||||
current_board[row][rook_x] = null
|
||||
|
||||
if(active_color == "w")
|
||||
game_flags |= GAME_FLAG_CASTLING_USED_PONE
|
||||
else
|
||||
game_flags |= GAME_FLAG_CASTLING_USED_PTWO
|
||||
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/chess/proc/update_valid_moves()
|
||||
valid_moves.Cut()
|
||||
if(length(selected_figure))
|
||||
var/x = selected_figure[1]
|
||||
var/y = selected_figure[2]
|
||||
valid_moves = generate_valid_moves(x, y)
|
||||
|
||||
/datum/board_game/chess/proc/square_under_attack(opponent_color, x, y)
|
||||
for(var/row = 1; row <= GRID_SIZE; row++)
|
||||
for(var/col = 1; col <= GRID_SIZE; col++)
|
||||
var/piece = current_board[row][col]
|
||||
if(!piece)
|
||||
continue
|
||||
|
||||
if(piece[1] != opponent_color)
|
||||
continue
|
||||
|
||||
var/dx = x - col
|
||||
var/dy = y - row
|
||||
|
||||
switch(piece[2])
|
||||
if("P")
|
||||
var/dir = (piece[1] == "w" ? -1 : 1)
|
||||
if(dy == dir && abs(dx) == 1)
|
||||
return TRUE
|
||||
if("N")
|
||||
if(abs(dx) == 2 && abs(dy) == 1 || abs(dx) == 1 && abs(dy) == 2)
|
||||
return TRUE
|
||||
if("B")
|
||||
if(abs(dx) == abs(dy) && path_clear(col, row, x, y))
|
||||
return TRUE
|
||||
if("R")
|
||||
if((dx == 0 || dy == 0) && path_clear(col, row, x, y))
|
||||
return TRUE
|
||||
if("Q")
|
||||
if((dx == 0 || dy == 0 || abs(dx) == abs(dy)) && path_clear(col, row, x, y))
|
||||
return TRUE
|
||||
if("K")
|
||||
if(abs(dx) <= 1 && abs(dy) <= 1)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/chess/proc/path_clear(from_x, from_y, to_x, to_y)
|
||||
var/dx = to_x - from_x
|
||||
var/dy = to_y - from_y
|
||||
var/step_x = dx == 0 ? 0 : (dx / abs(dx))
|
||||
var/step_y = dy == 0 ? 0 : (dy / abs(dy))
|
||||
var/x = from_x + step_x
|
||||
var/y = from_y + step_y
|
||||
|
||||
while(x != to_x || y != to_y)
|
||||
if(!field_check(x, y))
|
||||
return FALSE
|
||||
if(current_board[y][x] != null)
|
||||
return FALSE
|
||||
x += step_x
|
||||
y += step_y
|
||||
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/chess/proc/validate_victory(active_color)
|
||||
var/opponent_color = active_color == "w" ? "b" : "w"
|
||||
|
||||
var/king_pos = locate_king(opponent_color)
|
||||
if(!king_pos)
|
||||
var/mob/winning_player = (active_color == "w" ? player_two?.resolve() : player_one?.resolve())
|
||||
winner = winning_player?.name
|
||||
game_state = GAME_OVER
|
||||
return
|
||||
|
||||
var/in_check = square_under_attack(active_color, king_pos[1], king_pos[2])
|
||||
|
||||
if(in_check)
|
||||
game_flags |= (opponent_color == "w" ? GAME_FLAG_CHECK_PONE : GAME_FLAG_CHECK_PTWO)
|
||||
else
|
||||
game_flags &= ~(opponent_color == "w" ? GAME_FLAG_CHECK_PONE : GAME_FLAG_CHECK_PTWO)
|
||||
|
||||
var/no_moves = TRUE
|
||||
for(var/row = 1 to GRID_SIZE)
|
||||
for(var/col = 1 to GRID_SIZE)
|
||||
var/piece = current_board[row][col]
|
||||
if(!piece)
|
||||
continue
|
||||
if(piece[1] != opponent_color)
|
||||
continue
|
||||
var/list/moves = generate_valid_moves(col, row)
|
||||
if(length(moves) > 0)
|
||||
no_moves = FALSE
|
||||
|
||||
if(in_check && no_moves)
|
||||
var/mob/winning_player = active_color == "w" ? player_one?.resolve() : player_two?.resolve()
|
||||
winner = winning_player?.name
|
||||
game_state = GAME_OVER
|
||||
return
|
||||
if(!in_check && no_moves)
|
||||
winner = null
|
||||
game_state = GAME_OVER_DRAW
|
||||
|
||||
/datum/board_game/chess/proc/locate_king(color)
|
||||
var/king_symbol = color + "K"
|
||||
for(var/row = 1 to GRID_SIZE)
|
||||
for(var/col = 1 to GRID_SIZE)
|
||||
if(current_board[row][col] == king_symbol)
|
||||
return list(col, row)
|
||||
return null
|
||||
|
||||
/datum/board_game/chess/proc/generate_valid_moves(x, y)
|
||||
var/piece = current_board[y][x]
|
||||
var/moves = list()
|
||||
if(!piece)
|
||||
return moves
|
||||
|
||||
for(var/row = 1 to GRID_SIZE)
|
||||
for(var/col = 1 to GRID_SIZE)
|
||||
if(row == y && col == x)
|
||||
continue
|
||||
if(can_move_piece(x, y, col, row))
|
||||
UNTYPED_LIST_ADD(moves, list(col, row))
|
||||
return moves
|
||||
|
||||
/datum/board_game/chess/proc/can_move_piece(from_x, from_y, to_x, to_y)
|
||||
var/piece = current_board[from_y][from_x]
|
||||
if(!piece)
|
||||
return FALSE
|
||||
|
||||
var/dx = to_x - from_x
|
||||
var/dy = to_y - from_y
|
||||
var/abs_dx = abs(dx)
|
||||
var/abs_dy = abs(dy)
|
||||
|
||||
if(dx == 0 && dy == 0)
|
||||
return FALSE
|
||||
|
||||
var/color = piece[1]
|
||||
var/target = current_board[to_y][to_x]
|
||||
if(target && target[1] == color)
|
||||
return FALSE
|
||||
|
||||
var/can_move = FALSE
|
||||
|
||||
switch(piece[2])
|
||||
if("P")
|
||||
var/dir = (color == "w" ? -1 : 1)
|
||||
if(dx == 0 && dy == dir && !target)
|
||||
can_move = TRUE
|
||||
else if(dx == 0 && dy == 2*dir && !target && current_board[from_y + dir][from_x] == null && ((color == "w" && from_y == 7) || (color == "b" && from_y == 2)))
|
||||
can_move = TRUE
|
||||
else if(abs_dx == 1 && dy == dir && target)
|
||||
can_move = TRUE
|
||||
else if(abs_dx == 1 && dy == dir && !target && length(last_double_pawn_move))
|
||||
if(last_double_pawn_move[1] == to_x && last_double_pawn_move[2] == from_y && last_double_pawn_move[3] == (color == "w" ? "b" : "w"))
|
||||
can_move = TRUE
|
||||
if("N")
|
||||
if((abs_dx == 2 && abs_dy == 1) || (abs_dx == 1 && abs_dy == 2))
|
||||
can_move = TRUE
|
||||
if("B")
|
||||
if(abs_dx == abs_dy && path_clear(from_x, from_y, to_x, to_y))
|
||||
can_move = TRUE
|
||||
if("R")
|
||||
if((dx == 0 || dy == 0) && path_clear(from_x, from_y, to_x, to_y))
|
||||
can_move = TRUE
|
||||
if("Q")
|
||||
if((dx == 0 || dy == 0 || abs_dx == abs_dy) && path_clear(from_x, from_y, to_x, to_y))
|
||||
can_move = TRUE
|
||||
if("K")
|
||||
if(abs_dx <= 1 && abs_dy <= 1)
|
||||
can_move = TRUE
|
||||
else if(abs_dx == 2 && dy == 0)
|
||||
can_move = handle_castling(color, from_x, from_y, to_x, to_y)
|
||||
|
||||
if(!can_move)
|
||||
return FALSE
|
||||
|
||||
var/orig_piece = current_board[to_y][to_x]
|
||||
current_board[to_y][to_x] = piece
|
||||
current_board[from_y][from_x] = null
|
||||
|
||||
var/king_pos = locate_king(color)
|
||||
if(!king_pos)
|
||||
current_board[from_y][from_x] = piece
|
||||
current_board[to_y][to_x] = orig_piece
|
||||
return FALSE
|
||||
|
||||
var/in_check = square_under_attack(color == "w" ? "b" : "w", king_pos[1], king_pos[2])
|
||||
|
||||
current_board[from_y][from_x] = piece
|
||||
current_board[to_y][to_x] = orig_piece
|
||||
|
||||
return !in_check
|
||||
|
||||
/datum/board_game/chess/proc/can_en_passant(from_x, from_y, to_x, to_y)
|
||||
if(!last_double_pawn_move)
|
||||
return FALSE
|
||||
return last_double_pawn_move[1] == to_x && last_double_pawn_move[2] == from_y && last_double_pawn_move[3] == (current_board[from_y][from_x][1] == "w" ? "b" : "w")
|
||||
|
||||
/datum/board_game/chess/proc/field_check(new_x_location, new_y_location)
|
||||
if(new_x_location < 1 || new_x_location > GRID_SIZE)
|
||||
return FALSE
|
||||
if(new_y_location < 1 || new_y_location > GRID_SIZE)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/chess/proc/validate_coords(list/params)
|
||||
var/x_loc = text2num(params["loc_x"])
|
||||
var/y_loc = text2num(params["loc_y"])
|
||||
|
||||
if(!isnum(x_loc) || !isnum(y_loc))
|
||||
return null
|
||||
|
||||
if(!field_check(x_loc, y_loc))
|
||||
return null
|
||||
|
||||
return list(x_loc, y_loc)
|
||||
|
||||
/datum/board_game/chess/proc/get_defaultboard()
|
||||
var/list/deep_copy = list()
|
||||
for(var/list/row in default_board)
|
||||
UNTYPED_LIST_ADD(deep_copy, row.Copy())
|
||||
return deep_copy
|
||||
|
||||
#undef GAME_PLAYER_ONE
|
||||
#undef GAME_PLAYER_TWO
|
||||
#undef GAME_OVER
|
||||
#undef GAME_OVER_DRAW
|
||||
#undef GRID_SIZE
|
||||
#undef GAME_FLAG_CHECK_PONE
|
||||
#undef GAME_FLAG_CHECK_PTWO
|
||||
#undef GAME_FLAG_CASTLING_USED_PONE
|
||||
#undef GAME_FLAG_CASTLING_USED_PTWO
|
||||
|
||||
#undef GAME_ACTION_NONE
|
||||
#undef GAME_ACTION_SELECT
|
||||
#undef GAME_ACTION_END_TURN
|
||||
@@ -0,0 +1,330 @@
|
||||
#define GAME_PLAYER_ONE 1
|
||||
#define GAME_PLAYER_TWO 2
|
||||
#define GAME_OVER 3
|
||||
#define GAME_OVER_DRAW 4
|
||||
|
||||
/obj/structure/casino_table/board_game/four_row
|
||||
name = GAME_FOUR_ROW
|
||||
desc = "A game where two players need to connect their chips in a row."
|
||||
icon_state = "gamble_four"
|
||||
game_ui = /datum/board_game/four_row
|
||||
|
||||
|
||||
/datum/board_game/four_row
|
||||
name = GAME_FOUR_ROW
|
||||
table_icon = "gamble_four"
|
||||
var/datum/weakref/player_one
|
||||
var/datum/weakref/player_two
|
||||
var/list/placed_chips_pone = list()
|
||||
var/list/placed_chips_ptwo = list()
|
||||
var/list/winning_tiles = list()
|
||||
var/grid_x_size = 7
|
||||
var/grid_y_size = 6
|
||||
var/win_count = 4
|
||||
var/player_one_color = "yellow"
|
||||
var/player_two_color = "red"
|
||||
var/winner
|
||||
var/static/list/possible_colors = list("red", "yellow", "green", "orange", "blue", "cyan")
|
||||
|
||||
/datum/board_game/four_row/Destroy(force)
|
||||
player_one = null
|
||||
player_two = null
|
||||
. = ..()
|
||||
|
||||
/datum/board_game/four_row/tgui_interact(mob/user, datum/tgui/ui)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "FourInARow", name)
|
||||
ui.open()
|
||||
|
||||
/datum/board_game/four_row/tgui_static_data(mob/user)
|
||||
return list(
|
||||
"colors" = possible_colors
|
||||
)
|
||||
|
||||
/datum/board_game/four_row/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
|
||||
var/mob/player_one_mob = player_one?.resolve()
|
||||
var/mob/player_two_mob = player_two?.resolve()
|
||||
|
||||
return list(
|
||||
"player_one" = player_one_mob,
|
||||
"player_two" = player_two_mob,
|
||||
"placed_chips_pone" = placed_chips_pone,
|
||||
"placed_chips_ptwo" = placed_chips_ptwo,
|
||||
"game_state" = game_state,
|
||||
"grid_x_size" = grid_x_size,
|
||||
"grid_y_size" = grid_y_size,
|
||||
"player_one_color" = player_one_color,
|
||||
"player_two_color" = player_two_color,
|
||||
"win_count" = win_count,
|
||||
"winner" = winner,
|
||||
"has_won" = winner == ui.user.name,
|
||||
"winning_tiles" = winning_tiles,
|
||||
)
|
||||
|
||||
/datum/board_game/four_row/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("be_player_one")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_one?.resolve() == ui.user)
|
||||
player_one = null
|
||||
return TRUE
|
||||
player_one = WEAKREF(ui.user)
|
||||
return TRUE
|
||||
if("be_player_two")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_two?.resolve() == ui.user)
|
||||
player_two = null
|
||||
return TRUE
|
||||
player_two = WEAKREF(ui.user)
|
||||
return TRUE
|
||||
if("swap_players")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
var/datum/weakref/temp_player = player_one
|
||||
player_one = player_two
|
||||
player_two = temp_player
|
||||
if("set_color_one")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_one?.resolve() != ui.user)
|
||||
return FALSE
|
||||
var/new_color = params["color"]
|
||||
if(new_color == player_two_color)
|
||||
return FALSE
|
||||
if(!(new_color in possible_colors))
|
||||
return FALSE
|
||||
player_one_color = new_color
|
||||
return TRUE
|
||||
if("set_color_two")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_two?.resolve() != ui.user)
|
||||
return FALSE
|
||||
var/new_color = params["color"]
|
||||
if(new_color == player_one_color)
|
||||
return FALSE
|
||||
if(!(new_color in possible_colors))
|
||||
return FALSE
|
||||
player_two_color = new_color
|
||||
return TRUE
|
||||
if("change_size")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
var/new_size = text2num(params["size"])
|
||||
return set_new_size(new_size)
|
||||
if("change_win")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
return change_win_count(text2num(params["count"]))
|
||||
if("clear_game")
|
||||
if(game_state == GAME_SETUP)
|
||||
return FALSE
|
||||
reset(TRUE)
|
||||
return TRUE
|
||||
if("start_game")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
game_state = GAME_PLAYER_ONE
|
||||
return TRUE
|
||||
if("play_again")
|
||||
if(game_state < GAME_OVER)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
reset()
|
||||
return TRUE
|
||||
if("play_again_swapped")
|
||||
if(game_state < GAME_OVER)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
reset()
|
||||
var/datum/weakref/temp_player = player_one
|
||||
player_one = player_two
|
||||
player_two = temp_player
|
||||
return TRUE
|
||||
if("game_action")
|
||||
if(ui.user == player_one?.resolve() && game_state == GAME_PLAYER_ONE)
|
||||
if(player_actions(params["action"], params["data"], ui.user))
|
||||
if(game_state < GAME_OVER)
|
||||
game_state = GAME_PLAYER_TWO
|
||||
return TRUE
|
||||
if(ui.user == player_two?.resolve() && game_state == GAME_PLAYER_TWO)
|
||||
if(player_actions(params["action"], params["data"], ui.user))
|
||||
if(game_state < GAME_OVER)
|
||||
game_state = GAME_PLAYER_ONE
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/four_row/proc/change_win_count(new_count)
|
||||
if(!isnum(new_count))
|
||||
return FALSE
|
||||
if(new_count < 4 || new_count > 5)
|
||||
return FALSE
|
||||
win_count = new_count
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/four_row/proc/reset(full)
|
||||
placed_chips_pone.Cut()
|
||||
placed_chips_ptwo.Cut()
|
||||
winning_tiles.Cut()
|
||||
winner = null
|
||||
if(full)
|
||||
game_state = GAME_SETUP
|
||||
player_one = null
|
||||
player_two = null
|
||||
else
|
||||
game_state = GAME_PLAYER_ONE
|
||||
|
||||
/datum/board_game/four_row/proc/player_actions(action, list/params, mob/user)
|
||||
switch(action)
|
||||
if("place_chip")
|
||||
var/list/validated_data = validate_coords(params)
|
||||
if(!validated_data)
|
||||
return FALSE
|
||||
var/x_loc = validated_data[2]
|
||||
|
||||
var/target_y = 0
|
||||
for(var/y = grid_y_size; y >= 1; y--)
|
||||
var/key = "[x_loc],[y]"
|
||||
if(!placed_chips_pone[key] && !placed_chips_ptwo[key])
|
||||
target_y = y
|
||||
break
|
||||
|
||||
if(target_y == 0)
|
||||
return FALSE
|
||||
|
||||
var/key = "[x_loc],[target_y]"
|
||||
|
||||
if(game_state == GAME_PLAYER_ONE)
|
||||
placed_chips_pone[key] = TRUE
|
||||
else
|
||||
placed_chips_ptwo[key] = TRUE
|
||||
|
||||
validate_victory(x_loc, target_y, user.name)
|
||||
return TRUE
|
||||
|
||||
|
||||
/datum/board_game/four_row/proc/has_chip(list/player_list, x, y)
|
||||
return player_list["[x],[y]"]
|
||||
|
||||
/datum/board_game/four_row/proc/collect_direction(list/player_list, start_x, start_y, dx, dy)
|
||||
var/list/tiles = list()
|
||||
var/x = start_x
|
||||
var/y = start_y
|
||||
|
||||
while(field_check(x, y) && has_chip(player_list, x, y))
|
||||
tiles += "[x],[y]"
|
||||
x += dx
|
||||
y += dy
|
||||
|
||||
return tiles
|
||||
|
||||
/datum/board_game/four_row/proc/validate_victory(x, y, user_name)
|
||||
var/list/current_list
|
||||
|
||||
if(game_state == GAME_PLAYER_ONE)
|
||||
current_list = placed_chips_pone
|
||||
else
|
||||
current_list = placed_chips_ptwo
|
||||
|
||||
var/list/h1 = collect_direction(current_list, x, y, 1, 0)
|
||||
var/list/h2 = collect_direction(current_list, x, y, -1, 0)
|
||||
var/list/horizontal = h1 + h2
|
||||
horizontal -= "[x],[y]"
|
||||
|
||||
if(length(horizontal) >= win_count)
|
||||
winning_tiles = horizontal
|
||||
game_state = GAME_OVER
|
||||
winner = user_name
|
||||
return
|
||||
|
||||
var/list/v1 = collect_direction(current_list, x, y, 0, 1)
|
||||
var/list/v2 = collect_direction(current_list, x, y, 0, -1)
|
||||
var/list/vertical = v1 + v2
|
||||
vertical -= "[x],[y]"
|
||||
|
||||
if(length(vertical) >= win_count)
|
||||
winning_tiles = vertical
|
||||
game_state = GAME_OVER
|
||||
winner = user_name
|
||||
return
|
||||
|
||||
var/list/d1 = collect_direction(current_list, x, y, 1, 1)
|
||||
var/list/d2 = collect_direction(current_list, x, y, -1, -1)
|
||||
var/list/diag1 = d1 + d2
|
||||
diag1 -= "[x],[y]"
|
||||
|
||||
if(length(diag1) >= win_count)
|
||||
winning_tiles = diag1
|
||||
game_state = GAME_OVER
|
||||
winner = user_name
|
||||
return
|
||||
|
||||
var/list/d3 = collect_direction(current_list, x, y, 1, -1)
|
||||
var/list/d4 = collect_direction(current_list, x, y, -1, 1)
|
||||
var/list/diag2 = d3 + d4
|
||||
diag2 -= "[x],[y]"
|
||||
|
||||
if(length(diag2) >= win_count)
|
||||
winning_tiles = diag2
|
||||
game_state = GAME_OVER
|
||||
winner = user_name
|
||||
return
|
||||
check_draw()
|
||||
|
||||
/datum/board_game/four_row/proc/check_draw()
|
||||
for(var/x = 1 to grid_x_size)
|
||||
for(var/y = 1 to grid_y_size)
|
||||
var/key = "[x],[y]"
|
||||
if(!placed_chips_pone[key] && !placed_chips_ptwo[key])
|
||||
return FALSE
|
||||
|
||||
winner = null
|
||||
game_state = GAME_OVER_DRAW
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/four_row/proc/set_new_size(new_size)
|
||||
if(!isnum(new_size))
|
||||
return FALSE
|
||||
if(new_size < 5 || new_size > 10)
|
||||
return FALSE
|
||||
grid_x_size = new_size
|
||||
grid_y_size = new_size - 1
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/four_row/proc/field_check(new_x_location, new_y_location)
|
||||
if(new_x_location < 1 || new_x_location > grid_x_size)
|
||||
return FALSE
|
||||
if(new_y_location < 1 || new_y_location > grid_y_size)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/four_row/proc/validate_coords(list/params)
|
||||
var/x_loc = text2num(params["loc_x"])
|
||||
var/y_loc = text2num(params["loc_y"])
|
||||
|
||||
if(!isnum(x_loc) || !isnum(y_loc))
|
||||
return null
|
||||
|
||||
if(!field_check(x_loc, y_loc))
|
||||
return null
|
||||
|
||||
var/key = "[x_loc],[y_loc]"
|
||||
return list(key, x_loc, y_loc)
|
||||
|
||||
#undef GAME_PLAYER_ONE
|
||||
#undef GAME_PLAYER_TWO
|
||||
#undef GAME_OVER
|
||||
#undef GAME_OVER_DRAW
|
||||
@@ -0,0 +1,421 @@
|
||||
#define GAME_PLAYER_ONE 1
|
||||
#define GAME_PLAYER_TWO 2
|
||||
#define GAME_OVER 3
|
||||
#define GAME_OVER_DRAW 4
|
||||
#define NODE_COUNT 24
|
||||
|
||||
#define GAME_ACTION_NONE 0
|
||||
#define GAME_ACTION_SELECT 1
|
||||
#define GAME_ACTION_END_TURN 2
|
||||
|
||||
#define GAME_PHASE_PLACING 0
|
||||
#define GAME_PHASE_MOVING 1
|
||||
#define GAME_PHASE_REMOVING 2
|
||||
#define GAME_PHASE_NONE 3
|
||||
|
||||
/obj/structure/casino_table/board_game/nine_mens
|
||||
name = GAME_NINE_MENS_MORRIS
|
||||
desc = "A classic nine_mens game."
|
||||
icon_state = "gamble_ninemen"
|
||||
game_ui = /datum/board_game/nine_mens
|
||||
|
||||
/datum/board_game/nine_mens
|
||||
name = GAME_NINE_MENS_MORRIS
|
||||
table_icon = "gamble_ninemen"
|
||||
var/datum/weakref/player_one
|
||||
var/datum/weakref/player_two
|
||||
var/player_one_time = 0
|
||||
var/player_two_time = 0
|
||||
var/list/current_board = list(
|
||||
null, null, null,
|
||||
null, null, null,
|
||||
null, null, null,
|
||||
null, null, null,
|
||||
null, null, null,
|
||||
null, null, null,
|
||||
null, null, null,
|
||||
null, null, null
|
||||
)
|
||||
var/static/list/adjacent = list(
|
||||
list(2, 10),
|
||||
list(1, 3, 5),
|
||||
list(2, 15),
|
||||
list(5, 11),
|
||||
list(2, 4, 6, 8),
|
||||
list(5, 14),
|
||||
list(8, 12),
|
||||
list(5, 7, 9),
|
||||
list(8, 13),
|
||||
list(1, 11, 22),
|
||||
list(4, 10, 12, 19),
|
||||
list(7, 11, 16),
|
||||
list(9, 14, 18),
|
||||
list(6, 13, 15, 21),
|
||||
list(3, 14, 24),
|
||||
list(12, 17),
|
||||
list(16, 18, 20),
|
||||
list(13, 17),
|
||||
list(11, 20),
|
||||
list(17, 19, 21, 23),
|
||||
list(14, 20),
|
||||
list(10, 23),
|
||||
list(20, 22, 24),
|
||||
list(15, 23)
|
||||
)
|
||||
var/static/list/mills = list(
|
||||
list(1,2,3),
|
||||
list(4,5,6),
|
||||
list(7,8,9),
|
||||
list(10,11,12),
|
||||
list(13,14,15),
|
||||
list(16,17,18),
|
||||
list(19,20,21),
|
||||
list(22,23,24),
|
||||
|
||||
list(1,10,22),
|
||||
list(4,11,19),
|
||||
list(7,12,16),
|
||||
list(2,5,8),
|
||||
list(17,20,23),
|
||||
list(9,13,18),
|
||||
list(6,14,21),
|
||||
list(3,15,24)
|
||||
)
|
||||
var/list/valid_moves = list()
|
||||
var/list/valid_removes = list()
|
||||
var/selected_node
|
||||
var/turn_start_time = 0
|
||||
var/pone_pieces = 9
|
||||
var/ptwo_pieces = 9
|
||||
var/winner
|
||||
var/phase = GAME_PHASE_PLACING
|
||||
|
||||
/datum/board_game/nine_mens/Destroy(force)
|
||||
player_one = null
|
||||
player_two = null
|
||||
. = ..()
|
||||
|
||||
/datum/board_game/nine_mens/tgui_interact(mob/user, datum/tgui/ui)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "NineMen", name)
|
||||
ui.open()
|
||||
|
||||
/datum/board_game/nine_mens/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
|
||||
var/mob/player_one_mob = player_one?.resolve()
|
||||
var/mob/player_two_mob = player_two?.resolve()
|
||||
|
||||
return list(
|
||||
"player_one" = player_one_mob,
|
||||
"player_two" = player_two_mob,
|
||||
"player_one_time" = player_one_time + (game_state == GAME_PLAYER_ONE ? world.time - turn_start_time : 0),
|
||||
"player_two_time" = player_two_time + (game_state == GAME_PLAYER_TWO ? world.time - turn_start_time : 0),
|
||||
"current_board" = current_board,
|
||||
"selected_node" = selected_node,
|
||||
"valid_moves" = valid_moves,
|
||||
"valid_removes" = valid_removes,
|
||||
"game_state" = game_state,
|
||||
"winner" = winner,
|
||||
"has_won" = winner == ui.user.name,
|
||||
"phase" = phase,
|
||||
"pone_pieces" = pone_pieces,
|
||||
"ptwo_pieces" = ptwo_pieces
|
||||
)
|
||||
|
||||
/datum/board_game/nine_mens/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("be_player_one")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_one?.resolve() == ui.user)
|
||||
player_one = null
|
||||
return TRUE
|
||||
player_one = WEAKREF(ui.user)
|
||||
return TRUE
|
||||
if("be_player_two")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_two?.resolve() == ui.user)
|
||||
player_two = null
|
||||
return TRUE
|
||||
player_two = WEAKREF(ui.user)
|
||||
return TRUE
|
||||
if("swap_players")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
var/datum/weakref/temp_player = player_one
|
||||
player_one = player_two
|
||||
player_two = temp_player
|
||||
if("clear_game")
|
||||
if(game_state == GAME_SETUP)
|
||||
return FALSE
|
||||
reset(TRUE)
|
||||
return TRUE
|
||||
if("start_game")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
game_state = GAME_PLAYER_ONE
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if("play_again")
|
||||
if(game_state < GAME_OVER)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
reset()
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if("play_again_swapped")
|
||||
if(game_state < GAME_OVER)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
reset()
|
||||
var/datum/weakref/temp_player = player_one
|
||||
player_one = player_two
|
||||
player_two = temp_player
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if("game_action")
|
||||
if(ui.user == player_one?.resolve() && game_state == GAME_PLAYER_ONE)
|
||||
var/game_action = player_actions(params["action"], params["data"], ui.user, "w")
|
||||
if(game_action)
|
||||
if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN)
|
||||
game_state = GAME_PLAYER_TWO
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
if(ui.user == player_two?.resolve() && game_state == GAME_PLAYER_TWO)
|
||||
var/game_action = player_actions(params["action"], params["data"], ui.user, "b")
|
||||
if(game_action)
|
||||
if(game_state < GAME_OVER && game_action == GAME_ACTION_END_TURN)
|
||||
game_state = GAME_PLAYER_ONE
|
||||
turn_start_time = world.time
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/nine_mens/proc/reset(full)
|
||||
winner = null
|
||||
player_one_time = 0
|
||||
player_two_time = 0
|
||||
selected_node = null
|
||||
valid_moves.Cut()
|
||||
valid_removes.Cut()
|
||||
pone_pieces = 9
|
||||
ptwo_pieces = 9
|
||||
phase = GAME_PHASE_PLACING
|
||||
for(var/i = 1 to NODE_COUNT)
|
||||
current_board[i] = null
|
||||
if(full)
|
||||
game_state = GAME_SETUP
|
||||
player_one = null
|
||||
player_two = null
|
||||
else
|
||||
game_state = GAME_PLAYER_ONE
|
||||
|
||||
/datum/board_game/nine_mens/proc/player_actions(action, list/params, mob/user, active_color)
|
||||
switch(action)
|
||||
if("select_figure")
|
||||
var/node = validate_node(params)
|
||||
if(!node)
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
var/piece = current_board[node]
|
||||
if(!piece || piece[1] != active_color)
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
selected_node = node
|
||||
update_valid_moves()
|
||||
return GAME_ACTION_SELECT
|
||||
|
||||
if("move_figure")
|
||||
var/target_node = validate_node(params)
|
||||
if(!target_node)
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
switch(phase)
|
||||
if(GAME_PHASE_PLACING)
|
||||
if(current_board[target_node])
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
current_board[target_node] = active_color
|
||||
|
||||
if(active_color == "w")
|
||||
if(pone_pieces > 0)
|
||||
pone_pieces--
|
||||
else
|
||||
if(ptwo_pieces > 0)
|
||||
ptwo_pieces--
|
||||
|
||||
if(check_for_mill(target_node, active_color))
|
||||
phase = GAME_PHASE_REMOVING
|
||||
update_valid_removals(active_color == "w" ? "b" : "w")
|
||||
else
|
||||
if((ptwo_pieces && active_color == "w") || (pone_pieces && active_color == "b"))
|
||||
phase = GAME_PHASE_PLACING
|
||||
else
|
||||
phase = GAME_PHASE_MOVING
|
||||
return phase == GAME_PHASE_REMOVING ? GAME_ACTION_SELECT : GAME_ACTION_END_TURN
|
||||
|
||||
if(GAME_PHASE_MOVING)
|
||||
if(!selected_node)
|
||||
return GAME_ACTION_NONE
|
||||
if(!valid_move(active_color, selected_node, target_node))
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
current_board[target_node] = current_board[selected_node]
|
||||
current_board[selected_node] = null
|
||||
selected_node = null
|
||||
valid_moves.Cut()
|
||||
validate_victory(active_color)
|
||||
if(game_state < GAME_OVER)
|
||||
if(check_for_mill(target_node, active_color))
|
||||
phase = GAME_PHASE_REMOVING
|
||||
update_valid_removals(active_color == "w" ? "b" : "w")
|
||||
else
|
||||
phase = GAME_PHASE_MOVING
|
||||
else
|
||||
phase = GAME_PHASE_NONE
|
||||
|
||||
return phase == GAME_PHASE_REMOVING ? GAME_ACTION_SELECT : GAME_ACTION_END_TURN
|
||||
|
||||
if(GAME_PHASE_REMOVING)
|
||||
if(!valid_remove(target_node))
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
current_board[target_node] = null
|
||||
|
||||
valid_removes.Cut()
|
||||
validate_victory(active_color)
|
||||
if(game_state < GAME_OVER)
|
||||
if((ptwo_pieces && active_color == "w") || (pone_pieces && active_color == "b"))
|
||||
phase = GAME_PHASE_PLACING
|
||||
else
|
||||
phase = GAME_PHASE_MOVING
|
||||
else
|
||||
phase = GAME_PHASE_NONE
|
||||
|
||||
return GAME_ACTION_END_TURN
|
||||
|
||||
return GAME_ACTION_NONE
|
||||
|
||||
/datum/board_game/nine_mens/proc/update_valid_moves()
|
||||
valid_moves.Cut()
|
||||
if(selected_node)
|
||||
valid_moves = generate_valid_moves(selected_node)
|
||||
|
||||
/datum/board_game/nine_mens/proc/update_valid_removals(opponent_color)
|
||||
valid_removes.Cut()
|
||||
for(var/i = 1 to NODE_COUNT)
|
||||
if(current_board[i] && current_board[i][1] == opponent_color)
|
||||
if(!check_for_mill(i, opponent_color))
|
||||
valid_removes += i
|
||||
if(!length(valid_removes))
|
||||
for(var/i = 1 to NODE_COUNT)
|
||||
if(current_board[i] && current_board[i][1] == opponent_color)
|
||||
valid_removes += i
|
||||
|
||||
/datum/board_game/nine_mens/proc/valid_remove(picked_node)
|
||||
if(!picked_node || !current_board[picked_node] || !(picked_node in valid_removes))
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/nine_mens/proc/check_for_mill(node, color)
|
||||
for(var/mill in mills)
|
||||
if(node in mill)
|
||||
var/mill_complete = TRUE
|
||||
for(var/mill_node in mill)
|
||||
if(!current_board[mill_node] || current_board[mill_node][1] != color)
|
||||
mill_complete = FALSE
|
||||
break
|
||||
if(mill_complete)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/nine_mens/proc/valid_move(active_color, from_node, to_node)
|
||||
if(current_board[to_node])
|
||||
return FALSE
|
||||
|
||||
if(count_pieces(active_color) == 3)
|
||||
return TRUE
|
||||
|
||||
for(var/adj in adjacent[from_node])
|
||||
if(adj == to_node)
|
||||
return TRUE
|
||||
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/nine_mens/proc/validate_victory(active_color)
|
||||
var/opponent = (active_color == "w") ? "b" : "w"
|
||||
|
||||
if(count_pieces(opponent) < 3 || !has_moves(opponent))
|
||||
var/mob/winning_player = (active_color == "w" ? player_one?.resolve() : player_two?.resolve())
|
||||
winner = winning_player?.name
|
||||
game_state = GAME_OVER
|
||||
|
||||
/datum/board_game/nine_mens/proc/generate_valid_moves(node)
|
||||
var/list/moves = list()
|
||||
|
||||
if(!current_board[node])
|
||||
return moves
|
||||
|
||||
var/color = current_board[node][1]
|
||||
|
||||
if(count_pieces(color) == 3)
|
||||
for(var/i = 1 to NODE_COUNT)
|
||||
if(!current_board[i])
|
||||
moves += i
|
||||
return moves
|
||||
|
||||
for(var/adj in adjacent[node])
|
||||
if(!current_board[adj])
|
||||
moves += adj
|
||||
|
||||
return moves
|
||||
|
||||
/datum/board_game/nine_mens/proc/count_pieces(color)
|
||||
var/count = color == "w" ? pone_pieces : ptwo_pieces
|
||||
for(var/i = 1 to NODE_COUNT)
|
||||
if(current_board[i] && current_board[i][1] == color)
|
||||
count++
|
||||
return count
|
||||
|
||||
/datum/board_game/nine_mens/proc/has_moves(color)
|
||||
for(var/i = 1 to NODE_COUNT)
|
||||
if(current_board[i] && current_board[i][1] == color)
|
||||
if(length(generate_valid_moves(i)))
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/nine_mens/proc/validate_node(list/params)
|
||||
var/node = text2num(params["node_number"])
|
||||
|
||||
if(!isnum(node))
|
||||
return null
|
||||
|
||||
if(node < 1 || node > 24)
|
||||
return null
|
||||
|
||||
return node
|
||||
|
||||
#undef GAME_PLAYER_ONE
|
||||
#undef GAME_PLAYER_TWO
|
||||
#undef GAME_OVER
|
||||
#undef GAME_OVER_DRAW
|
||||
#undef NODE_COUNT
|
||||
|
||||
#undef GAME_ACTION_NONE
|
||||
#undef GAME_ACTION_SELECT
|
||||
#undef GAME_ACTION_END_TURN
|
||||
|
||||
#undef GAME_PHASE_PLACING
|
||||
#undef GAME_PHASE_MOVING
|
||||
#undef GAME_PHASE_REMOVING
|
||||
#undef GAME_PHASE_NONE
|
||||
@@ -0,0 +1,72 @@
|
||||
/obj/structure/casino_table/board_game/rpg_dice
|
||||
name = GAME_RGP_DICE
|
||||
desc = "A set of dice to roll with your friends."
|
||||
icon_state = "gamble_dice"
|
||||
game_ui = /datum/board_game/rpg_dice
|
||||
|
||||
/datum/board_game/rpg_dice
|
||||
name = GAME_RGP_DICE
|
||||
table_icon = "gamble_dice"
|
||||
var/list/last_rolls = list()
|
||||
|
||||
/datum/board_game/rpg_dice/tgui_interact(mob/user, datum/tgui/ui)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "RpgDice", name)
|
||||
ui.open()
|
||||
|
||||
/datum/board_game/rpg_dice/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
|
||||
return list(
|
||||
"last_rolls" = last_rolls,
|
||||
)
|
||||
|
||||
/datum/board_game/rpg_dice/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("roll_dice")
|
||||
var/dice_size = params["dice_size"]
|
||||
if(!isnum(dice_size) || dice_size > 10000)
|
||||
return FALSE
|
||||
var/dice_count = params["dice_count"]
|
||||
if(!isnum(dice_count) || dice_count < 1 || dice_count > 10)
|
||||
dice_count = 1
|
||||
var/modifier = params["dice_mod"]
|
||||
var/list/results = list()
|
||||
var/sum = 0
|
||||
if(!isnum(modifier))
|
||||
modifier = 0
|
||||
var/apply_to_all = params["mod_all"]
|
||||
for(var/dice in 1 to dice_count)
|
||||
var/result = rand(1, dice_size)
|
||||
UNTYPED_LIST_ADD(results, list("result" = result, "state" = check_crit(1, dice_size, result)))
|
||||
sum += result
|
||||
sum += apply_to_all ? modifier * dice_count : modifier
|
||||
UNTYPED_LIST_ADD(last_rolls, list("player" = ui.user, "count" = dice_count, "size" = dice_size, "results" = results, "mod" = modifier, "apply_to_all" = apply_to_all, "sum" = sum))
|
||||
return TRUE
|
||||
/* Requires rust g dice
|
||||
if("custom_roll")
|
||||
var/input = params["custom"]
|
||||
if(length(input) > 200)
|
||||
to_chat(ui.user, span_warning("Your input was too long."))
|
||||
return FALSE
|
||||
var/static/regex/valid_dice_roll = regex(@"^(\d+d\d+(\s*[\+\-\*\/]\s*\d+)?(\s*[\+\-\*\/]\s*\d+d\d+(\s*[\+\-\*\/]\s*\d+)?)*)$")
|
||||
if(!findtext(input, valid_dice_roll))
|
||||
to_chat(ui.user, span_warning("Invalid input, please follow the common pattern e.g.: 2d6 + 3d10 + 5"))
|
||||
return FALSE
|
||||
var/result = rustg_roll_dice("[input]")
|
||||
UNTYPED_LIST_ADD(last_rolls, list("player" = ui.user, "count" = input, "sum" = result))
|
||||
return TRUE
|
||||
*/
|
||||
if("clear_history")
|
||||
last_rolls.Cut()
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/rpg_dice/proc/check_crit(low, max, result)
|
||||
if(result == low)
|
||||
return 1
|
||||
if(result == max)
|
||||
return 2
|
||||
return 0
|
||||
@@ -0,0 +1,436 @@
|
||||
#define GAME_PLACE_SHIPS 1
|
||||
#define GAME_PLAYER_ONE 2
|
||||
#define GAME_PLAYER_TWO 3
|
||||
#define GAME_OVER 4
|
||||
#define GRID_SIZE 10
|
||||
#define PLAYER_ONE_PLACED_SHIPS 0x1
|
||||
#define PLAYER_TWO_PLACED_SHIPS 0x2
|
||||
|
||||
/obj/structure/casino_table/board_game/space_battle
|
||||
name = GAME_SPACE_BATTLE
|
||||
desc = "A game with the goal to destroy the opponent's ships."
|
||||
icon_state = "gamble_space"
|
||||
game_ui = /datum/board_game/space_battle
|
||||
|
||||
/datum/board_game/space_battle
|
||||
name = GAME_SPACE_BATTLE
|
||||
table_icon = "gamble_space"
|
||||
var/datum/weakref/player_one
|
||||
var/datum/weakref/player_two
|
||||
var/list/ship_count_pone = list()
|
||||
var/list/ship_count_ptwo = list()
|
||||
var/list/shots_fired_pone = list()
|
||||
var/list/shots_fired_ptwo = list()
|
||||
var/list/ships_placed_pone = list()
|
||||
var/list/ships_placed_ptwo = list()
|
||||
var/list/destroyed_ships_pone = list()
|
||||
var/list/destroyed_ships_ptwo = list()
|
||||
var/static/list/total_ships = list(
|
||||
"Carrier" = 1,
|
||||
"Cruiser" = 2,
|
||||
"Corvette" = 3,
|
||||
"Figher" = 4,
|
||||
)
|
||||
var/static/list/ship_sizes = list(
|
||||
"Carrier" = 6,
|
||||
"Cruiser" = 4,
|
||||
"Corvette" = 3,
|
||||
"Figher" = 2,
|
||||
)
|
||||
var/winner
|
||||
var/ships_have_been_placed = NONE
|
||||
|
||||
/datum/board_game/space_battle/Destroy(force)
|
||||
player_one = null
|
||||
player_two = null
|
||||
. = ..()
|
||||
|
||||
/datum/board_game/space_battle/tgui_interact(mob/user, datum/tgui/ui)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "SpaceBattle", name)
|
||||
ui.open()
|
||||
|
||||
/datum/board_game/space_battle/tgui_static_data(mob/user)
|
||||
return list(
|
||||
"ship_sizes" = ship_sizes,
|
||||
"total_ships" = total_ships
|
||||
)
|
||||
|
||||
/datum/board_game/space_battle/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
|
||||
var/mob/player_one_mob = player_one?.resolve()
|
||||
var/mob/player_two_mob = player_two?.resolve()
|
||||
|
||||
var/list/visible_ships = list()
|
||||
if(ui.user == player_one_mob || game_state == GAME_OVER)
|
||||
visible_ships += ships_placed_pone
|
||||
if(ui.user == player_two_mob || game_state == GAME_OVER)
|
||||
visible_ships += ships_placed_ptwo
|
||||
|
||||
return list(
|
||||
"current_player" = ui.user,
|
||||
"player_one" = player_one_mob,
|
||||
"player_two" = player_two_mob,
|
||||
"all_placed" = ships_have_been_placed == (PLAYER_ONE_PLACED_SHIPS | PLAYER_TWO_PLACED_SHIPS),
|
||||
"shots_fired_pone" = shots_fired_pone,
|
||||
"shots_fired_ptwo" = shots_fired_ptwo,
|
||||
"destroyed_ships_pone" = destroyed_ships_pone,
|
||||
"destroyed_ships_ptwo" = destroyed_ships_ptwo,
|
||||
"visible_ships" = visible_ships,
|
||||
"ship_count_pone" = ship_count_pone,
|
||||
"ship_count_ptwo" = ship_count_ptwo,
|
||||
"game_state" = game_state,
|
||||
"winner" = winner,
|
||||
"has_won" = winner == ui.user.name
|
||||
)
|
||||
|
||||
/datum/board_game/space_battle/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("be_player_one")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_one?.resolve() == ui.user)
|
||||
player_one = null
|
||||
return TRUE
|
||||
player_one = WEAKREF(ui.user)
|
||||
return TRUE
|
||||
if("be_player_two")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(player_two?.resolve() == ui.user)
|
||||
player_two = null
|
||||
return TRUE
|
||||
player_two = WEAKREF(ui.user)
|
||||
return TRUE
|
||||
if("swap_players")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
var/datum/weakref/temp_player = player_one
|
||||
player_one = player_two
|
||||
player_two = temp_player
|
||||
if("clear_game")
|
||||
if(game_state == GAME_SETUP)
|
||||
return FALSE
|
||||
reset(TRUE)
|
||||
return TRUE
|
||||
if("prepare_game")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
var/mob/player_one_mob = player_one?.resolve()
|
||||
var/mob/player_two_mob = player_two?.resolve()
|
||||
if(!player_one_mob || !player_two_mob)
|
||||
return FALSE
|
||||
game_state = GAME_PLACE_SHIPS
|
||||
ship_count_pone = get_remaining_ships(1)
|
||||
ship_count_ptwo = get_remaining_ships(2)
|
||||
return TRUE
|
||||
if("start_game")
|
||||
if(game_state != GAME_PLACE_SHIPS)
|
||||
return FALSE
|
||||
if(!(ships_have_been_placed == (PLAYER_ONE_PLACED_SHIPS | PLAYER_TWO_PLACED_SHIPS)))
|
||||
return FALSE
|
||||
var/mob/player_one_mob = player_one?.resolve()
|
||||
var/mob/player_two_mob = player_two?.resolve()
|
||||
if(!player_one_mob || !player_two_mob)
|
||||
return FALSE
|
||||
ship_count_pone = get_alive_ships(1)
|
||||
ship_count_ptwo = get_alive_ships(2)
|
||||
game_state = GAME_PLAYER_ONE
|
||||
return TRUE
|
||||
if("play_again")
|
||||
if(game_state < GAME_OVER)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
reset()
|
||||
return TRUE
|
||||
if("play_again_swapped")
|
||||
if(game_state < GAME_OVER)
|
||||
return FALSE
|
||||
if(!player_one?.resolve() || !player_two?.resolve())
|
||||
return FALSE
|
||||
reset()
|
||||
var/datum/weakref/temp_player = player_one
|
||||
player_one = player_two
|
||||
player_two = temp_player
|
||||
return TRUE
|
||||
if("place_ship")
|
||||
if(game_state != GAME_PLACE_SHIPS)
|
||||
return FALSE
|
||||
var/mob/player_one_mob = player_one?.resolve()
|
||||
var/mob/player_two_mob = player_two?.resolve()
|
||||
if(!player_one_mob || !player_two_mob)
|
||||
return FALSE
|
||||
|
||||
var/list/ship_data = params["ship"]
|
||||
if(!ship_data["name"] || !ship_data["coords"])
|
||||
return FALSE
|
||||
|
||||
var/player = ship_data["player"]
|
||||
if(!player)
|
||||
return FALSE
|
||||
|
||||
if(player == 1 && ui.user != player_one_mob)
|
||||
return FALSE
|
||||
if(player == 2 && ui.user != player_two_mob)
|
||||
return FALSE
|
||||
|
||||
var/allowed = total_ships[ship_data["name"]]
|
||||
if(!allowed)
|
||||
return FALSE
|
||||
|
||||
var/list/current_ships
|
||||
if(player == 1)
|
||||
current_ships = ships_placed_pone
|
||||
else
|
||||
current_ships = ships_placed_ptwo
|
||||
|
||||
var/count = 0
|
||||
for(var/list/existing in current_ships)
|
||||
if(existing["name"] == ship_data["name"])
|
||||
count++
|
||||
|
||||
if(count >= allowed)
|
||||
return FALSE
|
||||
|
||||
var/list/new_coords = ship_data["coords"]
|
||||
for(var/list/new_coord in new_coords)
|
||||
if(!isnum(new_coord[1]) || !isnum(new_coord[2]))
|
||||
return FALSE
|
||||
if(!field_check(new_coord[1], new_coord[2]))
|
||||
return FALSE
|
||||
|
||||
for(var/list/existing_ship in current_ships)
|
||||
for(var/list/coord in existing_ship["coords"])
|
||||
for(var/list/new_coord in new_coords)
|
||||
if(coord[1] == new_coord[1] && coord[2] == new_coord[2])
|
||||
return FALSE
|
||||
|
||||
UNTYPED_LIST_ADD(current_ships, ship_data)
|
||||
if(player == 1)
|
||||
ship_count_pone = get_remaining_ships(1)
|
||||
else
|
||||
ship_count_ptwo = get_remaining_ships(2)
|
||||
|
||||
return TRUE
|
||||
if("remove_ship")
|
||||
if(game_state != GAME_PLACE_SHIPS)
|
||||
return FALSE
|
||||
var/mob/player_one_mob = player_one?.resolve()
|
||||
var/mob/player_two_mob = player_two?.resolve()
|
||||
if(!player_one_mob || !player_two_mob)
|
||||
return FALSE
|
||||
|
||||
var/player = params["player"]
|
||||
if(player == 1 && ui.user != player_one_mob)
|
||||
return FALSE
|
||||
if(player == 2 && ui.user != player_two_mob)
|
||||
return FALSE
|
||||
|
||||
var/list/ships
|
||||
if(player == 1)
|
||||
ships = ships_placed_pone
|
||||
else
|
||||
ships = ships_placed_ptwo
|
||||
|
||||
var/loc_x = params["loc_x"]
|
||||
var/loc_y = params["loc_y"]
|
||||
for(var/i in length(ships) to 1 step -1)
|
||||
var/list/ship = ships[i]
|
||||
for(var/list/coord in ship["coords"])
|
||||
if(coord[1] == loc_x && coord[2] == loc_y)
|
||||
ships.Cut(i, i + 1)
|
||||
if(player == 1)
|
||||
ship_count_pone = get_remaining_ships(1)
|
||||
else
|
||||
ship_count_ptwo = get_remaining_ships(2)
|
||||
return TRUE
|
||||
return FALSE
|
||||
if("game_action")
|
||||
if(ui.user == player_one?.resolve() && game_state == GAME_PLAYER_ONE)
|
||||
if(params["data"]["player"] == 1)
|
||||
return FALSE
|
||||
if(player_actions(params["action"], params["data"], ui.user))
|
||||
if(game_state < GAME_OVER)
|
||||
game_state = GAME_PLAYER_TWO
|
||||
return TRUE
|
||||
if(ui.user == player_two?.resolve() && game_state == GAME_PLAYER_TWO)
|
||||
if(params["data"]["player"] == 2)
|
||||
return FALSE
|
||||
if(player_actions(params["action"], params["data"], ui.user))
|
||||
if(game_state < GAME_OVER)
|
||||
game_state = GAME_PLAYER_ONE
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/space_battle/proc/reset(full)
|
||||
ship_count_pone.Cut()
|
||||
ship_count_ptwo.Cut()
|
||||
shots_fired_pone.Cut()
|
||||
shots_fired_ptwo.Cut()
|
||||
ships_placed_pone.Cut()
|
||||
ships_placed_ptwo.Cut()
|
||||
destroyed_ships_pone.Cut()
|
||||
destroyed_ships_ptwo.Cut()
|
||||
winner = null
|
||||
ships_have_been_placed = NONE
|
||||
if(full)
|
||||
game_state = GAME_SETUP
|
||||
player_one = null
|
||||
player_two = null
|
||||
else
|
||||
game_state = GAME_PLACE_SHIPS
|
||||
|
||||
/datum/board_game/space_battle/proc/player_actions(action, list/params, mob/user)
|
||||
switch(action)
|
||||
if("fire_shot")
|
||||
var/list/validated_data = validate_coords(params)
|
||||
if(!validated_data)
|
||||
return FALSE
|
||||
|
||||
var/key = validated_data[1]
|
||||
|
||||
var/list/opponent_ships
|
||||
var/list/current_shots
|
||||
|
||||
if(game_state == GAME_PLAYER_ONE)
|
||||
current_shots = shots_fired_pone
|
||||
opponent_ships = ships_placed_ptwo
|
||||
else
|
||||
current_shots = shots_fired_ptwo
|
||||
opponent_ships = ships_placed_pone
|
||||
|
||||
if(!isnull(current_shots[key]))
|
||||
return FALSE
|
||||
|
||||
var/hit = 0
|
||||
for(var/list/ship in opponent_ships)
|
||||
for(var/coord in ship["coords"])
|
||||
if(coord[1] == validated_data[2] && coord[2] == validated_data[3])
|
||||
hit = 1
|
||||
break
|
||||
if(hit)
|
||||
break
|
||||
|
||||
current_shots[key] = hit
|
||||
|
||||
if(hit)
|
||||
validate_victory(opponent_ships, current_shots, user)
|
||||
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/space_battle/proc/validate_victory(list/opponent_ships, list/current_shots, mob/user)
|
||||
for(var/list/ship in opponent_ships)
|
||||
var/ship_destroyed = TRUE
|
||||
for(var/coord in ship["coords"])
|
||||
var/key = "[coord[1]],[coord[2]]"
|
||||
if(!current_shots[key])
|
||||
ship_destroyed = FALSE
|
||||
break
|
||||
|
||||
if(ship_destroyed)
|
||||
if(game_state == GAME_PLAYER_ONE)
|
||||
if(!(destroyed_ships_pone.Find(ship)))
|
||||
UNTYPED_LIST_ADD(destroyed_ships_pone, ship)
|
||||
ship_count_pone = get_alive_ships(1)
|
||||
else
|
||||
if(!(destroyed_ships_ptwo.Find(ship)))
|
||||
UNTYPED_LIST_ADD(destroyed_ships_ptwo, ship)
|
||||
ship_count_ptwo = get_alive_ships(2)
|
||||
|
||||
for(var/list/ship in opponent_ships)
|
||||
var/key
|
||||
for(var/coord in ship["coords"])
|
||||
key = "[coord[1]],[coord[2]]"
|
||||
if(!current_shots[key])
|
||||
return
|
||||
|
||||
winner = user.name
|
||||
game_state = GAME_OVER
|
||||
|
||||
/datum/board_game/space_battle/proc/field_check(new_x_location, new_y_location)
|
||||
if(new_x_location < 1 || new_x_location > GRID_SIZE)
|
||||
return FALSE
|
||||
if(new_y_location < 1 || new_y_location > GRID_SIZE)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/space_battle/proc/validate_coords(list/params)
|
||||
var/x_loc = text2num(params["loc_x"])
|
||||
var/y_loc = text2num(params["loc_y"])
|
||||
|
||||
if(!isnum(x_loc) || !isnum(y_loc))
|
||||
return null
|
||||
|
||||
if(!field_check(x_loc, y_loc))
|
||||
return null
|
||||
|
||||
var/key = "[x_loc],[y_loc]"
|
||||
return list(key, x_loc, y_loc)
|
||||
|
||||
/datum/board_game/space_battle/proc/get_remaining_ships(player)
|
||||
var/list/remaining_ships = list()
|
||||
var/list/placed_ships
|
||||
var/ship_placed_flag
|
||||
|
||||
if(player == 1)
|
||||
placed_ships = ships_placed_pone
|
||||
ship_placed_flag = PLAYER_ONE_PLACED_SHIPS
|
||||
else if(player == 2)
|
||||
placed_ships = ships_placed_ptwo
|
||||
ship_placed_flag = PLAYER_TWO_PLACED_SHIPS
|
||||
else
|
||||
return remaining_ships
|
||||
|
||||
for(var/ship_name in ship_sizes)
|
||||
var/allowed_count = total_ships[ship_name]
|
||||
var/placed_count = 0
|
||||
|
||||
for(var/list/placed_ship in placed_ships)
|
||||
if(placed_ship["name"] == ship_name)
|
||||
placed_count++
|
||||
|
||||
var/remaining_count = allowed_count - placed_count
|
||||
if(remaining_count > 0)
|
||||
remaining_ships[ship_name] = remaining_count
|
||||
|
||||
if(length(remaining_ships))
|
||||
ships_have_been_placed &= ~ship_placed_flag
|
||||
else
|
||||
ships_have_been_placed |= ship_placed_flag
|
||||
|
||||
return remaining_ships
|
||||
|
||||
/datum/board_game/space_battle/proc/get_alive_ships(player)
|
||||
var/list/alive_ships = list()
|
||||
var/list/ships
|
||||
|
||||
if(player == 1)
|
||||
ships = ships_placed_pone
|
||||
else if(player == 2)
|
||||
ships = ships_placed_ptwo
|
||||
else
|
||||
return alive_ships
|
||||
|
||||
for(var/list/ship in ships)
|
||||
if(game_state == GAME_PLAYER_ONE && !(destroyed_ships_pone.Find(ship)))
|
||||
alive_ships[ship["name"]] = length(alive_ships) ? alive_ships[ship["name"]] + 1 : 1
|
||||
|
||||
if(game_state == GAME_PLAYER_TWO && !(destroyed_ships_ptwo.Find(ship)))
|
||||
alive_ships[ship["name"]] = length(alive_ships) ? alive_ships[ship["name"]] + 1 : 1
|
||||
|
||||
return alive_ships
|
||||
|
||||
#undef GAME_PLACE_SHIPS
|
||||
#undef GAME_PLAYER_ONE
|
||||
#undef GAME_PLAYER_TWO
|
||||
#undef GAME_OVER
|
||||
#undef GRID_SIZE
|
||||
#undef PLAYER_ONE_PLACED_SHIPS
|
||||
#undef PLAYER_TWO_PLACED_SHIPS
|
||||
@@ -0,0 +1,51 @@
|
||||
#define GAME_PLAYER_ONE 1
|
||||
#define GRID_SIZE 3
|
||||
|
||||
/obj/structure/casino_table/board_game/tic_tac_toe
|
||||
name = GAME_TIC_TAC_TOE
|
||||
desc = "A small tic-tac-toe board."
|
||||
icon_state = "gamble_toe"
|
||||
game_ui = /datum/board_game/four_row/tic_tac_toe
|
||||
|
||||
/datum/board_game/four_row/tic_tac_toe
|
||||
name = GAME_TIC_TAC_TOE
|
||||
table_icon = "gamble_toe"
|
||||
grid_x_size = GRID_SIZE
|
||||
grid_y_size = GRID_SIZE
|
||||
win_count = GRID_SIZE
|
||||
|
||||
/datum/board_game/four_row/tic_tac_toe/tgui_static_data(mob/user)
|
||||
return list(
|
||||
"colors" = possible_colors - "blue"
|
||||
)
|
||||
|
||||
/datum/board_game/four_row/tic_tac_toe/player_actions(action, list/params, mob/user)
|
||||
switch(action)
|
||||
if("place_chip")
|
||||
var/list/validated_data = validate_coords(params)
|
||||
if(!validated_data)
|
||||
return FALSE
|
||||
|
||||
var/key = validated_data[1]
|
||||
if(placed_chips_pone[key] || placed_chips_ptwo[key])
|
||||
return FALSE
|
||||
|
||||
var/x_loc = validated_data[2]
|
||||
var/y_loc = validated_data[3]
|
||||
|
||||
if(game_state == GAME_PLAYER_ONE)
|
||||
placed_chips_pone[key] = TRUE
|
||||
else
|
||||
placed_chips_ptwo[key] = TRUE
|
||||
|
||||
validate_victory(x_loc, y_loc, user.name)
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/four_row/tic_tac_toe/set_new_size(new_size)
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/four_row/change_win_count(new_count)
|
||||
return FALSE
|
||||
|
||||
#undef GAME_PLAYER_ONE
|
||||
#undef GRID_SIZE
|
||||
@@ -0,0 +1,321 @@
|
||||
#define GAME_PLAYING 1
|
||||
#define GAME_LOST 2
|
||||
#define GAME_WON 3
|
||||
#define MAX_MINE_RATE 0.8
|
||||
|
||||
/obj/structure/casino_table/board_game/vore_sweeper
|
||||
name = GAME_SWEEPER
|
||||
desc = "A game about avoiding the mines"
|
||||
icon_state = "gamble_sweeper"
|
||||
game_ui = /datum/board_game/vore_sweeper
|
||||
|
||||
/datum/board_game/vore_sweeper
|
||||
name = GAME_SWEEPER
|
||||
table_icon = "gamble_sweeper"
|
||||
var/grid_size = 8
|
||||
var/mine_count = 10
|
||||
var/datum/weakref/dealer
|
||||
var/list/placed_mines = list()
|
||||
var/list/revealed_fields = list()
|
||||
var/list/placed_flags = list()
|
||||
|
||||
/datum/board_game/vore_sweeper/New(atom/holder)
|
||||
. = ..()
|
||||
parent = holder
|
||||
|
||||
/datum/board_game/vore_sweeper/Destroy(force)
|
||||
dealer = null
|
||||
parent = null
|
||||
. = ..()
|
||||
|
||||
/datum/board_game/vore_sweeper/tgui_interact(mob/user, datum/tgui/ui)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "VoreSweeper", name)
|
||||
ui.open()
|
||||
|
||||
/datum/board_game/vore_sweeper/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
|
||||
var/mob/dealer_mob = dealer?.resolve()
|
||||
|
||||
var/placed_mine_data = game_state > GAME_PLAYING || (ui.user == dealer_mob) ? placed_mines : null
|
||||
var/total_tiles = grid_size * grid_size
|
||||
return list(
|
||||
"grid_size" = grid_size,
|
||||
"mine_count" = mine_count,
|
||||
"max_mines" = round(total_tiles * MAX_MINE_RATE),
|
||||
"dealer" = dealer_mob,
|
||||
"placed_mines" = placed_mine_data,
|
||||
"revealed_fields" = revealed_fields,
|
||||
"placed_flags" = placed_flags,
|
||||
"game_state" = game_state,
|
||||
"is_dealer" = dealer_mob == ui.user
|
||||
)
|
||||
|
||||
/datum/board_game/vore_sweeper/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if("be_dealer")
|
||||
if(game_state == GAME_PLAYING)
|
||||
return FALSE
|
||||
dealer = WEAKREF(ui.user)
|
||||
return TRUE
|
||||
if("clear_dealer")
|
||||
var/mob/dealer_mob = dealer?.resolve()
|
||||
if(!dealer_mob)
|
||||
return FALSE
|
||||
if(dealer_mob == ui.user)
|
||||
parent.atom_say("[ui.user] stopped dealing.")
|
||||
dealer = null
|
||||
return TRUE
|
||||
if(get_dist(ui.user, dealer_mob) > 3)
|
||||
parent.atom_say("Dealer has been cleared by [ui.user].")
|
||||
dealer = null
|
||||
return TRUE
|
||||
return FALSE
|
||||
if("restart_game")
|
||||
var/mob/dealer_mob = dealer?.resolve()
|
||||
if(game_state < GAME_PLAYING)
|
||||
return FALSE
|
||||
placed_mines.Cut()
|
||||
revealed_fields.Cut()
|
||||
placed_flags.Cut()
|
||||
if(!dealer_mob && game_state > GAME_PLAYING)
|
||||
auto_place_mines(ui.user, TRUE)
|
||||
return TRUE
|
||||
game_state = GAME_SETUP
|
||||
return TRUE
|
||||
if("game_action")
|
||||
if(player_actions(params["action"], params["data"], ui.user))
|
||||
return TRUE
|
||||
return FALSE
|
||||
if("setup_action")
|
||||
if(dealer_actions(params["action"], params["data"], ui.user))
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/board_game/vore_sweeper/proc/player_actions(action, list/params, mob/user)
|
||||
if(user == dealer?.resolve())
|
||||
return FALSE
|
||||
if(game_state != GAME_PLAYING)
|
||||
return FALSE
|
||||
switch(action)
|
||||
if("open_field")
|
||||
var/list/validated_data = validate_coords(params)
|
||||
if(!validated_data)
|
||||
return FALSE
|
||||
var/key = validated_data[1]
|
||||
if(placed_flags[key])
|
||||
return FALSE
|
||||
if(revealed_fields[key])
|
||||
return FALSE
|
||||
if(placed_mines[key])
|
||||
game_state = GAME_LOST
|
||||
revealed_fields[key] = "M"
|
||||
return TRUE
|
||||
var/mine_count = count_surrounding_mines(validated_data[2], validated_data[3])
|
||||
revealed_fields[key] = mine_count
|
||||
if(!mine_count)
|
||||
reveal_empty_area(validated_data[2], validated_data[3])
|
||||
validate_victory()
|
||||
return TRUE
|
||||
if("toggle_flag")
|
||||
var/list/validated_data = validate_coords(params)
|
||||
if(!validated_data)
|
||||
return FALSE
|
||||
var/key = validated_data[1]
|
||||
if(revealed_fields[key])
|
||||
return FALSE
|
||||
if(placed_flags[key])
|
||||
placed_flags -= key
|
||||
return TRUE
|
||||
placed_flags[key] = TRUE
|
||||
validate_flag_victory()
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/vore_sweeper/proc/validate_victory()
|
||||
var/total_tiles = grid_size * grid_size
|
||||
var/safe_tiles = total_tiles - length(placed_mines)
|
||||
if(length(revealed_fields) >= safe_tiles)
|
||||
game_state = GAME_WON
|
||||
|
||||
/datum/board_game/vore_sweeper/proc/validate_flag_victory()
|
||||
var/all_flagged = TRUE
|
||||
|
||||
for(var/mine_key in placed_mines)
|
||||
if(placed_mines[mine_key] && !placed_flags[mine_key])
|
||||
all_flagged = FALSE
|
||||
break
|
||||
|
||||
for(var/flag_key in placed_flags)
|
||||
if(!placed_mines[flag_key])
|
||||
all_flagged = FALSE
|
||||
break
|
||||
|
||||
if(!all_flagged)
|
||||
return
|
||||
|
||||
for(var/x = 1 to grid_size)
|
||||
for(var/y = 1 to grid_size)
|
||||
var/key = "[x],[y]"
|
||||
|
||||
if(placed_mines[key])
|
||||
continue
|
||||
|
||||
revealed_fields[key] = count_surrounding_mines(x, y)
|
||||
|
||||
game_state = GAME_WON
|
||||
|
||||
/datum/board_game/vore_sweeper/proc/dealer_actions(action, list/params, mob/user)
|
||||
if(user != dealer?.resolve())
|
||||
return FALSE
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
switch(action)
|
||||
if("change_grid_size")
|
||||
var/new_grid_size = text2num(params["new_grid"])
|
||||
if(!new_grid_size)
|
||||
return FALSE
|
||||
if(new_grid_size < 4 || new_grid_size > 16)
|
||||
return FALSE
|
||||
validate_mine_count(mine_count, new_grid_size)
|
||||
grid_size = new_grid_size
|
||||
return TRUE
|
||||
if("change_mine_count")
|
||||
var/new_mine_count = text2num(params["new_mines"])
|
||||
if(!new_mine_count)
|
||||
return FALSE
|
||||
validate_mine_count(new_mine_count, grid_size)
|
||||
return TRUE
|
||||
if("place_mine")
|
||||
if(length(placed_mines) >= mine_count)
|
||||
return FALSE
|
||||
var/list/validated_data = validate_coords(params)
|
||||
if(!validated_data)
|
||||
return FALSE
|
||||
var/key = validated_data[1]
|
||||
if(placed_mines[key])
|
||||
return FALSE
|
||||
placed_mines[key] = TRUE
|
||||
return TRUE
|
||||
if("remove_mine")
|
||||
if(game_state != GAME_SETUP)
|
||||
return FALSE
|
||||
if(length(placed_mines) <= 0)
|
||||
return FALSE
|
||||
var/list/validated_data = validate_coords(params)
|
||||
if(!validated_data)
|
||||
return FALSE
|
||||
var/key = validated_data[1]
|
||||
placed_mines -= key
|
||||
return TRUE
|
||||
if("auto_place_mines")
|
||||
return auto_place_mines(user)
|
||||
if("auto_place_mines_self")
|
||||
return auto_place_mines(user, TRUE)
|
||||
if("clear_all_mines")
|
||||
placed_mines.Cut()
|
||||
return TRUE
|
||||
if("start_game")
|
||||
game_state = GAME_PLAYING
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/vore_sweeper/proc/reveal_empty_area(x, y)
|
||||
var/list/to_check = list(list(x, y))
|
||||
var/list/checked = list()
|
||||
var/i = 1
|
||||
|
||||
while(i <= length(to_check))
|
||||
var/current = to_check[i]
|
||||
i += 1
|
||||
var/cx = current[1]
|
||||
var/cy = current[2]
|
||||
var/key = "[cx],[cy]"
|
||||
|
||||
if(checked[key])
|
||||
continue
|
||||
checked[key] = TRUE
|
||||
|
||||
if(revealed_fields[key] || placed_flags[key])
|
||||
continue
|
||||
|
||||
var/adjacent_mines = count_surrounding_mines(cx, cy)
|
||||
revealed_fields[key] = adjacent_mines
|
||||
|
||||
if(adjacent_mines == 0)
|
||||
for(var/dx = -1 to 1)
|
||||
for(var/dy = -1 to 1)
|
||||
if(dx == 0 && dy == 0)
|
||||
continue
|
||||
var/nx = cx + dx
|
||||
var/ny = cy + dy
|
||||
if(field_check(nx, ny))
|
||||
to_check += list(list(nx, ny))
|
||||
|
||||
/datum/board_game/vore_sweeper/proc/auto_place_mines(mob/user, play)
|
||||
var/placed = length(placed_mines)
|
||||
if(placed && play)
|
||||
to_chat(user, span_warning("You can't do this while there are mines placed."))
|
||||
while(placed < mine_count)
|
||||
var/x = rand(1, grid_size)
|
||||
var/y = rand(1, grid_size)
|
||||
var/key = "[x],[y]"
|
||||
if(!placed_mines[key])
|
||||
placed_mines[key] = TRUE
|
||||
placed++
|
||||
if(play)
|
||||
dealer = null
|
||||
game_state = GAME_PLAYING
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/vore_sweeper/proc/field_check(new_x_location, new_y_location)
|
||||
if(new_x_location < 1 || new_x_location > grid_size)
|
||||
return FALSE
|
||||
if(new_y_location < 1 || new_y_location > grid_size)
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/board_game/vore_sweeper/proc/count_surrounding_mines(x, y)
|
||||
var/count = 0
|
||||
|
||||
for(var/dx = -1 to 1)
|
||||
for(var/dy = -1 to 1)
|
||||
if(dx == 0 && dy == 0)
|
||||
continue
|
||||
|
||||
var/check_x = x + dx
|
||||
var/check_y = y + dy
|
||||
|
||||
if(placed_mines["[check_x],[check_y]"])
|
||||
count++
|
||||
|
||||
return count
|
||||
|
||||
/datum/board_game/vore_sweeper/proc/validate_mine_count(mines, size)
|
||||
var/total_tiles = size * size
|
||||
var/max_mines = round(total_tiles * MAX_MINE_RATE)
|
||||
if(mines <= max_mines)
|
||||
mine_count = mines
|
||||
return
|
||||
parent.atom_say("The grid with [total_tiles] tiles only supports a maximum of [max_mines] mines.")
|
||||
mine_count = max_mines
|
||||
|
||||
/datum/board_game/vore_sweeper/proc/validate_coords(list/params)
|
||||
var/x_loc = text2num(params["loc_x"])
|
||||
var/y_loc = text2num(params["loc_y"])
|
||||
|
||||
if(!isnum(x_loc) || !isnum(y_loc))
|
||||
return null
|
||||
|
||||
if(!field_check(x_loc, y_loc))
|
||||
return null
|
||||
|
||||
var/key = "[x_loc],[y_loc]"
|
||||
return list(key, x_loc, y_loc)
|
||||
|
||||
#undef GAME_PLAYING
|
||||
#undef GAME_LOST
|
||||
#undef GAME_WON
|
||||
#undef MAX_MINE_RATE
|
||||
@@ -767,19 +767,26 @@ var/global/image/backplane
|
||||
return TRUE
|
||||
|
||||
|
||||
/atom/proc/living_mobs_in_view(var/range = world.view, var/count_held = FALSE)
|
||||
/atom/proc/living_mobs_in_view(var/range = world.view, var/count_held = FALSE, var/needs_client = FALSE)
|
||||
var/list/viewers = oviewers(src, range)
|
||||
if(count_held)
|
||||
viewers = viewers(src,range)
|
||||
var/list/living = list()
|
||||
for(var/mob/living/L in viewers)
|
||||
if(L.is_incorporeal())
|
||||
for(var/mob/living/living_in_view in viewers)
|
||||
if(living_in_view.is_incorporeal())
|
||||
continue
|
||||
living += L
|
||||
if(count_held)
|
||||
for(var/obj/item/holder/H in L.contents)
|
||||
if(istype(H.held_mob, /mob/living))
|
||||
living += H.held_mob
|
||||
if(needs_client && !living_in_view.client)
|
||||
continue
|
||||
living += living_in_view
|
||||
if(!count_held)
|
||||
continue
|
||||
for(var/obj/item/holder/mob_holder in living_in_view.contents)
|
||||
if(!isliving(mob_holder.held_mob))
|
||||
continue
|
||||
var/mob/living/held_living = mob_holder.held_mob
|
||||
if(needs_client && !held_living.client)
|
||||
continue
|
||||
living += held_living
|
||||
return living
|
||||
|
||||
/proc/censor_swears(t)
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
pda.current_app = src
|
||||
return 1
|
||||
|
||||
/datum/data/pda/app/proc/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/proc/update_ui(mob/user, list/data)
|
||||
|
||||
|
||||
// Utilities just have a button on the home screen, but custom code when clicked
|
||||
@@ -104,6 +104,6 @@
|
||||
pda.update_shortcuts()
|
||||
return 1
|
||||
|
||||
/datum/data/pda/utility/scanmode/proc/scan_mob(mob/living/C as mob, mob/living/user as mob)
|
||||
/datum/data/pda/utility/scanmode/proc/scan_mob(mob/living/C, mob/living/user)
|
||||
|
||||
/datum/data/pda/utility/scanmode/proc/scan_atom(atom/A as mob|obj|turf|area, mob/user as mob)
|
||||
/datum/data/pda/utility/scanmode/proc/scan_atom(atom/A, mob/user)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
var/message1 // used for status_displays
|
||||
var/message2
|
||||
|
||||
/datum/data/pda/app/status_display/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/status_display/update_ui(mob/user, list/data)
|
||||
data["records"] = list(
|
||||
"message1" = message1 ? message1 : "(none)",
|
||||
"message2" = message2 ? message2 : "(none)")
|
||||
@@ -63,7 +63,7 @@
|
||||
template = "pda_signaller"
|
||||
category = "Utilities"
|
||||
|
||||
/datum/data/pda/app/signaller/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/signaller/update_ui(mob/user, list/data)
|
||||
if(pda.cartridge && istype(pda.cartridge.radio, /obj/item/radio/integrated/signal))
|
||||
var/obj/item/radio/integrated/signal/R = pda.cartridge.radio
|
||||
data["frequency"] = R.frequency
|
||||
@@ -112,7 +112,7 @@
|
||||
QDEL_NULL(power_monitor)
|
||||
return ..()
|
||||
|
||||
/datum/data/pda/app/power/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/power/update_ui(mob/user, list/data)
|
||||
data.Add(power_monitor.tgui_data(user))
|
||||
|
||||
/datum/data/pda/app/power/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
@@ -128,7 +128,7 @@
|
||||
/datum/data/pda/app/crew_records
|
||||
var/datum/data/record/general_records = null
|
||||
|
||||
/datum/data/pda/app/crew_records/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/crew_records/update_ui(mob/user, list/data)
|
||||
var/list/records[0]
|
||||
|
||||
if(general_records && (general_records in GLOB.data_core.general))
|
||||
@@ -169,7 +169,7 @@
|
||||
|
||||
var/datum/data/record/medical_records = null
|
||||
|
||||
/datum/data/pda/app/crew_records/medical/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/crew_records/medical/update_ui(mob/user, list/data)
|
||||
var/list/records = ..()
|
||||
if(!records)
|
||||
return
|
||||
@@ -194,7 +194,7 @@
|
||||
|
||||
var/datum/data/record/security_records = null
|
||||
|
||||
/datum/data/pda/app/crew_records/security/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/crew_records/security/update_ui(mob/user, list/data)
|
||||
var/list/records = ..()
|
||||
if(!records)
|
||||
return
|
||||
@@ -217,7 +217,7 @@
|
||||
template = "pda_supply"
|
||||
category = "Quartermaster"
|
||||
|
||||
/datum/data/pda/app/supply/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/supply/update_ui(mob/user, list/data)
|
||||
var/supplyData[0]
|
||||
var/datum/shuttle/autodock/ferry/supply/shuttle = SSsupply.shuttle
|
||||
if (shuttle)
|
||||
@@ -254,7 +254,7 @@
|
||||
template = "pda_janitor"
|
||||
category = "Utilities"
|
||||
|
||||
/datum/data/pda/app/janitor/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/janitor/update_ui(mob/user, list/data)
|
||||
var/JaniData[0]
|
||||
var/turf/cl = get_turf(pda)
|
||||
|
||||
|
||||
@@ -32,15 +32,15 @@
|
||||
hold.max_storage_space = slots * 2
|
||||
hold.max_w_class = ITEMSIZE_SMALL
|
||||
|
||||
/obj/item/cartridge/storage/attack_hand(mob/user as mob)
|
||||
/obj/item/cartridge/storage/attack_hand(mob/user)
|
||||
if (hold.handle_attack_hand(user)) //otherwise interact as a regular storage item
|
||||
..(user)
|
||||
|
||||
/obj/item/cartridge/storage/attackby(obj/item/W as obj, mob/user as mob)
|
||||
/obj/item/cartridge/storage/attackby(obj/item/W, mob/user)
|
||||
..()
|
||||
return hold.attackby(W, user)
|
||||
|
||||
/obj/item/cartridge/storage/MouseDrop(obj/over_object as obj)
|
||||
/obj/item/cartridge/storage/MouseDrop(obj/over_object)
|
||||
if (hold.handle_mousedrop(usr, over_object))
|
||||
..(over_object)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
template = "pda_main_menu"
|
||||
hidden = 1
|
||||
|
||||
/datum/data/pda/app/main_menu/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/main_menu/update_ui(mob/user, list/data)
|
||||
title = pda.name
|
||||
|
||||
data["app"]["is_home"] = 1
|
||||
@@ -63,7 +63,7 @@
|
||||
note = "Thank you for choosing the [pda.model_name]!"
|
||||
notetitle = "Congratulations!"
|
||||
|
||||
/datum/data/pda/app/notekeeper/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/notekeeper/update_ui(mob/user, list/data)
|
||||
data["note"] = note // current pda notes
|
||||
data["notename"] = "Note [GLOB.alphabet_upper[currentnote]] : [notetitle]"
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
icon = "user"
|
||||
template = "pda_manifest"
|
||||
|
||||
/datum/data/pda/app/manifest/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/manifest/update_ui(mob/user, list/data)
|
||||
if(GLOB.data_core)
|
||||
GLOB.data_core.get_manifest_list()
|
||||
data["manifest"] = GLOB.PDA_Manifest
|
||||
@@ -221,7 +221,7 @@
|
||||
template = "pda_atmos_scan"
|
||||
category = "Utilities"
|
||||
|
||||
/datum/data/pda/app/atmos_scanner/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/atmos_scanner/update_ui(mob/user, list/data)
|
||||
data["aircontents"] = get_gas_mixture_default_scan_data(get_turf(user))
|
||||
|
||||
/datum/data/pda/app/news
|
||||
@@ -231,7 +231,7 @@
|
||||
|
||||
var/newsfeed_channel
|
||||
|
||||
/datum/data/pda/app/news/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/news/update_ui(mob/user, list/data)
|
||||
data["feeds"] = compile_news()
|
||||
data["latest_news"] = get_recent_news()
|
||||
if(newsfeed_channel)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/datum/data/pda/app/game_launcher
|
||||
name = "Game Launcher"
|
||||
icon = "dice"
|
||||
notify_icon = "dice-d20"
|
||||
title = "Game Launcher V1.0"
|
||||
template = "pda_game_launcher"
|
||||
|
||||
var/datum/board_game/vore_sweeper/voresweeper
|
||||
var/datum/board_game/four_row/fourrow
|
||||
var/datum/board_game/space_battle/spacebattle
|
||||
var/datum/board_game/rpg_dice/rpgdice
|
||||
var/datum/board_game/chess/chess
|
||||
var/datum/board_game/checkers/checkers
|
||||
var/datum/board_game/nine_mens/ninemens
|
||||
var/datum/board_game/four_row/tic_tac_toe/tictactoe
|
||||
|
||||
/datum/data/pda/app/game_launcher/update_ui(mob/user, list/data)
|
||||
data["available_games"] = list(GAME_SWEEPER = voresweeper, GAME_FOUR_ROW = fourrow, GAME_SPACE_BATTLE = spacebattle, GAME_RGP_DICE = rpgdice, GAME_CHESS = chess, GAME_CHECKERS = checkers, GAME_NINE_MENS_MORRIS = ninemens, GAME_TIC_TAC_TOE = tictactoe)
|
||||
|
||||
/datum/data/pda/app/game_launcher/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
|
||||
switch(action)
|
||||
if(GAME_SWEEPER)
|
||||
if(params["close"])
|
||||
if(!voresweeper)
|
||||
return FALSE
|
||||
QDEL_NULL(voresweeper)
|
||||
return TRUE
|
||||
if(!voresweeper)
|
||||
voresweeper = new(pda)
|
||||
voresweeper.tgui_interact(ui.user)
|
||||
return TRUE
|
||||
if(GAME_FOUR_ROW)
|
||||
if(params["close"])
|
||||
if(!fourrow)
|
||||
return FALSE
|
||||
QDEL_NULL(fourrow)
|
||||
return TRUE
|
||||
if(!fourrow)
|
||||
fourrow = new(pda)
|
||||
fourrow.tgui_interact(ui.user)
|
||||
return TRUE
|
||||
if(GAME_SPACE_BATTLE)
|
||||
if(params["close"])
|
||||
if(!spacebattle)
|
||||
return FALSE
|
||||
QDEL_NULL(spacebattle)
|
||||
return TRUE
|
||||
if(!spacebattle)
|
||||
spacebattle = new(pda)
|
||||
spacebattle.tgui_interact(ui.user)
|
||||
return TRUE
|
||||
if(GAME_RGP_DICE)
|
||||
if(params["close"])
|
||||
if(!rpgdice)
|
||||
return FALSE
|
||||
QDEL_NULL(rpgdice)
|
||||
return TRUE
|
||||
if(!rpgdice)
|
||||
rpgdice = new(pda)
|
||||
rpgdice.tgui_interact(ui.user)
|
||||
return TRUE
|
||||
if(GAME_CHESS)
|
||||
if(params["close"])
|
||||
if(!chess)
|
||||
return FALSE
|
||||
QDEL_NULL(chess)
|
||||
return TRUE
|
||||
if(!chess)
|
||||
chess = new(pda)
|
||||
chess.tgui_interact(ui.user)
|
||||
return TRUE
|
||||
if(GAME_CHECKERS)
|
||||
if(params["close"])
|
||||
if(!checkers)
|
||||
return FALSE
|
||||
QDEL_NULL(checkers)
|
||||
return TRUE
|
||||
if(!checkers)
|
||||
checkers = new(pda)
|
||||
checkers.tgui_interact(ui.user)
|
||||
return TRUE
|
||||
if(GAME_NINE_MENS_MORRIS)
|
||||
if(params["close"])
|
||||
if(!ninemens)
|
||||
return FALSE
|
||||
QDEL_NULL(ninemens)
|
||||
return TRUE
|
||||
if(!ninemens)
|
||||
ninemens = new(pda)
|
||||
ninemens.tgui_interact(ui.user)
|
||||
return TRUE
|
||||
if(GAME_TIC_TAC_TOE)
|
||||
if(params["close"])
|
||||
if(!tictactoe)
|
||||
return FALSE
|
||||
QDEL_NULL(tictactoe)
|
||||
return TRUE
|
||||
if(!tictactoe)
|
||||
tictactoe = new(pda)
|
||||
tictactoe.tgui_interact(ui.user)
|
||||
return TRUE
|
||||
return TRUE
|
||||
|
||||
/datum/data/pda/app/game_launcher/Destroy()
|
||||
if(voresweeper)
|
||||
QDEL_NULL(voresweeper)
|
||||
if(fourrow)
|
||||
QDEL_NULL(fourrow)
|
||||
if(spacebattle)
|
||||
QDEL_NULL(spacebattle)
|
||||
if(rpgdice)
|
||||
QDEL_NULL(rpgdice)
|
||||
if(chess)
|
||||
QDEL_NULL(chess)
|
||||
if(checkers)
|
||||
QDEL_NULL(checkers)
|
||||
if(checkers)
|
||||
QDEL_NULL(ninemens)
|
||||
if(tictactoe)
|
||||
QDEL_NULL(tictactoe)
|
||||
. = ..()
|
||||
@@ -18,7 +18,7 @@
|
||||
. = ..()
|
||||
unnotify()
|
||||
|
||||
/datum/data/pda/app/messenger/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/messenger/update_ui(mob/user, list/data)
|
||||
data["silent"] = notify_silent // does the pda make noise when it receives a message?
|
||||
data["toff"] = toff // is the messenger function turned off?
|
||||
data["active_conversation"] = active_conversation // Which conversation are we following right now?
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/datum/data/pda/messenger_plugin
|
||||
var/datum/data/pda/app/messenger/messenger
|
||||
|
||||
/datum/data/pda/messenger_plugin/proc/user_act(mob/user as mob, obj/item/pda/P)
|
||||
/datum/data/pda/messenger_plugin/proc/user_act(mob/user, obj/item/pda/P)
|
||||
|
||||
|
||||
/datum/data/pda/messenger_plugin/virus
|
||||
name = "*Send Virus*"
|
||||
|
||||
/datum/data/pda/messenger_plugin/virus/user_act(mob/user as mob, obj/item/pda/P)
|
||||
/datum/data/pda/messenger_plugin/virus/user_act(mob/user, obj/item/pda/P)
|
||||
var/datum/data/pda/app/messenger/M = P.find_program(/datum/data/pda/app/messenger)
|
||||
|
||||
if(M && !M.toff && pda.cartridge.charges > 0)
|
||||
@@ -19,7 +19,7 @@
|
||||
/datum/data/pda/messenger_plugin/virus/clown
|
||||
icon = "star"
|
||||
|
||||
/datum/data/pda/messenger_plugin/virus/clown/user_act(mob/user as mob, obj/item/pda/P)
|
||||
/datum/data/pda/messenger_plugin/virus/clown/user_act(mob/user, obj/item/pda/P)
|
||||
. = ..(user, P)
|
||||
if(.)
|
||||
user.show_message(span_notice("Virus sent!"), 1)
|
||||
@@ -30,7 +30,7 @@
|
||||
/datum/data/pda/messenger_plugin/virus/mime
|
||||
icon = "arrow-circle-down"
|
||||
|
||||
/datum/data/pda/messenger_plugin/virus/mime/user_act(mob/user as mob, obj/item/pda/P)
|
||||
/datum/data/pda/messenger_plugin/virus/mime/user_act(mob/user, obj/item/pda/P)
|
||||
. = ..(user, P)
|
||||
if(.)
|
||||
user.show_message(span_notice("Virus sent!"), 1)
|
||||
@@ -44,7 +44,7 @@
|
||||
name = "*Detonate*"
|
||||
icon = "exclamation-circle"
|
||||
|
||||
/datum/data/pda/messenger_plugin/virus/detonate/user_act(mob/user as mob, obj/item/pda/P)
|
||||
/datum/data/pda/messenger_plugin/virus/detonate/user_act(mob/user, obj/item/pda/P)
|
||||
. = ..(user, P)
|
||||
if(.)
|
||||
var/difficulty = 0
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
LAZYADD(serialized_guesses, list(out)) // Wrap it in a list so it stays a list
|
||||
|
||||
/datum/data/pda/app/nerdle/update_ui(mob/user as mob, list/data)
|
||||
/datum/data/pda/app/nerdle/update_ui(mob/user, list/data)
|
||||
data["guesses"] = serialized_guesses
|
||||
data["guesses_raw"] = guesses
|
||||
data["max"] = max_guesses
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
new/datum/data/pda/app/manifest,
|
||||
new/datum/data/pda/app/atmos_scanner,
|
||||
new/datum/data/pda/app/nerdle,
|
||||
new/datum/data/pda/app/game_launcher,
|
||||
new/datum/data/pda/utility/scanmode/notes,
|
||||
new/datum/data/pda/utility/flashlight)
|
||||
var/list/shortcut_cache = list()
|
||||
@@ -177,7 +178,7 @@
|
||||
/obj/item/pda/GetID()
|
||||
return id
|
||||
|
||||
/obj/item/pda/MouseDrop(obj/over_object as obj, src_location, over_location)
|
||||
/obj/item/pda/MouseDrop(obj/over_object, src_location, over_location)
|
||||
var/mob/M = usr
|
||||
if((!istype(over_object, /atom/movable/screen)) && can_use(usr))
|
||||
return attack_self(M)
|
||||
@@ -389,7 +390,7 @@
|
||||
start_program(find_program(/datum/data/pda/app/main_menu))
|
||||
|
||||
|
||||
/obj/item/pda/proc/id_check(mob/user as mob, choice as num)//To check for IDs; 1 for in-pda use, 2 for out of pda use.
|
||||
/obj/item/pda/proc/id_check(mob/user, choice)//To check for IDs; 1 for in-pda use, 2 for out of pda use.
|
||||
if(choice == 1)
|
||||
if (id)
|
||||
remove_id()
|
||||
@@ -411,7 +412,7 @@
|
||||
return 0
|
||||
|
||||
// access to status display signals
|
||||
/obj/item/pda/attackby(obj/item/C as obj, mob/user)
|
||||
/obj/item/pda/attackby(obj/item/C, mob/user)
|
||||
..()
|
||||
if(istype(C, /obj/item/cartridge) && !cartridge)
|
||||
cartridge = C
|
||||
@@ -456,11 +457,11 @@
|
||||
add_overlay("pda-pen")
|
||||
return
|
||||
|
||||
/obj/item/pda/attack(mob/living/C as mob, mob/living/user as mob)
|
||||
/obj/item/pda/attack(mob/living/C, mob/living/user)
|
||||
if (istype(C, /mob/living/carbon) && scanmode)
|
||||
scanmode.scan_mob(C, user)
|
||||
|
||||
/obj/item/pda/afterattack(atom/A as mob|obj|turf|area, mob/user as mob, proximity)
|
||||
/obj/item/pda/afterattack(atom/A, mob/user, proximity)
|
||||
if(proximity && scanmode)
|
||||
scanmode.scan_atom(A, user)
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
name = "Civilian Services Department (Relay)"
|
||||
cartridges_to_send_to = GLOB.civilian_cartridges
|
||||
|
||||
/obj/item/pda/clown/Crossed(atom/movable/AM as mob|obj) //Clown PDA is slippery.
|
||||
/obj/item/pda/clown/Crossed(atom/movable/AM) //Clown PDA is slippery.
|
||||
if(AM.is_incorporeal())
|
||||
return
|
||||
if (isliving(AM))
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
base_name = "Med Scanner"
|
||||
icon = "heart-o"
|
||||
|
||||
/datum/data/pda/utility/scanmode/medical/scan_mob(mob/living/C as mob, mob/living/user as mob)
|
||||
/datum/data/pda/utility/scanmode/medical/scan_mob(mob/living/C, mob/living/user)
|
||||
C.visible_message(span_warning("[user] has analyzed [C]'s vitals!"))
|
||||
user.show_message(span_notice("Analyzing Results for [C]:"))
|
||||
if(C.status_flags & FAKEDEATH)
|
||||
@@ -79,7 +79,7 @@
|
||||
base_name = "DNA Scanner"
|
||||
icon = "link"
|
||||
|
||||
/datum/data/pda/utility/scanmode/dna/scan_mob(mob/living/C as mob, mob/living/user as mob)
|
||||
/datum/data/pda/utility/scanmode/dna/scan_mob(mob/living/C, mob/living/user)
|
||||
if(ishuman(C))
|
||||
var/mob/living/carbon/human/H = C
|
||||
if(!istype(H.dna, /datum/dna))
|
||||
@@ -88,7 +88,7 @@
|
||||
to_chat(user, span_notice("[H]'s Fingerprints: [md5(H.dna.uni_identity)]"))
|
||||
scan_blood(C, user)
|
||||
|
||||
/datum/data/pda/utility/scanmode/dna/scan_atom(atom/A as mob|obj|turf|area, mob/user as mob)
|
||||
/datum/data/pda/utility/scanmode/dna/scan_atom(atom/A, mob/user)
|
||||
scan_blood(A, user)
|
||||
|
||||
/datum/data/pda/utility/scanmode/dna/proc/scan_blood(atom/A, mob/user)
|
||||
@@ -105,7 +105,7 @@
|
||||
base_name = "Halogen Counter"
|
||||
icon = "exclamation-circle"
|
||||
|
||||
/datum/data/pda/utility/scanmode/halogen/scan_mob(mob/living/C as mob, mob/living/user as mob)
|
||||
/datum/data/pda/utility/scanmode/halogen/scan_mob(mob/living/C, mob/living/user)
|
||||
C.visible_message(span_warning("[user] has analyzed [C]'s radiation levels!"))
|
||||
|
||||
user.show_message(span_notice("Analyzing Results for [C]:"))
|
||||
@@ -118,7 +118,7 @@
|
||||
base_name = "Reagent Scanner"
|
||||
icon = "flask"
|
||||
|
||||
/datum/data/pda/utility/scanmode/reagent/scan_atom(atom/A as mob|obj|turf|area, mob/user as mob)
|
||||
/datum/data/pda/utility/scanmode/reagent/scan_atom(atom/A, mob/user)
|
||||
if(!isnull(A.reagents))
|
||||
if(A.reagents.reagent_list.len > 0)
|
||||
var/reagents_length = A.reagents.reagent_list.len
|
||||
@@ -134,7 +134,7 @@
|
||||
base_name = "Gas Scanner"
|
||||
icon = "tachometer-alt"
|
||||
|
||||
/datum/data/pda/utility/scanmode/gas/scan_atom(atom/A as mob|obj|turf|area, mob/user as mob)
|
||||
/datum/data/pda/utility/scanmode/gas/scan_atom(atom/A, mob/user)
|
||||
pda.analyze_gases(A, user)
|
||||
|
||||
/datum/data/pda/utility/scanmode/notes
|
||||
@@ -146,7 +146,7 @@
|
||||
. = ..()
|
||||
notes = pda.find_program(/datum/data/pda/app/notekeeper)
|
||||
|
||||
/datum/data/pda/utility/scanmode/notes/scan_atom(atom/A as mob|obj|turf|area, mob/user as mob)
|
||||
/datum/data/pda/utility/scanmode/notes/scan_atom(atom/A, mob/user)
|
||||
if(notes && istype(A, /obj/item/paper))
|
||||
var/obj/item/paper/P = A
|
||||
var/list/brlist = list("p", "/p", "br", "hr", "h1", "h2", "h3", "h4", "/h1", "/h2", "/h3", "/h4")
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* tgui state: board_game_state
|
||||
*
|
||||
* Checks that the default living handling or if the user or the object are inside a belly
|
||||
**/
|
||||
|
||||
GLOBAL_DATUM_INIT(tgui_board_game_state, /datum/tgui_state/board_game_state, new)
|
||||
|
||||
/datum/tgui_state/board_game_state/can_use_topic(atom/src_object, mob/user)
|
||||
if(!isliving(user))
|
||||
return STATUS_UPDATE
|
||||
var/mob/living/living_user = user
|
||||
if(isbelly(living_user.loc) || isbelly(src_object.loc))
|
||||
return living_user.board_game_can_use_tgui_topic(src_object)
|
||||
return living_user.default_can_use_tgui_topic(src_object)
|
||||
|
||||
/mob/proc/board_game_can_use_tgui_topic(atom/src_object)
|
||||
return STATUS_CLOSE
|
||||
|
||||
/mob/living/board_game_can_use_tgui_topic(atom/src_object)
|
||||
if(is_incorporeal())
|
||||
return STATUS_CLOSE
|
||||
var/dist = get_dist(get_turf(src_object), get_turf(src))
|
||||
if(dist <= 1)
|
||||
return STATUS_INTERACTIVE
|
||||
return STATUS_CLOSE
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,18 @@
|
||||
import { Icon, Tooltip } from 'tgui-core/components';
|
||||
|
||||
export const CastlingUsed = (props: { used: boolean }) => {
|
||||
const { used } = props;
|
||||
|
||||
return (
|
||||
<Tooltip content={`Castling ${used ? '' : 'un'}used`}>
|
||||
{used ? (
|
||||
<Icon.Stack>
|
||||
<Icon name="dungeon" />
|
||||
<Icon name="ban" color="red" />
|
||||
</Icon.Stack>
|
||||
) : (
|
||||
<Icon name="dungeon" />
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
import { useState } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, Section, Stack } from 'tgui-core/components';
|
||||
import { numToLetter } from '../SpaceBattle/constants';
|
||||
import { checkDisabled, getGameIcons } from './functions';
|
||||
import { PromotionSelection } from './PromotionSelection';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const GameArea = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const {
|
||||
game_type,
|
||||
game_state,
|
||||
current_board,
|
||||
selected_figure,
|
||||
valid_moves,
|
||||
possible_jumps,
|
||||
} = data;
|
||||
const [promotionFloating, setPromotionFloating] =
|
||||
useState<React.JSX.Element | null>(null);
|
||||
|
||||
function handleClick(isValidMove: boolean, x: number, y: number) {
|
||||
if (selected_figure.length) {
|
||||
const piece =
|
||||
current_board[selected_figure[1] - 1][selected_figure[0] - 1];
|
||||
|
||||
const isPawnPromotion =
|
||||
piece &&
|
||||
piece[1] === 'P' &&
|
||||
((piece[0] === 'w' && y === 1) || (piece[0] === 'b' && y === 8));
|
||||
|
||||
if (isValidMove && isPawnPromotion && game_type === 'chess') {
|
||||
setPromotionFloating(
|
||||
<PromotionSelection
|
||||
x={x}
|
||||
y={y}
|
||||
iconText={piece}
|
||||
onPromotionFloating={setPromotionFloating}
|
||||
/>,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
act('game_action', {
|
||||
action: isValidMove ? 'move_figure' : 'select_figure',
|
||||
data: { loc_x: x, loc_y: y },
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Section
|
||||
fill
|
||||
title="Game Grid"
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
<Stack.Item grow> {game_state === 2 && promotionFloating}</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack vertical>
|
||||
<Stack.Item>
|
||||
{Array.from({ length: 9 }, (_, row) => (
|
||||
<Stack key={row}>
|
||||
{Array.from({ length: 9 }, (_, col) => {
|
||||
if (col === 0 && row === 0) {
|
||||
return (
|
||||
<Stack.Item key="corner">
|
||||
<Box width={1} />
|
||||
</Stack.Item>
|
||||
);
|
||||
} else if (col === 0 && row > 0) {
|
||||
return (
|
||||
<Stack.Item align="center" key={`row-${row}`}>
|
||||
<Box width={1}>{numToLetter[row - 1]}</Box>
|
||||
</Stack.Item>
|
||||
);
|
||||
} else if (row === 0 && col > 0) {
|
||||
return (
|
||||
<Stack.Item textAlign="center" key={`col-${col}`}>
|
||||
<Box width={5}>{col}</Box>
|
||||
</Stack.Item>
|
||||
);
|
||||
}
|
||||
|
||||
const boardX = row - 1;
|
||||
const boardY = 8 - col;
|
||||
const x = boardX + 1;
|
||||
const y = boardY + 1;
|
||||
const iconText = current_board.length
|
||||
? current_board[boardY][boardX]
|
||||
: '';
|
||||
const isValidMove = valid_moves.some(
|
||||
([moveX, moveY]) => moveX === x && moveY === y,
|
||||
);
|
||||
const canJump = possible_jumps?.some(
|
||||
([moveX, moveY]) => moveX === x && moveY === y,
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack.Item key={`${x}${y}`}>
|
||||
<Button
|
||||
disabled={
|
||||
checkDisabled(game_state) || !!promotionFloating
|
||||
}
|
||||
selected={
|
||||
selected_figure.length &&
|
||||
selected_figure[0] === x &&
|
||||
selected_figure[1] === y
|
||||
}
|
||||
color={
|
||||
isValidMove
|
||||
? 'teal'
|
||||
: canJump
|
||||
? 'yellow'
|
||||
: (boardX + boardY) % 2 === 0
|
||||
? 'light-grey'
|
||||
: 'gray'
|
||||
}
|
||||
onClick={() => handleClick(isValidMove, x, y)}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
width={5}
|
||||
height={5}
|
||||
fontSize={2.5}
|
||||
innerStyle={{
|
||||
color: iconText?.includes('b') ? 'black' : 'white',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
lineHeight: '60px',
|
||||
}}
|
||||
>
|
||||
{getGameIcons(game_type, iconText)}
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow> {game_state === 1 && promotionFloating}</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, Section, Stack } from 'tgui-core/components';
|
||||
import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip';
|
||||
import { gameTooltip } from './constants';
|
||||
import { gameStateToText, stateToColor } from './functions';
|
||||
import { PlayerPanel } from './PlayerPanel';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const PlayerMenu = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const {
|
||||
game_type,
|
||||
game_state,
|
||||
player_one,
|
||||
player_two,
|
||||
player_one_time,
|
||||
player_two_time,
|
||||
has_won,
|
||||
game_flags = 0,
|
||||
winner,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Game Status"
|
||||
buttons={
|
||||
<Stack>
|
||||
{(game_state > 0 && (
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('clear_game');
|
||||
}}
|
||||
>
|
||||
Clear Game
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
)) ||
|
||||
(game_state === 0 && !!player_one && !!player_two && (
|
||||
<>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('start_game');
|
||||
}}
|
||||
>
|
||||
Start Game
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('swap_players');
|
||||
}}
|
||||
>
|
||||
Swap Players
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
</>
|
||||
))}
|
||||
{!!player_one && !!player_two && game_state === 3 && (
|
||||
<>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('play_again');
|
||||
}}
|
||||
>
|
||||
Play Again
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('play_again_swapped');
|
||||
}}
|
||||
>
|
||||
Play Again (Swapped Sides)
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
</>
|
||||
)}
|
||||
<Stack.Item>
|
||||
<Button onClick={() => act('invite_player')}>Invite Player</Button>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<VorePanelTooltip
|
||||
tooltip={gameTooltip[game_type]}
|
||||
displayText="?"
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Stack fill>
|
||||
<PlayerPanel
|
||||
gameState={game_state}
|
||||
player={player_one}
|
||||
num={1}
|
||||
time={player_one_time}
|
||||
castlingUsed={!!(game_flags & 4)}
|
||||
/>
|
||||
<Stack.Item basis="33%">
|
||||
<Stack vertical textAlign="center">
|
||||
<Stack.Item>
|
||||
<Box color="label">Status:</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box color={stateToColor(game_state, game_flags, !!has_won)}>
|
||||
{gameStateToText(game_state, game_flags, winner)}
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<PlayerPanel
|
||||
gameState={game_state}
|
||||
player={player_two}
|
||||
num={2}
|
||||
time={player_two_time}
|
||||
castlingUsed={!!(game_flags & 8)}
|
||||
/>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Button, ColorBox, LabeledList, Stack } from 'tgui-core/components';
|
||||
import { formatTime } from 'tgui-core/format';
|
||||
import { numToText } from '../SpaceBattle/constants';
|
||||
import { CastlingUsed } from './CommonComponents';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const PlayerPanel = (props: {
|
||||
gameState: number;
|
||||
player: string | null;
|
||||
time: number;
|
||||
castlingUsed: boolean;
|
||||
num: number;
|
||||
}) => {
|
||||
const { data, act } = useBackend<Data>();
|
||||
const { game_type } = data;
|
||||
const { gameState, player, time, castlingUsed, num } = props;
|
||||
|
||||
const player_string = numToText[num - 1];
|
||||
|
||||
return (
|
||||
<Stack.Item basis="33%">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label={`Player ${num}`}>
|
||||
<Button.Confirm
|
||||
disabled={gameState !== 0}
|
||||
onClick={() => act(`be_player_${player_string}`)}
|
||||
>
|
||||
{player || `Be player ${num}`}
|
||||
</Button.Confirm>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Color">
|
||||
<ColorBox
|
||||
color={num === 1 ? 'white' : 'black'}
|
||||
style={{
|
||||
border: '1px solid white',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Time">{formatTime(time)}</LabeledList.Item>
|
||||
{game_type === 'chess' && (
|
||||
<LabeledList.Item label="Castling">
|
||||
<CastlingUsed used={castlingUsed} />
|
||||
</LabeledList.Item>
|
||||
)}
|
||||
</LabeledList>
|
||||
</Stack.Item>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Button, Stack } from 'tgui-core/components';
|
||||
import { possiblePromotions } from './constants';
|
||||
import { getGameIcons } from './functions';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const PromotionSelection = (props: {
|
||||
x: number;
|
||||
y: number;
|
||||
iconText: string;
|
||||
onPromotionFloating: React.Dispatch<
|
||||
React.SetStateAction<React.JSX.Element | null>
|
||||
>;
|
||||
}) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
const { x, y, iconText, onPromotionFloating } = props;
|
||||
return (
|
||||
<Stack fill vertical align="center" justify="center">
|
||||
{possiblePromotions.map((option) => (
|
||||
<Stack.Item key={option}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
act('game_action', {
|
||||
action: 'move_figure',
|
||||
data: { loc_x: x, loc_y: y, promotion_choice: option },
|
||||
});
|
||||
onPromotionFloating(null);
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
width={5}
|
||||
height={5}
|
||||
fontSize={2.5}
|
||||
innerStyle={{
|
||||
color: iconText?.includes('b') ? 'black' : 'white',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
lineHeight: '60px',
|
||||
}}
|
||||
>
|
||||
{getGameIcons('chess', iconText[0] + option)}
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
export const textToChess = {
|
||||
wK: '♔',
|
||||
wQ: '♕',
|
||||
wR: '♖',
|
||||
wB: '♗',
|
||||
wN: '♘',
|
||||
wP: '♙',
|
||||
|
||||
bK: '♚',
|
||||
bQ: '♛',
|
||||
bR: '♜',
|
||||
bB: '♝',
|
||||
bN: '♞',
|
||||
bP: '♟',
|
||||
} as const;
|
||||
|
||||
export const textToCheckers = {
|
||||
wM: '⛀',
|
||||
wK: '⛁',
|
||||
|
||||
bM: '⛂',
|
||||
bK: '⛃',
|
||||
} as const;
|
||||
|
||||
export const possiblePromotions = ['Q', 'R', 'B', 'N'] as const;
|
||||
|
||||
export const gameTooltip = {
|
||||
chess:
|
||||
"To play, the player's pick their side, white or black and then play in turns. Common chess rules apply.",
|
||||
checkers:
|
||||
"To play, the player's pick their side, white or black and then play in turns. Common checkers rules apply.",
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { textToCheckers, textToChess } from './constants';
|
||||
|
||||
export function getGameIcons(
|
||||
type: string,
|
||||
currentPosition: string | null,
|
||||
): string {
|
||||
if (!currentPosition) {
|
||||
return '';
|
||||
}
|
||||
switch (type) {
|
||||
case 'chess':
|
||||
return textToChess[currentPosition];
|
||||
case 'checkers':
|
||||
return textToCheckers[currentPosition];
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function stateToColor(
|
||||
gameState: number,
|
||||
gameFlags: number,
|
||||
isWinner: boolean,
|
||||
): string | undefined {
|
||||
if (gameState === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (gameState > 0 && gameState < 3) {
|
||||
if (
|
||||
(gameState === 1 && gameFlags & 1) ||
|
||||
(gameState === 2 && gameFlags & 2)
|
||||
) {
|
||||
return 'yellow';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (gameState === 3) {
|
||||
return isWinner ? 'green' : 'red';
|
||||
}
|
||||
return 'yellow';
|
||||
}
|
||||
|
||||
export function gameStateToText(
|
||||
gameState: number,
|
||||
gameFlags: number,
|
||||
winner: string | null,
|
||||
): string {
|
||||
if (gameState === 0) {
|
||||
return 'Setup';
|
||||
}
|
||||
if (gameState === 1) {
|
||||
if (gameFlags & 1) {
|
||||
return 'Player 1 (Check)';
|
||||
}
|
||||
return 'Player 1';
|
||||
}
|
||||
if (gameState === 2) {
|
||||
if (gameFlags & 2) {
|
||||
return 'Player 2 (Check)';
|
||||
}
|
||||
return 'Player 2';
|
||||
}
|
||||
if (gameState >= 3) {
|
||||
return `${winner} ${gameState === 3 ? 'Won' : 'Draw'}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function checkDisabled(gameState: number): boolean {
|
||||
if (gameState < 1 || gameState > 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { Stack } from 'tgui-core/components';
|
||||
import { GameArea } from './GameArea';
|
||||
import { PlayerMenu } from './PlayerMenu';
|
||||
|
||||
export const ChessCheckers = (props) => {
|
||||
return (
|
||||
<Window width={800} height={735}>
|
||||
<Window.Content>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
<PlayerMenu />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<GameArea />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
export type Data = {
|
||||
game_type: string;
|
||||
player_one: string;
|
||||
player_two: string;
|
||||
player_one_time: number;
|
||||
player_two_time: number;
|
||||
current_board: (string | null)[][];
|
||||
selected_figure: number[];
|
||||
valid_moves: number[][];
|
||||
game_state: number;
|
||||
winner: string;
|
||||
has_won: BooleanLike;
|
||||
game_flags?: number;
|
||||
possible_jumps?: number[][];
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Button, Icon, Section, Stack } from 'tgui-core/components';
|
||||
import { classes } from 'tgui-core/react';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const GameArea = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const {
|
||||
grid_x_size,
|
||||
grid_y_size,
|
||||
placed_chips_pone,
|
||||
placed_chips_ptwo,
|
||||
player_one_color,
|
||||
player_two_color,
|
||||
game_state,
|
||||
winning_tiles,
|
||||
} = data;
|
||||
|
||||
function playerColor(key: string): string {
|
||||
if (placed_chips_pone[key]) {
|
||||
return player_one_color;
|
||||
}
|
||||
if (placed_chips_ptwo[key]) {
|
||||
return player_two_color;
|
||||
}
|
||||
return 'black';
|
||||
}
|
||||
|
||||
const isTicTacToe = grid_x_size === 3;
|
||||
function playerIcon(key: string): React.JSX.Element | null {
|
||||
if (placed_chips_pone[key]) {
|
||||
return <Icon name="x" color={player_one_color} />;
|
||||
}
|
||||
if (placed_chips_ptwo[key]) {
|
||||
return <Icon name="o" color={player_two_color} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isWinningTile(key: string): boolean {
|
||||
return game_state === 3 && winning_tiles?.includes(key);
|
||||
}
|
||||
|
||||
return (
|
||||
<Section
|
||||
fill
|
||||
title="Game Grid"
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Stack justify="center">
|
||||
<Stack.Item>
|
||||
<Stack backgroundColor="#474747" vertical>
|
||||
<Stack.Item>
|
||||
{Array.from({ length: grid_y_size }, (_, row) => (
|
||||
<Stack key={row}>
|
||||
{Array.from({ length: grid_x_size }, (_, col) => {
|
||||
const x = col + 1;
|
||||
const y = row + 1;
|
||||
const key = `${x},${y}`;
|
||||
const tileClasses = isTicTacToe
|
||||
? ['TicTacToe__Tile']
|
||||
: ['FourInARow__Tile'];
|
||||
|
||||
let dynamicStyle: CSSProperties = {};
|
||||
|
||||
if (isWinningTile(key)) {
|
||||
tileClasses.push('winning-tile');
|
||||
dynamicStyle = {
|
||||
background: `radial-gradient(circle at 50% 50%, yellow 0%, ${playerColor(key)}, red 100%)`,
|
||||
};
|
||||
} else if (
|
||||
placed_chips_pone[key] ||
|
||||
placed_chips_ptwo[key]
|
||||
) {
|
||||
tileClasses.push('placed');
|
||||
dynamicStyle = {
|
||||
background: `radial-gradient(circle at 30% 30%, white 0%, ${playerColor(key)} 70%)`,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack.Item key={key}>
|
||||
<Button
|
||||
circular={!isTicTacToe}
|
||||
disabled={
|
||||
placed_chips_pone[key] ||
|
||||
placed_chips_ptwo[key] ||
|
||||
game_state === 3
|
||||
}
|
||||
color={isTicTacToe ? undefined : playerColor(key)}
|
||||
onClick={(_) => {
|
||||
act('game_action', {
|
||||
action: 'place_chip',
|
||||
data: { loc_x: x, loc_y: y },
|
||||
});
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
width={5}
|
||||
height={5}
|
||||
className={classes(tileClasses)}
|
||||
style={isTicTacToe ? undefined : dynamicStyle}
|
||||
>
|
||||
{playerIcon(key)}
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, NumberInput, Section, Stack } from 'tgui-core/components';
|
||||
import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip';
|
||||
import { gameTooltip } from './constants';
|
||||
import { gameStateToText, stateToColor } from './functions';
|
||||
import { PlayerPanel } from './PlayerPanel';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const PlayerMenu = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const {
|
||||
colors,
|
||||
player_one,
|
||||
player_two,
|
||||
game_state,
|
||||
grid_x_size,
|
||||
win_count,
|
||||
player_one_color,
|
||||
player_two_color,
|
||||
winner,
|
||||
has_won,
|
||||
} = data;
|
||||
|
||||
const isFourRow = grid_x_size > 3;
|
||||
const isTicTacToe = !isFourRow;
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Game Status"
|
||||
buttons={
|
||||
<Stack>
|
||||
{(game_state > 0 && (
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('clear_game');
|
||||
}}
|
||||
>
|
||||
Clear Game
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
)) ||
|
||||
(game_state === 0 && player_one && player_two && (
|
||||
<>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('start_game');
|
||||
}}
|
||||
>
|
||||
Start Game
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('swap_players');
|
||||
}}
|
||||
>
|
||||
Swap Players
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
</>
|
||||
))}
|
||||
{!!player_one && !!player_two && game_state === 3 && (
|
||||
<>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('play_again');
|
||||
}}
|
||||
>
|
||||
Play Again
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('play_again_swapped');
|
||||
}}
|
||||
>
|
||||
Play Again (Swapped Sides)
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
</>
|
||||
)}
|
||||
<Stack.Item>
|
||||
<Button onClick={() => act('invite_player')}>Invite Player</Button>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<VorePanelTooltip
|
||||
tooltip={gameTooltip[isFourRow ? 0 : 1]}
|
||||
displayText="?"
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Stack fill>
|
||||
<PlayerPanel
|
||||
ticTacToe={isTicTacToe}
|
||||
gameState={game_state}
|
||||
player={player_one}
|
||||
color={player_one_color}
|
||||
usableColors={colors.filter((c) => c !== player_two_color)}
|
||||
num={1}
|
||||
/>
|
||||
<Stack.Item basis="33%">
|
||||
<Stack vertical textAlign="center">
|
||||
<Stack.Item>
|
||||
<Stack justify="center">
|
||||
<Stack.Item>
|
||||
<Box color="label">Size:</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{game_state === 0 && isFourRow ? (
|
||||
<NumberInput
|
||||
width="30px"
|
||||
minValue={5}
|
||||
maxValue={10}
|
||||
stepPixelSize={40}
|
||||
value={grid_x_size}
|
||||
onChange={(value) => act('change_size', { size: value })}
|
||||
/>
|
||||
) : (
|
||||
grid_x_size
|
||||
)}
|
||||
</Stack.Item>
|
||||
<Stack.Item basis="10%" />
|
||||
<Stack.Item>
|
||||
<Box color="label">Win Con.:</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{game_state === 0 && isFourRow ? (
|
||||
<NumberInput
|
||||
minValue={4}
|
||||
maxValue={5}
|
||||
stepPixelSize={100}
|
||||
value={win_count}
|
||||
onChange={(value) => act('change_win', { count: value })}
|
||||
/>
|
||||
) : (
|
||||
win_count
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box color="label">Status:</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box
|
||||
color={stateToColor(
|
||||
game_state,
|
||||
player_one_color,
|
||||
player_two_color,
|
||||
!!has_won,
|
||||
)}
|
||||
>
|
||||
{gameStateToText(game_state, winner)}
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<PlayerPanel
|
||||
ticTacToe={isTicTacToe}
|
||||
gameState={game_state}
|
||||
player={player_two}
|
||||
color={player_two_color}
|
||||
usableColors={colors.filter((c) => c !== player_one_color)}
|
||||
num={2}
|
||||
/>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import {
|
||||
Button,
|
||||
ColorBox,
|
||||
Dropdown,
|
||||
Icon,
|
||||
LabeledList,
|
||||
Stack,
|
||||
} from 'tgui-core/components';
|
||||
import { numToText } from '../SpaceBattle/constants';
|
||||
|
||||
export const PlayerPanel = (props: {
|
||||
ticTacToe: boolean;
|
||||
gameState: number;
|
||||
player: string | null;
|
||||
color: string;
|
||||
usableColors: string[];
|
||||
num: number;
|
||||
}) => {
|
||||
const { act } = useBackend();
|
||||
const { ticTacToe, gameState, player, color, usableColors, num } = props;
|
||||
|
||||
const player_string = numToText[num - 1];
|
||||
|
||||
return (
|
||||
<Stack.Item basis="33%">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label={`Player ${num}`}>
|
||||
<Button.Confirm
|
||||
disabled={gameState !== 0}
|
||||
onClick={() => act(`be_player_${player_string}`)}
|
||||
>
|
||||
{player || `Be player ${num}`}
|
||||
</Button.Confirm>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label={'Color'}>
|
||||
<Stack align="center">
|
||||
<Stack.Item>
|
||||
<Dropdown
|
||||
width="100px"
|
||||
disabled={gameState !== 0 || !player}
|
||||
onSelected={(value) =>
|
||||
act(`set_color_${player_string}`, { color: value })
|
||||
}
|
||||
options={usableColors}
|
||||
selected={color}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{ticTacToe ? (
|
||||
<Icon color={color} name={num === 1 ? 'x' : 'o'} />
|
||||
) : (
|
||||
<ColorBox
|
||||
color={color}
|
||||
style={{
|
||||
border: '1px solid white',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Stack.Item>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export const gameTooltip = [
|
||||
'A two player game where players take turns with the goal to place the previously set up amount of chips into a connected row. ' +
|
||||
'Left click onto the playfield to palce your chip.',
|
||||
'A game where players take turns with the goal to place their marks in a line across the board.',
|
||||
];
|
||||
@@ -0,0 +1,39 @@
|
||||
export function stateToColor(
|
||||
gameState: number,
|
||||
playerOneColor: string,
|
||||
playeTwoColor: string,
|
||||
isWinner: boolean,
|
||||
): string | undefined {
|
||||
if (gameState < 1) {
|
||||
return undefined;
|
||||
}
|
||||
if (gameState === 1) {
|
||||
return playerOneColor;
|
||||
}
|
||||
if (gameState === 2) {
|
||||
return playeTwoColor;
|
||||
}
|
||||
if (gameState === 4) {
|
||||
return 'yellow';
|
||||
}
|
||||
return isWinner ? 'green' : 'red';
|
||||
}
|
||||
|
||||
export function gameStateToText(
|
||||
gameState: number,
|
||||
winner: string | null,
|
||||
): string {
|
||||
if (gameState < 1) {
|
||||
return 'Setup';
|
||||
}
|
||||
if (gameState === 1) {
|
||||
return 'Player 1';
|
||||
}
|
||||
if (gameState === 2) {
|
||||
return 'Player 2';
|
||||
}
|
||||
if (gameState === 4) {
|
||||
return 'Draw';
|
||||
}
|
||||
return `${winner} Won`;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { Stack } from 'tgui-core/components';
|
||||
import { GameArea } from './GameArea';
|
||||
import { PlayerMenu } from './PlayerMenu';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const FourInARow = (props) => {
|
||||
const { data } = useBackend<Data>();
|
||||
|
||||
const { grid_y_size } = data;
|
||||
const added_height = grid_y_size * 63;
|
||||
return (
|
||||
<Window width={685} height={200 + added_height}>
|
||||
<Window.Content>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
<PlayerMenu />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<GameArea />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
export type Data = {
|
||||
colors: string[];
|
||||
player_one: string | null;
|
||||
player_two: string | null;
|
||||
placed_chips_pone: Record<string, BooleanLike>;
|
||||
placed_chips_ptwo: Record<string, BooleanLike>;
|
||||
game_state: number;
|
||||
grid_x_size: number;
|
||||
grid_y_size: number;
|
||||
player_one_color: string;
|
||||
player_two_color: string;
|
||||
win_count: number;
|
||||
winner: string | null;
|
||||
has_won: BooleanLike;
|
||||
winning_tiles: string[];
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, Section } from 'tgui-core/components';
|
||||
import { nodePositions, textToNine } from './constants';
|
||||
import { checkDisabled } from './functions';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const GameArea = () => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const {
|
||||
game_state,
|
||||
current_board,
|
||||
selected_node,
|
||||
valid_moves,
|
||||
valid_removes,
|
||||
phase,
|
||||
} = data;
|
||||
|
||||
function handleClick(isValidMove: boolean, node: number) {
|
||||
act('game_action', {
|
||||
action:
|
||||
(!selected_node || !isValidMove) && phase === 1
|
||||
? 'select_figure'
|
||||
: 'move_figure',
|
||||
data: { node_number: node },
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Section fill title="Nine Men's Morris">
|
||||
<Box position="relative" width="400px" height="400px" mt="50px" mx="auto">
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ position: 'absolute', top: 0, left: 0 }}
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
>
|
||||
<rect x="1" y="1" width="98" height="98" />
|
||||
<rect x="17" y="17" width="66" height="66" />
|
||||
<rect x="33" y="33" width="34" height="34" />
|
||||
<line x1="1" y1="50" x2="33" y2="50" />
|
||||
<line x1="67" y1="50" x2="99" y2="50" />
|
||||
<line x1="50" y1="1" x2="50" y2="33" />
|
||||
<line x1="50" y1="67" x2="50" y2="99" />
|
||||
</svg>
|
||||
{Array.from({ length: 24 }, (_, i) => {
|
||||
const node = i + 1;
|
||||
const piece = current_board[i];
|
||||
const isSelected = selected_node === node;
|
||||
const isValidMove = valid_moves?.includes(node);
|
||||
const isValidRemove = valid_removes?.includes(node);
|
||||
const pos = nodePositions[node];
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="NineMen__Tile"
|
||||
key={node}
|
||||
circular
|
||||
left={`${pos.x}%`}
|
||||
top={`${pos.y}%`}
|
||||
selected={isSelected}
|
||||
color={isValidMove ? 'teal' : isValidRemove ? 'yellow' : 'grey'}
|
||||
onClick={() => handleClick(isValidMove, node)}
|
||||
disabled={checkDisabled(game_state)}
|
||||
innerStyle={{
|
||||
color: piece === 'b' ? 'black' : 'white',
|
||||
}}
|
||||
>
|
||||
{piece ? textToNine[piece] : ''}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, Section, Stack } from 'tgui-core/components';
|
||||
import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip';
|
||||
import { gameTooltip, phastToText } from './constants';
|
||||
import { gameStateToText, stateToColor } from './functions';
|
||||
import { PlayerPanel } from './PlayerPanel';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const PlayerMenu = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const {
|
||||
game_state,
|
||||
phase,
|
||||
player_one,
|
||||
player_two,
|
||||
player_one_time,
|
||||
player_two_time,
|
||||
has_won,
|
||||
winner,
|
||||
pone_pieces,
|
||||
ptwo_pieces,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Game Status"
|
||||
buttons={
|
||||
<Stack>
|
||||
{(game_state > 0 && (
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('clear_game');
|
||||
}}
|
||||
>
|
||||
Clear Game
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
)) ||
|
||||
(game_state === 0 && !!player_one && !!player_two && (
|
||||
<>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('start_game');
|
||||
}}
|
||||
>
|
||||
Start Game
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('swap_players');
|
||||
}}
|
||||
>
|
||||
Swap Players
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
</>
|
||||
))}
|
||||
{!!player_one && !!player_two && game_state === 3 && (
|
||||
<>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('play_again');
|
||||
}}
|
||||
>
|
||||
Play Again
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('play_again_swapped');
|
||||
}}
|
||||
>
|
||||
Play Again (Swapped Sides)
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
</>
|
||||
)}
|
||||
<Stack.Item>
|
||||
<Button onClick={() => act('invite_player')}>Invite Player</Button>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<VorePanelTooltip tooltip={gameTooltip} displayText="?" />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Stack fill>
|
||||
<PlayerPanel
|
||||
gameState={game_state}
|
||||
player={player_one}
|
||||
num={1}
|
||||
time={player_one_time}
|
||||
pieces={pone_pieces}
|
||||
/>
|
||||
<Stack.Item basis="33%">
|
||||
<Stack vertical textAlign="center">
|
||||
<Stack.Item>
|
||||
<Box color="label">Status:</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box color={stateToColor(game_state, !!has_won)}>
|
||||
{gameStateToText(game_state, winner)}
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>{phastToText[phase]}</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<PlayerPanel
|
||||
gameState={game_state}
|
||||
player={player_two}
|
||||
num={2}
|
||||
time={player_two_time}
|
||||
pieces={ptwo_pieces}
|
||||
/>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Button, ColorBox, LabeledList, Stack } from 'tgui-core/components';
|
||||
import { formatTime } from 'tgui-core/format';
|
||||
import { numToText } from '../SpaceBattle/constants';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const PlayerPanel = (props: {
|
||||
gameState: number;
|
||||
player: string | null;
|
||||
pieces: number;
|
||||
time: number;
|
||||
num: number;
|
||||
}) => {
|
||||
const { act } = useBackend<Data>();
|
||||
const { gameState, player, pieces, time, num } = props;
|
||||
|
||||
const player_string = numToText[num - 1];
|
||||
|
||||
return (
|
||||
<Stack.Item basis="33%">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label={`Player ${num}`}>
|
||||
<Button.Confirm
|
||||
disabled={gameState !== 0}
|
||||
onClick={() => act(`be_player_${player_string}`)}
|
||||
>
|
||||
{player || `Be player ${num}`}
|
||||
</Button.Confirm>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Color">
|
||||
<ColorBox
|
||||
color={num === 1 ? 'white' : 'black'}
|
||||
style={{
|
||||
border: '1px solid white',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Not Placed">{pieces}</LabeledList.Item>
|
||||
<LabeledList.Item label="Time">{formatTime(time)}</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Stack.Item>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
export const textToNine = {
|
||||
w: '⛀',
|
||||
b: '⛂',
|
||||
} as const;
|
||||
|
||||
export const gameTooltip =
|
||||
'To play the game, players place their pieces in turns. When one player manages to place three pieces in a row, they remove an opponent piece. After a player places 9 pieces, they must move their pieces to open, adjacent positions. If a player only has 3 pieces, they can jump any piece to any open space. A player loses as soon as less than three pieces are left.';
|
||||
|
||||
export const phastToText = ['Place', 'Move', 'Remove', ''] as const;
|
||||
|
||||
export const nodePositions = {
|
||||
1: { x: 1, y: 1 },
|
||||
2: { x: 50, y: 1 },
|
||||
3: { x: 99, y: 1 },
|
||||
|
||||
4: { x: 17, y: 17 },
|
||||
5: { x: 50, y: 17 },
|
||||
6: { x: 83, y: 17 },
|
||||
|
||||
7: { x: 33, y: 33 },
|
||||
8: { x: 50, y: 33 },
|
||||
9: { x: 67, y: 33 },
|
||||
|
||||
10: { x: 1, y: 50 },
|
||||
11: { x: 17, y: 50 },
|
||||
12: { x: 33, y: 50 },
|
||||
13: { x: 67, y: 50 },
|
||||
14: { x: 83, y: 50 },
|
||||
15: { x: 99, y: 50 },
|
||||
|
||||
16: { x: 33, y: 67 },
|
||||
17: { x: 50, y: 67 },
|
||||
18: { x: 67, y: 67 },
|
||||
|
||||
19: { x: 17, y: 83 },
|
||||
20: { x: 50, y: 83 },
|
||||
21: { x: 83, y: 83 },
|
||||
|
||||
22: { x: 1, y: 99 },
|
||||
23: { x: 50, y: 99 },
|
||||
24: { x: 99, y: 99 },
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
export function stateToColor(
|
||||
gameState: number,
|
||||
isWinner: boolean,
|
||||
): string | undefined {
|
||||
if (gameState === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (gameState === 3) {
|
||||
return isWinner ? 'green' : 'red';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function gameStateToText(
|
||||
gameState: number,
|
||||
winner: string | null,
|
||||
): string {
|
||||
if (gameState === 0) {
|
||||
return 'Setup';
|
||||
}
|
||||
if (gameState === 1) {
|
||||
return 'Player 1';
|
||||
}
|
||||
if (gameState === 2) {
|
||||
return 'Player 2';
|
||||
}
|
||||
if (gameState >= 3) {
|
||||
return `${winner} ${gameState === 3 ? 'Won' : 'Draw'}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function checkDisabled(gameState: number): boolean {
|
||||
if (gameState < 1 || gameState > 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { Stack } from 'tgui-core/components';
|
||||
import { GameArea } from './GameArea';
|
||||
import { PlayerMenu } from './PlayerMenu';
|
||||
|
||||
export const NineMen = (props) => {
|
||||
return (
|
||||
<Window width={800} height={735}>
|
||||
<Window.Content>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
<PlayerMenu />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<GameArea />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
export type Data = {
|
||||
player_one: string;
|
||||
player_two: string;
|
||||
player_one_time: number;
|
||||
player_two_time: number;
|
||||
current_board: (string | null)[];
|
||||
selected_node: number | null;
|
||||
valid_moves: number[];
|
||||
valid_removes: number[];
|
||||
game_state: number;
|
||||
winner: string;
|
||||
has_won: BooleanLike;
|
||||
phase: number;
|
||||
pone_pieces: number;
|
||||
ptwo_pieces: number;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, LabeledList, Section, Stack } from 'tgui-core/components';
|
||||
|
||||
type Data = {
|
||||
available_games: Record<string, string | null>;
|
||||
};
|
||||
|
||||
export const pda_game_launcher = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const { available_games } = data;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Section title="Game Launcher V1.0">
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
Play your favourite games everywhere, any time! Don't get caught!
|
||||
</Stack.Item>
|
||||
<LabeledList>
|
||||
{Object.keys(available_games).map((game) => {
|
||||
const gameAction = available_games[game];
|
||||
return (
|
||||
<LabeledList.Item key={game} label={game}>
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<Button onClick={() => act(game)}>Play {game}</Button>
|
||||
</Stack.Item>
|
||||
{!!gameAction && (
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => act(game, { close: true })}
|
||||
icon="circle-xmark"
|
||||
iconColor="red"
|
||||
color="transparent"
|
||||
tooltip="forcefully close the game"
|
||||
/>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
</LabeledList.Item>
|
||||
);
|
||||
})}
|
||||
</LabeledList>
|
||||
</Stack>
|
||||
</Section>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,248 @@
|
||||
import { type RefObject, useRef, useState } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import {
|
||||
Button,
|
||||
LabeledList,
|
||||
NumberInput,
|
||||
Section,
|
||||
Stack,
|
||||
} from 'tgui-core/components';
|
||||
import { KEY } from 'tgui-core/keys';
|
||||
import { classes } from 'tgui-core/react';
|
||||
import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip';
|
||||
import { gameTooltip, MAX_HISTORY, presetDice } from './constants';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const PlayerMenu = (props) => {
|
||||
const { act } = useBackend<Data>();
|
||||
const [diceSize, setDiceSize] = useState(6);
|
||||
const [diceCount, setDiceCount] = useState(1);
|
||||
const [modifier, setModifier] = useState(0);
|
||||
const [applyToAll, setApplyToAll] = useState(false);
|
||||
const [customInput, setCustomInput] = useState('');
|
||||
|
||||
const [history, setHistory] = useState<string[]>([]);
|
||||
const [historyIndex, setHistoryIndex] = useState<number>(-1);
|
||||
|
||||
const inputRef: RefObject<HTMLInputElement | null> = useRef(null);
|
||||
|
||||
function updateHistory(newEntry: string) {
|
||||
if (newEntry === history[historyIndex]) return;
|
||||
|
||||
const newHistory = [...history, newEntry];
|
||||
|
||||
if (newHistory.length > MAX_HISTORY) {
|
||||
newHistory.shift();
|
||||
}
|
||||
|
||||
setHistory(newHistory);
|
||||
setHistoryIndex(newHistory.length);
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
|
||||
if (event.getModifierState('AltGraph')) return;
|
||||
|
||||
switch (event.key) {
|
||||
case KEY.PageUp:
|
||||
event.preventDefault();
|
||||
inputRef.current?.blur();
|
||||
handleUndo();
|
||||
requestAnimationFrame(() => inputRef.current?.focus());
|
||||
break;
|
||||
case KEY.PageDown:
|
||||
event.preventDefault();
|
||||
inputRef.current?.blur();
|
||||
handleRedo();
|
||||
requestAnimationFrame(() => inputRef.current?.focus());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleUndo() {
|
||||
if (historyIndex > 0) {
|
||||
const prevState = history[historyIndex - 1];
|
||||
setCustomInput(prevState);
|
||||
setHistoryIndex(historyIndex - 1);
|
||||
}
|
||||
requestAnimationFrame(() => inputRef.current?.focus());
|
||||
}
|
||||
|
||||
function handleRedo() {
|
||||
if (historyIndex < history.length) {
|
||||
const nextState = history[historyIndex + 1];
|
||||
setCustomInput(nextState);
|
||||
setHistoryIndex(historyIndex + 1);
|
||||
} else if (historyIndex === history.length) {
|
||||
setCustomInput('');
|
||||
}
|
||||
requestAnimationFrame(() => inputRef.current?.focus());
|
||||
}
|
||||
|
||||
const firstHalf = presetDice.slice(0, 4);
|
||||
const secondHalf = presetDice.slice(4);
|
||||
return (
|
||||
<Section
|
||||
title="Dice Selection"
|
||||
fill
|
||||
buttons={
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<Button onClick={() => act('invite_player')}>Invite Player</Button>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<VorePanelTooltip tooltip={gameTooltip} displayText="?" />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
<Stack>
|
||||
{firstHalf.map((dice, index) => (
|
||||
<Stack.Item grow key={index}>
|
||||
<Button
|
||||
onClick={() => act('roll_dice', { dice_size: dice })}
|
||||
className={classes(['RpgDice__dice', `d${dice}`, 'side'])}
|
||||
>
|
||||
{dice}
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack textAlign="center">
|
||||
{secondHalf.map((dice, index) => (
|
||||
<Stack.Item grow key={index}>
|
||||
<Button
|
||||
onClick={() => act('roll_dice', { dice_size: dice })}
|
||||
className={classes(['RpgDice__dice', `d${dice}`, 'side'])}
|
||||
>
|
||||
{dice}
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Divider />
|
||||
<Stack.Item>
|
||||
<Stack>
|
||||
<Stack.Item grow>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Size">
|
||||
<NumberInput
|
||||
width="50px"
|
||||
minValue={1}
|
||||
maxValue={10000}
|
||||
value={diceSize}
|
||||
onChange={setDiceSize}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Count">
|
||||
<NumberInput
|
||||
width="50px"
|
||||
stepPixelSize={40}
|
||||
minValue={1}
|
||||
maxValue={10}
|
||||
value={diceCount}
|
||||
onChange={setDiceCount}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Modifier">
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<NumberInput
|
||||
width="50px"
|
||||
value={modifier}
|
||||
onChange={setModifier}
|
||||
/>
|
||||
<Stack.Item inline>
|
||||
<Button.Checkbox
|
||||
checked={applyToAll}
|
||||
onClick={() => setApplyToAll(!applyToAll)}
|
||||
>
|
||||
Apply to all
|
||||
</Button.Checkbox>
|
||||
</Stack.Item>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
onClick={() =>
|
||||
act('roll_dice', {
|
||||
dice_size: diceSize,
|
||||
dice_count: diceCount,
|
||||
dice_mod: modifier,
|
||||
mod_all: applyToAll,
|
||||
})
|
||||
}
|
||||
icon="dice"
|
||||
iconSize={2}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
{/** Requires rust g dice
|
||||
<Stack.Divider />
|
||||
<Stack.Item>
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
disabled
|
||||
onClick={handleUndo}
|
||||
icon="rotate-left"
|
||||
iconSize={2}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Button
|
||||
disabled
|
||||
onClick={handleRedo}
|
||||
icon="rotate-right"
|
||||
iconSize={2}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
disabled
|
||||
onClick={() => {
|
||||
updateHistory(customInput);
|
||||
act('custom_roll', { custom: customInput });
|
||||
setCustomInput('');
|
||||
requestAnimationFrame(() => inputRef.current?.focus());
|
||||
}}
|
||||
iconSize={2}
|
||||
icon="paper-plane"
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack fill>
|
||||
<Stack.Item grow>
|
||||
<Input
|
||||
disabled
|
||||
fluid
|
||||
ref={inputRef}
|
||||
onEnter={(value) => {
|
||||
updateHistory(value);
|
||||
act('custom_roll', { custom: value });
|
||||
setCustomInput('');
|
||||
requestAnimationFrame(() => inputRef.current?.focus());
|
||||
}}
|
||||
maxLength={200}
|
||||
value={customInput}
|
||||
onChange={setCustomInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
*/}
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Fragment, type RefObject, useEffect, useRef } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, Section, Stack } from 'tgui-core/components';
|
||||
import { stateToColor } from './constants';
|
||||
import type { Data, Result } from './types';
|
||||
|
||||
export const RollArea = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const { last_rolls } = data;
|
||||
|
||||
const messagesEndRef: RefObject<HTMLDivElement | null> = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const scroll = messagesEndRef.current;
|
||||
if (!scroll) return;
|
||||
|
||||
const isAtBottom =
|
||||
Math.abs(scroll.scrollHeight - scroll.scrollTop - scroll.offsetHeight) <
|
||||
48;
|
||||
|
||||
if (isAtBottom) {
|
||||
scroll.scrollTop = scroll.scrollHeight;
|
||||
}
|
||||
}, [last_rolls]);
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Roll history"
|
||||
ref={messagesEndRef}
|
||||
scrollable
|
||||
fill
|
||||
buttons={
|
||||
<Button.Confirm onBlur={() => act('clear_history')} icon="trash" />
|
||||
}
|
||||
>
|
||||
<Stack vertical fill>
|
||||
{last_rolls.map((roll, index) => (
|
||||
<Stack.Item key={index}>
|
||||
<Box inline color="label">
|
||||
{roll.player}:
|
||||
</Box>
|
||||
<Box inline bold>
|
||||
{roll.count}
|
||||
</Box>
|
||||
{!!roll.size && (
|
||||
<Box inline bold>
|
||||
d{roll.size}
|
||||
</Box>
|
||||
)}
|
||||
{!!roll.mod && (
|
||||
<Box bold inline>
|
||||
+{roll.mod}
|
||||
</Box>
|
||||
)}
|
||||
<Box inline preserveWhitespace>
|
||||
{`: `}
|
||||
</Box>
|
||||
{!!roll.results && (
|
||||
<DetailedResults
|
||||
results={roll.results}
|
||||
mod={roll.mod || 0}
|
||||
applyToAll={!!roll.apply_to_all}
|
||||
/>
|
||||
)}
|
||||
{(!roll.results || roll.results.length > 1 || !!roll.mod) && (
|
||||
<Box inline bold>
|
||||
{roll.sum}
|
||||
</Box>
|
||||
)}
|
||||
</Stack.Item>
|
||||
))}
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export const DetailedResults = (props: {
|
||||
results: Result[];
|
||||
mod: number;
|
||||
applyToAll: boolean;
|
||||
}) => {
|
||||
const { results, mod, applyToAll } = props;
|
||||
|
||||
const moreThanOne = results.length > 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!!mod && !applyToAll && moreThanOne && <Box inline>{`(`}</Box>}
|
||||
{results.map((result, index) => (
|
||||
<Fragment key={index}>
|
||||
{!!mod && applyToAll && moreThanOne && <Box inline>{`(`}</Box>}
|
||||
<Box
|
||||
inline
|
||||
color={stateToColor[result.state]}
|
||||
bold={!!stateToColor[result.state] || !moreThanOne}
|
||||
>
|
||||
{result.result}
|
||||
</Box>
|
||||
<Box inline preserveWhitespace>
|
||||
{!!mod && applyToAll && ` + ${mod}`}
|
||||
{!!mod && applyToAll && !!moreThanOne && `)`}
|
||||
</Box>
|
||||
{(moreThanOne || !!mod) &&
|
||||
(index < results.length - 1 ? (
|
||||
<Box inline preserveWhitespace>{` + `}</Box>
|
||||
) : (
|
||||
<Box inline preserveWhitespace>
|
||||
{!!mod && !applyToAll && !!moreThanOne && `)`}
|
||||
{!!mod && !applyToAll && ` + ${mod}`}
|
||||
{` = `}
|
||||
</Box>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export const presetDice = [4, 6, 8, 10, 12, 20, 50, 100] as const;
|
||||
|
||||
export const gameTooltip =
|
||||
'A simple dice roller. Use predefined dice sizes and counts to get a detailed breakdown of the rolls. Or use a fully custom query like 1d10 + 5 + 2d5 - 6. ' +
|
||||
"For those won't be any per dice breakdown.";
|
||||
|
||||
export const MAX_HISTORY = 10;
|
||||
|
||||
export const stateToColor = [undefined, 'red', 'green'] as const;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { Stack } from 'tgui-core/components';
|
||||
import { PlayerMenu } from './PlayerMenu';
|
||||
import { RollArea } from './RollArea';
|
||||
|
||||
export const RpgDice = (props) => {
|
||||
return (
|
||||
<Window width={900} height={450}>
|
||||
<Window.Content>
|
||||
<Stack fill>
|
||||
<Stack.Item basis="33%">
|
||||
<PlayerMenu />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow mb="5px">
|
||||
<RollArea />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
export type Data = {
|
||||
last_rolls: {
|
||||
player: string;
|
||||
count: number;
|
||||
size?: number;
|
||||
results?: Result[];
|
||||
mod?: number;
|
||||
apply_to_all?: BooleanLike;
|
||||
sum: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type Result = { result: number; state: number };
|
||||
@@ -0,0 +1,277 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, Section, Stack } from 'tgui-core/components';
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
import { numToLetter } from './constants';
|
||||
import { generateShipCoordinates } from './functions';
|
||||
import type { Data, Ship } from './types';
|
||||
|
||||
export const GameArea = (props: {
|
||||
playerOnePlaceShip: string;
|
||||
playerTwoPlaceShip: string;
|
||||
playerOneOrientation: 'vertical' | 'horizontal';
|
||||
playerTwoOrientation: 'vertical' | 'horizontal';
|
||||
}) => {
|
||||
const { data } = useBackend<Data>();
|
||||
|
||||
const {
|
||||
current_player,
|
||||
player_one,
|
||||
player_two,
|
||||
shots_fired_pone,
|
||||
shots_fired_ptwo,
|
||||
destroyed_ships_pone,
|
||||
destroyed_ships_ptwo,
|
||||
ship_count_pone,
|
||||
ship_count_ptwo,
|
||||
} = data;
|
||||
|
||||
const {
|
||||
playerOnePlaceShip,
|
||||
playerTwoPlaceShip,
|
||||
playerOneOrientation,
|
||||
playerTwoOrientation,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Section
|
||||
fill
|
||||
title="Game Grid"
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<Playfield
|
||||
player={1}
|
||||
isOponent={current_player === player_two}
|
||||
isSelf={current_player === player_one}
|
||||
shotsFired={shots_fired_ptwo}
|
||||
destroyedShips={destroyed_ships_ptwo}
|
||||
shipBeingPlaced={playerOnePlaceShip}
|
||||
orientation={playerOneOrientation}
|
||||
shipCount={ship_count_pone}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item minWidth="20px" />
|
||||
<Stack.Item>
|
||||
<Playfield
|
||||
player={2}
|
||||
isOponent={current_player === player_one}
|
||||
isSelf={current_player === player_two}
|
||||
shotsFired={shots_fired_pone}
|
||||
destroyedShips={destroyed_ships_pone}
|
||||
shipBeingPlaced={playerTwoPlaceShip}
|
||||
orientation={playerTwoOrientation}
|
||||
shipCount={ship_count_ptwo}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const Playfield = (props: {
|
||||
player: number;
|
||||
isOponent: boolean;
|
||||
isSelf: boolean;
|
||||
shotsFired: Record<string, BooleanLike>;
|
||||
destroyedShips: Ship[];
|
||||
shipBeingPlaced: string;
|
||||
orientation: 'vertical' | 'horizontal';
|
||||
shipCount: Record<string, number>;
|
||||
}) => {
|
||||
const { data, act } = useBackend<Data>();
|
||||
const { game_state, ship_sizes, visible_ships } = data;
|
||||
const {
|
||||
player,
|
||||
isOponent,
|
||||
isSelf,
|
||||
shotsFired,
|
||||
destroyedShips,
|
||||
shipBeingPlaced,
|
||||
orientation,
|
||||
shipCount,
|
||||
} = props;
|
||||
|
||||
const [highlightedCells, setHighlightedCells] = useState<
|
||||
{ x: number; y: number }[]
|
||||
>([]);
|
||||
const [invalidCells, setInvalidCells] = useState(false);
|
||||
const lastLoc = useRef<[number, number] | null>(null);
|
||||
|
||||
const handleButtonHover = (x: number, y: number) => {
|
||||
setHighlightedCells([]);
|
||||
if (!shipCount[shipBeingPlaced]) {
|
||||
return;
|
||||
}
|
||||
const size = ship_sizes[shipBeingPlaced];
|
||||
const newHighlightedCells: { x: number; y: number }[] = [];
|
||||
let isInvalid = false;
|
||||
lastLoc.current = [x, y];
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
const newX = orientation === 'horizontal' ? x + i : x;
|
||||
const newY = orientation === 'vertical' ? y + i : y;
|
||||
|
||||
if (newX < 1 || newY < 1 || newX >= 11 || newY >= 11) {
|
||||
isInvalid = true;
|
||||
break;
|
||||
}
|
||||
for (const ship of visible_ships) {
|
||||
if (
|
||||
ship.coords?.some(
|
||||
(coord) => coord[0] === newX && coord[1] === newY,
|
||||
) &&
|
||||
ship.player === player
|
||||
) {
|
||||
isInvalid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
newHighlightedCells.push({ x: newX, y: newY });
|
||||
}
|
||||
|
||||
setHighlightedCells(newHighlightedCells);
|
||||
setInvalidCells(isInvalid);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (lastLoc.current) {
|
||||
handleButtonHover(lastLoc.current[0], lastLoc.current[1]);
|
||||
}
|
||||
}, [orientation, shipCount, shipBeingPlaced]);
|
||||
|
||||
function handlePlacement(x: number, y: number) {
|
||||
const shipCoords = generateShipCoordinates(
|
||||
x,
|
||||
y,
|
||||
ship_sizes[shipBeingPlaced],
|
||||
orientation,
|
||||
);
|
||||
|
||||
if (game_state === 1 && isSelf) {
|
||||
const shipData = {
|
||||
player: player,
|
||||
name: shipBeingPlaced,
|
||||
coords: shipCoords,
|
||||
};
|
||||
|
||||
act('place_ship', {
|
||||
ship: shipData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getCellStyle(x: number, y: number) {
|
||||
const key = `${x},${y}`;
|
||||
let cellStyle: React.CSSProperties = {};
|
||||
|
||||
if (visible_ships) {
|
||||
for (const ship of visible_ships) {
|
||||
if (
|
||||
ship.coords?.some((coord) => coord[0] === x && coord[1] === y) &&
|
||||
ship.player === player
|
||||
) {
|
||||
cellStyle.backgroundColor = 'gray';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (game_state >= 2) {
|
||||
const shot = shotsFired[key];
|
||||
if (shot === 1) {
|
||||
cellStyle.backgroundColor = 'red';
|
||||
} else if (shot === 0) {
|
||||
cellStyle.backgroundColor = 'white';
|
||||
}
|
||||
}
|
||||
|
||||
for (const ship of destroyedShips) {
|
||||
if (ship.coords?.some((coord) => coord[0] === x && coord[1] === y)) {
|
||||
cellStyle = {
|
||||
border: '2px solid gold',
|
||||
backgroundColor: 'maroon',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return cellStyle;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack backgroundColor="#474747" vertical>
|
||||
<Stack.Item>
|
||||
{Array.from({ length: 11 }, (_, row) => (
|
||||
<Stack key={row}>
|
||||
{Array.from({ length: 11 }, (_, col) => {
|
||||
const x = col;
|
||||
const y = row;
|
||||
const key = `${x},${y}`;
|
||||
|
||||
const isHighlighted =
|
||||
highlightedCells.some((cell) => cell.x === x && cell.y === y) &&
|
||||
game_state === 1 &&
|
||||
isSelf;
|
||||
|
||||
return (
|
||||
<Stack.Item key={key} align="center" textAlign="center">
|
||||
{col === 0 && row === 0 ? (
|
||||
<Box width={1} />
|
||||
) : col === 0 && row > 0 ? (
|
||||
<Box width={1}>{row - 1}</Box>
|
||||
) : row === 0 && col > 0 ? (
|
||||
<Box width={5}>{numToLetter[col - 1]}</Box>
|
||||
) : (
|
||||
<div onMouseEnter={() => handleButtonHover(x, y)}>
|
||||
<Button
|
||||
disabled={
|
||||
((game_state === 2 && isOponent) ||
|
||||
isSelf ||
|
||||
game_state === 4) &&
|
||||
!(isSelf && isOponent)
|
||||
}
|
||||
onClick={(_) => {
|
||||
if (game_state === 1) {
|
||||
handlePlacement(x, y);
|
||||
return;
|
||||
}
|
||||
act('game_action', {
|
||||
action: 'fire_shot',
|
||||
data: { loc_x: x, loc_y: y, player: player },
|
||||
});
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
if (game_state === 1) {
|
||||
act('remove_ship', {
|
||||
loc_x: x,
|
||||
loc_y: y,
|
||||
player: player,
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: isHighlighted
|
||||
? invalidCells
|
||||
? 'rgba(255, 0, 0, 0.5)'
|
||||
: 'rgba(76, 175, 80, 0.5)'
|
||||
: getCellStyle(x, y).backgroundColor,
|
||||
border: getCellStyle(x, y).border,
|
||||
}}
|
||||
width={5}
|
||||
height={5}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Stack.Item>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, Section, Stack } from 'tgui-core/components';
|
||||
import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip';
|
||||
import { gameStateToText, gameTooltip } from './constants';
|
||||
import { stateToColor } from './functions';
|
||||
import { PlayerPanel } from './PlayerPanel';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const PlayerMenu = (props: {
|
||||
playerOnePlaceShip: string;
|
||||
onPlayerOnePlaceShip: React.Dispatch<React.SetStateAction<string>>;
|
||||
playerTwoPlaceShip: string;
|
||||
onPlayerTwoPlaceShip: React.Dispatch<React.SetStateAction<string>>;
|
||||
playerOneOrientation: 'horizontal' | 'vertical';
|
||||
onPlayerOneOrientation: React.Dispatch<
|
||||
React.SetStateAction<'horizontal' | 'vertical'>
|
||||
>;
|
||||
playerTwoOrientation: 'horizontal' | 'vertical';
|
||||
onPlayerTwoOrientation: React.Dispatch<
|
||||
React.SetStateAction<'horizontal' | 'vertical'>
|
||||
>;
|
||||
}) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const {
|
||||
ship_sizes,
|
||||
player_one,
|
||||
player_two,
|
||||
all_placed,
|
||||
ship_count_pone,
|
||||
ship_count_ptwo,
|
||||
game_state,
|
||||
winner,
|
||||
has_won,
|
||||
} = data;
|
||||
|
||||
const {
|
||||
playerOnePlaceShip,
|
||||
onPlayerOnePlaceShip,
|
||||
playerTwoPlaceShip,
|
||||
onPlayerTwoPlaceShip,
|
||||
playerOneOrientation,
|
||||
onPlayerOneOrientation,
|
||||
playerTwoOrientation,
|
||||
onPlayerTwoOrientation,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Game Status"
|
||||
buttons={
|
||||
<Stack>
|
||||
{(game_state > 0 && (
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('clear_game');
|
||||
}}
|
||||
>
|
||||
Clear Game
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
)) ||
|
||||
(game_state === 0 && !!player_one && !!player_two && (
|
||||
<>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('prepare_game');
|
||||
}}
|
||||
>
|
||||
Prepare Game
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('swap_players');
|
||||
}}
|
||||
>
|
||||
Swap Players
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
</>
|
||||
))}
|
||||
{game_state === 1 && !!player_one && !!player_two && !!all_placed && (
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('start_game');
|
||||
}}
|
||||
>
|
||||
Start Game
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
)}
|
||||
{!!player_one && !!player_two && game_state === 4 && (
|
||||
<>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('play_again');
|
||||
}}
|
||||
>
|
||||
Play Again
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('play_again_swapped');
|
||||
}}
|
||||
>
|
||||
Play Again (Swapped Sides)
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
</>
|
||||
)}
|
||||
<Stack.Item>
|
||||
<Button onClick={() => act('invite_player')}>Invite Player</Button>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<VorePanelTooltip tooltip={gameTooltip} displayText="?" />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Stack fill>
|
||||
<PlayerPanel
|
||||
gameState={game_state}
|
||||
player={player_one}
|
||||
num={1}
|
||||
availableShips={ship_sizes}
|
||||
shipCount={ship_count_pone}
|
||||
playerPlaceShip={playerOnePlaceShip}
|
||||
onPlayerPlaceShip={onPlayerOnePlaceShip}
|
||||
playerRotation={playerOneOrientation}
|
||||
onPlayerRotation={onPlayerOneOrientation}
|
||||
/>
|
||||
<Stack.Item basis="33%">
|
||||
<Stack vertical textAlign="center">
|
||||
<Stack.Item>
|
||||
<Box color="label">Status:</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box color={stateToColor(game_state, !!has_won)}>
|
||||
{gameStateToText[game_state] ?? `${winner} Won`}
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<PlayerPanel
|
||||
gameState={game_state}
|
||||
player={player_two}
|
||||
num={2}
|
||||
availableShips={ship_sizes}
|
||||
shipCount={ship_count_ptwo}
|
||||
playerPlaceShip={playerTwoPlaceShip}
|
||||
onPlayerPlaceShip={onPlayerTwoPlaceShip}
|
||||
playerRotation={playerTwoOrientation}
|
||||
onPlayerRotation={onPlayerTwoOrientation}
|
||||
/>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, LabeledList, Stack } from 'tgui-core/components';
|
||||
import { numToText } from './constants';
|
||||
import { getNextAvailableShip } from './functions';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const PlayerPanel = (props: {
|
||||
gameState: number;
|
||||
player: string | null;
|
||||
num: number;
|
||||
availableShips: Record<string, number>;
|
||||
shipCount: Record<string, number>;
|
||||
playerPlaceShip: string;
|
||||
onPlayerPlaceShip: React.Dispatch<React.SetStateAction<string>>;
|
||||
playerRotation: 'horizontal' | 'vertical';
|
||||
onPlayerRotation: React.Dispatch<
|
||||
React.SetStateAction<'horizontal' | 'vertical'>
|
||||
>;
|
||||
}) => {
|
||||
const { data, act } = useBackend<Data>();
|
||||
const { current_player } = data;
|
||||
const {
|
||||
gameState,
|
||||
player,
|
||||
num,
|
||||
shipCount,
|
||||
availableShips,
|
||||
playerPlaceShip,
|
||||
onPlayerPlaceShip,
|
||||
playerRotation,
|
||||
onPlayerRotation,
|
||||
} = props;
|
||||
|
||||
const player_string = numToText[num - 1];
|
||||
|
||||
useEffect(() => {
|
||||
if (gameState !== 1) return;
|
||||
console.log(shipCount[playerPlaceShip]);
|
||||
if (!shipCount[playerPlaceShip]) {
|
||||
const nextShip = getNextAvailableShip(playerPlaceShip, shipCount);
|
||||
if (nextShip) {
|
||||
onPlayerPlaceShip(nextShip);
|
||||
}
|
||||
}
|
||||
}, [gameState, playerPlaceShip, shipCount, onPlayerPlaceShip]);
|
||||
|
||||
return (
|
||||
<Stack.Item basis="33%">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label={`Player ${num}`}>
|
||||
<Stack>
|
||||
<Stack.Item grow>
|
||||
<Button.Confirm
|
||||
disabled={gameState !== 0}
|
||||
onClick={() => act(`be_player_${player_string}`)}
|
||||
>
|
||||
{player || `Be player ${num}`}
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
icon="arrow-rotate-right"
|
||||
onClick={() =>
|
||||
onPlayerRotation(
|
||||
playerRotation === 'horizontal' ? 'vertical' : 'horizontal',
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label={`Remaining Ships`}>
|
||||
<Stack vertical>
|
||||
{Object.keys(availableShips).map((ship) => {
|
||||
const remaining = shipCount[ship] || 0;
|
||||
return (
|
||||
<Stack.Item key={ship}>
|
||||
<Button.Checkbox
|
||||
checked={playerPlaceShip === ship}
|
||||
onClick={() => onPlayerPlaceShip(ship)}
|
||||
disabled={player !== current_player}
|
||||
>
|
||||
<Box inline>
|
||||
{ship}: {remaining} - Size: {availableShips[ship]}
|
||||
</Box>
|
||||
</Button.Checkbox>
|
||||
</Stack.Item>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Stack.Item>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
export const numToText = ['one', 'two'] as const;
|
||||
|
||||
export const numToLetter = [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
] as const;
|
||||
|
||||
export const gameStateToText = [
|
||||
'Setup',
|
||||
'Ship Placement',
|
||||
'Player 1',
|
||||
'Player 2',
|
||||
undefined,
|
||||
] as const;
|
||||
|
||||
export const gameTooltip =
|
||||
"A two player game where the goal is to sink the opponent's ships. To start the game, join with a friend and switch to the setup phase. " +
|
||||
'Once there, each player places their ships. As soon as both players have placed their ships, you can start playing. ' +
|
||||
"The goal is to sink the opponent's ships by hitting them during your turn. You can place your ships by clicking onto the playfield. " +
|
||||
'The current position is shown as a preview. Left click places a ship. You can rotate it with a middle click or remove it with a right click. ' +
|
||||
"To switch the ship type, use the mousewheel. Click on your opponent's field during your turn to fire at the selected location.";
|
||||
@@ -0,0 +1,40 @@
|
||||
export function stateToColor(
|
||||
gameState: number,
|
||||
isWinner: boolean,
|
||||
): string | undefined {
|
||||
if (gameState < 4) {
|
||||
return undefined;
|
||||
}
|
||||
return isWinner ? 'green' : 'red';
|
||||
}
|
||||
|
||||
export function generateShipCoordinates(
|
||||
x: number,
|
||||
y: number,
|
||||
size: number,
|
||||
orientation: 'horizontal' | 'vertical',
|
||||
) {
|
||||
const coords: [number, number][] = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
const newX = orientation === 'horizontal' ? x + i : x;
|
||||
const newY = orientation === 'vertical' ? y + i : y;
|
||||
coords.push([newX, newY]);
|
||||
}
|
||||
return coords;
|
||||
}
|
||||
|
||||
export function getNextAvailableShip(
|
||||
currentShip: string,
|
||||
remainingShips: Record<string, number>,
|
||||
) {
|
||||
const availableShipNames = Object.keys(remainingShips);
|
||||
const currentShipIndex = availableShipNames.indexOf(currentShip);
|
||||
|
||||
for (let i = currentShipIndex + 1; i < availableShipNames.length; i++) {
|
||||
const nextShip = availableShipNames[i];
|
||||
if (remainingShips[nextShip] > 0) {
|
||||
return nextShip;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { Stack } from 'tgui-core/components';
|
||||
import { GameArea } from './GameArea';
|
||||
import { PlayerMenu } from './PlayerMenu';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const SpaceBattle = (props) => {
|
||||
const { data } = useBackend<Data>();
|
||||
const { ship_sizes, game_state, current_player, player_one, player_two } =
|
||||
data;
|
||||
|
||||
const [playerOnePlaceShip, setPlayerOnePlaceShip] = useState<string>(
|
||||
Object.keys(ship_sizes)[0],
|
||||
);
|
||||
const [playerTwoPlaceShip, setPlayerTwoPlaceShip] = useState<string>(
|
||||
Object.keys(ship_sizes)[0],
|
||||
);
|
||||
|
||||
const [playerOneOrientation, setPlayerOneOrientation] = useState<
|
||||
'horizontal' | 'vertical'
|
||||
>('horizontal');
|
||||
const [playerTwoOrientation, setPlayerTwoOrientation] = useState<
|
||||
'horizontal' | 'vertical'
|
||||
>('horizontal');
|
||||
|
||||
const availableShips = Object.keys(ship_sizes);
|
||||
|
||||
const [mousePos, setMousePos] = useState<{ x: number; y: number }>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
setMousePos({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (e.button === 1) {
|
||||
if (game_state !== 1) return;
|
||||
|
||||
const playerOneGrid = { x1: 0, y1: 0, x2: 650, y2: 910 };
|
||||
const playerTwoGrid = { x1: 700, y1: 0, x2: 1400, y2: 910 };
|
||||
|
||||
const isPlayerOne =
|
||||
mousePos.x >= playerOneGrid.x1 &&
|
||||
mousePos.x <= playerOneGrid.x2 &&
|
||||
mousePos.y >= playerOneGrid.y1 &&
|
||||
mousePos.y <= playerOneGrid.y2;
|
||||
|
||||
const isPlayerTwo =
|
||||
mousePos.x >= playerTwoGrid.x1 &&
|
||||
mousePos.x <= playerTwoGrid.x2 &&
|
||||
mousePos.y >= playerTwoGrid.y1 &&
|
||||
mousePos.y <= playerTwoGrid.y2;
|
||||
|
||||
if (isPlayerOne && current_player === player_one) {
|
||||
setPlayerOneOrientation((prev) =>
|
||||
prev === 'horizontal' ? 'vertical' : 'horizontal',
|
||||
);
|
||||
}
|
||||
|
||||
if (isPlayerTwo && current_player === player_two) {
|
||||
setPlayerTwoOrientation((prev) =>
|
||||
prev === 'horizontal' ? 'vertical' : 'horizontal',
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mousedown', handleMouseDown);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mousedown', handleMouseDown);
|
||||
};
|
||||
}, [mousePos, game_state, current_player, player_one, player_two]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (game_state !== 1) return;
|
||||
|
||||
const playerOneGrid = { x1: 0, y1: 0, x2: 650, y2: 910 };
|
||||
const playerTwoGrid = { x1: 700, y1: 0, x2: 1400, y2: 910 };
|
||||
|
||||
const isPlayerOne =
|
||||
mousePos.x >= playerOneGrid.x1 &&
|
||||
mousePos.x <= playerOneGrid.x2 &&
|
||||
mousePos.y >= playerOneGrid.y1 &&
|
||||
mousePos.y <= playerOneGrid.y2;
|
||||
|
||||
const isPlayerTwo =
|
||||
mousePos.x >= playerTwoGrid.x1 &&
|
||||
mousePos.x <= playerTwoGrid.x2 &&
|
||||
mousePos.y >= playerTwoGrid.y1 &&
|
||||
mousePos.y <= playerTwoGrid.y2;
|
||||
|
||||
if (isPlayerOne && current_player === player_one) {
|
||||
const currentShipIndex = availableShips.indexOf(playerOnePlaceShip);
|
||||
let newShipIndex = currentShipIndex;
|
||||
if (e.deltaY > 0) {
|
||||
newShipIndex = (currentShipIndex + 1) % availableShips.length;
|
||||
} else if (e.deltaY < 0) {
|
||||
newShipIndex =
|
||||
(currentShipIndex - 1 + availableShips.length) %
|
||||
availableShips.length;
|
||||
}
|
||||
setPlayerOnePlaceShip(availableShips[newShipIndex]);
|
||||
}
|
||||
|
||||
if (isPlayerTwo && current_player === player_two) {
|
||||
const currentShipIndex = availableShips.indexOf(playerTwoPlaceShip);
|
||||
let newShipIndex = currentShipIndex;
|
||||
if (e.deltaY > 0) {
|
||||
newShipIndex = (currentShipIndex + 1) % availableShips.length;
|
||||
} else if (e.deltaY < 0) {
|
||||
newShipIndex =
|
||||
(currentShipIndex - 1 + availableShips.length) %
|
||||
availableShips.length;
|
||||
}
|
||||
setPlayerTwoPlaceShip(availableShips[newShipIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('wheel', handleWheel);
|
||||
return () => {
|
||||
window.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, [
|
||||
game_state,
|
||||
current_player,
|
||||
playerOnePlaceShip,
|
||||
playerTwoPlaceShip,
|
||||
availableShips,
|
||||
mousePos,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Window width={1400} height={910}>
|
||||
<Window.Content>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
<PlayerMenu
|
||||
playerOnePlaceShip={playerOnePlaceShip}
|
||||
onPlayerOnePlaceShip={setPlayerOnePlaceShip}
|
||||
playerTwoPlaceShip={playerTwoPlaceShip}
|
||||
onPlayerTwoPlaceShip={setPlayerTwoPlaceShip}
|
||||
playerOneOrientation={playerOneOrientation}
|
||||
onPlayerOneOrientation={setPlayerOneOrientation}
|
||||
playerTwoOrientation={playerTwoOrientation}
|
||||
onPlayerTwoOrientation={setPlayerTwoOrientation}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<GameArea
|
||||
playerOnePlaceShip={playerOnePlaceShip}
|
||||
playerTwoPlaceShip={playerTwoPlaceShip}
|
||||
playerOneOrientation={playerOneOrientation}
|
||||
playerTwoOrientation={playerTwoOrientation}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
export type Ship = {
|
||||
name: string;
|
||||
size: number;
|
||||
coords?: [number, number][];
|
||||
player?: number;
|
||||
};
|
||||
|
||||
export type Data = {
|
||||
current_player: string;
|
||||
ship_sizes: Record<string, number>;
|
||||
total_ships: Record<string, number>;
|
||||
player_one: string;
|
||||
player_two: string;
|
||||
ships_have_been_placed: BooleanLike;
|
||||
shots_fired_pone: Record<string, number>;
|
||||
shots_fired_ptwo: Record<string, number>;
|
||||
destroyed_ships_pone: Ship[];
|
||||
destroyed_ships_ptwo: Ship[];
|
||||
visible_ships: Ship[];
|
||||
ship_count_pone: Record<string, number>;
|
||||
ship_count_ptwo: Record<string, number>;
|
||||
game_state: number;
|
||||
winner: string;
|
||||
has_won: BooleanLike;
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import { isNumber } from 'es-toolkit';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Box, Button, Section, Stack } from 'tgui-core/components';
|
||||
import { numberToColor } from './constants';
|
||||
import { checkDisabled } from './functions';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const GameArea = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
const [mineHit, setMineHit] = useState('');
|
||||
|
||||
const {
|
||||
grid_size,
|
||||
revealed_fields,
|
||||
placed_flags,
|
||||
game_state,
|
||||
placed_mines,
|
||||
is_dealer,
|
||||
} = data;
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(revealed_fields).length === 0) {
|
||||
setMineHit('');
|
||||
}
|
||||
}, [revealed_fields]);
|
||||
|
||||
useEffect(() => {
|
||||
for (const key in revealed_fields) {
|
||||
if (revealed_fields[key] === 'M') {
|
||||
setMineHit(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [revealed_fields]);
|
||||
|
||||
const getTileContent = (x: number, y: number) => {
|
||||
const key = `${x},${y}`;
|
||||
|
||||
if (revealed_fields[key] !== undefined) {
|
||||
if (revealed_fields[key] === 'M') {
|
||||
return '💣';
|
||||
}
|
||||
return revealed_fields[key];
|
||||
}
|
||||
|
||||
if (placed_flags[key]) {
|
||||
if (game_state > 1 && !placed_mines?.[key]) {
|
||||
return <Box style={{ textDecoration: 'line-through' }}>🚩</Box>;
|
||||
}
|
||||
return '🚩';
|
||||
}
|
||||
|
||||
if ((game_state > 1 || is_dealer) && placed_mines && placed_mines[key]) {
|
||||
return game_state === 3 ? '🚩' : '💣';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<Section
|
||||
fill
|
||||
title="Game Grid"
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Stack justify="center">
|
||||
<Stack.Item>
|
||||
<Stack vertical>
|
||||
<Stack.Item>
|
||||
{Array.from({ length: grid_size }, (_, row) => (
|
||||
<Stack key={row}>
|
||||
{Array.from({ length: grid_size }, (_, col) => {
|
||||
const x = col + 1;
|
||||
const y = row + 1;
|
||||
const key = `${x},${y}`;
|
||||
const displayText = getTileContent(x, y);
|
||||
return (
|
||||
<Stack.Item key={key}>
|
||||
<Button
|
||||
disabled={checkDisabled(
|
||||
revealed_fields[key] !== undefined,
|
||||
game_state,
|
||||
!!is_dealer,
|
||||
)}
|
||||
color={mineHit === key && 'red '}
|
||||
onClick={(_) => {
|
||||
act(is_dealer ? 'setup_action' : 'game_action', {
|
||||
action: is_dealer ? 'place_mine' : 'open_field',
|
||||
data: { loc_x: x, loc_y: y },
|
||||
});
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault();
|
||||
act(is_dealer ? 'setup_action' : 'game_action', {
|
||||
action: is_dealer ? 'remove_mine' : 'toggle_flag',
|
||||
data: { loc_x: x, loc_y: y },
|
||||
});
|
||||
}}
|
||||
textColor={
|
||||
isNumber(displayText) && numberToColor[displayText]
|
||||
}
|
||||
width={5}
|
||||
height={5}
|
||||
fontSize={2.5}
|
||||
innerStyle={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
lineHeight: '60px',
|
||||
}}
|
||||
>
|
||||
{!!displayText && displayText}
|
||||
</Button>
|
||||
</Stack.Item>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Icon,
|
||||
LabeledList,
|
||||
NumberInput,
|
||||
Section,
|
||||
Stack,
|
||||
} from 'tgui-core/components';
|
||||
import { VorePanelTooltip } from '../VorePanel/VorePanelElements/VorePanelTooltip';
|
||||
import { gameStateToColor, gameStateToIcon, gameTooltip } from './constants';
|
||||
import { getRemainingMines, getScoreColor } from './functions';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const PlayerMenu = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
const {
|
||||
grid_size,
|
||||
mine_count,
|
||||
max_mines,
|
||||
dealer,
|
||||
placed_mines,
|
||||
placed_flags,
|
||||
game_state,
|
||||
is_dealer,
|
||||
} = data;
|
||||
|
||||
const remainingMines = getRemainingMines(
|
||||
game_state,
|
||||
mine_count,
|
||||
!!is_dealer,
|
||||
placed_mines,
|
||||
placed_flags,
|
||||
);
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Game Status"
|
||||
buttons={
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
{(game_state > 0 && (
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('restart_game');
|
||||
}}
|
||||
>
|
||||
Restart Game
|
||||
</Button.Confirm>
|
||||
)) ||
|
||||
(game_state === 0 && dealer && !remainingMines && (
|
||||
<Button.Confirm
|
||||
onClick={() => {
|
||||
act('setup_action', { action: 'start_game' });
|
||||
}}
|
||||
>
|
||||
Start Game
|
||||
</Button.Confirm>
|
||||
))}
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button onClick={() => act('invite_player')}>Invite Player</Button>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<VorePanelTooltip tooltip={gameTooltip} displayText="?" />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Stack vertical>
|
||||
<Stack.Item>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Dealer">
|
||||
{dealer ? (
|
||||
<Button.Confirm onClick={() => act('clear_dealer')}>
|
||||
Clear Dealer ({dealer})
|
||||
</Button.Confirm>
|
||||
) : game_state !== 1 ? (
|
||||
<Button onClick={() => act('be_dealer')}>Be Dealer</Button>
|
||||
) : (
|
||||
'Single Player Mode'
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Grid Size">
|
||||
{is_dealer ? (
|
||||
<NumberInput
|
||||
width="30px"
|
||||
value={grid_size}
|
||||
minValue={4}
|
||||
maxValue={16}
|
||||
stepPixelSize={40}
|
||||
onChange={(value) =>
|
||||
act('setup_action', {
|
||||
action: 'change_grid_size',
|
||||
data: { new_grid: value },
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
grid_size
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Mine Count">
|
||||
{is_dealer ? (
|
||||
<NumberInput
|
||||
width="40px"
|
||||
value={mine_count}
|
||||
minValue={
|
||||
placed_mines ? Object.keys(placed_mines).length || 1 : 1
|
||||
}
|
||||
maxValue={max_mines}
|
||||
stepPixelSize={10}
|
||||
onChange={(value) =>
|
||||
act('setup_action', {
|
||||
action: 'change_mine_count',
|
||||
data: { new_mines: value },
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
mine_count
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
{!!is_dealer && (
|
||||
<LabeledList.Item label="Ranomize Mines">
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() =>
|
||||
act('setup_action', { action: 'auto_place_mines' })
|
||||
}
|
||||
>
|
||||
Place All
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() =>
|
||||
act('setup_action', { action: 'clear_all_mines' })
|
||||
}
|
||||
>
|
||||
Clear All
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
{(!placed_mines || !Object.keys(placed_mines).length) && (
|
||||
<Stack.Item>
|
||||
<Button.Confirm
|
||||
onClick={() =>
|
||||
act('setup_action', {
|
||||
action: 'auto_place_mines_self',
|
||||
})
|
||||
}
|
||||
>
|
||||
Play Alone
|
||||
</Button.Confirm>
|
||||
</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
</LabeledList.Item>
|
||||
)}
|
||||
</LabeledList>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack fill>
|
||||
<Stack.Item grow>
|
||||
<Box preserveWhitespace inline color="label">
|
||||
{`Remaining Mines: `}
|
||||
</Box>
|
||||
<Box
|
||||
fontSize={2}
|
||||
inline
|
||||
color={getScoreColor(remainingMines, game_state)}
|
||||
>
|
||||
{remainingMines}
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Icon
|
||||
size={2}
|
||||
name={gameStateToIcon[game_state]}
|
||||
color={gameStateToColor[game_state]}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
export const gameStateToIcon = [
|
||||
'gear',
|
||||
'face-grimace',
|
||||
'skull-crossbones',
|
||||
'face-grin-stars',
|
||||
] as const;
|
||||
|
||||
export const gameStateToColor = ['gray', 'yellow', 'red', 'green'] as const;
|
||||
|
||||
export const numberToColor = [
|
||||
undefined,
|
||||
'#85ceff',
|
||||
'#12ff12',
|
||||
'#ff0000',
|
||||
'#ff00ff',
|
||||
'#ff7b00',
|
||||
'cyan',
|
||||
'black',
|
||||
'white',
|
||||
] as const;
|
||||
|
||||
export const gameTooltip =
|
||||
'A two player or singleplayer game where the goal is to reveal all fields without hitting any mines. ' +
|
||||
'Have a friend be the dealer and set the playfield up, set it how you want it, or randomize and then play on your own.' +
|
||||
'Left click to reveal a field. Right click to place a flag.';
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
export function checkDisabled(
|
||||
revealed: boolean,
|
||||
gameState: number,
|
||||
isDealer: boolean,
|
||||
): boolean {
|
||||
if (isDealer && gameState > 0) {
|
||||
return true;
|
||||
}
|
||||
if (!isDealer && gameState < 1 && gameState > 2) return true;
|
||||
if (revealed) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getRemainingMines(
|
||||
gameState: number,
|
||||
mineCount: number,
|
||||
isDealer: boolean,
|
||||
placedMines: Record<string, BooleanLike> | null,
|
||||
placedFlags: Record<string, BooleanLike>,
|
||||
): number {
|
||||
if (gameState === 3) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const mineTotal = Object.keys(placedMines || {}).length;
|
||||
const flagTotal = Object.keys(placedFlags).length;
|
||||
|
||||
return mineCount - (isDealer ? mineTotal : flagTotal);
|
||||
}
|
||||
|
||||
export function getScoreColor(
|
||||
remainingMines: number,
|
||||
game_state: number,
|
||||
): string | undefined {
|
||||
if (remainingMines < 0 || (!remainingMines && game_state === 1)) {
|
||||
return 'red';
|
||||
}
|
||||
if (game_state === 3) {
|
||||
return 'green';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { Stack } from 'tgui-core/components';
|
||||
import { GameArea } from './GameArea';
|
||||
import { PlayerMenu } from './PlayerMenu';
|
||||
import type { Data } from './types';
|
||||
|
||||
export const VoreSweeper = (props) => {
|
||||
const { data } = useBackend<Data>();
|
||||
|
||||
const { grid_size, is_dealer } = data;
|
||||
const added_height = grid_size * 64;
|
||||
const added_width = grid_size * 66;
|
||||
return (
|
||||
<Window
|
||||
width={Math.max(25 + added_width, 350)}
|
||||
height={(is_dealer ? 260 : 230) + added_height}
|
||||
>
|
||||
<Window.Content>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
<PlayerMenu />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<GameArea />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
export type Data = {
|
||||
grid_size: number;
|
||||
mine_count: number;
|
||||
max_mines: number;
|
||||
dealer: string | null;
|
||||
placed_mines: Record<string, BooleanLike> | null;
|
||||
revealed_fields: Record<string, number | string>;
|
||||
placed_flags: Record<string, BooleanLike>;
|
||||
game_state: number;
|
||||
is_dealer: BooleanLike;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
.FourInARow__Tile {
|
||||
border: 2px solid gray;
|
||||
background: black;
|
||||
box-shadow: 0 0 5px white;
|
||||
transform: scale(0.75);
|
||||
|
||||
&.winning-tile {
|
||||
border: 3px solid gold;
|
||||
box-shadow: 0 0 15px 5px gold;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
&.placed {
|
||||
border: 2px solid black;
|
||||
box-shadow:
|
||||
inset 0 2px 5px rgba(255, 255, 255, 0.6),
|
||||
0 2px 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.TicTacToe__Tile {
|
||||
border: 2px solid gray;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2.5rem;
|
||||
|
||||
&.winning-tile {
|
||||
text-shadow:
|
||||
0 0 5px gold,
|
||||
0 0 10px gold,
|
||||
0 0 15px gold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.NineMen__Tile {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
line-height: '60px';
|
||||
justify-content: 'center';
|
||||
display: 'flex';
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
.RpgDice__dice {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
font-family: 'Arial', sans-serif;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.RpgDice__dice.d4 {
|
||||
background-color: rgb(255, 115, 0);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
|
||||
}
|
||||
|
||||
.RpgDice__dice.d6 {
|
||||
background-color: #ad00c4;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 10px;
|
||||
box-shadow: inset 0 0 0 1px #ad00c4;
|
||||
}
|
||||
|
||||
.RpgDice__dice.d8 {
|
||||
background-color: #00a6ffcf;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
|
||||
}
|
||||
|
||||
.RpgDice__dice.d10 {
|
||||
background-color: #00d9ff;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
clip-path: polygon(50% 0%, 100% 15%, 100% 85%, 50% 100%, 0% 85%, 0% 15%);
|
||||
}
|
||||
|
||||
.RpgDice__dice.d12 {
|
||||
background-color: #ffc400;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
clip-path: polygon(50% 0%, 100% 20%, 80% 80%, 20% 80%, 0% 20%);
|
||||
}
|
||||
|
||||
.RpgDice__dice.d20 {
|
||||
background-color: #ff0000;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
clip-path: polygon(
|
||||
50% 0%,
|
||||
100% 10%,
|
||||
90% 50%,
|
||||
90% 50%,
|
||||
100% 90%,
|
||||
50% 100%,
|
||||
0% 90%,
|
||||
10% 50%,
|
||||
10% 50%,
|
||||
0% 10%
|
||||
);
|
||||
}
|
||||
|
||||
.RpgDice__dice.d50 {
|
||||
background-color: #ff66cc;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
clip-path: polygon(
|
||||
50% 0%,
|
||||
80% 20%,
|
||||
100% 50%,
|
||||
80% 80%,
|
||||
50% 100%,
|
||||
20% 80%,
|
||||
0% 50%,
|
||||
20% 20%
|
||||
);
|
||||
}
|
||||
|
||||
.RpgDice__dice.d100 {
|
||||
background-color: #00ff00;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
clip-path: polygon(
|
||||
50% 0%,
|
||||
85% 15%,
|
||||
100% 50%,
|
||||
85% 85%,
|
||||
50% 100%,
|
||||
15% 85%,
|
||||
0% 50%,
|
||||
15% 15%
|
||||
);
|
||||
}
|
||||
|
||||
.RpgDice__dice .Button__content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -58,6 +58,9 @@
|
||||
@include meta.load-css('./interfaces/Wires.scss');
|
||||
@include meta.load-css('./interfaces/PlushEditor.scss');
|
||||
@include meta.load-css('./interfaces/ColorMatrixEditor.scss');
|
||||
@include meta.load-css('./interfaces/FourInARow.scss');
|
||||
@include meta.load-css('./interfaces/RpgDice.scss');
|
||||
@include meta.load-css('./interfaces/NineMen.scss');
|
||||
|
||||
// Layouts
|
||||
@include meta.load-css('./layouts/Layout.scss');
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
#include "code\__defines\belly_messages.dm"
|
||||
#include "code\__defines\belly_modes_vr.dm"
|
||||
#include "code\__defines\blueprints.dm"
|
||||
#include "code\__defines\board_game.dm"
|
||||
#include "code\__defines\borer.dm"
|
||||
#include "code\__defines\borg_overlays.dm"
|
||||
#include "code\__defines\callbacks.dm"
|
||||
@@ -2400,6 +2401,15 @@
|
||||
#include "code\modules\casino\premium_vendors.dm"
|
||||
#include "code\modules\casino\slots.dm"
|
||||
#include "code\modules\casino\spawnergrenade_casino.dm"
|
||||
#include "code\modules\casino\board_games\board_table.dm"
|
||||
#include "code\modules\casino\board_games\checkers.dm"
|
||||
#include "code\modules\casino\board_games\chess.dm"
|
||||
#include "code\modules\casino\board_games\four_in_a_row.dm"
|
||||
#include "code\modules\casino\board_games\nine_mens.dm"
|
||||
#include "code\modules\casino\board_games\rpg_dice.dm"
|
||||
#include "code\modules\casino\board_games\space_battle.dm"
|
||||
#include "code\modules\casino\board_games\tic_tac_toe.dm"
|
||||
#include "code\modules\casino\board_games\vore_sweeper.dm"
|
||||
#include "code\modules\catalogue\atoms.dm"
|
||||
#include "code\modules\catalogue\catalogue_data.dm"
|
||||
#include "code\modules\catalogue\catalogue_data_vr.dm"
|
||||
@@ -4139,6 +4149,7 @@
|
||||
#include "code\modules\pda\cart_apps.dm"
|
||||
#include "code\modules\pda\cart_vr.dm"
|
||||
#include "code\modules\pda\core_apps.dm"
|
||||
#include "code\modules\pda\game_launcher.dm"
|
||||
#include "code\modules\pda\messenger.dm"
|
||||
#include "code\modules\pda\messenger_plugins.dm"
|
||||
#include "code\modules\pda\nerdle.dm"
|
||||
@@ -4735,6 +4746,7 @@
|
||||
#include "code\modules\tgui\modules\ntos-only\uav.dm"
|
||||
#include "code\modules\tgui\states\admin.dm"
|
||||
#include "code\modules\tgui\states\always.dm"
|
||||
#include "code\modules\tgui\states\board_game.dm"
|
||||
#include "code\modules\tgui\states\conscious.dm"
|
||||
#include "code\modules\tgui\states\contained.dm"
|
||||
#include "code\modules\tgui\states\deep_inventory.dm"
|
||||
|
||||
Reference in New Issue
Block a user