diff --git a/ext/src.zip b/ext/src.zip
index 9119d18..3ebecc5 100644
Binary files a/ext/src.zip and b/ext/src.zip differ
diff --git a/src/app.cpp b/src/app.cpp
index a583071..a0f080e 100644
--- a/src/app.cpp
+++ b/src/app.cpp
@@ -14,7 +14,9 @@ constexpr auto context_ci = le::Context::CreateInfo{
 };
 } // namespace
 
-App::App() : m_context(context_ci), m_data_loader(le::FileDataLoader::upfind("assets")) { bind_services(); }
+App::App() : m_context(context_ci), m_data_loader(le::FileDataLoader::upfind("assets")), m_resources(m_context.create_asset_loader(&m_data_loader)) {
+	bind_services();
+}
 
 void App::run() {
 	auto game = Game{&m_services};
@@ -40,5 +42,7 @@ void App::bind_services() {
 	// m_data_loader is bound to both the interface and the concrete class for use through either type.
 	m_services.bind<le::IDataLoader>(&m_data_loader);
 	m_services.bind<le::FileDataLoader>(&m_data_loader);
+
+	m_services.bind(&m_resources);
 }
 } // namespace miracle
diff --git a/src/app.hpp b/src/app.hpp
index de44f45..90a9b92 100644
--- a/src/app.hpp
+++ b/src/app.hpp
@@ -2,6 +2,7 @@
 #include <le2d/context.hpp>
 #include <le2d/file_data_loader.hpp>
 #include <le2d/service_locator.hpp>
+#include <resources.hpp>
 
 namespace miracle {
 class App {
@@ -15,6 +16,7 @@ class App {
 
 	le::Context m_context;
 	le::FileDataLoader m_data_loader{};
+	Resources m_resources;
 
 	le::ServiceLocator m_services{};
 };
diff --git a/src/enemy.hpp b/src/enemy.hpp
index 20f6773..81d6c2a 100644
--- a/src/enemy.hpp
+++ b/src/enemy.hpp
@@ -7,9 +7,8 @@
 #include <le2d/renderer.hpp>
 #include <le2d/service_locator.hpp>
 #include <cstddef>
-#include <optional>
-#include "glm/vec2.hpp"
-#include "le2d/texture.hpp"
+#include <memory>
+#include "le2d/resource/texture.hpp"
 
 namespace miracle {
 class Enemy {
@@ -35,7 +34,7 @@ class Enemy {
 
   private:
 	gsl::not_null<le::ServiceLocator const*> m_services;
-	std::optional<le::Texture> m_texture;
+	std::unique_ptr<le::ITexture> m_texture;
 	le::drawable::Circle m_sprite{};
 	glm::vec2 m_target_pos{};
 	float m_move_speed{};
diff --git a/src/game.cpp b/src/game.cpp
index 547aac5..c178b32 100644
--- a/src/game.cpp
+++ b/src/game.cpp
@@ -1,7 +1,7 @@
 #include <game.hpp>
 #include <glm/gtx/norm.hpp>
 #include <le2d/context.hpp>
-#include <algorithm>
+#include <resources.hpp>
 #include <cstddef>
 #include <format>
 #include <iterator>
@@ -9,8 +9,6 @@
 #include <vector>
 #include "enemy.hpp"
 #include "kvf/time.hpp"
-#include "le2d/asset_loader.hpp"
-#include "le2d/data_loader.hpp"
 #include "le2d/drawable/text.hpp"
 #include "lighthouse.hpp"
 #include "util/random.hpp"
@@ -18,10 +16,9 @@
 namespace miracle {
 Game::Game(gsl::not_null<le::ServiceLocator const*> services) : m_services(services), m_lighthouse(services), m_light(services) {
 	spawn_wave();
-	auto const& data_loader = services->get<le::IDataLoader>();
-	auto const& context = services->get<le::Context>();
-	auto const asset_loader = le::AssetLoader{&data_loader, &context};
-	m_font = asset_loader.load_font("fonts/specialElite.ttf");
+	auto& resources = services->get<Resources>();
+	m_font = resources.load<le::IFont>("fonts/specialElite.ttf");
+	if (!m_font) { throw std::runtime_error{"Failed to load font"}; }
 }
 
 void Game::on_cursor_pos(le::event::CursorPos const& cursor_pos) {
@@ -79,7 +76,7 @@ void Game::update_score(int points) {
 	m_score += points;
 	m_score_str.clear();
 	std::format_to(std::back_inserter(m_score_str), "Score: {}", m_score);
-	m_score_text.set_string(m_font, m_score_str);
+	m_score_text.set_string(*m_font, m_score_str);
 }
 
 void Game::update_health_text() {
@@ -95,7 +92,7 @@ void Game::update_health_text() {
 		std::format_to(std::back_inserter(m_health_str), "Health: {:.1f}", m_lighthouse.get_health());
 	}
 
-	m_health_text.set_string(m_font, m_health_str);
+	m_health_text.set_string(*m_font, m_health_str);
 }
 
 } // namespace miracle
diff --git a/src/game.hpp b/src/game.hpp
index 2b52532..8f0e63e 100644
--- a/src/game.hpp
+++ b/src/game.hpp
@@ -4,10 +4,9 @@
 #include <le2d/event.hpp>
 #include <le2d/renderer.hpp>
 #include <le2d/service_locator.hpp>
-#include <cstddef>
 #include "enemy.hpp"
 #include "le2d/drawable/text.hpp"
-#include "le2d/font.hpp"
+#include "le2d/resource/font.hpp"
 #include "light.hpp"
 #include "lighthouse.hpp"
 
@@ -29,7 +28,7 @@ class Game {
 	Lighthouse m_lighthouse;
 	Light m_light;
 
-	le::Font m_font{};
+	le::IFont* m_font{};
 	le::drawable::Text m_score_text{};
 	le::drawable::Text m_health_text{};
 	int m_score{};
diff --git a/src/light.hpp b/src/light.hpp
index 5b0a1f4..79333bb 100644
--- a/src/light.hpp
+++ b/src/light.hpp
@@ -1,12 +1,11 @@
 #pragma once
 
 #include "enemy.hpp"
-#include "glm/vec2.hpp"
 #include "gsl/pointers"
 #include "le2d/drawable/shape.hpp"
 #include "le2d/renderer.hpp"
+#include "le2d/resource/texture.hpp"
 #include "le2d/service_locator.hpp"
-#include "le2d/texture.hpp"
 
 namespace miracle {
 class Light {
@@ -19,7 +18,7 @@ class Light {
 
   private:
 	gsl::not_null<le::ServiceLocator const*> m_services;
-	std::optional<le::Texture> m_texture;
+	std::unique_ptr<le::ITexture> m_texture;
 	le::drawable::Circle m_sprite{};
 	float m_diameter{};
 };
diff --git a/src/lighthouse.cpp b/src/lighthouse.cpp
index 0be75fd..10279e4 100644
--- a/src/lighthouse.cpp
+++ b/src/lighthouse.cpp
@@ -1,17 +1,13 @@
 #include <glm/gtc/matrix_transform.hpp>
 #include <lighthouse.hpp>
+#include <resources.hpp>
 #include "glm/gtx/norm.hpp"
-#include "le2d/asset_loader.hpp"
-#include "le2d/data_loader.hpp"
 
 namespace miracle {
 Lighthouse::Lighthouse(gsl::not_null<le::ServiceLocator const*> services) : m_services(services) {
 	m_sprite.create(m_hitbox_diameter);
-	auto const& data_loader = services->get<le::IDataLoader>();
-	auto const& context = services->get<le::Context>();
-	auto const asset_loader = le::AssetLoader{&data_loader, &context};
-	m_texture = asset_loader.load_texture("images/lighthouse.png");
-	m_sprite.texture = &m_texture.value();
+	auto& resources = services->get<Resources>();
+	m_sprite.texture = resources.load<le::ITexture>("images/lighthouse.png");
 }
 
 void Lighthouse::rotate_towards_cursor(glm::vec2 cursor_pos) {
diff --git a/src/lighthouse.hpp b/src/lighthouse.hpp
index 9170f64..711f3f1 100644
--- a/src/lighthouse.hpp
+++ b/src/lighthouse.hpp
@@ -6,10 +6,7 @@
 #include <le2d/event.hpp>
 #include <le2d/renderer.hpp>
 #include <le2d/service_locator.hpp>
-#include <cstddef>
-#include <optional>
 #include "enemy.hpp"
-#include "le2d/texture.hpp"
 
 namespace miracle {
 class Lighthouse {
@@ -27,7 +24,6 @@ class Lighthouse {
 	gsl::not_null<le::ServiceLocator const*> m_services;
 	float m_hitbox_diameter{150.0f};
 	float m_visibility_diameter{250.0f};
-	std::optional<le::Texture> m_texture;
 	le::drawable::Circle m_sprite{};
 	float m_health{100};
 };
diff --git a/src/resources.cpp b/src/resources.cpp
new file mode 100644
index 0000000..514538d
--- /dev/null
+++ b/src/resources.cpp
@@ -0,0 +1,15 @@
+#include <resources.hpp>
+
+namespace miracle {
+Resources::Resources(le::AssetLoader asset_loader) : m_asset_loader(std::move(asset_loader)) {}
+
+void Resources::erase(std::string_view const uri) {
+	if (auto const it = m_map.find(uri); it != m_map.end()) { m_map.erase(it); }
+}
+
+auto Resources::find(std::string_view const uri, std::type_index const type) const -> le::IAsset* {
+	auto const it = m_map.find(uri);
+	if (it == m_map.end() || it->second.type != type) { return nullptr; }
+	return it->second.asset.get();
+}
+} // namespace miracle
diff --git a/src/resources.hpp b/src/resources.hpp
new file mode 100644
index 0000000..510dc80
--- /dev/null
+++ b/src/resources.hpp
@@ -0,0 +1,67 @@
+#pragma once
+#include <djson/string_table.hpp>
+#include <klib/assert.hpp>
+#include <le2d/asset/asset_loader.hpp>
+#include <concepts>
+
+namespace miracle {
+class Resources {
+  public:
+	explicit Resources(le::AssetLoader asset_loader);
+
+	template <std::derived_from<le::IAsset> AssetTypeT>
+	[[nodiscard]] auto insert(std::string uri, std::unique_ptr<AssetTypeT> asset) -> AssetTypeT& {
+		KLIB_ASSERT(asset);
+		auto& ret = *asset;
+		m_map.insert_or_assign(std::move(uri), Entry::create(std::move(asset)));
+		return ret;
+	}
+
+	void erase(std::string_view uri);
+
+	[[nodiscard]] auto contains(std::string_view const uri) const -> bool { return m_map.contains(uri); }
+
+	template <std::derived_from<le::IAsset> AssetTypeT>
+	[[nodiscard]] auto peek(std::string_view const uri) const -> AssetTypeT* {
+		auto const it = m_map.find(uri);
+		if (it == m_map.end() || it->second.type != typeid(AssetTypeT)) { return nullptr; }
+		return static_cast<AssetTypeT*>(it->second.asset.get());
+	}
+
+	template <std::derived_from<le::IAsset> AssetTypeT>
+	[[nodiscard]] auto at(std::string_view const uri) const -> AssetTypeT& {
+		auto* ret = peek<AssetTypeT>(uri);
+		KLIB_ASSERT(ret);
+		return *ret;
+	}
+
+	template <std::derived_from<le::IAsset> AssetTypeT>
+	[[nodiscard]] auto load(std::string_view const uri) -> AssetTypeT* {
+		if (auto* ret = peek<AssetTypeT>(uri); ret) { return ret; }
+		std::unique_ptr<AssetTypeT> asset = m_asset_loader.load<AssetTypeT>(uri);
+		if (!asset) { return nullptr; }
+		return &insert(std::string{uri}, std::move(asset));
+	}
+
+	void clear() { m_map.clear(); }
+
+	[[nodiscard]] auto size() const -> std::size_t { return m_map.size(); }
+	[[nodiscard]] auto is_empty() const -> bool { return m_map.empty(); }
+
+  private:
+	struct Entry {
+		template <typename T>
+		[[nodiscard]] static auto create(std::unique_ptr<T> asset) -> Entry {
+			return Entry{.type = typeid(T), .asset = std::move(asset)};
+		}
+
+		std::type_index type;
+		std::unique_ptr<le::IAsset> asset{};
+	};
+
+	[[nodiscard]] auto find(std::string_view uri, std::type_index type) const -> le::IAsset*;
+
+	le::AssetLoader m_asset_loader;
+	dj::StringTable<Entry> m_map{};
+};
+} // namespace miracle
