Compare commits

..

No commits in common. "main" and "luneraria/s1-strategie" have entirely different histories.

245 changed files with 59369 additions and 68909 deletions

View file

@ -1,596 +0,0 @@
{
"id": "brunhild",
"group": "Characters",
"source": "tools/meshy/build_figure.py",
"exporter_version": 11,
"blender": "5.2.2 LTS",
"fbx_settings": {
"use_selection": true,
"use_visible": false,
"use_active_collection": false,
"global_scale": 1.0,
"apply_unit_scale": true,
"apply_scale_options": "FBX_SCALE_UNITS",
"axis_forward": "Y",
"axis_up": "Z",
"bake_space_transform": false,
"object_types": [
"ARMATURE",
"EMPTY",
"MESH"
],
"use_mesh_modifiers": true,
"use_mesh_modifiers_render": true,
"mesh_smooth_type": "OFF",
"use_subsurf": false,
"use_mesh_edges": false,
"use_tspace": false,
"use_triangles": false,
"colors_type": "LINEAR",
"prioritize_active_color": true,
"use_custom_props": false,
"add_leaf_bones": false,
"primary_bone_axis": "Y",
"secondary_bone_axis": "X",
"use_armature_deform_only": true,
"armature_nodetype": "NULL",
"bake_anim": true,
"bake_anim_use_all_bones": true,
"bake_anim_use_nla_strips": false,
"bake_anim_use_all_actions": true,
"bake_anim_force_startend_keying": true,
"bake_anim_step": 1.0,
"bake_anim_simplify_factor": 0.0,
"path_mode": "STRIP",
"embed_textures": false,
"batch_mode": "OFF"
},
"checks": [
{
"check": "turn_applied",
"ok": true,
"detail": "[]"
},
{
"check": "units_metric",
"ok": true,
"detail": "METRIC"
},
{
"check": "unit_scale_1",
"ok": true,
"detail": "1.0"
},
{
"check": "no_negative_scale",
"ok": true,
"detail": "{}"
},
{
"check": "object_scale_1",
"ok": true,
"detail": "{}"
},
{
"check": "mesh_names_unique",
"ok": true,
"detail": "['BrunhildBody_LOD0', 'BrunhildBody_LOD1', 'BrunhildSpeer_LOD0', 'BrunhildSpeer_LOD1']"
},
{
"check": "material_slots_filled",
"ok": true,
"detail": "{}"
},
{
"check": "normals_unit_length",
"ok": true,
"detail": "{}"
},
{
"check": "no_blend_shapes",
"ok": true,
"detail": "{}"
},
{
"check": "triangles_match_godot",
"ok": true,
"detail": "28706 fbx / 28706 godot"
},
{
"check": "one_armature",
"ok": true,
"detail": "['BrunhildRig']"
},
{
"check": "single_root_bone",
"ok": true,
"detail": "['Root']"
},
{
"check": "root_bone_name",
"ok": true,
"detail": "['Root']"
},
{
"check": "bone_count_match_godot",
"ok": true,
"detail": "25 / 25"
},
{
"check": "bone_names_portable",
"ok": true,
"detail": "[]"
},
{
"check": "skinned_meshes_present",
"ok": true,
"detail": ""
},
{
"check": "clips_match_godot",
"ok": true,
"detail": "[]"
},
{
"check": "required_clips_present",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_mesh_names",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_triangles",
"ok": true,
"detail": "28706 / 28706"
},
{
"check": "roundtrip_materials",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_bones",
"ok": true,
"detail": "[]"
},
{
"check": "roundtrip_clips",
"ok": true,
"detail": "['ability', 'attack_a', 'attack_b', 'death', 'hit', 'idle', 'ultimate', 'walk']"
}
],
"generator": {
"clips": {
"idle": {
"source": "Idle",
"frames": [
0.0,
96.0
],
"seconds": 4.0,
"loop": true,
"contact_seconds": -1.0
},
"walk": {
"source": "Walk_Fight_Forward",
"frames": [
0.0,
40.0
],
"seconds": 1.2917,
"loop": true,
"contact_seconds": -1.0
},
"attack_a": {
"source": "Thrust_Slash",
"frames": [
4,
30
],
"seconds": 0.9583,
"loop": false,
"contact_seconds": 0.516
},
"attack_b": {
"source": "Charged_Upward_Slash",
"frames": [
0,
45
],
"seconds": 1.0,
"loop": false,
"contact_seconds": 0.5556
},
"hit": {
"source": "Hit_Reaction",
"frames": [
0,
14
],
"seconds": 0.5,
"loop": false,
"contact_seconds": -1.0
},
"ability": {
"source": "Over_Shoulder_Throw",
"frames": [
50,
100
],
"seconds": 1.0,
"loop": false,
"contact_seconds": 0.64
},
"death": {
"source": "Shot_and_Fall_Backward",
"frames": [
0.0,
84.0
],
"seconds": 2.4167,
"loop": false,
"contact_seconds": -1.0
},
"ultimate": {
"source": "Basic_Jump",
"frames": [
20,
80
],
"seconds": 1.2917,
"loop": false,
"contact_seconds": 0.775
}
},
"textures": {
"glow_share": 0.0,
"palette_gains": {},
"size": [
2048,
2048
]
},
"unit": 0.009999999776482582,
"lod": {
"lod0_triangles": 20663,
"lod1_triangles": 4999,
"lod1_vertices": 2468,
"welded_triangles": 20663,
"lod0_screen": 0.2
},
"props": [
{
"name": "BrunhildSpeer",
"triangles": 2545,
"textures": {
"glow_share": 0.0,
"palette_gains": {},
"size": [
2048,
2048
]
},
"lod": {
"lod0_triangles": 2545,
"lod1_triangles": 499,
"lod1_vertices": 252,
"welded_triangles": 2545,
"lod0_screen": 0.2
}
}
]
},
"inputs_sha256": {
"tools/meshy/brunhild/figure.json": "19ccf5ca996bba99f8681c8b02ce221f7700175f7d66b820c5a953017e2305d9",
"assets-source/meshy/brunhild/brunhild-skelett.glb": "f089ffcec953c76f03997cdc19f3eb2f126b7199e9698e4e0a9d5654fca1dd18",
"assets-source/meshy/brunhild/brunhild-animationen.glb": "aca3433fec67155457f05046880e30772f9fba21ff1789912acf700f26ca7bfc",
"assets-source/meshy/brunhild/brunhild-speer.glb": "a7637375a32752913dc3929fe099d3c6e2e5d75a0f0665d0244af1637459f364"
},
"source_sha256": "5a0df3fc4ab4228c0b631a038098db9e79c833ae69ddd2457a8062eb1b493283",
"removed_modifiers": 0,
"kind": "skinned",
"fps": 24.0,
"objects": 5,
"meshes": {
"BrunhildBody_LOD0": {
"mesh": "BrunhildBody_LOD0",
"triangles": 20663,
"vertices": 28942,
"ngons": 0,
"materials": [
"AI_Brunhild"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [],
"active_color": null,
"custom_normals": true,
"normals_unit_length": true,
"bad_normals": 0,
"degenerate_triangles": 0,
"shape_keys": 0,
"skinned": true,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-0.4473,
0.0,
-0.2325
],
"max": [
0.4473,
1.8,
0.2325
]
}
},
"BrunhildSpeer_LOD0": {
"mesh": "BrunhildSpeer_LOD0",
"triangles": 2545,
"vertices": 3856,
"ngons": 0,
"materials": [
"AI_BrunhildSpeer"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [],
"active_color": null,
"custom_normals": true,
"normals_unit_length": true,
"bad_normals": 0,
"degenerate_triangles": 0,
"shape_keys": 0,
"skinned": true,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
0.1895,
0.7103,
-0.6358
],
"max": [
0.5065,
1.2324,
1.2914
]
}
},
"BrunhildBody_LOD1": {
"mesh": "BrunhildBody_LOD1",
"triangles": 4999,
"vertices": 2468,
"ngons": 0,
"materials": [
"AI_Brunhild"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"bad_normals": 0,
"degenerate_triangles": 0,
"shape_keys": 0,
"skinned": true,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
-0.4478,
0.0007,
-0.2335
],
"max": [
0.4466,
1.8008,
0.2343
]
}
},
"BrunhildSpeer_LOD1": {
"mesh": "BrunhildSpeer_LOD1",
"triangles": 499,
"vertices": 252,
"ngons": 0,
"materials": [
"AI_BrunhildSpeer"
],
"uv_layers": [
"UVMap"
],
"color_attributes": [],
"active_color": null,
"custom_normals": false,
"normals_unit_length": true,
"bad_normals": 0,
"degenerate_triangles": 0,
"shape_keys": 0,
"skinned": true,
"scale": [
1.0,
1.0,
1.0
],
"bounds_unity": {
"min": [
0.189,
0.7108,
-0.6379
],
"max": [
0.5086,
1.2317,
1.2931
]
}
}
},
"armatures": {
"BrunhildRig": {
"bones": [
"Root",
"Hips",
"Spine02",
"Spine01",
"Spine",
"neck",
"Head",
"head_end",
"headfront",
"LeftShoulder",
"LeftArm",
"LeftForeArm",
"LeftHand",
"RightShoulder",
"RightArm",
"RightForeArm",
"RightHand",
"LeftUpLeg",
"LeftLeg",
"LeftFoot",
"LeftToeBase",
"RightUpLeg",
"RightLeg",
"RightFoot",
"RightToeBase"
],
"roots": [
"Root"
]
}
},
"empties": {},
"bounds_unity": {
"min": [
-0.4478,
0.0,
-0.6379
],
"max": [
0.5086,
1.8008,
1.2931
]
},
"clips": [
{
"name": "ability",
"frames": [
0.0,
24.0
],
"seconds": 1.0,
"loop": false,
"contact_seconds": 0.64
},
{
"name": "attack_a",
"frames": [
0.0,
23.0
],
"seconds": 0.9583,
"loop": false,
"contact_seconds": 0.516
},
{
"name": "attack_b",
"frames": [
0.0,
24.0
],
"seconds": 1.0,
"loop": false,
"contact_seconds": 0.5556
},
{
"name": "death",
"frames": [
0.0,
58.0
],
"seconds": 2.4167,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "hit",
"frames": [
0.0,
12.0
],
"seconds": 0.5,
"loop": false,
"contact_seconds": -1.0
},
{
"name": "idle",
"frames": [
0.0,
96.0
],
"seconds": 4.0,
"loop": true,
"contact_seconds": -1.0
},
{
"name": "ultimate",
"frames": [
0.0,
31.0
],
"seconds": 1.2917,
"loop": false,
"contact_seconds": 0.775
},
{
"name": "walk",
"frames": [
0.0,
31.0
],
"seconds": 1.2917,
"loop": true,
"contact_seconds": -1.0
}
],
"expected_from_godot": {
"triangles": 28706,
"bones": 25,
"clips": [
"idle",
"walk",
"attack_a",
"attack_b",
"hit",
"ability",
"death",
"ultimate"
]
},
"meshes_with_ngons": {},
"meshes_without_uv": [],
"triangles": 28706,
"fbx": "Assets/Luneraria/Art/Characters/brunhild.fbx",
"fbx_bytes": 3796828,
"ok": true
}

View file

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

View file

@ -1,373 +0,0 @@
fileFormatVersion: 2
guid: 29d894f788c5f4fbbacf819091c19542
ModelImporter:
serializedVersion: 24300
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
searchTexturesGlobally: 0
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 0
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 0
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations:
- serializedVersion: 16
name: ability
takeName: BrunhildRig|ability
internalID: 3282932801577121271
firstFrame: 0
lastFrame: 24
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events:
- time: 0.64
functionName: OnLuneContact
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: attack_a
takeName: BrunhildRig|attack_a
internalID: -4088893210095135303
firstFrame: 0
lastFrame: 23
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events:
- time: 0.5384535
functionName: OnLuneContact
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: attack_b
takeName: BrunhildRig|attack_b
internalID: -3393105429929367649
firstFrame: 0
lastFrame: 24
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events:
- time: 0.5556
functionName: OnLuneContact
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: death
takeName: BrunhildRig|death
internalID: -575409383583182742
firstFrame: 0
lastFrame: 58
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: hit
takeName: BrunhildRig|hit
internalID: 4649535545551490279
firstFrame: 0
lastFrame: 12
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: idle
takeName: BrunhildRig|idle
internalID: -7637435868926338065
firstFrame: 0
lastFrame: 96
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 1
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: ultimate
takeName: BrunhildRig|ultimate
internalID: 7528617810713444434
firstFrame: 0
lastFrame: 31
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events:
- time: 0.5999845
functionName: OnLuneContact
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: walk
takeName: BrunhildRig|walk
internalID: -759485606388579005
firstFrame: 0
lastFrame: 31
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 1
loopBlend: 0
loopBlendOrientation: 0
loopBlendPositionY: 0
loopBlendPositionXZ: 0
keepOriginalOrientation: 0
keepOriginalPositionY: 1
keepOriginalPositionXZ: 0
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events: []
transformMask: []
maskType: 3
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
isReadable: 1
meshes:
lODScreenPercentages:
- 0.25
- 0.01
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 0
importBlendShapes: 0
importCameras: 0
importLights: 0
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 1
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
generateMeshLods: 0
meshLodGenerationFlags: 0
maximumMeshLod: -1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 3
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 1
humanDescription:
serializedVersion: 3
human: []
skeleton: []
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 0
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 2
humanoidOversampling: 1
avatarSetup: 1
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,275 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1102 &-9220055677375747029
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: walk
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: -759485606388579005, guid: 29d894f788c5f4fbbacf819091c19542, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &-4719041364407145157
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: death
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: -575409383583182742, guid: 29d894f788c5f4fbbacf819091c19542, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &-3340885724969997654
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: ultimate
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7528617810713444434, guid: 29d894f788c5f4fbbacf819091c19542, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: brunhild
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: Base Layer
m_StateMachine: {fileID: 4919687160989328924}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
--- !u!1102 &604826683839612195
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: ability
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 3282932801577121271, guid: 29d894f788c5f4fbbacf819091c19542, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &3419020185831067391
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: attack_a
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: -4088893210095135303, guid: 29d894f788c5f4fbbacf819091c19542, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &4117362530366511861
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: hit
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 4649535545551490279, guid: 29d894f788c5f4fbbacf819091c19542, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &4353629903250305412
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: attack_b
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: -3393105429929367649, guid: 29d894f788c5f4fbbacf819091c19542, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1107 &4919687160989328924
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Base Layer
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: 6833392093826740936}
m_Position: {x: 200, y: 0, z: 0}
- serializedVersion: 1
m_State: {fileID: 604826683839612195}
m_Position: {x: 235, y: 65, z: 0}
- serializedVersion: 1
m_State: {fileID: 3419020185831067391}
m_Position: {x: 270, y: 130, z: 0}
- serializedVersion: 1
m_State: {fileID: 4353629903250305412}
m_Position: {x: 305, y: 195, z: 0}
- serializedVersion: 1
m_State: {fileID: -4719041364407145157}
m_Position: {x: 340, y: 260, z: 0}
- serializedVersion: 1
m_State: {fileID: 4117362530366511861}
m_Position: {x: 375, y: 325, z: 0}
- serializedVersion: 1
m_State: {fileID: -3340885724969997654}
m_Position: {x: 410, y: 390, z: 0}
- serializedVersion: 1
m_State: {fileID: -9220055677375747029}
m_Position: {x: 445, y: 455, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: 6833392093826740936}
--- !u!1102 &6833392093826740936
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: idle
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: -7637435868926338065, guid: 29d894f788c5f4fbbacf819091c19542, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:

View file

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 8f779b941008d48fea9cfb256567bb9d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 MiB

View file

@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 0dbeeb8c48f344e64962b71d5e9fcca1
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
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
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
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
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 MiB

View file

@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: f701c67235f354e4dac0ee3235b94b3d
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 0
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
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
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: 1
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

View file

@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 1722b2d9a084347b8845372bf878a61f
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 0
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
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
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
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 MiB

View file

@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: ed6f51893270d4a81a491e1b895a5815
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
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
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
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
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 MiB

View file

@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 336afb236cf9f438dae9687f76420327
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
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
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
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
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 MiB

View file

@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 9b19528e38c1f468a8dfb54927968778
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 0
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
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
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: 1
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

View file

@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: cf3a5ed7600ba42349191e05cfe011e8
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 0
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
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
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
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 MiB

View file

@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 1fadc4bab123948b3b3f54a2fe52173a
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
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
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
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
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -60,13 +60,7 @@ namespace Luneraria.Core
new HeroSpec { Id = "held_sylvara", Name = "Sylvara", Title = "Die Mondklinge", Model = "sylvara", Cost = 260, RoleText = "Assassine / Nahkampf", Provisional = false, Summary = "Springt in die zweite Reihe, wirft Sicheln und wirbelt mit dem Mondsicheltanz durch Gruppen.", new HeroSpec { Id = "held_sylvara", Name = "Sylvara", Title = "Die Mondklinge", Model = "sylvara", Cost = 260, RoleText = "Assassine / Nahkampf", Provisional = false, Summary = "Springt in die zweite Reihe, wirft Sicheln und wirbelt mit dem Mondsicheltanz durch Gruppen.",
PassiveText = "Mondsprung: einmal pro Kampf auf den nächsten Fernkämpfer oder Unterstützer in 8 m, der höchstens 2 Begleiter in 3 m hat. Der erste Treffer danach macht 150 % Schaden; Mondschleier schützt 4 s mit 25 % ihrer Lebenspunkte.", PassiveText = "Mondsprung: einmal pro Kampf auf den nächsten Fernkämpfer oder Unterstützer in 8 m, der höchstens 2 Begleiter in 3 m hat. Der erste Treffer danach macht 150 % Schaden; Mondschleier schützt 4 s mit 25 % ihrer Lebenspunkte.",
AbilityText = "Sichelwurf: ab 4 s alle 6 s 90 % Schaden an ihrem Ziel und bis zu 2 weiteren Gegnern in 2,5 m.", AbilityText = "Sichelwurf: ab 4 s alle 6 s 90 % Schaden an ihrem Ziel und bis zu 2 weiteren Gegnern in 2,5 m.",
UltimateName = "Mondsicheltanz", UltimateText = "Einmal pro Kampf, frühestens nach 2 s: 220 % Schaden an bis zu 5 Gegnern in 5 m, die 2 s um 30 % verlangsamt werden; danach Mondschleier. Wann er ausgelöst wird, legst du in der Planung fest.", Base = new UnitDef { MaxHp = 1450.0, Damage = 42.0, AttackType = 0, ArmorType = 0, AttackRange = 1.2, Speed = 2.8, Ability = "sylvara", AbilityDescription = "Mondsprung auf ungeschützte Fernkämpfer · Sichelwurf alle 6 s · Ultimate Mondsicheltanz nach Planungsregel." } }, UltimateName = "Mondsicheltanz", UltimateText = "Einmal pro Kampf, frühestens nach 2 s: 220 % Schaden an bis zu 5 Gegnern in 5 m, die 2 s um 30 % verlangsamt werden; danach Mondschleier. Wann er ausgelöst wird, legst du in der Planung fest.", Base = new UnitDef { MaxHp = 1450.0, Damage = 42.0, AttackType = 0, ArmorType = 0, AttackRange = 1.2, Speed = 2.8, Ability = "sylvara", AbilityDescription = "Mondsprung auf ungeschützte Fernkämpfer · Sichelwurf alle 6 s · Ultimate Mondsicheltanz nach Planungsregel." } }
// H2: Brunhild, the second hero of the champions concept.
new HeroSpec { Id = "held_brunhild", Name = "Brunhild", Title = "Die Walküre", Model = "brunhild", Cost = 260, RoleText = "Heldenjägerin / Nahkampf", Provisional = false, Summary = "Markiert mit dem Speer, stürzt sich auf Helden und betäubt mit dem Walkürensturz.",
PassiveText = "Heldenjägerin: +25 % Schaden gegen Helden und Angriffschampions.",
AbilityText = "Speerwurf: ab 3 s alle 6 s 120 % Schaden am fernsten Gegner in 7 m; er ist 4 s markiert und nimmt 15 % mehr Schaden.",
UltimateName = "Walkürensturz", UltimateText = "Einmal pro Kampf, frühestens nach 2 s: Sprung auf ihr Ziel (ein Held oder der markierte Gegner zuerst), 200 % Schaden an allen Gegnern in 3 m und 1 s Betäubung (Bosse sind immun); danach Walkürenschild (30 % ihrer Lebenspunkte, 5 s). Wann sie springt, legst du in der Planung fest.",
Base = new UnitDef { MaxHp = 2000.0, Damage = 42.0, AttackType = 1, ArmorType = 1, AttackRange = 2.0, Speed = 2.4, Ability = "brunhild", AbilityDescription = "Heldenjägerin · Speerwurf markiert alle 6 s · Ultimate Walkürensturz nach Planungsregel." } }
); );
/// <summary>Heroes of the Godot reference; ContentTests compares exactly these, later heroes follow them.</summary> /// <summary>Heroes of the Godot reference; ContentTests compares exactly these, later heroes follow them.</summary>
@ -97,11 +91,6 @@ namespace Luneraria.Core
{ {
new[] { new TalentOption("mondsilber", "Mondsilber", "Einzelziel: Der Sichelwurf trifft nur das Ziel, dafür mit 200 % statt 90 % Schaden."), new TalentOption("sichelsturm", "Sichelsturm", "Fläche: Der Sichelwurf trifft bis zu 5 statt 3 Gegner in 3,5 m.") }, new[] { new TalentOption("mondsilber", "Mondsilber", "Einzelziel: Der Sichelwurf trifft nur das Ziel, dafür mit 200 % statt 90 % Schaden."), new TalentOption("sichelsturm", "Sichelsturm", "Fläche: Der Sichelwurf trifft bis zu 5 statt 3 Gegner in 3,5 m.") },
new[] { new TalentOption("neumond", "Neumond", "+20 % Angriffstempo."), new TalentOption("vollmond", "Vollmond", "Mondsicheltanz: bis zu 7 statt 5 Ziele, 280 % statt 220 % Schaden.") } new[] { new TalentOption("neumond", "Neumond", "+20 % Angriffstempo."), new TalentOption("vollmond", "Vollmond", "Mondsicheltanz: bis zu 7 statt 5 Ziele, 280 % statt 220 % Schaden.") }
} },
{ "held_brunhild", new[]
{
new[] { new TalentOption("walkuerenblut", "Walkürenblut", "Einzelziel: Der Speerwurf macht gegen Helden und Champions doppelten Schaden."), new TalentOption("speerregen", "Speerregen", "Fläche: Der Speerwurf trifft die drei fernsten Gegner in 7 m.") },
new[] { new TalentOption("rabenschwinge", "Rabenschwinge", "+20 % Angriffstempo."), new TalentOption("goetterdaemmerung", "Götterdämmerung", "Walkürensturz: 2 s Betäubung in 4 m statt 1 s in 3 m.") }
} } } }
}; };
@ -113,7 +102,6 @@ namespace Luneraria.Core
public static readonly Dictionary<string, TalentOption[]> HeroSkins = new Dictionary<string, TalentOption[]> public static readonly Dictionary<string, TalentOption[]> HeroSkins = new Dictionary<string, TalentOption[]>
{ {
{ "held_sylvara", new[] { new TalentOption("blutmond", "Blutmond", "Variante: rote Klingen und Runen, dunkelrotes Tuch, nachgedunkeltes Silber.") } }, { "held_sylvara", new[] { new TalentOption("blutmond", "Blutmond", "Variante: rote Klingen und Runen, dunkelrotes Tuch, nachgedunkeltes Silber.") } },
{ "held_brunhild", new[] { new TalentOption("eiswalkuere", "Eiswalküre", "Variante: eisblauer Umhang und Federn statt Rot, frostiges Gold.") } },
}; };
/// <summary> /// <summary>
@ -130,13 +118,6 @@ namespace Luneraria.Core
new TalentOption("notfall", "Notfall", "Wenn Sylvara unter 40 % Leben fällt und ein Gegner in 4 m steht."), new TalentOption("notfall", "Notfall", "Wenn Sylvara unter 40 % Leben fällt und ein Gegner in 4 m steht."),
new TalentOption("sofort", "Sofort", "Beim ersten Gegner in 4 m."), new TalentOption("sofort", "Sofort", "Beim ersten Gegner in 4 m."),
} }, } },
{ "held_brunhild", new[]
{
new TalentOption("gruppe", "Gruppe", "Sobald mindestens 3 Gegner in 4 m um ihr Ziel stehen."),
new TalentOption("held", "Heldenjagd", "Sobald ein gegnerischer Held oder Angriffschampion in 7 m steht."),
new TalentOption("notfall", "Notfall", "Wenn Brunhild unter 40 % Leben fällt und ein Gegner in 7 m steht."),
new TalentOption("sofort", "Sofort", "Beim ersten Gegner in 7 m."),
} },
}; };
public static readonly OrderedMap<string, ItemSpec> Items = Ordered( public static readonly OrderedMap<string, ItemSpec> Items = Ordered(
@ -145,17 +126,9 @@ namespace Luneraria.Core
new ItemSpec { Id = "rankenmantel", Name = "Rankenmantel", Cost = 200, Text = "+10 % Schadensminderung", Reduction = 0.1 }, new ItemSpec { Id = "rankenmantel", Name = "Rankenmantel", Cost = 200, Text = "+10 % Schadensminderung", Reduction = 0.1 },
new ItemSpec { Id = "funkenfokus", Name = "Funkenfokus", Cost = 200, Text = "+15 % Angriffstempo", AttackSpeed = 0.15 }, new ItemSpec { Id = "funkenfokus", Name = "Funkenfokus", Cost = 200, Text = "+15 % Angriffstempo", AttackSpeed = 0.15 },
new ItemSpec { Id = "quellamulett", Name = "Quellamulett", Cost = 220, Text = "Regeneriert 1,5 % der Lebenspunkte pro Sekunde", Regen = 0.015 }, new ItemSpec { Id = "quellamulett", Name = "Quellamulett", Cost = 220, Text = "Regeneriert 1,5 % der Lebenspunkte pro Sekunde", Regen = 0.015 },
new ItemSpec { Id = "kristallsplitter", Name = "Kristallsplitter", Cost = 260, Text = "Jeder Treffer: 25 % an bis zu 2 Gegner in 1,5 m · ab Stufe 3", Splash = 0.25, MinLevel = 3 }, new ItemSpec { Id = "kristallsplitter", Name = "Kristallsplitter", Cost = 260, Text = "Jeder Treffer: 25 % an bis zu 2 Gegner in 1,5 m · ab Stufe 3", Splash = 0.25, MinLevel = 3 }
// S2 recipes (extended rules only; Dota 2 / League of Legends): two items plus a fee become one stronger item.
new ItemSpec { Id = "bollwerk", Name = "Bollwerk", Cost = 500, Text = "Rezept: Basaltschild + Rankenmantel + 120 · +600 Lebenspunkte, +15 % Schadensminderung", HpFlat = 600.0, Reduction = 0.15, Parts = new[] { "basaltschild", "rankenmantel" }, RecipeFee = 120 },
new ItemSpec { Id = "glutsturm", Name = "Glutsturm", Cost = 520, Text = "Rezept: Glutklinge + Funkenfokus + 140 · +25 % Schaden, +22 % Angriffstempo", Damage = 0.25, AttackSpeed = 0.22, Parts = new[] { "glutklinge", "funkenfokus" }, RecipeFee = 140 },
new ItemSpec { Id = "quellherz", Name = "Quellherz", Cost = 520, Text = "Rezept: Quellamulett + Basaltschild + 120 · +450 Lebenspunkte, regeneriert 2,2 % pro Sekunde", HpFlat = 450.0, Regen = 0.022, Parts = new[] { "quellamulett", "basaltschild" }, RecipeFee = 120 },
new ItemSpec { Id = "splitterkrone", Name = "Splitterkrone", Cost = 600, Text = "Rezept: Kristallsplitter + Glutklinge + 160 · +20 % Schaden; jeder Treffer 35 % an bis zu 2 Gegner in 1,5 m · ab Stufe 5", Damage = 0.20, Splash = 0.35, MinLevel = 5, Parts = new[] { "kristallsplitter", "glutklinge" }, RecipeFee = 160 }
); );
/// <summary>Items of the Godot reference; ContentTests compares exactly these, the S2 recipes follow them.</summary>
public static readonly string[] ReferenceItems = { "basaltschild", "glutklinge", "rankenmantel", "funkenfokus", "quellamulett", "kristallsplitter" };
public static readonly OrderedMap<string, ChampionSpec> Champions = Ordered( public static readonly OrderedMap<string, ChampionSpec> Champions = Ordered(
new ChampionSpec { Id = "ch_schutz", Name = "Rostschieber", RoleText = "Schutz", Model = "s02", Cost = 150, Base = "s02", Text = "Schildträger: startet mit Schild, gibt alle 6 s bis zu 4 Verbündeten in 4 m einen Schild von 120.", ViewScale = 1.25 }, new ChampionSpec { Id = "ch_schutz", Name = "Rostschieber", RoleText = "Schutz", Model = "s02", Cost = 150, Base = "s02", Text = "Schildträger: startet mit Schild, gibt alle 6 s bis zu 4 Verbündeten in 4 m einen Schild von 120.", ViewScale = 1.25 },
new ChampionSpec { Id = "ch_druck", Name = "Dunstsänger", RoleText = "Druck auf Unterstützer", Model = "s03", Cost = 150, Base = "s03", Text = "Sucht Unterstützer und Fernkämpfer und verlangsamt deren Angriffe.", ViewScale = 1.25, Priority = "support" }, new ChampionSpec { Id = "ch_druck", Name = "Dunstsänger", RoleText = "Druck auf Unterstützer", Model = "s03", Cost = 150, Base = "s03", Text = "Sucht Unterstützer und Fernkämpfer und verlangsamt deren Angriffe.", ViewScale = 1.25, Priority = "support" },
@ -164,7 +137,7 @@ namespace Luneraria.Core
/// <summary>Base troop id to role; iteration order matters for RoleOf (first match).</summary> /// <summary>Base troop id to role; iteration order matters for RoleOf (first match).</summary>
public static readonly OrderedMap<string, string> MatchRoles = Pairs("g01", "melee", "g03", "front", "g05", "ranged", "g07", "splash", "g09", "support", "w01", "melee", "w03", "front", "w05", "ranged", "w07", "splash", "w09", "support"); public static readonly OrderedMap<string, string> MatchRoles = Pairs("g01", "melee", "g03", "front", "g05", "ranged", "g07", "splash", "g09", "support", "w01", "melee", "w03", "front", "w05", "ranged", "w07", "splash", "w09", "support");
public static readonly OrderedMap<string, string> HeroRoles = Pairs("held_ringborke", "front", "held_quellgeflecht", "support", "held_funkenlanze", "ranged", "held_sylvara", "melee", "held_brunhild", "melee"); public static readonly OrderedMap<string, string> HeroRoles = Pairs("held_ringborke", "front", "held_quellgeflecht", "support", "held_funkenlanze", "ranged", "held_sylvara", "melee");
public static readonly string[] RowOrder = { "front", "melee", "splash", "ranged", "support" }; public static readonly string[] RowOrder = { "front", "melee", "splash", "ranged", "support" };
public static readonly string[] BotNames = { "Eichrufer", "Aschenzunge", "Quellwächter", "Glutfaust", "Moosauge", "Basaltherz", "Rindenkrone", "Funkenschritt" }; public static readonly string[] BotNames = { "Eichrufer", "Aschenzunge", "Quellwächter", "Glutfaust", "Moosauge", "Basaltherz", "Rindenkrone", "Funkenschritt" };
public static readonly string[] WavesMelee = { "e01", "e02", "e05" }; public static readonly string[] WavesMelee = { "e01", "e02", "e05" };
@ -187,8 +160,7 @@ namespace Luneraria.Core
/// bot match). Usable as a fixed profile per slot (MatchOptions.Profiles, balance runs). /// bot match). Usable as a fixed profile per slot (MatchOptions.Profiles, balance runs).
/// </summary> /// </summary>
public static readonly OrderedMap<string, BotProfile> ExtraBotProfiles = Ordered( public static readonly OrderedMap<string, BotProfile> ExtraBotProfiles = Ordered(
new BotProfile { Name = "assassine", Front = 0.3, Melee = 0.25, Ranged = 0.25, Splash = 0.1, Support = 0.1, UpgradeFrom = 6, SupportLane = 0.0, Hero = "held_sylvara", HeroRound = 3, Items = new[] { "glutklinge", "funkenfokus", "basaltschild" }, TalentChoices = new[] { 1, 0 }, Champion = "" }, new BotProfile { Name = "assassine", Front = 0.3, Melee = 0.25, Ranged = 0.25, Splash = 0.1, Support = 0.1, UpgradeFrom = 6, SupportLane = 0.0, Hero = "held_sylvara", HeroRound = 3, Items = new[] { "glutklinge", "funkenfokus", "basaltschild" }, TalentChoices = new[] { 1, 0 }, Champion = "ch_druck" }
new BotProfile { Name = "walkuere", Front = 0.3, Melee = 0.25, Ranged = 0.25, Splash = 0.1, Support = 0.1, UpgradeFrom = 6, SupportLane = 0.0, Hero = "held_brunhild", HeroRound = 3, Items = new[] { "glutklinge", "basaltschild", "funkenfokus" }, TalentChoices = new[] { 0, 1 }, Champion = "" }
); );
} }
} }

View file

@ -105,11 +105,6 @@ namespace Luneraria.Core
public double Regen; public double Regen;
public double Splash; public double Splash;
public int MinLevel = 1; public int MinLevel = 1;
/// <summary>S2 recipe items (extended rules): the two items it is made of and the fee on top; Cost is the sum.</summary>
public string[] Parts = new string[0];
public int RecipeFee;
public bool IsRecipe { get { return Parts.Length > 0; } }
} }
public sealed class ChampionSpec public sealed class ChampionSpec

View file

@ -63,8 +63,6 @@ namespace Luneraria.Core
// S1: extended rules (MatchOptions.Extended; the reference rules ignore all of these). // S1: extended rules (MatchOptions.Extended; the reference rules ignore all of these).
/// <summary>Legion TD 2: a booked attack champion raises the booker's income by cost / this, for good.</summary> /// <summary>Legion TD 2: a booked attack champion raises the booker's income by cost / this, for good.</summary>
public int ChampionIncomeDivisor = 12; public int ChampionIncomeDivisor = 12;
/// <summary>S3: share of a champion's bounty the defenders get under the extended rules (Legion TD 2: less than the send costs).</summary>
public double ChampionBountyShare = 0.5;
/// <summary>Dota 2 barracks: waves of a team on a lane whose enemy inner tower fell get stronger.</summary> /// <summary>Dota 2 barracks: waves of a team on a lane whose enemy inner tower fell get stronger.</summary>
public double BarracksBoost = 0.25; public double BarracksBoost = 0.25;
public int BarracksExtraMonsters = 1; public int BarracksExtraMonsters = 1;
@ -78,39 +76,8 @@ namespace Luneraria.Core
/// <summary>Moon soul (all stacks): heroes regenerate this share of their health per second.</summary> /// <summary>Moon soul (all stacks): heroes regenerate this share of their health per second.</summary>
public double SoulRegen = 0.01; public double SoulRegen = 0.01;
// S2: extended rules.
/// <summary>Dota 2 glyph: a planned lane's towers turn invulnerable for this long once one falls below half health.</summary>
public double GlyphSeconds = 5.0;
public double GlyphThreshold = 0.5;
public int GlyphCooldown = 4;
/// <summary>Legion TD 2 king: Ancient levels bought by the team's players; price per level, attack and repair per level.</summary>
public int CoreLevelMax = 3;
public int CoreLevelPrice = 150;
public double[] CoreDamage = { 0.0, 45.0, 75.0, 110.0 };
public double CoreRange = 7.5;
public double CoreRepair = 1000.0;
// S3: neutral camps (League of Legends buff camps) under the extended rules.
/// <summary>A guardian stands at the middle of every lane in rounds divisible by this.</summary>
public int CampEvery = 3;
public double CampHp = 1500.0;
public double CampDamage = 30.0;
public double CampRange = 2.5;
/// <summary>Units within this distance decide which team takes a fallen guardian.</summary>
public double CampClaimRadius = 8.0;
/// <summary>Combats a lane buff lasts; Glutsegen damage, Mondschild shield share, Beute gold per player.</summary>
public int CampBuffRounds = 2;
public double CampDamageBuff = 0.10;
public double CampShieldBuff = 0.10;
public int CampLootGold = 50;
/// <summary>S3 wards (Dota 2): price and rounds a ward watches its lane.</summary>
public int WardCost = 40;
public int WardRounds = 3;
public static LuneBalance Default() { return new LuneBalance(); } public static LuneBalance Default() { return new LuneBalance(); }
public int CoreUpgradeCost(int level) { return CoreLevelPrice * (level + 1); }
public int BuybackCost(int level) { return BuybackBase + BuybackPerLevel * level; } public int BuybackCost(int level) { return BuybackBase + BuybackPerLevel * level; }
public int RoundIncome(int roundNumber) public int RoundIncome(int roundNumber)

View file

@ -130,7 +130,7 @@ namespace Luneraria.Core
reduction += 0.15; reduction += 0.15;
if (hero.Talents.Contains("fernsicht")) if (hero.Talents.Contains("fernsicht"))
definition.AttackRange = definition.AttackRange + 1.5; definition.AttackRange = definition.AttackRange + 1.5;
if (hero.Talents.Contains("glutsalve") || hero.Talents.Contains("neumond") || hero.Talents.Contains("rabenschwinge")) if (hero.Talents.Contains("glutsalve") || hero.Talents.Contains("neumond"))
attackSpeed += 0.20; attackSpeed += 0.20;
definition.UltimateRule = UltimateRuleFor(hero); definition.UltimateRule = UltimateRuleFor(hero);
definition.Skin = ValidSkin(hero.HeroId, hero.Skin) ? hero.Skin ?? "" : ""; definition.Skin = ValidSkin(hero.HeroId, hero.Skin) ? hero.Skin ?? "" : "";

View file

@ -624,14 +624,6 @@ namespace Luneraria.Core
MatchState.Boss.Claim = source.Claim != null ? source.Claim.Clone() : null; MatchState.Boss.Claim = source.Claim != null ? source.Claim.Clone() : null;
if (source.Blessing != null && source.Blessing.Length == 2) if (source.Blessing != null && source.Blessing.Length == 2)
MatchState.Boss.Blessing = (int[])source.Blessing.Clone(); MatchState.Boss.Blessing = (int[])source.Blessing.Clone();
if (source.GlyphReady != null && source.GlyphReady.Length == 2)
MatchState.Boss.GlyphReady = (int[])source.GlyphReady.Clone();
if (source.CoreLevel != null && source.CoreLevel.Length == 2)
MatchState.Boss.CoreLevel = (int[])source.CoreLevel.Clone();
if (source.CampBuff != null && source.CampBuff.Length == 6)
MatchState.Boss.CampBuff = (int[])source.CampBuff.Clone();
if (source.Wards != null && source.Wards.Length == 6)
MatchState.Boss.Wards = (int[])source.Wards.Clone();
} }
public void ApplyLobby(List<LobbyEntry> entries) public void ApplyLobby(List<LobbyEntry> entries)
@ -711,7 +703,6 @@ namespace Luneraria.Core
// H1: leap and ultimate counters ride in the flag bits (8 bits each, compared for change only). // H1: leap and ultimate counters ride in the flag bits (8 bits each, compared for change only).
unit.LeapSerial = (flags >> 2) & 0xFF; unit.LeapSerial = (flags >> 2) & 0xFF;
unit.UltimateSerial = (flags >> 10) & 0xFF; unit.UltimateSerial = (flags >> 10) & 0xFF;
unit.NetStunned = (flags & (1 << 18)) != 0;
unit.AttackProgress = data[baseIndex + 7]; unit.AttackProgress = data[baseIndex + 7];
unit.HitCount = (int)data[baseIndex + 8]; unit.HitCount = (int)data[baseIndex + 8];
unit.AttackSerial = (int)data[baseIndex + 9]; unit.AttackSerial = (int)data[baseIndex + 9];
@ -732,13 +723,6 @@ namespace Luneraria.Core
building.Hp = buildingHp[index]; building.Hp = buildingHp[index];
building.Alive = building.Hp > 0.0; building.Alive = building.Hp > 0.0;
} }
for (int team = 0; team < 2 && buildingHp.Length >= buildings.Count + 4; team++)
{
int lane = (int)buildingHp[buildings.Count + team * 2] - 1;
float left = buildingHp[buildings.Count + team * 2 + 1];
combat.GlyphLane[team] = lane >= 0 && lane < 3 ? LuneMap.Lanes[lane] : "";
combat.GlyphUntil[team] = lane >= 0 ? combat.Elapsed + left : -1.0;
}
} }
} }
@ -821,7 +805,7 @@ namespace Luneraria.Core
{ {
if (!unit.IsMobile()) if (!unit.IsMobile())
continue; continue;
int flags = (unit.Alive ? 1 : 0) | (unit.InRange ? 2 : 0) | ((unit.LeapSerial & 0xFF) << 2) | ((unit.UltimateSerial & 0xFF) << 10) | (unit.Stunned() ? 1 << 18 : 0); int flags = (unit.Alive ? 1 : 0) | (unit.InRange ? 2 : 0) | ((unit.LeapSerial & 0xFF) << 2) | ((unit.UltimateSerial & 0xFF) << 10);
data.Add(unit.Id); data.Add(unit.Id);
data.Add((float)unit.Position.X); data.Add((float)unit.Position.X);
data.Add((float)unit.Position.Y); data.Add((float)unit.Position.Y);
@ -842,19 +826,7 @@ namespace Luneraria.Core
public float[] BuildingHpArray() public float[] BuildingHpArray()
{ {
IReadOnlyList<BuildingDef> buildings = MatchState.Map.Buildings; IReadOnlyList<BuildingDef> buildings = MatchState.Map.Buildings;
// S4: four more values after the buildings carry the running glyph of each team (lane var data = new float[buildings.Count];
// index + 1, seconds left) so guests see the glyph ring; older guests ignore them.
var data = new float[buildings.Count + 4];
LuneCombat glyphs = MatchState.Phase == Phase.Combat ? MatchState.Combat : null;
for (int team = 0; team < 2 && glyphs != null; team++)
{
double left = glyphs.GlyphUntil[team] - glyphs.Elapsed;
if (glyphs.GlyphLane[team].Length > 0 && left > 0.0)
{
data[buildings.Count + team * 2] = LuneMap.LaneIndex(glyphs.GlyphLane[team]) + 1;
data[buildings.Count + team * 2 + 1] = (float)left;
}
}
for (int index = 0; index < buildings.Count; index++) for (int index = 0; index < buildings.Count; index++)
{ {
double hp; double hp;
@ -889,11 +861,7 @@ namespace Luneraria.Core
NextAvailableRound = MatchState.Boss.NextAvailableRound, NextAvailableRound = MatchState.Boss.NextAvailableRound,
SettledRound = MatchState.Boss.SettledRound, SettledRound = MatchState.Boss.SettledRound,
Claim = MatchState.Boss.Claim != null ? MatchState.Boss.Claim.Clone() : null, Claim = MatchState.Boss.Claim != null ? MatchState.Boss.Claim.Clone() : null,
Blessing = (int[])MatchState.Boss.Blessing.Clone(), Blessing = (int[])MatchState.Boss.Blessing.Clone()
GlyphReady = (int[])MatchState.Boss.GlyphReady.Clone(),
CoreLevel = (int[])MatchState.Boss.CoreLevel.Clone(),
CampBuff = (int[])MatchState.Boss.CampBuff.Clone(),
Wards = (int[])MatchState.Boss.Wards.Clone()
}; };
if (MatchState.Combat != null) if (MatchState.Combat != null)
state.DestroyedCores.AddRange(MatchState.Combat.DestroyedCores); state.DestroyedCores.AddRange(MatchState.Combat.DestroyedCores);

View file

@ -19,15 +19,10 @@ namespace Luneraria.Core
if (match.BotProfiles.Count == 6) if (match.BotProfiles.Count == 6)
return match.BotProfiles[slot]; return match.BotProfiles[slot];
// S1: with extended rules the rotation also holds the profiles of later heroes (Sylvara). // S1: with extended rules the rotation also holds the profiles of later heroes (Sylvara).
if (!match.Extended)
{
IReadOnlyList<string> reference = ContentData.BotProfiles.Keys;
return reference[(slot * 7 + match.SeedValue) % reference.Count];
}
var names = new List<string>(ContentData.BotProfiles.Keys); var names = new List<string>(ContentData.BotProfiles.Keys);
names.AddRange(ContentData.ExtraBotProfiles.Keys); if (match.Extended)
// S3 fix: with seven profiles the reference step 7 gave every seat the same profile. names.AddRange(ContentData.ExtraBotProfiles.Keys);
return names[(slot * 5 + match.SeedValue) % names.Count]; return names[(slot * 7 + match.SeedValue) % names.Count];
} }
public static void Plan(LuneMatch match, int slot, string profileName = "") public static void Plan(LuneMatch match, int slot, string profileName = "")
@ -45,17 +40,6 @@ namespace Luneraria.Core
// Items and champions are planned before the army so the roster cannot drain the reserve. // Items and champions are planned before the army so the roster cannot drain the reserve.
PlanItems(match, slot, profile, 110); PlanItems(match, slot, profile, 110);
PlanChampion(match, slot, profile); PlanChampion(match, slot, profile);
// S2 team actions come before the army too, so a threatened base gets its Ancient level.
if (match.Extended)
PlanTeam(match, slot);
// S3 (extended rules): from its hero round a bot saves for the hero instead of buying troops
// (the reference bots spend everything and recruit around round 11).
if (match.Extended && player.Draft.Hero == null && match.RoundNumber >= profile.HeroRound && player.Draft.Roster.Count >= 3)
{
PlanBoss(match, slot, profile);
match.SetReady(slot);
return;
}
for (int guard = 0; guard < 40; guard++) for (int guard = 0; guard < 40; guard++)
{ {
List<RosterEntry> roster = player.Draft.Roster; List<RosterEntry> roster = player.Draft.Roster;
@ -106,60 +90,9 @@ namespace Luneraria.Core
} }
} }
/// <summary>
/// S2 team actions (extended rules only): hold the glyph for the own lane when one of its
/// towers is below 60 %, and raise the Ancient once the base is under threat.
/// </summary>
static void PlanTeam(LuneMatch match, int slot)
{
Player player = match.Players[slot];
if (match.RoundNumber >= match.Boss.GlyphReady[player.Team])
{
foreach (BuildingDef definition in match.Map.Buildings)
{
if (definition.Team == player.Team && definition.Lane == player.Lane && definition.Tier != "core"
&& match.BuildingHp[definition.Id] > 0.0 && match.BuildingHp[definition.Id] < definition.MaxHp * 0.6)
{
match.Submit(slot, Command.Glyph(player.Lane));
break;
}
}
}
int level = match.Boss.CoreLevel[player.Team] + player.Draft.CoreInvest;
bool threatened = false;
foreach (BuildingDef definition in match.Map.Buildings)
{
if (definition.Team == player.Team && definition.Tier == "inner" && match.BuildingHp[definition.Id] <= 0.0)
threatened = true;
}
if (threatened && level < match.Balance.CoreLevelMax && player.Draft.Gold >= match.Balance.CoreUpgradeCost(level))
match.Submit(slot, Command.CoreUpgrade());
}
/// <summary>S2: both own towers of the player's lane stand above 70 % (an extra grotto hunter leaves only a safe lane).</summary>
static bool LaneSafe(LuneMatch match, Player player)
{
foreach (BuildingDef definition in match.Map.Buildings)
{
if (definition.Team == player.Team && definition.Lane == player.Lane && (definition.Tier == "outer" || definition.Tier == "inner")
&& match.BuildingHp[definition.Id] < definition.MaxHp * 0.7)
return false;
}
return true;
}
static void PlanItems(LuneMatch match, int slot, BotProfile profile, int reserve) static void PlanItems(LuneMatch match, int slot, BotProfile profile, int reserve)
{ {
HeroState hero = match.Players[slot].Draft.Hero; HeroState hero = match.Players[slot].Draft.Hero;
// S2 recipes: combine owned parts first (frees a slot), one per planning.
if (hero != null && match.Extended)
{
foreach (ItemSpec recipe in ContentData.Items.Values)
{
if (recipe.IsRecipe && match.Players[slot].Draft.Gold >= recipe.RecipeFee + reserve && match.Submit(slot, Command.ItemBuy(recipe.Id)).Ok)
break;
}
}
if (hero == null || hero.Items.Count >= LuneContent.ItemSlots) if (hero == null || hero.Items.Count >= LuneContent.ItemSlots)
return; return;
foreach (string itemId in profile.Items) foreach (string itemId in profile.Items)
@ -215,18 +148,14 @@ namespace Luneraria.Core
string lane = WeakestLane(match, 1 - player.Team); string lane = WeakestLane(match, 1 - player.Team);
match.Submit(slot, Command.BossLaneChoice(lane)); match.Submit(slot, Command.BossLaneChoice(lane));
} }
// S2 (extended rules): mid players also hunt the boss from round 8, so the Mondsegen of S1 really happens. if (!profile.HuntsBoss || match.RoundNumber < profile.BossFrom || !match.GrottoAvailable())
bool hunts = profile.HuntsBoss || (match.Extended && LuneMap.LaneIndex(player.Lane) == 1 && LaneSafe(match, player));
int from = profile.HuntsBoss ? profile.BossFrom : 8;
int troops = profile.HuntsBoss ? profile.BossTroops : 5;
if (!hunts || match.RoundNumber < from || !match.GrottoAvailable())
return; return;
List<RosterEntry> roster = player.Draft.Roster; List<RosterEntry> roster = player.Draft.Roster;
if (roster.Count < troops) if (roster.Count < profile.BossTroops)
return; return;
var picks = new List<RosterEntry>(roster); var picks = new List<RosterEntry>(roster);
GdMath.SortStable(picks, (a, b) => a.Invested == b.Invested ? a.Seq < b.Seq : a.Invested > b.Invested); GdMath.SortStable(picks, (a, b) => a.Invested == b.Invested ? a.Seq < b.Seq : a.Invested > b.Invested);
int keep = GdMath.Min(troops - 1, picks.Count); int keep = GdMath.Min(profile.BossTroops - 1, picks.Count);
picks.RemoveRange(keep, picks.Count - keep); picks.RemoveRange(keep, picks.Count - keep);
HeroState hero = player.Draft.Hero; HeroState hero = player.Draft.Hero;
// The boss formula is public. Lanchester estimate: total HP x total damage of the // The boss formula is public. Lanchester estimate: total HP x total damage of the

View file

@ -22,8 +22,6 @@ namespace Luneraria.Core
public string BuildingId = ""; public string BuildingId = "";
public int Xp; public int Xp;
public List<string> XpHeroes = new List<string>(); public List<string> XpHeroes = new List<string>();
/// <summary>S3 camp guardians: the team that takes the camp, -1 for none.</summary>
public int ClaimTeam = -1;
} }
/// <summary> /// <summary>
@ -58,10 +56,6 @@ namespace Luneraria.Core
public int GrottoActiveTeam = -1; public int GrottoActiveTeam = -1;
/// <summary>none, success or failed.</summary> /// <summary>none, success or failed.</summary>
public string GrottoOutcome = "none"; public string GrottoOutcome = "none";
/// <summary>S2 glyph per team: the held lane ("" none), when its towers stop being invulnerable, whether it fired.</summary>
public readonly string[] GlyphLane = { "", "" };
public readonly double[] GlyphUntil = { -1.0, -1.0 };
public readonly bool[] GlyphUsed = new bool[2];
int _nextId = 1; int _nextId = 1;
readonly int[] _teamCount = new int[3]; readonly int[] _teamCount = new int[3];
@ -297,7 +291,7 @@ namespace Luneraria.Core
} }
foreach (LuneUnit unit in living) foreach (LuneUnit unit in living)
{ {
if (unit.IsMobile() || unit.Kind == "tower" || (unit.Kind == "core" && unit.Def.Damage > 0.0)) if (unit.IsMobile() || unit.Kind == "tower")
{ {
bool keep = unit.Target != null && unit.Target.Alive; bool keep = unit.Target != null && unit.Target.Alive;
if (unit.LeapTarget != null) if (unit.LeapTarget != null)
@ -323,7 +317,7 @@ namespace Luneraria.Core
if (heading.LengthSquared() > 0.000001) if (heading.LengthSquared() > 0.000001)
unit.Facing = GdMath.Atan2(heading.X, heading.Y); unit.Facing = GdMath.Atan2(heading.X, heading.Y);
unit.LeapTarget = null; unit.LeapTarget = null;
if (unit.Zone == "lane" && unit.Kind != "camp") if (unit.Zone == "lane")
UpdateProgress(unit, plan.Value); UpdateProgress(unit, plan.Value);
} }
foreach (LuneUnit unit in living) foreach (LuneUnit unit in living)
@ -356,40 +350,10 @@ namespace Luneraria.Core
RecordDeath(unit); RecordDeath(unit);
} }
} }
TriggerGlyphs();
UpdateGrotto(); UpdateGrotto();
CheckEnd(); CheckEnd();
} }
/// <summary>S2: a tower of the glyph lane is invulnerable while its team's glyph runs.</summary>
public bool Glyphed(LuneUnit unit)
{
if (unit.Kind != "tower" || unit.Team > 1)
return false;
return GlyphLane[unit.Team].Length > 0 && unit.Lane == GlyphLane[unit.Team] && Elapsed < GlyphUntil[unit.Team] - 0.000001;
}
/// <summary>S2 (Dota 2 glyph): the first time a tower of the held lane is below the threshold, the glyph fires.</summary>
void TriggerGlyphs()
{
for (int team = 0; team < 2; team++)
{
if (GlyphLane[team].Length == 0 || GlyphUsed[team])
continue;
IReadOnlyList<LuneUnit> buildings = Buildings.Values;
for (int i = 0; i < buildings.Count; i++)
{
LuneUnit tower = buildings[i];
if (tower.Alive && tower.Kind == "tower" && tower.Team == team && tower.Lane == GlyphLane[team] && tower.Hp < tower.MaxHp * Balance.GlyphThreshold)
{
GlyphUsed[team] = true;
GlyphUntil[team] = Elapsed + Balance.GlyphSeconds;
break;
}
}
}
}
/// <summary>Expeditions fight each other first; the boss wakes when at most one team remains.</summary> /// <summary>Expeditions fight each other first; the boss wakes when at most one team remains.</summary>
void UpdateGrotto() void UpdateGrotto()
{ {
@ -430,8 +394,7 @@ namespace Luneraria.Core
bool mobileAlive = !GrottoResolved; bool mobileAlive = !GrottoResolved;
foreach (LuneUnit unit in Units) foreach (LuneUnit unit in Units)
{ {
// S3: a camp guardian alone does not hold the combat open. if (unit.Alive && unit.IsMobile() && unit.Zone == "lane")
if (unit.Alive && unit.IsMobile() && unit.Zone == "lane" && unit.Kind != "camp")
{ {
mobileAlive = true; mobileAlive = true;
break; break;
@ -470,7 +433,6 @@ namespace Luneraria.Core
switch (unit.Kind) switch (unit.Kind)
{ {
case "monster": xp = 12 + RoundNumber; break; case "monster": xp = 12 + RoundNumber; break;
case "camp": xp = 30 + 2 * RoundNumber; break;
case "champion": xp = 30 + 2 * RoundNumber; break; case "champion": xp = 30 + 2 * RoundNumber; break;
case "boss": xp = LuneContent.BossXp; break; case "boss": xp = LuneContent.BossXp; break;
case "troop": case "troop":
@ -485,35 +447,9 @@ namespace Luneraria.Core
ev.XpHeroes.Add(other.PersistentUid); ev.XpHeroes.Add(other.PersistentUid);
} }
ev.Xp = xp; ev.Xp = xp;
if (unit.Kind == "camp")
ev.ClaimTeam = CampClaim(unit);
Events.Add(ev); Events.Add(ev);
} }
/// <summary>
/// S3: the team with more living units near a fallen guardian takes the camp, on equal
/// numbers the one with more health there; an exact tie (mirrored armies) takes nothing.
/// Units dying in the same step do not count (health above zero), so order never decides.
/// </summary>
int CampClaim(LuneUnit camp)
{
var count = new int[2];
var health = new double[2];
foreach (LuneUnit other in Units)
{
if (other.Hp > 0.0 && other.Team < 2 && other.IsMobile() && other.Zone == camp.Zone && other.Position.DistanceTo(camp.Position) <= Balance.CampClaimRadius)
{
count[other.Team]++;
health[other.Team] += other.Hp;
}
}
if (count[0] != count[1])
return count[0] > count[1] ? 0 : 1;
if (Math.Abs(health[0] - health[1]) > 0.5)
return health[0] > health[1] ? 0 : 1;
return -1;
}
// ---------------------------------------------------------------- targeting and movement // ---------------------------------------------------------------- targeting and movement
static bool Hostile(LuneUnit unit, LuneUnit other) static bool Hostile(LuneUnit unit, LuneUnit other)
{ {
@ -527,11 +463,11 @@ namespace Luneraria.Core
{ {
if (!current.IsBuilding() || BuildingAttackable(current, gates)) if (!current.IsBuilding() || BuildingAttackable(current, gates))
{ {
if (!unit.IsStationary() || unit.Position.DistanceTo(current.Position) <= unit.AttackRange()) if (unit.Kind != "tower" || unit.Position.DistanceTo(current.Position) <= unit.AttackRange())
return current; return current;
} }
} }
double radius = unit.IsStationary() ? unit.AttackRange() : Balance.AggroRadius; double radius = unit.Kind == "tower" ? unit.AttackRange() : Balance.AggroRadius;
LuneUnit best = null; LuneUnit best = null;
double bestDistance = double.PositiveInfinity; double bestDistance = double.PositiveInfinity;
string priority = unit.Def.Priority; string priority = unit.Def.Priority;
@ -569,7 +505,7 @@ namespace Luneraria.Core
bestDistance = d; bestDistance = d;
} }
} }
if (best != null || unit.IsStationary()) if (best != null || unit.Kind == "tower")
return best; return best;
// Buildings: nearby attackable ones; at the end of a lane the whole base is in reach. // Buildings: nearby attackable ones; at the end of a lane the whole base is in reach.
for (int i = 0; i < buildings.Count; i++) for (int i = 0; i < buildings.Count; i++)
@ -612,8 +548,6 @@ namespace Luneraria.Core
{ {
if (unit.LeapTarget != null) if (unit.LeapTarget != null)
return new KeyValuePair<Vec2, double>(LeapLanding(unit, unit.LeapTarget), unit.Progress); return new KeyValuePair<Vec2, double>(LeapLanding(unit, unit.LeapTarget), unit.Progress);
if (unit.Kind == "camp")
return new KeyValuePair<Vec2, double>(unit.Position, unit.Progress);
double stepLength = unit.Speed() * Balance.MarchSpeedFactor * unit.MovementFactor() * dt; double stepLength = unit.Speed() * Balance.MarchSpeedFactor * unit.MovementFactor() * dt;
Vec2 goal = unit.Position; Vec2 goal = unit.Position;
double travel = 0.0; double travel = 0.0;
@ -713,7 +647,7 @@ namespace Luneraria.Core
{ {
unit.InRange = false; unit.InRange = false;
LuneUnit target = unit.Target; LuneUnit target = unit.Target;
if (target == null || !target.Alive || (unit.Kind == "core" && unit.Def.Damage <= 0.0)) if (target == null || !target.Alive || unit.Kind == "core")
{ {
unit.AttackProgress = 0.0; unit.AttackProgress = 0.0;
return; return;
@ -730,7 +664,7 @@ namespace Luneraria.Core
return; return;
} }
unit.InRange = true; unit.InRange = true;
double interval = unit.IsBuilding() ? Balance.TowerAttackInterval : 1.0; double interval = unit.Kind == "tower" ? Balance.TowerAttackInterval : 1.0;
unit.AttackProgress += dt * unit.AttackSpeedFactor() / interval; unit.AttackProgress += dt * unit.AttackSpeedFactor() / interval;
while (unit.AttackProgress >= 1.0 - 0.000001) while (unit.AttackProgress >= 1.0 - 0.000001)
{ {
@ -747,8 +681,6 @@ namespace Luneraria.Core
} }
if (unit.HitCount % 3 == 0 && unit.HasTalent("stammschlag")) if (unit.HitCount % 3 == 0 && unit.HasTalent("stammschlag"))
damage *= 1.6; damage *= 1.6;
if (unit.Ability() == "brunhild" && HeroLike(target))
damage *= BrunhildHunter;
if (unit.LeapStrike) if (unit.LeapStrike)
{ {
damage *= SylvaraLeapStrike; damage *= SylvaraLeapStrike;
@ -772,8 +704,6 @@ namespace Luneraria.Core
// The building gate applies to every source: ranged, splash, DoT and abilities. // The building gate applies to every source: ranged, splash, DoT and abilities.
if (target.IsBuilding() && !BuildingAttackable(target, gates)) if (target.IsBuilding() && !BuildingAttackable(target, gates))
return; return;
if (Glyphed(target))
return;
double current; double current;
_pendingDamage.TryGetValue(target.Id, out current); _pendingDamage.TryGetValue(target.Id, out current);
_pendingDamage[target.Id] = current + amount; _pendingDamage[target.Id] = current + amount;
@ -813,9 +743,6 @@ namespace Luneraria.Core
case "sylvara": case "sylvara":
unit.AbilityReady = SylvaraFirstThrow; unit.AbilityReady = SylvaraFirstThrow;
break; break;
case "brunhild":
unit.AbilityReady = BrunhildFirstSpear;
break;
} }
} }
@ -868,8 +795,6 @@ namespace Luneraria.Core
{ {
if (unit.Ability() == "sylvara") if (unit.Ability() == "sylvara")
SylvaraStep(unit, gates); SylvaraStep(unit, gates);
else if (unit.Ability() == "brunhild")
BrunhildStep(unit, gates);
double regen = unit.Def.Regen; double regen = unit.Def.Regen;
if (regen > 0.0 && unit.Hp < unit.MaxHp) if (regen > 0.0 && unit.Hp < unit.MaxHp)
QueueHeal(unit, unit.MaxHp * regen * Balance.SimStep); QueueHeal(unit, unit.MaxHp * regen * Balance.SimStep);
@ -976,9 +901,6 @@ namespace Luneraria.Core
case "sylvara": case "sylvara":
success = SylvaraThrow(unit, gates); success = SylvaraThrow(unit, gates);
break; break;
case "brunhild":
success = BrunhildSpear(unit, gates);
break;
} }
if (success) if (success)
{ {
@ -1114,97 +1036,6 @@ namespace Luneraria.Core
return true; return true;
} }
// H2 Brunhild
public const double BrunhildFirstSpear = 3.0;
public const double BrunhildSpearRange = 7.0;
public const double BrunhildHunter = 1.25;
public const double BrunhildShield = 0.30;
static bool HeroLike(LuneUnit unit) { return unit.Kind == "hero" || unit.Kind == "champion"; }
/// <summary>Enemies within the radius, farthest first (ties by id), mobile only.</summary>
List<LuneUnit> Farthest(LuneUnit unit, double radius, int maximum)
{
List<LuneUnit> result = NearbyMobile(unit, radius, 1000);
result.Reverse();
GdMath.SortStable(result, (a, b) =>
{
double ad = a.Position.DistanceTo(unit.Position);
double bd = b.Position.DistanceTo(unit.Position);
return GdMath.IsEqualApprox(ad, bd) ? a.Id < b.Id : ad > bd;
});
if (result.Count > maximum)
result.RemoveRange(maximum, result.Count - maximum);
return result;
}
/// <summary>Speerwurf: 120 % to the farthest enemy in 7 m (Speerregen: three), marked 4 s (+15 % damage taken).</summary>
bool BrunhildSpear(LuneUnit unit, Dictionary<string, bool> gates)
{
List<LuneUnit> victims = Farthest(unit, BrunhildSpearRange, unit.HasTalent("speerregen") ? 3 : 1);
if (victims.Count == 0)
return false;
double damage = unit.Def.Damage * (1.0 + unit.DamageBonus()) * 1.2;
foreach (LuneUnit victim in victims)
{
double hit = damage * (HeroLike(victim) ? BrunhildHunter * (unit.HasTalent("walkuerenblut") ? 2.0 : 1.0) : 1.0);
QueueDamage(victim, victim.Mitigated(hit, unit.Def.AttackType), gates);
AddEffect(victim, "valkyrie_mark", "vulnerable", 0.15, 4.0);
}
return true;
}
/// <summary>
/// Walkürensturz (once per combat after 2 s, by the planned rule): she leaps to her target
/// (an enemy hero or champion in 7 m first, then the marked enemy, then her current target),
/// deals 200 % to every enemy within 3 m of it and stuns them 1 s (Götterdämmerung: 2 s in
/// 4 m); bosses are not stunned. Decided on the state at the start of the step.
/// </summary>
void BrunhildStep(LuneUnit unit, Dictionary<string, bool> gates)
{
if (unit.UltimateUsed || string.IsNullOrEmpty(unit.Def.UltimateRule) || unit.CombatTime + 0.000001 < LuneContent.UltimateDelay)
return;
List<LuneUnit> reach = NearbyMobile(unit, BrunhildSpearRange, 1000);
if (reach.Count == 0)
return;
LuneUnit target = reach.Find(HeroLike) ?? reach.Find(other => other.Effects.ContainsKey("valkyrie_mark"))
?? (unit.Target != null && reach.Contains(unit.Target) ? unit.Target : reach[0]);
bool full = unit.HasTalent("goetterdaemmerung");
double radius = full ? 4.0 : 3.0;
bool release = false;
switch (unit.Def.UltimateRule)
{
case "gruppe":
release = Nearby(unit, false, target.Position, 4.0, 1000).FindAll(other => other.IsMobile()).Count >= 3;
break;
case "held":
release = HeroLike(target);
break;
case "notfall":
release = unit.Hp < unit.MaxHp * 0.4;
break;
case "sofort":
release = true;
break;
}
if (!release)
return;
double damage = unit.Def.Damage * (1.0 + unit.DamageBonus()) * 2.0;
foreach (LuneUnit victim in Nearby(unit, false, target.Position, radius, 1000))
{
if (!victim.IsMobile())
continue;
QueueDamage(victim, victim.Mitigated(damage * (HeroLike(victim) ? BrunhildHunter : 1.0), unit.Def.AttackType), gates);
if (!victim.IsBoss())
AddEffect(victim, "valkyrie_stun", "stun", 1.0, full ? 2.0 : 1.0);
}
// Walkürenschild: after the leap she is shielded like Sylvara after hers.
_pendingShields.Add(new PendingShield { Unit = unit, Amount = unit.MaxHp * BrunhildShield, Duration = 5.0 });
unit.LeapTarget = target;
unit.UltimateUsed = true;
unit.UltimateSerial += 1;
}
void OnHit(LuneUnit unit, LuneUnit primary, List<LuneUnit> living, Dictionary<string, bool> gates) void OnHit(LuneUnit unit, LuneUnit primary, List<LuneUnit> living, Dictionary<string, bool> gates)
{ {
string ability = unit.Ability(); string ability = unit.Ability();

View file

@ -1,163 +0,0 @@
using System.Collections.Generic;
namespace Luneraria.Core
{
/// <summary>
/// S3 fog of war (Dota 2 vision and wards, extended rules). It only decides what a viewer's
/// team sees; the simulation never reads it, so the rules and every replay stay the same.
/// In combat an enemy unit is visible within reach of an own living unit, an own living
/// building or one of the team's wards (a ward watches the middle of its lane). In planning,
/// enemy armies show only on scouted lanes: a ward there, or the enemy's outer tower of that
/// lane has fallen. Neutral units (camp guardians, the grotto boss) are always visible.
/// </summary>
public static class LuneFog
{
public const double UnitSight = 9.0;
public const double BuildingSight = 10.0;
public const double WardSight = 12.0;
struct Source
{
public Vec2 At;
public double Radius;
public string Zone;
}
/// <summary>The lane is scouted for the team in planning (ward, fallen enemy outer tower, or a ward in the own draft).</summary>
public static bool LaneScouted(LuneMatch match, int team, string lane, Draft ownDraft = null)
{
int index = LuneMap.LaneIndex(lane);
if (index < 0)
return false;
if (match.Boss.Wards[team * 3 + index] > 0)
return true;
if (ownDraft != null && ownDraft.WardLanes.Contains(lane))
return true;
foreach (BuildingDef definition in match.Map.Buildings)
{
if (definition.Team == 1 - team && definition.Lane == lane && definition.Tier == "outer")
return match.BuildingHp[definition.Id] <= 0.0;
}
return false;
}
/// <summary>
/// Planning (S3): the enemy's committed army on the lanes the team has scouted, as the
/// match will field it (troops and heroes, lane owner first). The planning preview shows
/// these; unscouted lanes show nothing of the enemy. Empty without the extended rules.
/// </summary>
public static List<LuneMatch.ArmyEntry> ScoutedEnemies(LuneMatch match, int team, Draft ownDraft = null)
{
var result = new List<LuneMatch.ArmyEntry>();
if (match == null || !match.Extended || team < 0 || team > 1)
return result;
int enemy = 1 - team;
foreach (string lane in LuneMap.Lanes)
{
if (!LaneScouted(match, team, lane, ownDraft))
continue;
for (int slot = enemy * 3; slot < enemy * 3 + 3; slot++)
{
foreach (LuneMatch.ArmyEntry entry in match.ArmyEntries(slot))
{
if (entry.Lane == lane)
result.Add(entry);
}
}
}
return result;
}
/// <summary>Middle of a lane, where a ward watches.</summary>
public static Vec2 WardPoint(LuneMatch match, string lane)
{
return match.Map.LanePoint(0, lane, match.Map.LanePaths[lane].Length * 0.5, 0.0);
}
/// <summary>
/// S4: everything that gives the team sight in the shown combat (or planning preview):
/// own living units, own standing buildings (from the match, so the planning preview has
/// them too) and the team's wards. Each as point, radius and zone.
/// </summary>
public static List<KeyValuePair<Vec2, double>> Vision(LuneMatch match, LuneCombat shown, int team, string zone = "lane")
{
var result = new List<KeyValuePair<Vec2, double>>();
if (match == null || team < 0 || team > 1)
return result;
if (shown != null)
{
foreach (LuneUnit unit in shown.Units)
{
if (unit.Team == team && unit.Alive && unit.IsMobile() && unit.Zone == zone)
result.Add(new KeyValuePair<Vec2, double>(unit.Position, UnitSight));
}
}
if (zone == "lane")
{
foreach (BuildingDef definition in match.Map.Buildings)
{
if (definition.Team == team && match.BuildingHp[definition.Id] > 0.0)
result.Add(new KeyValuePair<Vec2, double>(definition.Position, BuildingSight));
}
foreach (string lane in LuneMap.Lanes)
{
if (match.Boss.Wards[team * 3 + LuneMap.LaneIndex(lane)] > 0)
result.Add(new KeyValuePair<Vec2, double>(WardPoint(match, lane), WardSight));
}
}
return result;
}
/// <summary>
/// Ids of the enemy units the team cannot see in the shown combat (the live combat, or
/// the planning preview when planning). Empty without the extended rules or for team -1.
/// </summary>
public static HashSet<int> Hidden(LuneMatch match, LuneCombat shown, int team, bool planning, Draft ownDraft = null)
{
var hidden = new HashSet<int>();
if (match == null || shown == null || !match.Extended || team < 0 || team > 1)
return hidden;
if (planning)
{
foreach (LuneUnit unit in shown.Units)
{
if (unit.Team == 1 - team && unit.IsMobile() && (unit.Zone != "lane" || !LaneScouted(match, team, unit.Lane, ownDraft)))
hidden.Add(unit.Id);
}
return hidden;
}
var sources = new List<Source>();
foreach (LuneUnit unit in shown.Units)
{
if (unit.Team != team || !unit.Alive)
continue;
if (unit.IsMobile())
sources.Add(new Source { At = unit.Position, Radius = UnitSight, Zone = unit.Zone });
else if (unit.IsBuilding())
sources.Add(new Source { At = unit.Position, Radius = BuildingSight, Zone = "lane" });
}
foreach (string lane in LuneMap.Lanes)
{
if (match.Boss.Wards[team * 3 + LuneMap.LaneIndex(lane)] > 0)
sources.Add(new Source { At = WardPoint(match, lane), Radius = WardSight, Zone = "lane" });
}
foreach (LuneUnit unit in shown.Units)
{
if (unit.Team != 1 - team || !unit.IsMobile())
continue;
bool seen = false;
foreach (Source source in sources)
{
if (source.Zone == unit.Zone && unit.Position.DistanceTo(source.At) <= source.Radius)
{
seen = true;
break;
}
}
if (!seen)
hidden.Add(unit.Id);
}
return hidden;
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: c7893fc71c204c29bacd289a276d0be5

View file

@ -50,10 +50,6 @@ namespace Luneraria.Core
SeedValue = options.Seed; SeedValue = options.Seed;
Extended = options.Extended; Extended = options.Extended;
Boss.Blessing = new int[2]; Boss.Blessing = new int[2];
Boss.GlyphReady = new[] { 1, 1 };
Boss.CoreLevel = new int[2];
Boss.CampBuff = new int[6];
Boss.Wards = new int[6];
RoundNumber = 1; RoundNumber = 1;
Phase = Phase.Planning; Phase = Phase.Planning;
Winner = Ongoing; Winner = Ongoing;
@ -259,8 +255,6 @@ namespace Luneraria.Core
ItemSpec item; ItemSpec item;
if (!ContentData.Items.TryGetValue(itemId, out item)) if (!ContentData.Items.TryGetValue(itemId, out item))
return Result(false, "Unbekanntes Item."); return Result(false, "Unbekanntes Item.");
if (item.IsRecipe)
return Combine(hero, draft, item);
if (hero.Items.Contains(itemId)) if (hero.Items.Contains(itemId))
return Result(false, item.Name + " trägt dein Held bereits; Items sind einzigartig."); return Result(false, item.Name + " trägt dein Held bereits; Items sind einzigartig.");
if (hero.Items.Count >= LuneContent.ItemSlots) if (hero.Items.Count >= LuneContent.ItemSlots)
@ -326,48 +320,6 @@ namespace Luneraria.Core
hero.Skin = skin; hero.Skin = skin;
return Result(true, skin.Length == 0 ? "Standard-Aussehen gewählt." : "Skin gewählt."); return Result(true, skin.Length == 0 ? "Standard-Aussehen gewählt." : "Skin gewählt.");
} }
case "glyph":
{
if (!Extended)
return Result(false, "Die Glyphe gibt es nur mit erweiterten Regeln.");
string lane = command.Lane ?? "";
if (lane.Length > 0 && LuneMap.LaneIndex(lane) < 0)
return Result(false, "Unbekannte Lane.");
if (lane.Length > 0 && RoundNumber < Boss.GlyphReady[player.Team])
return Result(false, string.Format("Die Glyphe ist ab Runde {0} wieder bereit.", Boss.GlyphReady[player.Team]));
draft.GlyphLane = lane;
return Result(true, lane.Length == 0 ? "Glyphe zurückgenommen." : "Glyphe für " + LuneMap.LaneNames[lane] + " bereitgehalten.");
}
case "ward":
{
if (!Extended)
return Result(false, "Späher gibt es nur mit erweiterten Regeln.");
string lane = command.Lane ?? "";
int index = LuneMap.LaneIndex(lane);
if (index < 0)
return Result(false, "Unbekannte Lane.");
if (draft.WardLanes.Contains(lane))
return Result(false, "Auf dieser Lane steht schon dein Späher.");
if (draft.Gold < Balance.WardCost)
return Result(false, string.Format("Es fehlen {0} Gold.", Balance.WardCost - draft.Gold));
draft.Gold -= Balance.WardCost;
draft.WardLanes.Add(lane);
return Result(true, "Späher auf " + LuneMap.LaneNames[lane] + " gesetzt (" + Balance.WardRounds + " Runden).");
}
case "core_upgrade":
{
if (!Extended)
return Result(false, "Ancient-Stufen gibt es nur mit erweiterten Regeln.");
int level = Boss.CoreLevel[player.Team] + draft.CoreInvest;
if (level >= Balance.CoreLevelMax)
return Result(false, "Der Ancient hat die höchste Stufe.");
int cost = Balance.CoreUpgradeCost(level);
if (draft.Gold < cost)
return Result(false, string.Format("Es fehlen {0} Gold.", cost - draft.Gold));
draft.Gold -= cost;
draft.CoreInvest += 1;
return Result(true, string.Format("Ancient auf Stufe {0} gestärkt ({1} Gold).", level + 1, cost));
}
case "buyback": case "buyback":
{ {
if (!Extended) if (!Extended)
@ -507,31 +459,13 @@ namespace Luneraria.Core
claim.Lane = draft.BossLane; claim.Lane = draft.BossLane;
player.Undo.Clear(); player.Undo.Clear();
} }
if (Extended)
CommitTeamActions();
Combat = new LuneCombat(Map, Balance, RoundNumber); Combat = new LuneCombat(Map, Balance, RoundNumber);
foreach (BuildingDef definition in Map.Buildings) foreach (BuildingDef definition in Map.Buildings)
{ {
if (BuildingHp[definition.Id] > 0.0) if (BuildingHp[definition.Id] > 0.0)
{ Combat.AddBuilding(definition, BuildingHp[definition.Id]);
LuneUnit building = Combat.AddBuilding(definition, BuildingHp[definition.Id]);
int level = Extended && definition.Tier == "core" ? Boss.CoreLevel[definition.Team] : 0;
if (level > 0)
{
// S2 (Legion TD 2 king): the Ancient shoots at attackers in its reach.
building.Def.Damage = Balance.CoreDamage[GdMath.Min(level, Balance.CoreDamage.Length - 1)];
building.Def.AttackRange = Balance.CoreRange;
}
}
}
if (Extended)
{
for (int team = 0; team < 2; team++)
Combat.GlyphLane[team] = _glyphLane[team];
} }
SpawnWaves(); SpawnWaves();
if (Extended)
SpawnCamps();
SpawnChampions(); SpawnChampions();
SpawnDeployedBoss(); SpawnDeployedBoss();
SpawnArmies(); SpawnArmies();
@ -596,162 +530,10 @@ namespace Luneraria.Core
} }
} }
// ---------------------------------------------------------------- S3 extended rules
public static readonly string[] CampNames = { "Glutwächter", "Mondwächter", "Goldwächter" };
public static readonly string[] CampBuffNames = { "Glutsegen", "Mondschild", "Beute" };
/// <summary>S3: a camp round (League of Legends buff camps) has a guardian in the middle of every lane.</summary>
public bool CampRound(int round) { return Extended && round > 0 && round % GdMath.Max(1, Balance.CampEvery) == 0; }
public int CampBuffLeft(int team, string lane) { return Boss.CampBuff[team * 3 + LuneMap.LaneIndex(lane)]; }
/// <summary>A neutral guardian scaled like the waves of the round; the model is the Bruchhirsch.</summary>
public UnitDef CampDefinition(string lane)
{
UnitDef definition = DataCatalog.GetEnemy("e07");
definition.DisplayName = CampNames[LuneMap.LaneIndex(lane)];
definition.MaxHp = Balance.CampHp * LuneWaves.Strength(Balance, RoundNumber, Balance.WaveHpGrowth);
definition.Damage = Balance.CampDamage * LuneWaves.Strength(Balance, RoundNumber, Balance.WaveDamageGrowth);
definition.AttackRange = Balance.CampRange;
definition.Speed = 0.0;
definition.Bounty = 0;
definition.IsBoss = false;
definition.ViewScale = 1.35;
definition.Ability = "";
return definition;
}
void SpawnCamps()
{
if (!CampRound(RoundNumber))
return;
foreach (string lane in LuneMap.Lanes)
{
double middle = Map.LanePaths[lane].Length * 0.5;
LuneUnit camp = Combat.Spawn("camp", LuneCombat.Neutral, lane, CampDefinition(lane), middle, 0.0);
camp.Position = Map.LanePoint(0, lane, middle, 0.0);
camp.PreviousPosition = camp.Position;
}
}
/// <summary>Camp buffs on the army: Glutsegen (top) raises damage; Mondschild (mid) is applied as a shield at spawn.</summary>
UnitDef Buffed(UnitDef definition, int team)
{
if (Boss.CampBuff[team * 3] > 0)
definition.Damage *= 1.0 + Balance.CampDamageBuff;
return definition;
}
void CampShield(LuneUnit unit)
{
if (Extended && unit.Team < 2 && Boss.CampBuff[unit.Team * 3 + 1] > 0)
unit.GrantShield(unit.MaxHp * Balance.CampShieldBuff);
}
/// <summary>
/// Settlement of the camps: running buffs lose a combat, then every claimed camp gives its
/// lane's buff to the team that took it (Beute pays at once).
/// </summary>
void SettleCamps(int[] gains)
{
for (int i = 0; i < Boss.CampBuff.Length; i++)
Boss.CampBuff[i] = GdMath.Max(0, Boss.CampBuff[i] - 1);
for (int i = 0; i < Boss.Wards.Length; i++)
Boss.Wards[i] = GdMath.Max(0, Boss.Wards[i] - 1);
foreach (CombatEvent ev in Combat.Events)
{
if (ev.Kind != "camp" || ev.ClaimTeam < 0)
continue;
int lane = LuneMap.LaneIndex(ev.Lane);
if (lane == 2)
{
for (int slot = ev.ClaimTeam * 3; slot < ev.ClaimTeam * 3 + 3; slot++)
gains[slot] += Balance.CampLootGold;
}
else
{
Boss.CampBuff[ev.ClaimTeam * 3 + lane] = Balance.CampBuffRounds;
}
}
}
// ---------------------------------------------------------------- S2 extended rules
readonly string[] _glyphLane = { "", "" };
/// <summary>
/// Team actions of the drafts (S2): the glyph lane of the first player in slot order who
/// holds one, and the Ancient levels all players of a team bought (capped; the excess is
/// refunded). Each level repairs the Ancient by CoreRepair.
/// </summary>
void CommitTeamActions()
{
for (int team = 0; team < 2; team++)
{
_glyphLane[team] = "";
for (int slot = team * 3; slot < team * 3 + 3; slot++)
{
Player player = Players[slot];
// S3 wards: a new ward watches its lane for WardRounds combats.
foreach (string lane in player.Draft.WardLanes)
{
int index = team * 3 + LuneMap.LaneIndex(lane);
Boss.Wards[index] = GdMath.Max(Boss.Wards[index], Balance.WardRounds);
}
if (_glyphLane[team].Length == 0 && player.Draft.GlyphLane.Length > 0 && RoundNumber >= Boss.GlyphReady[team])
_glyphLane[team] = player.Draft.GlyphLane;
for (int i = 0; i < player.Draft.CoreInvest; i++)
{
int level = Boss.CoreLevel[team];
if (level >= Balance.CoreLevelMax)
{
player.Gold += Balance.CoreUpgradeCost(level - 1);
continue;
}
Boss.CoreLevel[team] = level + 1;
foreach (BuildingDef definition in Map.Buildings)
{
if (definition.Team == team && definition.Tier == "core" && BuildingHp[definition.Id] > 0.0)
BuildingHp[definition.Id] = GdMath.Min(definition.MaxHp, BuildingHp[definition.Id] + Balance.CoreRepair);
}
}
}
}
}
/// <summary>S2 recipe (Dota 2 / League of Legends): both parts and the fee become the recipe item, freeing a slot.</summary>
CommandResult Combine(HeroState hero, Draft draft, ItemSpec item)
{
if (!Extended)
return Result(false, "Rezepte gibt es nur mit erweiterten Regeln.");
if (hero.Items.Contains(item.Id))
return Result(false, item.Name + " trägt dein Held bereits; Items sind einzigartig.");
foreach (string part in item.Parts)
{
if (!hero.Items.Contains(part))
return Result(false, item.Name + " braucht " + string.Join(" und ", Array.ConvertAll(item.Parts, p => ContentData.Items[p].Name)) + ".");
}
if (LuneContent.LevelFor(hero.Xp) < item.MinLevel)
return Result(false, string.Format("{0} erfordert Stufe {1}.", item.Name, item.MinLevel));
if (draft.Gold < item.RecipeFee)
return Result(false, string.Format("Es fehlen {0} Gold.", item.RecipeFee - draft.Gold));
draft.Gold -= item.RecipeFee;
int at = hero.Items.IndexOf(item.Parts[0]);
foreach (string part in item.Parts)
{
hero.Items.Remove(part);
hero.FreshItems.Remove(part);
}
hero.Items.Insert(GdMath.Min(at, hero.Items.Count), item.Id);
return Result(true, item.Name + " geschmiedet.");
}
// ---------------------------------------------------------------- S1 extended rules // ---------------------------------------------------------------- S1 extended rules
/// <summary>Legion TD 2: income a booked champion adds for good.</summary> /// <summary>Legion TD 2: income a booked champion adds for good.</summary>
public int ChampionIncome(int cost) { return cost / GdMath.Max(1, Balance.ChampionIncomeDivisor); } public int ChampionIncome(int cost) { return cost / GdMath.Max(1, Balance.ChampionIncomeDivisor); }
/// <summary>S3: the defenders' bounty of a champion (halved under the extended rules).</summary>
public int ChampionBountyFor(int bounty) { return Extended ? (int)Math.Floor(bounty * Balance.ChampionBountyShare) : bounty; }
public int BuybackCost(HeroState hero) { return Balance.BuybackCost(LuneContent.LevelFor(hero.Xp)); } public int BuybackCost(HeroState hero) { return Balance.BuybackCost(LuneContent.LevelFor(hero.Xp)); }
/// <summary>Dota 2 barracks: the enemy inner tower of this lane has fallen, so this team's waves there are reinforced.</summary> /// <summary>Dota 2 barracks: the enemy inner tower of this lane has fallen, so this team's waves there are reinforced.</summary>
@ -819,8 +601,6 @@ namespace Luneraria.Core
if (booking.DeployRound == RoundNumber && booking.Lane == lane) if (booking.DeployRound == RoundNumber && booking.Lane == lane)
{ {
UnitDef definition = LuneContent.ChampionDefinition(booking.ChampionId, RoundNumber, Balance); UnitDef definition = LuneContent.ChampionDefinition(booking.ChampionId, RoundNumber, Balance);
if (Extended)
definition.Bounty = ChampionBountyFor(definition.Bounty);
Combat.Spawn("champion", team, lane, definition, 13.0, ((double)(index % 3) - 1.0) * 1.4, slot, booking.Uid); Combat.Spawn("champion", team, lane, definition, 13.0, ((double)(index % 3) - 1.0) * 1.4, slot, booking.Uid);
index += 1; index += 1;
} }
@ -922,10 +702,9 @@ namespace Luneraria.Core
ArmyEntry entry = ordered[index]; ArmyEntry entry = ordered[index];
int row = index / 4; int row = index / 4;
double lateral = ((double)(index % 4) - 1.5) * 1.2; double lateral = ((double)(index % 4) - 1.5) * 1.2;
LuneUnit spawned = Combat.Spawn(entry.Kind, team, lane, Extended ? Buffed(Blessed(entry.Definition, team, entry.Kind), team) : entry.Definition, LuneUnit spawned = Combat.Spawn(entry.Kind, team, lane, Extended ? Blessed(entry.Definition, team, entry.Kind) : entry.Definition,
GdMath.Max(0.4, 7.2 - (double)row * 1.5), lateral, entry.Slot, entry.Uid); GdMath.Max(0.4, 7.2 - (double)row * 1.5), lateral, entry.Slot, entry.Uid);
Exhaust(spawned); Exhaust(spawned);
CampShield(spawned);
} }
} }
} }
@ -943,9 +722,8 @@ namespace Luneraria.Core
{ {
ArmyEntry entry = ordered[index]; ArmyEntry entry = ordered[index];
Vec2 at = entryPoint - inward * (double)(index / 3) * 1.2 + across * ((double)(index % 3) - 1.0) * 1.1; Vec2 at = entryPoint - inward * (double)(index / 3) * 1.2 + across * ((double)(index % 3) - 1.0) * 1.1;
LuneUnit spawned = Combat.SpawnAt(entry.Kind, team, Grotto, Extended ? Buffed(Blessed(entry.Definition, team, entry.Kind), team) : entry.Definition, at, entry.Slot, entry.Uid); LuneUnit spawned = Combat.SpawnAt(entry.Kind, team, Grotto, Extended ? Blessed(entry.Definition, team, entry.Kind) : entry.Definition, at, entry.Slot, entry.Uid);
Exhaust(spawned); Exhaust(spawned);
CampShield(spawned);
expedition = true; expedition = true;
} }
} }
@ -1032,12 +810,6 @@ namespace Luneraria.Core
} }
if (Extended) if (Extended)
{ {
// S2 (Dota 2 glyph): only a glyph that fired goes on cooldown.
for (int team = 0; team < 2; team++)
{
if (Combat.GlyphUsed[team])
Boss.GlyphReady[team] = RoundNumber + 1 + Balance.GlyphCooldown;
}
// S1 (Dota 2 buyback, adapted): a hero that fell is exhausted for the next combat. // S1 (Dota 2 buyback, adapted): a hero that fell is exhausted for the next combat.
foreach (LuneUnit unit in Combat.Units) foreach (LuneUnit unit in Combat.Units)
{ {
@ -1045,8 +817,6 @@ namespace Luneraria.Core
Players[unit.OwnerSlot].Hero.Exhausted = !unit.Alive; Players[unit.OwnerSlot].Hero.Exhausted = !unit.Alive;
} }
} }
if (Extended)
SettleCamps(gains);
int income = Balance.RoundIncome(RoundNumber); int income = Balance.RoundIncome(RoundNumber);
foreach (Player player in Players) foreach (Player player in Players)
{ {
@ -1124,7 +894,6 @@ namespace Luneraria.Core
// S1 (League of Legends dragon souls): each conquest blesses the team for good. // S1 (League of Legends dragon souls): each conquest blesses the team for good.
if (Extended) if (Extended)
Boss.Blessing[team] = GdMath.Min(Balance.BlessingMax, Boss.Blessing[team] + 1); Boss.Blessing[team] = GdMath.Min(Balance.BlessingMax, Boss.Blessing[team] + 1);
// (S2 glyph cooldowns are settled in Settle.)
result = "won"; result = "won";
} }
else if (Combat.GrottoOutcome == "failed") else if (Combat.GrottoOutcome == "failed")

View file

@ -30,7 +30,7 @@ namespace Luneraria.Core
{ {
/// <summary>Attack type (row) against armour type (column): Klinge, Durchschlag, Impuls.</summary> /// <summary>Attack type (row) against armour type (column): Klinge, Durchschlag, Impuls.</summary>
public static readonly double[,] DamageMatrix = { { 1.30, 0.75, 1.00 }, { 1.00, 1.30, 0.75 }, { 0.75, 1.00, 1.30 } }; public static readonly double[,] DamageMatrix = { { 1.30, 0.75, 1.00 }, { 1.00, 1.30, 0.75 }, { 0.75, 1.00, 1.30 } };
public static readonly string[] MobileKinds = { "troop", "hero", "monster", "champion", "boss", "camp" }; public static readonly string[] MobileKinds = { "troop", "hero", "monster", "champion", "boss" };
/// <summary>Sequential per combat, assigned in spawn order (deterministic).</summary> /// <summary>Sequential per combat, assigned in spawn order (deterministic).</summary>
public int Id; public int Id;
@ -79,20 +79,12 @@ namespace Luneraria.Core
public int AbilitySerial; public int AbilitySerial;
public int LeapSerial; public int LeapSerial;
public int UltimateSerial; public int UltimateSerial;
/// <summary>S4: stunned as reported by the host's frame (guests have no effects); presentation only.</summary>
public bool NetStunned;
/// <summary>Stunned for the view: the own effect on the host, the frame flag on a guest.</summary>
public bool ShowsStun() { return NetStunned || Stunned(); }
public double HitFlash; public double HitFlash;
public bool InRange; public bool InRange;
public double DeathTime = -1.0; public double DeathTime = -1.0;
public bool IsBuilding() { return Kind == "tower" || Kind == "core"; } public bool IsBuilding() { return Kind == "tower" || Kind == "core"; }
/// <summary>Buildings and S3 camp guardians stay where they are and only fight in reach.</summary>
public bool IsStationary() { return IsBuilding() || Kind == "camp"; }
public bool IsMobile() { return Array.IndexOf(MobileKinds, Kind) >= 0; } public bool IsMobile() { return Array.IndexOf(MobileKinds, Kind) >= 0; }
public double AttackRange() { return Def.AttackRange; } public double AttackRange() { return Def.AttackRange; }
@ -108,8 +100,7 @@ namespace Luneraria.Core
public double Mitigated(double raw, int attackType) public double Mitigated(double raw, int attackType)
{ {
double value = GdMath.Max(0.0, raw) * DamageMatrix[GdMath.Clamp(attackType, 0, 2), ArmorType()]; double value = GdMath.Max(0.0, raw) * DamageMatrix[GdMath.Clamp(attackType, 0, 2), ArmorType()];
// H2: a unit marked by Brunhild's spear takes more damage ("vulnerable"; 0 for everyone else). return value * (1.0 - DamageReduction());
return value * (1.0 - DamageReduction()) * (1.0 + StrongestEffect("vulnerable"));
} }
/// <summary>Applies an already mitigated amount through the shield.</summary> /// <summary>Applies an already mitigated amount through the shield.</summary>
@ -215,13 +206,8 @@ namespace Luneraria.Core
return GdMath.Min(0.35, result + Def.ReductionBonus); return GdMath.Min(0.35, result + Def.ReductionBonus);
} }
/// <summary>H2: Brunhild's stun stops attacks and movement for its duration.</summary>
public bool Stunned() { return StrongestEffect("stun") > 0.0; }
public double AttackSpeedFactor() public double AttackSpeedFactor()
{ {
if (Stunned())
return 0.0;
double slowing = StrongestEffect("attack_slow") * (IsBoss() ? 0.5 : 1.0); double slowing = StrongestEffect("attack_slow") * (IsBoss() ? 0.5 : 1.0);
double bonus = Def.AttackSpeedBonus; double bonus = Def.AttackSpeedBonus;
return 1.0 + GdMath.Clamp(StrongestEffect("attack_speed") - slowing + bonus, -0.25, 0.5); return 1.0 + GdMath.Clamp(StrongestEffect("attack_speed") - slowing + bonus, -0.25, 0.5);
@ -233,8 +219,6 @@ namespace Luneraria.Core
public double MovementFactor() public double MovementFactor()
{ {
if (Stunned())
return 0.0;
return 1.0 - GdMath.Min(0.35, StrongestEffect("movement_slow") * (IsBoss() ? 0.5 : 1.0)); return 1.0 - GdMath.Min(0.35, StrongestEffect("movement_slow") * (IsBoss() ? 0.5 : 1.0));
} }

View file

@ -75,17 +75,10 @@ namespace Luneraria.Core
public List<Booking> NewBookings = new List<Booking>(); public List<Booking> NewBookings = new List<Booking>();
/// <summary>Lane chosen by the boss chooser in this planning, "" if unchanged.</summary> /// <summary>Lane chosen by the boss chooser in this planning, "" if unchanged.</summary>
public string BossLane = ""; public string BossLane = "";
/// <summary>S2: lane the player holds the team glyph ready for, "" = none.</summary>
public string GlyphLane = "";
/// <summary>S2: Ancient levels this player buys in this planning.</summary>
public int CoreInvest;
/// <summary>S3: lanes the player places a ward on in this planning.</summary>
public List<string> WardLanes = new List<string>();
public Draft Clone() public Draft Clone()
{ {
var copy = new Draft { Gold = Gold, NextSeq = NextSeq, BossLane = BossLane, GlyphLane = GlyphLane, CoreInvest = CoreInvest, Hero = Hero != null ? Hero.Clone() : null, var copy = new Draft { Gold = Gold, NextSeq = NextSeq, BossLane = BossLane, Hero = Hero != null ? Hero.Clone() : null };
WardLanes = new List<string>(WardLanes) };
foreach (RosterEntry entry in Roster) foreach (RosterEntry entry in Roster)
copy.Roster.Add(entry.Clone()); copy.Roster.Add(entry.Clone());
foreach (Booking booking in NewBookings) foreach (Booking booking in NewBookings)
@ -137,14 +130,6 @@ namespace Luneraria.Core
public int SettledRound; public int SettledRound;
/// <summary>S1 (extended rules): Mondsegen stacks per team from grotto victories (League of Legends dragon souls).</summary> /// <summary>S1 (extended rules): Mondsegen stacks per team from grotto victories (League of Legends dragon souls).</summary>
public int[] Blessing = new int[2]; public int[] Blessing = new int[2];
/// <summary>S2 (extended rules): first round each team's glyph can be held again (Dota 2).</summary>
public int[] GlyphReady = { 1, 1 };
/// <summary>S2 (extended rules): Ancient level per team, 0..3 (Legion TD 2 king upgrades).</summary>
public int[] CoreLevel = new int[2];
/// <summary>S3 (extended rules): combats left of the camp buffs, index team × 3 + lane (top Glutsegen, mid Mondschild).</summary>
public int[] CampBuff = new int[6];
/// <summary>S3 (extended rules): rounds left of each team's ward per lane (team × 3 + lane).</summary>
public int[] Wards = new int[6];
} }
public sealed class BossLogEntry public sealed class BossLogEntry
@ -269,11 +254,5 @@ namespace Luneraria.Core
public static Command HeroSkin(string skin) { return new Command { Type = "hero_skin", TypeId = skin }; } public static Command HeroSkin(string skin) { return new Command { Type = "hero_skin", TypeId = skin }; }
/// <summary>S1: buys an exhausted hero back to full strength for the next combat.</summary> /// <summary>S1: buys an exhausted hero back to full strength for the next combat.</summary>
public static Command Buyback() { return new Command { Type = "buyback" }; } public static Command Buyback() { return new Command { Type = "buyback" }; }
/// <summary>S2: hold the team glyph for a lane in the coming combat ("" withdraws it).</summary>
public static Command Glyph(string lane) { return new Command { Type = "glyph", Lane = lane }; }
/// <summary>S2: buy the next Ancient level for the team.</summary>
public static Command CoreUpgrade() { return new Command { Type = "core_upgrade" }; }
/// <summary>S3: place a ward on a lane (fog of war).</summary>
public static Command Ward(string lane) { return new Command { Type = "ward", Lane = lane }; }
} }
} }

View file

@ -48,7 +48,6 @@ namespace Luneraria.EditorTools
} }
library.HeroRing = Marker("Ring_Hero", LuneMaterialFactory.Hex("f2c96b"), 2.2f, false); library.HeroRing = Marker("Ring_Hero", LuneMaterialFactory.Hex("f2c96b"), 2.2f, false);
library.MoonBurst = Marker("Ring_MoonBurst", LuneMaterialFactory.Hex("8fb8ff"), 4f, false); library.MoonBurst = Marker("Ring_MoonBurst", LuneMaterialFactory.Hex("8fb8ff"), 4f, false);
library.FogOverlay = FogMaterial();
for (int team = 0; team < 2; team++) for (int team = 0; team < 2; team++)
{ {
library.OwnerRing[team] = Marker("Ring_Owner_" + team, LuneMaterialFactory.TeamColor[team], 3f, false); library.OwnerRing[team] = Marker("Ring_Owner_" + team, LuneMaterialFactory.TeamColor[team], 3f, false);
@ -181,24 +180,6 @@ namespace Luneraria.EditorTools
return existing; return existing;
} }
/// <summary>S4: the fog of war overlay material (night blue, alpha from the grid's vertices).</summary>
static Material FogMaterial()
{
string path = LuneMaterialFactory.Folder + "/Markers/Fog_Overlay.mat";
Directory.CreateDirectory(Path.GetDirectoryName(path));
Shader shader = Shader.Find("Luneraria/FogOverlay");
var material = AssetDatabase.LoadAssetAtPath<Material>(path);
if (material == null)
{
material = new Material(shader);
AssetDatabase.CreateAsset(material, path);
}
material.shader = shader;
material.SetColor("_Color", new Color(.03f, .04f, .08f, 1f));
EditorUtility.SetDirty(material);
return material;
}
/// <summary>Unshaded marker colour (Godot _flat with emission energy); bars draw on top.</summary> /// <summary>Unshaded marker colour (Godot _flat with emission energy); bars draw on top.</summary>
static Material Marker(string name, Color srgb, float energy, bool onTop) static Material Marker(string name, Color srgb, float energy, bool onTop)
{ {

View file

@ -189,14 +189,6 @@ MonoBehaviour:
Neutral: {fileID: 2100000, guid: 2844b8628dab84e70bea79fbffa9958f, type: 2} Neutral: {fileID: 2100000, guid: 2844b8628dab84e70bea79fbffa9958f, type: 2}
Blau: {fileID: 0} Blau: {fileID: 0}
Rot: {fileID: 0} Rot: {fileID: 0}
- Key: AI_Brunhild
Neutral: {fileID: 2100000, guid: 86538db76bda54b19acbf73c7e1e6432, type: 2}
Blau: {fileID: 0}
Rot: {fileID: 0}
- Key: AI_BrunhildSpeer
Neutral: {fileID: 2100000, guid: 683e7a7f8c9ff4bafbe24e22287ad674, type: 2}
Blau: {fileID: 0}
Rot: {fileID: 0}
- Key: AI_Funkenlanze - Key: AI_Funkenlanze
Neutral: {fileID: 2100000, guid: d5a88e3321a884b30bd8a8606b78090d, type: 2} Neutral: {fileID: 2100000, guid: d5a88e3321a884b30bd8a8606b78090d, type: 2}
Blau: {fileID: 0} Blau: {fileID: 0}
@ -753,14 +745,6 @@ MonoBehaviour:
Neutral: {fileID: 2100000, guid: 09c239a4cf52e4af4969869c24b078ff, type: 2} Neutral: {fileID: 2100000, guid: 09c239a4cf52e4af4969869c24b078ff, type: 2}
Blau: {fileID: 0} Blau: {fileID: 0}
Rot: {fileID: 0} Rot: {fileID: 0}
- Key: AI_Brunhild_Eiswalkuere
Neutral: {fileID: 2100000, guid: 8058295d8bee34a8b831603ad53f5501, type: 2}
Blau: {fileID: 0}
Rot: {fileID: 0}
- Key: AI_BrunhildSpeer_Eiswalkuere
Neutral: {fileID: 2100000, guid: 40b5dbee4b87547ef9b92e96deeb30a6, type: 2}
Blau: {fileID: 0}
Rot: {fileID: 0}
- Key: AI_Sylvara_Blutmond - Key: AI_Sylvara_Blutmond
Neutral: {fileID: 2100000, guid: aa1c3bf7df6bd46ddbe3d0c8051d5b82, type: 2} Neutral: {fileID: 2100000, guid: aa1c3bf7df6bd46ddbe3d0c8051d5b82, type: 2}
Blau: {fileID: 0} Blau: {fileID: 0}

View file

@ -1,31 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Fog_Overlay
m_Shader: {fileID: 4800000, guid: 7633b9f391704a8f84ae742a82128282, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs: []
m_Ints: []
m_Floats: []
m_Colors:
- _Color: {r: 0.03, g: 0.04, b: 0.08, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View file

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 7846e5bf0b03a456ab8720cbca2c8d9d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,68 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: AI_Brunhild
m_Shader: {fileID: 4800000, guid: 9809bd5750e264586bd30e14a6f1bd4e, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 0
m_EnableInstancingVariants: 1
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _AlbedoMap:
m_Texture: {fileID: 2800000, guid: 0dbeeb8c48f344e64962b71d5e9fcca1, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _NormalMap:
m_Texture: {fileID: 2800000, guid: f701c67235f354e4dac0ee3235b94b3d, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OrmMap:
m_Texture: {fileID: 2800000, guid: 1722b2d9a084347b8845372bf878a61f, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _StoneMap:
m_Texture: {fileID: 2800000, guid: 2e11a30a0df8143ab84722ec0a116b6d, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _Cull: 2
- _Energy: 1
- _Flicker: 0
- _Glow: 1.25
- _Metallic: 1
- _Mode: 0
- _NormalStrength: 1
- _ObjectSpace: 0
- _Quality: 1
- _Relief: 0.016
- _Rim: 0.35
- _Roughness: 1
- _Seed: 0
- _ShadowNormalScale: 0
- _StoneScale: 1
- _UvMaps: 1
- _VertexColor: 0
m_Colors:
- _AlbedoMean: {r: 1, g: 1, b: 1, a: 1}
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _OrmMean: {r: 1, g: 1, b: 1, a: 1}
- _RimColor: {r: 0.8, g: 0.86, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View file

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 86538db76bda54b19acbf73c7e1e6432
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,68 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: AI_BrunhildSpeer
m_Shader: {fileID: 4800000, guid: 9809bd5750e264586bd30e14a6f1bd4e, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 0
m_EnableInstancingVariants: 1
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _AlbedoMap:
m_Texture: {fileID: 2800000, guid: 336afb236cf9f438dae9687f76420327, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _NormalMap:
m_Texture: {fileID: 2800000, guid: 9b19528e38c1f468a8dfb54927968778, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OrmMap:
m_Texture: {fileID: 2800000, guid: cf3a5ed7600ba42349191e05cfe011e8, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _StoneMap:
m_Texture: {fileID: 2800000, guid: 2e11a30a0df8143ab84722ec0a116b6d, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _Cull: 2
- _Energy: 1
- _Flicker: 0
- _Glow: 1.25
- _Metallic: 1
- _Mode: 0
- _NormalStrength: 1
- _ObjectSpace: 0
- _Quality: 1
- _Relief: 0.016
- _Rim: 0.35
- _Roughness: 1
- _Seed: 0
- _ShadowNormalScale: 0
- _StoneScale: 1
- _UvMaps: 1
- _VertexColor: 0
m_Colors:
- _AlbedoMean: {r: 1, g: 1, b: 1, a: 1}
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _OrmMean: {r: 1, g: 1, b: 1, a: 1}
- _RimColor: {r: 0.8, g: 0.86, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View file

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 683e7a7f8c9ff4bafbe24e22287ad674
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,68 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: AI_BrunhildSpeer_Eiswalkuere
m_Shader: {fileID: 4800000, guid: 9809bd5750e264586bd30e14a6f1bd4e, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 0
m_EnableInstancingVariants: 1
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _AlbedoMap:
m_Texture: {fileID: 2800000, guid: 1fadc4bab123948b3b3f54a2fe52173a, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _NormalMap:
m_Texture: {fileID: 2800000, guid: 9b19528e38c1f468a8dfb54927968778, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OrmMap:
m_Texture: {fileID: 2800000, guid: cf3a5ed7600ba42349191e05cfe011e8, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _StoneMap:
m_Texture: {fileID: 2800000, guid: 2e11a30a0df8143ab84722ec0a116b6d, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _Cull: 2
- _Energy: 1
- _Flicker: 0
- _Glow: 1.25
- _Metallic: 1
- _Mode: 0
- _NormalStrength: 1
- _ObjectSpace: 0
- _Quality: 1
- _Relief: 0.016
- _Rim: 0.35
- _Roughness: 1
- _Seed: 0
- _ShadowNormalScale: 0
- _StoneScale: 1
- _UvMaps: 1
- _VertexColor: 0
m_Colors:
- _AlbedoMean: {r: 1, g: 1, b: 1, a: 1}
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _OrmMean: {r: 1, g: 1, b: 1, a: 1}
- _RimColor: {r: 0.8, g: 0.86, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View file

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 40b5dbee4b87547ef9b92e96deeb30a6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,68 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: AI_Brunhild_Eiswalkuere
m_Shader: {fileID: 4800000, guid: 9809bd5750e264586bd30e14a6f1bd4e, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 0
m_EnableInstancingVariants: 1
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _AlbedoMap:
m_Texture: {fileID: 2800000, guid: ed6f51893270d4a81a491e1b895a5815, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _NormalMap:
m_Texture: {fileID: 2800000, guid: f701c67235f354e4dac0ee3235b94b3d, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OrmMap:
m_Texture: {fileID: 2800000, guid: 1722b2d9a084347b8845372bf878a61f, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _StoneMap:
m_Texture: {fileID: 2800000, guid: 2e11a30a0df8143ab84722ec0a116b6d, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _Cull: 2
- _Energy: 1
- _Flicker: 0
- _Glow: 1.25
- _Metallic: 1
- _Mode: 0
- _NormalStrength: 1
- _ObjectSpace: 0
- _Quality: 1
- _Relief: 0.016
- _Rim: 0.35
- _Roughness: 1
- _Seed: 0
- _ShadowNormalScale: 0
- _StoneScale: 1
- _UvMaps: 1
- _VertexColor: 0
m_Colors:
- _AlbedoMean: {r: 1, g: 1, b: 1, a: 1}
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _OrmMean: {r: 1, g: 1, b: 1, a: 1}
- _RimColor: {r: 0.8, g: 0.86, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View file

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 8058295d8bee34a8b831603ad53f5501
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -226,15 +226,6 @@ namespace Luneraria.Net
} }
Int(text, boss.Blessing != null && boss.Blessing.Length > 0 ? boss.Blessing[0] : 0); Int(text, boss.Blessing != null && boss.Blessing.Length > 0 ? boss.Blessing[0] : 0);
Int(text, boss.Blessing != null && boss.Blessing.Length > 1 ? boss.Blessing[1] : 0); Int(text, boss.Blessing != null && boss.Blessing.Length > 1 ? boss.Blessing[1] : 0);
for (int team = 0; team < 2; team++)
{
Int(text, boss.GlyphReady != null && boss.GlyphReady.Length > team ? boss.GlyphReady[team] : 1);
Int(text, boss.CoreLevel != null && boss.CoreLevel.Length > team ? boss.CoreLevel[team] : 0);
}
for (int i = 0; i < 6; i++)
Int(text, boss.CampBuff != null && boss.CampBuff.Length > i ? boss.CampBuff[i] : 0);
for (int i = 0; i < 6; i++)
Int(text, boss.Wards != null && boss.Wards.Length > i ? boss.Wards[i] : 0);
var cores = state.DestroyedCores ?? new List<int>(); var cores = state.DestroyedCores ?? new List<int>();
Int(text, cores.Count); Int(text, cores.Count);
foreach (int core in cores) foreach (int core in cores)
@ -287,9 +278,6 @@ namespace Luneraria.Net
Int(text, draft.Gold); Int(text, draft.Gold);
Int(text, draft.NextSeq); Int(text, draft.NextSeq);
Str(text, draft.BossLane ?? ""); Str(text, draft.BossLane ?? "");
Str(text, draft.GlyphLane ?? "");
Int(text, draft.CoreInvest);
Str(text, string.Join("\x1d", draft.WardLanes));
WriteRosterList(text, draft.Roster); WriteRosterList(text, draft.Roster);
Int(text, draft.Hero != null ? 1 : 0); Int(text, draft.Hero != null ? 1 : 0);
if (draft.Hero != null) if (draft.Hero != null)
@ -669,15 +657,6 @@ namespace Luneraria.Net
}; };
} }
state.Boss.Blessing = new[] { Integer(), Integer() }; state.Boss.Blessing = new[] { Integer(), Integer() };
for (int team = 0; team < 2; team++)
{
state.Boss.GlyphReady[team] = Integer(1);
state.Boss.CoreLevel[team] = Integer();
}
for (int i = 0; i < 6; i++)
state.Boss.CampBuff[i] = Integer();
for (int i = 0; i < 6; i++)
state.Boss.Wards[i] = Integer();
int coreCount = Integer(); int coreCount = Integer();
for (int i = 0; i < coreCount; i++) for (int i = 0; i < coreCount; i++)
state.DestroyedCores.Add(Integer()); state.DestroyedCores.Add(Integer());
@ -726,8 +705,7 @@ namespace Luneraria.Net
row.Bookings = ReadBookingList(); row.Bookings = ReadBookingList();
if (Flag()) if (Flag())
{ {
var draft = new Draft { Gold = Integer(), NextSeq = Integer(), BossLane = Text() ?? "", GlyphLane = Text() ?? "", CoreInvest = Integer() }; var draft = new Draft { Gold = Integer(), NextSeq = Integer(), BossLane = Text() ?? "" };
AddSplit(draft.WardLanes, Text());
draft.Roster = ReadRosterList(); draft.Roster = ReadRosterList();
if (Flag()) if (Flag())
draft.Hero = ReadHero(); draft.Hero = ReadHero();

File diff suppressed because it is too large Load diff

View file

@ -25,9 +25,6 @@ MonoBehaviour:
- Id: basaltbastion_meshy - Id: basaltbastion_meshy
Prefab: {fileID: 919132149155446097, guid: aa5734da897ea44808385f37d34070bf, type: 3} Prefab: {fileID: 919132149155446097, guid: aa5734da897ea44808385f37d34070bf, type: 3}
Controller: {fileID: 9100000, guid: a456a0a7fa6d34b699183f75137f8f86, type: 2} Controller: {fileID: 9100000, guid: a456a0a7fa6d34b699183f75137f8f86, type: 2}
- Id: brunhild
Prefab: {fileID: 919132149155446097, guid: 29d894f788c5f4fbbacf819091c19542, type: 3}
Controller: {fileID: 9100000, guid: 8f779b941008d48fea9cfb256567bb9d, type: 2}
- Id: e01 - Id: e01
Prefab: {fileID: 919132149155446097, guid: 940635ed8b965461daabecd5cafdba92, type: 3} Prefab: {fileID: 919132149155446097, guid: 940635ed8b965461daabecd5cafdba92, type: 3}
Controller: {fileID: 0} Controller: {fileID: 0}
@ -273,7 +270,6 @@ MonoBehaviour:
- {fileID: 2100000, guid: 77d940cdf3d6043cb93c647607c3e35e, type: 2} - {fileID: 2100000, guid: 77d940cdf3d6043cb93c647607c3e35e, type: 2}
HeroRing: {fileID: 2100000, guid: 862d58435f9824d969aecf8551257a64, type: 2} HeroRing: {fileID: 2100000, guid: 862d58435f9824d969aecf8551257a64, type: 2}
MoonBurst: {fileID: 2100000, guid: c79261fd1aa5e42479782d3bac53f7ba, type: 2} MoonBurst: {fileID: 2100000, guid: c79261fd1aa5e42479782d3bac53f7ba, type: 2}
FogOverlay: {fileID: 2100000, guid: 7846e5bf0b03a456ab8720cbca2c8d9d, type: 2}
BarBack: {fileID: 2100000, guid: 8efe289a463614fee8dcb19e789f2bf8, type: 2} BarBack: {fileID: 2100000, guid: 8efe289a463614fee8dcb19e789f2bf8, type: 2}
BarFill: BarFill:
- {fileID: 2100000, guid: 778bc4f74208340e38dc099b897f3db3, type: 2} - {fileID: 2100000, guid: 778bc4f74208340e38dc099b897f3db3, type: 2}

View file

@ -1,39 +0,0 @@
// S4 fog of war: a ground-hugging grid whose vertex alpha darkens what the viewer's team cannot see.
Shader "Luneraria/FogOverlay"
{
Properties
{
_Color("Fog colour", Color) = (0.03, 0.04, 0.07, 1)
}
SubShader
{
Tags { "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" "Queue" = "Transparent-50" }
Pass
{
Name "Fog"
Tags { "LightMode" = "UniversalForward" }
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
ZTest LEqual
Cull Off
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
half4 _Color;
CBUFFER_END
struct Attributes { float4 positionOS : POSITION; half4 color : COLOR; };
struct Varyings { float4 positionCS : SV_POSITION; half alpha : TEXCOORD0; };
Varyings Vert(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.alpha = input.color.a;
return output;
}
half4 Frag(Varyings input) : SV_Target { return half4(_Color.rgb, _Color.a * input.alpha); }
ENDHLSL
}
}
}

View file

@ -1,9 +0,0 @@
fileFormatVersion: 2
guid: 7633b9f391704a8f84ae742a82128282
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View file

@ -50,7 +50,7 @@ namespace Luneraria.Core.Tests
public static IList<CheckSuite> All() public static IList<CheckSuite> All()
{ {
return new List<CheckSuite> { new ContentSuite(), new CoreRulesSuite(), new M2RulesSuite(), new GoldenSuite(), new DeterminismSuite(), new NetSuite(), new NetRoundsSuite(), new H1HeroSuite(), new S1RulesSuite(), new S2RulesSuite(), new S3RulesSuite(), new H2HeroSuite(), new S4NetSuite() }; return new List<CheckSuite> { new ContentSuite(), new CoreRulesSuite(), new M2RulesSuite(), new GoldenSuite(), new DeterminismSuite(), new NetSuite(), new NetRoundsSuite(), new H1HeroSuite(), new S1RulesSuite() };
} }
} }
} }

View file

@ -90,7 +90,7 @@ namespace Luneraria.Core.Tests
// Heroes added after the port (H1) follow the reference heroes; those must match exactly. // Heroes added after the port (H1) follow the reference heroes; those must match exactly.
Check(expected.Keys.SequenceEqual(ContentData.ReferenceHeroes) && ContentData.Heroes.Keys.Take(ContentData.ReferenceHeroes.Length).SequenceEqual(ContentData.ReferenceHeroes), Check(expected.Keys.SequenceEqual(ContentData.ReferenceHeroes) && ContentData.Heroes.Keys.Take(ContentData.ReferenceHeroes.Length).SequenceEqual(ContentData.ReferenceHeroes),
"heroes: same reference ids in the same order, later heroes after them"); "heroes: same reference ids in the same order, later heroes after them");
Check(ContentData.Heroes.Keys.Skip(ContentData.ReferenceHeroes.Length).SequenceEqual(new[] { "held_sylvara", "held_brunhild" }), "heroes: exactly the documented later heroes (H1 Sylvara, H2 Brunhild)"); Check(ContentData.Heroes.Keys.Skip(ContentData.ReferenceHeroes.Length).SequenceEqual(new[] { "held_sylvara" }), "heroes: exactly the documented later heroes (H1: held_sylvara)");
foreach (KeyValuePair<string, object> pair in expected) foreach (KeyValuePair<string, object> pair in expected)
{ {
var entry = MiniJson.Obj(pair.Value); var entry = MiniJson.Obj(pair.Value);
@ -116,10 +116,7 @@ namespace Luneraria.Core.Tests
void CompareItems(OrderedMap<string, object> expected) void CompareItems(OrderedMap<string, object> expected)
{ {
// S2 recipe items follow the reference items; those must match exactly. Check(expected.Keys.SequenceEqual(ContentData.Items.Keys), "items: same ids in the same order");
Check(expected.Keys.SequenceEqual(ContentData.ReferenceItems) && ContentData.Items.Keys.Take(ContentData.ReferenceItems.Length).SequenceEqual(ContentData.ReferenceItems),
"items: same reference ids in the same order, recipes after them");
Check(ContentData.Items.Values.Skip(ContentData.ReferenceItems.Length).All(item => item.IsRecipe), "items after the reference are recipes (S2)");
foreach (KeyValuePair<string, object> pair in expected) foreach (KeyValuePair<string, object> pair in expected)
{ {
var entry = MiniJson.Obj(pair.Value); var entry = MiniJson.Obj(pair.Value);

View file

@ -1,148 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace Luneraria.Core.Tests
{
/// <summary>
/// H2: Brunhild, the Valkyrie. Purchase and rules, hunter passive, spear with mark, the leap
/// with area damage and stun (bosses immune), talents, skin, bot profile; reference untouched.
/// </summary>
public sealed class H2HeroSuite : CheckSuite
{
public override string Name { get { return "h2"; } }
protected override void RunChecks()
{
TestPurchase();
TestSpear();
TestUltimate();
TestPassiveAndTalents();
TestBots();
}
static LuneCombat Arena(HeroState hero, out LuneUnit brunhild, params KeyValuePair<string, double>[] enemies)
{
var balance = LuneBalance.Default();
var combat = new LuneCombat(new LuneMap(balance), balance, 5);
UnitDef definition = LuneContent.HeroDefinition(hero);
definition.Role = "melee";
brunhild = combat.Spawn("hero", 0, "mid", definition, 40.0, 0.0, 0, "h0-1");
Vec2 forward = combat.Map.LaneForward(0, "mid", 40.0);
foreach (KeyValuePair<string, double> enemy in enemies)
{
bool isHero = enemy.Key == "held";
UnitDef def = isHero ? LuneContent.HeroDefinition(new HeroState { HeroId = "held_ringborke" }) : ContentData.Enemies[enemy.Key].Clone();
def.MaxHp = 50000.0;
def.Speed = 0.0;
LuneUnit unit = combat.Spawn(isHero ? "hero" : "monster", 1, "mid", def, 40.0, 0.0, isHero ? 3 : -1, isHero ? "h3-1" : "");
unit.Position = brunhild.Position + forward * enemy.Value;
unit.PreviousPosition = unit.Position;
}
return combat;
}
static KeyValuePair<string, double> At(string id, double distance) { return new KeyValuePair<string, double>(id, distance); }
static void Run(LuneCombat combat, double seconds)
{
int steps = (int)(seconds / combat.Balance.SimStep + 0.5);
for (int i = 0; i < steps && !combat.Finished; i++)
combat.Step();
}
void TestPurchase()
{
var state = new LuneMatch();
state.Setup(new MatchOptions { Seed = 71, HumanSlot = -1 });
state.Players[0].Draft.Gold += 1000;
Check(state.Submit(0, Command.HeroBuy("held_brunhild")).Ok, "Brunhild can be recruited for 260");
Check(LuneContent.UltimateRuleFor(state.Players[0].Draft.Hero) == "gruppe", "default rule Gruppe");
Check(state.Submit(0, Command.UltimateRule("held")).Ok && state.Submit(0, Command.HeroSkin("eiswalkuere")).Ok, "rule Heldenjagd and skin Eiswalküre");
Check(!state.Submit(0, Command.HeroSkin("blutmond")).Ok, "Sylvara's skin is not hers");
UnitDef definition = LuneContent.HeroDefinition(state.Players[0].Draft.Hero);
Check(definition.Id == "brunhild" && definition.Skin == "eiswalkuere" && definition.UltimateRule == "held" && GdMath.IsEqualApprox(definition.AttackRange, 2.0), "definition carries model, skin, rule and the spear's reach");
}
void TestSpear()
{
LuneUnit brunhild;
LuneCombat combat = Arena(new HeroState { HeroId = "held_brunhild", Ultimate = "notfall" }, out brunhild, At("e01", 1.5), At("e01", 4.0), At("e04", 6.5));
LuneUnit far = combat.Units.First(u => u.Def.Id == "e04");
Run(combat, LuneCombat.BrunhildFirstSpear - 0.1);
Check(brunhild.AbilitySerial == 0, "no spear before 3 s");
Run(combat, 0.2);
Check(brunhild.AbilitySerial == 1 && far.Effects.ContainsKey("valkyrie_mark"), "the first spear marks the farthest enemy in 7 m");
double raw = 100.0;
var probe = new LuneUnit { Def = far.Def };
Check(GdMath.IsEqualApprox(far.Mitigated(raw, 0), probe.Mitigated(raw, 0) * 1.15), "a marked enemy takes 15 % more damage");
}
void TestUltimate()
{
LuneUnit brunhild;
LuneCombat combat = Arena(new HeroState { HeroId = "held_brunhild", Ultimate = "held" }, out brunhild, At("e01", 1.5), At("held", 6.0), At("e01", 6.8), At("e07", 6.5));
LuneUnit hero = combat.Units.First(u => u.Kind == "hero" && u.Team == 1);
LuneUnit boss = combat.Units.First(u => u.Def.Id == "e07");
Run(combat, LuneContent.UltimateDelay - 0.1);
Check(brunhild.UltimateSerial == 0, "no ultimate before 2 s");
Run(combat, 0.1);
Check(brunhild.UltimateSerial == 1, "Heldenjagd fires with an enemy hero in 7 m");
Check(brunhild.Position.DistanceTo(hero.Position) < 1.5, "she leaps next to the hero");
Check(hero.Stunned() && !boss.Stunned(), "the hero is stunned, the boss is immune");
Check(hero.AttackSpeedFactor() == 0.0 && hero.MovementFactor() == 0.0, "a stunned unit neither attacks nor moves");
Run(combat, 1.1);
Check(!hero.Stunned(), "the stun ends after 1 s");
Run(combat, 8.0);
Check(brunhild.UltimateSerial == 1, "once per combat");
LuneUnit calm;
LuneCombat group = Arena(new HeroState { HeroId = "held_brunhild" }, out calm, At("e01", 3.0), At("e01", 3.5));
Run(group, 4.0);
Check(calm.UltimateSerial == 0, "Gruppe waits for three enemies at the target");
}
void TestPassiveAndTalents()
{
LuneUnit brunhild;
LuneCombat combat = Arena(new HeroState { HeroId = "held_brunhild", Ultimate = "notfall" }, out brunhild, At("held", 1.5));
LuneUnit hero = combat.Units.First(u => u.Kind == "hero" && u.Team == 1);
double before = hero.Hp;
int guard = 0;
while (brunhild.HitCount == 0 && guard++ < 200)
combat.Step();
double expected = hero.Mitigated(brunhild.Def.Damage * (1.0 + brunhild.DamageBonus()) * LuneCombat.BrunhildHunter, brunhild.Def.AttackType);
Check(GdMath.IsEqualApprox(before - hero.Hp, expected), "Heldenjägerin: +25 % against heroes");
UnitDef raven = LuneContent.HeroDefinition(new HeroState { HeroId = "held_brunhild", Xp = 1000, Talents = new List<string> { "speerregen", "rabenschwinge" } });
Check(GdMath.IsEqualApprox(raven.AttackSpeedBonus, 0.20), "Rabenschwinge: +20 % attack tempo");
LuneUnit rain;
LuneCombat spears = Arena(new HeroState { HeroId = "held_brunhild", Ultimate = "notfall", Talents = new List<string> { "speerregen", "goetterdaemmerung" } }, out rain,
At("e01", 4.0), At("e01", 5.0), At("e01", 6.0), At("e01", 1.0));
Run(spears, 3.1);
Check(spears.Units.Count(u => u.Effects.ContainsKey("valkyrie_mark")) == 3, "Speerregen: three marked enemies");
LuneUnit dusk;
LuneCombat stun = Arena(new HeroState { HeroId = "held_brunhild", Ultimate = "sofort", Talents = new List<string> { "speerregen", "goetterdaemmerung" } }, out dusk, At("e01", 2.0));
Run(stun, 2.05);
LuneUnit victim = stun.Units.First(u => u.Kind == "monster");
Run(stun, 1.5);
Check(victim.Stunned(), "Götterdämmerung: the stun lasts 2 s");
}
void TestBots()
{
bool picked = false;
for (int seed = 1; seed <= 16 && !picked; seed++)
{
var probe = new LuneMatch();
probe.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Extended = true });
for (int slot = 0; slot < 6; slot++)
picked |= LuneBot.ProfileFor(probe, slot) == "walkuere";
}
Check(picked, "extended bots pick Brunhild's profile");
var reference = new LuneMatch();
reference.Setup(new MatchOptions { Seed = 3, HumanSlot = -1 });
bool clean = true;
for (int slot = 0; slot < 6; slot++)
clean &= !ContentData.ExtraBotProfiles.ContainsKey(LuneBot.ProfileFor(reference, slot));
Check(clean, "reference rotation untouched");
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 2fa3dc51ddee4a919bec93bf34fc29f9

View file

@ -1,162 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace Luneraria.Core.Tests
{
/// <summary>
/// S2: glyph (Dota 2), item recipes (Dota 2 / League of Legends), Ancient levels (Legion TD 2
/// king) and the bots' grotto hunt under the extended rules; none of it under the reference rules.
/// </summary>
public sealed class S2RulesSuite : CheckSuite
{
public override string Name { get { return "s2"; } }
protected override void RunChecks()
{
TestReference();
TestGlyph();
TestRecipes();
TestCore();
TestBots();
}
static LuneMatch NewMatch(bool extended, int seed = 51)
{
var state = new LuneMatch();
state.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Extended = extended,
Bunds = new List<string> { "glutpakt", "wurzelbund", "glutpakt", "wurzelbund", "glutpakt", "wurzelbund" } });
return state;
}
void TestReference()
{
LuneMatch state = NewMatch(false);
state.Players[0].Draft.Gold += 3000;
Check(!state.Submit(0, Command.Glyph("mid")).Ok, "reference: no glyph");
Check(!state.Submit(0, Command.CoreUpgrade()).Ok, "reference: no Ancient levels");
state.Submit(0, Command.HeroBuy("held_ringborke"));
state.Submit(0, Command.ItemBuy("basaltschild"));
state.Submit(0, Command.ItemBuy("rankenmantel"));
Check(!state.Submit(0, Command.ItemBuy("bollwerk")).Ok, "reference: no recipes");
state.BeginCombat();
Check(state.Combat.Buildings.Values.All(b => b.Kind != "core" || b.Def.Damage == 0.0), "reference: the Ancients do not shoot");
}
void TestGlyph()
{
LuneMatch state = NewMatch(true);
Check(!state.Submit(0, Command.Glyph("river")).Ok, "unknown lane rejected");
Check(state.Submit(1, Command.Glyph("mid")).Ok && state.Players[1].Draft.GlyphLane == "mid", "a player holds the team glyph for a lane");
state.Submit(0, Command.Glyph("top"));
state.BeginCombat();
Check(state.Combat.GlyphLane[0] == "top", "the first seat in slot order decides the team glyph");
LuneUnit tower = state.Combat.Buildings["blau_top_outer"];
tower.Hp = tower.MaxHp * 0.4;
state.Combat.Step();
Check(state.Combat.GlyphUsed[0] && state.Combat.Glyphed(tower), "the glyph fires once a tower of the lane is below half health");
double before = tower.Hp;
var gates = state.Combat.AliveMap();
state.Combat.QueueDamage(tower, 500.0, gates);
Check(!state.Combat.HasPendingDamage(tower), "glyphed towers take no damage");
Check(!state.Combat.Glyphed(state.Combat.Buildings["blau_mid_outer"]), "other lanes are not protected");
for (int i = 0; i < 120; i++)
state.Combat.Step();
Check(!state.Combat.Glyphed(tower), "the glyph ends after 5 s");
state.FinishCombat();
Check(state.Boss.GlyphReady[0] == state.RoundNumber - 1 + 1 + 4, "a fired glyph returns after 4 rounds (" + state.Boss.GlyphReady[0] + ")");
Check(!state.Submit(0, Command.Glyph("top")).Ok, "on cooldown the glyph cannot be held");
Check(state.Boss.GlyphReady[1] == 1, "the other team's glyph is untouched");
LuneMatch idle = NewMatch(true);
idle.Submit(2, Command.Glyph("bot"));
idle.BeginCombat();
idle.FinishCombat();
Check(idle.Combat.GlyphUsed[0] || idle.Boss.GlyphReady[0] == 1, "a glyph that never fired costs no cooldown");
}
void TestRecipes()
{
LuneMatch state = NewMatch(true);
state.Players[0].Draft.Gold += 3000;
state.Submit(0, Command.HeroBuy("held_ringborke"));
Check(!state.Submit(0, Command.ItemBuy("bollwerk")).Ok, "a recipe needs its parts");
state.Submit(0, Command.ItemBuy("basaltschild"));
state.Submit(0, Command.ItemBuy("rankenmantel"));
state.Submit(0, Command.ItemBuy("glutklinge"));
int gold = state.GoldOf(0);
Check(state.Submit(0, Command.ItemBuy("bollwerk")).Ok && state.GoldOf(0) == gold - 120, "combining costs the fee of 120");
HeroState hero = state.Players[0].Draft.Hero;
Check(hero.Items.SequenceEqual(new[] { "bollwerk", "glutklinge" }), "parts become the recipe item, a slot is free");
UnitDef definition = LuneContent.HeroDefinition(hero);
Check(GdMath.IsEqualApprox(definition.MaxHp, 2200.0 + 600.0) && GdMath.IsEqualApprox(definition.ReductionBonus, 0.15), "Bollwerk: +600 health, +15 % reduction");
Check(state.Undo(0).Ok && state.Players[0].Draft.Hero.Items.Count == 3, "combining can be undone");
state.Submit(0, Command.ItemBuy("bollwerk"));
Check(!state.Submit(0, Command.ItemBuy("splitterkrone")).Ok, "Splitterkrone needs its parts and level 5");
state.BeginCombat();
state.FinishCombat();
int sellGold = state.GoldOf(0);
Check(state.Submit(0, Command.ItemSell("bollwerk")).Ok && state.GoldOf(0) == sellGold + 250, "a recipe item sells for half its total (250)");
}
void TestCore()
{
LuneMatch state = NewMatch(true);
state.Players[0].Draft.Gold += 3000;
state.Players[1].Draft.Gold += 3000;
state.BuildingHp["blau_ancient"] = 5000.0;
int gold = state.GoldOf(0);
Check(state.Submit(0, Command.CoreUpgrade()).Ok && state.GoldOf(0) == gold - 150, "the first Ancient level costs 150");
Check(state.Submit(0, Command.CoreUpgrade()).Ok && state.GoldOf(0) == gold - 450, "the second costs 300");
Check(state.Submit(1, Command.CoreUpgrade()).Ok && state.Submit(1, Command.CoreUpgrade()).Ok, "a teammate buys too (priced by the committed level)");
state.BeginCombat();
Check(state.Boss.CoreLevel[0] == 3 && state.Boss.CoreLevel[1] == 0, "levels are capped at 3 for the team");
Check(state.Players[1].Gold > 0, "the teammate's excess level is refunded");
LuneUnit core = state.Combat.Buildings["blau_ancient"];
Check(GdMath.IsEqualApprox(core.Def.Damage, 110.0) && GdMath.IsEqualApprox(core.Def.AttackRange, 7.5), "level 3: the Ancient shoots 110 in 7.5 m");
Check(GdMath.IsEqualApprox(core.Hp, 8000.0), "each level repaired 1,000 (5,000 → 8,000)");
Check(!state.Submit(0, Command.CoreUpgrade()).Ok, "no commands in combat");
state.FinishCombat();
Check(!state.Submit(0, Command.CoreUpgrade()).Ok, "no level above 3");
}
void TestBots()
{
int hunts = 0;
int blessings = 0;
int combines = 0;
int glyphs = 0;
int cores = 0;
string first = "";
for (int seed = 1; seed <= 12; seed++)
{
LuneMatch match = NewMatch(true, seed);
while (match.Phase != Phase.Over && match.RoundNumber <= 80)
{
for (int slot = 0; slot < 6; slot++)
LuneBot.Plan(match, slot);
if (match.Players.Any(p => p.Draft.Roster.Any(r => r.Lane == LuneMatch.Grotto)))
hunts++;
match.BeginCombat();
match.FinishCombat();
glyphs += match.Combat.GlyphUsed.Count(used => used);
}
blessings += match.Boss.Blessing[0] + match.Boss.Blessing[1];
cores += match.Boss.CoreLevel[0] + match.Boss.CoreLevel[1];
combines += match.Players.Count(p => p.Hero != null && p.Hero.Items.Any(id => ContentData.Items[id].IsRecipe));
if (seed == 1)
first = match.RoundNumber + "/" + match.Winner;
}
Check(hunts > 0 && blessings > 0, "extended bots go to the grotto and win Mondsegen (" + hunts + " expeditions, " + blessings + " stacks in 12 matches)");
// Bots recruit heroes late (reference bot behaviour), so recipes rarely come up in whole
// matches; a prepared planning shows that a bot forges one when it can.
LuneMatch forge = NewMatch(true, 3);
Player bot = forge.Players[0];
bot.Hero = new HeroState { Uid = "h0-9", Seq = 9, HeroId = "held_ringborke", Lane = bot.Lane, Xp = 300, Invested = 260, Items = new List<string> { "basaltschild", "rankenmantel" } };
bot.Gold = 1200;
bot.Draft = forge.Snapshot(bot);
LuneBot.Plan(forge, 0);
Check(bot.Draft.Hero.Items.Contains("bollwerk"), "a bot forges Bollwerk from its parts (" + string.Join("+", bot.Draft.Hero.Items) + ")");
Check(glyphs > 0, "extended bots fire glyphs (" + glyphs + ")");
Note("12 extended matches: expeditions " + hunts + ", Mondsegen " + blessings + ", recipe heroes " + combines + ", glyphs " + glyphs + ", Ancient levels " + cores + "; seed 1 " + first);
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 04183de557804f08aac74a3d73fe5f3c

View file

@ -1,183 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace Luneraria.Core.Tests
{
/// <summary>
/// S3: bots that recruit their heroes early and a rotation over all profiles, neutral camps
/// (League of Legends buff camps), wards and the fog of war (Dota 2) under the extended
/// rules; none of it under the reference rules.
/// </summary>
public sealed class S3RulesSuite : CheckSuite
{
public override string Name { get { return "s3"; } }
protected override void RunChecks()
{
TestReference();
TestBots();
TestCamps();
TestWards();
TestFog();
}
static LuneMatch NewMatch(bool extended, int seed = 61)
{
var state = new LuneMatch();
state.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Extended = extended,
Bunds = new List<string> { "glutpakt", "wurzelbund", "glutpakt", "wurzelbund", "glutpakt", "wurzelbund" } });
return state;
}
void TestReference()
{
LuneMatch state = NewMatch(false);
state.RoundNumber = 3;
state.Players[0].Draft.Gold += 500;
Check(!state.Submit(0, Command.Ward("mid")).Ok, "reference: no wards");
state.BeginCombat();
Check(state.Combat.Units.All(u => u.Kind != "camp"), "reference: no camps in round 3");
Check(LuneFog.Hidden(state, state.Combat, 0, false).Count == 0, "reference: no fog");
}
void TestBots()
{
var profiles = new HashSet<string>();
LuneMatch probe = NewMatch(true, 1);
for (int slot = 0; slot < 6; slot++)
profiles.Add(LuneBot.ProfileFor(probe, slot));
Check(profiles.Count == 6, "extended rotation gives six different profiles (" + string.Join(",", profiles) + ")");
int heroes = 0;
int seats = 0;
for (int seed = 1; seed <= 6; seed++)
{
LuneMatch match = new LuneMatch();
match.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Extended = true });
while (match.Phase != Phase.Over && match.RoundNumber <= 6)
{
for (int slot = 0; slot < 6; slot++)
LuneBot.Plan(match, slot);
match.BeginCombat();
match.FinishCombat();
}
heroes += match.Players.Count(p => p.Hero != null);
seats += 6;
}
Check(heroes * 2 >= seats, "extended bots have most heroes after round 6 (" + heroes + " of " + seats + ")");
LuneMatch reference = new LuneMatch();
reference.Setup(new MatchOptions { Seed = 1, HumanSlot = -1 });
for (int round = 0; round < 6; round++)
{
for (int slot = 0; slot < 6; slot++)
LuneBot.Plan(reference, slot);
reference.BeginCombat();
reference.FinishCombat();
}
Check(reference.Players.Count(p => p.Hero != null) == 0, "reference bots keep their late heroes (parity)");
LuneMatch bounty = NewMatch(true);
Check(bounty.ChampionBountyFor(100) == 50 && NewMatch(false).ChampionBountyFor(100) == 100, "extended: defenders get half a champion's bounty");
}
void TestCamps()
{
LuneMatch state = NewMatch(true);
Check(!state.CampRound(1) && !state.CampRound(2) && state.CampRound(3) && state.CampRound(6), "camps every third round");
state.BeginCombat();
Check(state.Combat.Units.All(u => u.Kind != "camp"), "no camp in round 1");
state.FinishCombat();
state.RoundNumber = 3;
state.Players[0].Draft = state.Snapshot(state.Players[0]);
state.BeginCombat();
List<LuneUnit> camps = state.Combat.Units.Where(u => u.Kind == "camp").ToList();
Check(camps.Count == 3 && camps.All(c => c.Team == LuneCombat.Neutral && c.Zone == "lane"), "three neutral guardians, one per lane");
LuneUnit top = camps.First(c => c.Lane == "top");
Vec2 start = top.Position;
for (int i = 0; i < 40; i++)
state.Combat.Step();
Check(top.Position.DistanceTo(start) < 0.000001, "a guardian never moves");
// Blau's units stand next to the top guardian when it falls.
foreach (LuneUnit unit in state.Combat.Units.Where(u => u.Team == 0 && u.IsMobile()).Take(4))
unit.Position = top.Position + new Vec2(1.0, 0.0);
top.Hp = 0.0;
LuneUnit bot = camps.First(c => c.Lane == "bot");
foreach (LuneUnit unit in state.Combat.Units.Where(u => u.Team == 1 && u.IsMobile()).Take(4))
unit.Position = bot.Position + new Vec2(1.0, 0.0);
bot.Hp = 0.0;
state.Combat.Step();
CombatEvent topEvent = state.Combat.Events.First(e => e.Kind == "camp" && e.Lane == "top");
Check(topEvent.ClaimTeam == 0, "the team with more units near the guardian takes the camp");
int[] before = state.Players.Select(p => p.Gold).ToArray();
state.FinishCombat();
Check(state.Boss.CampBuff[0] == 2 && state.Boss.CampBuff[3] == 0, "Glutsegen for Blau for two combats");
int redGain = state.Players[4].Gold - before[4] - state.LastSettlement.Gains[4] + 50;
Check(state.LastSettlement.Gains[3] >= 50 && state.LastSettlement.Gains[4] >= 50 && state.LastSettlement.Gains[5] >= 50, "Beute: Rot's players get 50 gold each");
state.Players[0].Draft.Gold += 500;
state.Submit(0, Command.Buy("g01", "top"));
state.BeginCombat();
LuneUnit troop = state.Combat.Units.First(u => u.Kind == "troop" && u.OwnerSlot == 0 && u.Def.Id == "g01");
Check(GdMath.IsEqualApprox(troop.Def.Damage, DataCatalog.GetUnit("g01").Damage * 1.10), "Glutsegen: +10 % damage");
state.FinishCombat();
state.Boss.CampBuff[4] = 1;
state.Players[3].Draft.Gold += 500;
state.Submit(3, Command.Buy("w01", "top"));
state.BeginCombat();
LuneUnit red = state.Combat.Units.First(u => u.Kind == "troop" && u.Team == 1);
Check(GdMath.IsEqualApprox(red.Shield, red.MaxHp * 0.10) || red.Shield > red.MaxHp * 0.10, "Mondschild: shield of 10 % at the start");
Check(redGain >= 0, "settlement consistent");
// Combats end without waiting for a lone guardian.
LuneMatch lonely = NewMatch(true);
lonely.RoundNumber = 3;
lonely.BeginCombat();
foreach (LuneUnit unit in lonely.Combat.Units.Where(u => u.Kind != "camp" && u.IsMobile()))
unit.Hp = 0.0;
lonely.Combat.Step();
lonely.Combat.Step();
Check(lonely.Combat.Finished && lonely.Combat.EndReason == "cleared", "a guardian alone does not hold the combat open");
}
void TestWards()
{
LuneMatch state = NewMatch(true);
int gold = state.GoldOf(0);
Check(!state.Submit(0, Command.Ward("river")).Ok, "unknown lane rejected");
Check(state.Submit(0, Command.Ward("mid")).Ok && state.GoldOf(0) == gold - 40, "a ward costs 40");
Check(!state.Submit(0, Command.Ward("mid")).Ok, "one ward per lane and planning");
Check(LuneFog.LaneScouted(state, 0, "mid", state.Players[0].Draft) && !LuneFog.LaneScouted(state, 0, "mid"), "the own draft ward scouts at once, the team sees it after the commit");
state.BeginCombat();
Check(state.Boss.Wards[1] == 3 && state.Boss.Wards[4] == 0, "the team ward watches mid for three rounds");
state.FinishCombat();
Check(state.Boss.Wards[1] == 2 && LuneFog.LaneScouted(state, 0, "mid") && !LuneFog.LaneScouted(state, 1, "mid"), "it counts down and scouts only for Blau");
state.BuildingHp["rot_top_outer"] = 0.0;
Check(LuneFog.LaneScouted(state, 0, "top"), "a fallen enemy outer tower scouts the lane");
}
void TestFog()
{
LuneMatch state = NewMatch(true);
state.RoundNumber = 3;
state.BeginCombat();
HashSet<int> hidden = LuneFog.Hidden(state, state.Combat, 0, false);
Check(state.Combat.Units.Where(u => u.Team == 1 && u.IsMobile()).All(u => hidden.Contains(u.Id)), "at combat start Blau sees no Rot unit (they stand at their base)");
Check(state.Combat.Units.Where(u => u.Team != 1).All(u => !hidden.Contains(u.Id)), "own and neutral units are always visible");
LuneUnit enemy = state.Combat.Units.First(u => u.Team == 1 && u.IsMobile());
LuneUnit own = state.Combat.Units.First(u => u.Team == 0 && u.IsMobile() && u.Zone == enemy.Zone);
enemy.Position = own.Position + new Vec2(5.0, 0.0);
Check(!LuneFog.Hidden(state, state.Combat, 0, false).Contains(enemy.Id), "an enemy within 9 m of an own unit is visible");
enemy.Position = LuneFog.WardPoint(state, enemy.Lane == "grotto" ? "mid" : enemy.Lane);
bool before = LuneFog.Hidden(state, state.Combat, 0, false).Contains(enemy.Id);
state.Boss.Wards[LuneMap.LaneIndex(enemy.Lane)] = 2;
Check(before && !LuneFog.Hidden(state, state.Combat, 0, false).Contains(enemy.Id), "a ward reveals the middle of its lane");
state.FinishCombat();
LuneMatch plan = NewMatch(true);
plan.Players[3].Draft.Gold += 200;
plan.Submit(3, Command.Buy("w01", "top"));
plan.Submit(3, Command.Buy("w01", "mid"));
plan.BeginCombat();
plan.FinishCombat();
plan.Boss.Wards[0] = 1;
List<LuneMatch.ArmyEntry> scouted = LuneFog.ScoutedEnemies(plan, 0);
Check(scouted.Any(e => e.Lane == "top" && e.Slot == 3) && scouted.All(e => e.Lane == "top"), "planning: the warded lane shows Rot's committed army, the others nothing");
Check(LuneFog.ScoutedEnemies(plan, 1).Count == 0 && LuneFog.ScoutedEnemies(NewMatch(false), 0).Count == 0, "no scouting without a ward or under the reference rules");
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 9632c5c9699a4bc99db6e8b3479cf103

View file

@ -1,55 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace Luneraria.Core.Tests
{
/// <summary>
/// S4: what guests see of the extended rules. Over the in-process loopback a guest receives
/// the running glyph of each team (extra values after the building HP of a frame) and the
/// stun flag of every unit, and its fog sources match the host's (LuneFog.Vision).
/// </summary>
public sealed class S4NetSuite : CheckSuite
{
public override string Name { get { return "s4"; } }
protected override void RunChecks()
{
var hub = new LuneLoopback();
LuneSession host = hub.Attach(1, new LuneSession());
var options = new MatchOptions { Seed = 42, HumanSlot = 0, Extended = true,
Bunds = new List<string> { "glutpakt", "wurzelbund", "glutpakt", "wurzelbund", "glutpakt", "wurzelbund" } };
Check(host.HostNetwork(options, "Host"), "host starts");
LuneSession guest = hub.Attach(10, new LuneSession());
guest.JoinNetwork("Gast");
guest.OnClientConnected();
Check(guest.MatchState != null && guest.MatchState.Extended, "the guest's match has the extended rules");
host.PlanBotSeats();
host.Submit(Command.Buy("g01"));
host.ReadyUp();
guest.Submit(Command.Buy(guest.MatchState.BaseIds(guest.MatchState.Players[guest.LocalSlot].Bund)[0]));
guest.ReadyUp();
for (int i = 0; i < 20 && host.MatchState.Phase != Phase.Combat; i++)
host.HostTick(LuneNet.FrameInterval);
Check(host.MatchState.Phase == Phase.Combat && guest.MatchState.Phase == Phase.Combat, "both sides are in combat");
LuneCombat combat = host.MatchState.Combat;
for (int i = 0; i < 10; i++)
host.MatchState.StepCombat();
combat.GlyphLane[0] = "mid";
combat.GlyphUntil[0] = combat.Elapsed + 3.0;
LuneUnit target = combat.Units.First(u => u.IsMobile() && u.Team == 1 && u.Alive);
target.ApplyEffect("valkyrie_stun", "stun", 1.0, 2.0);
host.HostTick(LuneNet.FrameInterval);
LuneCombat mirror = guest.MatchState.Combat;
Check(mirror.GlyphLane[0] == "mid" && mirror.GlyphLane[1] == "", "the guest knows Blau's glyph on mid");
LuneUnit tower;
Check(mirror.Buildings.TryGetValue("blau_mid_outer", out tower) && mirror.Glyphed(tower), "the guest sees the mid tower glyphed");
LuneUnit copy = mirror.Units.FirstOrDefault(u => u.Id == target.Id);
Check(copy != null && copy.ShowsStun(), "the guest sees the stun");
Check(mirror.Units.Where(u => u.Id != target.Id && u.IsMobile()).All(u => !u.ShowsStun()), "nobody else looks stunned");
int team = 0;
int hostSources = LuneFog.Vision(host.MatchState, combat, team).Count;
int guestSources = LuneFog.Vision(guest.MatchState, mirror, team).Count;
Check(hostSources == guestSources && hostSources > 0, "fog sources agree on host and guest (" + hostSources + "/" + guestSources + ")");
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: df9ba1b1ef594679b037f9c14faa13c0

View file

@ -53,9 +53,5 @@ namespace Luneraria.Core.Tests
[Test] public void NetRounds() { RunSuite(new NetRoundsSuite()); } [Test] public void NetRounds() { RunSuite(new NetRoundsSuite()); }
[Test] public void H1Heroes() { RunSuite(new H1HeroSuite()); } [Test] public void H1Heroes() { RunSuite(new H1HeroSuite()); }
[Test] public void S1Rules() { RunSuite(new S1RulesSuite()); } [Test] public void S1Rules() { RunSuite(new S1RulesSuite()); }
[Test] public void S2Rules() { RunSuite(new S2RulesSuite()); }
[Test] public void S3Rules() { RunSuite(new S3RulesSuite()); }
[Test] public void H2Heroes() { RunSuite(new H2HeroSuite()); }
[Test] public void S4Net() { RunSuite(new S4NetSuite()); }
} }
} }

View file

@ -61,7 +61,7 @@ namespace Luneraria.UI.Tests
s.Hud.OpenHero(); s.Hud.OpenHero();
yield return HudSession.Frames(2); yield return HudSession.Frames(2);
s.Sync(); s.Sync();
Assert.AreEqual(LuneContent.Heroes.Count, s.Hud.HeroDetail.RecruitButtons.Count, "every hero can be recruited"); Assert.AreEqual(4, s.Hud.HeroDetail.RecruitButtons.Count, "four heroes to recruit");
Assert.IsTrue(s.Hud.HeroDetail.RecruitButtons.ContainsKey("held_sylvara"), "Sylvara can be recruited"); Assert.IsTrue(s.Hud.HeroDetail.RecruitButtons.ContainsKey("held_sylvara"), "Sylvara can be recruited");
s.Click(s.Hud.HeroDetail.RecruitButtons["held_sylvara"]); s.Click(s.Hud.HeroDetail.RecruitButtons["held_sylvara"]);
yield return HudSession.Frames(2); yield return HudSession.Frames(2);

View file

@ -1,175 +0,0 @@
using System;
using System.Collections;
using System.Linq;
using Luneraria.Core;
using Luneraria.View;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace Luneraria.UI.Tests
{
/// <summary>
/// H2: Brunhild in the real scene (derived from the H1 test). BrunhildHud checks recruiting,
/// the tab "Ultimate & Skin" (four rules, two skins), the command slot, her figure with the
/// ultimate clip and the Eiswalküre material on body and spear, and the ultimate in combat.
/// BrunhildBilder captures the H2 evidence (1920×1080); LUNE_EVIDENCE names the folder below
/// docs/port/screenshots (default h2/spiel).
/// </summary>
public sealed class H2HeroViewsTests
{
static void Send(HudSession s, Command command)
{
CommandResult result = s.Match.Submit(HudSession.Human, command);
Assert.IsTrue(result.Ok, command.Type + ": " + result.Message);
s.Game.RebuildPreview();
s.Sync();
}
static LuneUnit Hero(HudSession s)
{
return s.Match.Combat?.Units.FirstOrDefault(u => u.Kind == "hero" && u.OwnerSlot == HudSession.Human);
}
static LuneUnitView ViewOf(LuneUnit unit)
{
return UnityEngine.Object.FindObjectsByType<LuneUnitView>(FindObjectsSortMode.None).FirstOrDefault(v => v.Unit == unit);
}
/// <summary>Steps the combat in single simulation steps until the condition holds (or the combat ends).</summary>
static IEnumerator Until(HudSession s, Func<bool> done, double limit = 60.0)
{
double time = 0.0;
while (!done() && time < limit && s.Runner.Current == LuneMatchRunner.Stage.Combat)
{
s.Runner.Advance(.05);
time += .05;
if ((int)(time / .05) % 10 == 0)
yield return null;
}
yield return HudSession.Frames(2);
s.Sync();
}
[UnityTest, Timeout(600000)]
public IEnumerator BrunhildHud()
{
var s = new HudSession();
yield return s.Open(1920, 1080, seed: 4, startRound: 6, extraGold: 1500);
s.Hud.OpenHero();
yield return HudSession.Frames(2);
s.Sync();
Assert.AreEqual(LuneContent.Heroes.Count, s.Hud.HeroDetail.RecruitButtons.Count, "every hero can be recruited");
Assert.IsTrue(s.Hud.HeroDetail.RecruitButtons.ContainsKey("held_brunhild"), "Brunhild can be recruited");
s.Click(s.Hud.HeroDetail.RecruitButtons["held_brunhild"]);
yield return HudSession.Frames(2);
s.Sync();
Assert.AreEqual("held_brunhild", s.Me.Draft.Hero?.HeroId, "the recruit button buys Brunhild");
s.Hud.OpenHero(LuneHeroDetail.TabUltimate);
yield return HudSession.Frames(2);
s.Sync();
Assert.AreEqual(4, s.Hud.HeroDetail.RuleCards.Count, "four trigger rules");
Assert.AreEqual(2, s.Hud.HeroDetail.SkinCards.Count, "standard look and Eiswalküre");
Assert.IsTrue(s.Hud.HeroDetail.RuleCards["gruppe"].On, "default rule Gruppe is marked");
s.Click(s.Hud.HeroDetail.RuleCards["held"]);
yield return HudSession.Frames(2);
s.Sync();
Assert.AreEqual("held", s.Me.Draft.Hero.Ultimate, "a rule card sets the rule");
s.Click(s.Hud.HeroDetail.SkinCards["eiswalkuere"]);
yield return HudSession.Frames(2);
s.Sync();
Assert.AreEqual("eiswalkuere", s.Me.Draft.Hero.Skin, "a skin card sets the skin");
Assert.IsTrue(HudSession.Visible(s.Hud.HeroCard.CommandSlot), "the hero card shows the ultimate");
StringAssert.Contains("HELDENJAGD", s.Hud.HeroCard.CommandSlot.Text, "the command slot names the rule");
s.Hud.CloseModal();
s.Game.Key(KeyCode.Space);
yield return HudSession.Frames(2);
LuneUnit hero = Hero(s);
Assert.IsNotNull(hero, "Brunhild fights");
Assert.AreEqual("eiswalkuere", hero.Def.Skin);
yield return HudSession.Frames(3);
LuneUnitView view = ViewOf(hero);
Assert.IsNotNull(view, "Brunhild has a view");
Animator animator = view.GetComponentInChildren<Animator>();
Assert.IsNotNull(animator, "skinned figure");
Assert.IsTrue(animator.HasState(0, Animator.StringToHash("ultimate")), "ultimate clip");
string[] materials = view.GetComponentsInChildren<Renderer>().SelectMany(r => r.sharedMaterials).Where(m => m != null).Select(m => m.name).Distinct().ToArray();
Assert.Contains("AI_Brunhild_Eiswalkuere", materials, "the Blutmond material is dressed: " + string.Join(", ", materials));
Assert.Contains("AI_BrunhildSpeer_Eiswalkuere", materials, "both sickles in Blutmond");
yield return Until(s, () => hero.UltimateSerial > 0 || !hero.Alive);
Assert.IsTrue(hero.UltimateSerial > 0 || !hero.Alive, "the ultimate fires (or Brunhild fell first)");
if (hero.UltimateSerial > 0)
StringAssert.Contains("GENUTZT", s.Hud.HeroCard.CommandSlot.Text, "the card reports the fired ultimate");
s.Close();
}
[UnityTest, Timeout(900000), Explicit("Bildwerkzeug: nur gezielt per -testFilter")]
public IEnumerator BrunhildBilder()
{
string evidence = Environment.GetEnvironmentVariable("LUNE_EVIDENCE");
string folder = "docs/port/screenshots/" + (string.IsNullOrEmpty(evidence) ? "h2/spiel" : evidence) + "/";
var s = new HudSession();
yield return s.Open(1920, 1080, seed: 4, startRound: 6, extraGold: 1500);
yield return s.Starter();
s.Leave();
s.Hud.OpenHero();
yield return new WaitForSecondsRealtime(.6f);
yield return s.Shot(folder + "01-heldenwahl.png");
Send(s, Command.HeroBuy("held_brunhild"));
s.Hud.OpenHero(LuneHeroDetail.TabValues);
yield return new WaitForSecondsRealtime(.6f);
yield return s.Shot(folder + "02-heldenfenster.png");
s.Hud.OpenHero(LuneHeroDetail.TabUltimate);
yield return new WaitForSecondsRealtime(.8f);
yield return s.Shot(folder + "03-ultimate-und-skin.png");
s.Hud.CloseModal();
yield return new WaitForSecondsRealtime(.3f);
LuneUnit waiting = s.View.Preview?.Units.FirstOrDefault(u => u.Kind == "hero" && u.OwnerSlot == HudSession.Human);
Assert.IsNotNull(waiting, "Brunhild in the planning preview");
s.Rig.LookAt(waiting.Position, 9f, true);
yield return new WaitForSecondsRealtime(.6f);
yield return s.Shot(folder + "04-aufstellung.png");
s.Game.Key(KeyCode.Space);
yield return HudSession.Frames(2);
LuneUnit hero = Hero(s);
yield return Until(s, () => hero.UltimateSerial > 0 || !hero.Alive);
s.Driver.Paused = true;
s.Rig.LookAt(hero.Position, 11f, true);
yield return new WaitForSecondsRealtime(.35f);
yield return s.Shot(folder + "05-walkuerensturz.png");
s.Rig.LookAt(hero.Position, 11f, true);
yield return new WaitForSecondsRealtime(.2f);
yield return s.Shot(folder + "06-betaeubt.png");
yield return new WaitForSecondsRealtime(.6f);
yield return s.Shot(folder + "07-nach-dem-sturz.png");
s.Rig.LookAt(hero.Position, 20f, true);
yield return new WaitForSecondsRealtime(.4f);
yield return s.Shot(folder + "08-kampf-lane.png");
s.Driver.Paused = false;
yield return s.FinishCombat();
// Space is blocked for a moment after a combat (no accidental skip of the planning).
float waited = 0f;
while (s.Hud.ReadyBlocked() && waited < 5f)
{
waited += Time.unscaledDeltaTime;
yield return null;
}
Send(s, Command.HeroSkin("eiswalkuere"));
s.Hud.OpenHero(LuneHeroDetail.TabUltimate);
yield return new WaitForSecondsRealtime(.8f);
yield return s.Shot(folder + "09-skin-eiswalkuere-fenster.png");
s.Hud.CloseModal();
int round = s.Match.RoundNumber;
s.Game.Key(KeyCode.Space);
yield return HudSession.Frames(2);
Assert.AreEqual(LuneMatchRunner.Stage.Combat, s.Runner.Current, "combat of round " + round);
hero = Hero(s);
yield return s.Fight(9.0);
s.Driver.Paused = true;
s.Rig.LookAt(hero.Position, 9f, true);
yield return new WaitForSecondsRealtime(.5f);
yield return s.Shot(folder + "10-skin-eiswalkuere-kampf.png");
s.Close();
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: c13db54917f6463ebc138e12a6922458

View file

@ -188,8 +188,8 @@ namespace Luneraria.UI.Tests
yield return s.Open(); yield return s.Open();
yield return s.Tap(() => s.Hud.HeroCard); yield return s.Tap(() => s.Hud.HeroCard);
Check(s.Hud.ModalOpen, "hero card opens the hero window"); Check(s.Hud.ModalOpen, "hero card opens the hero window");
// H1/H2: every hero of the catalogue (G3: three; Sylvara and Brunhild follow). // H1: every hero of the catalogue (G3: three; Sylvara is the fourth).
Check(s.Hud.HeroDetail.RecruitButtons.Count == LuneContent.Heroes.Count && LuneContent.Heroes.Count == 5, "all five heroes to recruit (H2)"); Check(s.Hud.HeroDetail.RecruitButtons.Count == LuneContent.Heroes.Count && LuneContent.Heroes.Count == 4, "all four heroes to recruit (H1)");
int heroCost = LuneContent.Heroes.Values[0].Cost; int heroCost = LuneContent.Heroes.Values[0].Cost;
bool unaffordable = s.Match.GoldOf(HudSession.Human) < heroCost; bool unaffordable = s.Match.GoldOf(HudSession.Human) < heroCost;
Check(!unaffordable || s.Hud.HeroDetail.RecruitButtons.Values.All(b => b.Disabled), "recruiting blocked without gold"); Check(!unaffordable || s.Hud.HeroDetail.RecruitButtons.Values.All(b => b.Disabled), "recruiting blocked without gold");
@ -205,8 +205,7 @@ namespace Luneraria.UI.Tests
// Items: buy one, the card turns unique; slots fill up; higher items need levels. // Items: buy one, the card turns unique; slots fill up; higher items need levels.
yield return s.Tap(() => s.Hud.HeroDetail.Tabs.Tabs[LuneHeroDetail.TabItems]); yield return s.Tap(() => s.Hud.HeroDetail.Tabs.Tabs[LuneHeroDetail.TabItems]);
Dictionary<string, LuneItemSlot> cards = s.Hud.HeroDetail.ItemCards; Dictionary<string, LuneItemSlot> cards = s.Hud.HeroDetail.ItemCards;
// S2: recipes only under the extended rules; the verify plays the reference rules. Check(cards.Count == LuneContent.Items.Count, "item shop lists every item");
Check(cards.Count == LuneContent.Items.Values.Count(item => !item.IsRecipe || s.Match.Extended), "item shop lists every item of the rules in play");
string first = cards.First(c => c.Value.Current == LuneItemSlot.State.Available).Key; string first = cards.First(c => c.Value.Current == LuneItemSlot.State.Available).Key;
yield return s.Tap(() => cards[first]); yield return s.Tap(() => cards[first]);
Check(s.Model.OwnHero.Items.Contains(first), "item bought by click"); Check(s.Model.OwnHero.Items.Contains(first), "item bought by click");

View file

@ -69,8 +69,7 @@ namespace Luneraria.UI.Tests
StringAssert.Contains("davon 15 Söldner", texts, "the gold display shows the champion income"); StringAssert.Contains("davon 15 Söldner", texts, "the gold display shows the champion income");
StringAssert.Contains("SEGEN 2/4", texts, "the team header shows the Mondsegen"); StringAssert.Contains("SEGEN 2/4", texts, "the team header shows the Mondsegen");
Assert.IsTrue(HudSession.Visible(s.Hud.Lanes["mid"].Barracks), "mid shows the barracks mark"); Assert.IsTrue(HudSession.Visible(s.Hud.Lanes["mid"].Barracks), "mid shows the barracks mark");
bool topBroken = s.Match.BarracksBroken(0, "top") || s.Match.BarracksBroken(1, "top"); Assert.IsFalse(HudSession.Visible(s.Hud.Lanes["top"].Barracks), "top shows none");
Assert.AreEqual(topBroken, HudSession.Visible(s.Hud.Lanes["top"].Barracks), "top shows the mark exactly when an inner top tower fell");
Assert.IsTrue(s.Me.Hero.Exhausted, "the fallen hero is exhausted"); Assert.IsTrue(s.Me.Hero.Exhausted, "the fallen hero is exhausted");
s.Hud.OpenHero(); s.Hud.OpenHero();
yield return HudSession.Frames(2); yield return HudSession.Frames(2);

View file

@ -1,130 +0,0 @@
using System;
using System.Collections;
using System.Linq;
using Luneraria.Core;
using Luneraria.View;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace Luneraria.UI.Tests
{
/// <summary>
/// S2 in the real scene: the team actions (glyph lane, Ancient level) in the team panel, the
/// recipe in the hero window's item shop, the glyph ring on an invulnerable tower and a
/// levelled Ancient in combat. S2Bilder (explicit) captures them at 1920×1080;
/// LUNE_EVIDENCE names the folder below docs/port/screenshots (default s2/spiel).
/// </summary>
public sealed class S2RulesViewsTests
{
static void Send(HudSession s, Command command)
{
CommandResult result = s.Match.Submit(HudSession.Human, command);
Assert.IsTrue(result.Ok, command.Type + ": " + result.Message);
s.Game.RebuildPreview();
s.Sync();
}
static IEnumerator Plan(HudSession s)
{
Assert.IsTrue(s.Match.Extended, "extended match");
yield return s.Starter();
s.Leave();
Send(s, Command.HeroBuy("held_ringborke"));
Send(s, Command.ItemBuy("basaltschild"));
Send(s, Command.ItemBuy("rankenmantel"));
// Public fixture: Blau's outer mid tower is already damaged.
s.Match.BuildingHp["blau_mid_outer"] = s.Match.Map.BuildingDef("blau_mid_outer").MaxHp * 0.55;
s.Sync();
}
static GameObject Ring(string id)
{
return UnityEngine.Object.FindObjectsByType<Transform>(FindObjectsInactive.Include, FindObjectsSortMode.None)
.Where(t => t.name == "GlyphRing_" + id).Select(t => t.gameObject).FirstOrDefault();
}
[UnityTest, Timeout(600000)]
public IEnumerator S2Hud()
{
var s = new HudSession();
yield return s.Open(1920, 1080, seed: 4, startRound: 6, extraGold: 3000, extended: true);
yield return Plan(s);
Assert.IsTrue(HudSession.Visible(s.Hud.Team.CoreButton), "team actions are visible under the extended rules");
s.Click(s.Hud.Team.GlyphButtons["mid"]);
yield return HudSession.Frames(2);
s.Sync();
Assert.AreEqual("mid", s.Me.Draft.GlyphLane, "the mid button holds the glyph");
Assert.IsTrue(s.Hud.Team.GlyphButtons["mid"].On, "the button shows it");
int gold = s.Me.Draft.Gold;
s.Click(s.Hud.Team.CoreButton);
yield return HudSession.Frames(2);
s.Sync();
Assert.AreEqual(1, s.Me.Draft.CoreInvest, "the Ancient button buys a level");
Assert.AreEqual(gold - 150, s.Me.Draft.Gold, "for 150 gold");
s.Hud.OpenHero(LuneHeroDetail.TabItems);
yield return HudSession.Frames(2);
s.Sync();
Assert.IsTrue(s.Hud.HeroDetail.ItemCards.ContainsKey("bollwerk"), "recipes appear in the shop");
Assert.AreEqual(LuneItemSlot.State.Available, s.Hud.HeroDetail.ItemCards["bollwerk"].Current, "Bollwerk can be forged");
Assert.AreEqual(LuneItemSlot.State.MissingParts, s.Hud.HeroDetail.ItemCards["glutsturm"].Current, "Glutsturm lacks its parts");
s.Click(s.Hud.HeroDetail.ItemCards["bollwerk"]);
yield return HudSession.Frames(2);
s.Sync();
CollectionAssert.AreEqual(new[] { "bollwerk" }, s.Me.Draft.Hero.Items, "the shop card forges Bollwerk");
s.Hud.CloseModal();
s.Game.Key(KeyCode.Space);
yield return HudSession.Frames(2);
Assert.AreEqual("mid", s.Match.Combat.GlyphLane[0], "Blau holds the glyph on mid");
LuneUnit core = s.Match.Combat.Buildings["blau_ancient"];
Assert.AreEqual(45.0, core.Def.Damage, 1e-9, "the Ancient shoots at level 1");
LuneUnit tower = s.Match.Combat.Buildings["blau_mid_outer"];
tower.Hp = tower.MaxHp * .45;
s.Runner.Advance(.05);
yield return HudSession.Frames(3);
Assert.IsTrue(s.Match.Combat.Glyphed(tower), "the glyph fires below half health");
GameObject ring = Ring("blau_mid_outer");
Assert.IsTrue(ring != null && ring.activeSelf, "the tower shows the glyph ring");
s.Close();
}
[UnityTest, Timeout(600000), Explicit("Bildwerkzeug: nur gezielt per -testFilter")]
public IEnumerator S2Bilder()
{
string evidence = Environment.GetEnvironmentVariable("LUNE_EVIDENCE");
string folder = "docs/port/screenshots/" + (string.IsNullOrEmpty(evidence) ? "s2/spiel" : evidence) + "/";
var s = new HudSession();
yield return s.Open(1920, 1080, seed: 4, startRound: 6, extraGold: 3000, extended: true);
yield return Plan(s);
Send(s, Command.Glyph("mid"));
Send(s, Command.CoreUpgrade());
yield return new WaitForSecondsRealtime(.5f);
yield return s.Shot(folder + "01-team-aktionen.png");
s.Hover(s.Hud.Team.CoreButton);
yield return new WaitForSecondsRealtime(.3f);
yield return s.Shot(folder + "02-ancient-tooltip.png");
s.Leave();
s.Hud.OpenHero(LuneHeroDetail.TabItems);
yield return new WaitForSecondsRealtime(.6f);
s.Hover(s.Hud.HeroDetail.ItemCards["bollwerk"]);
yield return new WaitForSecondsRealtime(.3f);
yield return s.Shot(folder + "03-rezept-im-shop.png");
Send(s, Command.ItemBuy("bollwerk"));
s.Leave();
yield return new WaitForSecondsRealtime(.4f);
yield return s.Shot(folder + "04-bollwerk-geschmiedet.png");
s.Hud.CloseModal();
s.Game.Key(KeyCode.Space);
yield return HudSession.Frames(2);
LuneUnit tower = s.Match.Combat.Buildings["blau_mid_outer"];
tower.Hp = tower.MaxHp * .45;
s.Runner.Advance(.05);
yield return s.Fight(1.0);
s.Driver.Paused = true;
s.Rig.LookAt(tower.Position, 14f, true);
yield return new WaitForSecondsRealtime(.5f);
yield return s.Shot(folder + "05-glyphe-am-turm.png");
s.Close();
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: f22ed633e8804b2f85703375622e0e99

View file

@ -1,113 +0,0 @@
using System;
using System.Collections;
using System.Linq;
using Luneraria.Core;
using Luneraria.View;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace Luneraria.UI.Tests
{
/// <summary>
/// S3 in the real scene: camp guardians on the lanes in a camp round, the team panel's ward
/// and camp rows, a ward that brings the enemy army of its lane into the planning preview,
/// and the fog of war that hides enemy figures nobody of the own team sees. S3Bilder
/// (explicit) captures them at 1920×1080; LUNE_EVIDENCE names the folder below
/// docs/port/screenshots (default s3/spiel).
/// </summary>
public sealed class S3RulesViewsTests
{
static void Send(HudSession s, Command command)
{
CommandResult result = s.Match.Submit(HudSession.Human, command);
Assert.IsTrue(result.Ok, command.Type + ": " + result.Message);
s.Game.RebuildPreview();
s.Sync();
}
static IEnumerator WaitReady(HudSession s)
{
float waited = 0f;
while (s.Hud.ReadyBlocked() && waited < 5f)
{
waited += Time.unscaledDeltaTime;
yield return null;
}
}
static LuneUnitView[] Views(HudSession s)
{
return UnityEngine.Object.FindObjectsByType<LuneUnitView>(FindObjectsInactive.Include, FindObjectsSortMode.None);
}
[UnityTest, Timeout(600000)]
public IEnumerator S3Hud()
{
var s = new HudSession();
yield return s.Open(1920, 1080, seed: 4, startRound: 3, extraGold: 1000, extended: true);
Assert.IsTrue(s.Match.CampRound(3), "round 3 is a camp round");
StringAssert.Contains("diese Runde", s.Hud.Team.CampState.text, "the team panel announces the guardians");
Assert.IsTrue(HudSession.Visible(s.Hud.Lanes["mid"].CampMark), "the lane shows the camp mark");
yield return s.Starter();
s.Leave();
s.Game.Key(KeyCode.Space);
yield return HudSession.Frames(3);
Assert.AreEqual(3, s.Match.Combat.Units.Count(u => u.Kind == "camp"), "three guardians in combat");
yield return HudSession.Frames(2);
Assert.IsTrue(Views(s).Any(v => v.Unit != null && v.Unit.Kind == "camp" && v.gameObject.activeSelf), "guardians have visible figures");
Assert.IsTrue(s.Game.View.Hidden.Count > 0, "the fog hides Rot's distant units at the start");
LuneUnitView hiddenView = Views(s).First(v => v.Unit != null && s.Game.View.Hidden.Contains(v.Unit.Id));
Assert.IsFalse(hiddenView.gameObject.activeSelf, "a hidden enemy has no visible figure");
Assert.AreEqual(1, hiddenView.Unit.Team, "only enemies are hidden");
yield return s.FinishCombat();
yield return WaitReady(s);
Assert.IsTrue(s.Match.Players.Skip(3).Any(p => p.Roster.Count > 0), "Rot has a committed army now");
int enemiesBefore = s.Game.View.Preview.Units.Count(u => u.Team == 1);
Assert.AreEqual(0, enemiesBefore, "without scouting the preview shows no enemy");
Send(s, Command.Ward("mid"));
yield return HudSession.Frames(2);
Assert.IsTrue(s.Hud.Team.WardButtons["mid"].On, "the mid ward button shows the ward");
Assert.IsTrue(s.Game.View.Preview.Units.Any(u => u.Team == 1 && u.Lane == "mid"), "the ward shows Rot's mid army in the preview");
Assert.IsFalse(s.Game.View.Preview.Units.Any(u => u.Team == 1 && u.Lane == "top"), "top stays unscouted");
s.Close();
}
[UnityTest, Timeout(600000), Explicit("Bildwerkzeug: nur gezielt per -testFilter")]
public IEnumerator S3Bilder()
{
string evidence = Environment.GetEnvironmentVariable("LUNE_EVIDENCE");
string folder = "docs/port/screenshots/" + (string.IsNullOrEmpty(evidence) ? "s3/spiel" : evidence) + "/";
var s = new HudSession();
yield return s.Open(1920, 1080, seed: 4, startRound: 3, extraGold: 1000, extended: true);
yield return s.Starter();
s.Leave();
yield return new WaitForSecondsRealtime(.4f);
yield return s.Shot(folder + "01-planung-lager-angekuendigt.png");
s.Game.Key(KeyCode.Space);
yield return HudSession.Frames(2);
yield return s.Fight(7.0);
s.Driver.Paused = true;
LuneUnit camp = s.Match.Combat.Units.Where(u => u.Kind == "camp" && u.Lane == "mid").First();
s.Rig.LookAt(camp.Position, 14f, true);
yield return new WaitForSecondsRealtime(.5f);
yield return s.Shot(folder + "02-kampf-um-das-mondlager.png");
s.Rig.LookAt(camp.Position, 40f, true);
yield return new WaitForSecondsRealtime(.5f);
yield return s.Shot(folder + "03-nebel-im-kampf.png");
s.Driver.Paused = false;
yield return s.FinishCombat();
yield return WaitReady(s);
Send(s, Command.Ward("mid"));
LuneUnit enemy = s.Game.View.Preview.Units.FirstOrDefault(u => u.Team == 1 && u.Lane == "mid");
if (enemy != null)
s.Rig.LookAt(enemy.Position, 18f, true);
yield return new WaitForSecondsRealtime(.6f);
yield return s.Shot(folder + "04-spaeher-zeigt-gegnerische-aufstellung.png");
s.Hover(s.Hud.Team.WardButtons["mid"]);
yield return new WaitForSecondsRealtime(.3f);
yield return s.Shot(folder + "05-spaeher-tooltip.png");
s.Close();
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: b8e0bd7c590a42fcb9c90dfa0e1458fa

View file

@ -1,87 +0,0 @@
using System;
using System.Collections;
using System.Linq;
using Luneraria.Core;
using Luneraria.View;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
namespace Luneraria.UI.Tests
{
/// <summary>
/// S4 in the real scene: the fog of war darkens the terrain outside the team's vision (light
/// at the own base, dark at the enemy base), and a stunned unit shows the gold ring over its
/// head. S4Bilder (explicit) captures both at 1920×1080; LUNE_EVIDENCE names the folder below
/// docs/port/screenshots (default s4/spiel).
/// </summary>
public sealed class S4PolishViewsTests
{
static float AlphaAt(LuneFogOverlay fog, Vec2 point)
{
int side = Mathf.CeilToInt((float)(LuneMap.HalfExtent * 2.0) / LuneFogOverlay.Cell) + 1;
int column = Mathf.Clamp(Mathf.RoundToInt((float)((point.X + LuneMap.HalfExtent) / LuneFogOverlay.Cell)), 0, side - 1);
int row = Mathf.Clamp(Mathf.RoundToInt((float)((point.Y + LuneMap.HalfExtent) / LuneFogOverlay.Cell)), 0, side - 1);
return fog.Alpha[row * side + column];
}
static IEnumerator Fight(HudSession s)
{
yield return s.Starter();
s.Leave();
s.Game.Key(KeyCode.Space);
yield return HudSession.Frames(2);
yield return s.Fight(4.0);
yield return HudSession.Frames(4);
}
[UnityTest, Timeout(600000)]
public IEnumerator S4Views()
{
var s = new HudSession();
yield return s.Open(1920, 1080, seed: 4, startRound: 6, extraGold: 500, extended: true);
yield return Fight(s);
LuneFogOverlay fog = s.Game.View.Fog;
Assert.IsNotNull(fog, "an extended match with a human seat has the fog overlay");
Assert.IsTrue(fog.gameObject.activeSelf && fog.Refreshes > 0, "the fog is active and refreshed");
Assert.Less(AlphaAt(fog, s.Match.Map.CorePosition(0)), .05f, "the own Ancient stands in light");
Assert.Greater(AlphaAt(fog, s.Match.Map.CorePosition(1)), .5f, "the enemy Ancient lies in the fog");
LuneUnit victim = s.Match.Combat.Units.First(u => u.IsMobile() && u.Alive && u.Team == 0);
victim.ApplyEffect("valkyrie_stun", "stun", 1.0, 3.0);
yield return HudSession.Frames(3);
LuneUnitView view = UnityEngine.Object.FindObjectsByType<LuneUnitView>(FindObjectsSortMode.None).First(v => v.Unit == victim);
Transform ring = view.transform.Find("StunRing");
Assert.IsTrue(ring != null && ring.gameObject.activeSelf, "a stunned unit shows the stun ring");
s.Close();
var reference = new HudSession();
yield return reference.Open(1920, 1080, seed: 4, startRound: 6, extraGold: 500);
yield return Fight(reference);
Assert.IsTrue(reference.Game.View.Fog == null || !reference.Game.View.Fog.gameObject.activeSelf, "no fog under the reference rules");
reference.Close();
}
[UnityTest, Timeout(600000), Explicit("Bildwerkzeug: nur gezielt per -testFilter")]
public IEnumerator S4Bilder()
{
string evidence = Environment.GetEnvironmentVariable("LUNE_EVIDENCE");
string folder = "docs/port/screenshots/" + (string.IsNullOrEmpty(evidence) ? "s4/spiel" : evidence) + "/";
var s = new HudSession();
yield return s.Open(1920, 1080, seed: 4, startRound: 6, extraGold: 500, extended: true);
yield return Fight(s);
s.Driver.Paused = true;
s.Rig.LookAt(new Vec2(0.0, 0.0), 60f, true);
yield return new WaitForSecondsRealtime(.6f);
yield return s.Shot(folder + "01-nebel-uebersicht.png");
LuneUnit front = s.Match.Combat.Units.Where(u => u.IsMobile() && u.Alive && u.Team == 0 && u.Lane == "mid").OrderByDescending(u => u.Progress).First();
s.Rig.LookAt(front.Position, 22f, true);
yield return new WaitForSecondsRealtime(.5f);
yield return s.Shot(folder + "02-nebelgrenze-mid.png");
foreach (LuneUnit unit in s.Match.Combat.Units.Where(u => u.IsMobile() && u.Alive && u.Position.DistanceTo(front.Position) < 6.0))
unit.ApplyEffect("valkyrie_stun", "stun", 1.0, 10.0);
s.Rig.LookAt(front.Position, 9f, true);
yield return new WaitForSecondsRealtime(.6f);
yield return s.Shot(folder + "03-betaeubung.png");
s.Close();
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 62a4209ed52348a182c63f43dc65a85b

View file

@ -430,15 +430,6 @@ namespace Luneraria.UI
} }
unit.PreviousPosition = unit.Position; unit.PreviousPosition = unit.Position;
} }
// S3 (extended rules): scouted lanes show the enemy's committed army in its formation.
var enemyRows = new Dictionary<string, int>();
foreach (LuneMatch.ArmyEntry entry in LuneFog.ScoutedEnemies(Match, player.Team, player.Draft))
{
int index = enemyRows.GetValueOrDefault(entry.Lane, 0);
enemyRows[entry.Lane] = index + 1;
LuneUnit unit = preview.Spawn(entry.Kind, 1 - player.Team, entry.Lane, entry.Definition, Math.Max(.4, 7.2 - (index / 4) * 1.5), (index % 4 - 1.5) * 1.2, entry.Slot, entry.Uid);
unit.PreviousPosition = unit.Position;
}
View.Preview = preview; View.Preview = preview;
} }
@ -459,10 +450,6 @@ namespace Luneraria.UI
Bind(); Bind();
} }
TrackStage(); TrackStage();
// S3 fog of war: only a human seat in an extended match sees through its team's eyes.
View.FogSlot = Human && Match.Extended ? Seat : -1;
if (Hud != null && Hud.Model != null)
Hud.Model.Hidden = View.Hidden;
UpdateDeployment(); UpdateDeployment();
HandleKeys(); HandleKeys();
HandleClicks(); HandleClicks();

View file

@ -137,7 +137,7 @@ namespace Luneraria.UI
Subtitle = spec.RoleText + " · " + spec.Cost + " Gold · Einsatz in Welle " + round + " – " + LuneUi.Upper(LuneMap.LaneNames[TargetLane]), Subtitle = spec.RoleText + " · " + spec.Cost + " Gold · Einsatz in Welle " + round + " – " + LuneUi.Upper(LuneMap.LaneNames[TargetLane]),
// S1 (Legion TD 2): sending is also an investment in income. // S1 (Legion TD 2): sending is also an investment in income.
Text = spec.Text + (_model != null && _model.State.Extended ? "\nSöldner-Einkommen: +" + _model.State.ChampionIncome(spec.Cost) + " Gold in jeder folgenden Runde, dauerhaft." : ""), Text = spec.Text + (_model != null && _model.State.Extended ? "\nSöldner-Einkommen: +" + _model.State.ChampionIncome(spec.Cost) + " Gold in jeder folgenden Runde, dauerhaft." : ""),
Note = "Einmaliger Einsatz: kämpft eine Welle lang und verschwindet danach. Die Ankündigung ist für alle sichtbar; sein Tod bringt dem Gegner " + (_model != null ? _model.State.ChampionBountyFor(definition.Bounty) : definition.Bounty) + " Gold.", Note = "Einmaliger Einsatz: kämpft eine Welle lang und verschwindet danach. Die Ankündigung ist für alle sichtbar; sein Tod bringt dem Gegner " + definition.Bounty + " Gold.",
Warn = Cards[championId].Reason, Warn = Cards[championId].Reason,
}; };
tip.Stats.AddRange(LuneStats.Rows(definition)); tip.Stats.AddRange(LuneStats.Rows(definition));

View file

@ -115,18 +115,6 @@ namespace Luneraria.UI
readonly List<Row> _rows = new List<Row>(); readonly List<Row> _rows = new List<Row>();
readonly Label _header; readonly Label _header;
/// <summary>S2 team actions (extended rules): glyph lane toggles and the Ancient level.</summary>
public event Action<Command> CommandRequested;
public readonly Dictionary<string, LuneButton> GlyphButtons = new Dictionary<string, LuneButton>();
public LuneButton CoreButton;
/// <summary>S3 wards per lane (fog of war).</summary>
public readonly Dictionary<string, LuneButton> WardButtons = new Dictionary<string, LuneButton>();
VisualElement _actions;
Label _glyphState;
Label _coreState;
/// <summary>S3: running camp buffs of the own team and the next camp round.</summary>
public Label CampState;
string _actionSignature = "";
readonly LuneGlyphElement _emblem; readonly LuneGlyphElement _emblem;
string _signature = ""; string _signature = "";
@ -157,122 +145,6 @@ namespace Luneraria.UI
int captured = index; int captured = index;
LuneTooltip.Attach(element, () => Tip(captured)); LuneTooltip.Attach(element, () => Tip(captured));
} }
// S2: team actions; hidden under the reference rules.
_actions = LuneUi.Box(true, 3f, column);
_actions.style.marginTop = 4f;
VisualElement glyphRow = LuneUi.Box(false, 4f, _actions);
LuneUi.Glyph("tower", LuneUiTheme.BronzeLight, 14f, glyphRow);
LuneUi.Label("GLYPHE", "HudCaps", glyphRow).style.minWidth = 64f;
foreach (string lane in LuneMap.Lanes)
{
var button = new LuneButton(lane == "bot" ? "BOT" : LuneUi.Upper(LuneMap.LaneNames[lane]), LuneButtons.HudToggle, glyphRow);
string captured = lane;
button.Clicked += () => CommandRequested?.Invoke(Command.Glyph(button.On ? "" : captured));
GlyphButtons[lane] = button;
}
_glyphState = LuneUi.Label("", "HudMuted", glyphRow);
LuneTooltip.Attach(glyphRow, () => new LuneTip
{
Title = "Glyphe (wie in Dota 2)", Glyph = "tower", GlyphColor = LuneUiTheme.BronzeLight,
Text = "Halte die Glyphe für eine Lane bereit: Fällt dort im Kampf ein eigener Turm unter 50 %, werden die Türme der Lane 5 s unverwundbar. Gilt für das ganze Team; danach 4 Runden Abklingzeit. Löst sie nicht aus, bleibt sie bereit.",
Note = "Entscheidet sich ein Mitspieler auf einem früheren Platz anders, gilt seine Wahl.",
});
VisualElement coreRow = LuneUi.Box(false, 4f, _actions);
LuneUi.Glyph("crown", LuneUiTheme.BronzeLight, 14f, coreRow);
LuneUi.Label("ANCIENT", "HudCaps", coreRow).style.minWidth = 64f;
_coreState = LuneUi.Label("", "HudText", coreRow);
_coreState.style.flexGrow = 1f;
CoreButton = new LuneButton("", LuneButtons.HudButton, coreRow);
CoreButton.Clicked += () => CommandRequested?.Invoke(Command.CoreUpgrade());
LuneTooltip.Attach(coreRow, () => new LuneTip
{
Title = "Ancient stärken (wie der König in Legion TD 2)", Glyph = "crown", GlyphColor = LuneUiTheme.BronzeLight,
Text = "Jeder im Team kann Stufen kaufen (höchstens 3). Jede Stufe repariert den Ancient um 1.000 und lässt ihn auf Angreifer in 7,5 m schießen: 45 / 75 / 110 Schaden pro Sekunde.",
Note = "Preis: 150 × nächste Stufe.",
});
VisualElement wardRow = LuneUi.Box(false, 4f, _actions);
LuneUi.Glyph("star", LuneUiTheme.BronzeLight, 14f, wardRow);
LuneUi.Label("SPÄHER", "HudCaps", wardRow).style.minWidth = 64f;
foreach (string lane in LuneMap.Lanes)
{
var button = new LuneButton("", LuneButtons.HudToggle, wardRow);
string captured = lane;
button.Clicked += () => CommandRequested?.Invoke(Command.Ward(captured));
WardButtons[lane] = button;
}
LuneTooltip.Attach(wardRow, () => new LuneTip
{
Title = "Späher (wie die Wards in Dota 2)", Glyph = "star", GlyphColor = LuneUiTheme.BronzeLight,
Text = "Nebel des Krieges: Gegner siehst du nur nahe deiner Einheiten (9 m) und Gebäude (10 m). Ein Späher kostet 40 Gold, wacht 3 Runden über die Mitte seiner Lane (12 m) und zeigt in der Planung die gegnerische Aufstellung dort. Ist der äußere Turm des Gegners gefallen, ist die Lane auch ohne Späher aufgeklärt.",
Note = "Die Zahl auf dem Knopf: verbleibende Runden des Team-Spähers.",
});
VisualElement campRow = LuneUi.Box(false, 4f, _actions);
LuneUi.Glyph("emblem_2", LuneUiTheme.Team[2], 14f, campRow);
LuneUi.Label("LAGER", "HudCaps", campRow).style.minWidth = 64f;
CampState = LuneUi.Clip(LuneUi.Label("", "HudText", campRow));
CampState.style.flexGrow = 1f;
LuneTooltip.Attach(campRow, () => new LuneTip
{
Title = "Neutrallager (wie die Buff-Camps in League of Legends)", Glyph = "emblem_2", GlyphColor = LuneUiTheme.Team[2],
Text = "Jede dritte Runde steht in der Mitte jeder Lane ein neutraler Wächter, der beide Teams angreift. Fällt er, nimmt das Team mit mehr Einheiten in 8 m das Lager (bei Gleichstand: mehr Leben).\nTop · Glutsegen: +10 % Schaden, 2 Kämpfe.\nMid · Mondschild: Schild von 10 % Leben zu Kampfbeginn, 2 Kämpfe.\nBot · Beute: sofort 50 Gold für jeden im Team.",
});
LuneUi.Show(_actions, false);
}
/// <summary>S2: glyph and Ancient level of the own team (planning commands; hidden under the reference rules).</summary>
public void ApplyActions(LuneHudModel model)
{
LuneMatch state = model.State;
bool on = state.Extended;
int team = model.Team;
Draft draft = model.Me.Draft;
int ready = state.Boss.GlyphReady[team];
int level = state.Boss.CoreLevel[team];
int pending = model.Planning ? draft.CoreInvest : 0;
string held = model.Planning ? draft.GlyphLane : "";
int[] buffs = state.Boss.CampBuff;
string signature = string.Join(",", buffs) + "|" + string.Join(",", state.Boss.Wards) + "|" + string.Join(",", draft.WardLanes) + "|" + on + "|" + model.Planning + "|" + model.Locked + "|" + ready + "|" + state.RoundNumber + "|" + level + "|" + pending + "|" + held + "|" + model.OwnGold;
if (signature == _actionSignature)
return;
_actionSignature = signature;
LuneUi.Show(_actions, on);
if (!on)
return;
bool canAct = model.Planning && !model.Locked;
bool glyphReady = state.RoundNumber >= ready;
foreach (KeyValuePair<string, LuneButton> pair in GlyphButtons)
{
pair.Value.On = held == pair.Key;
pair.Value.Disabled = !canAct || !glyphReady;
}
_glyphState.text = glyphReady ? (held.Length > 0 ? "bereit" : "") : "ab Runde " + ready;
int shown = level + pending;
_coreState.text = "STUFE " + shown + "/" + state.Balance.CoreLevelMax + (pending > 0 ? " (+" + pending + ")" : "");
bool max = shown >= state.Balance.CoreLevelMax;
int cost = max ? 0 : state.Balance.CoreUpgradeCost(shown);
CoreButton.Text = max ? "MAX" : "+" + cost + " G";
CoreButton.Disabled = !canAct || max || model.OwnGold < cost;
foreach (KeyValuePair<string, LuneButton> pair in WardButtons)
{
int left = state.Boss.Wards[team * 3 + LuneMap.LaneIndex(pair.Key)];
bool placed = model.Planning && draft.WardLanes.Contains(pair.Key);
string name = pair.Key == "bot" ? "BOT" : LuneUi.Upper(LuneMap.LaneNames[pair.Key]);
pair.Value.Text = placed ? name + " +" + state.Balance.WardRounds : left > 0 ? name + " " + left : name;
pair.Value.On = placed || left > 0;
pair.Value.Disabled = !canAct || placed || model.OwnGold < state.Balance.WardCost;
}
var parts = new List<string>();
for (int lane = 0; lane < 2; lane++)
{
int left = buffs[team * 3 + lane];
if (left > 0)
parts.Add(LuneMatch.CampBuffNames[lane] + " " + left);
}
int next = state.RoundNumber;
while (!state.CampRound(next) && next < state.RoundNumber + 10)
next++;
string upcoming = next == state.RoundNumber ? "Wächter diese Runde" : "Wächter in Runde " + next;
CampState.text = parts.Count > 0 ? string.Join(" · ", parts) + " · " + upcoming : upcoming;
} }
/// <summary>S1: Mondsegen stacks of the own team (-1 under the reference rules).</summary> /// <summary>S1: Mondsegen stacks of the own team (-1 under the reference rules).</summary>
@ -529,19 +401,17 @@ namespace Luneraria.UI
/// </summary> /// </summary>
public sealed class LuneItemSlot : LuneButton public sealed class LuneItemSlot : LuneButton
{ {
public enum State { Empty, Equipped, Available, Unaffordable, Incompatible, Unique, Full, Locked, MissingParts } public enum State { Empty, Equipped, Available, Unaffordable, Incompatible, Unique, Full, Locked }
static readonly Dictionary<State, string> StateText = new Dictionary<State, string> static readonly Dictionary<State, string> StateText = new Dictionary<State, string>
{ {
{ State.Equipped, "AUSGERÜSTET" }, { State.Available, "KAUFBAR" }, { State.Unaffordable, "ZU TEUER" }, { State.Incompatible, "STUFE FEHLT" }, { State.Equipped, "AUSGERÜSTET" }, { State.Available, "KAUFBAR" }, { State.Unaffordable, "ZU TEUER" }, { State.Incompatible, "STUFE FEHLT" },
{ State.Unique, "GETRAGEN" }, { State.Full, "PLÄTZE VOLL" }, { State.Locked, "GESPERRT" }, { State.Empty, "LEER" }, { State.MissingParts, "TEILE FEHLEN" }, { State.Unique, "GETRAGEN" }, { State.Full, "PLÄTZE VOLL" }, { State.Locked, "GESPERRT" }, { State.Empty, "LEER" },
}; };
static readonly Dictionary<string, Color> ItemColors = new Dictionary<string, Color> static readonly Dictionary<string, Color> ItemColors = new Dictionary<string, Color>
{ {
{ "basaltschild", LuneUiTheme.Hex("9fb2c8") }, { "glutklinge", LuneUiTheme.Hex("ff9a5a") }, { "rankenmantel", LuneUiTheme.Hex("8fd08a") }, { "basaltschild", LuneUiTheme.Hex("9fb2c8") }, { "glutklinge", LuneUiTheme.Hex("ff9a5a") }, { "rankenmantel", LuneUiTheme.Hex("8fd08a") },
{ "funkenfokus", LuneUiTheme.Hex("f5d76a") }, { "quellamulett", LuneUiTheme.Hex("7ad6e6") }, { "kristallsplitter", LuneUiTheme.Hex("c7a2ff") }, { "funkenfokus", LuneUiTheme.Hex("f5d76a") }, { "quellamulett", LuneUiTheme.Hex("7ad6e6") }, { "kristallsplitter", LuneUiTheme.Hex("c7a2ff") },
// S2 recipes: the colour of their first part, a little brighter.
{ "bollwerk", LuneUiTheme.Hex("c3d3e6") }, { "glutsturm", LuneUiTheme.Hex("ffb27a") }, { "quellherz", LuneUiTheme.Hex("a2e8f2") }, { "splitterkrone", LuneUiTheme.Hex("dcc2ff") },
}; };
public string ItemId = ""; public string ItemId = "";
@ -615,13 +485,13 @@ namespace Luneraria.UI
} }
LuneContent.Items.TryGetValue(ItemId, out ItemSpec item); LuneContent.Items.TryGetValue(ItemId, out ItemSpec item);
_name.text = item != null ? item.Name : ""; _name.text = item != null ? item.Name : "";
_cost.text = item == null ? "" : item.IsRecipe ? "+" + item.RecipeFee : item.Cost.ToString(); _cost.text = item != null ? item.Cost.ToString() : "";
LuneUi.SetColor(_cost, state == State.Unaffordable ? LuneUiTheme.Danger : LuneUiTheme.Bright); LuneUi.SetColor(_cost, state == State.Unaffordable ? LuneUiTheme.Danger : LuneUiTheme.Bright);
_effect.text = item != null ? item.Text : ""; _effect.text = item != null ? item.Text : "";
Color badge = state == State.Equipped ? LuneUiTheme.Good : state == State.Available ? LuneUiTheme.BronzeLight : LuneUiTheme.Muted; Color badge = state == State.Equipped ? LuneUiTheme.Good : state == State.Available ? LuneUiTheme.BronzeLight : LuneUiTheme.Muted;
if (state == State.Unaffordable) if (state == State.Unaffordable)
badge = LuneUiTheme.Danger; badge = LuneUiTheme.Danger;
string glyph = state == State.Equipped || state == State.Unique ? "check" : state == State.Incompatible || state == State.Locked || state == State.MissingParts ? "lock" : ""; string glyph = state == State.Equipped || state == State.Unique ? "check" : state == State.Incompatible || state == State.Locked ? "lock" : "";
_badge.SetState(StateText[state], badge, glyph, state == State.Equipped); _badge.SetState(StateText[state], badge, glyph, state == State.Equipped);
Disabled = !usable; Disabled = !usable;
} }

View file

@ -352,11 +352,7 @@ namespace Luneraria.UI
portrait.style.height = 170f; portrait.style.height = 170f;
column.Add(portrait); column.Add(portrait);
portrait.ShowModel(spec.Model, true); portrait.ShowModel(spec.Model, true);
// H2: five heroes share the window; the columns get more compact. LuneUi.Label(spec.Name, "HudTitle", column);
bool compact = LuneContent.Heroes.Count > 4;
Label title = LuneUi.Clip(LuneUi.Label(spec.Name, "HudTitle", column));
if (compact)
title.style.fontSize = 24f;
LuneUi.Label(spec.Title, "HudMuted", column); LuneUi.Label(spec.Title, "HudMuted", column);
UnitDef definition = model.HeroDefinition(new HeroState { HeroId = heroId }); UnitDef definition = model.HeroDefinition(new HeroState { HeroId = heroId });
VisualElement role = LuneUi.Box(false, 5f, column); VisualElement role = LuneUi.Box(false, 5f, column);
@ -366,7 +362,7 @@ namespace Luneraria.UI
{ {
VisualElement line = LuneUi.Box(false, 6f, column); VisualElement line = LuneUi.Box(false, 6f, column);
LuneUi.Glyph(entry[0], LuneUiTheme.Muted, 13f, line); LuneUi.Glyph(entry[0], LuneUiTheme.Muted, 13f, line);
LuneUi.Label(entry[1], "HudMuted", line).style.minWidth = compact ? 88f : 104f; LuneUi.Label(entry[1], "HudMuted", line).style.minWidth = 104f;
LuneUi.Clip(LuneUi.Label(entry[2], "HudText", line)).style.flexGrow = 1f; LuneUi.Clip(LuneUi.Label(entry[2], "HudText", line)).style.flexGrow = 1f;
} }
Label summary = LuneUi.Wrap(LuneUi.Label(spec.Summary, "HudText", column)); Label summary = LuneUi.Wrap(LuneUi.Label(spec.Summary, "HudText", column));
@ -572,9 +568,6 @@ namespace Luneraria.UI
{ {
string itemId = pair.Key; string itemId = pair.Key;
ItemSpec item = pair.Value; ItemSpec item = pair.Value;
// S2 recipes exist only under the extended rules.
if (item.IsRecipe && !model.State.Extended)
continue;
if (column % 2 == 0) if (column % 2 == 0)
line = LuneUi.Box(false, 8f, grid); line = LuneUi.Box(false, 8f, grid);
column++; column++;
@ -583,19 +576,15 @@ namespace Luneraria.UI
card.style.flexBasis = 0f; card.style.flexBasis = 0f;
line.Add(card); line.Add(card);
LuneItemSlot.State state = LuneItemSlot.State.Available; LuneItemSlot.State state = LuneItemSlot.State.Available;
bool partsMissing = item.IsRecipe && !item.Parts.All(items.Contains);
int price = item.IsRecipe ? item.RecipeFee : item.Cost;
if (items.Contains(itemId)) if (items.Contains(itemId))
state = LuneItemSlot.State.Unique; state = LuneItemSlot.State.Unique;
else if (!canAct) else if (!canAct)
state = LuneItemSlot.State.Locked; state = LuneItemSlot.State.Locked;
else if (partsMissing)
state = LuneItemSlot.State.MissingParts;
else if (level < item.MinLevel) else if (level < item.MinLevel)
state = LuneItemSlot.State.Incompatible; state = LuneItemSlot.State.Incompatible;
else if (!item.IsRecipe && items.Count >= LuneContent.ItemSlots) else if (items.Count >= LuneContent.ItemSlots)
state = LuneItemSlot.State.Full; state = LuneItemSlot.State.Full;
else if (gold < price) else if (gold < item.Cost)
state = LuneItemSlot.State.Unaffordable; state = LuneItemSlot.State.Unaffordable;
card.SetItem(itemId, state); card.SetItem(itemId, state);
ItemCards[itemId] = card; ItemCards[itemId] = card;
@ -607,13 +596,11 @@ namespace Luneraria.UI
}; };
string why = state == LuneItemSlot.State.Unique ? "Dein Held trägt es bereits; Items sind einzigartig." string why = state == LuneItemSlot.State.Unique ? "Dein Held trägt es bereits; Items sind einzigartig."
: state == LuneItemSlot.State.Locked ? WhyNot(model) : state == LuneItemSlot.State.Locked ? WhyNot(model)
: state == LuneItemSlot.State.MissingParts ? "Braucht " + string.Join(" und ", item.Parts.Select(id => LuneContent.Items[id].Name)) + " im Inventar."
: state == LuneItemSlot.State.Incompatible ? "Erfordert Heldenstufe " + item.MinLevel + "." : state == LuneItemSlot.State.Incompatible ? "Erfordert Heldenstufe " + item.MinLevel + "."
: state == LuneItemSlot.State.Full ? "Alle " + LuneContent.ItemSlots + " Plätze belegt; verkaufe zuerst ein Item." : state == LuneItemSlot.State.Full ? "Alle " + LuneContent.ItemSlots + " Plätze belegt; verkaufe zuerst ein Item."
: state == LuneItemSlot.State.Unaffordable ? "Es fehlen " + (price - gold) + " Gold." : state == LuneItemSlot.State.Unaffordable ? "Es fehlen " + (item.Cost - gold) + " Gold."
: item.IsRecipe ? "Klick: schmieden (beide Teile werden eins, ein Platz wird frei)." : "Klick: kaufen und ausrüsten."; : "Klick: kaufen und ausrüsten.";
string title = item.IsRecipe ? item.Name + " · Rezept +" + item.RecipeFee + " Gold" : item.Name + " · " + item.Cost + " Gold"; LuneTooltip.Attach(card, () => new LuneTip { Title = item.Name + " · " + item.Cost + " Gold", Glyph = itemId, GlyphColor = LuneItemSlot.ColorOf(itemId), Text = item.Text, Note = why });
LuneTooltip.Attach(card, () => new LuneTip { Title = title, Glyph = itemId, GlyphColor = LuneItemSlot.ColorOf(itemId), Text = item.Text, Note = why });
} }
} }

View file

@ -107,8 +107,6 @@ namespace Luneraria.UI
VisualElement _system; VisualElement _system;
VisualElement _left; VisualElement _left;
LuneTeamPanel _team; LuneTeamPanel _team;
/// <summary>The own team panel (S2 team actions; tests).</summary>
public LuneTeamPanel Team { get { return _team; } }
VisualElement _right; VisualElement _right;
LunePanel _bottom; LunePanel _bottom;
LuneResourceDisplay _gold; LuneResourceDisplay _gold;
@ -440,7 +438,6 @@ namespace Luneraria.UI
_left.Add(teamPanel); _left.Add(teamPanel);
_team = new LuneTeamPanel(); _team = new LuneTeamPanel();
teamPanel.Add(_team); teamPanel.Add(_team);
_team.CommandRequested += command => CommandRequested?.Invoke(command);
Feed = new LuneEventFeed(); Feed = new LuneEventFeed();
_left.Add(Feed); _left.Add(Feed);
Feed.FocusRequested += FocusTarget; Feed.FocusRequested += FocusTarget;
@ -755,7 +752,6 @@ namespace Luneraria.UI
if (fighting && _mapTick >= MapRate) if (fighting && _mapTick >= MapRate)
{ {
_mapTick = 0f; _mapTick = 0f;
Minimap.Hidden = Model.Hidden;
Minimap.Refresh(Match.Announcements()); Minimap.Refresh(Match.Announcements());
} }
if (_feedTick >= FeedRate) if (_feedTick >= FeedRate)
@ -803,7 +799,6 @@ namespace Luneraria.UI
pair.Value.Apply(Model.Lane(pair.Key), Model.Team, delta, reduced); pair.Value.Apply(Model.Lane(pair.Key), Model.Team, delta, reduced);
_team.Blessing = Model.State.Extended ? Model.State.Boss.Blessing[Model.Team] : -1; _team.Blessing = Model.State.Extended ? Model.State.Boss.Blessing[Model.Team] : -1;
_team.Apply(Model.Members(Model.Team), Model.Team, planning, Model.Networked); _team.Apply(Model.Members(Model.Team), Model.Team, planning, Model.Networked);
_team.ApplyActions(Model);
SyncRight(planning, fighting); SyncRight(planning, fighting);
SyncBottom(planning, fighting); SyncBottom(planning, fighting);
SyncReady(planning, fighting); SyncReady(planning, fighting);
@ -812,7 +807,6 @@ namespace Luneraria.UI
{ {
Minimap.ViewerTeam = Model.Team; Minimap.ViewerTeam = Model.Team;
Minimap.HumanSlot = HumanSlot; Minimap.HumanSlot = HumanSlot;
Minimap.Hidden = Model.Hidden;
Minimap.Refresh(Match.Announcements()); Minimap.Refresh(Match.Announcements());
} }
if (_modal.Open) if (_modal.Open)

View file

@ -36,8 +36,6 @@ namespace Luneraria.UI
readonly LuneGlyphElement _enemyShape; readonly LuneGlyphElement _enemyShape;
/// <summary>S1 barracks: crossed swords in the own team colour (own waves reinforced) or red (enemy waves).</summary> /// <summary>S1 barracks: crossed swords in the own team colour (own waves reinforced) or red (enemy waves).</summary>
public readonly LuneGlyphElement Barracks; public readonly LuneGlyphElement Barracks;
/// <summary>S3: neutral hexagon while a camp guardian stands on the lane this round.</summary>
public readonly LuneGlyphElement CampMark;
float _lowerHold; float _lowerHold;
float _pulse; float _pulse;
string _signature = ""; string _signature = "";
@ -63,8 +61,6 @@ namespace Luneraria.UI
LuneUi.Spacer(head); LuneUi.Spacer(head);
Barracks = LuneUi.Glyph("melee", LuneUiTheme.Muted, 14f, head); Barracks = LuneUi.Glyph("melee", LuneUiTheme.Muted, 14f, head);
LuneUi.Show(Barracks, false); LuneUi.Show(Barracks, false);
CampMark = LuneUi.Glyph("emblem_2", LuneUiTheme.Team[2], 14f, head);
LuneUi.Show(CampMark, false);
_badge = new LuneStatusBadge(); _badge = new LuneStatusBadge();
head.Add(_badge); head.Add(_badge);
VisualElement chain = LuneUi.Box(false, 3f, column); VisualElement chain = LuneUi.Box(false, 3f, column);
@ -130,7 +126,6 @@ namespace Luneraria.UI
Color tint = LuneUiTheme.Threat[Level]; Color tint = LuneUiTheme.Threat[Level];
_badge.SetState(LuneLaneThreat.Names[Level], tint, Level >= 2 ? LuneLaneThreat.Glyphs[Level] : "", Level == LuneLaneThreat.Critical); _badge.SetState(LuneLaneThreat.Names[Level], tint, Level >= 2 ? LuneLaneThreat.Glyphs[Level] : "", Level == LuneLaneThreat.Critical);
LuneUi.Show(Barracks, entry.OwnBarracks || entry.EnemyBarracks); LuneUi.Show(Barracks, entry.OwnBarracks || entry.EnemyBarracks);
LuneUi.Show(CampMark, entry.Camp.Length > 0);
Barracks.Color = entry.EnemyBarracks ? LuneUiTheme.Danger : LuneUiTheme.TeamColor(ownTeam); Barracks.Color = entry.EnemyBarracks ? LuneUiTheme.Danger : LuneUiTheme.TeamColor(ownTeam);
_ownShape.Glyph = "emblem_" + ownTeam; _ownShape.Glyph = "emblem_" + ownTeam;
_ownShape.Color = LuneUiTheme.TeamColor(ownTeam); _ownShape.Color = LuneUiTheme.TeamColor(ownTeam);
@ -206,9 +201,6 @@ namespace Luneraria.UI
tip.Stat("melee", "Kaserne", "gegnerischer innerer Turm gefallen: eure Wellen +25 % und ein Monster mehr"); tip.Stat("melee", "Kaserne", "gegnerischer innerer Turm gefallen: eure Wellen +25 % und ein Monster mehr");
if (Data.EnemyBarracks) if (Data.EnemyBarracks)
tip.Stat("warning", "Kaserne", "euer innerer Turm ist gefallen: gegnerische Wellen +25 % und ein Monster mehr"); tip.Stat("warning", "Kaserne", "euer innerer Turm ist gefallen: gegnerische Wellen +25 % und ein Monster mehr");
// S3 camps (League of Legends): a guardian in the middle of the lane this round.
if (Data.Camp.Length > 0)
tip.Stat("emblem_2", "Lager", Data.Camp);
var notes = new List<string>(); var notes = new List<string>();
foreach (Announcement danger in Data.Dangers) foreach (Announcement danger in Data.Dangers)
{ {

View file

@ -18,9 +18,6 @@ namespace Luneraria.UI
/// </summary> /// </summary>
public sealed class LuneMinimap : VisualElement public sealed class LuneMinimap : VisualElement
{ {
/// <summary>S3 fog of war: enemy units hidden from the viewer (set by the HUD).</summary>
public HashSet<int> Hidden;
public const float Cell = 6f; public const float Cell = 6f;
/// <summary>Radius of the grotto arena (lune_terrain.gd ARENA_RADIUS).</summary> /// <summary>Radius of the grotto arena (lune_terrain.gd ARENA_RADIUS).</summary>
public const float ArenaRadius = 7.8f; public const float ArenaRadius = 7.8f;
@ -157,9 +154,6 @@ namespace Luneraria.UI
{ {
if (!unit.Alive || !unit.IsMobile()) if (!unit.Alive || !unit.IsMobile())
continue; continue;
// S3 fog of war: enemies nobody of the team sees are not drawn.
if (Hidden != null && Hidden.Contains(unit.Id))
continue;
Units++; Units++;
if (unit.Kind == "hero" || unit.Kind == "champion" || unit.Kind == "boss") if (unit.Kind == "hero" || unit.Kind == "champion" || unit.Kind == "boss")
{ {

View file

@ -53,8 +53,6 @@ namespace Luneraria.UI
public int Income { get { return State.Balance.RoundIncome(State.RoundNumber) + IncomeBonus; } } public int Income { get { return State.Balance.RoundIncome(State.RoundNumber) + IncomeBonus; } }
/// <summary>S1: the own permanent champion income (Legion TD 2), 0 under the reference rules.</summary> /// <summary>S1: the own permanent champion income (Legion TD 2), 0 under the reference rules.</summary>
public int IncomeBonus { get { return State.Extended ? Me.IncomeBonus : 0; } } public int IncomeBonus { get { return State.Extended ? Me.IncomeBonus : 0; } }
/// <summary>S3 fog of war: ids of enemy units the viewer's team cannot see (filled by the game each frame).</summary>
public HashSet<int> Hidden = new HashSet<int>();
/// <summary>After Ready the plan is binding (the host refuses later commands).</summary> /// <summary>After Ready the plan is binding (the host refuses later commands).</summary>
public bool Locked { get { return Planning && Me.Ready; } } public bool Locked { get { return Planning && Me.Ready; } }
public List<RosterEntry> OwnRoster { get { return Planning ? Me.Draft.Roster : Me.Roster; } } public List<RosterEntry> OwnRoster { get { return Planning ? Me.Draft.Roster : Me.Roster; } }
@ -308,29 +306,6 @@ namespace Luneraria.UI
/// <summary>S1 barracks: own waves reinforced here (enemy inner tower fell) / enemy waves reinforced.</summary> /// <summary>S1 barracks: own waves reinforced here (enemy inner tower fell) / enemy waves reinforced.</summary>
public bool OwnBarracks; public bool OwnBarracks;
public bool EnemyBarracks; public bool EnemyBarracks;
/// <summary>S3: text of the camp on this lane this round, "" without one.</summary>
public string Camp = "";
}
/// <summary>S3: the camp guardian of a lane in a camp round (alive in combat, expected in planning).</summary>
string CampText(string lane)
{
if (!State.CampRound(State.RoundNumber))
return "";
int index = LuneMap.LaneIndex(lane);
string reward = index == 0 ? "Glutsegen (+10 % Schaden, 2 Kämpfe)" : index == 1 ? "Mondschild (Schild 10 % Leben, 2 Kämpfe)" : "Beute (50 Gold je Spieler)";
if (Fighting)
{
bool alive = false;
foreach (LuneUnit unit in State.Combat.Units)
{
if (unit.Kind == "camp" && unit.Lane == lane && unit.Alive)
alive = true;
}
if (!alive)
return "";
}
return LuneMatch.CampNames[index] + " in der Lanemitte · Sieg: " + reward;
} }
public LaneInfo Lane(string lane) public LaneInfo Lane(string lane)
@ -343,9 +318,8 @@ namespace Luneraria.UI
{ {
Lane = lane, Title = LuneMap.LaneNames[lane].ToUpperInvariant(), OwnPlayer = PlayerName(ownSeat), EnemyPlayer = PlayerName(enemySeat), OwnIsMe = ownSeat == Slot, Lane = lane, Title = LuneMap.LaneNames[lane].ToUpperInvariant(), OwnPlayer = PlayerName(ownSeat), EnemyPlayer = PlayerName(enemySeat), OwnIsMe = ownSeat == Slot,
Towers = new[] { Building(own + "_" + lane + "_inner"), Building(own + "_" + lane + "_outer"), Building(enemy + "_" + lane + "_outer"), Building(enemy + "_" + lane + "_inner") }, Towers = new[] { Building(own + "_" + lane + "_inner"), Building(own + "_" + lane + "_outer"), Building(enemy + "_" + lane + "_outer"), Building(enemy + "_" + lane + "_inner") },
Threat = LuneLaneThreat.Evaluate(State, Team, lane, Hidden), OwnHero = HeroOf(ownSeat), EnemyHero = HeroOf(enemySeat), Threat = LuneLaneThreat.Evaluate(State, Team, lane), OwnHero = HeroOf(ownSeat), EnemyHero = HeroOf(enemySeat),
OwnBarracks = State.Extended && State.BarracksBroken(Team, lane), EnemyBarracks = State.Extended && State.BarracksBroken(Enemy, lane), OwnBarracks = State.Extended && State.BarracksBroken(Team, lane), EnemyBarracks = State.Extended && State.BarracksBroken(Enemy, lane),
Camp = CampText(lane),
}; };
foreach (Announcement entry in State.Announcements()) foreach (Announcement entry in State.Announcements())
{ {
@ -354,7 +328,7 @@ namespace Luneraria.UI
} }
if (Fighting) if (Fighting)
{ {
LuneLaneThreat.Battle battle = LuneLaneThreat.Battlefield(State, Team, lane, Hidden); LuneLaneThreat.Battle battle = LuneLaneThreat.Battlefield(State, Team, lane);
result.HasCounts = true; result.HasCounts = true;
result.OwnCount = battle.OwnCount; result.OwnCount = battle.OwnCount;
result.EnemyCount = battle.EnemyCount; result.EnemyCount = battle.EnemyCount;

View file

@ -68,7 +68,7 @@ namespace Luneraria.UI
return LuneUiTheme.Round(value * 100.0) + " %"; return LuneUiTheme.Round(value * 100.0) + " %";
} }
public static Result Evaluate(LuneMatch state, int team, string lane, ICollection<int> hidden = null) public static Result Evaluate(LuneMatch state, int team, string lane)
{ {
var result = new Result(); var result = new Result();
int level = Calm; int level = Calm;
@ -122,7 +122,7 @@ namespace Luneraria.UI
} }
if (state.Phase == Phase.Combat && state.Combat != null) if (state.Phase == Phase.Combat && state.Combat != null)
{ {
Battle battle = Battlefield(state, team, lane, hidden); Battle battle = Battlefield(state, team, lane);
double length = battle.Length; double length = battle.Length;
if (battle.EnemyCount > 0) if (battle.EnemyCount > 0)
{ {
@ -161,8 +161,7 @@ namespace Luneraria.UI
/// Units of both sides on one lane: counts, HP sums and the deepest enemy position measured /// Units of both sides on one lane: counts, HP sums and the deepest enemy position measured
/// from the own base along the lane (from positions only). /// from the own base along the lane (from positions only).
/// </summary> /// </summary>
/// <summary>Units on the lane in combat; S3: enemies in the fog (hidden) are not counted.</summary> public static Battle Battlefield(LuneMatch state, int team, string lane)
public static Battle Battlefield(LuneMatch state, int team, string lane, ICollection<int> hidden = null)
{ {
LanePath path = state.Map.LanePaths[lane]; LanePath path = state.Map.LanePaths[lane];
var result = new Battle { Length = path.Length }; var result = new Battle { Length = path.Length };
@ -172,8 +171,6 @@ namespace Luneraria.UI
{ {
if (!unit.Alive || !unit.IsMobile() || unit.Zone != "lane" || unit.Lane != lane || unit.Team > 1) if (!unit.Alive || !unit.IsMobile() || unit.Zone != "lane" || unit.Lane != lane || unit.Team > 1)
continue; continue;
if (hidden != null && hidden.Contains(unit.Id))
continue;
double s = path.Project(unit.Position); double s = path.Project(unit.Position);
double fromOwnBase = team == 0 ? s : path.Length - s; double fromOwnBase = team == 0 ? s : path.Length - s;
if (unit.Team == team) if (unit.Team == team)

View file

@ -16,7 +16,7 @@ namespace Luneraria.UI
"ancient", "warning", "check", "close", "bot", "offline", "clock", "heart", "tempo", "range", "armor", "ancient", "warning", "check", "close", "bot", "offline", "clock", "heart", "tempo", "range", "armor",
"move", "star", "lock", "up", "lane_top", "lane_mid", "lane_bot", "grotto", "pause", "play", "menu", "move", "star", "lock", "up", "lane_top", "lane_mid", "lane_bot", "grotto", "pause", "play", "menu",
"gear", "human", "sword", "basaltschild", "glutklinge", "rankenmantel", "funkenfokus", "quellamulett", "gear", "human", "sword", "basaltschild", "glutklinge", "rankenmantel", "funkenfokus", "quellamulett",
"kristallsplitter", "hero", "skull", "bollwerk", "glutsturm", "quellherz", "splitterkrone" }; "kristallsplitter", "hero", "skull" };
const float Tau = Mathf.PI * 2f; const float Tau = Mathf.PI * 2f;
static readonly Dictionary<string, Vector2[]> Cache = new Dictionary<string, Vector2[]>(); static readonly Dictionary<string, Vector2[]> Cache = new Dictionary<string, Vector2[]>();
@ -28,16 +28,6 @@ namespace Luneraria.UI
return; return;
var f = new Frame(rect); var f = new Frame(rect);
Color dark = accent.a > 0f ? accent : new Color(color.r * .45f, color.g * .45f, color.b * .45f, color.a); Color dark = accent.a > 0f ? accent : new Color(color.r * .45f, color.g * .45f, color.b * .45f, color.a);
// S2 recipes: the first part's symbol inside a forged ring with a small spark.
Luneraria.Core.ItemSpec recipe;
if (Luneraria.Core.ContentData.Items.TryGetValue(glyph, out recipe) && recipe.IsRecipe)
{
float inset = rect.width * .16f;
Draw(painter, recipe.Parts[0], new Rect(rect.x + inset, rect.y + inset, rect.width - 2f * inset, rect.height - 2f * inset), color, accent);
ArcLine(painter, f.P(.5f, .5f), f.S * .47f, 0f, Tau, color, Mathf.Max(1f, f.S * .06f));
Shard(painter, f, V(.82f, .18f), 0f, .14f, LuneUiTheme.Hex("fff2c0"));
return;
}
switch (glyph) switch (glyph)
{ {
case "coin": case "coin":

View file

@ -1,91 +0,0 @@
using System.Collections.Generic;
using Luneraria.Core;
using UnityEngine;
namespace Luneraria.View
{
/// <summary>
/// S4 fog of war in the terrain: a grid over the map (2 m cells) that follows the ground and
/// darkens every point no vision source of the viewer's team reaches (LuneFog.Vision: own
/// units 9 m, own buildings 10 m, wards 12 m). The alpha has a soft 2 m edge and is
/// recomputed ten times a second. Presentation only; the rules never read it.
/// </summary>
public sealed class LuneFogOverlay : MonoBehaviour
{
public const float Cell = 2f;
public const float Darkness = .55f;
public const float Edge = 2f;
public const float Interval = .1f;
Mesh _mesh;
Vec2[] _points;
Color32[] _colors;
float _timer;
/// <summary>Darkness of every grid point (0 = seen), for tests.</summary>
public float[] Alpha { get; private set; }
public int Refreshes { get; private set; }
public void Build(LuneGround ground, Material material)
{
int side = Mathf.CeilToInt((float)(LuneMap.HalfExtent * 2.0) / Cell) + 1;
var vertices = new Vector3[side * side];
_points = new Vec2[side * side];
for (int row = 0; row < side; row++)
{
for (int column = 0; column < side; column++)
{
var map = new Vec2(-LuneMap.HalfExtent + column * Cell, -LuneMap.HalfExtent + row * Cell);
float height = ground != null ? ground.StandHeight(map) : 0f;
_points[row * side + column] = map;
vertices[row * side + column] = LuneSpace.World(map, height + .45f);
}
}
var triangles = new List<int>();
for (int row = 0; row + 1 < side; row++)
{
for (int column = 0; column + 1 < side; column++)
{
int a = row * side + column;
triangles.AddRange(new[] { a, a + side, a + 1, a + 1, a + side, a + side + 1 });
}
}
_colors = new Color32[vertices.Length];
Alpha = new float[vertices.Length];
_mesh = new Mesh { name = "FogOverlay", indexFormat = UnityEngine.Rendering.IndexFormat.UInt32 };
_mesh.SetVertices(vertices);
_mesh.SetTriangles(triangles, 0);
_mesh.SetColors(_colors);
_mesh.RecalculateBounds();
gameObject.AddComponent<MeshFilter>().sharedMesh = _mesh;
var renderer = gameObject.AddComponent<MeshRenderer>();
renderer.sharedMaterial = material;
renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
renderer.receiveShadows = false;
}
/// <summary>Recomputes the darkness at most every Interval seconds (force: now).</summary>
public void Tick(float dt, List<KeyValuePair<Vec2, double>> sources, bool force = false)
{
_timer -= dt;
if (!force && _timer > 0f)
return;
_timer = Interval;
Refreshes++;
for (int i = 0; i < _points.Length; i++)
{
double best = double.PositiveInfinity;
foreach (KeyValuePair<Vec2, double> source in sources)
{
double gap = _points[i].DistanceTo(source.Key) - source.Value;
if (gap < best)
best = gap;
}
float t = Mathf.Clamp01((float)(best + Edge * .5) / Edge);
Alpha[i] = Darkness * t;
_colors[i] = new Color32(0, 0, 0, (byte)Mathf.RoundToInt(Alpha[i] * 255f));
}
_mesh.SetColors(_colors);
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: a539f6fdd87d43728442d53d45a23e97

View file

@ -69,34 +69,6 @@ namespace Luneraria.View
} }
public IEnumerable<LuneUnitView> Units { get { return _units.Values; } } public IEnumerable<LuneUnitView> Units { get { return _units.Values; } }
/// <summary>S3 fog of war: the seat whose team's vision applies (-1 = everything visible).</summary>
public int FogSlot = -1;
/// <summary>Ids of the units hidden in the fog this frame.</summary>
public HashSet<int> Hidden { get; private set; } = new HashSet<int>();
static readonly HashSet<int> Empty = new HashSet<int>();
/// <summary>S4: the darkening grid of the fog (null until a fogged match runs).</summary>
public LuneFogOverlay Fog { get; private set; }
void FogTick(LuneMatch match, float dt)
{
bool on = FogSlot >= 0 && FogSlot < match.Players.Count && match.Extended && Library != null && Library.FogOverlay != null;
if (!on)
{
if (Fog != null && Fog.gameObject.activeSelf)
Fog.gameObject.SetActive(false);
return;
}
if (Fog == null)
{
var go = new GameObject("FogOverlay");
go.transform.SetParent(transform, false);
Fog = go.AddComponent<LuneFogOverlay>();
Fog.Build(Library.Ground, Library.FogOverlay);
}
if (!Fog.gameObject.activeSelf)
Fog.gameObject.SetActive(true);
Fog.Tick(dt, LuneFog.Vision(match, _combat, match.Players[FogSlot].Team));
}
/// <summary>Main-thread cost of the last frame's view update (milliseconds).</summary> /// <summary>Main-thread cost of the last frame's view update (milliseconds).</summary>
public double LastSyncMs { get; private set; } public double LastSyncMs { get; private set; }
readonly System.Diagnostics.Stopwatch _clock = new System.Diagnostics.Stopwatch(); readonly System.Diagnostics.Stopwatch _clock = new System.Diagnostics.Stopwatch();
@ -136,11 +108,6 @@ namespace Luneraria.View
_combat = shown; _combat = shown;
} }
bool showUnits = _combat != null && (!planning || _combat == Preview); bool showUnits = _combat != null && (!planning || _combat == Preview);
// S3 fog of war (extended rules): enemies the viewer's team cannot see are hidden.
Hidden = FogSlot >= 0 && FogSlot < match.Players.Count && showUnits
? LuneFog.Hidden(match, _combat, match.Players[FogSlot].Team, planning, planning ? match.Players[FogSlot].Draft : null)
: Empty;
FogTick(match, dt);
float alpha = Driver.Runner.Current == LuneMatchRunner.Stage.Combat ? (float)Driver.Runner.Clock.Alpha : 1f; float alpha = Driver.Runner.Current == LuneMatchRunner.Stage.Combat ? (float)Driver.Runner.Clock.Alpha : 1f;
if (showUnits) if (showUnits)
{ {
@ -155,9 +122,6 @@ namespace Luneraria.View
view.Bind(unit, Library); view.Bind(unit, Library);
_units[unit.Id] = view; _units[unit.Id] = view;
} }
bool visible = !Hidden.Contains(unit.Id);
if (view.gameObject.activeSelf != visible)
view.gameObject.SetActive(visible);
view.Sync(alpha, dt, ViewCamera != null ? ViewCamera.transform : null); view.Sync(alpha, dt, ViewCamera != null ? ViewCamera.transform : null);
} }
} }
@ -182,43 +146,11 @@ namespace Luneraria.View
view.SetHealth((float)hp, (float)definition.MaxHp, _synced); view.SetHealth((float)hp, (float)definition.MaxHp, _synced);
if (view.Tier == "core") if (view.Tier == "core")
SyncAncientBar(view, (float)hp, (float)definition.MaxHp); SyncAncientBar(view, (float)hp, (float)definition.MaxHp);
// S2: a levelled Ancient shoots like a tower. if (unit != null && unit.Kind == "tower" && _combat != null)
if (unit != null && (unit.Kind == "tower" || (unit.Kind == "core" && unit.Def.Damage > 0.0)) && _combat != null)
TowerAttack(view, unit); TowerAttack(view, unit);
GlyphRing(definition, view, unit != null && fighting && _combat.Glyphed(unit));
} }
} }
/// <summary>S2 (Dota 2 glyph): a bright hexagon ring around an invulnerable tower.</summary>
readonly Dictionary<string, GameObject> _glyphRings = new Dictionary<string, GameObject>();
void GlyphRing(BuildingDef definition, LuneBuildingView view, bool on)
{
_glyphRings.TryGetValue(definition.Id, out GameObject ring);
if (!on)
{
if (ring != null)
ring.SetActive(false);
return;
}
if (ring == null)
{
if (Library == null || Library.RingHexagon == null || Library.MoonBurst == null)
return;
ring = new GameObject("GlyphRing_" + definition.Id);
ring.transform.SetParent(view.transform, false);
ring.transform.localPosition = new Vector3(0f, .12f, 0f);
ring.transform.localScale = new Vector3(4.2f, 1f, 4.2f);
ring.AddComponent<MeshFilter>().sharedMesh = Library.RingHexagon;
var renderer = ring.AddComponent<MeshRenderer>();
renderer.sharedMaterial = Library.MoonBurst;
renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
_glyphRings[definition.Id] = ring;
}
ring.SetActive(true);
ring.transform.Rotate(0f, 40f * Time.deltaTime, 0f);
}
void TowerAttack(LuneBuildingView view, LuneUnit tower) void TowerAttack(LuneBuildingView view, LuneUnit tower)
{ {
_towerSerials.TryGetValue(tower.BuildingId, out int last); _towerSerials.TryGetValue(tower.BuildingId, out int last);

View file

@ -38,8 +38,6 @@ namespace Luneraria.View
public Material HeroRing; public Material HeroRing;
/// <summary>H1: expanding ring of a hero's ultimate (moon light).</summary> /// <summary>H1: expanding ring of a hero's ultimate (moon light).</summary>
public Material MoonBurst; public Material MoonBurst;
/// <summary>S4: fog of war overlay (Luneraria/FogOverlay, vertex alpha).</summary>
public Material FogOverlay;
public Material BarBack; public Material BarBack;
public Material[] BarFill = new Material[3]; public Material[] BarFill = new Material[3];
public Material[] Bolt = new Material[3]; public Material[] Bolt = new Material[3];

View file

@ -30,8 +30,6 @@ namespace Luneraria.View
/// <summary>H1: expanding moon ring of an ultimate (seconds left, 0 = none).</summary> /// <summary>H1: expanding moon ring of an ultimate (seconds left, 0 = none).</summary>
Transform _burst; Transform _burst;
float _burstTime; float _burstTime;
/// <summary>S4: a small gold ring circling over the head of a stunned unit.</summary>
Transform _stun;
float _attackFlash; float _attackFlash;
float _time; float _time;
Vector3 _lastVisual; Vector3 _lastVisual;
@ -118,9 +116,6 @@ namespace Luneraria.View
_burst.localPosition = new Vector3(0f, .08f, 0f); _burst.localPosition = new Vector3(0f, .08f, 0f);
_burst.gameObject.SetActive(false); _burst.gameObject.SetActive(false);
} }
_stun = Marker("StunRing", library.RingRound, library.HeroRing);
_stun.localScale = new Vector3(.55f, 1f, .55f);
_stun.gameObject.SetActive(false);
transform.position = Ground(unit.Position); transform.position = Ground(unit.Position);
transform.rotation = LuneSpace.Facing(unit.Facing); transform.rotation = LuneSpace.Facing(unit.Facing);
_lastVisual = transform.position; _lastVisual = transform.position;
@ -169,14 +164,6 @@ namespace Luneraria.View
if (ultimate && _burst != null) if (ultimate && _burst != null)
_burstTime = BurstSeconds; _burstTime = BurstSeconds;
Burst(dt); Burst(dt);
bool stunned = Unit.Alive && Unit.ShowsStun();
if (_stun.gameObject.activeSelf != stunned)
_stun.gameObject.SetActive(stunned);
if (stunned)
{
_stun.localPosition = new Vector3(0f, _height + .15f, 0f);
_stun.Rotate(0f, 360f * dt, 0f, Space.Self);
}
if (attacked || ability) if (attacked || ability)
_attackFlash = .28f; _attackFlash = .28f;
_attackFlash = Mathf.Max(0f, _attackFlash - dt); _attackFlash = Mathf.Max(0f, _attackFlash - dt);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -106,5 +106,3 @@ Die Fähigkeiten setzen die **Ultimate-Bedingungen** aus [IDEEN.md](IDEEN.md) vo
**Sylvara mit einem Varianten-Skin (Blutmond).** Ziel: das Slot-System einmal vollständig prüfen (Körper, Waffen-Prop, Umhang, VFX-Palette, Teamfarbe, Variantenmaterial), bevor weitere Helden folgen. **Sylvara mit einem Varianten-Skin (Blutmond).** Ziel: das Slot-System einmal vollständig prüfen (Körper, Waffen-Prop, Umhang, VFX-Palette, Teamfarbe, Variantenmaterial), bevor weitere Helden folgen.
**Umgesetzt in H1** (10. Oktober 2026): Sylvara als voller Held mit Ultimate-Auslöser und Blutmond-Skin, siehe [H1-SYLVARA.md](H1-SYLVARA.md). Werte nach der Balance-Abstimmung: 1.450 Leben, Sprung nur auf ungeschützte Ziele, Mondschleier, Tanz 220 %. **Umgesetzt in H1** (10. Oktober 2026): Sylvara als voller Held mit Ultimate-Auslöser und Blutmond-Skin, siehe [H1-SYLVARA.md](H1-SYLVARA.md). Werte nach der Balance-Abstimmung: 1.450 Leben, Sprung nur auf ungeschützte Ziele, Mondschleier, Tanz 220 %.
**Umgesetzt in H2** (11. Oktober 2026): Brunhild als voller Held mit Markierung, Betäubung und Skin Eiswalküre, siehe [H2-BRUNHILD.md](H2-BRUNHILD.md). Abweichung vom Konzept: Der Speerwurf trifft den fernsten Gegner in 7 m (statt eines beliebigen Ziels), und nach dem Sturz schützt der Walkürenschild.

View file

@ -1,91 +0,0 @@
# H2 · Brunhild, die Walküre
Stand: 11. Oktober 2026 · Branch `luneraria/h2-brunhild` (von `luneraria/s3-strategie`).
Grundlage: [CHAMPIONS-KONZEPT.md](CHAMPIONS-KONZEPT.md) und das Muster aus [H1-SYLVARA.md](H1-SYLVARA.md). Auftrag: „weiter“; Brunhild war die Empfehlung seit S1.
![Walkürensturz](../port/screenshots/h2/spiel/05-walkuerensturz.jpg)
## Kurzfassung
- **Neue Heldin:** Brunhild ist der fünfte kaufbare Held, Heldenjägerin in der Nahkampfreihe, 260 Gold. Sie ist das Gegenstück zu Siegfried.
- **Kit:**
- Heldenjägerin (passiv)
- Speerwurf mit Markierung
- Ultimate Walkürensturz: Sprung, Flächenschaden, Betäubung
- zwei Talentstufen
- vier Auslöser
- **Neue Regelbausteine** (wirken nur durch Brunhild, deshalb bleibt die Referenz unverändert):
- **Markierung** (`vulnerable`): Das Ziel nimmt mehr Schaden.
- **Betäubung** (`stun`): Das Ziel greift weder an noch bewegt es sich. Bosse sind immun.
- **Skin:** „Eiswalküre“ (Variante): Der karmesinrote Umhang wird eisblau, das Gold kühler, Haut und Haar bleiben.
- **Figur:** Meshy-Figur mit dem Speer als eigenem Modell; das Ultimate nutzt den Clip „Jump Attack“. Verbrauch: 113 Credits, danach sind 110 übrig.
- **Bots:** Mit erweiterten Regeln wählen sie das Profil `walkuere` (acht Profile in der Rotation, Schritt 5, weiterhin sechs verschiedene je Partie).
## Regeln
| Teil | Wirkung |
| --- | --- |
| Grundwerte | 2.000 Leben, 42 Schaden, Durchschlag gegen Schwer, Reichweite 2,0 m (Speer), Tempo 2,4, Rolle Nahkampf |
| Heldenjägerin (passiv) | +25 % Schaden gegen Helden und Angriffschampions, auch bei Speer und Sturz |
| Speerwurf | Ab 3 s alle 6 s: 120 % Schaden am fernsten Gegner in 7 m. Er ist 4 s markiert und nimmt 15 % mehr Schaden |
| Walkürensturz (Ultimate) | Einmal pro Kampf, frühestens nach 2 s. Sprung auf das Ziel: zuerst ein Held oder Angriffschampion in 7 m, dann der markierte Gegner, dann ihr Ziel. 200 % Schaden an allen Gegnern in 3 m und 1 s Betäubung (Bosse immun). Danach Walkürenschild: Schild von 30 % ihrer Lebenspunkte für 5 s |
| Auslöser | **Gruppe** (Standard): 3 Gegner in 4 m um das Ziel · **Heldenjagd:** ein Held oder Champion in 7 m · **Notfall:** unter 40 % Leben · **Sofort:** erster Gegner in 7 m |
| Talente Stufe 3 | **Walkürenblut:** Speerwurf gegen Helden und Champions doppelt · **Speerregen:** die drei fernsten Gegner |
| Talente Stufe 6 | **Rabenschwinge:** +20 % Angriffstempo · **Götterdämmerung:** 2 s Betäubung in 4 m |
## Balance
Probe-Vergleich (`dotnet run -c Release --project tools/dotnet/Luneraria.Balance 40`): gleiches Bot-Profil, nur der Held getauscht, 80 Partien.
| Fassung | Brunhild | Überlebenszeit im Kampf |
| --- | --- | --- |
| Erste Fassung (1.700 Leben, kein Schild) | 0 % | fällt in jedem Kampf nach etwa 25 s (Ringborke etwa 42 s, Funkenlanze etwa 51 s) |
| Mit 2.000 Leben und Walkürenschild | **50 %**, wie Ringborke, Funkenlanze und Sylvara | – |
Die Siegquoten einzelner Bot-Profile in der Rotation schwanken stark mit den Teamkombinationen (15 bis 83 von 90) und taugen deshalb nicht für Heldenvergleiche. Maßgeblich ist der Probe-Vergleich.
## Technik
- **Kern:**
- `ContentData`: Held, Talente, Auslöser, Skin, Bot-Profil.
- `LuneCombat.BrunhildStep` / `BrunhildSpear`: Der Sprung nutzt dieselbe Landung wie Sylvaras Mondsprung.
- `LuneUnit`: `vulnerable` in `Mitigated`, `Stunned()` in Angriffstempo und Bewegung.
- **Figur:**
- `tools/meshy/brunhild/figure.json`, Speer als Prop in der rechten Hand.
- Export 23/23, zwei Läufe byteidentisch.
- **Skin:**
- `tools/meshy/brunhild/skins.json`.
- `skin_variant.py` kann jetzt Farbbänder über 0° (Rot) und eine Mindestsättigung, damit Hauttöne unverändert bleiben.
- **HUD:** Bei mehr als vier Helden werden die Spalten der Heldenwahl kompakter (Name 24 px, schmalere Beschriftung).
## Tests und Nachweise
| Prüfung | Ergebnis |
| --- | --- |
| Kern (.NET 8) | 619/619, darunter **h2 22**: Kauf, Regel, Skin; Speer erst ab 3 s, markiert den fernsten Gegner, +15 % erlittener Schaden; Sturz bei Heldenjagd, Landung am Helden, Betäubung (Boss immun), keine Bewegung und kein Angriff, Ende nach 1 s, einmal pro Kampf; Gruppe wartet; +25 % gegen Helden; Talente; Bot-Profil nur erweitert |
| Golden und Fingerabdrücke | 0 Abweichungen; `DCE95C96556F3A1A` bei 30/60/120 fps und unbegrenzt unverändert |
| Unity EditMode | 34/34 |
| Unity PlayMode | 19/19, neu `H2HeroViewsTests.BrunhildHud`: fünf Helden, Regel- und Skin-Karten, Befehlsplatz „HELDENJAGD“, Figur mit Ultimate-Clip, Eiswalküre-Material an Körper und Speer, Ultimate im Kampf. Verify A–I 718/0; die Prüfung der Heldenwahl erwartet jetzt fünf Helden |
| Asset-Prüfung | Export 23/23, byteidentisch in zwei Läufen |
| Player-Selbsttest | macOS 30/30 |
| Leistung | A/B im selben Player, abwechselnd, je sechs Heldinnen. Karte: Brunhild 138,1 / 142,9 / 143,3 fps, Sylvara 142,9 / 141,7 / 141,2 fps, Mittel gleich (141,4 zu 141,9). Mid nah: 153,3 / 157,1 / 157,8 zu 161,8 / 162,5 / 163,2 fps, also −4 %. Brunhild hat weniger Dreiecke (28.706 zu 30.782); der Unterschied kommt aus dem Kampfgeschehen im Nahbild. Rohdaten in `docs/port/logs/h2/` |
**Bilder** in `docs/port/screenshots/h2/spiel/`:
1. Heldenwahl mit fünf Helden
2. Heldenfenster
3. Ultimate und Skin
4. Aufstellung
5. Walkürensturz
6. Betäubt
7. Nach dem Sturz
8. Lane
9. Eiswalküre im Fenster
10. Eiswalküre im Kampf
Das Figurenblatt liegt in `figur/`.
## Offene Punkte
- **Betäubung sichtbar machen:** Betäubte Einheiten erkennt man bisher nur daran, dass sie stillstehen.
- **Brunhild und Siegfried als Paar:** Ein Götterdämmerung-Set-Skin ist möglich, sobald Siegfried als Held kommt.
- **Meshy-Guthaben:** 110 Credits übrig, das reicht für einen weiteren Helden mit Waffe knapp nicht. Die nächste Monatsgutschrift kommt laut Meshy im November.

View file

@ -1,46 +0,0 @@
# Hauptstand · 11. Oktober 2026
Stand: 11. Oktober 2026 · Branch `main` (vorgerückt) · Tag `stand-2026-10-11`.
Champions of Luneraria in Unity mit allen Phasen bis S4.
- **Zusammenführung:** `main` enthielt bisher nur den ersten Commit („Unity-Projekt angelegt“). Alle Phasen-Branches liegen in einer Linie; `main` wurde ohne Konflikte auf den Endstand vorgerückt.
- **Phasen-Branches:** Sie bleiben als Nachweis erhalten.
## Was der Stand enthält
| Phase | Inhalt | Doku |
| --- | --- | --- |
| U1–U5.5 | Port aus Godot mit Verhaltensgleichheit, Welt, HUD, Netzwerk, PBR-Grafik, alle zehn Glutpakt-Truppen als Meshy-Figuren | `docs/port/PORT-STATUS.md`, `PORT-QA.md` |
| Konzept | Ideensammlung Strategie/MOBA, sieben neue Champions, Skin-Regeln | [IDEEN.md](IDEEN.md), [CHAMPIONS-KONZEPT.md](CHAMPIONS-KONZEPT.md) |
| H1 | Sylvara: Ultimate-Auslöser in der Planung, Skin-System (Blutmond) | [H1-SYLVARA.md](H1-SYLVARA.md) |
| S1 | Regelschalter „Erweitert“: Söldner-Einkommen, Kasernen, Rückkauf, Mondsegen | [S1-STRATEGIE.md](S1-STRATEGIE.md) |
| S2 | Glyphe, Item-Rezepte, Ancient-Stufen, Grottenjagd der Bots | [S2-STRATEGIE.md](S2-STRATEGIE.md) |
| S3 | frühe Bot-Helden (Rotationsfehler behoben), Neutrallager, Fog of War mit Spähern | [S3-STRATEGIE.md](S3-STRATEGIE.md) |
| H2 | Brunhild: Markierung, Betäubung, Skin Eiswalküre | [H2-BRUNHILD.md](H2-BRUNHILD.md) |
| S4 | Betäubung sichtbar, Nebel im Gelände, Glyphe und Betäubung für Gäste | [S4-FEINSCHLIFF.md](S4-FEINSCHLIFF.md) |
## Spielen
- **Startbildschirm:** „Allein spielen“, „Hosten“ oder „Beitreten“. Der Schalter **REGELN** steht auf ERWEITERT (Legion TD 2, Dota 2, LoL); REFERENZ spielt die Regeln des Godot-Originals.
- **Helden (5):** Ringborke, Quellgeflecht, Funkenlanze, Sylvara, Brunhild. Ultimate-Auslöser und Skin wählst du im Heldenfenster unter „Ultimate & Skin“.
- **Team-Aktionen im Teampanel links:** Glyphe, Ancient-Stufe, Späher, Lager-Übersicht.
- **macOS-Player:** `Builds/macOS/Luneraria.app` im Arbeitsordner, aus dem der Stand gebaut wurde. Ein frischer Bau entsteht mit `LunePlayerBuild.BuildMac`.
## Prüfstand
| Prüfung | Ergebnis |
| --- | --- |
| Kern (.NET 8) | 627/627: content 77, core 91, m2 73, golden 157, determinism 6, net 43, net_rounds 12, h1 42, s1 31, s2 34, s3 31, h2 22, s4 8 |
| Parität zur Godot-Referenz | Golden 51.221 Werte, 0 Abweichungen; Bot-Fingerabdrücke `DCE95C96556F3A1A` (Seed 2) und `7B8E43AEFC0D60C5` (Seed 9 ab Welle 21) unverändert. Alle neuen Regeln wirken nur mit `MatchOptions.Extended` |
| Unity | EditMode 35/35, PlayMode 20/20, Verify A–I 718/0, Player-Selbsttest 30/30 |
## Offene Punkte
- **Build-Module für Windows und Linux** (Zielplattformen) fehlen weiterhin; Installation nur nach Freigabe.
- **Git LFS:** Das Repo wächst mit jeder Figur um rund 40 bis 60 MB.
- **Weitere Helden des Konzepts:** Thalion, Ilyra, Isolde, Aurelian, Rowan (Meshy-Guthaben 110 Credits, nächste Gutschrift im November).
- **Regeln, die du entscheiden musst:**
- manuelles Ultimate im Kampf (braucht Kampfbefehle im Netz)
- Quellgeflecht-Dominanz in Probe-Vergleichen (Referenzbalance)
- kürzere Partien mit Lagern beobachten
- **Netz:** Der Nebel ist Darstellung, keine Geheimhaltung; Gäste erhalten alle Einheiten.

View file

@ -84,12 +84,6 @@ Legende:
| Respawn und Rückkauf | umgesetzt in S1, abgeschwächt (Erschöpfung statt Aussetzen) | | Respawn und Rückkauf | umgesetzt in S1, abgeschwächt (Erschöpfung statt Aussetzen) |
| Drachen-Analogon | umgesetzt in S1 als Mondsegen über die Grotte | | Drachen-Analogon | umgesetzt in S1 als Mondsegen über die Grotte |
| Schadens- und Rüstungstypen | bestand bereits (3 × 3); Farben im HUD seit G3 | | Schadens- und Rüstungstypen | bestand bereits (3 × 3); Farben im HUD seit G3 |
| Glyphe | umgesetzt in S2 |
| Item-Rezepte | umgesetzt in S2 |
| Ancient stärken (Legion TD 2) | umgesetzt in S2 (neu, nicht in der ursprünglichen Liste) |
| Fog of War mit Spähern | umgesetzt in S3 (Darstellung, Planungsaufklärung; ohne Gelände-Shader) |
| Neutrallager | umgesetzt in S3 (Lanemitte, Glutsegen, Mondschild, Beute) |
| Schreine | durch die Lager abgedeckt; eigene Schreine offen |
## Empfohlene Reihenfolge ## Empfohlene Reihenfolge

View file

@ -1,87 +0,0 @@
# S2 · Glyphe, Item-Rezepte und Ancient-Stufen
Stand: 11. Oktober 2026 · Branch `luneraria/s2-strategie` (von `luneraria/s1-strategie` = `8e4b52f`).
Grundlage: [IDEEN.md](IDEEN.md) und [S1-STRATEGIE.md](S1-STRATEGIE.md). Auftrag: „mach weiter mit S2“.
![Glyphe am Turm](../port/screenshots/s2/spiel/05-glyphe-am-turm.jpg)
## Kurzfassung
| Vorbild | Mechanik | Strategische Frage |
| --- | --- | --- |
| Dota 2 (Glyphe der Befestigung) | **Glyphe:** Das Team hält sie in der Planung für eine Lane bereit. Fällt dort im Kampf ein eigener Turm unter 50 %, sind die Türme dieser Lane 5 s unverwundbar. Danach folgen 4 Runden Abklingzeit. Löst sie nicht aus, bleibt sie bereit | Wo greift der Gegner an? Die Glyphe ist eine Wette auf seinen Plan |
| Dota 2 / League of Legends (Rezepte) | **Item-Rezepte:** Zwei Items plus ein Rezeptpreis ergeben ein stärkeres Item, und ein Platz wird frei. Es gibt Bollwerk, Glutsturm, Quellherz und Splitterkrone | Breit kaufen oder auf ein Rezept hinarbeiten? |
| Legion TD 2 (König-Upgrades) | **Ancient-Stufen:** Jeder Spieler eines Teams kann Stufen des eigenen Ancients kaufen (höchstens 3, Preis 150 × nächste Stufe). Jede Stufe repariert 1.000 und lässt den Ancient auf Angreifer in 7,5 m schießen (45 / 75 / 110 Schaden pro Sekunde) | Gold in Angriff, Armee oder in die letzte Verteidigung? |
| S1-Befund | **Bots und Grotte:** Mit erweiterten Regeln jagen auch die Mid-Bots ab Runde 8 den Boss, aber nur, wenn ihre Lane sicher ist (beide eigenen Türme über 70 %) | Mondsegen kommt in Bot-Partien jetzt wirklich vor |
Wie in S1 wirkt alles nur mit `MatchOptions.Extended`. Ohne den Schalter bleiben die Referenzregeln unverändert. Das betrifft Golden-Replays, Fingerabdrücke und die HUD-Prüfung; die Suite `s2` prüft das ausdrücklich.
## Rezepte
| Rezept | Teile | Preis | Wirkung |
| --- | --- | ---: | --- |
| Bollwerk | Basaltschild + Rankenmantel | 120 | +600 Leben, +15 % Schadensminderung |
| Glutsturm | Glutklinge + Funkenfokus | 140 | +25 % Schaden, +22 % Angriffstempo |
| Quellherz | Quellamulett + Basaltschild | 120 | +450 Leben, 2,2 % Regeneration pro Sekunde |
| Splitterkrone | Kristallsplitter + Glutklinge | 160 | +20 % Schaden; Treffer 35 % an 2 Gegner in 1,5 m; ab Stufe 5 |
Das fertige Item nimmt den Platz des ersten Teils ein, damit wird ein Platz frei. Verkauft bringt es die Hälfte des Gesamtpreises (Teile plus Rezept). Rückgängig machen geht wie bei jedem Planungsschritt.
## Regeldetails
- **Glyphe (Teamentscheidung):** Es gilt die Wahl des ersten Spielers in Platzreihenfolge, der eine Lane hält. Mitspieler sehen ihre Pläne gegenseitig nicht, das ist bestehende Privatsphäre. Die Unverwundbarkeit greift für jeden Schaden, auch Fläche und Schaden über Zeit. Nur eine ausgelöste Glyphe startet die Abklingzeit.
- **Ancient-Stufen:** Der Preis richtet sich nach der festen Teamstufe plus den eigenen Käufen dieser Planung. Kaufen zwei Spieler gleichzeitig über Stufe 3 hinaus, wird der Überschuss erstattet. Der Ancient zielt wie ein Turm (nur in Reichweite, Einheiten zuerst) und nutzt den Turmtakt.
- **Netz:** `Draft.GlyphLane`, `Draft.CoreInvest`, `BossState.GlyphReady` und `BossState.CoreLevel` werden übertragen. Den Glyphen-Ring sehen Host und Offline-Spieler; Gäste sehen den Kampfzustand der Gebäude nur über die Lebenspunkte (bekannte Einschränkung, wie bei den Turmschüssen).
## Oberfläche
| Stelle | Anzeige |
| --- | --- |
| Teampanel | Zeile **GLYPHE** mit TOP, MID, BOT und Status („bereit“ / „ab Runde N“); Zeile **ANCIENT** mit „STUFE 1/3 (+1)“ und Kaufknopf „+300 G“; Tooltips nennen die Vorbilder |
| Heldenfenster, Items | Rezepte mit Rahmen-Symbol, Preis „+120“, Zustand KAUFBAR, TEILE FEHLEN oder STUFE FEHLT; Klick schmiedet |
| Feld | Bei ausgelöster Glyphe ein drehender heller Sechseckring um die geschützten Türme; ein Ancient mit Stufe schießt sichtbar wie ein Turm |
## Balance (Bot-Partien)
Werkzeug: `dotnet run -c Release --project tools/dotnet/Luneraria.Balance s1 60`. Ausgewertet sind 60 Seeds mit der normalen Profil-Rotation, Rohdaten in `docs/port/logs/s2/balance-s2.txt`.
| | Referenz | S1 | S2 |
| --- | --- | --- | --- |
| Siege Blau / Rot | 30 / 30 | 29 / 31 | 30 / 30 |
| Ø Runden | 10,7 | 12,8 | 12,1 |
| Wer den ersten Turm nimmt, gewinnt | 40/60 | 43/60 | 43/60 |
| Mondsegen-Stapel | – | 8 | 25 |
| Ausgelöste Glyphen | – | – | 128 |
| Gekaufte Ancient-Stufen | – | – | 50 |
| Rezepte bei Bots | – | – | 0 |
So ist die Balance entstanden:
- **Erste Fassung:** Mid-Bots zogen ohne Lane-Prüfung in die Grotte. Dadurch stieg „erster Turm gewinnt“ auf 52 von 60, die Grottengänge rissen Lanes auf.
- **Lane-Prüfung (über 70 %):** Damit liegt der Wert wieder bei 43 von 60.
- **Ancient-Stufen:** Die Bots kauften sie erst, nachdem ihr Teamplan vor den Heerkauf gezogen wurde.
- **Rezepte:** Bots werben ihre Helden erst um Runde 10 bis 12 an (bestehendes Referenzverhalten) und kommen deshalb selten zu zwei passenden Items. Rezepte sind daher vor allem ein Werkzeug für Menschen. Dass ein Bot mit beiden Teilen schmiedet, prüft `s2` gezielt.
## Tests und Nachweise
| Prüfung | Ergebnis |
| --- | --- |
| Kern (.NET 8) | 566/566, darunter **s2 34** (Referenz unberührt, Glyphe: Teamwahl, Auslösen, Schutz, Ende, Abklingzeit nur nach Auslösen; Rezepte: Teile, Preis, Platz frei, Rückgängig, Verkauf, Stufe; Ancient: Preise, Teamdeckel mit Erstattung, Reparatur, Schuss; Bots: Grotte, Mondsegen, Glyphen, Schmieden) |
| Golden und Fingerabdrücke | 0 Abweichungen; `DCE95C96556F3A1A` bei 30/60/120 fps und unbegrenzt unverändert |
| Unity EditMode | 32/32 |
| Unity PlayMode | 17/17 (neu `S2RulesViewsTests.S2Hud`); Verify A–I 718/0, eine Prüfung angepasst: „item shop lists every item of the rules in play“ (unter Referenzregeln ohne Rezepte) |
| Player-Selbsttest | macOS 30/30 |
| Leistung | sechs Sylvaras, Seed 9 ab Welle 2: Karte 148,4–150,1 fps, Mid nah 169,5–172,8 fps, HUD 0,04–0,05 ms. Gleich S1 (148,5–150,3 / 170,1–171,7) |
Die Bilder liegen in `docs/port/screenshots/s2/spiel/`:
1. Team-Aktionen
2. Ancient-Tooltip
3. Rezept im Shop
4. Bollwerk geschmiedet
5. Glyphe am Turm
## Offene Punkte
- **Fog of War mit Spähern (S3):** braucht einen Umbau von Sichtmodell, Planungsvorschau und Netz-Privatsphäre.
- **Neutrallager und Schreine (LoL-Buffs):** brauchen neue Kartenorte samt Ansichten.
- **Glyphen-Ring auf Gästen:** Dafür muss der Glyphen-Zustand in den Kampfrahmen.
- **Bots werben Helden spät:** Ein früherer Heldenkauf würde Items und Rezepte in Bot-Partien beleben. Das ist eine eigene Bot-Phase, im erweiterten Modus aber ohne Paritätsfolgen machbar.

View file

@ -1,110 +0,0 @@
# S3 · Frühere Bot-Helden, Neutrallager, Fog of War
Stand: 11. Oktober 2026 · Branch `luneraria/s3-strategie` (von `luneraria/s2-strategie`).
Auftrag: „1–3 umsetzen“:
1. Fog of War mit Spähern
2. Neutrallager und Schreine
3. Bots kaufen Helden früher
Alles wirkt nur mit erweiterten Regeln (`MatchOptions.Extended`). Die Referenzregeln, Golden-Replays und Fingerabdrücke bleiben unverändert.
![Späher zeigt die gegnerische Mid-Aufstellung](../port/screenshots/s3/spiel/04-spaeher-zeigt-gegnerische-aufstellung.jpg)
## A · Bots (Commit `e756a90`)
| Befund | Behebung |
| --- | --- |
| Bots werben ihre Helden erst um Runde 11 bis 12 an: Sie geben jede Runde fast alles für Truppen aus und sammeln die 260 Gold nie an | Mit erweiterten Regeln sparen Bots ab ihrer Heldenrunde (sobald 3 Truppen stehen), bis der Held gekauft ist. Helden kommen jetzt in Runde 4 bis 6 |
| **Fehler aus S1:** Mit sieben Profilen ergibt `(Platz × 7 + Seed) % 7` für alle sechs Plätze dasselbe Profil. Die Sylvara-Quote von 50 % in S1 war deshalb ein Artefakt | Rotation mit Schritt 5: sechs verschiedene Profile je Partie. Die Referenz mit sechs Profilen nutzt unverändert Schritt 7 |
| Profile mit Angriffschampions verlieren in kurzen Partien: Das Kopfgeld des Champions (70 + 6 × Runde) ist ein Geschenk an den Verteidiger | Verteidiger bekommen mit erweiterten Regeln das halbe Kopfgeld (wie in Legion TD 2: weniger, als der Söldner kostet). Das Assassinen-Profil bucht keinen Champion |
Das Balance-Werkzeug spielt jeden Seed jetzt zweimal, beim zweiten Mal mit vertauschten Teamprofilen. Damit zählt eine starke Profilkombination für beide Seiten, und nur ein echter Seitenvorteil bliebe übrig.
## B · Neutrallager (League of Legends)
In jeder dritten Runde (3, 6, 9 …) steht in der Mitte jeder Lane ein neutraler Wächter (Modell: Bruchhirsch). Er bewegt sich nicht, greift beide Teams in 2,5 m an und wächst mit den Wellen.
- **Wer gewinnt:** Fällt der Wächter, nimmt das Team mit mehr lebenden Einheiten in 8 m das Lager. Bei gleicher Zahl entscheidet die Summe der Lebenspunkte. Nur exakt gespiegelte Lagen bleiben ohne Sieger. Es gibt kein Last-Hitting; Einheiten, die im selben Schritt fallen, zählen nicht.
- **Kampfende:** Ein allein übriger Wächter hält den Kampf nicht offen.
| Lane | Lager | Belohnung |
| --- | --- | --- |
| Top | Glutwächter | **Glutsegen:** +10 % Schaden für Truppen und Helden des Teams, 2 Kämpfe (wie Red Buff) |
| Mid | Mondwächter | **Mondschild:** Schild von 10 % Leben zu Kampfbeginn, 2 Kämpfe (wie Blue Buff/Baron) |
| Bot | Goldwächter | **Beute:** sofort 50 Gold für jeden im Team |
**HUD:**
- Teampanel-Zeile **LAGER** mit laufenden Buffs und „Wächter diese Runde / in Runde N“.
- Ein Sechseck an jeder Lane, die in dieser Runde einen Wächter hat; der Lane-Tooltip nennt die Belohnung.
**Messung** (40 Bot-Partien):
- 330 Wächter, alle gefallen, im Schnitt nach 17,5 s.
- Blau holt 157, Rot 173, kein Gleichstand (vor dem Lebenspunkte-Entscheid waren es 96 Gleichstände).
## C · Fog of War mit Spähern (Dota 2)
Der Nebel ist reine Darstellung und Information für den Menschen am eigenen Sitz. Die Simulation liest ihn nie (`LuneFog` im Kern, vom Spiel abgefragt).
**Im Kampf:**
- Gegnerische Figuren sind nur sichtbar in 9 m um eigene Einheiten, in 10 m um eigene Gebäude und in 12 m um die Lanemitte eines eigenen Spähers.
- Unsichtbare Gegner verschwinden aus dem 3D-Feld, von der Minimap, aus den Namensschildern und aus der Einheitenzählung der Lane-Anzeige.
- Neutrale Einheiten (Wächter, Grottenboss) bleiben immer sichtbar.
**In der Planung:**
- Bisher zeigte die Vorschau nur die eigene Armee.
- Jetzt zeigt sie auf **aufgeklärten** Lanes zusätzlich die festgelegte gegnerische Armee (Truppen und Held) in ihrer Aufstellung.
- Aufgeklärt ist eine Lane mit einem Späher des Teams, mit einem eigenen Späher dieser Planung oder wenn der äußere Turm des Gegners dort gefallen ist.
**Späher:**
- 40 Gold, wacht 3 Runden über seine Lane und gilt für das ganze Team (Befehl `ward`).
- Teampanel-Zeile **SPÄHER**: TOP, MID, BOT; die Zahl zeigt die Restrunden.
**Bots** nutzen den Nebel nicht. Sie lesen ohnehin nur öffentliche Informationen.
**Bekannte Grenzen:**
- Der Nebel ist im Gelände nicht als Abdunklung sichtbar, nur durch fehlende Gegner. Dafür braucht es einen eigenen Nebel-Shader.
- Im Netzwerkspiel erhalten Gäste alle Einheiten im Kampfrahmen. Der Nebel ist Darstellung, keine Netz-Geheimhaltung.
## Balance (gespiegelte Bot-Partien)
Rohdaten: `docs/port/logs/s3/balance-s3a-bots.txt` (nach Teil A) und `balance-s3.txt` (mit Lagern).
| | Referenz | S3 nur Bots | S3 mit Lagern |
| --- | --- | --- | --- |
| Partien | 120 | 120 | 120 |
| Blau / Rot | 60 / 60 | 59 / 61 | 57 / 63 |
| Ø Runden | 10,3 | 10,1 | 9,0 |
| Wer den ersten Turm nimmt, gewinnt | 100 | 104 | **53** |
| Sylvara (Plätze / Siege) | – | 102 / 42 | 102 / 48 |
| Rezept-Items bei Bots | – | 0 | 4 |
| Mondsegen-Stapel | – | 60 | 67 |
Bewertung:
- Mit den Lagern sagt der erste Turm kaum noch etwas über den Sieger aus (53 von 120). Umkämpfte Buffs führen zu Wenden wie die Objectives in LoL.
- Die Partien werden etwas kürzer, weil die Buffs Druck aufbauen. Das sollten wir mit echten Spielern beobachten.
- Beide Seiten bleiben ausgeglichen.
## Tests und Nachweise
| Prüfung | Ergebnis |
| --- | --- |
| Kern (.NET 8) | 597/597, darunter **s3 31** (Referenz ohne Späher, Lager und Nebel; sechs verschiedene Profile; frühe Bot-Helden, Referenz-Bots weiterhin spät; halbes Champion-Kopfgeld; Lager: Runden, drei Wächter, kein Schritt, Teamwahl, Glutsegen, Mondschild, Beute, Kampfende; Späher: Preis, eine pro Lane, Teamwirkung, Countdown, Aufklärung durch gefallenen Turm; Nebel: Start, Sicht 9 m, Späherpunkt, Planungsvorschau nur auf aufgeklärten Lanes) |
| Golden und Fingerabdrücke | 0 Abweichungen; `DCE95C96556F3A1A` bei 30/60/120 fps und unbegrenzt unverändert |
| Unity EditMode | 33/33 |
| Unity PlayMode | 18/18 (neu `S3RulesViewsTests.S3Hud`: Lager-Ankündigung, Sechseck, Wächter-Figuren, versteckte Gegnerfiguren, Späher bringt die Mid-Armee in die Vorschau, Top bleibt verborgen); Verify A–I 718/0; der S1-Test prüft das Kasernen-Symbol jetzt am Spielzustand (die stärkeren Bots brechen Top früher) |
| Player-Selbsttest | macOS 30/30 |
| Leistung | sechs Sylvaras, Seed 9 ab Welle 2: Karte 144,4–150,0 fps, Mid nah 167,4–172,2 fps, HUD 0,04–0,06 ms. Im Rahmen von S2; die Nebelberechnung kostet nicht messbar mehr |
**Bilder** in `docs/port/screenshots/s3/spiel/`:
1. Planung mit angekündigten Lagern
2. Kampf um das Mondlager
3. Nebel im Kampf (Übersicht)
4. Späher zeigt die gegnerische Aufstellung
5. Späher-Tooltip
## Offene Punkte
- **Nebel-Abdunklung im Gelände:** braucht einen eigenen Shader; die Minimap könnte Sichtkreise zeigen.
- **Nebel als Netz-Geheimhaltung:** Gäste erhalten derzeit alle Einheiten.
- **Bots setzen keine Späher:** Sie brauchen keine Sicht.
- **Kürzere Partien mit Lagern** (9,0 Runden): mit echten Spielern beobachten, eventuell Lager erst ab Runde 6.

View file

@ -1,32 +0,0 @@
# S4 · Feinschliff: Betäubung, Nebel im Gelände, Glyphe für Gäste
Stand: 11. Oktober 2026 · Branch `luneraria/s4-feinschliff` (von `luneraria/h2-brunhild`).
Auftrag: „1 und 2 durchführen“. Punkt 1 ist der Feinschliff aus S3 und H2 ohne Meshy-Credits, Punkt 2 die Zusammenführung zum Hauptstand (siehe unten).
![Nebel in der Übersicht](../port/screenshots/s4/spiel/01-nebel-uebersicht.jpg)
## Inhalt
| Punkt | Umsetzung |
| --- | --- |
| Betäubung sichtbar | Ein goldener Ring kreist über dem Kopf jeder betäubten Einheit (`LuneUnitView`, Marker `StunRing`). Gäste sehen ihn ebenfalls: Der Host überträgt den Zustand als Bit 18 in den Flags des Kampfrahmens (`LuneUnit.NetStunned`, `ShowsStun()`) |
| Nebel im Gelände | `LuneFogOverlay` ist ein Gitter aus 2-m-Zellen über die ganze Karte, das dem Gelände folgt (0,45 m darüber). Die Deckkraft der Gitterpunkte wird zehnmal pro Sekunde aus den Sichtquellen berechnet, mit weicher Kante von 2 m: außerhalb jeder Sicht 55 % Nachtblau, innerhalb klar. Shader `Luneraria/FogOverlay` (transparent, Deckkraft aus den Vertexfarben). Die Sichtquellen kommen aus `LuneFog.Vision`; dieselbe Funktion steuert das Ausblenden. Der Nebel ist nur aktiv mit erweiterten Regeln und einem menschlichen Sitz |
| Glyphe für Gäste | Der Kampfrahmen hängt an die Gebäude-Lebenspunkte vier Werte an: je Team die Lane der laufenden Glyphe und die Restsekunden. Gäste setzen damit `GlyphLane`/`GlyphUntil` und sehen den Glyphen-Ring. Ältere Gäste lesen nur die Gebäudewerte und ignorieren den Rest |
Die Simulation ist unberührt: Betäubungs-Ring, Nebel-Gitter und Gast-Glyphe sind reine Darstellung.
## Tests und Nachweise
| Prüfung | Ergebnis |
| --- | --- |
| Kern (.NET 8) | 627/627, darunter **s4 8**: Über die Loopback-Verbindung sieht der Gast die Glyphe von Blau auf Mid, den Mid-Turm als geschützt und die Betäubung genau einer Einheit; Host und Gast zählen gleich viele Sichtquellen |
| Golden und Fingerabdrücke | 0 Abweichungen; `DCE95C96556F3A1A` unverändert |
| Unity EditMode | 35/35 |
| Unity PlayMode | 20/20, neu `S4PolishViewsTests.S4Views`: Nebel aktiv, eigener Ancient im Licht (< 5 %), gegnerischer Ancient im Nebel (> 50 %), Betäubungs-Ring sichtbar, kein Nebel unter den Referenzregeln. Verify A–I 718/0 |
| Player-Selbsttest | macOS 30/30 |
| Leistung | Benchmark (Bot-Partie, sechs Brunhilds): Karte 148,6–151,5 fps, Mid nah 163,4–167,3 fps, Ansichten 0,19–0,22 ms. Der Benchmark hat keinen menschlichen Sitz, der Nebel ist dort also aus. Nicht gemessene Schätzung für den Nebel: 1.521 Gitterpunkte gegen 50–90 Sichtquellen, zehnmal pro Sekunde |
Bilder in `docs/port/screenshots/s4/spiel/`:
1. Nebel in der Übersicht
2. Nebelgrenze auf Mid
3. Betäubung

Some files were not shown because too many files have changed in this diff Show more