添加视觉小说编辑器

This commit is contained in:
milimoe 2025-02-13 00:14:11 +08:00
parent e24346bf57
commit 7ff4b56444
Signed by: milimoe
GPG Key ID: 05D280912DA6C69E
9 changed files with 670 additions and 20 deletions

8
Desktop/App.xaml Normal file
View File

@ -0,0 +1,8 @@
<Application x:Class="Milimoe.FunGame.Testing.Desktop.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Solutions/NovelEditor/NovelEditor.xaml">
<Application.Resources>
</Application.Resources>
</Application>

12
Desktop/App.xaml.cs Normal file
View File

@ -0,0 +1,12 @@
using Application = System.Windows.Application;
namespace Milimoe.FunGame.Testing.Desktop
{
public partial class App : Application
{
public App()
{
}
}
}

View File

@ -8,6 +8,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<BaseOutputPath>..\bin\</BaseOutputPath> <BaseOutputPath>..\bin\</BaseOutputPath>
<RootNamespace>Milimoe.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace> <RootNamespace>Milimoe.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
<UseWPF>True</UseWPF>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -22,6 +23,10 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Page Include="App.xaml" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="FunGame.Core"> <Reference Include="FunGame.Core">
<HintPath>..\..\FunGame.Core\bin\Debug\net9.0\FunGame.Core.dll</HintPath> <HintPath>..\..\FunGame.Core\bin\Debug\net9.0\FunGame.Core.dll</HintPath>

View File

@ -1,20 +1,20 @@
using Milimoe.FunGame.Testing.Desktop.Solutions; //using Milimoe.FunGame.Testing.Desktop.Solutions;
namespace Desktop //namespace Desktop
{ //{
internal static class Program // internal static class Program
{ // {
/// <summary> // /// <summary>
/// The main entry point for the application. // /// The main entry point for the application.
/// </summary> // /// </summary>
[STAThread] // [STAThread]
static void Main() // static void Main()
{ // {
// To customize application configuration such as set high DPI settings or default font, // // To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration. // // see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize(); // ApplicationConfiguration.Initialize();
//Application.Run(new ChessBoardExample.ChessBoardExample()); // //Application.Run(new ChessBoardExample.ChessBoardExample());
Application.Run(new EntityEditor()); // Application.Run(new EntityEditor());
} // }
} // }
} //}

View File

@ -0,0 +1,89 @@
<Window x:Class="Milimoe.FunGame.Testing.Desktop.Solutions.NovelEditor.NovelEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Milimoe.FunGame.Testing.Desktop.Solutions.NovelEditor"
mc:Ignorable="d"
Title="NovelEditor" Height="450" Width="800"
>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧:节点列表和操作按钮 -->
<StackPanel Grid.Column="0" Margin="10">
<Button Content="添加节点" Click="AddNodeButton_Click" Margin="0,0,0,10"/>
<Button Content="编辑节点" Click="EditNodeButton_Click" Margin="0,0,0,10"/>
<ListBox x:Name="NodeListBox" SelectionChanged="NodeListBox_SelectionChanged" d:ItemsSource="{d:SampleData ItemCount=5}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Key}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
<!-- 右侧:节点详情 -->
<ScrollViewer Grid.Column="1" Margin="10">
<StackPanel Grid.Column="1" Margin="10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="节点详情" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"/>
<!-- 导航按钮 -->
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="上一个" Click="PreviousSentence_Click" Margin="5,0,0,0" Width="80"/>
<Button Content="下一个" Click="NextSentence_Click" Margin="5,0,0,0" Width="80"/>
</StackPanel>
</Grid>
<TextBlock x:Name="NodeKeyText" Margin="0,8,0,0" LineHeight="20" />
<TextBlock x:Name="NodeNameText" Margin="0,8,0,0" LineHeight="20" />
<TextBlock x:Name="NodeContentText" Margin="0,8,0,0" TextWrapping="Wrap" LineHeight="20"/>
<!-- 显示条件 -->
<TextBlock Text="显示条件" FontSize="14" FontWeight="Bold" Margin="0,10,0,0"/>
<TextBlock x:Name="NodeConditionsText" Margin="0,5,0,0" TextWrapping="Wrap" LineHeight="20" />
<!-- 选项 -->
<TextBlock Text="选项" FontSize="14" FontWeight="Bold" Margin="0,10,0,0"/>
<ListBox x:Name="OptionsListBox" d:ItemsSource="{d:SampleData ItemCount=5}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Width="auto">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- 使用 ItemsControl 为每个 TargetNode 创建一个按钮 -->
<ItemsControl Grid.Column="1" ItemsSource="{Binding Targets}" HorizontalAlignment="Right">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<DockPanel LastChildFill="False" HorizontalAlignment="Right"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Key}" Click="OptionButton_Click" Tag="{Binding}" Margin="5" Width="80" DockPanel.Dock="Right"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding Name}" FontWeight="Bold"/>
<TextBlock Text="{Binding Conditions}" Margin="10,0,0,0" TextWrapping="Wrap" Foreground="Gray"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</ScrollViewer>
</Grid>
</Window>

View File

@ -0,0 +1,218 @@
using System.Windows;
using System.Windows.Controls;
using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Model;
using Button = System.Windows.Controls.Button;
using MessageBox = System.Windows.MessageBox;
namespace Milimoe.FunGame.Testing.Desktop.Solutions.NovelEditor
{
/// <summary>
/// NovelEditor.xaml 的交互逻辑
/// </summary>
public partial class NovelEditor : Window
{
public static Dictionary<Character, int> Likability { get; } = [];
public static Dictionary<string, Func<bool>> Conditions { get; } = [];
public static Character MainCharacter { get; set; } = Factory.GetCharacter();
public static Character { get; set; } = Factory.GetCharacter();
private readonly NovelConfig _config;
public NovelEditor()
{
InitializeComponent();
MainCharacter.Name = "主角";
MainCharacter.NickName = "主角";
.Name = "马猴烧酒";
.NickName = "魔法少女";
Likability.Add(, 100);
Conditions.Add("马猴烧酒的好感度低于50", () => 50());
Conditions.Add("主角攻击力大于20", () => 20(MainCharacter));
Conditions.Add("马猴烧酒攻击力大于20", () => 20());
// 如果需要,初始化小说
//NovelTest.CreateNovels();
// 小说配置
_config = new NovelConfig("novel1", "chapter1");
LoadNovelData();
// 绑定节点列表
NodeListBox.ItemsSource = _config.Values;
if (_config.Count > 0)
{
NodeListBox.SelectedIndex = 0;
}
}
private void LoadNovelData()
{
// 加载已有的小说数据
_config.LoadConfig(Conditions);
}
private void NodeListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (NodeListBox.SelectedItem is NovelNode selectedNode)
{
// 显示节点详情
NodeKeyText.Text = "编号:" + selectedNode.Key;
NodeNameText.Text = "角色:" + selectedNode.Name;
NodeContentText.Text = "文本:" + selectedNode.Content;
// 显示节点的显示条件
NodeConditionsText.Text = FormatConditions(selectedNode);
// 显示选项
OptionsListBox.ItemsSource = selectedNode.Options.Select(option => new
{
option.Name,
option.Targets,
Conditions = FormatOptionConditions(option)
}).ToList();
}
}
private void PreviousSentence_Click(object sender, RoutedEventArgs e)
{
if (NodeListBox.SelectedItem is NovelNode selectedNode)
{
if (selectedNode.Previous is null)
{
if (NodeListBox.SelectedIndex - 1 >= 0)
{
MessageBox.Show("此节点没有定义上一个节点!现在跳转列表的上一个节点。");
NodeListBox.SelectedIndex--;
}
else
{
MessageBox.Show("此节点没有定义上一个节点,并且已经达到列表顶部!");
}
}
else
{
NodeListBox.SelectedItem = selectedNode.Previous;
}
}
}
private void NextSentence_Click(object sender, RoutedEventArgs e)
{
if (NodeListBox.SelectedItem is NovelNode selectedNode)
{
NovelNode? next = selectedNode.Next;
if (next is null)
{
if (NodeListBox.SelectedIndex + 1 < NodeListBox.Items.Count)
{
MessageBox.Show("此节点没有定义下一个节点!现在跳转列表的下一个节点。");
NodeListBox.SelectedIndex++;
}
else
{
MessageBox.Show("此节点没有定义下一个节点,并且已经到达列表底部!现在跳转到列表的起始处。");
NodeListBox.SelectedIndex = 0;
}
}
else
{
NodeListBox.SelectedItem = next;
}
}
}
private static string FormatConditions(NovelNode node)
{
List<string> conditions = [];
if (node.AndPredicates.Count != 0)
{
conditions.Add("需满足以下所有条件:");
foreach (string condition in node.AndPredicates.Keys)
{
conditions.Add($"- {condition}");
}
}
if (node.OrPredicates.Count != 0)
{
conditions.Add("需满足以下任意一个条件:");
foreach (string condition in node.OrPredicates.Keys)
{
conditions.Add($"- {condition}");
}
}
return conditions.Count != 0 ? string.Join(Environment.NewLine, conditions) : "无显示条件";
}
private static string FormatOptionConditions(NovelOption option)
{
List<string> conditions = [];
if (option.AndPredicates.Count != 0)
{
conditions.Add("需满足以下所有条件:");
foreach (string condition in option.AndPredicates.Keys)
{
conditions.Add($"- {condition}");
}
}
if (option.OrPredicates.Count != 0)
{
conditions.Add("需满足以下任意一个条件:");
foreach (string condition in option.OrPredicates.Keys)
{
conditions.Add($"- {condition}");
}
}
return conditions.Count != 0 ? string.Join(Environment.NewLine, conditions) : "";
}
private void AddNodeButton_Click(object sender, RoutedEventArgs e)
{
NovelNode newNode = new()
{
Key = "示例节点编号",
Name = "示例发言人",
Content = "示例发言内容"
};
_config.Add(newNode.Key, newNode);
NodeListBox.Items.Refresh();
}
private void EditNodeButton_Click(object sender, RoutedEventArgs e)
{
if (NodeListBox.SelectedItem is NovelNode selectedNode)
{
selectedNode.Content = "此示例节点已被编辑";
NodeContentText.Text = "文本:" + selectedNode.Content;
}
}
private void OptionButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.Tag is NovelNode node && NodeListBox.Items.OfType<NovelNode>().FirstOrDefault(n => n.Key == node.Key) is NovelNode target)
{
NodeListBox.SelectedItem = target;
NodeListBox.ScrollIntoView(target);
}
}
public static bool 20(Character character)
{
return character.ATK > 20;
}
public static bool 50(Character character)
{
return Likability[character] > 50;
}
}
}

View File

@ -0,0 +1,104 @@
using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Model;
namespace Milimoe.FunGame.Testing.Desktop.Solutions.NovelEditor
{
internal class NovelTest
{
internal static void CreateNovels()
{
NovelNode node1 = new()
{
Key = "node1",
Name = "声音",
Content = "听说你在等我?我来了!"
};
NovelNode node2 = new()
{
Key = "node2",
Name = NovelEditor.MainCharacter.NickName,
Content = "什么人!"
};
node1.NextNodes.Add(node2);
node2.Previous = node1;
NovelOption option1 = new()
{
Key = "option1",
Name = "你好。"
};
NovelOption option2 = new()
{
Key = "option2",
Name = "我不认识你。",
AndPredicates = new()
{
{ "主角攻击力大于20", NovelEditor.Conditions["主角攻击力大于20"] }
}
};
NovelNode node3 = new()
{
Key = "node3",
Name = NovelEditor..NickName,
Content = "你好,我叫【马猴烧酒】!",
Options = [option1, option2]
};
NovelNode node4 = new()
{
Key = "node4",
Name = NovelEditor..NickName,
Content = "你的名字是?"
};
NovelNode node5 = new()
{
Key = "node5",
Name = NovelEditor..NickName,
Content = "滚,谁要认识你?"
};
node2.NextNodes.Add(node3);
option1.Targets.Add(node4);
option2.Targets.Add(node5);
NovelNode node6 = new()
{
Key = "node6",
Content = "旁白:示例结束。"
};
NovelOption option3 = new()
{
Key = "option3",
Name = "重新开始游戏",
Targets = [node1]
};
NovelNode node7 = new()
{
Key = "node7",
Priority = 2,
Content = "旁白:示例结束,你被马猴烧酒吃掉了。",
Options = [option3],
AndPredicates = new()
{
{ "主角攻击力大于20", NovelEditor.Conditions["主角攻击力大于20"] }
},
OrPredicates = new()
{
{ "马猴烧酒的好感度低于50", NovelEditor.Conditions["马猴烧酒的好感度低于50"] },
{ "马猴烧酒攻击力大于20", NovelEditor.Conditions["马猴烧酒攻击力大于20"] }
}
};
node4.NextNodes.Add(node6);
node5.NextNodes.Add(node6);
node5.NextNodes.Add(node7);
NovelConfig config = new("novel1", "chapter1")
{
{ node1.Key, node1 },
{ node2.Key, node2 },
{ node3.Key, node3 },
{ node4.Key, node4 },
{ node5.Key, node5 },
{ node6.Key, node6 },
{ node7.Key, node7 }
};
config.SaveConfig();
}
}
}

View File

@ -4,7 +4,7 @@ using Oshima.FunGame.OshimaModules;
using Oshima.FunGame.OshimaServers.Service; using Oshima.FunGame.OshimaServers.Service;
using Oshima.FunGame.WebAPI.Controllers; using Oshima.FunGame.WebAPI.Controllers;
//7 _ = new Milimoe.FunGame.Testing.Solutions.Novels();
//_ = new Milimoe.FunGame.Testing.Tests.CheckDLL(); //_ = new Milimoe.FunGame.Testing.Tests.CheckDLL();

214
Library/Solutions/Novels.cs Normal file
View File

@ -0,0 +1,214 @@
using System.Text;
using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Model;
using MilimoeFunGame.Testing.Characters;
namespace Milimoe.FunGame.Testing.Solutions
{
public class Novels
{
public static Dictionary<Character, int> Likability { get; } = [];
public static Dictionary<string, Func<bool>> Conditions { get; } = [];
public static bool 20(Character character)
{
return character.ATK > 20;
}
public static bool 50(Character character)
{
return Likability[character] > 50;
}
public Novels()
{
Character main = OshimaCharacters.Oshima;
Character character = OshimaCharacters.;
Likability.Add(character, 100);
Conditions.Add("马猴烧酒的好感度低于50", () => 50(character));
Conditions.Add("主角攻击力大于20", () => 20(main));
Conditions.Add("马猴烧酒攻击力大于20", () => 20(character));
NovelNode node1 = new()
{
Key = "node1",
Name = "声音",
Content = "听说你在等我?我来了!"
};
NovelNode node2 = new()
{
Key = "node2",
Name = main.NickName,
Content = "什么人!"
};
node1.NextNodes.Add(node2);
node2.Previous = node1;
NovelOption option1 = new()
{
Key = "option1",
Name = "你好。"
};
NovelOption option2 = new()
{
Key = "option2",
Name = "我不认识你。",
AndPredicates = new()
{
{ "主角攻击力大于20", Conditions["主角攻击力大于20"] }
}
};
NovelNode node3 = new()
{
Key = "node3",
Name = character.NickName,
Content = "你好,我叫【马猴烧酒】!",
Options = [option1, option2]
};
NovelNode node4 = new()
{
Key = "node4",
Name = character.NickName,
Content = "你的名字是?"
};
NovelNode node5 = new()
{
Key = "node5",
Name = character.NickName,
Content = "滚,谁要认识你?"
};
node2.NextNodes.Add(node3);
option1.Targets.Add(node4);
option2.Targets.Add(node5);
NovelNode node6 = new()
{
Key = "node6",
Content = "旁白:示例结束。"
};
NovelNode node7 = new()
{
Key = "node7",
Priority = 2,
Content = "旁白:示例结束,你被马猴烧酒吃掉了。",
AndPredicates = new()
{
{ "主角攻击力大于20", Conditions["主角攻击力大于20"] }
},
OrPredicates = new()
{
{ "马猴烧酒的好感度低于50", Conditions["马猴烧酒的好感度低于50"] },
{ "马猴烧酒攻击力大于20", Conditions["马猴烧酒攻击力大于20"] }
}
};
node4.NextNodes.Add(node6);
node5.NextNodes.Add(node6);
node5.NextNodes.Add(node7);
NovelConfig config = new("novel1", "chapter1")
{
{ node1.Key, node1 },
{ node2.Key, node2 },
{ node3.Key, node3 },
{ node4.Key, node4 },
{ node5.Key, node5 },
{ node6.Key, node6 },
{ node7.Key, node7 }
};
config.SaveConfig();
NovelConfig config2 = new("novel1", "chapter1");
config2.LoadConfig(Conditions);
foreach (NovelNode node in config2.Values)
{
StringBuilder builder = new();
builder.AppendLine("== 节点:" + node.Key + " ==");
if (node.AndPredicates.Union(node.OrPredicates).Any())
{
builder.AppendLine("对话触发条件(需满足以下所有条件):");
int count = 0;
if (node.AndPredicates.Count > 0)
{
count++;
builder.AppendLine(count + ". " + "满足以下所有子条件:");
int subCount = 0;
foreach (string ap in node.AndPredicates.Keys)
{
subCount++;
builder.AppendLine("(" + subCount + ") " + ap);
}
}
if (node.OrPredicates.Count > 0)
{
count++;
builder.AppendLine(count + ". " + "满足以下任意一个子条件:");
int subCount = 0;
foreach (string op in node.OrPredicates.Keys)
{
subCount++;
builder.AppendLine("(" + subCount + ") " + op);
}
}
}
if (node.Name != "")
{
builder.Append(node.Name + "说:");
}
builder.AppendLine(node.Content);
if (node.Options.Count > 0)
{
builder.AppendLine("选项:");
int count = 0;
foreach (NovelOption option in node.Options)
{
count++;
builder.AppendLine(count + ". " + option.Name + "【可跳转:" + string.Join("", option.Targets.Select(t => t.Key)) + "】");
if (option.AndPredicates.Union(option.OrPredicates).Any())
{
builder.AppendLine("选项显示条件(需满足以下所有条件):");
int optionCount = 0;
if (option.AndPredicates.Count > 0)
{
optionCount++;
builder.AppendLine(optionCount + ". " + "满足以下所有子条件:");
int subCount = 0;
foreach (string ap in option.AndPredicates.Keys)
{
subCount++;
builder.AppendLine("(" + subCount + ") " + ap);
}
}
if (option.OrPredicates.Count > 0)
{
optionCount++;
builder.AppendLine(optionCount + ". " + "满足以下任意一个子条件:");
int subCount = 0;
foreach (string op in option.OrPredicates.Keys)
{
subCount++;
builder.AppendLine("(" + subCount + ") " + op);
}
}
}
}
}
if (node.NextNodes.Count > 0)
{
builder.AppendLine("下一句对话:");
int count = 0;
foreach (NovelNode next in node.NextNodes)
{
count++;
builder.AppendLine(count + ". " + next.Key);
}
}
Console.WriteLine(builder.ToString());
}
}
}
}