diff --git a/.gitignore b/.gitignore index f7be2bd..a91af6d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,5 @@ venv/ *.gif~ *.jpeg~ *.jpeg~ -*.kra *.kra~ diff --git a/src/blessfrey-gdd/Mercur b/src/blessfrey-gdd/Mercur index 57ccd3f..fb4f999 100644 --- a/src/blessfrey-gdd/Mercur +++ b/src/blessfrey-gdd/Mercur @@ -1 +1 @@ -

All acts of god are attributed to him. His steed is the MessageBus. He is technically an entity.

+

All acts of god are attributed to him. His steed is the MessageBus. He is a God, technically a base-based entity. His job is Mailman/Mailman.

diff --git a/src/blessfrey-gdd/environment-effect b/src/blessfrey-gdd/environment-effect new file mode 100644 index 0000000..611ffb2 --- /dev/null +++ b/src/blessfrey-gdd/environment-effect @@ -0,0 +1,3 @@ +

Works like a skill in most instances, but it has a persistent effect that is triggered instead of activated.

+ +

Usually, there's going to be some Area2D (covers the lava area, etc) that will cast the skill upon collision with a tangible and end the skill when the tangible exits.

diff --git a/src/blessfrey-gdd/passive-skill b/src/blessfrey-gdd/passive-skill new file mode 100644 index 0000000..bb9297c --- /dev/null +++ b/src/blessfrey-gdd/passive-skill @@ -0,0 +1,33 @@ +

description

+

Passive skills bestow a constant effect and are perpetually applied in the background.

+ +

flow

+ + +

Skills perform their function on their user and target, but they are also applied to their target's applied_skills, a list of skill usages.

+ +

structure

+

+ +

logic

+

end

+

A skill naturally ends after completion. After all the keywords are complete, the skill will complete.

+ +

A skill prematurely ends if it is removed (the remove keyword targets applied skills) or reapplied (a duplicate skill ends earlier instances of the skill then is applied normally).

+ +

When a skill ends for any reason, all keywords must also end.

diff --git a/src/blessfrey-gdd/perk b/src/blessfrey-gdd/perk index 13c6f6c..bd3a61d 100644 --- a/src/blessfrey-gdd/perk +++ b/src/blessfrey-gdd/perk @@ -1,3 +1,4 @@

Jobs and gigs have attributes associated with them, while only jobs have perks. Each attribute defines a specialization within the job's overall playstyle. A perk is the same as an attribute, but characters only have access to their job's perk, not their gig's perk.

Perks bestow a constant effect on the character. For instance, the Brawler's Critical Eye increases his critical hit rate.

Each perk and attribute is associated with a set of skills. You can increase and decrease their values in safe areas (no permanent decision), and their skills' effectiveness with scale with their attributes.

+

Perks are activated at _ready or job change onto their owner.

diff --git a/src/diary/entries/230224 b/src/diary/entries/230224 new file mode 100644 index 0000000..b0e66f6 --- /dev/null +++ b/src/diary/entries/230224 @@ -0,0 +1,694 @@ + +

Godot Tutorial: Chat Room using WebSocket

+august 31, 2022
+#webdev
+
+

Follow along to make your first mini WebSocket application in GDScript (with a little JSON). The client and the server will be two separate projects. I build upon the NetworkedMultiplayerENet chat room tutorial by Miziziziz(archive link) and the HTML5 and WebSocket tutorial in the Godot documentation.

+ +
+ +

Why WebSocket over UDP?

+

UDP is fast but inaccurate. It is best used for real-time action gameplay. TCP is slow but accurate. It is best for sharing data. You can read more about it on the Multiplayer doc page.

+

WebSocket uses a TCP connection. Ultimately, I am studying Godot to make a slow-paced adventure browser game, so this is one of the protocols I am considering and the protocol we will use for the tutorial.

+ +

Do I need a dedicated server for testing?

+

Nope! You can test client & server code on your own computer. I am only testing on a single computer at home for now without accessing an outside VPS, etc.

+ +
+ +

Make your projects.

+

Make one for the server and one for the client. You can do almost everything in the engine. If you export your projects after changes, though, you can easily see debug information for both sides of the application and test multiple clients. Godot is fast and light for a game engine, so I don't think it's unreasonable to add an exporting step to your workflow.

+ +

I named my projects ChatServer and ChatClient.

+ +

Setting up the server.

+ +

Node Hierarchy.

+

One scene with a node is enough for the server.

+(Screenshot: Node hierarchy is just one node named Server.)
+ +

Declare your variables.

+

+const PORT = 8080 
+var _server = WebSocketServer.new()
+
+ +

PORT is the port your applications will use. If you are having trouble connecting, try 9001, etc, until you find a free one.

+ +

Build your skeleton.

+

+func _ready():
+	_server.connect('client_connected', self, "_connected")
+	_server.connect('client_disconnected', self, "_disconnected")
+	_server.connect('client_close_request', self, "_close_request")
+	_server.connect("data_received", self, "_on_data_received")
+
+	var err = _server.listen(PORT)
+	if err != OK:
+		print_debug("Unable to start server")
+		set_process(false)
+	print_debug("server started")
+	
+func _connected(id, protocol):
+	print_debug("Client %d connected with protocol: 
+        %s" % [id, protocol])
+	
+func _close_request(id, code, reason):
+	print_debug("Client %d disconnecting with code: 
+        %d, reason: %s" % [id, code, reason])
+	
+func _disconnected(id, was_clean = false):
+	print_debug("Client %d disconnected, clean: 
+        %s" % [id, str(was_clean)])
+	
+func _on_data_received(id):
+	pass
+	
+func _process(_delta):
+	_server.poll()
+
+ +

The _ready method prepares the server to catch all the signals it needs for a simple application. client_connected triggers when a client connects, client_disconnected when one disconnects, client_close_request when one requests to close, and data_received when a packet is received. Fairly obvious. The next block of code starts up the server, or at least makes an attempt.

+ +

So long as the process is running, the server polls constantly.

+ +

Let's start the client before we get any further.

+ +

Setting up the client.

+ +

Design work.

+

The client requires both some basic functionality and a barebones UI.

+

To go for this look...

+ +(Screenshot: The look of the UI - a title bar on top, a large TextEdit field for everyone's messages, and a LineEdit beneath for entering messages. If you are 'logged out,' a username field and join button will appear at the bottom.) + +

...we'll need this tree.

+ +(Screenshot: Node hierarchy is a Node named Client a Panel Container child named UI. The UI has a VBoxContainer child containing a Label, TextEdit, LineEdit, and an HBoxContainer. The HBox contains a LineEdit and a button.) + +

The root node will hold the programmatic half of the client, while the UI node will hold all the code for displaying things to the user.

+ +

To make UIs, I like to block everything out in containers, especially VBoxContainers and HboxContainers. I made the UI a PanelContainer so everything lays on the default gray background (or any custom background, choosable in the Inspector as a StyleBoxTexture).

+ +(Screenshot: The PanelContainer Inspector>Theme Overrides>Styles>Panel>New StyleBoxTexture.) + +

Also in the Inspector, set the size (rect_size) to 250 by 600px. Make sure the Position is still (0,0). You can also match the window's size by going to the top-left menu>Project>Project Settings...>General>Display>Window>Size and set Width to 250 and Height to 600.

+ +(Screenshot: PanelContainer Inspector>Rect>Size...x = 250 and y = 600.) +(Screenshot: Project Settings.) + +

The layout is more vertical overall, so the first structural node is a VBoxContainer. Add it as a child, then go to Inspector>Size Flags. Check Fill and Expand for both Horizontal and Vertical, so it automatically fills the PanelContainer. When you check Fill and Expand, it will take up 100% of the space in that direction of its parent when it is the only child. If there are multiple children, each with Expand, they will evenly divide the space. If one child has fixed dimensions, it will stay that size, while all the Expand children evenly split up the remainder.

+ +(Screenshot: VBoxContainer's Size Flags are checked for Horizontal Fill and Expand and Vertical Fill and Expand.) + +

For the Label, add the application's title in the Inspector's Text field, set Align to Center, and set the Size Flags to Horizontal Fill and Expand.

+ +(Screenshot: VBoxContainer's Size Flags are checked for Horizontal Fill and Expand and Vertical Fill and Expand.) + +

For the ChatLog (TextEdit), check the Show Line Numbers and Wrap Enabled options then set Size Flags to Fill and Expand for both Horizontal and Vertical.

+ +

For the ChatInput (LineEdit), under Placeholder, set the Text to "Enter your message..." and the Size Flags to Fill and Expand Horizontally.

+ +

The next section is laid out horizontally, so use an HBoxContainer. Add a LineEdit for the Username with "Username" as its Placeholder Text and the Horizontal Fill and Expand boxes checked. Also add a button with its Text set to Join and its Horizontal Fill and Expand boxes checked.

+ +

Client Skeleton

+ +

Here is the first sketch of the UI code. The Client is accessible as get_parent(), the important nodes as variables, and the signals related to entering a message and pressing the join button are handled programmatically.

+ +

+UI.gd (Client/UI PanelContainer)
+
+extends PanelContainer
+
+onready var chat_log = $VBox/ChatLog
+onready var chat_input = $VBox/ChatInput
+onready var join_button = $VBox/HBox/JoinButton
+onready var username = $VBox/HBox/Name
+
+func display_message(packet): 
+    pass
+	
+func join_chat():
+	pass
+	
+func _input(event):
+	if event is InputEventKey:
+		if event.pressed and event.scancode == KEY_ENTER:
+			get_parent().send_message(chat_input.text)
+			chat_input.text = ""
+	
+func _ready():
+	join_button.connect("button_up", self, "join_chat")
+
+ +

UI always takes up so much space without actually doing that much...Let's block out the functionality on the root node's script.

+ +

+Client.gd (Client Node)
+
+extends Node
+
+export var websocket_url = "ws://127.0.0.1:8080"
+var _client = WebSocketClient.new()
+onready var UI = $UI
+
+var chat_name
+
+func join_chat(username):
+    pass
+	
+func send_message(message):
+	pass
+	
+sync func receive_message(packet):
+	pass
+
+func _ready():
+	_client.connect("connection_closed", self, "_closed")
+	_client.connect("connection_error", self, "_error")
+	_client.connect("connection_established", self, "_connected")
+	_client.connect("data_received", self, "_on_data_received")
+
+    var err = _client.connect_to_url(websocket_url)
+	if err != OK:
+		print_debug("Unable to connect")
+		set_process(false)
+	
+func _error(was_clean = false):
+	print_debug("Error, clean: ", was_clean)
+	
+func _closed(was_clean = false):
+	print_debug("Closed, clean: ", was_clean)
+	
+func _connected(protocol = ""):
+	print_debug("Connected with protocol: ", protocol)
+	
+func _on_data_received():
+	pass
+	
+func _process(_delta):
+	_client.poll()
+
+ +

It resembles the server. You can add a security protocol when you use wss:// instead of ws:/ for the websocket_url, but that is outside the scope of this tutorial. 127.0.0.1 loops back to your own computer, so you can use this for testing at home without involving an outside VPS, etc. Then, the port must match whatever you use for the server's PORT constant. 8080 ended up working for me.

+ +

And with that, we have enough to try it out.

+ +

Export & Run

+ +

Export just the server for now. If you want to try multiple clients, export the client, too, but there isn't a lot to test yet. To Export, go to the top-left menu>Project>Export... If you've been here before, choose the right presets for making a standalone application for your computer. Otherwise, on the top, click Add... then choose your operating system. When you have your preset, make sure under the Resources tab, 'Export all resources in the project' is selected. Otherwise, all the default stuff is good. Personally, I keep all my Godot exports in Exports folder next to my Godot projects, but you put it somewhere accessible. If you have an option, export in Debug Mode.

+ +(Screenshot: Default export options for Linux.) + +

If you run in Linux, you can get the debug info in your terminal by opening a terminal in the export directory and typing ./ChatServer.x86_64 or whatever you named your application.

+ +

...Or you can just run both in their editors for now. No big deal.

+ +(Screenshot: Both applications are running and connecting to each other.) + +

But however you run, if you run the server first and then the client, you will see your connection in the debug output! You have completed an extremely basic networking application. You can see the full project at this stage on my repo: server and client. + +


+ +

Communicating between Server + Client.

+

If we can pass one more hurdle, writing the chat room code will barely even need a tutorial: passing information between the server and client.

+ +

Writing packets in JSON.

+

Information will be sent in packets. You can literally pass your chat message strings, etc, as packets without much touchup. Something like put_packet("hey how's it going?".to_utf8()) will run, but even a little chat app will be passing lots of different data back and forth. JSON strings are freeform enough to fit any online application's needs. If you don't know JSON syntax, glance over the Godot documentation. It's very close to the dictionary in most languages.

+ +

Some examples of packets we'll use are test packets, chat message packets, server message packets, user joining packets, and user leaving packets. I'm going to use the 'type' key to distinguish incoming packets.

+ +

My chat message packets look like this:

+ +
{'type': 'chat_message', 'name': "Bell", 'message': "hey guys"}
+ +

To send them, you need to convert them to JSON strings then convert those strings into UTF-8. You will have to do the reverse on the receiving end. Outgoing packets will look like

+ +

+_client.get_peer(1).put_packet(JSON.print(
+    {'type': 'chat_message', 'name': "Bell", 'message': "hey guys"}).to_utf8()
+)
+
+ +

while incoming messages will need extra preparation:

+ +

+	var json = JSON.parse(_client.get_peer(1).get_packet().get_string_from_utf8())
+	var packet = json.result
+
+ +

JSON packets in action.

+ +Let's build up the server's data reception method. + +

+Server.gd (Node)
+func _on_data_received(id):
+	
+	var original = _server.get_peer(id).get_packet().get_string_from_utf8()
+	var json = JSON.parse(original)
+	var packet = json.result
+	if typeof(packet) != 18:
+		push_error("%s is not a dictionary" % [packet])
+		get_tree().quit()
+	
+	print_debug("Got data from client %d: %s" % [id, packet])
+
+ +

We don't have to send the id argument manually. The server will understand the source of incoming data automatically. _server.get_peer(id) gives us the source. _server.get_peer(id).get_packet() gives us the packet they are sending.

+ +

typeof returns 18 for dictionaries. You can see all the types and their enum values in Godot's @GlobalScope documentation. Packets should always be dictionaries the way I'm writing this, so if anything else is received, the application will push an error and crash. Replace the code with a print_debug warning if you want, but I like the immediacy of a crash.

+ +

For now, it will print any valid packet's contents.

+ +

The client's data reception method should be similar:

+ +

+Client.gd (Node)
+func _on_data_received():
+	var json = JSON.parse(_client.get_peer(1).get_packet().get_string_from_utf8())
+	var packet = json.result
+	if typeof(packet) != 18:
+		push_error("%s is not a dictionary" % [packet])
+		get_tree().quit()
+	print_debug("Got data from server: ", packet)
+
+ +

Let's add a few put_packets to receive, too. The _connected methods on the server and client are the easiest places to test.

+ +


+Server.gd (Node)
+func _connected(id, protocol):
+	print_debug("Client %d connected with protocol: %s" 
+		% [id, protocol])
+	_server.get_peer(id).put_packet(JSON.print(
+		{'type': 'test', 'message': "test packet from server"}).to_utf8())
+
+ +

+Client.gd (Node)
+func _connected(protocol = ""):
+	print_debug("Connected with protocol: ", protocol)
+	_client.get_peer(1).put_packet(JSON.print(
+		{'type': 'test', 'message': "test packet from client"}).to_utf8())
+
+ +

Let's see if the server and client can share data now.

+ +(Screenshot: Godot's debug info echos both test packets sent from the server and client.) + +

Boom. That's enough networking knowledge to get you started. You can see the full project at this stage on my repo: server and client.

+ +
+ +

Writing that chat room.

+ +

Writing the UI.

+ +

A chat room is driven by the UI, so UI.gd is the best place to start. The UI needs to let the user join, send messages, and see messages sent by the user and others. Anything beyond showing buttons and letting the user type into fields is beyond the scope of the UI and will be passed to the root node.

+ +(Screenshot: The ChatInput is hidden at the start.) + +

+UI.gd (PanelContainer)
+func display_message(packet): 
+	if packet['type'] == "chat_message":
+		chat_log.text += packet['name'] + ": " + packet['message'] + "\n"
+	elif packet.has("message"):
+		chat_log.text += packet['message'] + "\n"
+	
+func join_chat():
+	if !username.text:
+		display_message(
+			{'type': "alert", 
+				"message": "!!! Enter your username before joining chat."}
+		)
+	else:
+		get_parent().join_chat(username.text)
+		chat_input.show()
+		join_button.hide()
+		username.hide()
+	
+func _input(event):
+	if event is InputEventKey and chat_input.text:
+		if event.pressed and event.scancode == KEY_ENTER:
+			get_parent().send_message(chat_input.text)
+			chat_input.text = ""
+	
+func _ready():
+	join_button.connect("button_up", self, "join_chat")
+
+ +

Also hide the ChatInput by clicking the eye on the node tree.

+ +

The display_message method will lightly format incoming chat messages and messages from other sources then show them as a line in the chat log. If the user clicks the join button but left the username field blank, an alert only visible to the user will be shown in the chat log. Otherwise, the join button and username field are hidden, and the chat input becomes available. join_chat is called off the parent, so the client can handle the rest. The _input method sends messages entered by the user to the root then clears the field for future messages.

+ +

Writing the first part of the client's back end.

+ +

+Client.gd (Node)
+
+export var websocket_url = "ws://127.0.0.1:8080"
+var _client = WebSocketClient.new()
+onready var UI = $UI
+var chat_name
+
+func join_chat(username):
+	chat_name = username
+	_client.get_peer(1).put_packet(JSON.print(
+			{'type': "user_joined", 'name': username}
+		).to_utf8()
+	)
+	
+func send_message(message):
+	_client.get_peer(1).put_packet(JSON.print(
+			{'type': 'chat_message', 'name': chat_name, 'message': message}
+		).to_utf8()
+	)
+	
+func _error(was_clean = false):
+	print_debug("Error, clean: ", was_clean)
+	set_process(false)
+	
+func _closed(was_clean = false):
+	print_debug("Closed, clean: ", was_clean)
+	set_process(false)
+	
+func _connected(protocol = ""):
+	print_debug("Connected with protocol: ", protocol)
+	_client.get_peer(1).put_packet(JSON.print(
+		{'type': 'test', 'message': "test packet from client"}
+	).to_utf8())
+	UI.join_button.show()
+	UI.username.show()
+	
+func _on_data_received():
+	var json = JSON.parse(
+		_client.get_peer(1).get_packet().get_string_from_utf8()
+	)
+	var packet = json.result
+	if typeof(packet) != 18:
+		push_error("%s is not a dictionary" % [packet])
+		get_tree().quit()
+	print_debug("Got data from server: ", packet)
+
+ +

When the user clicks the join button, the client saves the username and sends it to the server. We can distinguish this from chat_messages, etc, by marking it as a "user_joined" packet. The client also sends the username and chat message from the UI's _input to the server. Both errors and voluntary or involuntary disconnections will stop the process. That covers everything we need to send messages. We'll cover receiving messages after we finish the server.

+ +

Writing the server.

+ +

+Server.gd (Node)
+
+const PORT = 8080
+var _server = WebSocketServer.new()
+var IDs = {}
+var peers = []
+
+func _ready():
+	_server.connect('client_connected', self, "_connected")
+	_server.connect('client_disconnected', self, "_disconnected")
+	_server.connect('client_close_request', self, "_close_request")
+	_server.connect("data_received", self, "_on_data_received")
+	
+	var err = _server.listen(PORT)
+	if err != OK:
+		print_debug("Unable to start server")
+		set_process(false)
+	print_debug("server started")
+	
+func _connected(id, protocol):
+	print_debug("Client %d connected with protocol: %s" % [id, protocol])
+	_server.get_peer(id).put_packet(JSON.print(
+		{'type': 'test', 'message': "Server test packet"}).to_utf8()
+	)
+	peers.append(id)
+	
+func _close_request(id, code, reason):
+	print_debug("Client %d disconnecting with code: %d, reason: %s" 
+		% [id, code, reason])
+	
+func _disconnected(id, was_clean = false):
+	print_debug("Client %d disconnected, clean: %s" 
+		% [id, str(was_clean)])
+	var user = IDs[id]
+	peers.erase(id)
+	for p in peers:
+		_server.get_peer(p).put_packet(JSON.print(
+			{'type': "server_message", "message": user 
+				+ "(#" + str(id) + ") left the chat."}
+		).to_utf8())
+	
+func _on_data_received(id):
+	
+	var original = _server.get_peer(id).get_packet().get_string_from_utf8()
+	var json = JSON.parse(original)
+	var packet = json.result
+	if typeof(packet) != 18:
+		push_error("%s is not a dictionary" % [packet])
+		get_tree().quit()
+	
+	print_debug("Got data from client %d: %s ... echoing" % [id, packet])
+	if packet['type'] == "user_joined": 
+		IDs[id] = packet['name']
+		for p in peers:
+			_server.get_peer(p).put_packet(
+				JSON.print(
+					{'type': "server_message", "message": packet['name'] 
+						+ "(#" + str(id) + ") joined the chat."}
+					).to_utf8())
+	elif packet['type'] == "chat_message" or packet['type'] == "server_message":
+		for p in peers:
+			_server.get_peer(p).put_packet(JSON.print(packet).to_utf8())
+	
+func _process(_delta):
+	_server.poll()
+
+
+ +

Upon a connection, the _connected method adds the id of the connection to the peers array. Upon a disconnection, it is erased. This way, you can access all users at any time by looping through the peers array.

+ +

The most dynamic method is _on_data_received, since it will react to a variety of packets. If it's a user_joined packet, it will save the id of his connection and his chosen username to the ID dictionary. Then the server will send a server_message to every connected client to announce the new member. If it is some kind of message, it will also send that to every client so everyone can see the same messages.

+ +

Lastly, upon a disconnection, the server will send a server_message to all clients about the departure.

+ +

Now let's make sure the client is ready to receive all these messages...and we'll be done!

+ +

Finishing up the client.

+ +

+Client.gd (Node)
+func receive_message(packet):
+	UI.display_message(packet)
+
+func _on_data_received():
+	var json = JSON.parse(
+		_client.get_peer(1).get_packet().get_string_from_utf8()
+	)
+	var packet = json.result
+	if typeof(packet) != 18:
+		push_error("%s is not a dictionary" % [packet])
+		get_tree().quit()
+	print_debug("Got data from server: ", packet)
+	if packet['type'] == "chat_message" or packet['type'] == "server_message":
+		receive_message(packet) 
+
+ +

If the client receives a chat_message or server_message from the server, it will call receive_message, which passes it to the UI for formatting and display.

+ +

Export both your server and client, so you can open lots of clients and fill your room with lots of people. Everyone will be able to see people joining, leaving, and chatting. Just like a real chat app!

+ +(Screenshot: A little chatplay using Jehoshaphat, Ahab, and Micaiah from 1 Kings 22.) + +

So the scrollbar covers some of the messages. And the scrolling is unusably annoying. The oldest messages should either disappear, or the chat should always be locked at the bottom on the latest messages. And there's no way to change your username without totally exiting and rejoining. And there must be some way to disable the pointless graphical window on the server. And there's probably a million more issues in addition to those.

+ +

But we're here to learn the basics, so we did an adequately good job. So good job anyway! Have fun improving from here and consider writing TCP guides of your own because UDP dominates the tutorial market. Later.^^

+ +
+ +

Check over the finished scripts.

+ +

You can see the finished project on my repo: server and client.

+ +

Server side.

+

+Server.gd (Node)
+
+extends Node
+
+const PORT = 8080
+var _server = WebSocketServer.new()
+var IDs = {}
+var peers = []
+
+func _ready():
+	_server.connect('client_connected', self, "_connected")
+	_server.connect('client_disconnected', self, "_disconnected")
+	_server.connect('client_close_request', self, "_close_request")
+	_server.connect("data_received", self, "_on_data_received")
+	
+	var err = _server.listen(PORT)
+	if err != OK:
+		print_debug("Unable to start server")
+		set_process(false)
+	print_debug("server started")
+	
+func _connected(id, protocol):
+	print_debug("Client %d connected with protocol: %s" % [id, protocol])
+	_server.get_peer(id).put_packet(JSON.print(
+		{'type': 'test', 'message': "Server test packet"}).to_utf8()
+	)
+	peers.append(id)
+	
+func _close_request(id, code, reason):
+	print_debug("Client %d disconnecting with code: %d, reason: %s" 
+		% [id, code, reason])
+	
+func _disconnected(id, was_clean = false):
+	print_debug("Client %d disconnected, clean: %s" 
+		% [id, str(was_clean)])
+	var user = IDs[id]
+	peers.erase(id)
+	for p in peers:
+		_server.get_peer(p).put_packet(JSON.print(
+			{'type': "server_message", "message": user 
+				+ "(#" + str(id) + ") left the chat."}
+		).to_utf8())
+	
+func _on_data_received(id):
+	
+	var original = _server.get_peer(id).get_packet().get_string_from_utf8()
+	var json = JSON.parse(original)
+	var packet = json.result
+	if typeof(packet) != 18:
+		push_error("%s is not a dictionary" % [packet])
+		get_tree().quit()
+	
+	print_debug("Got data from client %d: %s ... echoing" % [id, packet])
+	if packet['type'] == "user_joined": 
+		IDs[id] = packet['name']
+		for p in peers:
+			_server.get_peer(p).put_packet(
+				JSON.print(
+					{'type': "server_message", "message": packet['name'] 
+						+ "(#" + str(id) + ") joined the chat."}
+					).to_utf8())
+	elif packet['type'] == "chat_message" or packet['type'] == "server_message":
+		for p in peers:
+			_server.get_peer(p).put_packet(JSON.print(packet).to_utf8())
+	
+func _process(_delta):
+	_server.poll()
+
+ +

Client side.

+

+Client.gd (Node)
+extends Node
+
+export var websocket_url = "ws://127.0.0.1:8080"
+var _client = WebSocketClient.new()
+onready var UI = $UI
+var chat_name
+
+func join_chat(username):
+	chat_name = username
+	_client.get_peer(1).put_packet(JSON.print(
+			{'type': "user_joined", 'name': username}
+		).to_utf8()
+	)
+	
+func send_message(message):
+	_client.get_peer(1).put_packet(JSON.print(
+			{'type': 'chat_message', 'name': chat_name, 'message': message}
+		).to_utf8()
+	)
+	
+func receive_message(packet):
+	UI.display_message(packet)
+
+func _ready():
+	_client.connect("connection_closed", self, "_closed")
+	_client.connect("connection_error", self, "_error")
+	_client.connect("connection_established", self, "_connected")
+	_client.connect("data_received", self, "_on_data_received")
+	
+	var err = _client.connect_to_url(websocket_url)
+	if err != OK:
+		print_debug("Unable to connect")
+		set_process(false)
+	
+func _error(was_clean = false):
+	print_debug("Error. Clean break? ", was_clean)
+	set_process(false)
+	
+func _closed(was_clean = false):
+	print_debug("Closed. Clean break? ", was_clean)
+	set_process(false)
+	
+func _connected(protocol = ""):
+	print_debug("Connected with protocol: ", protocol)
+	_client.get_peer(1).put_packet(JSON.print(
+		{'type': 'test', 'message': "test packet from client"}
+	).to_utf8())
+	UI.join_button.show()
+	UI.username.show()
+	
+func _on_data_received():
+	var json = JSON.parse(
+		_client.get_peer(1).get_packet().get_string_from_utf8()
+	)
+	var packet = json.result
+	if typeof(packet) != 18:
+		push_error("%s is not a dictionary" % [packet])
+		get_tree().quit()
+	print_debug("Got data from server: ", packet)
+	if packet['type'] == "chat_message" or packet['type'] == "server_message":
+		receive_message(packet) 
+	
+func _process(_delta):
+	_client.poll()
+
+ +
+ +

+UI.gd (PanelContainer)
+
+extends PanelContainer
+
+onready var chat_log = $VBox/ChatLog
+onready var chat_input = $VBox/ChatInput
+onready var join_button = $VBox/HBox/JoinButton
+onready var username = $VBox/HBox/Name
+
+func display_message(packet): 
+	if packet['type'] == "chat_message":
+		chat_log.text += packet['name'] + ": " + packet['message'] + "\n"
+	elif packet.has("message"):
+		chat_log.text += packet['message'] + "\n"
+	
+func join_chat():
+	if !username.text:
+		display_message(
+            {'type': "alert", "message": "!!! Enter your username before joining chat."}
+        )
+	else:
+		get_parent().join_chat(username.text)
+		chat_input.show()
+		join_button.hide()
+		username.hide()
+	
+func _input(event):
+	if event is InputEventKey and chat_input.text:
+		if event.pressed and event.scancode == KEY_ENTER:
+			get_parent().send_message(chat_input.text)
+			chat_input.text = ""
+	
+func _ready():
+	join_button.connect("button_up", self, "join_chat")
+
+ +
+

Last updated February 26, 2022

+
diff --git a/src/index.py b/src/index.py index f4aa27a..c9c80d9 100644 --- a/src/index.py +++ b/src/index.py @@ -341,7 +341,7 @@ def serve_img(filename): @route('/favicon.ico', method='GET') def get_favicon(): - return static_file('favicon.ico', root='static/img/') + return static_file('favicon.ico', root='static/img') # Serve XML @route('/static/xml/') diff --git a/src/static/css/all.css b/src/static/css/all.css index cfcfa3c..1dd5b73 100755 --- a/src/static/css/all.css +++ b/src/static/css/all.css @@ -78,6 +78,11 @@ h2 a:hover,h2 a:active { h2 { font-size: 1.5em; } +hr { + background-color: transparent; + margin-top: 2em; + margin-bottom: 2em; +} p { line-height: 2em; margin-bottom: 1.5em; @@ -94,10 +99,17 @@ p { color: white; } pre,code { - background-color: transparent; + background-color: #d5c7d5; + font-size: 0.9em; + margin-top: 1.5em; + margin-bottom: 1.5em; + padding-left: 0.5em; + padding-right: 0.5em; + padding-bottom: 0.1em; } -.code { - background-color: transparent; +img { + margin-top: 1.5em; + margin-bottom: 1.5em; } .verse { background-color: transparent; diff --git a/src/static/css/entry.css b/src/static/css/entry.css index 07730d1..0c0c2b1 100644 --- a/src/static/css/entry.css +++ b/src/static/css/entry.css @@ -1,6 +1,10 @@ hr { border: 1px solid black; } +img { + display: block; + text-align: center; +} .content-grid { grid-area: 3 / 1 / 4 / 4; diff --git a/src/static/img/char/Silke/travel.kra b/src/static/img/char/Silke/travel.kra new file mode 100644 index 0000000..1057b64 Binary files /dev/null and b/src/static/img/char/Silke/travel.kra differ diff --git a/src/static/img/ent/ChatClient_Conversation_1Kings22.png b/src/static/img/ent/ChatClient_Conversation_1Kings22.png new file mode 100644 index 0000000..3f068ee Binary files /dev/null and b/src/static/img/ent/ChatClient_Conversation_1Kings22.png differ diff --git a/src/static/img/ent/Chatbox_ClientCommunicating.png b/src/static/img/ent/Chatbox_ClientCommunicating.png new file mode 100644 index 0000000..89dc7e5 Binary files /dev/null and b/src/static/img/ent/Chatbox_ClientCommunicating.png differ diff --git a/src/static/img/ent/Chatbox_ClientConnected.png b/src/static/img/ent/Chatbox_ClientConnected.png new file mode 100644 index 0000000..dcdaf7e Binary files /dev/null and b/src/static/img/ent/Chatbox_ClientConnected.png differ diff --git a/src/static/img/ent/Chatbox_ClientHierarchy.png b/src/static/img/ent/Chatbox_ClientHierarchy.png new file mode 100644 index 0000000..dfd0be1 Binary files /dev/null and b/src/static/img/ent/Chatbox_ClientHierarchy.png differ diff --git a/src/static/img/ent/Chatbox_ClientProjectSettings.png b/src/static/img/ent/Chatbox_ClientProjectSettings.png new file mode 100644 index 0000000..f986ac9 Binary files /dev/null and b/src/static/img/ent/Chatbox_ClientProjectSettings.png differ diff --git a/src/static/img/ent/Chatbox_Export.png b/src/static/img/ent/Chatbox_Export.png new file mode 100644 index 0000000..6547364 Binary files /dev/null and b/src/static/img/ent/Chatbox_Export.png differ diff --git a/src/static/img/ent/Chatbox_ExportAdd.png b/src/static/img/ent/Chatbox_ExportAdd.png new file mode 100644 index 0000000..10ff546 Binary files /dev/null and b/src/static/img/ent/Chatbox_ExportAdd.png differ diff --git a/src/static/img/ent/Chatbox_Hide.png b/src/static/img/ent/Chatbox_Hide.png new file mode 100644 index 0000000..663dc75 Binary files /dev/null and b/src/static/img/ent/Chatbox_Hide.png differ diff --git a/src/static/img/ent/Chatbox_LabelSizeFlags.png b/src/static/img/ent/Chatbox_LabelSizeFlags.png new file mode 100644 index 0000000..c538889 Binary files /dev/null and b/src/static/img/ent/Chatbox_LabelSizeFlags.png differ diff --git a/src/static/img/ent/Chatbox_ServerHierarchy.png b/src/static/img/ent/Chatbox_ServerHierarchy.png new file mode 100644 index 0000000..751b4c7 Binary files /dev/null and b/src/static/img/ent/Chatbox_ServerHierarchy.png differ diff --git a/src/static/img/ent/Chatbox_ShowLineNumbers.png b/src/static/img/ent/Chatbox_ShowLineNumbers.png new file mode 100644 index 0000000..314a1a1 Binary files /dev/null and b/src/static/img/ent/Chatbox_ShowLineNumbers.png differ diff --git a/src/static/img/ent/Chatbox_SizeFlags.png b/src/static/img/ent/Chatbox_SizeFlags.png new file mode 100644 index 0000000..6efb293 Binary files /dev/null and b/src/static/img/ent/Chatbox_SizeFlags.png differ diff --git a/src/static/img/ent/Chatbox_Stylebox.png b/src/static/img/ent/Chatbox_Stylebox.png new file mode 100644 index 0000000..44828a8 Binary files /dev/null and b/src/static/img/ent/Chatbox_Stylebox.png differ diff --git a/src/static/img/ent/Chatbox_UI.png b/src/static/img/ent/Chatbox_UI.png new file mode 100644 index 0000000..1221297 Binary files /dev/null and b/src/static/img/ent/Chatbox_UI.png differ diff --git a/src/static/img/ent/Chatbox_UI_rect_size.png b/src/static/img/ent/Chatbox_UI_rect_size.png new file mode 100644 index 0000000..51d56b1 Binary files /dev/null and b/src/static/img/ent/Chatbox_UI_rect_size.png differ diff --git a/src/static/img/host/cole3.png b/src/static/img/host/cole3.png new file mode 100644 index 0000000..2eb8f1a Binary files /dev/null and b/src/static/img/host/cole3.png differ diff --git a/src/static/img/host/logo4.png b/src/static/img/host/logo4.png new file mode 100644 index 0000000..3271162 Binary files /dev/null and b/src/static/img/host/logo4.png differ diff --git a/src/story/aries-training b/src/story/aries-training new file mode 100644 index 0000000..8073dae --- /dev/null +++ b/src/story/aries-training @@ -0,0 +1,60 @@ +goal: establish that Aries cares about Helia + +Yeah. I don't think it would take much to get me up to speed. +Let's spar! +Think so? +With practice, you'll learn. +>Eh, that wasn't your best. +>You'll get it. Keep trying. +Okay. + +I come here looking for you. +<...(raised eyebrows) +Oh, this is awkward. No, I don't really like the gym. +Yeah! +It's exciting. +I've never done it before. +Is that important to you? Surely that is never necessary here. It seems so safe and slow. +I'm fine! +Oh...I appreciate the training. +No...I'd rather not, honestly. +You know, I never feel like working out before I get here. +Oh yes. I have to force it. It feels good once I've started, but breaking the inertia. +Probably not. Probably just us. (smiles slyly) + + diff --git a/src/story/night- b/src/story/night- new file mode 100644 index 0000000..3de3f1d --- /dev/null +++ b/src/story/night- @@ -0,0 +1,6 @@ +It's a quiet place to be. +No- let's chat. +(go to tables) +The vision for Blessfrey


    + % for i in ["achievements", "armor-rating", "attribute", "attribute-point", "cast", "character", "controls", "credits", "damage-type", "death-penalty", "design-philosophy", "dialogue", "docs", "gear", "gig", "groups", "highlight", "ID", "inspect menu", "inventory", "item", "job", "keyword", "language", "life", "marketing", "mechanics", "Mercur", "milestones", "miracles", "pathfinding", "perk", "pip", "proxemics", "punishment", "setting", "skill", "spirit", "status-effect", "story", "style guide", "terms", "vibe", "website"]: + + % for i in ["achievements", "armor-rating", "attribute", "attribute-point", "cast", "character", "controls", "credits", "damage-type", "death-penalty", "dialogue", "docs", "environment-effect", "gear", "gig", "groups", "highlight", "ID", "inspect menu", "inventory", "item", "job", "keyword", "language", "life", "mechanics", "Mercur", "milestones", "miracles", "pathfinding", "perk", "pip", "proxemics", "setting", "skill", "spirit", "status-effect", "story", "style guide", "terms", "vibe", "website"]: +
  • {{random.choice(['.','•','☆','★'])}} {{i}}
  • % end
diff --git a/src/views/diary-boxes.tpl b/src/views/diary-boxes.tpl index e2ae260..2271d12 100644 --- a/src/views/diary-boxes.tpl +++ b/src/views/diary-boxes.tpl @@ -40,7 +40,8 @@
-
Blessfrey.me is not using sponsored posts or affiliate links.
+

disclosure

+ Blessfrey.me is not using sponsored posts or affiliate links.

Blessfrey.me is not collecting personal information + has no cookies.

diff --git a/src/views/ll-milestones.tpl b/src/views/ll-milestones.tpl new file mode 100644 index 0000000..bac405a --- /dev/null +++ b/src/views/ll-milestones.tpl @@ -0,0 +1,38 @@ +% import random +% rebase('frame.tpl') +
+

lemonland milestones

+

This is a list of everything I need to add before the Blessfrey is complete, version by version. I can add more in subsequent updates, but I have to draw the line somewhere. The list is broken into versions.

+
+

Focus and finish the game!

+ +


+

legend

+

nothing at all, designed, basic implementation, intentionally designed, documented, but with known issues, playtested and polished...or at least as done as it will ever be

+ + + +


+ + +

0.0 - ship literally anything at all

+
    +
  • {{random.choice(['.','•','☆','★'])}} feature: export, embed
  • +
  • {{random.choice(['.','•','☆','★'])}} usernames must be unique
  • +
  • {{random.choice(['.','•','☆','★'])}} pet names must be unique
  • +
  • {{random.choice(['.','•','☆','★'])}} usernames can't have unexpected characters like あ
  • +
  • {{random.choice(['.','•','☆','★'])}} pet names can't have unexpected characters like あ
  • +
+
  • {{random.choice(['.','•','☆','★'])}} accounts have a persistent username
  • +
  • {{random.choice(['.','•','☆','★'])}} can capture pet and add it to your pet list
  • + + +


    + + +
    + + + + diff --git a/src/views/me.tpl b/src/views/me.tpl index 21a0015..2ebe1a2 100644 --- a/src/views/me.tpl +++ b/src/views/me.tpl @@ -25,7 +25,7 @@

    other projects

      -
    • {{random.choice(['.','•','☆','★'])}} blessfrey.me - The most obvious, this very portfolio and blog website. It is coded in Bottle: Python, SimpleTemplate, and HTML+CSS. Any embedded applications are probably be HTML5.
    • +
    • {{random.choice(['.','•','☆','★'])}} blessfrey.me - The most obvious, this very portfolio and blog website. It is coded in Bottle: Python, SimpleTemplate, and HTML+CSS. Any embedded applications are probably HTML5.
    • {{random.choice(['.','•','☆','★'])}} Cat Store - Java, text-based cat breeding and cat show life sim.
    • {{random.choice(['.','•','☆','★'])}} itch.io (chimchooree) and GitLab (chimchooree) for micro games and general coding projects.
    • {{random.choice(['.','•','☆','★'])}} OCs from gamedev projects, creative writing, and games are stored in the characters page.
    • diff --git a/src/views/milestones.tpl b/src/views/milestones.tpl index 11b1cda..52dc730 100644 --- a/src/views/milestones.tpl +++ b/src/views/milestones.tpl @@ -117,6 +117,7 @@

      skills

        +
      • {{random.choice(['.','•','☆','★'])}} skillbar slots can be empty
      • {{random.choice(['.','•','☆','★'])}} skill loop
      • {{random.choice(['.','•','☆','★'])}} attack skill loop
      • {{random.choice(['.','•','☆','★'])}} skills consume an spirit cost
      • @@ -142,6 +143,7 @@
      • {{random.choice(['.','•','☆','★'])}} duplicate skills cannot be equipped onto the skillbar. All but the rightmost copy will be kept.
      • {{random.choice(['.','•','☆','★'])}} Echos let you duplicate skills temporarily, captures let you sideload a dupe on the bar, thievery skills also, etc. After zoning, only the rightmost dupe will be kept.
      • {{random.choice(['.','•','☆','★'])}} environment effect is a type of skill that is triggered rather than activated. It cannot be learned or used.
      • +
      • {{random.choice(['.','•','☆','★'])}} environment effect continuously is applied to new characters entering area
      • {{random.choice(['.','•','☆','★'])}} skills can be canceled
      • {{random.choice(['.','•','☆','★'])}} skills can be canceled during their activation time
      • {{random.choice(['.','•','☆','★'])}} skills can be canceled by pressing a movement key
      • @@ -150,6 +152,11 @@
      • {{random.choice(['.','•','☆','★'])}} canceled skills still exact their initial costs
      • {{random.choice(['.','•','☆','★'])}} canceled skills do not need to recharge
      • {{random.choice(['.','•','☆','★'])}} canceled skills do not cause aftercast delay
      • +
      • {{random.choice(['.','•','☆','★'])}} skills can affect multiple targets
      • +
      • {{random.choice(['.','•','☆','★'])}} skills can fail if not targeting a human
      • +
      • {{random.choice(['.','•','☆','★'])}} skill can be self-targeting, regardless of target
      • +
      • {{random.choice(['.','•','☆','★'])}} skills can affect everything within an area
      • +
      • {{random.choice(['.','•','☆','★'])}} skills can affect everything of a certain type
      • skill autoequip from save✗
      • skill equip new skill/empty slot/rearrange skillbar✗
      • skill use by click/keyboard/AI selection✗