Browse Source

Initial commit

jamesperet 2 years ago
commit
917fb1c94d
42 changed files with 1841 additions and 0 deletions
  1. 71 0
      .gitignore
  2. 8 0
      Editor.meta
  3. 8 0
      Editor/Icons.meta
  4. BIN
      Editor/Icons/OnStorylineGameActionTriggerIcon.png
  5. 92 0
      Editor/Icons/OnStorylineGameActionTriggerIcon.png.meta
  6. 8 0
      Prefabs.meta
  7. 299 0
      Prefabs/StoryButton.prefab
  8. 7 0
      Prefabs/StoryButton.prefab.meta
  9. 149 0
      Prefabs/StoryText.prefab
  10. 7 0
      Prefabs/StoryText.prefab.meta
  11. 194 0
      Readme.md
  12. 7 0
      Readme.md.meta
  13. 8 0
      Runtime.meta
  14. 8 0
      Runtime/GameActions.meta
  15. 8 0
      Runtime/GameActions/Triggers.meta
  16. 112 0
      Runtime/GameActions/Triggers/OnStorylineGameActionTrigger.cs
  17. 11 0
      Runtime/GameActions/Triggers/OnStorylineGameActionTrigger.cs.meta
  18. 19 0
      Runtime/KairoEngine.StorySystem.asmdef
  19. 7 0
      Runtime/KairoEngine.StorySystem.asmdef.meta
  20. 171 0
      Runtime/StoryController.cs
  21. 11 0
      Runtime/StoryController.cs.meta
  22. 76 0
      Runtime/StorySystemModule.cs
  23. 11 0
      Runtime/StorySystemModule.cs.meta
  24. 8 0
      Runtime/UI.meta
  25. 36 0
      Runtime/UI/StoryViewButton.cs
  26. 11 0
      Runtime/UI/StoryViewButton.cs.meta
  27. 20 0
      Runtime/UI/StoryViewText.cs
  28. 11 0
      Runtime/UI/StoryViewText.cs.meta
  29. 193 0
      Runtime/UI/StoryViewUI.cs
  30. 11 0
      Runtime/UI/StoryViewUI.cs.meta
  31. 8 0
      Tests.meta
  32. 8 0
      Tests/Editor.meta
  33. 155 0
      Tests/Editor/InkTests.cs
  34. 11 0
      Tests/Editor/InkTests.cs.meta
  35. 24 0
      Tests/Editor/KairoEngine.StorySystem.EditorTests.asmdef
  36. 7 0
      Tests/Editor/KairoEngine.StorySystem.EditorTests.asmdef.meta
  37. 8 0
      Tests/Runtime.meta
  38. 8 0
      Tests/test-script.ink
  39. 7 0
      Tests/test-script.ink.meta
  40. 1 0
      Tests/test-script.json
  41. 7 0
      Tests/test-script.json.meta
  42. 15 0
      package.json

+ 71 - 0
.gitignore

@@ -0,0 +1,71 @@
+# This .gitignore file should be placed at the root of your Unity project directory
+#
+# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore
+#
+/[Ll]ibrary/
+/[Tt]emp/
+/[Oo]bj/
+/[Bb]uild/
+/[Bb]uilds/
+/[Ll]ogs/
+/[Uu]ser[Ss]ettings/
+
+# MemoryCaptures can get excessive in size.
+# They also could contain extremely sensitive data
+/[Mm]emoryCaptures/
+
+# Asset meta data should only be ignored when the corresponding asset is also ignored
+!/[Aa]ssets/**/*.meta
+
+# Uncomment this line if you wish to ignore the asset store tools plugin
+# /[Aa]ssets/AssetStoreTools*
+
+# Autogenerated Jetbrains Rider plugin
+/[Aa]ssets/Plugins/Editor/JetBrains*
+
+# Visual Studio cache directory
+.vs/
+
+# Gradle cache directory
+.gradle/
+
+# Autogenerated VS/MD/Consulo solution and project files
+ExportedObj/
+.consulo/
+*.csproj
+*.unityproj
+*.sln
+*.suo
+*.tmp
+*.user
+*.userprefs
+*.pidb
+*.booproj
+*.svd
+*.pdb
+*.mdb
+*.opendb
+*.VC.db
+
+# Unity3D generated meta files
+*.pidb.meta
+*.pdb.meta
+*.mdb.meta
+
+# Unity3D generated file on crash reports
+sysinfo.txt
+
+# Builds
+*.apk
+*.aab
+*.unitypackage
+
+# Crashlytics generated file
+crashlytics-build.properties
+
+# Packed Addressables
+/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
+
+# Temporary auto-generated Android Assets
+/[Aa]ssets/[Ss]treamingAssets/aa.meta
+/[Aa]ssets/[Ss]treamingAssets/aa/*

+ 8 - 0
Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 03397bd7cdbdea240b4e9595953b41f2
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Editor/Icons.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7784bd0b0bee7c24aa06865446f932ec
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
Editor/Icons/OnStorylineGameActionTriggerIcon.png


+ 92 - 0
Editor/Icons/OnStorylineGameActionTriggerIcon.png.meta

@@ -0,0 +1,92 @@
+fileFormatVersion: 2
+guid: b501ccd93713a8e49aff65eaac666e7b
+TextureImporter:
+  internalIDToNameTable: []
+  externalObjects: {}
+  serializedVersion: 11
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 1
+    sRGBTexture: 1
+    linearTexture: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapsPreserveCoverage: 0
+    alphaTestReferenceValue: 0.5
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  streamingMipmaps: 0
+  streamingMipmapsPriority: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: 1
+  maxTextureSize: 2048
+  textureSettings:
+    serializedVersion: 2
+    filterMode: -1
+    aniso: -1
+    mipBias: -100
+    wrapU: -1
+    wrapV: -1
+    wrapW: -1
+  nPOTScale: 1
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spritePixelsToUnits: 100
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spriteGenerateFallbackPhysicsShape: 1
+  alphaUsage: 1
+  alphaIsTransparency: 0
+  spriteTessellationDetail: -1
+  textureType: 0
+  textureShape: 1
+  singleChannelComponent: 0
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  applyGammaDecoding: 0
+  platformSettings:
+  - serializedVersion: 3
+    buildTarget: DefaultTexturePlatform
+    maxTextureSize: 2048
+    resizeAlgorithm: 0
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+    androidETC2FallbackOverride: 0
+    forceMaximumCompressionQuality_BC6H_BC7: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+    physicsShape: []
+    bones: []
+    spriteID: 
+    internalID: 0
+    vertices: []
+    indices: 
+    edges: []
+    weights: []
+    secondaryTextures: []
+  spritePackingTag: 
+  pSDRemoveMatte: 0
+  pSDShowRemoveMatteOption: 0
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Prefabs.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5acb9a3587b35d742be6b4a60b261113
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 299 - 0
Prefabs/StoryButton.prefab

@@ -0,0 +1,299 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &600446434398737882
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 600446434398737883}
+  - component: {fileID: 600446434398737879}
+  - component: {fileID: 5263916527200715245}
+  - component: {fileID: 600446434398737878}
+  - component: {fileID: 600446434398737877}
+  - component: {fileID: 5804493103053590526}
+  m_Layer: 0
+  m_Name: StoryButton
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &600446434398737883
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 600446434398737882}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_Children:
+  - {fileID: 600446435196768707}
+  m_Father: {fileID: 0}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0, y: 0}
+  m_AnchorMax: {x: 0, y: 0}
+  m_AnchoredPosition: {x: 0, y: 0}
+  m_SizeDelta: {x: 200, y: 35}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &600446434398737879
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 600446434398737882}
+  m_CullTransparentMesh: 0
+--- !u!114 &5263916527200715245
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 600446434398737882}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: a20e0c5ee2a85564cb395afbe210f4e9, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  text: {fileID: 600446435196768764}
+--- !u!114 &600446434398737878
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 600446434398737882}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 1
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_Sprite: {fileID: 21300000, guid: f3ebab8dd0edebb4ea2c41ee07689d21, type: 3}
+  m_Type: 1
+  m_PreserveAspect: 0
+  m_FillCenter: 1
+  m_FillMethod: 4
+  m_FillAmount: 1
+  m_FillClockwise: 1
+  m_FillOrigin: 0
+  m_UseSpriteMesh: 0
+  m_PixelsPerUnitMultiplier: 2
+--- !u!114 &600446434398737877
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 600446434398737882}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Navigation:
+    m_Mode: 0
+    m_WrapAround: 0
+    m_SelectOnUp: {fileID: 0}
+    m_SelectOnDown: {fileID: 0}
+    m_SelectOnLeft: {fileID: 0}
+    m_SelectOnRight: {fileID: 0}
+  m_Transition: 2
+  m_Colors:
+    m_NormalColor: {r: 0.7924528, g: 0.7924528, b: 0.7924528, a: 1}
+    m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+    m_PressedColor: {r: 0.4056604, g: 0.4056604, b: 0.4056604, a: 1}
+    m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
+    m_DisabledColor: {r: 0.5660378, g: 0.5660378, b: 0.5660378, a: 0.5019608}
+    m_ColorMultiplier: 1
+    m_FadeDuration: 0.1
+  m_SpriteState:
+    m_HighlightedSprite: {fileID: 21300000, guid: 1bf482148462845458b328ab23f37079,
+      type: 3}
+    m_PressedSprite: {fileID: 21300000, guid: c122ba97fa7af8e46a2f5b403df86462, type: 3}
+    m_SelectedSprite: {fileID: 21300000, guid: 1bf482148462845458b328ab23f37079, type: 3}
+    m_DisabledSprite: {fileID: 21300000, guid: fc5b8ec7e8b7f7546be9be934d76d5f9, type: 3}
+  m_AnimationTriggers:
+    m_NormalTrigger: Normal
+    m_HighlightedTrigger: Highlighted
+    m_PressedTrigger: Pressed
+    m_SelectedTrigger: Selected
+    m_DisabledTrigger: Disabled
+  m_Interactable: 1
+  m_TargetGraphic: {fileID: 600446434398737878}
+  m_OnClick:
+    m_PersistentCalls:
+      m_Calls:
+      - m_Target: {fileID: 5263916527200715245}
+        m_TargetAssemblyTypeName: 
+        m_MethodName: OnClick
+        m_Mode: 1
+        m_Arguments:
+          m_ObjectArgument: {fileID: 0}
+          m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
+          m_IntArgument: 0
+          m_FloatArgument: 0
+          m_StringArgument: Start Game
+          m_BoolArgument: 0
+        m_CallState: 2
+--- !u!114 &5804493103053590526
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 600446434398737882}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_HorizontalFit: 0
+  m_VerticalFit: 0
+--- !u!1 &600446435196768706
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 600446435196768707}
+  - component: {fileID: 600446435196768765}
+  - component: {fileID: 600446435196768764}
+  m_Layer: 0
+  m_Name: Text (TMP)
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &600446435196768707
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 600446435196768706}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1.2, z: 1}
+  m_Children: []
+  m_Father: {fileID: 600446434398737883}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0, y: 0}
+  m_AnchorMax: {x: 1, y: 1}
+  m_AnchoredPosition: {x: 0, y: -0.30000305}
+  m_SizeDelta: {x: 0, y: 4.62}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &600446435196768765
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 600446435196768706}
+  m_CullTransparentMesh: 0
+--- !u!114 &600446435196768764
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 600446435196768706}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 1
+  m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_text: 'Button '
+  m_isRightToLeft: 0
+  m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
+  m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
+  m_fontSharedMaterials: []
+  m_fontMaterial: {fileID: 0}
+  m_fontMaterials: []
+  m_fontColor32:
+    serializedVersion: 2
+    rgba: 4292730333
+  m_fontColor: {r: 0.8679245, g: 0.8679245, b: 0.8679245, a: 1}
+  m_enableVertexGradient: 0
+  m_colorMode: 3
+  m_fontColorGradient:
+    topLeft: {r: 1, g: 1, b: 1, a: 1}
+    topRight: {r: 1, g: 1, b: 1, a: 1}
+    bottomLeft: {r: 1, g: 1, b: 1, a: 1}
+    bottomRight: {r: 1, g: 1, b: 1, a: 1}
+  m_fontColorGradientPreset: {fileID: 0}
+  m_spriteAsset: {fileID: 0}
+  m_tintAllSprites: 0
+  m_StyleSheet: {fileID: 0}
+  m_TextStyleHashCode: -1183493901
+  m_overrideHtmlColors: 0
+  m_faceColor:
+    serializedVersion: 2
+    rgba: 4294967295
+  m_fontSize: 18
+  m_fontSizeBase: 18
+  m_fontWeight: 400
+  m_enableAutoSizing: 0
+  m_fontSizeMin: 18
+  m_fontSizeMax: 72
+  m_fontStyle: 1
+  m_HorizontalAlignment: 2
+  m_VerticalAlignment: 512
+  m_textAlignment: 65535
+  m_characterSpacing: 0
+  m_wordSpacing: 0
+  m_lineSpacing: 0
+  m_lineSpacingMax: 0
+  m_paragraphSpacing: 0
+  m_charWidthMaxAdj: 0
+  m_enableWordWrapping: 1
+  m_wordWrappingRatios: 0.4
+  m_overflowMode: 0
+  m_linkedTextComponent: {fileID: 0}
+  parentLinkedComponent: {fileID: 0}
+  m_enableKerning: 1
+  m_enableExtraPadding: 0
+  checkPaddingRequired: 0
+  m_isRichText: 1
+  m_parseCtrlCharacters: 1
+  m_isOrthographic: 1
+  m_isCullingEnabled: 0
+  m_horizontalMapping: 0
+  m_verticalMapping: 0
+  m_uvLineOffset: 0
+  m_geometrySortingOrder: 0
+  m_IsTextObjectScaleStatic: 0
+  m_VertexBufferAutoSizeReduction: 1
+  m_useMaxVisibleDescender: 1
+  m_pageToDisplay: 1
+  m_margin: {x: 8, y: 8, z: 8, w: 8}
+  m_isUsingLegacyAnimationComponent: 0
+  m_isVolumetricText: 0
+  m_hasFontAssetChanged: 0
+  m_baseMaterial: {fileID: 0}
+  m_maskOffset: {x: 0, y: 0, z: 0, w: 0}

+ 7 - 0
Prefabs/StoryButton.prefab.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 58547ff9574e44145851a37d5cbb0041
+PrefabImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 149 - 0
Prefabs/StoryText.prefab

@@ -0,0 +1,149 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &8488480106928356192
+GameObject:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  serializedVersion: 6
+  m_Component:
+  - component: {fileID: 896551629948592682}
+  - component: {fileID: 985905598230117637}
+  - component: {fileID: 135200324193600158}
+  - component: {fileID: 8744469494832626071}
+  m_Layer: 0
+  m_Name: StoryText
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &896551629948592682
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8488480106928356192}
+  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_Children: []
+  m_Father: {fileID: 0}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0.5, y: 0.5}
+  m_AnchorMax: {x: 0.5, y: 0.5}
+  m_AnchoredPosition: {x: 0, y: 0}
+  m_SizeDelta: {x: 635.0227, y: 134.43971}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!222 &985905598230117637
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8488480106928356192}
+  m_CullTransparentMesh: 0
+--- !u!114 &135200324193600158
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8488480106928356192}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: b38fddfb871a31544ba52530cc989354, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  text: {fileID: 8744469494832626071}
+--- !u!114 &8744469494832626071
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_CorrespondingSourceObject: {fileID: 0}
+  m_PrefabInstance: {fileID: 0}
+  m_PrefabAsset: {fileID: 0}
+  m_GameObject: {fileID: 8488480106928356192}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 1
+  m_Maskable: 1
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+  m_text: New Text
+  m_isRightToLeft: 0
+  m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
+  m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
+  m_fontSharedMaterials: []
+  m_fontMaterial: {fileID: 0}
+  m_fontMaterials: []
+  m_fontColor32:
+    serializedVersion: 2
+    rgba: 4294967295
+  m_fontColor: {r: 1, g: 1, b: 1, a: 1}
+  m_enableVertexGradient: 0
+  m_colorMode: 3
+  m_fontColorGradient:
+    topLeft: {r: 1, g: 1, b: 1, a: 1}
+    topRight: {r: 1, g: 1, b: 1, a: 1}
+    bottomLeft: {r: 1, g: 1, b: 1, a: 1}
+    bottomRight: {r: 1, g: 1, b: 1, a: 1}
+  m_fontColorGradientPreset: {fileID: 0}
+  m_spriteAsset: {fileID: 0}
+  m_tintAllSprites: 0
+  m_StyleSheet: {fileID: 0}
+  m_TextStyleHashCode: -1183493901
+  m_overrideHtmlColors: 0
+  m_faceColor:
+    serializedVersion: 2
+    rgba: 4294967295
+  m_fontSize: 36
+  m_fontSizeBase: 36
+  m_fontWeight: 400
+  m_enableAutoSizing: 0
+  m_fontSizeMin: 18
+  m_fontSizeMax: 72
+  m_fontStyle: 0
+  m_HorizontalAlignment: 1
+  m_VerticalAlignment: 256
+  m_textAlignment: 65535
+  m_characterSpacing: 0
+  m_wordSpacing: 0
+  m_lineSpacing: 0
+  m_lineSpacingMax: 0
+  m_paragraphSpacing: 0
+  m_charWidthMaxAdj: 0
+  m_enableWordWrapping: 1
+  m_wordWrappingRatios: 0.4
+  m_overflowMode: 0
+  m_linkedTextComponent: {fileID: 0}
+  parentLinkedComponent: {fileID: 0}
+  m_enableKerning: 1
+  m_enableExtraPadding: 0
+  checkPaddingRequired: 0
+  m_isRichText: 1
+  m_parseCtrlCharacters: 1
+  m_isOrthographic: 1
+  m_isCullingEnabled: 0
+  m_horizontalMapping: 0
+  m_verticalMapping: 0
+  m_uvLineOffset: 0
+  m_geometrySortingOrder: 0
+  m_IsTextObjectScaleStatic: 0
+  m_VertexBufferAutoSizeReduction: 1
+  m_useMaxVisibleDescender: 1
+  m_pageToDisplay: 1
+  m_margin: {x: 0, y: 0, z: 0, w: 0}
+  m_isUsingLegacyAnimationComponent: 0
+  m_isVolumetricText: 0
+  m_hasFontAssetChanged: 0
+  m_baseMaterial: {fileID: 0}
+  m_maskOffset: {x: 0, y: 0, z: 0, w: 0}

+ 7 - 0
Prefabs/StoryText.prefab.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 55f98f8ec372b6445bf2d55d3db287e3
+PrefabImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 194 - 0
Readme.md

@@ -0,0 +1,194 @@
+# 📦 KairoEngine.StorySystem v0.1.2
+
+The Story System uses the Ink Language and runtime to navigate through a story written in a plain text file. This package contains the Story Module that receives an ink story and runs it. The story can show lines and branches, execute functions in unity and wait for events. Unity also has an API for navigating, getting and setting variables in the story.
+
+### 🛑Required packages
+
+- `KairoEngine.Core`
+- `Ink`
+- `UniRX`
+- `TextMeshPro`
+
+### 📄Namespaces
+
+- `KairoEngine.StorySystem`
+- `KairoEngine.StorySystem.UI`
+- ``KairoEngine.StorySystem.EditorTests``
+
+### 📙Modules
+
+- **Story Module** – Loads a story configuration, instantiates a story controller and starts the story playback.
+
+### 🔷Components
+
+- `StoryViewUI` – A generic UI for showing text and buttons based on story events.
+- `StoryButton` – A button used in the Story View UI
+- ``StoryText`` – A text object used in the Story View UI
+
+### ✔Getting Started
+
+To get started using the Story module, first add it to a game config and add an installer prefab to a scene. Then install [Inky](), the Ink editor. Create a new Ink story, save the file and add it to the Unity project. Unity will generate a JSON file from the Ink file then add it the story module configuration.
+
+A simple example Ink story:
+
+```
+Once upon a time...
+
+ * There was a choice.
+ * There was another choice.
+
+- And they lived happily ever after.
+    -> END
+```
+
+When this story is run on Unity after been loaded with the story module, it will read the first line and send it as an event to the game. Then it will read all choices and send them as a single event to the game. The story system will wait for another event containing the chosen path. The system will then send an event containing the last line of text in the story and then will stop.
+
+The story system publishes events containing ``StoryStepData`` that other scripts can subscribe to. This object contains data about that passage of the story. It can be a line of text, a list of choices or a integer for the path that has been selected.
+
+When the Ink story runs, lines and branches will be sent as events imidiatly in order. Wait functions can delay that process. Once a branch has been reached, the story will wait for an event to be sent back containing the selected path.
+
+Below, in the Unity Functions section there are instructions on how to send and receive story events with code examples.
+
+### 🧰Ink Functions
+
+Use these functions in an Ink document to execute things in Unity. Each function has a usage pattern and the actual function that needs to go somewhere inside the Ink document.
+
+##### Wait
+
+Make the story wait for a certain amount of time.  In the Ink story file, use ``~Wait(time)`` anywhere in the story to make unity wait the amout of time before continuing the story.
+
+```javascript
+// Example Usage
+Something happens
+~ Wait(3000)
+After three seconds another thing happens
+```
+
+Add the functions below for declaring a external function to Ink and a fallback function to make it work in the Inky editor
+
+```javascript
+EXTERNAL WaitForTime(time)
+=== function Wait(time) ===
+~WaitForTime(time)
+<> <i>(Wait {time} ms)</i>
+=== function WaitForTime(time) ===
+~x = ""
+```
+
+##### Enable / Disable GameObject
+
+Enable and disable GameObjects. In Ink, use ``~Enable("StoryGameObjectName")`` or ``~ Disable("StoryGameObjectName")``. Add prefabs for the GameObjects used in the story to the list on the Story Module config.
+
+```javascript
+// Show a menu in the game and close it after the choice is made
+~ Enable("MainMenuUI")
+Main Menu
+    + [Start Game]
+        ~ Disable("MainMenuUI")
+        -> StartGame
+    + [End Game]
+        ~ Disable("MainMenuUI")
+        -> END
+```
+
+Add the function below anywhere on the ink document:
+
+```javascript
+EXTERNAL Enable(name)
+EXTERNAL Disable(name)
+=== function Enable(name) ===
+> Enable <i>{name}</i>
+=== function Disable(name) ===
+> Disable <i>{name}</i>
+```
+
+##### Wait for Event
+
+##### Trigger Events
+
+Triggers a ``GenericEvent`` in unity. Pass in a string containing the name of the event to be triggered. Example:
+
+```javascript
+~ TriggerEvent("MyCustomEvent")
+EXTERNAL TriggerEvent(name)
+=== function TriggerEvent(name) ===
+> Trigger event "<i>{name}</i>"
+```
+
+##### Change Scene
+
+##### Control Slideshow
+
+##### Set Camera
+
+##### PlaySFX
+
+##### Play Soudtrack
+
+### 🧰Unity Functions
+
+These are functions used in Unity to change and navigate a Ink story. Also there are helper function for doing various things like observers and events.
+
+##### Go to Story Knot
+
+##### Get Variable
+
+##### Set Variable
+
+##### Receive Story Events
+
+To receive story events, simply subscribe to events with the story name and a function.
+
+```csharp
+EventManager.broadcast.StartListening(storyName, OnStoryStep);
+```
+
+The function for these events needs to receive story step data, for example:
+
+```csharp
+void OnStoryStepData(StoryStepData storyStep)
+{
+    Debug.Log($"> {storyStep.text}");
+}
+```
+
+Don't forget to stop listening for events when the script is done. For example, using a monobehaviour derived class:
+
+```csharp
+void OnDisable()
+{
+    EventManager.broadcast.StopListening(storyName, OnStoryStep);
+}
+```
+
+##### Send Story Events
+
+When the story has reached a branch, it will wait ultil it receives an event containing the selected path.
+
+```csharp
+int path = 0;
+StoryStepData storyStep = new StoryStepData(StoryStepType.Path, text, null, null, path)
+EventManager.broadcast.Trigger(storyName, storyStep )
+```
+
+##### Story Observer
+
+Instead of declaring delegates using events, it's possible to subscribe to story steps using observers:
+
+```csharp
+CompositeDisposable composite = new CompositeDisposable();
+// Subscribe to the custom observer
+// Other UniRX functions can be chained here
+StoryController.Lines().Subscribe(storyStep => {
+    Debug.Log(storyStep.text);
+    // Stop the observer
+    composite.Dispose()
+}).AddTo(composite);
+```
+
+The Story Controller Lines observer requires the ``kairoEngine.StorySystem`` namespace.
+
+### 🎈Back Log
+
+- [ ] Support for multiple stories
+- [ ] More tests

+ 7 - 0
Readme.md.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c81a693285e322c438e92bb48f5b6cd8
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: b1d107a34545ab149850f51bbb1e29e1
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime/GameActions.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 052b9dcc2c8c9cd4692dfcd896ce3b56
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime/GameActions/Triggers.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d19192ca2ce508f4e8b34c898ed7cec7
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 112 - 0
Runtime/GameActions/Triggers/OnStorylineGameActionTrigger.cs

@@ -0,0 +1,112 @@
+using System.Collections;
+using System.Linq;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using KairoEngine.Core;
+using KairoEngine.Core.GameActions;
+using KairoEngine.StorySystem;
+
+namespace KairoEngine.StorySystem.GameActions
+{
+
+    [System.Serializable, HideReferenceObjectPicker]
+    public class OnStorylineGameActionTrigger : GameActionTriggerBase
+    {
+        public enum StorylineCondition
+        {
+            Contains,
+            Equal,
+            NotEqual
+        }
+
+        public override string name 
+        { 
+            get
+            {
+                return $"On Storyline";
+            }
+        }
+
+        public override GameActionTriggersController controller { 
+            get => _controller; 
+            set 
+            {
+                _controller = value;
+                typeName = "OnStorylineGameActionTrigger";
+            }
+        }
+        public override string GetTypeName() => "OnStorylineGameActionTrigger";
+        public override string GetTriggerName() => "On Storyline";
+
+        [IconFoldoutGroup("@name","Assets/Plugins/KairoEngine/StorySystem/Editor/Icons/OnStorylineGameActionTriggerIcon.png")]
+        public string storyName = "Storyline";
+        [IconFoldoutGroup("@name")]  public StorylineCondition condition = StorylineCondition.Contains;
+
+        [IconFoldoutGroup("@name")]  public StoryStepType storyStepType = StoryStepType.Line;
+        [IconFoldoutGroup("@name")]  public string value = "";
+
+        public override void OnEnable() 
+        { 
+            EventManager.broadcast.StartListening(storyName, OnStoryStep);
+        }
+
+        public override void Update() 
+        { 
+            
+        }
+
+        public override void OnDisable() 
+        { 
+            EventManager.broadcast.StopListening(storyName, OnStoryStep);
+        }
+
+        private void OnStoryStep(StoryStepData storyStep)
+        {
+            if(storyStep.category != storyStepType) return;
+            switch (storyStep.category)
+            {
+                case StoryStepType.Line:
+                    if(condition == StorylineCondition.Equal && value == storyStep.text) TriggerActions();
+                    if(condition == StorylineCondition.Contains && storyStep.text.Contains(value)) TriggerActions();
+                    if(condition == StorylineCondition.NotEqual && value != storyStep.text) controller.TriggerActions();
+                    break;
+                case StoryStepType.Branch:
+                    foreach (var branch in storyStep.branches)
+                    {
+                        if(condition == StorylineCondition.Equal && value == branch) TriggerActions();
+                        if(condition == StorylineCondition.Contains && branch.Contains(value)) TriggerActions();
+                        if(condition == StorylineCondition.NotEqual && value != branch) controller.TriggerActions();
+                    } 
+                    break;
+                case StoryStepType.Path:
+                    if(condition == StorylineCondition.Equal && value == storyStep.text) TriggerActions();
+                    if(condition == StorylineCondition.Contains && storyStep.text.Contains(value)) TriggerActions();
+                    if(condition == StorylineCondition.NotEqual && value != storyStep.text) TriggerActions();
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        public void TriggerActions()
+        {
+            EventManager.broadcast.StopListening(storyName, OnStoryStep);
+            controller.TriggerActions();
+        }
+
+        public static OnStorylineGameActionTrigger JSONToOnStorylineGameActionTrigger(string data)
+        {
+            return JsonUtility.FromJson<OnStorylineGameActionTrigger>(data);
+        }
+
+        private OnStorylineGameActionTrigger Duplicate()
+        {
+            OnStorylineGameActionTrigger trigger = new OnStorylineGameActionTrigger();
+            trigger.controller = controller;
+            return trigger;
+        }
+
+    }
+}
+

+ 11 - 0
Runtime/GameActions/Triggers/OnStorylineGameActionTrigger.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 76ac5cf6f145c454db74a6e9f8c4adad
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 19 - 0
Runtime/KairoEngine.StorySystem.asmdef

@@ -0,0 +1,19 @@
+{
+    "name": "KairoEngine.StorySystem",
+    "references": [
+        "GUID:7e5ae6a38d1532248b4c890eca668b06",
+        "GUID:142285d3db5e7e849b02ea3a75bc2de7",
+        "GUID:58bed0e7c5306824586d7eda03609289",
+        "GUID:560b04d1a97f54a4e82edc0cbbb69285",
+        "GUID:6055be8ebefd69e48b49212b09b47b2f"
+    ],
+    "includePlatforms": [],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
Runtime/KairoEngine.StorySystem.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: dc1ff8ea02e2e9449ab31f83ffba726e
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 171 - 0
Runtime/StoryController.cs

@@ -0,0 +1,171 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using KairoEngine.Core;
+using Ink.Runtime;
+using UniRx;
+
+namespace KairoEngine.StorySystem
+{
+    // Todo: Request events for getting and setting story variables
+
+    public class StoryController
+    {
+	    private TextAsset inkJSONAsset = null;
+	    private Story story;
+        private string name;
+        private bool logText = false;
+        private bool started = false;
+        private int lineInverval = 250;
+
+        public bool wait = false;
+        
+        private CompositeDisposable disposables = new CompositeDisposable();
+        private List<StoryObject> storyObjects;
+
+        public StoryController(TextAsset inkJSONAsset, string storyName, bool logText = false)
+        {
+            this.inkJSONAsset = inkJSONAsset;
+            this.name = storyName;
+            this.story = new Story (inkJSONAsset.text);
+            this.logText = logText;
+        }
+
+        public StoryController(TextAsset inkJSONAsset, string storyName, List<StoryObject> storyObjects, Transform parent = null, bool logText = false, int lineInverval = 250)
+        {
+            this.inkJSONAsset = inkJSONAsset;
+            this.name = storyName;
+            this.story = new Story (inkJSONAsset.text);
+            this.logText = logText;
+            this.lineInverval = lineInverval;
+            this.storyObjects = storyObjects;
+            foreach (var item in storyObjects) 
+            {
+                item.instance = GameObject.Instantiate(item.prefab, parent);
+                item.instance.SetActive(false);
+                item.instance.name = item.instance.name.Replace("(Clone)", "");
+            }
+        }
+
+        public void Start()
+        {
+            if(started) return;
+            started = true;
+            GenericEvents.StartListening(name, JumpToPath);
+            EventManager.broadcast.StartListening(name, SelectStoryBranch);
+            EventManager.broadcast.StartListening(name, PublishStorylineToObservers);
+            if(logText) Debug.Log($"Starting {name}");
+            story.BindExternalFunction ("WaitForTime", (int time) => Wait(time), false);
+            story.BindExternalFunction ("Enable", (string name) => EnableGameObject(name), false);
+            story.BindExternalFunction ("Disable", (string name) => DisableGameObject(name), false); 
+            story.BindExternalFunction ("TriggerEvent", (string name) => GenericEvents.Trigger(name), false);
+            var storyStream = Observable.Interval(TimeSpan.FromMilliseconds(250))
+                //.Where(_ => wait == false)
+                .Subscribe(_ => StoryLoop())
+                .AddTo(disposables);
+        } 
+
+        public void Stop()
+        {
+            GenericEvents.StopListening(name, JumpToPath);
+            EventManager.broadcast.StopListening(name, SelectStoryBranch);
+            EventManager.broadcast.StopListening(name, PublishStorylineToObservers);
+            disposables.Clear();
+            started = false;
+            if(logText) Debug.Log($"{name} has stoped.");
+        }
+
+        private void StoryLoop()
+        {
+            //Debug.Log(wait);
+            if(wait) return;
+            // Read all the content until we can't continue any more
+            if (story.canContinue) {
+                // Continue gets the next line of the story
+                string text = story.Continue ();
+                // This removes any white space from the text.
+                text = text.Trim();
+                // Create StoryStepData
+                StoryStepData storyStep = new StoryStepData(StoryStepType.Line, text, story.currentTags, null);
+                EventManager.broadcast.Trigger(name, storyStep);
+                // Display the text if log is enabled
+                if(logText) Debug.Log(storyStep.text);
+            }
+            else if( story.currentChoices.Count > 0 ) 
+            {
+                List<string> choices = new List<string>();
+                for (int i = 0; i < story.currentChoices.Count; ++i) 
+                {
+                    Choice choice = story.currentChoices [i];
+                    choices.Add(choice.text);
+                    if(logText) Debug.Log($"Choice {i + 1}. {choice.text}");
+                }
+                StoryStepData storyStep = new StoryStepData(StoryStepType.Branch, "", story.currentTags, choices);
+                EventManager.broadcast.Trigger(name, storyStep);
+                wait = true;
+            }
+            else Stop(); 
+        }
+
+        private void SelectStoryBranch(StoryStepData storyStep)
+        {
+            if(storyStep.category == StoryStepType.Path)
+            {
+                story.ChooseChoiceIndex (storyStep.path);
+                wait = false;
+                StoryLoop();
+            }
+        }
+
+        static Subject<StoryStepData> subject;
+
+        public static IObservable<StoryStepData> Lines()
+        {
+            if(subject == null) subject = new Subject<StoryStepData>();
+            return subject.AsObservable();
+        }
+
+        private void PublishStorylineToObservers(StoryStepData storyStep)
+        {
+            if(subject != null) subject.OnNext(storyStep);
+        }
+
+        public void JumpToPath(string path)
+        {
+            if(story != null) 
+            {
+                if(logText) Debug.Log($"Jumping to story knot \"{path}\"");
+                story.ChoosePathString(path);
+                wait = false;
+            }
+        }
+
+        #region ExternalStoryFunctions
+
+        private void Wait(int time)
+        {
+            wait = true;
+            Timer.Execute(time, () => wait = false);
+        }
+
+        private void EnableGameObject(string name)
+        {
+            for (int i = 0; i < storyObjects.Count; i++)
+            {
+                if(storyObjects[i].name == name) storyObjects[i].instance.SetActive(true);
+            }
+        }
+
+        private void DisableGameObject(string name)
+        {
+            for (int i = 0; i < storyObjects.Count; i++)
+            {
+                if(storyObjects[i].name == name) storyObjects[i].instance.SetActive(false);
+            }
+        }
+
+        #endregion
+    }
+}
+

+ 11 - 0
Runtime/StoryController.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 68d00ef6d9bf24f4da1728d307562116
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 76 - 0
Runtime/StorySystemModule.cs

@@ -0,0 +1,76 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using KairoEngine.Core;
+using KairoEngine.Core.ModuleSystem;
+using Ink.Runtime;
+
+namespace KairoEngine.StorySystem
+{
+    public enum StoryInitType
+    {
+        OnLoad,
+        OnEvent,
+        None
+    }
+
+    [System.Serializable]
+    public class StoryObject
+    {
+        public string name;
+        public GameObject prefab;
+        [HideInInspector] public GameObject instance;
+    }
+
+    public class StorySystemModule : IGameModule
+    {
+        private string _name = "Story Module";
+
+        public string name { get =>  _name; set => _name = value; }
+
+        public TextAsset inkJSONAsset = null;
+        public string storyName = "Storyline";
+
+        public bool showStoryLog = false;
+
+        public int lineInverval = 250;
+
+        [LabelText("Start")] public StoryInitType startType;
+
+        [ShowIf("@startType == StoryInitType.OnEvent")] public string eventStartName = "StartStory";
+
+        [Space] public List<StoryObject> storyObjects = new List<StoryObject>();
+
+        private StoryController storyController;
+        private Transform storyObjectsContainer = null;
+
+        public void Load(Transform parent)
+        {
+            if(storyObjects.Count > 0) storyObjectsContainer = new GameObject("StoryObjects").transform;
+            if(storyObjectsContainer != null) storyObjectsContainer.transform.parent = parent;
+            if(startType == StoryInitType.OnLoad) LoadStory(storyObjectsContainer);
+            else if(startType == StoryInitType.OnEvent) GenericEvents.StartListening(eventStartName, () => LoadStory(storyObjectsContainer));
+        }
+
+        private void LoadStory(Transform parent)
+        {
+            if(inkJSONAsset != null) storyController = new StoryController(inkJSONAsset, storyName, storyObjects, parent, showStoryLog, lineInverval);
+            else Debug.LogError($"Missing Ink Story asset in StorySystemModule");
+            if(storyController != null) storyController.Start();
+        }
+
+        public void Reset()
+        {
+            showStoryLog = false;
+            inkJSONAsset = null;
+            storyController = null;
+        }
+
+        public void Destroy()
+        {
+            if(storyController != null) storyController.Stop();
+            if(startType == StoryInitType.OnEvent) GenericEvents.StopListening(eventStartName, () => LoadStory(storyObjectsContainer));
+        }
+    }
+}

+ 11 - 0
Runtime/StorySystemModule.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8a7e3ff148b0481439734b09279b092b
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime/UI.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 3c773439d354a8a43aca555311f10521
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 36 - 0
Runtime/UI/StoryViewButton.cs

@@ -0,0 +1,36 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using TMPro;
+using KairoEngine.Core;
+using KairoEngine.StorySystem;
+
+namespace KairoEngine.StorySystem.UI
+{
+    public class StoryViewButton : MonoBehaviour
+    {
+        public TextMeshProUGUI text;
+
+        private string storyName;
+        private StoryStepData storyStep;
+        private int value;
+        private StoryViewUI storyView;
+
+        public void Init(string storyName, StoryStepData storyStep, int n, StoryViewUI storyView)
+        {
+            this.storyName = storyName;
+            this.storyStep = storyStep;
+            this.value = n;
+            this.storyView = storyView;
+            text.text = this.storyStep.branches[this.value];
+        }
+
+        public void OnClick()
+        {
+            //Debug.Log("Button Clicked");
+            StoryStepData newStoryStep = new StoryStepData(StoryStepType.Path, storyStep.branches[value], null, null, value);
+            storyView.ButtonClicked(newStoryStep);
+            EventManager.broadcast.Trigger(storyName, newStoryStep);
+        }
+    }
+}

+ 11 - 0
Runtime/UI/StoryViewButton.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a20e0c5ee2a85564cb395afbe210f4e9
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 20 - 0
Runtime/UI/StoryViewText.cs

@@ -0,0 +1,20 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using TMPro;
+using KairoEngine.Core;
+using KairoEngine.StorySystem;
+
+namespace KairoEngine.StorySystem.UI
+{
+    public class StoryViewText : MonoBehaviour
+    {
+        public TextMeshProUGUI text;
+
+        public void Setup(StoryStepData storyStep)
+        {
+            text.text = storyStep.text;
+        }
+    }
+}
+

+ 11 - 0
Runtime/UI/StoryViewText.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b38fddfb871a31544ba52530cc989354
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 193 - 0
Runtime/UI/StoryViewUI.cs

@@ -0,0 +1,193 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.UI;
+using KairoEngine.Core;
+using Sirenix.OdinInspector;
+using KairoEngine.UI;
+using KairoEngine.StorySystem;
+
+namespace KairoEngine.StorySystem.UI
+{
+    public class StoryViewUI : SerializedMonoBehaviour, IUiSystemElement
+    {
+        public string storyName = "Storyline";
+
+        public Transform panel;
+        
+        [BoxGroup("Story Lines")]public bool showLines = false;
+        [BoxGroup("Story Lines"), ShowIf("@showLines")] public bool showMultipleLines = true;
+        [BoxGroup("Story Lines"), ShowIf("@showLines && showMultipleLines")] public bool destroyLinesAfterPath = true;
+        [BoxGroup("Story Lines"), ShowIf("@showLines && showMultipleLines")] public GameObject storyLinePrefab;
+        [BoxGroup("Story Lines"), ShowIf("@showLines && showMultipleLines")] public Transform lineContainer;
+        [BoxGroup("Story Lines"), ShowIf("@showLines && !showMultipleLines")] public StoryViewText lineText;
+        [BoxGroup("Story Lines"), ShowIf("@showLines && showMultipleLines")] public ScrollRect scrollRect = null;
+
+        [BoxGroup("Story Branches")] public bool showBranches = true;
+        [BoxGroup("Story Branches"), ShowIf("@showBranches")] public bool hidePanelAfterChoice = true;
+        [BoxGroup("Story Branches"), ShowIf("@showBranches")] public GameObject storyBranchPrefab;
+        [BoxGroup("Story Branches"), ShowIf("@showBranches")] public Transform branchContainer;
+
+        [BoxGroup("Portraits")] public bool usePortraits = false;
+        [BoxGroup("Portraits"), ShowIf("@usePortraits")] public Image portraitImage;
+        [BoxGroup("Portraits"), ShowIf("@usePortraits")] public GameObject portraitContainer;
+
+        [BoxGroup("Portraits"), ShowInInspector, PropertySpace(4,2), ShowIf("@usePortraits")] 
+        public Dictionary<string, Sprite> portraits = new Dictionary<string, Sprite>();
+
+        private List<GameObject> buttons;
+        private StoryStepData lastBranch;
+
+        private List<GameObject> lines = new List<GameObject>();
+
+        private bool isVisible = false;
+
+        void Awake()
+        {
+            // Test if all variables are present
+            if(panel == null) Debug.LogError("Panel is missing in StoryViewUI", this.gameObject);
+            if(showLines && storyLinePrefab == null) Debug.LogError("StoryViewUI is missing a Story line Prefab", this.gameObject);
+            if(showLines && lineContainer == null) Debug.LogError("StoryViewUI is missing a Story line Container", this.gameObject);
+            if(showBranches && storyBranchPrefab == null) Debug.LogError("StoryViewUI is missing a Story Branch Prefab", this.gameObject);
+            if(showBranches && branchContainer == null) Debug.LogError("StoryViewUI is missing a Story Branch Container", this.gameObject);
+        }
+
+        void OnEnable()
+        {
+            EventManager.broadcast.StartListening(storyName, OnStoryStep);
+            UiManager.RegisterElement("StoryView", this, this.transform, isVisible);
+        }
+
+        void OnDisable()
+        {
+            EventManager.broadcast.StopListening(storyName, OnStoryStep);
+            UiManager.UnregisterElement(this);
+        }
+
+        private void OnStoryStep(StoryStepData storyStep)
+        {
+            switch (storyStep.category)
+            {
+                case StoryStepType.Line:
+                    if(showLines) ShowStoryLine(storyStep);
+                    break;
+                case StoryStepType.Branch:
+                    if(showBranches) ShowStoryBranch(storyStep);
+                    break;
+                case StoryStepType.Path:
+                    ShowStoryPath(storyStep);
+                    break;
+                default:
+                    break;
+            }
+        }
+        private void ShowStoryLine(StoryStepData storyStep)
+        {
+            if(showLines == false) return;
+            ShowPanel();
+            if(showMultipleLines)
+            {
+                GameObject obj = Instantiate(storyLinePrefab, lineContainer);
+                obj.name = obj.name.Replace("(Clone)", "");
+                StoryViewText line = obj.GetComponent<StoryViewText>();
+                line.Setup(storyStep);
+                lines.Add(obj);
+                StartCoroutine(AutoScroll());
+            }
+            else lineText.Setup(storyStep);
+            if(usePortraits) ShowPortrait(storyStep);
+        }
+
+        private void ShowStoryBranch(StoryStepData storyStep)
+        {
+            if(buttons != null)
+            {
+                for (int i = 0; i < buttons.Count; i++) Destroy(buttons[i]);
+            }
+            buttons = new List<GameObject>();
+            ShowPanel();
+            for (int i = 0; i < storyStep.branches.Count; i++)
+            {
+                GameObject obj = Instantiate(storyBranchPrefab, branchContainer);
+                obj.name = obj.name.Replace("(Clone)", "");
+                StoryViewButton btn = obj.GetComponent<StoryViewButton>();
+                btn.Init(storyName, storyStep, i, this);
+                buttons.Add(obj);
+                StartCoroutine(AutoScroll());
+            }
+            lastBranch = storyStep;
+        }
+
+        private void ShowStoryPath(StoryStepData storyStep)
+        {
+            if(lastBranch == null) return;
+            if(hidePanelAfterChoice) Hide();
+            foreach (var btn in buttons) Destroy(btn);
+            buttons = new List<GameObject>();
+            lastBranch = null;
+            if(destroyLinesAfterPath) foreach (var line in lines) Destroy(line);
+        }
+
+        private void ShowPortrait(StoryStepData storyStep)
+        {
+            var s = storyStep.text;
+            var name = s.IndexOf(" ") > -1 
+                            ? s.Substring(0,s.IndexOf(" "))
+                            : s;
+            if (name.EndsWith(":"))
+            {
+                name = name.Remove(name.Length - 1, 1);
+                foreach(KeyValuePair<string, Sprite> keyValue in portraits)
+                {
+                    string key = keyValue.Key;
+                    if(key == name)
+                    {
+                        Sprite sprite = keyValue.Value;
+                        portraitImage.sprite = sprite;
+                        portraitContainer.SetActive(true);
+                        return;
+                    }
+                    
+                }
+            }
+            portraitContainer.SetActive(false);
+        }
+
+        public void ButtonClicked(StoryStepData storyStep)
+        {
+            //Debug.Log("Button clicked");
+            if(hidePanelAfterChoice) UiManager.HideElement(this);
+        }
+
+        public void Show()
+        {
+            isVisible = true;
+            ShowPanel();
+        }
+
+        public void Hide()
+        {
+            isVisible = false;
+            panel.gameObject.SetActive(false);
+        }
+
+        public bool IsVisible() => isVisible;
+
+        private void ShowPanel()
+        {
+
+            if(isVisible) panel.gameObject.SetActive(true);
+        }
+
+        private IEnumerator AutoScroll()
+        {
+            if(lineContainer != null)
+                LayoutRebuilder.ForceRebuildLayoutImmediate(lineContainer.GetComponent<RectTransform>());
+            yield return new WaitForEndOfFrame();
+            yield return new WaitForEndOfFrame();
+            if(scrollRect != null) scrollRect.verticalNormalizedPosition = 0;
+        }
+
+    }
+}
+

+ 11 - 0
Runtime/UI/StoryViewUI.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5dc8ce7998364d348bdc00785b28f3ef
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Tests.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: cbce0e1090813bd418ebab84e10eb0e4
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Tests/Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 071249bff6d9e6d468229351c18a7f9e
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 155 - 0
Tests/Editor/InkTests.cs

@@ -0,0 +1,155 @@
+using System.Collections;
+using System.Collections.Generic;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEditor;
+using UnityEngine.TestTools;
+using KairoEngine.Core;
+using KairoEngine.StorySystem;
+using UniRx;
+
+namespace KairoEngine.StorySystem.EditorTests
+{
+    public class StorySystemTests
+    {
+        private StoryController storyController;
+        private string storyName = "TestStory";
+
+        private List<string> lines;
+
+        [SetUp]
+        public void Setup()
+        {
+            GenericEventsStoryExtensions.list = new Dictionary<string, System.Action<StoryStepData>>();
+            lines = new List<string>();
+            lines.Add("Once upon a time...");
+            lines.Add("There was a prince and a princess.");
+            lines.Add("They lived happily ever after.");
+            string scriptPath = "Assets/Plugins/KairoEngine/StorySystem/Tests/test-script.json";
+            TextAsset storyJSON =  AssetDatabase.LoadAssetAtPath<TextAsset>(scriptPath); 
+            if(storyJSON != null) storyController = new StoryController(storyJSON, storyName, false);
+            else Debug.LogError($"Could not load file \"{scriptPath}\"");
+            
+        }
+
+        [TearDown]
+        public void TearDown()
+        {
+            if(storyController != null) 
+            {
+                storyController.Stop();
+                storyController = null;
+            }
+        }
+
+        [Test]
+        public void StoryController_Constructor_test()
+        {
+            Assert.NotNull(storyController);
+        }
+
+        [UnityTest]
+        public IEnumerator StoryController_Start()
+        {
+            string result = "";
+            bool done = false;
+            CompositeDisposable cancel = new CompositeDisposable();
+            StoryController.Lines().Subscribe(storyStep => {
+                Debug.Log(storyStep.text);
+                result = storyStep.text;
+                done = true;
+                cancel.Dispose();
+            }).AddTo(cancel);
+            storyController.Start();
+            while(done == false) 
+            {
+                yield return null;
+            }
+            Assert.AreEqual("Once upon a time...", result);
+        }
+
+        [UnityTest]
+        public IEnumerator StoryController_Receive_Lines()
+        {
+            string result = "";
+            int nextLine = 0;
+            bool done = false;
+            CompositeDisposable cancel = new CompositeDisposable();
+            StoryController.Lines().Subscribe(storyStep => {
+                if(storyStep.category != StoryStepType.Line) return;
+                result = storyStep.text;
+                //Debug.Log($"{nextLine}: {storyStep.text}");
+                nextLine += 1;
+                if(nextLine >= 2) done = true;
+            }).AddTo(cancel);
+            storyController.Start();
+            while(done == false) 
+            {
+                yield return null;
+            }
+            cancel.Dispose();
+            Assert.AreEqual("There was a prince and a princess.", result);
+        }
+
+        [UnityTest]
+        public IEnumerator StoryController_Receive_Branches()
+        {
+            List<string> branches = new List<string>();
+            bool done = false;
+            CompositeDisposable cancel = new CompositeDisposable();
+            StoryController.Lines().Subscribe(storyStep => {
+                if(storyStep.category != StoryStepType.Branch) return;
+                for (int i = 0; i < storyStep.branches.Count; i++)
+                {
+                    branches.Add(storyStep.branches[i]);
+                }
+                done = true;
+            }).AddTo(cancel);
+            storyController.Start();
+            while(done == false) 
+            {
+                yield return null;
+            }
+            cancel.Dispose();
+            Assert.AreEqual(2, branches.Count);
+            Assert.AreEqual("There was one choice.", branches[0]);
+            Assert.AreEqual("or another choice.", branches[1]);
+        }
+
+        [UnityTest]
+        public IEnumerator StoryController_Select_Path()
+        {
+            List<string> branches = new List<string>();
+            string text = "";
+            bool pathSelected = false;
+            bool done = false;
+            CompositeDisposable cancel = new CompositeDisposable();
+            StoryController.Lines().Subscribe(storyStep => {
+                if(storyStep.category == StoryStepType.Line) Debug.Log($"{storyStep.text} ({storyStep.category})");
+                if(storyStep.category == StoryStepType.Line && pathSelected)
+                {
+                    text = storyStep.text;
+                    done = true;
+                }
+                if(storyStep.category == StoryStepType.Branch)
+                {
+                    StoryStepData newStoryStep = new StoryStepData(StoryStepType.Path, storyStep.branches[0], null, null, 0);
+                    pathSelected = true;
+                    for (int i = 0; i < storyStep.branches.Count; i++) 
+                    {
+                        if(i != 0) Debug.Log($"{storyStep.branches[i]} ({storyStep.category})");
+                        else Debug.Log($"{newStoryStep.text} ({storyStep.category}/{newStoryStep.category})");
+                    }
+                    EventManager.broadcast.Trigger(storyName, newStoryStep);
+                }
+            }).AddTo(cancel);
+            storyController.Start();
+            while(done == false) 
+            {
+                yield return null;
+            }
+            cancel.Dispose();
+            Assert.AreEqual("They lived happily ever after.", text);
+        }
+    }
+}

+ 11 - 0
Tests/Editor/InkTests.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a09c441199058d740b74a5895cb0935f
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 24 - 0
Tests/Editor/KairoEngine.StorySystem.EditorTests.asmdef

@@ -0,0 +1,24 @@
+{
+    "name": "KairoEngine.StorySystem.EditorTests",
+    "references": [
+        "GUID:7e5ae6a38d1532248b4c890eca668b06",
+        "GUID:dc1ff8ea02e2e9449ab31f83ffba726e",
+        "GUID:0acc523941302664db1f4e527237feb3",
+        "GUID:27619889b8ba8c24980f49ee34dbb44a",
+        "GUID:58bed0e7c5306824586d7eda03609289",
+        "GUID:560b04d1a97f54a4e82edc0cbbb69285"
+    ],
+    "includePlatforms": [
+        "Editor"
+    ],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": true,
+    "precompiledReferences": [
+        "nunit.framework.dll"
+    ],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
Tests/Editor/KairoEngine.StorySystem.EditorTests.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 2d3ba47ea83c6df49994de4c74720dd8
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Tests/Runtime.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a332415fb906d1448afc3acf9148472d
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Tests/test-script.ink

@@ -0,0 +1,8 @@
+Once upon a time...
+There was a prince and a princess.
+
+ * [There was one choice.]
+ * [or another choice.]
+
+- They lived happily ever after.
+    -> END

+ 7 - 0
Tests/test-script.ink.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 36f4b540400478048b0d8df297499608
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 1 - 0
Tests/test-script.json

@@ -0,0 +1 @@
+{"inkVersion":20,"root":[["^Once upon a time...","\n","^There was a prince and a princess.","\n","ev","str","^There was one choice.","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^or another choice.","/str","/ev",{"*":"0.c-1","flg":20},{"c-0":["\n",{"->":"0.g-0"},{"#f":5}],"c-1":["\n",{"->":"0.g-0"},{"#f":5}],"g-0":["^They lived happily ever after.","\n","end",["done",{"#f":5,"#n":"g-1"}],{"#f":5}]}],"done",{"#f":1}],"listDefs":{}}

+ 7 - 0
Tests/test-script.json.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 686119fa7159f9345968f036783f2556
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 15 - 0
package.json

@@ -0,0 +1,15 @@
+{
+    "name": "at.kairoscope.kairoengine.story-system",
+    "displayName": "KairoEngine Story System",
+    "version": "0.1.2",
+    "unity": "2020.3",
+    "description": "Kairoengine story system based on the Ink language.",
+    "dependencies": {
+      "at.kairoscope.thirdparty.ink":"1.0.0"
+    },
+    "repository": {
+      "type": "git",
+      "url": "https://git.kairoscope.net/kairoengine/story-system.git"
+    },
+    "author": "Kairoscope"
+  }