As a final project for a software engineering bootcamp, I decided to make a text-based multiplayer RPG game engine. Using Python, Flask, & Flask-SocketIO for the backend and React & TypeScript for the frontend I've gotten my project to a point where it functions (mostly) the way I imagined initially. However, I want to continue developing the game on the side as a hobby, and I feel like there are numerous things that should be optimized before I start adding more functionality to it.
A few things I feel like could be improved:
- Combat is currently a caricature of combat. I'm still trying to figure out the best way to handle engaging another entity in combat and the process behind retaliating against an entity attacking you
- The way that objects are instantiated into the world currently doesn't lend itself to world persistence. Is pickling/dilling/shelving every object the best way to handle saving the world, or should I be using a db?
- The classes for all entities. I'm unsure if I'm duplicating information with the way I reference certain objects, and if all the attributes are necessary.
- Text input parsing. Currently my parsing is just splitting the input from a user into the first word and the rest of the data, and filtering it through many "if" statements. Is there a more efficient way of handling this?
Most relevant code:
Backend:
events.py: run when users connect or disconnect
from flask import session, request, current_app
from ... import socketio
from flask_socketio import join_room, leave_room, emit
from app.models import PlayerAccount, User
import app.blueprints.main.objects as objects
import dill
#Socket IO events, should only be three. On connection, on disconnect, and whenever data is sent
client_list = [] #List of clients currently connected
world = objects.World() #Instatiating world class to hold all rooms, players, and characters
def world_timer(app):
print('world timer triggered')
socketio.sleep(10)
with app.app_context():
while True:
if client_list:
socketio.sleep(10)
for character in world.npcs.values():
character.ambiance()
for room in world.rooms.values():
room.ambiance()
for player in world.players.values():
player_account = PlayerAccount.query.get(player.id)
player_account.player_info = dill.dumps(player)
player_account.save()
else:
world.timer_active = False
print('world timer deactivated')
break
#This is an event that occurs whenever a new connection is detected by the socketio server. Connection needs to properly connect the user with their Player object, update the Player object's session_id so private server emits can be transmitted to that player only
@socketio.on('connect')
def connect(auth):
session['user_id'] = auth
print(session['user_id'])
current_user = User.query.filter(User.id == auth).first()
active_player = current_user.accounts.filter(PlayerAccount.is_active == True).first() #Pulls the active player information
if not active_player.player_info: #Checks to see if the active player is a new player
player = objects.Player(id=active_player.id, name=active_player.player_name, description="A newborn player, fresh to the world.", account=active_player.user_id)
#Creates a new player object
active_player.player_info = dill.dumps(player) #Pickles and writes new player object to active player info
active_player.save() #Saves pickled data to player database
else:
player = dill.loads(active_player.player_info, ignore=False) #Loads pickled data in to the player
username = player.name
location = player.location
player.session_id = request.sid
world.players.update({player.id: player})
if not world.timer_active:
world.timer_active = True
socketio.start_background_task(world_timer, current_app._get_current_object())
client_list.append(player.session_id)
print(f'client list is {client_list}')
print(f'players connected is {world.players}')
session['player_id'] = player.id
session['player'] = player
join_room(location)
player.location_map()
player.inventory_update()
socketio.emit('event', {'message': f'{username} has connected to the server'})
socketio.emit('event', {'message': f'{username} appears with a puff of smoke.'}, to=location, skip_sid=player.session_id)
player.connection()
#Event that handles disconnection. Unsure if I should be saving player information on disconnect or periodically. Likely both. Need to remove the player and client from the list of active connections. If all players are disconnected, world state should be saved and server activity spun down.
@socketio.on('disconnect')
def disconnect():
player_id = int(session['player_id'])
player_account = PlayerAccount.query.get(player_id)
player = world.players[player_id]
room = player.location
client_list.pop(client_list.index(player.session_id))
player_account.player_info = dill.dumps(player)
player_account.save()
del world.players[player_id]
leave_room(room)
player.disconnection()
socketio.emit('event', {'message': f'{player.name} has left the server'})
socketio.emit('event', {'message': f'{player.name} disappears, leaving slight wisps of smoke behind.'}, to=room)
objects.py: All entities in the game
from ... import socketio
from flask import request
import app.blueprints.main.events as events
import dill
import itertools
from flask_socketio import join_room, leave_room
import app.blueprints.main.rooms as room_file
import app.blueprints.main.NPCs as npc_file
import app.blueprints.main.items as item_file
from random import randint
#World class that holds all entities
class World():
def __init__(self) -> None:
self.timer_active = False
rooms = {}
for room in room_file.room_dict.values():
new_room = Room(room['name'], room['description'], room['position'], room['exits'], room['icon'], room['ambiance_list'])
rooms.update({new_room.position: new_room})
self.rooms = rooms
self.players = {}
npcs = {}
for npc in npc_file.npc_dict.values():
new_npc = NPC(npc['name'], npc['aliases'], npc['description'], npc['deceased'], npc['health'], npc['level'], npc['location'], npc['home'], npc['ambiance_list'], npc['can_wander'], npc['attackable'])
self.rooms[new_npc.location].contents['NPCs'].update({new_npc.id: new_npc})
npcs.update({new_npc.id: new_npc})
self.npcs = npcs
for item in item_file.item_dict.values():
new_item = Item(item['name'], item['description'], item['aliases'], item['group'], item['weight'], item['location'])
for room_id, room in self.rooms.items():
if new_item.location == room_id:
room.contents['Items'].update({new_item.id: new_item})
print(f'added {new_item.name} to {room.name}. Room inventory is {room.contents}')
def world_test(self):
for room in self.rooms.values():
print(id(room), room.name, room.contents)
def world_who(self, player):
player_list = []
for player_who in self.players.values():
player_list.append(player_who.name)
output = '<br/> Currently Online Players: <br/>'
for player_who in player_list:
output += f'{player_who}<br/>'
socketio.emit('event', {'message': output}, to=player.session_id)
#Might not use this, and instead use individual pickles for each entity type for modularization
# def world_save(self):
# with open('app/data/world_db.pkl', 'wb') as dill_file:
# dill.dump(self, dill_file)
# socketio.emit('event', {'message': 'world saved'})
# def room_save(self):
# with open('app/data/room_db.pkl', 'wb') as dill_file:
# dill.dump(self.rooms, dill_file)
# socketio.emit('event', {'message': 'rooms saved'})
#Overall class for any interactable object in the world
class Entity():
def __init__(self, name, description) -> None:
self.name = name #Shorthand name for an entity
self.description = description #Every entity needs to be able to be looked at
#Test function currently, but every entity needs to be able to describe itself when looked at
def describe(self):
pass
#Class for rooms. Rooms should contain all other objects (NPCs, Items, Players, anything else that gets added)
class Room(Entity):
id = itertools.count()
def __init__(self, name, description, position, exits, icon, ambiance_list) -> None:
super().__init__(name, description)
self.id = next(Room.id)
self.position = position #Coordinates in the grid system for a room, will be used when a character moves rooms
self.exits = exits #List of rooms that are connected to this room. Should be N,S,E,W but may expand so a player can "move/go shop or someting along those lines"
self.icon = icon #Icon for the world map, should consist of two ASCII characters (ie: "/\" for a mountain)
self.contents = {'NPCs': {}, 'Players': {}, 'Items': {}} #Dictionary containing all NPCs, Players, and Items currently in the room. Values will be modified depending on character movement, NPC generation, and item movement
self.ambiance_list = ambiance_list
def describe_exits(self):
output = '<strong>[</strong> Exits: '
for each in self.exits:
output += f'⟪<strong>{each}</strong>⟫ '
output += '<strong>]</strong>'
return output
def describe_contents(self, caller):
output = ''
item_output = ''
player_list = dict(self.contents['Players'])
del player_list[caller.id]
player_key = list(enumerate(player_list))
item_list = dict(self.contents['Items'])
item_key = list(enumerate(item_list))
npc_list = dict(self.contents['NPCs'])
corpse_list = {}
for id, npc in npc_list.copy().items():
if npc.deceased:
corpse_list[id] = npc_list.pop(id)
corpse_key = list(enumerate(corpse_list))
npc_key = list(enumerate(npc_list))
total_list = []
total_corpses = []
total_items = []
for i in range(len(player_list)):
total_list.append(f'<em><strong>{player_list[player_key[i][1]].name}</strong></em>')
for i in range(len(npc_list)):
total_list.append(npc_list[npc_key[i][1]].name)
for i in range(len(corpse_list)):
total_corpses.append(corpse_list[corpse_key[i][1]].name)
for i in range(len(item_list)):
total_items.append(item_list[item_key[i][1]].name)
if len(total_list) == 0:
pass
elif len(total_list) == 1:
output += f'{total_list[0]} stands here.'
elif len(total_list) == 2:
output += f'{total_list[0]} and {total_list[1]} stand here.'
else:
for i in range(len(total_list)):
if i == len(total_list)-1:
output += f'and {total_list[i]} stand here.'
else: output += f'{total_list[i]}, '
if len(total_corpses) == 0:
pass
elif len(total_corpses) == 1:
output += f' The body of {total_corpses[0]} is on the ground here.'
elif len(total_corpses) == 2:
output += f' The bodies of {total_corpses[0]} and {total_corpses[1]} lay here.'
else:
for i in range(len(total_corpses)):
if i == 0:
item_output += f'The bodies of {total_corpses[i]}, '
if i == len(total_corpses)-1:
item_output += f'and {total_corpses[i]} lay here.'
else: item_output += f'{total_corpses[i]}, '
if len(total_items) == 0:
pass
elif len(total_items) == 1:
item_output += f' There is a {total_items[0]} here.'
elif len(total_items) == 2:
item_output += f' There is a {total_items[0]} and a {total_items[1]} here.'
else:
for i in range(len(total_items)):
if i == 0:
item_output += f'A {total_items[i]}, '
if i == len(total_items)-1:
item_output += f'and {total_items[i]} lay here.'
else: item_output += f'a {total_items[i]}, '
socketio.emit('event', {'message': output}, to=caller.session_id)
socketio.emit('event', {'message': item_output}, to=caller.session_id)
socketio.emit('event', {'message': self.describe_exits()}, to=caller.session_id)
def ambiance(self):
if randint(1,5) == 5:
socketio.emit('event', {'message': f'{self.ambiance_list[randint(0,len(self.ambiance_list)-1)]}'}, to=self.position)
#Broad class for any entity capable of independent and autonomous action that affects the world in some way
default_stats = {
'strength': 10,
'endurance': 10,
'intelligence': 10,
'wisdom': 10,
'charisma': 10,
'agility': 10
}
class Character(Entity):
def __init__(self, name, description, health, level, location, stats, deceased) -> None:
super().__init__(name, description)
self.health = health #All characters should have a health value
self.level = level #All characters should have a level value
self.location = location #All characters should have a location, reflecting their current room and referenced when moving
self.stats = stats #All characters should have a stat block.
self.deceased = deceased #Indicator of if a character is alive or not. If True, inventory can be looted
self.inventory = [] #List of items in character's inventory. May swap to a dictionary of lists so items can be placed in categories
#Class that users control to interact with the world. Unsure if I need to have this mixed in with the models side or if it would be easier to pickle the entire class and pass that to the database?
class Player(Character):
def __init__(self, id, account, name, description, health=100, level=1, location='0,0', stats=dict(default_stats), deceased=False) -> None:
super().__init__(name, description, health, level, location, stats, deceased)
self.id = id
self.account = account #User account associated with the player character
self.session_id = '' #Session ID so messages can be broadcast to players without other members of a room or server seeing the message. Session ID is unique to every connection, so part of the connection process must be to assign the new value to the player's session_id
self.inventory = list([])
self.player_map = ''
self.in_combat = False
def connection(self):
events.world.rooms[self.location].contents['Players'].update({self.id: self})
socketio.emit('event', {'message': f'<br/><strong>{events.world.rooms[self.location].name}</strong>'}, to=self.session_id)
socketio.emit('event', {'message': events.world.rooms[self.location].description}, to=self.session_id)
events.world.rooms[self.location].describe_contents(self)
def disconnection(self):
del events.world.rooms[self.location].contents['Players'][self.id]
# def player_save(self):
# player_account = PlayerAccount.query.get(self.id)
# player_account.player_info = dill.dumps(self)
# player_account.save()
# print(f'{self.name} saved')
# return
def set_description(self, data):
self.description = data
socketio.emit('event', {'message': f'Your description has been updated to "{self.description}"'}, to=self.session_id)
def look(self, data, room):
if data == '':
# socketio.emit('event', {'message': self.location}, to=self.session_id)
socketio.emit('event', {'message': f'<br/><strong>{room.name}</strong>'}, to=self.session_id)
socketio.emit('event', {'message': room.description}, to=self.session_id)
room.describe_contents(self)
elif data in ['me', 'myself']:
socketio.emit('event', {'message': 'You really want me to describe you, to <em>you</em>? You really are something. Get your kicks somewhere else, bucko.'}, to=self.session_id)
else:
for player in room.contents['Players'].values():
if player == self:
continue
if player.name.lower().startswith(data.lower()):
socketio.emit('event', {'message': player.description}, to=self.session_id)
return
for npc in room.contents['NPCs'].values():
for alias in npc.aliases:
if alias.lower().startswith(data.lower()):
if npc.deceased:
socketio.emit('event', {'message': npc.description_deceased}, to=self.session_id)
return
socketio.emit('event', {'message': npc.description}, to=self.session_id)
return
for item in room.contents['Items'].values():
for alias in item.aliases:
if alias.lower().startswith(data.lower()):
socketio.emit('event', {'message': item.description}, to=self.session_id)
return
for direction, room in room.exits.items():
if direction.lower().startswith(data.lower()):
if events.world.rooms[room].name.startswith('The'):
socketio.emit('event', {'message': f'{events.world.rooms[room].name} lies that way.'}, to=self.session_id)
return
socketio.emit('event', {'message': f'The {events.world.rooms[room].name} lies that way.'}, to=self.session_id)
return
def location_map(self):
self_map = []
if '0,' not in self.location and '1,' not in self.location and '2,' not in self.location and '3,' not in self.location and '4,' not in self.location and '5,' not in self.location and '6,' not in self.location and '7,' not in self.location and '8,' not in self.location and '9,' not in self.location:
for i in range(1,10):
if i != 5:
if i == 3:
self_map.append('<span style="background-color: tan"> </span><br>')
elif i == 6:
self_map.append('<span style="background-color: tan"> </span><br>')
else:
self_map.append('<span style="background-color: tan"> </span>')
else: self_map.append('<span style="background-color:LightBlue; color: DodgerBlue">()</span>')
else:
for i in range(1,10):
lon, lat = self.location.split(',', 1)
lon = int(lon)
lat = int(lat)
if i == 1:
lat += 1
lon -= 1
try:
self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
except KeyError:
self_map.append('<span class="map-square" style="background-color: tan"> </span>')
if i == 2:
lat += 1
try:
self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
except KeyError:
self_map.append('<span class="map-square" style="background-color: tan"> </span>')
if i == 3:
lat += 1
lon += 1
try:
self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}<br>')
except KeyError:
self_map.append('<span class="map-square" style="background-color: tan"> </span><br>')
if i == 4:
lon -= 1
try:
self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
except KeyError:
self_map.append('<span class="map-square" style="background-color: tan"> </span>')
if i == 5:
self_map.append('<span class="map-square" style="background-color:LightBlue; color: DodgerBlue">()</span>')
if i == 6:
lon += 1
try:
self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}<br>')
except KeyError:
self_map.append('<span class="map-square" style="background-color: tan"> </span><br>')
if i == 7:
lat -= 1
lon -= 1
try:
self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
except KeyError:
self_map.append('<span class="map-square" style="background-color: tan"> </span>')
if i == 8:
lat -= 1
try:
self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
except KeyError:
self_map.append('<span class="map-square" style="background-color: tan"> </span>')
if i == 9:
lat -= 1
lon += 1
try:
self_map.append(f'{events.world.rooms[f"{lon},{lat}"].icon}')
except KeyError:
self_map.append('<span class="map-square" style="background-color: tan"> </span>')
output = '<tt class="map-square" style="margin-bottom: 0">'
for icon in self_map:
output += icon
output += '</tt>'
self.player_map = output
socketio.emit('map', {'map': self.player_map}, to=self.session_id)
def speak(self, data):
socketio.emit('event', {'message': f'{self.name} says "{data}"'}, room=self.location, include_self=False)
socketio.emit('event', {'message': f'You say "{data}"'}, to=self.session_id)
def whisper(self, whisper_player, data):
socketio.emit('event', {'message': f'The wind breathes against your ear, and you can faintly hear "{data}". You get a feeling it\'s from {self.name}.'}, to=whisper_player.session_id)
socketio.emit('event', {'message': 'You whisper your message to the wind.'}, to=self.session_id)
def combat(self, victim):
if not victim.attackable:
print()
socketio.emit('event', {'message': 'It\'s ok. I get it. Sometimes someone says something and it just makes you so angry. Unfortunately, whoever you\'re currently targeted with your untethered rage is just so important we can\'t let you hurt them. Sorry bud, go punch a tree.'}, to=self.session_id)
return
if isinstance(victim, NPC):
if victim.deceased:
socketio.emit('event', {'message': 'Jesus man they\'re already dead calm down. Go talk to a psychiatrist or something.'}, to=self.session_id)
else:
socketio.emit('event', {'message': f'{self.name} slams their fist into {victim.name}, killing them instantly.'}, to=self.location, skip_sid=self.session_id)
socketio.emit('event', {'message': f'You slam your fist into {victim.name}, killing them instantly.'}, to=self.session_id)
victim.deceased = True
return
def move(self, direction, room):
if direction not in room.exits:
socketio.emit('event', {'message': 'You can\'t go that way.'}, to=self.session_id)
return
leave_room(self.location)
if direction in ['out', 'in']:
socketio.emit('event', {'message': f'{self.name} moves towards the {room.exits[direction]}'}, room=self.location)
else:
socketio.emit('event', {'message': f'{self.name} moves towards the {direction}'}, room=self.location)
if self.id in room.contents['Players']:
del room.contents['Players'][self.id]
if direction in ['out', 'in']:
socketio.emit('event', {'message': f'You move towards the {events.world.rooms[room.exits[direction]].name}'}, to=self.session_id)
else:
socketio.emit('event', {'message': f'You move towards the {direction}'}, to=self.session_id)
new_location = room.exits[direction]
if direction in ['out', 'in']:
print(events.world.rooms[new_location].exits)
came_from = [i for i in events.world.rooms[new_location].exits if events.world.rooms[new_location].exits[i]==self.location]
else:
came_from = [i for i in events.world.rooms[new_location].exits if events.world.rooms[new_location].exits[i]==self.location]
socketio.emit('event', {'message': f'{self.name} arrives from the {came_from[0]}'}, room=new_location)
socketio.sleep(.5)
self.location = new_location
join_room(self.location)
self.location_map()
events.world.rooms[self.location].contents['Players'].update({self.id: self})
socketio.emit('event', {'message': f'<br/><strong>{events.world.rooms[self.location].name}</strong>'}, to=self.session_id)
socketio.emit('event', {'message': events.world.rooms[self.location].description}, to=self.session_id)
events.world.rooms[self.location].describe_contents(self)
def inventory_update(self):
inventory_send = []
for item_inv in self.inventory:
item_info = {
'id': item_inv.id,
'name': item_inv.name,
'group': item_inv.group
}
inventory_send.append(item_info)
socketio.emit('inventory', {'inventory': inventory_send}, to=self.session_id)
def inventory_display(self):
inventory_send = []
if not self.inventory:
socketio.emit('event', {'message': 'Sorry bud, you don\'t have anything in your inventory right now.'}, to=self.session_id)
else:
output = '<br/>Inventory:<br/>'
for item_inv in self.inventory:
item_info = {
'id': item_inv.id,
'name': item_inv.name,
'group': item_inv.group
}
output += f'{item_inv.name}<br/>'
inventory_send.append(item_info)
print(inventory_send)
socketio.emit('event', {'message': output}, to=self.session_id)
socketio.emit('inventory', {'inventory': inventory_send}, to=self.session_id)
def item_get(self, item, room):
inventory_send = []
for item_id, room_item in room.contents['Items'].copy().items():
if item in room_item.aliases:
self.inventory.append(room.contents['Items'].pop(item_id))
socketio.emit('event', {'message': f'You get the {room_item.name}.'}, to=self.session_id)
socketio.emit('event', {'message': f'You see {self.name} grab a {room_item.name}.'}, to=self.location, skip_sid=self.session_id)
for item_inv in self.inventory:
item_info = {
'id': item_inv.id,
'name': item_inv.name,
'group': item_inv.group
}
inventory_send.append(item_info)
socketio.emit('inventory', {'inventory': inventory_send}, to=self.session_id)
return
socketio.emit('event', {'message': f'I have absolutely no clue what you\'re trying to get here. I don\'t see a "{item}" here.'}, to=self.session_id)
def item_drop(self, item, room):
inventory_send = []
for i in range(len(self.inventory.copy())):
for alias in self.inventory[i].aliases:
if item == alias:
drop_item = self.inventory.pop(i)
room.contents['Items'][drop_item.id] = drop_item
socketio.emit('event', {'message': f'You drop the {drop_item.name}.'}, to=self.session_id)
socketio.emit('event', {'message': f'You see {self.name} drop a {drop_item.name}.'}, to=self.location, skip_sid=self.session_id)
for item_inv in self.inventory:
item_info = {
'id': item_inv.id,
'name': item_inv.name,
'group': item_inv.group
}
inventory_send.append(item_info)
socketio.emit('inventory', {'inventory': inventory_send}, to=self.session_id)
return
socketio.emit('event', {'message': f'Check your pockets, cause I don\'t see a "{item}" in your inventory.'}, to=self.session_id)
#Class that is controlled by the server. Capable of being interacted with.
class NPC(Character):
id = itertools.count()
def __init__(self, name, aliases, description, deceased, health, level, location, home, ambiance_list, can_wander, attackable, stats=dict(default_stats)) -> None:
self.id = next(NPC.id)
self.name = name
self.aliases = aliases
self.description = description
self.description_deceased = f'The corpse of {self.name}, crumpled and torn.'
self.deceased = deceased
self.health = health
self.level = level
self.location = location
self.home = home #Spawn location if NPC is killed. Can also double as a bound to prevent NPC from wandering too far from home during world timer movement
self.ambiance_list = ambiance_list
self.can_wander = can_wander
self.inventory = list([])
self.in_combat = False
self.attackable = attackable
def ambiance(self):
if self.deceased:
return
numb = randint(1,5)
if numb == 5:
socketio.emit('event', {'message': f'{self.ambiance_list[randint(0,len(self.ambiance_list)-1)]}'}, to=self.location)
elif numb == 4:
if self.can_wander:
lon, lat = self.location.split(',', 1)
lon = int(lon)
lat = int(lat)
move_x = randint(-1,1)
move_y = randint(-1,1)
lon += move_x
lat += move_y
new_location = f'{lon},{lat}'
for direction, coords in events.world.rooms[self.location].exits.items():
if new_location == coords:
socketio.emit('event', {'message': f'{self.name} wanders away towards the {direction}.'}, to=self.location)
del events.world.rooms[self.location].contents['NPCs'][self.id]
self.location=new_location
events.world.rooms[self.location].contents['NPCs'].update({self.id: self})
socketio.emit('event', {'message': f'{self.name} wanders in.'}, to=self.location)
break
#Class that is incapable of autonomous action. Inanimate objects and such.
class Item(Entity):
id = itertools.count()
def __init__(self, name, description, aliases, group, weight, location=None, equippable=False) -> None:
super().__init__(name, description)
self.aliases = aliases
self.id = next(Item.id)
self.group = group #Type of item. Food, equipment, quest, etc
self.weight = weight #Weight of item. Will be used alongside character strength to determine if the item can be picked up, pulled to inventory, etc
self.location = location
self.equippable = equippable #Can you equip the item. Unsure if redundant with equipment class
#Subclass of items that is capable of being equipped by the Character class
class Equipment(Item):
def __init__(self, name, description, group, weight, category, equippable) -> None:
super().__init__(name, description, group, weight, equippable)
self.equippable = True #All equipment should be equippable
self.category = category #Further granularity with group. Is the equipment armor, clothing, sword, axe, etc. Unsure if I should instead make child classes of equipment instead so I can dictate specific stat blocks
commands.py: parses user input and calls relevant functions/methods
from flask_login import current_user
from ... import socketio
from flask_socketio import join_room, leave_room, emit
import app.blueprints.main.events as events
#List of commands that are dependant on input from the client. Still unsure as to if I need the middle man function.
@socketio.event
def client(data):
current_player = events.world.players[session.get('player_id')]
current_room = events.world.rooms[current_player.location]
content = {
'player': current_player,
'room': current_room,
'command': data['command'].lower(),
'data': data['data'],
}
if content['command'] == 'who':
events.world.world_who(content['player'])
return
elif content['command'] == 'say':
say(content['player'], content['data'])
return
elif content['command'] in ['attack', 'kill', 'hurt', 'k', 'fight']:
combat(content['player'], content['data'].lower())
return
elif content['command'][0] == '"':
new_data = content['command'][1:]
if ' ' in content['data']:
data_final = f'{new_data} {content["data"]}'
say(content['player'], data_final)
else: say(content['player'], new_data)
return
elif content['command'] in ['i', 'inv', 'inv']:
inventory_display(content['player'])
return
elif content['command'] in ['get', 'grab', 'pick', 'pickup', 'g']:
item_get(content['player'], content['data'], content['room'])
elif content['command'] in ['drop', 'discard', 'd']:
item_drop(content['player'], content['data'], content['room'])
elif content['command'] == 'whisper':
whisper(content['player'], content['data'])
return
elif content['command'].lower() in ['move', 'go', 'north', 'south', 'east', 'west', 'n', 's', 'e', 'w', 'out', 'in']:
content['data'] = content['data'].lower()
if not content['data']:
content['data'] = content['command'].lower()
if content['data'] == 'n':
content['data'] = 'north'
if content['data'] == 's':
content['data'] = 'south'
if content['data'] == 'e':
content['data'] = 'east'
if content['data'] == 'w':
content['data'] = 'west'
move(player=content['player'], direction=content['data'], room=content['room'])
return
elif content['command'] == 'look' or content['command'] == 'l':
look(player=content['player'], data=content['data'], room=content['room'])
return
elif content['command'] == 'test':
test(content['player'], content['data'])
elif content['command'] == 'save':
save(content['player'], content['data'])
elif content['command'] == '@describe':
set_description(content['player'], content['data'])
return
else:
socketio.emit('event', {'message': 'Sorry, that\'s not a command I currently understand!'}, to=content['player'].session_id)
return
#This event should be moved to the Character class and using their move method
def say(player, data):
player.speak(data)
def whisper(player, data):
split = data.split(' ', 1)
whisper_player = split[0]
whisper_data = split[1]
for world_player in events.world.players.values():
if world_player.name == whisper_player:
player.whisper(whisper_player=world_player, data=whisper_data)
return
socketio.emit('event', {'message': 'That player either doesn\'t exist, or isn\'t currently online.'}, to=player.session_id)
def combat(player, data):
print(data)
victim = None
for victim_player in events.world.players.values():
if victim_player.name.lower() == data:
victim = victim_player
player.combat(victim=victim)
return
for victim_npc in events.world.npcs.values():
new_alias = []
for alias in victim_npc.aliases:
new_alias.append(alias.lower())
if victim_npc.name.lower() == data or data in new_alias:
victim = victim_npc
player.combat(victim=victim)
return
socketio.emit('event', {'message': f'Woah there killer! I\'m not sure who you\'re trying to attack, but "{data}" certainly isn\'t here. Maybe take out your aggression on a punching bag?'}, to=player.session_id)
def inventory_display(player):
player.inventory_display()
def item_get(player, data, room):
player.item_get(data, room)
def item_drop(player, data, room):
player.item_drop(data, room)
def move(player, direction, room):
player.move(direction=direction, room=room)
#This event should be moved to the Player class and using their look method
def look(player, room, data=''):
player.look(data=data, room=room)
#These are primarily test functions to make sure the world timer is functional
def test(player, data):
pass
def save(player, player_id, sid, location, data):
events.world.world_save()
events.world.room_save()
def set_description(player, data):
player.set_description(data)
sample NPC
npc_dict = {
'Crier': {
'name': 'Bob, the Town Crier',
'aliases': ['Bob', 'Crier', 'The Town Crier'],
'description': 'A young man dressed in flamboyant but common clothing, looking about as if to see who he\'ll announce the latest news to.',
'location': '0,0',
'home': '0,0',
'deceased': False,
'health': 100,
'level': 1,
'ambiance_list': ['The Town Crier yells "Murder in the forest! B\'ware!"', 'The Town Crier yawns and scratches himself.','You hear the Town Crier mutter "Why is it always murder? Nobody ever wants to hear about the ducks."'],
'can_wander': True,
'attackable': True,
},
sample room
room_dict = {
(0,0): {
'name': 'Town Square',
'description': 'This is the center of town! People flow around you, living in their own quiet worlds. You notice that nobody is making eye contact with you, and some people go out of their way to avoid you.',
'position': '0,0',
'exits': {
'north': '0,1',
'south': '0,-1',
'east': '1,0',
'west': '-1,0'
},
'icon': '<span class="map-square" style="background-color:white; color:black">()</span>',
'ambiance_list': ['People move around you, busy about their day', 'The local town drunk stumbles by, muttering to people nobody sees.']
},
sample item
item_dict = {
'coin': {
'name': 'coin',
'aliases': ['coin', 'money',],
'description': 'Shiny! Someone\'s going to have a bad day when they realize they dropped this. Be a thief, do it. Morality is a construct of society, and you\'ve moved beyond such petty concerns.',
'group': 'money',
'weight': 1,
'location': '1,1'
},
Frontend:
Terminal.tsx: Client-side terminal for display and input
import { useEffect, useRef, useState } from "react";
import { io } from "socket.io-client";
import Map from "../Map/Map";
import Messages from "../Messages/Messages";
import CommandList from "../CommandList/CommandList";
import InventoryList from "../InventoryList/InventoryList";
import React from "react";
const Terminal = () => {
let apiURL:string
if (import.meta.env.MODE === 'development') {
apiURL = 'http://localhost:5000'
} else {
apiURL = 'https://retromooapi.onrender.com'
};
let socketio = useRef(
io(apiURL, {
auth: localStorage.user_id,
autoConnect: false,
})
);
const [messages, setMessages] = useState([{ message: "", time: "" }]);
const [map, setMap] = useState("");
const [message, setMessage] = useState("");
const [inventory, setInventory] = useState<
{ id: number; name: string; group: string }[]
>([]);
useEffect(() => {
socketio.current.connect();
return () => {
socketio.current.disconnect();
};
}, []);
useEffect(() => {
socketio.current.on("event", (data: { message: string }) => {
setMessages((curr) => [
...curr,
{ message: data.message, time: new Date().toLocaleString() },
]);
});
socketio.current.on("map", (data: { map: string }) => {
setMap(data.map);
});
socketio.current.on(
"inventory",
(data: { inventory: { id: number; name: string; group: string }[] }) => {
setInventory(data.inventory);
}
);
}, []);
const submitTask = (event: React.FormEvent) => {
event.preventDefault();
const payload = {
command: message.substr(0, message.indexOf(" ")),
data: message.substr(message.indexOf(" ") + 1),
};
if (payload.command === "" && payload.data) {
payload.command = payload.data;
payload.data = "";
}
console.log(payload);
socketio.current.emit("client", payload);
setMessage("");
};
return (
<div className="papyrus-box">
<div className="map-box" id="map-box">
<Map map={map} />
</div>
<div className="room-commands">
<CommandList />
</div>
<div className="room-inventory">
<InventoryList inventory={inventory} />
</div>
<div className="room-box">
<h2 className="room-header">Narnia</h2>
<div className="message-box">
<div className="messages" id="messages">
<Messages messages={messages} />
</div>
<form className="input" id="input-form" onSubmit={submitTask}>
<input
type="text"
name="message"
id="message"
placeholder="Type here!"
required
value={message}
onChange={(event) => {
setMessage(event.target.value);
}}
/>
<button id="send-btn" type="submit">
Send
</button>
</form>
</div>
</div>
</div>
);
};
export default Terminal;