diff --git a/data/events/test_event.json b/data/events/test_event.json
index c23ed8d..0d2e838 100644
--- a/data/events/test_event.json
+++ b/data/events/test_event.json
@@ -3,12 +3,11 @@
   "items": [
     {
       "type": "text",
-      "texts": [
-        "This is a bucket",
-        "Dear God",
-        "There's more!",
-        "No!"
-      ]
+      "text": "This is a bucket"
+    },
+    {
+      "type": "text",
+      "text": "Dear God"
     }
   ]
 }
\ No newline at end of file
diff --git a/data/map project.tiled-session b/data/map project.tiled-session
index 5157b4f..921a6ac 100644
--- a/data/map project.tiled-session	
+++ b/data/map project.tiled-session	
@@ -37,7 +37,7 @@
         "overworld.tsx"
     ],
     "project": "map project.tiled-project",
-    "property.type": "tileSolidType",
+    "property.type": "string",
     "recentFiles": [
         "overworld.tsx",
         "map.tmj",
diff --git a/data/map.tmj b/data/map.tmj
index 739284f..c310d4a 100644
--- a/data/map.tmj
+++ b/data/map.tmj
@@ -425,6 +425,11 @@
                 {
                  "id":13,
                  "properties":[
+                        {
+                         "name":"interactEvent",
+                         "type":"string",
+                         "value":"test_event"
+                        }, 
                         {
                          "name":"interactText",
                          "type":"string",
@@ -434,7 +439,7 @@
                          "name":"interactType",
                          "propertytype":"npcInteractType",
                          "type":"string",
-                         "value":"NPC_INTERACT_TYPE_TEXT"
+                         "value":"NPC_INTERACT_TYPE_EVENT"
                         }],
                  "template":"templates\/NPC.tx",
                  "x":6539.95833333333,
diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt
index dbb7017..9fc6bf7 100644
--- a/src/dusk/CMakeLists.txt
+++ b/src/dusk/CMakeLists.txt
@@ -25,6 +25,7 @@ target_sources(${DUSK_TARGET_NAME}
 add_subdirectory(assert)
 add_subdirectory(display)
 add_subdirectory(entity)
+add_subdirectory(event)
 add_subdirectory(item)
 add_subdirectory(locale)
 add_subdirectory(physics)
diff --git a/src/dusk/entity/npc.c b/src/dusk/entity/npc.c
index d576077..3065f8b 100644
--- a/src/dusk/entity/npc.c
+++ b/src/dusk/entity/npc.c
@@ -37,41 +37,10 @@ void npcInteract(entity_t *player, entity_t *self) {
       break;
 
     case NPC_INTERACT_TYPE_EVENT:
+      eventSetActive(self->npc.eventData);
       break;
 
     default:
       assertUnreachable("Unknown NPC interaction type");
   }
-
-  // uiTextboxSetText(
-  //   "Hello World how are you today? Hope things are going well for you! I am "
-  //   "having a great day, hope you are too. Did I ever tell you about the tragedy "
-  //   "of Darth Plagueis the Wise? He was a dark lord of the Sith, "
-  //   "so powerful and so wise he could use the Force to influence the midichlorians"
-  // );
-  // const char_t *name = "Dom";
-  // const char_t *key = "name";
-  // uint16_t len = languageFormat(
-  //   "test.npc.text",
-  //   NULL,
-  //   0,
-  //   (const char_t *[]){ key },
-  //   (const char_t *[]){ name },
-  //   1
-  // );
-
-  // char_t *buffer = malloc(sizeof(char_t) + len + 1);
-  // assertNotNull(buffer, "Failed to allocate buffer for NPC text");
-
-  // languageFormat(
-  //   "test.npc.text",
-  //   buffer,
-  //   len + 1,
-  //   (const char_t *[]){ key },
-  //   (const char_t *[]){ name },
-  //   1
-  // );
-
-  // uiTextboxSetText(buffer);
-  // free(buffer);
 }
\ No newline at end of file
diff --git a/src/dusk/entity/npc.h b/src/dusk/entity/npc.h
index ad03843..bc903d9 100644
--- a/src/dusk/entity/npc.h
+++ b/src/dusk/entity/npc.h
@@ -1,6 +1,6 @@
 
 #pragma once
-#include "dusk.h"
+#include "event/eventlist.h"
 
 typedef struct _entity_t entity_t;
 
@@ -15,7 +15,8 @@ typedef struct {
   npcinteracttype_t interactType;
 
   union {
-    char_t* text;
+    const char_t* text;
+    const eventdata_t *eventData;
   };
 } npc_t;
 
diff --git a/src/dusk/event/CMakeLists.txt b/src/dusk/event/CMakeLists.txt
new file mode 100644
index 0000000..b052311
--- /dev/null
+++ b/src/dusk/event/CMakeLists.txt
@@ -0,0 +1,11 @@
+# Copyright (c) 2025 Dominic Masters
+#
+# This software is released under the MIT License.
+# https://opensource.org/licenses/MIT
+
+# Sources
+target_sources(${DUSK_TARGET_NAME}
+  PRIVATE
+    event.c
+    eventtext.c
+)
\ No newline at end of file
diff --git a/src/dusk/event/event.c b/src/dusk/event/event.c
index 0def703..fd391c8 100644
--- a/src/dusk/event/event.c
+++ b/src/dusk/event/event.c
@@ -5,4 +5,69 @@
  * https://opensource.org/licenses/MIT
  */
 
-#include "event.h"
\ No newline at end of file
+#include "event.h"
+#include "util/memory.h"
+#include "assert/assert.h"
+
+eventcallback_t EVENT_CALLBACKS[] = {
+  [EVENT_TYPE_NULL] = { NULL, NULL },
+  [EVENT_TYPE_TEXT] = { eventTextStart, eventTextUpdate },
+};
+
+event_t EVENT;
+
+void eventInit() {
+  memoryZero(&EVENT, sizeof(event_t));
+}
+
+void eventUpdate() {
+  if(EVENT.active == NULL) {
+    return; // No active event to update
+  }
+
+  const eventitem_t *item = &EVENT.active->items[EVENT.item];
+  assertNotNull(
+    EVENT_CALLBACKS[item->type].update,
+    "Event type does not have an update callback"
+  );
+
+  EVENT_CALLBACKS[item->type].update(item);
+}
+
+void eventSetActive(const eventdata_t *event) {
+  assertNotNull(event, "Event data cannot be NULL");
+  assertTrue(
+    event->itemCount <= EVENT_ITEM_COUNT_MAX,
+    "Event count too high"
+  );
+  assertTrue(event->itemCount > 0, "Event must have at least one item");
+
+  EVENT.active = event;
+  EVENT.item = 0;
+
+  const eventitem_t *firstItem = &EVENT.active->items[EVENT.item];
+
+  assertNotNull(
+    EVENT_CALLBACKS[firstItem->type].start,
+    "Event type does not have a start callback"
+  );
+  EVENT_CALLBACKS[firstItem->type].start(firstItem);
+}
+
+void eventNext() {
+  assertNotNull(EVENT.active, "No active event to proceed with");
+  assertTrue(EVENT.item < EVENT.active->itemCount, "No more items in the event");
+
+  EVENT.item++;
+  if (EVENT.item >= EVENT.active->itemCount) {
+    EVENT.active = NULL;
+    return;
+  }
+
+  const eventitem_t *nextItem = &EVENT.active->items[EVENT.item];
+  assertNotNull(
+    EVENT_CALLBACKS[nextItem->type].start,
+    "Event type does not have a start callback"
+  );
+  EVENT_CALLBACKS[nextItem->type].start(nextItem);
+}
\ No newline at end of file
diff --git a/src/dusk/event/event.h b/src/dusk/event/event.h
index edab15b..42ede28 100644
--- a/src/dusk/event/event.h
+++ b/src/dusk/event/event.h
@@ -6,24 +6,41 @@
  */
 
 #pragma once
-#include "eventtext.h"
-
-typedef enum {
-  EVENT_TYPE_NULL = 0,
-  EVENT_TYPE_TEXT,
-} eventtype_t;
-
-typedef struct _eventitem_t {
-  eventtype_t type;
-
-  union {
-    eventtext_t texts;
-  };
-} eventitem_t;
-
-#define EVENT_ITEM_COUNT_MAX 32
+#include "eventdata.h"
 
 typedef struct {
-  uint8_t itemCount;
-  eventitem_t items[EVENT_ITEM_COUNT_MAX];
-} event_t;
\ No newline at end of file
+  eventdata_t data;
+  const eventdata_t *active;
+  uint8_t item;
+} event_t;
+
+typedef struct {
+  void (*start)(const eventitem_t *item);
+  void (*update)(const eventitem_t *item);
+} eventcallback_t;
+
+extern eventcallback_t EVENT_CALLBACKS[EVENT_TYPE_COUNT];
+extern event_t EVENT;
+
+/**
+ * Initializes the event system.
+ */
+void eventInit();
+
+/**
+ * Updates the active event.
+ */
+void eventUpdate();
+
+/**
+ * Sets the active event.
+ * 
+ * @param event The event to set as active.
+ */
+void eventSetActive(const eventdata_t *eventData);
+
+/**
+ * Goes to the next item in the active event. Only meant to be called by
+ * event items.
+ */
+void eventNext();
\ No newline at end of file
diff --git a/src/dusk/event/eventdata.h b/src/dusk/event/eventdata.h
new file mode 100644
index 0000000..75799a0
--- /dev/null
+++ b/src/dusk/event/eventdata.h
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2025 Dominic Masters
+ * 
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma once
+#include "eventitem.h"
+
+typedef struct {
+  uint8_t itemCount;
+  eventitem_t items[EVENT_ITEM_COUNT_MAX];
+} eventdata_t;
diff --git a/src/dusk/event/eventitem.h b/src/dusk/event/eventitem.h
new file mode 100644
index 0000000..3174f31
--- /dev/null
+++ b/src/dusk/event/eventitem.h
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2025 Dominic Masters
+ * 
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#pragma ocne
+#include "eventtext.h"
+
+typedef enum {
+  EVENT_TYPE_NULL = 0,
+  EVENT_TYPE_TEXT,
+} eventtype_t;
+
+#define EVENT_TYPE_COUNT 2
+
+typedef struct _eventitem_t {
+  eventtype_t type;
+
+  union {
+    eventtext_t text;
+  };
+} eventitem_t;
+
+#define EVENT_ITEM_COUNT_MAX 32
\ No newline at end of file
diff --git a/src/dusk/event/eventtext.c b/src/dusk/event/eventtext.c
new file mode 100644
index 0000000..f9eee4c
--- /dev/null
+++ b/src/dusk/event/eventtext.c
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2025 Dominic Masters
+ * 
+ * This software is released under the MIT License.
+ * https://opensource.org/licenses/MIT
+ */
+
+#include "event.h"
+#include "ui/uitextbox.h"
+#include "assert/assert.h"
+
+void eventTextStart(const eventitem_t *item) {
+  assertNotNull(item, "Event item cannot be NULL");
+  assertTrue(item->type == EVENT_TYPE_TEXT, "Event item must be of type TEXT");
+  assertNotNull(item->text, "Event item must have at least one text");
+  uiTextboxSetText(item->text);
+}
+
+void eventTextUpdate(const eventitem_t *item) {
+  assertNotNull(item, "Event item cannot be NULL");
+  assertTrue(item->type == EVENT_TYPE_TEXT, "Event item must be of type TEXT");
+
+  if(!UI_TEXTBOX.visible) {
+    eventNext();
+  }
+}
\ No newline at end of file
diff --git a/src/dusk/event/eventtext.h b/src/dusk/event/eventtext.h
index 2ebd7ac..bddb2ca 100644
--- a/src/dusk/event/eventtext.h
+++ b/src/dusk/event/eventtext.h
@@ -12,4 +12,25 @@ typedef struct _eventitem_t eventitem_t;
 
 #define EVENT_TEXT_STRING_COUNT_MAX 8
 
-typedef const char_t* eventtext_t[EVENT_TEXT_STRING_COUNT_MAX];
\ No newline at end of file
+typedef const char_t* eventtext_t;
+
+/**
+ * Starts the text event for the given item.
+ * 
+ * @param item The event item to start.
+ */
+void eventTextStart(const eventitem_t *item);
+
+/**
+ * Updates the text event for the given item.
+ * 
+ * @param item The event item to update.
+ */
+void eventTextUpdate(const eventitem_t *item);
+
+/**
+ * Query whether the text event is done or not.
+ * 
+ * @param item The event item to check.
+ */
+bool_t eventTextIsDone(const eventitem_t *item);
\ No newline at end of file
diff --git a/src/dusk/main.c b/src/dusk/main.c
index 2c001cd..8cf791d 100644
--- a/src/dusk/main.c
+++ b/src/dusk/main.c
@@ -11,13 +11,14 @@
 #include "display/scene.h"
 #include "world/overworld.h"
 #include "input.h"
-
+#include "event/event.h"
 #include "ui/uitextbox.h"
 
 // Press F5 to compile and run the compiled game.gb in the Emulicous Emulator/Debugger
 void main(void) {
   renderInit();
   inputInit();
+  eventInit();
   uiTextboxInit();
   
   overworldInit();
@@ -30,6 +31,7 @@ void main(void) {
     
     overworldUpdate();
     uiTextboxUpdate();
+    eventUpdate();
 
     // Update input for next frame.
     inputUpdate();
diff --git a/src/dusk/ui/uitextbox.c b/src/dusk/ui/uitextbox.c
index 9900d64..aebb79a 100644
--- a/src/dusk/ui/uitextbox.c
+++ b/src/dusk/ui/uitextbox.c
@@ -188,5 +188,4 @@ void uiTextboxSetText(const char_t *text) {
 
     assertUnreachable("Code should not reach here, all cases handled.");
   }
-  printf("test");
 }
\ No newline at end of file
diff --git a/tools/eventcompile/eventcompile.py b/tools/eventcompile/eventcompile.py
index 1dc9ec7..361bb54 100644
--- a/tools/eventcompile/eventcompile.py
+++ b/tools/eventcompile/eventcompile.py
@@ -55,8 +55,8 @@ for jsonFile in jsonFiles:
     f.write(f"// Generated event header for {jsonFile}\n")
     f.write(f"// Generated at {now}\n")
     f.write("#pragma once\n\n")
-    f.write("#include \"event/event.h\"\n\n")
-    f.write(f"static const event_t EVENT_{key.upper()} = {{\n")
+    f.write("#include \"event/eventdata.h\"\n\n")
+    f.write(f"static const eventdata_t EVENT_{key.upper()} = {{\n")
     f.write(f"  .itemCount = {len(data['items'])},\n")
     f.write(f"  .items = {{\n")
     for i, item in enumerate(data['items']):
@@ -69,14 +69,11 @@ for jsonFile in jsonFiles:
 
       # Text(s) Type
       if itemType == 'text':
-        if 'texts' not in item or not isinstance(item['texts'], list) or len(item['texts']) == 0:
-          print(f"Error: Item {i} in '{jsonFile}' of type 'text' does not contain 'texts' field.")
+        if 'text' not in item:
+          print(f"Error: Item {i} in '{jsonFile}' of type 'text' does not contain 'text' field.")
           sys.exit(1)
         f.write(f"      .type = EVENT_TYPE_TEXT,\n")
-        f.write(f"      .texts = {{\n")
-        for text in item['texts']:
-          f.write(f"        \"{text}\",\n")
-        f.write(f"      }},\n")
+        f.write(f"      .text = \"{item['text']}\",\n")
 
 
       else:
@@ -87,4 +84,20 @@ for jsonFile in jsonFiles:
     f.write(f"  }},\n")
     f.write(f"}};\n\n")
 
-    eventFiles.append(key)
\ No newline at end of file
+    eventFiles.append(key)
+
+# Write the event list header
+eventListFile = os.path.join(outputDir, "eventlist.h")
+with open(eventListFile, 'w', encoding='utf-8') as f:
+    f.write(f"// Generated event list header\n")
+    f.write(f"// Generated at {now}\n")
+    f.write("#pragma once\n\n")
+    f.write("#include \"event/event.h\"\n")
+    for event in eventFiles:
+        f.write(f"#include \"event/{event}.h\"\n")
+    f.write("\n")
+    f.write(f"#define EVENT_LIST_COUNT {len(eventFiles)}\n\n")
+    f.write("static const eventdata_t* EVENT_LIST[EVENT_LIST_COUNT] = {\n")
+    for event in eventFiles:
+        f.write(f"  &EVENT_{event.upper()},\n")
+    f.write("};\n\n")
\ No newline at end of file
diff --git a/tools/mapcompile/entityParser.py b/tools/mapcompile/entityParser.py
index b4130b5..53336e5 100644
--- a/tools/mapcompile/entityParser.py
+++ b/tools/mapcompile/entityParser.py
@@ -30,11 +30,35 @@ def parseEntity(obj, chunkData):
   obj['dir'] = 'ENTITY_DIR_SOUTH'
   obj['type'] = entType
 
+  def getProperty(propName):
+    for prop in obj['properties']:
+      if prop['name'] == propName:
+        return prop['value']
+    return None
+
   # Handle per-type properties
   if entType == 'ENTITY_TYPE_NPC':
+    interactType = getProperty('interactType')
+    if interactType is None:
+      print(f"NPC entity missing 'interactType' property: {obj['id']}")
+      sys.exit(1)
+
     obj['data'] = {}
     obj['data']['npc'] = {}
-    obj['data']['npc']['interactType'] = 'NPC_INTERACT_TYPE_TEXT'
-    obj['data']['npc']['text'] = '"test.npc.text"'
+    obj['data']['npc']['interactType'] = interactType
+
+    if interactType == 'NPC_INTERACT_TYPE_TEXT':
+      text = getProperty('interactText')
+      if text is None:
+        print(f"NPC entity missing 'interactText' property: {obj['id']}")
+        sys.exit(1)
+      obj['data']['npc']['text'] = text
+
+    elif interactType == 'NPC_INTERACT_TYPE_EVENT':
+      event = getProperty('interactEvent')
+      if event is None:
+        print(f"NPC entity missing 'interactEvent' property: {obj['id']}")
+        sys.exit(1)
+      obj['data']['npc']['eventData'] = f'&EVENT_{event.upper()}'
 
   return obj
\ No newline at end of file