diff --git a/source/actions.py b/source/actions.py
index cce0c16..6e07a10 100644
--- a/source/actions.py
+++ b/source/actions.py
@@ -1,5 +1,7 @@
 from typing import Any
 
+import colors
+
 class BaseAction:
 	entity: Any
 
@@ -57,14 +59,27 @@ class MeleeAction(DirectionAction):
 
 		if not target:
 			return False
-		
+
+		#apply damage
 		damage = self.entity.fighter.attack - target.fighter.defense
+		target.fighter.current_hp -= damage
+
+		#calculate output
+		engine = self.entity.floor_map.engine
+		msg_text = f"{self.entity.name} attacked {target.name}"
+		msg_color = colors.white
+
+		if self.entity is engine.player:
+			msg_color = colors.player_atk
+		else:
+			msg_color = colors.enemy_atk
 
 		if damage > 0:
-			print(f"{self.entity.name} attacked {target.name} for {damage} damage")
-			target.fighter.current_hp -= damage
+			msg_text += f" for {damage} damage"
 		else:
-			print(f"{self.entity.name} attacked {target.name} but was ineffective")
+			msg_text += f" but was ineffective"
+
+		engine.message_log.add_message(text = msg_text, fg=msg_color)
 
 		return True
 
diff --git a/source/colors.py b/source/colors.py
new file mode 100644
index 0000000..d446790
--- /dev/null
+++ b/source/colors.py
@@ -0,0 +1,15 @@
+#copy/pasted, because reasons
+white = (0xFF, 0xFF, 0xFF)
+black = (0x0, 0x0, 0x0)
+
+player_atk = (0xE0, 0xE0, 0xE0)
+enemy_atk = (0xFF, 0xC0, 0xC0)
+
+player_die = (0xFF, 0x30, 0x30)
+enemy_die = (0xFF, 0xA0, 0x30)
+
+welcome_text = (0x20, 0xA0, 0xFF)
+
+bar_text = white
+bar_filled = (0x0, 0x60, 0x0)
+bar_empty = (0x40, 0x10, 0x10)
diff --git a/source/components/fighter.py b/source/components/fighter.py
index a18b6f4..cdb80ea 100644
--- a/source/components/fighter.py
+++ b/source/components/fighter.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 from typing import Any
 
+import colors
 from components.base_component import BaseComponent
 
 class Fighter(BaseComponent):
@@ -29,13 +30,15 @@ class Fighter(BaseComponent):
 
 	
 	def die_and_despawn(self) -> None:
-		if self.entity is self.entity.floor_map.engine.player and self.entity.ai:
+		engine = self.entity.floor_map.engine
+
+		if self.entity is engine.player and self.entity.ai:
 			from event_handler import GameOverEventHandler
 			self.entity.floor_map.engine.event_handler = GameOverEventHandler(self.entity.floor_map.engine)
-			print("You died")
+			engine.message_log.add_message("You died.", colors.player_die)
 
 		else:
-			print(f"The {self.entity.name} died")
+			engine.message_log.add_message(f"The {self.entity.name} died", colors.enemy_die)
 
 		self.entity.char = "%"
 		self.entity.color = (191, 0, 0)
diff --git a/source/engine.py b/source/engine.py
index 3a9ee07..6a977a7 100644
--- a/source/engine.py
+++ b/source/engine.py
@@ -2,16 +2,23 @@ from tcod.context import Context
 from tcod.console import Console
 from tcod.map import compute_fov
 
+from message_log import Message, MessageLog
+from render_functions import render_hp_bar
+
 import entity_types
 from floor_map import FloorMap #TODO: replace with "DungeonMap"
 
 class Engine:
-	def __init__(self, floor_map: FloorMap):
+	def __init__(self, floor_map: FloorMap, intro_msg: Message = None):
 		from event_handler import InGameEventHandler
 		self.event_handler = InGameEventHandler(self)
 		self.floor_map = floor_map
 		self.floor_map.engine = self #references everywhere!
 
+		self.message_log = MessageLog()
+		if intro_msg:
+			self.message_log.push_message(intro_msg)
+
 		#grab the player object
 		self.player = self.floor_map.player
 
@@ -38,9 +45,18 @@ class Engine:
 		self.floor_map.render(console)
 
 		#UI
-		console.print(
-			x=1, y=47,
-			string=f"HP: {self.player.fighter.current_hp}/{self.player.fighter.current_hp}",
+		render_hp_bar(
+			console = console,
+			current_value = self.player.fighter.current_hp,
+			max_value = self.player.fighter.maximum_hp,
+			total_width = 20
+		)
+		self.message_log.render(
+			console=console,
+			x=21,
+			y=45 - 5,
+			width = 40,
+			height = 5
 		)
 
 		#send to the screen
diff --git a/source/entity_types.py b/source/entity_types.py
index 96a7af4..18c7d61 100644
--- a/source/entity_types.py
+++ b/source/entity_types.py
@@ -8,7 +8,7 @@ player = Actor(
 	name = "Player",
 	walkable = False,
 	ai_class = BaseAI,
-	fighter = Fighter(hp = 10, attack = 2, defense = 0),
+	fighter = Fighter(hp = 10, attack = 2, defense = 1),
 )
 
 #gobbos
@@ -18,7 +18,7 @@ gobbo = Actor(
 	name = "Gobbo",
 	walkable = False,
 	ai_class  = AttackOnSight,
-	fighter = Fighter(hp = 5, attack = 2, defense = 0),
+	fighter = Fighter(hp = 5, attack = 1, defense = 0),
 )
 
 gobbo_red = Actor(
diff --git a/source/main.py b/source/main.py
index 396fed5..2b46ef0 100755
--- a/source/main.py
+++ b/source/main.py
@@ -3,6 +3,8 @@ import tcod
 
 from engine import Engine
 from procgen import generate_floor_map
+from message_log import Message
+import colors
 
 def main() -> None:
 	#tcod stuff
@@ -16,6 +18,8 @@ def main() -> None:
 
 	w, h = context.recommended_console_size(min_columns=10, min_rows=10)
 
+	print (w,h)
+
 	console = tcod.console.Console(
 		width = w,
 		height = h + 5,
@@ -24,7 +28,8 @@ def main() -> None:
 
 	engine = Engine(
 		#is created externally, because
-		floor_map = generate_floor_map(80, 45, 10, 10)
+		floor_map = generate_floor_map(80, 45, 10, 10),
+		intro_msg = Message("Welcome to the Cave of Gobbos!", colors.welcome_text)
 	)
 
 	#game loop that never returns
diff --git a/source/message_log.py b/source/message_log.py
new file mode 100644
index 0000000..3aefa0f
--- /dev/null
+++ b/source/message_log.py
@@ -0,0 +1,49 @@
+from typing import List, Reversible, Tuple
+from textwrap import TextWrapper
+
+from tcod.console import Console
+
+import colors
+
+
+class Message:
+	def __init__(self, text: str, fg: Tuple[int, int, int] = colors.white, count: int = 1):
+		self.raw_text = text
+		self.fg = fg
+		self.count = count
+
+	@property
+	def full_text(self) -> str:
+		if self.count > 1:
+			return f"{self.raw_text} (x{self.count})"
+		return self.raw_text
+
+
+class MessageLog:
+	def __init__(self):
+		self.messages: List[Message] = []
+
+	def add_message(self, text: str, fg: Tuple[int, int, int] = colors.white, *, stack: bool = True) -> None:
+		if stack and self.messages and text == self.messages[-1].raw_text:
+			self.messages[-1].count += 1
+		else:
+			self.messages.append(Message(text, fg))
+	
+	def push_message(self, msg: Message) -> None:
+		self.messages.append(msg)
+
+	def render(self, console: Console, x: int, y: int, width: int, height: int) -> None:
+		self.render_messages(console, x, y, width, height, self.messages)
+
+	@staticmethod
+	def render_messages(console: Console, x: int, y: int, width: int, height: int, messages: Reversible[Message]) -> None:
+		y_offset = height - 1
+
+		wrapper = TextWrapper(width=width, subsequent_indent = " ")
+
+		for message in reversed(messages):
+			for line in reversed(wrapper.wrap(message.full_text)): #oh, neat
+				console.print(x=x,y=y + y_offset,string=line,fg=message.fg)
+				y_offset -= 1
+				if y_offset < 0:
+					return
\ No newline at end of file
diff --git a/source/render_functions.py b/source/render_functions.py
new file mode 100644
index 0000000..352fbd8
--- /dev/null
+++ b/source/render_functions.py
@@ -0,0 +1,15 @@
+from __future__ import annotations
+
+import colors
+
+from tcod.console import Console
+
+def render_hp_bar(console: Console, current_value: int, max_value: int, total_width: int) -> None:
+	bar_width = int(float(current_value) / max_value * total_width)
+
+	console.draw_rect(x=0, y=45, width=total_width, height=1, ch=1, bg=colors.bar_empty)
+
+	if bar_width > 0:
+		console.draw_rect(x=0, y=45, width=bar_width, height=1, ch=1, bg=colors.bar_filled)
+	
+	console.print(x=1, y=45, string=f"HP: {current_value}/{max_value}", fg=colors.bar_text)
\ No newline at end of file