Browse Source

Merge branch 'level-1'

* level-1: (27 commits)
  Add support for culling objects that are out-of-bounds using the polygon-polygon intersection check.
  Split level::update into sensible sectors.
  Bugfix: Don't pass along the bullet prototype each time.
  Use the internal scaling mechanism of engine::to_object.
  Fix multithread allocation crash caused by using static variables.
  *Fix* object scaling. Fix some json type names. Add world data to the game initialization.
  Properly bind the player to the level.
  Fixing updates for enemy and burstshot pattern.
  Make loading a world a little cleaner for now...
  Add world code.
  Start defining player.
  Bind enemy to factory.
  Link bullet_pattern_factory to actor construction
  Start writing ctor.
  Move bullets.
  Make bullet collidable.
  actor binding basics
  const-correct for graphics::manager. Add some wave data to levels.
  Start defining actors and things better.
  Add bullet to level stub.
  ...
Sam Jaffe 6 years ago
parent
commit
a95630f1c3

+ 108 - 0
.clang-format

@@ -0,0 +1,108 @@
+---
+Language:        Cpp
+# BasedOnStyle:  LLVM
+AccessModifierOffset: -2
+AlignAfterOpenBracket: Align
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+AlignEscapedNewlines: Right
+AlignOperands:   true
+AlignTrailingComments: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortBlocksOnASingleLine: true
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: true
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: false
+BinPackArguments: true
+BinPackParameters: true
+BraceWrapping:   
+  AfterClass:      false
+  AfterControlStatement: false
+  AfterEnum:       false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  BeforeCatch:     false
+  BeforeElse:      false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeBraces: Attach
+BreakBeforeInheritanceComma: false
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeColon
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: true
+ColumnLimit:     80
+CommentPragmas:  '^ IWYU pragma:'
+CompactNamespaces: true
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: false
+DisableFormat:   false
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: false
+ForEachMacros:   
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+IncludeCategories: 
+  - Regex:           '^"(llvm|llvm-c|clang|clang-c)/'
+    Priority:        2
+  - Regex:           '^(<|"(gtest|gmock|isl|json)/)'
+    Priority:        3
+  - Regex:           '.*'
+    Priority:        1
+IncludeIsMainRegex: '(Test)?$'
+IndentCaseLabels: false
+IndentWidth:     2
+IndentWrappedFunctionNames: false
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: true
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: All
+ObjCBlockIndentWidth: 2
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 19
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakString: 1000
+PenaltyExcessCharacter: 1000000
+PenaltyReturnTypeOnItsOwnLine: 60
+PointerAlignment: Middle
+ReflowComments:  true
+SortIncludes:    true
+SortUsingDeclarations: true
+SpaceAfterCStyleCast: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeParens: ControlStatements
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles:  false
+SpacesInContainerLiterals: true
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+Standard:        Cpp11
+TabWidth:        8
+UseTab:          Never
+...
+

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "external/resource_factory"]
+	path = external/resource_factory
+	url = git@freenas:utility/resource_factory

+ 35 - 0
Resources/scripts/entity/drake.json

@@ -0,0 +1,35 @@
+{
+  "name":"Drake",
+  "type":"enemy",
+  "hp":10,
+  "scale":0.15,
+  "solid":true,
+  "points":{
+    "damage":100,
+    "kill":500
+  },
+  "material":{
+    "texture":{
+      "file":"data/images/rock.png",
+      "uniform":"u_diffuseMap"
+    }
+  },
+  "attack":[
+    {
+      "id":"burstshot",
+      "timeBetweenBullets":0.1,
+      "bulletsPerWave":15,
+      "burstAngle":360,
+      "bulletSpeed":900,
+      "prototype":{
+        "damage":2,
+        "scale":10.0,
+        "material":{
+          "texture":{
+            "uniform":"u_diffuseMap"
+          }
+        }
+      }
+    }
+  ]
+}

+ 0 - 0
Resources/scripts/entity/elder-dragon.json


+ 31 - 0
Resources/scripts/level/1/level.json

@@ -0,0 +1,31 @@
+{
+  "timeBetweenWaves":10,
+  "waves":[
+    [
+      {"id":"Drake", "position":[350, 600]},
+      {"id":"Drake", "position":[800, 600]},
+      {"id":"Drake", "position":[1250, 600]}
+    ],
+    [
+      {"id":"Drake", "position":[200, 800]},
+      {"id":"Drake", "position":[500, 800]},
+      {"id":"Drake", "position":[800, 800]},
+      {"id":"Drake", "position":[1100, 800]},
+      {"id":"Drake", "position":[1400, 800]}
+    ],
+    [
+      {"id":"Drake", "position":[200, 400]},
+      {"id":"Drake", "position":[500, 400]},
+      {"id":"Drake", "position":[800, 400]},
+      {"id":"Drake", "position":[1100, 400]},
+      {"id":"Drake", "position":[1400, 400]}
+    ],
+    [
+      {"id":"ElderDragon", "position":[600, 600]}
+    ]
+  ],
+  "mappings":{
+    "Drake":"drake.json",
+    "ElderDragon":"elder-dragon.json"
+  }
+}

+ 9 - 0
Resources/scripts/level/world.json

@@ -0,0 +1,9 @@
+{
+  "name":"world-1",
+  "levels":[
+    {
+      "name":"light-1",
+      "path":"1"
+    }
+  ]
+}

+ 37 - 0
Resources/scripts/player/player.json

@@ -0,0 +1,37 @@
+{
+  "name":"Player",
+  "type":"player",
+  "hp":100,
+  "scale":0.25,
+  "speed":400,
+  "solid":true,
+  "material":{
+    "texture":{
+      "file":"data/images/rock.png",
+      "uniform":"u_diffuseMap"
+    }
+  },
+  "attack":[
+    {
+      "id":"burstshot",
+      "timeBetweenBullets":0.05,
+      "burstAngle":120.0,
+      "centerAngle":90.0,
+      "bulletSpeed":900,
+      "prototype":{
+        "damage":2,
+        "size":10.0,
+        "material":{
+          "texture":{
+            "uniform":"u_diffuseMap"
+          }
+        }
+      }
+    }
+  ],
+  "shield":{
+    "max":100.0,
+    "regen":1.0
+  },
+  "level":1
+}

+ 88 - 0
danmaku.xcodeproj/project.pbxproj

@@ -16,6 +16,19 @@
 		CD1C83402298A9E000825C4E /* libgameutils.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD7E882322960D9C00D877FE /* libgameutils.dylib */; };
 		CD1C83412298A9E000825C4E /* libgraphics.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD7E883922960DBF00D877FE /* libgraphics.dylib */; };
 		CD1C83432298A9F500825C4E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CD1C83422298A9F500825C4E /* MainMenu.xib */; };
+		CD49F73D229AFF2900EB8926 /* scripts in Resources */ = {isa = PBXBuildFile; fileRef = CD49F736229AFF2800EB8926 /* scripts */; };
+		CD49F75F229B09F800EB8926 /* level.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD49F75E229B09F800EB8926 /* level.cxx */; };
+		CD49F762229B0A0500EB8926 /* enemy.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD49F761229B0A0500EB8926 /* enemy.cxx */; };
+		CD49F764229B0A2400EB8926 /* danmaku in Resources */ = {isa = PBXBuildFile; fileRef = CD49F763229B0A2400EB8926 /* danmaku */; };
+		CD49F767229B0A3000EB8926 /* player.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD49F766229B0A3000EB8926 /* player.cxx */; };
+		CD49F76A229B0A6C00EB8926 /* bullet.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD49F769229B0A6C00EB8926 /* bullet.cxx */; };
+		CD49F76D229B0A8000EB8926 /* bullet_pattern.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD49F76C229B0A8000EB8926 /* bullet_pattern.cxx */; };
+		CD49F778229B0FCD00EB8926 /* actor.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD49F777229B0FCD00EB8926 /* actor.cxx */; };
+		CD49F783229B194C00EB8926 /* burstshot_pattern.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD49F782229B194C00EB8926 /* burstshot_pattern.cxx */; };
+		CD49F784229B25DE00EB8926 /* libmath.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD7E883022960DBC00D877FE /* libmath.dylib */; };
+		CD49F786229B291D00EB8926 /* libjsoncpp.1.8.4.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD49F785229B291D00EB8926 /* libjsoncpp.1.8.4.dylib */; };
+		CD49F794229C22A800EB8926 /* serial.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD49F793229C22A800EB8926 /* serial.cxx */; };
+		CD49F7B3229C530A00EB8926 /* world.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD49F7B2229C530A00EB8926 /* world.cxx */; };
 		CD7E87A52295FCED00D877FE /* danmakuUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD7E87A42295FCED00D877FE /* danmakuUITests.m */; };
 /* End PBXBuildFile section */
 
@@ -68,6 +81,18 @@
 		CD1C82E22298A46900825C4E /* data */ = {isa = PBXFileReference; lastKnownFileType = folder; name = data; path = Resources/data; sourceTree = "<group>"; };
 		CD1C83292298A89E00825C4E /* danmaku.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = danmaku.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		CD1C83422298A9F500825C4E /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = MainMenu.xib; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
+		CD49F736229AFF2800EB8926 /* scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; name = scripts; path = Resources/scripts; sourceTree = "<group>"; };
+		CD49F75E229B09F800EB8926 /* level.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = level.cxx; sourceTree = "<group>"; };
+		CD49F761229B0A0500EB8926 /* enemy.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = enemy.cxx; sourceTree = "<group>"; };
+		CD49F763229B0A2400EB8926 /* danmaku */ = {isa = PBXFileReference; lastKnownFileType = folder; name = danmaku; path = include/danmaku; sourceTree = "<group>"; };
+		CD49F766229B0A3000EB8926 /* player.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = player.cxx; sourceTree = "<group>"; };
+		CD49F769229B0A6C00EB8926 /* bullet.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = bullet.cxx; sourceTree = "<group>"; };
+		CD49F76C229B0A8000EB8926 /* bullet_pattern.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = bullet_pattern.cxx; sourceTree = "<group>"; };
+		CD49F777229B0FCD00EB8926 /* actor.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = actor.cxx; sourceTree = "<group>"; };
+		CD49F782229B194C00EB8926 /* burstshot_pattern.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = burstshot_pattern.cxx; sourceTree = "<group>"; };
+		CD49F785229B291D00EB8926 /* libjsoncpp.1.8.4.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libjsoncpp.1.8.4.dylib; path = ../../../../../../opt/local/lib/libjsoncpp.1.8.4.dylib; sourceTree = "<group>"; };
+		CD49F793229C22A800EB8926 /* serial.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = serial.cxx; sourceTree = "<group>"; };
+		CD49F7B2229C530A00EB8926 /* world.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = world.cxx; sourceTree = "<group>"; };
 		CD7E87862295FCEA00D877FE /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
 		CD7E87872295FCEA00D877FE /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
 		CD7E878D2295FCEA00D877FE /* GameView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GameView.h; sourceTree = "<group>"; };
@@ -90,6 +115,8 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CD49F786229B291D00EB8926 /* libjsoncpp.1.8.4.dylib in Frameworks */,
+				CD49F784229B25DE00EB8926 /* libmath.dylib in Frameworks */,
 				CD1C833F2298A9E000825C4E /* libengine.dylib in Frameworks */,
 				CD1C83402298A9E000825C4E /* libgameutils.dylib in Frameworks */,
 				CD1C83412298A9E000825C4E /* libgraphics.dylib in Frameworks */,
@@ -106,6 +133,38 @@
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
+		CD49F756229B09E100EB8926 /* src */ = {
+			isa = PBXGroup;
+			children = (
+				CD49F75E229B09F800EB8926 /* level.cxx */,
+				CD49F793229C22A800EB8926 /* serial.cxx */,
+				CD49F7B2229C530A00EB8926 /* world.cxx */,
+				CD49F781229B104900EB8926 /* entity */,
+				CD49F77F229B103200EB8926 /* pattern */,
+			);
+			path = src;
+			sourceTree = "<group>";
+		};
+		CD49F77F229B103200EB8926 /* pattern */ = {
+			isa = PBXGroup;
+			children = (
+				CD49F76C229B0A8000EB8926 /* bullet_pattern.cxx */,
+				CD49F782229B194C00EB8926 /* burstshot_pattern.cxx */,
+			);
+			path = pattern;
+			sourceTree = "<group>";
+		};
+		CD49F781229B104900EB8926 /* entity */ = {
+			isa = PBXGroup;
+			children = (
+				CD49F777229B0FCD00EB8926 /* actor.cxx */,
+				CD49F761229B0A0500EB8926 /* enemy.cxx */,
+				CD49F766229B0A3000EB8926 /* player.cxx */,
+				CD49F769229B0A6C00EB8926 /* bullet.cxx */,
+			);
+			path = entity;
+			sourceTree = "<group>";
+		};
 		CD7E877A2295FCEA00D877FE = {
 			isa = PBXGroup;
 			children = (
@@ -114,7 +173,10 @@
 				CD7E881D22960D9C00D877FE /* gameutils.xcodeproj */,
 				CD7E880B22960D8200D877FE /* engine.xcodeproj */,
 				CD1C82E22298A46900825C4E /* data */,
+				CD49F736229AFF2800EB8926 /* scripts */,
 				CD7E87852295FCEA00D877FE /* danmaku */,
+				CD49F763229B0A2400EB8926 /* danmaku */,
+				CD49F756229B09E100EB8926 /* src */,
 				CD7E87A32295FCED00D877FE /* danmakuUITests */,
 				CD7E87842295FCEA00D877FE /* Products */,
 				CD7E87CE2295FFC500D877FE /* Frameworks */,
@@ -158,6 +220,7 @@
 		CD7E87CE2295FFC500D877FE /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				CD49F785229B291D00EB8926 /* libjsoncpp.1.8.4.dylib */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
@@ -338,9 +401,11 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CD49F764229B0A2400EB8926 /* danmaku in Resources */,
 				CD1C833E2298A9D800825C4E /* Assets.xcassets in Resources */,
 				CD1C833D2298A9D600825C4E /* data in Resources */,
 				CD1C83432298A9F500825C4E /* MainMenu.xib in Resources */,
+				CD49F73D229AFF2900EB8926 /* scripts in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -358,8 +423,17 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CD49F76D229B0A8000EB8926 /* bullet_pattern.cxx in Sources */,
 				CD1C833B2298A98200825C4E /* GameView.mm in Sources */,
+				CD49F778229B0FCD00EB8926 /* actor.cxx in Sources */,
 				CD1C833C2298A98700825C4E /* main.m in Sources */,
+				CD49F767229B0A3000EB8926 /* player.cxx in Sources */,
+				CD49F794229C22A800EB8926 /* serial.cxx in Sources */,
+				CD49F783229B194C00EB8926 /* burstshot_pattern.cxx in Sources */,
+				CD49F762229B0A0500EB8926 /* enemy.cxx in Sources */,
+				CD49F76A229B0A6C00EB8926 /* bullet.cxx in Sources */,
+				CD49F75F229B09F800EB8926 /* level.cxx in Sources */,
+				CD49F7B3229C530A00EB8926 /* world.cxx in Sources */,
 				CD1C833A2298A97F00825C4E /* AppDelegate.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -388,6 +462,10 @@
 					"$(inherited)",
 					"@executable_path/../Frameworks",
 				);
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					/opt/local/lib,
+				);
 				PRODUCT_BUNDLE_IDENTIFIER = leumasjaffe.danmaku;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 			};
@@ -406,6 +484,10 @@
 					"$(inherited)",
 					"@executable_path/../Frameworks",
 				);
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					/opt/local/lib,
+				);
 				PRODUCT_BUNDLE_IDENTIFIER = leumasjaffe.danmaku;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 			};
@@ -467,9 +549,12 @@
 				MTL_FAST_MATH = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = macosx;
+				SYSTEM_HEADER_SEARCH_PATHS = /opt/local/include/;
 				USER_HEADER_SEARCH_PATHS = (
 					"$(PROJECT_DIR)/../game/include/expect/include",
 					"$(BUILT_PRODUCTS_DIR)/usr/local/include/",
+					"$(PROJECT_DIR)/include/",
+					"$(PROJECT_DIR)/external/resource_factory/include/",
 				);
 			};
 			name = Debug;
@@ -523,9 +608,12 @@
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_FAST_MATH = YES;
 				SDKROOT = macosx;
+				SYSTEM_HEADER_SEARCH_PATHS = /opt/local/include/;
 				USER_HEADER_SEARCH_PATHS = (
 					"$(PROJECT_DIR)/../game/include/expect/include",
 					"$(BUILT_PRODUCTS_DIR)/usr/local/include/",
+					"$(PROJECT_DIR)/include/",
+					"$(PROJECT_DIR)/external/resource_factory/include/",
 				);
 			};
 			name = Release;

+ 2 - 3
danmaku/AppDelegate.m

@@ -15,8 +15,7 @@
 NSWindowStyleMask const resizable = NSWindowStyleMaskResizable;
 NSWindowStyleMask const borderless = NSWindowStyleMaskBorderless;
 
-- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
-{
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
   gameview = [[GameView alloc] initWithFrame:[[self window] frame]];
   
   // Insert code here to initialize your application
@@ -25,7 +24,7 @@ NSWindowStyleMask const borderless = NSWindowStyleMaskBorderless;
   [[self window] makeMainWindow];
 }
 
-- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
+- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)app {
   return YES;
 }
 

+ 4 - 4
danmaku/Base.lproj/MainMenu.xib

@@ -671,14 +671,14 @@
         <window title="DanmakuRPG" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="GameWindow">
             <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
             <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
-            <rect key="contentRect" x="0.0" y="0.0" width="1200" height="700"/>
-            <rect key="screenRect" x="0.0" y="0.0" width="1440" height="877"/>
+            <rect key="contentRect" x="0.0" y="0.0" width="1400" height="800"/>
+            <rect key="screenRect" x="0.0" y="0.0" width="1400" height="800"/>
             <view key="contentView" id="EiT-Mj-1SZ">
-                <rect key="frame" x="0.0" y="0.0" width="1200" height="700"/>
+                <rect key="frame" x="0.0" y="0.0" width="1400" height="800"/>
                 <autoresizingMask key="autoresizingMask"/>
                 <subviews>
                     <openGLView fixedFrame="YES" useAuxiliaryDepthBufferStencil="NO" useDoubleBufferingEnabled="YES" allowOffline="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9rQ-QQ-4fS" customClass="GameView">
-                        <rect key="frame" x="0.0" y="0.0" width="1200" height="700"/>
+                        <rect key="frame" x="0.0" y="0.0" width="1400" height="800"/>
                         <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
                     </openGLView>
                 </subviews>

+ 30 - 18
danmaku/GameView.mm

@@ -11,6 +11,7 @@
 #include <memory>
 #include <OpenGL/gl3.h>
 
+#include "danmaku/world.hpp"
 #include "game/engine/events.hpp"
 #include "game/engine/game_dispatch.hpp"
 #include "game/graphics/renderer.hpp"
@@ -18,10 +19,15 @@
 namespace env { namespace detail {
   extern void resize_screen(math::vec2i const&);
 }}
-static std::shared_ptr<graphics::direct_renderer> renderer;
-static std::shared_ptr<engine::game_dispatch> game;
 
-@implementation GameView
+@implementation GameView {
+  std::shared_ptr<graphics::direct_renderer> renderer;
+  std::shared_ptr<engine::game_dispatch> game;
+};
+
+- (id)initWithCoder:(NSCoder *)decoder {
+  return self = [super initWithCoder:decoder];
+}
 
 - (id)initWithFrame:(NSRect)aRect {
   NSOpenGLPixelFormatAttribute attr[] = {
@@ -32,13 +38,22 @@ static std::shared_ptr<engine::game_dispatch> game;
   };
   
   // create pixel format
-  NSOpenGLPixelFormat *nsglFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
+  NSOpenGLPixelFormat *nsglFormat = [[NSOpenGLPixelFormat alloc]
+                                     initWithAttributes:attr];
 
   // create the context...
   if (!(self = [super initWithFrame:aRect pixelFormat:nsglFormat])) {
     return nil;
   }
 
+  using graphics::direct_renderer;
+  using graphics::driver;
+  renderer = std::make_shared<direct_renderer>(driver::openGL);
+  game = std::make_shared<engine::game_dispatch>(renderer);
+  auto world = danmaku::world::load_world("scripts/level/world.json", game);
+  game->register_scene(world);
+  game->activate_scene("light-1");
+
   // make the context current
   NSOpenGLContext* context = [[NSOpenGLContext alloc] initWithFormat:nsglFormat
                                                         shareContext:nil];
@@ -50,13 +65,6 @@ static std::shared_ptr<engine::game_dispatch> game;
 }
 
 - (void)prepareOpenGL {
-  using graphics::direct_renderer;
-  using graphics::driver;
-  renderer = std::make_shared<direct_renderer>(driver::openGL);
-  game = std::make_shared<engine::game_dispatch>(renderer);
-  
-  renderer->clear();
-  
   // enable vertical sychronization to refresh rate
   const GLint vals = 0x01;
   [[self openGLContext] setValues:&vals forParameter:NSOpenGLCPSwapInterval];
@@ -72,8 +80,10 @@ static std::shared_ptr<engine::game_dispatch> game;
   
   env::detail::resize_screen({{(int)dirtyRect.size.width,
                                (int)dirtyRect.size.height}});
-  renderer->clear();
-  game->render();
+  if (game) {
+    renderer->clear();
+    game->render();
+  }
   //  glFlush();
   // the correct way to do double buffering on Mac is this:
   [[self openGLContext] flushBuffer];
@@ -144,13 +154,14 @@ static std::shared_ptr<engine::game_dispatch> game;
   [self press_key:LEFT_ARROW forEvent:theEvent ns_key:0x007b down:YES];
   [self press_key:RIGHT_ARROW forEvent:theEvent ns_key:0x007c down:YES];
 
-//  NSLog(@"keyDown -> { 0x%04x, 0x%08lx, 0x%02x=%c }", [theEvent keyCode],
-//        [theEvent modifierFlags], c, c);
+  NSLog(@"keyDown -> { 0x%04x, 0x%08lx, 0x%02x=%c }", [theEvent keyCode],
+        [theEvent modifierFlags], c, c);
   game->process_key_event(c, true);
 }
 
 - (void)keyUp:(NSEvent *)theEvent {
-  //  NSLog(@"keyUp -> { 0x%04x, 0x%08lx }", [theEvent keyCode], [theEvent modifierFlags]);
+  NSLog(@"keyUp -> { 0x%04x, 0x%08lx }", [theEvent keyCode],
+        [theEvent modifierFlags]);
   NSString *str = [theEvent charactersIgnoringModifiers];
   unichar c = [str characterAtIndex:0];
   
@@ -183,7 +194,7 @@ static NSTimeInterval const FRAME_INTERVAL = 1.0 / 60.0;
 
 - (void)windowDidBecomeMain:(NSNotification *)notification {
   NSLog(@"window did become main");
-  
+
   // env::resume_clock();
   [self setNeedsDisplay:YES];
   
@@ -224,7 +235,8 @@ static NSTimeInterval const FRAME_INTERVAL = 1.0 / 60.0;
     (NSOpenGLPixelFormatAttribute)0
   };
   
-  NSOpenGLPixelFormat *pf = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes];
+  NSOpenGLPixelFormat *pf = [[NSOpenGLPixelFormat alloc]
+                             initWithAttributes:attributes];
   [self setPixelFormat:pf];
 }
 

+ 1 - 0
external/resource_factory

@@ -0,0 +1 @@
+Subproject commit 1a2b2209461a0bb046e69b183e3ac491495fb1e4

+ 40 - 0
include/danmaku/actor.hpp

@@ -0,0 +1,40 @@
+//
+//  actor.hpp
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include "game/engine/entity.hpp"
+
+namespace objects { namespace prototype {
+  template <typename, typename...> class factory;
+}}
+
+namespace graphics {
+  class manager;
+}
+
+namespace danmaku {
+  class level;
+  struct actor : public engine::entity {
+    using engine::entity::entity;
+    virtual ~actor() = default;
+    virtual void update(float delta) = 0;
+    virtual void level(class level *) = 0;
+    virtual class level * level() const = 0;
+  };
+
+  struct bullet_pattern;
+  using attack_factory = std::function<std::shared_ptr<bullet_pattern>(
+      Json::Value const &, actor *)>;
+  using actor_factory =
+      objects::prototype::factory<std::unique_ptr<actor>, Json::Value const &,
+                                  graphics::object const &, attack_factory>;
+}
+
+#define BIND_ACTOR(name, function)                                             \
+  bool const _ = actor_factory::instance().bind(name, function)

+ 26 - 0
include/danmaku/bullet.hpp

@@ -0,0 +1,26 @@
+//
+//  bullet.hpp
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include "actor.hpp"
+
+namespace danmaku {
+  class bullet : public engine::collidable {
+  public:
+    bullet(int damage, math::vec2 const & vel, graphics::object const & obj);
+    void update(float delta);
+
+    void set_position(math::vec2 const & ll, math::radian facing);
+    void set_velocity(math::vec2 const & v) { velocity_ = v; }
+
+  private:
+    int damage_;
+    math::vec2 velocity_;
+  };
+}

+ 42 - 0
include/danmaku/bullet_pattern.hpp

@@ -0,0 +1,42 @@
+//
+//  bullet_pattern.hpp
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <json/forwards.h>
+#include <memory>
+
+namespace objects { namespace prototype {
+  template <typename, typename...> class factory;
+}}
+
+namespace graphics {
+  class manager;
+}
+
+namespace danmaku {
+  class actor;
+
+  struct bullet_timer {
+    float const interval;
+    float remaining{0.f};
+  };
+
+  struct bullet_pattern {
+    virtual ~bullet_pattern() = default;
+    virtual void update(float delta) = 0;
+  };
+
+  using bullet_pattern_factory =
+      objects::prototype::factory<std::shared_ptr<bullet_pattern>, actor *,
+                                  Json::Value const &,
+                                  graphics::manager const &>;
+}
+
+#define BIND_BULLET_PATTERN(name, function)                                    \
+  bool const _ = bullet_pattern_factory::instance().bind(name, function)

+ 35 - 0
include/danmaku/enemy.hpp

@@ -0,0 +1,35 @@
+//
+//  enemy.hpp
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include "actor.hpp"
+
+namespace danmaku {
+  struct points_map {
+    int on_damage;
+    int on_kill;
+  };
+
+  class enemy : public actor {
+  public:
+    enemy(Json::Value const & json, graphics::object const & obj,
+          attack_factory get_attack);
+
+    void update(float delta) override;
+    void level(class level * l) override { level_ = l; }
+    class level * level() const override {
+      return level_;
+    }
+
+  private:
+    class level * level_{nullptr};
+    points_map points_;
+    std::vector<std::shared_ptr<bullet_pattern>> attack_;
+  };
+}

+ 53 - 0
include/danmaku/level.hpp

@@ -0,0 +1,53 @@
+//
+//  level.hpp
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include "game/engine/scene.hpp"
+
+#include <deque>
+#include <json/forwards.h>
+#include <memory>
+#include <vector>
+
+namespace danmaku {
+  class actor;
+  class bullet;
+  class player;
+  class level : public engine::scene {
+  public:
+    level(std::string const & id, Json::Value const & json,
+          std::shared_ptr<engine::game_dispatch>);
+    level(Json::Value const & json, std::shared_ptr<engine::game_dispatch>);
+    ~level();
+    void update(float delta) override;
+    void render(graphics::renderer & renderer) override;
+
+    void set_player(player * p) { player_ = p; }
+    void add_bullet(bullet b);
+
+  private:
+    template <typename T>
+    void perform_oob_culling_impl(std::vector<std::unique_ptr<T>> &);
+    void perform_oob_culling();
+    void update_waves(float delta);
+    void update_object(float delta);
+    void check_collisions();
+
+  private:
+    player * player_;
+    std::vector<std::unique_ptr<actor>> actors_;
+    std::vector<std::unique_ptr<bullet>> bullets_;
+
+    struct {
+      float const interval;
+      float remainder{0.f};
+    } wave_timer_;
+    std::deque<std::vector<std::unique_ptr<actor>>> waves_;
+  };
+}

+ 31 - 0
include/danmaku/player.hpp

@@ -0,0 +1,31 @@
+//
+//  player.hpp
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include "actor.hpp"
+
+namespace danmaku {
+  class bullet;
+  class player : public actor {
+  public:
+    player(Json::Value const & json, graphics::object const & obj,
+           attack_factory get_attack);
+
+    void hit(bullet const & b);
+    void update(float delta) override {}
+    void level(class level * l) override { level_ = l; }
+    class level * level() const override {
+      return level_;
+    }
+
+  private:
+    class level * level_;
+    std::vector<std::shared_ptr<bullet_pattern>> attack_;
+  };
+}

+ 36 - 0
include/danmaku/serial.hpp

@@ -0,0 +1,36 @@
+//
+//  serial.hpp
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/27/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <memory>
+
+#include <json/forwards.h>
+
+#include "game/engine/serial.hpp"
+
+namespace danmaku {
+  class actor;
+  class player;
+  struct points_map;
+}
+namespace graphics {
+  class manager;
+}
+
+namespace danmaku {
+  points_map to_points_map(Json::Value const & json);
+  std::unique_ptr<actor> to_actor(Json::Value const & json,
+                                  graphics::manager const & mgr);
+  std::unique_ptr<player> to_player(Json::Value const & json,
+                                    graphics::manager const & mgr);
+
+  template <typename F, typename... Args>
+  auto to_vector(Json::Value const & json, F && func, Args &&... args)
+      -> std::vector<decltype(func(json, args...))>;
+}

+ 25 - 0
include/danmaku/serial.tpp

@@ -0,0 +1,25 @@
+//
+//  serial.tpp
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/27/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <json/json.h>
+#include <vector>
+
+namespace danmaku {
+  template <typename F, typename... Args>
+  auto to_vector(Json::Value const & json, F && func, Args &&... args)
+      -> std::vector<decltype(func(json, args...))> {
+    std::vector<decltype(func(json, args...))> out;
+    out.reserve(json.size());
+    for (int i = 0; i < json.size(); ++i) {
+      out.emplace_back(func(json[i], args...));
+    }
+    return out;
+  }
+}

+ 38 - 0
include/danmaku/world.hpp

@@ -0,0 +1,38 @@
+//
+//  world.hpp
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/27/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <memory>
+#include <vector>
+
+#include "game/engine/scene.hpp"
+
+namespace danmaku {
+  class level;
+  class player;
+
+  class world : public engine::scene {
+  public:
+    world(std::shared_ptr<engine::game_dispatch> game,
+          std::unique_ptr<player> player,
+          std::vector<std::shared_ptr<level>> levels);
+    ~world();
+
+    void update(float delta) override {}
+    void render(graphics::renderer & renderer) override {}
+
+    static std::shared_ptr<world>
+    load_world(std::string const & path,
+               std::shared_ptr<engine::game_dispatch>);
+
+  private:
+    std::unique_ptr<player> player_;
+    std::vector<std::shared_ptr<level>> levels_;
+  };
+}

+ 13 - 0
src/entity/actor.cxx

@@ -0,0 +1,13 @@
+//
+//  actor.cxx
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "danmaku/actor.hpp"
+
+#include "resource_factory/prototype_factory.hpp"
+
+INSTANTIATE_PROTOTYPE_FACTORY_2(danmaku::actor_factory);

+ 26 - 0
src/entity/bullet.cxx

@@ -0,0 +1,26 @@
+//
+//  bullet.cxx
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "danmaku/bullet.hpp"
+
+#include "game/math/angle.hpp"
+#include "game/math/common.hpp"
+
+using namespace danmaku;
+
+bullet::bullet(int damage, math::vec2 const & vel, graphics::object const & obj)
+    : engine::collidable(obj), damage_(damage), velocity_(vel) {}
+
+void bullet::update(float delta) { move(velocity_ * delta); }
+
+void bullet::set_position(math::vec2 const & ll, math::radian facing) {
+  auto & size = render_info_.location.size;
+  render_info_.location.origin = ll;
+  render_info_.points =
+      math::rotate(ll + size / 2.f, render_info_.location, facing);
+}

+ 39 - 0
src/entity/enemy.cxx

@@ -0,0 +1,39 @@
+//
+//  enemy.cxx
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "danmaku/enemy.hpp"
+
+#include <json/json.h>
+
+#include "danmaku/bullet_pattern.hpp"
+#include "danmaku/serial.hpp"
+#include "resource_factory/prototype_factory.hpp"
+
+using namespace danmaku;
+
+enemy::enemy(Json::Value const & json, graphics::object const & obj,
+             attack_factory get_attack)
+    : actor(json, obj), points_(to_points_map(json["points"])) {
+  attack_ = to_vector(json["attack"], get_attack, this);
+}
+
+void enemy::update(float delta) {
+  entity::update(delta);
+  attack_[0]->update(delta);
+}
+
+std::unique_ptr<enemy> make_enemy(Json::Value const & json,
+                                  graphics::object const & obj,
+                                  attack_factory get_attack) {
+  return std::make_unique<enemy>(json, obj, get_attack);
+}
+
+BIND_ACTOR("enemy", &make_enemy);
+
+// to_vector(Json::Value, attack_factory, actor*)
+#include "danmaku/serial.tpp"

+ 24 - 0
src/entity/player.cxx

@@ -0,0 +1,24 @@
+//
+//  player.cxx
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "danmaku/player.hpp"
+
+#include <json/json.h>
+
+#include "danmaku/serial.hpp"
+
+using namespace danmaku;
+
+player::player(Json::Value const & json, graphics::object const & obj,
+               attack_factory get_attack)
+    : actor(json, obj), attack_(to_vector(json["attack"], get_attack, this)) {}
+
+void player::hit(bullet const & b) {}
+
+// to_vector(Json::Value, attack_factory, actor*)
+#include "danmaku/serial.tpp"

+ 146 - 0
src/level.cxx

@@ -0,0 +1,146 @@
+//
+//  level.cxx
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "danmaku/level.hpp"
+
+#include <json/json.h>
+
+#include "danmaku/actor.hpp"
+#include "danmaku/bullet.hpp"
+#include "danmaku/player.hpp"
+#include "danmaku/serial.hpp"
+#include "game/graphics/renderer.hpp"
+#include "game/math/common.hpp"
+#include "game/util/env.hpp"
+#include "resource_factory/prototype_factory.hpp"
+
+using namespace danmaku;
+
+struct wave_helper {
+  using wave_t = std::vector<std::unique_ptr<actor>>;
+
+  wave_t get_wave(Json::Value const & json) {
+    wave_t out;
+    for (int i = 0; i < json.size(); ++i) {
+      std::string file = mappings[json[i]["id"].asString()].asString();
+      std::string path = env::resource_file("scripts/entity/" + file);
+      Json::Value actor = engine::read_file(path);
+      actor["position"] = json[i]["position"];
+      out.emplace_back(to_actor(actor, manager));
+    }
+    return out;
+  }
+
+  std::deque<wave_t> operator()() {
+    std::deque<wave_t> out;
+    for (int i = 0; i < waves.size(); ++i) {
+      out.emplace_back(get_wave(waves[i]));
+    }
+    return out;
+  }
+
+  wave_helper(Json::Value const & json, graphics::manager const & mgr)
+      : waves(json["waves"]), mappings(json["mappings"]), manager(mgr) {}
+
+  Json::Value const & waves;
+  Json::Value const & mappings;
+  graphics::manager const & manager;
+};
+
+level::level(Json::Value const & json,
+             std::shared_ptr<engine::game_dispatch> dispatch)
+    : engine::scene(json["id"].asString(), dispatch),
+      player_(), wave_timer_{json["timeBetweenWaves"].asFloat()},
+      waves_(wave_helper{json, graphics_manager()}()) {}
+
+level::level(std::string const & id, Json::Value const & json,
+             std::shared_ptr<engine::game_dispatch> dispatch)
+    : engine::scene(id, dispatch),
+      player_(), wave_timer_{json["timeBetweenWaves"].asFloat()},
+      waves_(wave_helper{json, graphics_manager()}()) {}
+
+level::~level() {}
+
+void level::add_bullet(bullet b) {
+  bullets_.emplace_back(std::make_unique<bullet>(std::move(b)));
+  // TODO: Add to correct collision boxes...
+  collidables[0].emplace_back(bullets_.back().get());
+}
+
+template <typename T>
+void level::perform_oob_culling_impl(std::vector<std::unique_ptr<T>> & data) {
+  // TODO: Aquire screen size correctly...?
+  math::dim2::rectangle bound{{{0.f, 0.f}}, {{3840.f, 2160.f}}};
+  auto is_oob = [&bound](std::unique_ptr<T> & ptr) {
+    return !math::intersects(ptr->render_info().points, bound);
+  };
+  auto it = std::remove_if(data.begin(), data.end(), is_oob);
+  auto remove_collider = [this](std::unique_ptr<T> & ptr) {
+    // TODO: colliders, use the collision enums.
+    collidables[0].erase(
+        std::remove(collidables[0].begin(), collidables[0].end(), ptr.get()),
+        collidables[0].end());
+  };
+  std::for_each(it, data.end(), remove_collider);
+  data.erase(it, data.end());
+}
+
+void level::perform_oob_culling() {
+  //  perform_oob_culling_impl(actors_);
+  perform_oob_culling_impl(bullets_);
+}
+
+void level::update(float delta) {
+  perform_oob_culling();
+  update_waves(delta);
+  update_object(delta);
+  check_collisions();
+}
+
+void level::update_object(float delta) {
+  player_->update(delta);
+  for (auto & a : actors_) {
+    a->update(delta);
+  }
+  for (auto & b : bullets_) {
+    b->update(delta);
+  }
+}
+
+void level::check_collisions() {
+  engine::scene::check_collisions();
+  // TODO: This is a bad hack until I can properly disassociate collision info
+  for (auto & b : bullets_) {
+    if (math::intersects(player_->render_info().points,
+                         b->render_info().points)) {
+      player_->hit(*b);
+    }
+  }
+}
+
+void level::update_waves(float delta) {
+  if (waves_.empty()) { return; }
+  if ((wave_timer_.remainder -= delta) <= 0.f) {
+    wave_timer_.remainder = wave_timer_.interval;
+    for (auto & actor : waves_.front()) {
+      actor->level(this);
+      actors_.emplace_back(std::move(actor));
+    }
+    waves_.pop_front();
+  }
+}
+
+void level::render(graphics::renderer & renderer) {
+  renderer.draw(player_->render_info());
+  for (auto & a : actors_) {
+    renderer.draw(a->render_info());
+  }
+  for (auto & b : bullets_) {
+    renderer.draw(b->render_info());
+  }
+}

+ 13 - 0
src/pattern/bullet_pattern.cxx

@@ -0,0 +1,13 @@
+//
+//  bullet_pattern.cxx
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "danmaku/bullet_pattern.hpp"
+
+#include "resource_factory/prototype_factory.hpp"
+
+INSTANTIATE_PROTOTYPE_FACTORY_2(danmaku::bullet_pattern_factory);

+ 100 - 0
src/pattern/burstshot_pattern.cxx

@@ -0,0 +1,100 @@
+//
+//  burstshot_pattern.cxx
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/26/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include <json/json.h>
+#include <vector>
+
+#include "danmaku/bullet.hpp"
+#include "danmaku/bullet_pattern.hpp"
+#include "danmaku/level.hpp"
+#include "game/engine/serial.hpp"
+#include "game/math/angle.hpp"
+#include "game/math/common.hpp"
+#include "game/math/math_fwd.hpp"
+#include "resource_factory/prototype_factory.hpp"
+#include "vector/vector.hpp"
+
+using namespace danmaku;
+
+using shot = std::pair<math::vec2, float>;
+
+struct burstshot : public danmaku::bullet_pattern {
+  static std::shared_ptr<burstshot>
+  create(danmaku::actor *, Json::Value const &, graphics::manager const &);
+
+  burstshot(danmaku::actor *, float shot_interval, std::vector<shot> shots,
+            bullet bullet);
+  void update(float delta) override;
+
+  danmaku::actor * actor;
+  danmaku::bullet_timer shot_timer;
+  float last_fired{std::numeric_limits<float>::lowest()};
+  std::size_t idx{0};
+  std::vector<shot> shots;
+  bullet bullet_proto;
+};
+
+std::vector<shot> shot_vector(std::size_t count, float facing, float covered,
+                              float speed, bool clockwise) {
+  if (covered == 360.f) { ++count; }
+  if (clockwise) { covered = -covered; }
+
+  std::vector<shot> out(count);
+
+  float start = facing + (.5f * covered);
+  float end = start - covered;
+  if (clockwise != (end < start)) { std::swap(start, end); }
+
+  float const delta = (end - start) / count;
+  float current = start;
+  for (std::size_t i = 0; i < count; ++i, current += delta) {
+    out[i].first = math::rotate({{speed, 0}}, math::degree(current));
+    out[i].second = current + 90;
+  }
+  return out;
+}
+
+burstshot::burstshot(danmaku::actor * actor, float shot_interval,
+                     std::vector<shot> shots, bullet blt)
+    : actor(actor), shot_timer{shot_interval}, shots(shots), bullet_proto(blt) {
+}
+
+bullet make_bullet(Json::Value const & json,
+                   graphics::manager const & manager) {
+  return bullet(json["damage"].asInt(), {}, engine::to_object(json, manager));
+}
+
+std::shared_ptr<burstshot>
+burstshot::create(danmaku::actor * actor, Json::Value const & json,
+                  graphics::manager const & manager) {
+  return std::make_shared<burstshot>(
+      actor, json["timeBetweenBullets"].asFloat(),
+      shot_vector(json["bulletsPerWave"].asUInt(),
+                  json["centerAngle"].asFloat(), json["burstAngle"].asFloat(),
+                  json["bulletSpeed"].asFloat(), json["clockwise"].asBool()),
+      make_bullet(json["prototype"], manager));
+}
+
+void burstshot::update(float delta) {
+  if ((shot_timer.remaining -= delta) <= 0.f) {
+    shot_timer.remaining = shot_timer.interval;
+    float shots_fired = delta / shots.size();
+    while (shots_fired-- > 0) {
+      bullet new_bullet{bullet_proto};
+      new_bullet.set_velocity(shots[idx].first);
+      new_bullet.set_position(actor->render_info().location.origin,
+                              math::degree(shots[idx].second));
+      actor->level()->add_bullet(std::move(new_bullet));
+      if (++idx == shots.size()) { idx = 0; }
+    }
+  }
+}
+
+namespace danmaku {
+  BIND_BULLET_PATTERN("burstshot", &burstshot::create);
+}

+ 44 - 0
src/serial.cxx

@@ -0,0 +1,44 @@
+//
+//  serial.cxx
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/27/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "danmaku/serial.hpp"
+
+#include <json/json.h>
+
+#include "danmaku/actor.hpp"
+#include "danmaku/bullet_pattern.hpp"
+#include "danmaku/enemy.hpp"
+#include "danmaku/player.hpp"
+#include "resource_factory/prototype_factory.hpp"
+
+namespace danmaku {
+  std::unique_ptr<actor> to_actor(Json::Value const & json,
+                                  graphics::manager const & mgr) {
+    auto & bp_factory = bullet_pattern_factory::instance();
+    auto get_attack = [&](Json::Value const & j, actor * a) {
+      return bp_factory.get(j["id"].asString(), std::move(a), j, mgr);
+    };
+    return actor_factory::instance().get(json["type"].asString(), json,
+                                         engine::to_object(json, mgr),
+                                         get_attack);
+  }
+
+  std::unique_ptr<player> to_player(Json::Value const & json,
+                                    graphics::manager const & mgr) {
+    auto & bp_factory = bullet_pattern_factory::instance();
+    auto get_attack = [&](Json::Value const & j, actor * a) {
+      return bp_factory.get(j["id"].asString(), std::move(a), j, mgr);
+    };
+    return std::make_unique<player>(json, engine::to_object(json, mgr),
+                                    get_attack);
+  }
+
+  points_map to_points_map(Json::Value const & json) {
+    return {json["damage"].asInt(), json["kill"].asInt()};
+  }
+}

+ 61 - 0
src/world.cxx

@@ -0,0 +1,61 @@
+//
+//  world.cxx
+//  danmaku
+//
+//  Created by Sam Jaffe on 5/27/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "danmaku/world.hpp"
+
+#include <json/json.h>
+
+#include "danmaku/level.hpp"
+#include "danmaku/player.hpp"
+#include "danmaku/serial.hpp"
+#include "game/engine/game_dispatch.hpp"
+#include "game/util/env.hpp"
+
+using namespace danmaku;
+
+Json::Value player_json(std::string const & player_file) {
+  return engine::read_file(env::resource_file("scripts/player/" + player_file));
+}
+
+std::shared_ptr<level>
+level_from_path(Json::Value const & json, std::string const & path,
+                std::shared_ptr<engine::game_dispatch> game) {
+  Json::Value level_json =
+      engine::read_file(path + "/" + json["path"].asString() + "/level.json");
+  return std::make_shared<level>(json["name"].asString(), level_json, game);
+}
+
+world::world(std::shared_ptr<engine::game_dispatch> game,
+             std::unique_ptr<player> player,
+             std::vector<std::shared_ptr<level>> levels)
+    : engine::scene("world", game), player_(std::move(player)),
+      levels_(levels) {
+  for (auto & l : levels_) {
+    l->set_player(player_.get());
+    game->register_scene(l);
+  }
+}
+
+world::~world() {}
+
+std::shared_ptr<world>
+world::load_world(std::string const & path,
+                  std::shared_ptr<engine::game_dispatch> game) {
+  std::string const absolute = env::resource_file(path);
+  std::string const directory =
+      absolute.substr(0, absolute.find_last_of('/') + 1);
+  Json::Value const json = engine::read_file(absolute);
+  auto out = std::make_shared<world>(
+      game, to_player(player_json("player.json"), game->graphics_manager()),
+      to_vector(json["levels"], level_from_path, directory, game));
+  game->register_scene(out);
+  return out;
+}
+
+// to_vector(Json::Value const &, ...)
+#include "danmaku/serial.tpp"