添加新功能

This commit is contained in:
milimoe 2026-05-14 20:19:29 +08:00
parent a769cdf25c
commit 67187200ca
Signed by: milimoe
GPG Key ID: 9554D37E4B8991D0
18 changed files with 1026 additions and 65 deletions

View File

@ -0,0 +1,89 @@
using System.Text;
using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Interface.Entity;
using Milimoe.FunGame.Core.Library.Constant;
using Oshima.FunGame.OshimaModules.BusinessSimulation.Interface;
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Entity
{
public class HumanResource : BaseEntity, IDescription, IBusinessSimulationEntity
{
public virtual QualityType QualityType { get; set; } = QualityType.White;
public virtual HumanResourceType HumanResourceType { get; set; } = HumanResourceType.General;
public virtual string Description { get; set; } = "";
public virtual string GeneralDescription { get; set; } = "";
public virtual string BackgroundStory { get; set; } = "";
public virtual string Category { get; set; } = "";
public bool Enable { get; set; } = true;
public Dictionary<string, string> SkillInfo { get; set; } = [];
public string ToString(bool isShowGeneralDescription, bool isShowInStore = false)
{
StringBuilder builder = new();
builder.AppendLine($"【{Name}】");
string itemquality = ItemSet.GetQualityTypeName(QualityType);
string itemtype = GetHumanResourceTypeName(HumanResourceType);
if (itemtype != "") itemtype = $" {itemtype}";
builder.AppendLine($"{itemquality + itemtype}");
if (!string.IsNullOrWhiteSpace(Category)) builder.AppendLine(Category);
if (isShowGeneralDescription && GeneralDescription != "")
{
builder.AppendLine("描述:" + GeneralDescription);
}
else if (Description != "")
{
builder.AppendLine("描述:" + Description);
}
if (SkillInfo.Count > 0)
{
builder.AppendLine("=== 技能 ===");
foreach (var skill in SkillInfo)
{
builder.AppendLine($"{skill.Key}{(!string.IsNullOrWhiteSpace(skill.Value) ? $"{skill.Value}" : "")}");
}
}
if (BackgroundStory != "")
{
builder.AppendLine($"\"{BackgroundStory}\"");
}
return builder.ToString();
}
public override bool Equals(IBaseEntity? other) => other is HumanResource hr && GetIdName() == hr.GetIdName();
public static string GetHumanResourceTypeName(HumanResourceType type)
{
return type switch
{
HumanResourceType.General => "通用人力资源",
HumanResourceType.Employee => "员工",
HumanResourceType.Manager => "经理",
HumanResourceType.Team => "团队",
HumanResourceType.CooperationPartner => "合作伙伴",
_ => "未知人力资源"
};
}
public virtual void UpdateSkillInfo()
{
}
}
public enum HumanResourceType
{
General,
Employee,
Manager,
Team,
CooperationPartner
}
}

View File

@ -0,0 +1,28 @@
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Entity
{
public class DeviceMovableProperty : MovableProperty
{
public override MovablePropertyType MovablePropertyType => MovablePropertyType.Device;
public virtual string DeviceType { get; set; } = "";
public virtual double ProductionSpeedBonusPercent { get; set; } = 0;
public virtual double EnergyConsumption { get; set; } = 0;
public virtual int DeviceLevel { get; set; } = 1;
public List<string> CompatibleItem { get; set; } = [];
public DeviceMovableProperty()
{
UpdateSkillInfo();
}
public override void UpdateSkillInfo()
{
base.UpdateSkillInfo();
if (ProductionSpeedBonusPercent > 0) SkillInfo["生产速度"] = $"{ProductionSpeedBonusPercent:0.##}%";
if (EnergyConsumption > 0) SkillInfo["能耗"] = $"{EnergyConsumption:0.##} 每日";
if (DeviceLevel > 0) SkillInfo["设备等级"] = $"{DeviceLevel}";
if (CompatibleItem.Count > 0) SkillInfo["适用货物"] = $"\r\n{string.Join("\r\n", CompatibleItem)}";
}
}
}

View File

@ -0,0 +1,136 @@
using System.Text;
using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Interface.Entity;
using Milimoe.FunGame.Core.Library.Constant;
using Oshima.FunGame.OshimaModules.BusinessSimulation.Interface;
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Entity
{
public class MovableProperty : BaseEntity, IDescription, IBusinessSimulationEntity
{
public virtual QualityType QualityType { get; set; } = QualityType.White;
public virtual MovablePropertyType MovablePropertyType { get; set; } = MovablePropertyType.General;
public virtual string Description { get; set; } = "";
public virtual string GeneralDescription { get; set; } = "";
public virtual string BackgroundStory { get; set; } = "";
public virtual double Price { get; set; } = 0;
public virtual string Category { get; set; } = "";
public virtual double MaintenanceCost { get; set; } = 0;
public virtual string MaintenanceUnit { get; set; } = "每日";
public bool Enable { get; set; } = true;
public bool IsPurchasable { get; set; } = true;
public double OriginalPrice { get; set; } = 0;
public bool IsSellable { get; set; } = true;
public DateTime NextSellableTime { get; set; } = DateTime.MinValue;
public Dictionary<string, string> SkillInfo { get; set; } = [];
public string ToString(bool isShowGeneralDescription, bool isShowInStore = false)
{
StringBuilder builder = new();
builder.AppendLine($"【{Name}】");
string itemquality = ItemSet.GetQualityTypeName(QualityType);
string itemtype = GetMovablePropertyTypeName(MovablePropertyType);
if (itemtype != "") itemtype = $" {itemtype}";
builder.AppendLine($"{itemquality + itemtype}");
if (!string.IsNullOrWhiteSpace(Category)) builder.AppendLine(Category);
if (isShowInStore && Price > 0)
{
builder.AppendLine($"售价:{Price:0.##} {GameplayEquilibriumConstant.InGameCurrency}");
}
else if (Price > 0)
{
builder.AppendLine($"回收价:{Price:0.##} {GameplayEquilibriumConstant.InGameCurrency}");
}
if (OriginalPrice > 0) builder.AppendLine($"在商店或市场中出售时,售价不得超过原价:{OriginalPrice:0.##} {GameplayEquilibriumConstant.InGameCurrency}");
if (isShowInStore)
{
if (IsSellable)
{
builder.AppendLine($"购买此资产后可立即出售");
}
else if (NextSellableTime != DateTime.MinValue)
{
builder.AppendLine($"购买此资产后,将在 {NextSellableTime.ToString(General.GeneralDateTimeFormatChinese)} 后可出售");
}
}
else
{
if (IsSellable)
{
builder.AppendLine("可出售");
}
else if (!IsSellable && NextSellableTime != DateTime.MinValue)
{
builder.AppendLine($"此资产将在 {NextSellableTime.ToString(General.GeneralDateTimeFormatChinese)} 后可出售");
}
else if (!IsSellable)
{
builder.AppendLine("不可出售");
}
}
if (MaintenanceCost > 0)
{
builder.AppendLine($"维护费:{(MaintenanceUnit != "" ? MaintenanceUnit : "")} {MaintenanceCost:0.##} {GameplayEquilibriumConstant.InGameCurrency}");
}
if (isShowGeneralDescription && GeneralDescription != "")
{
builder.AppendLine("资产描述:" + GeneralDescription);
}
else if (Description != "")
{
builder.AppendLine("资产描述:" + Description);
}
if (SkillInfo.Count > 0)
{
builder.AppendLine("=== 资产技能 ===");
foreach (var skill in SkillInfo)
{
builder.AppendLine($"{skill.Key}{(!string.IsNullOrWhiteSpace(skill.Value) ? $"{skill.Value}" : "")}");
}
}
if (BackgroundStory != "")
{
builder.AppendLine($"\"{BackgroundStory}\"");
}
return builder.ToString();
}
public override bool Equals(IBaseEntity? other) => other is MovableProperty mp && GetIdName() == mp.GetIdName();
public static string GetMovablePropertyTypeName(MovablePropertyType type)
{
return type switch
{
MovablePropertyType.General => "通用资产",
MovablePropertyType.OfficeAsset => "办公资产",
MovablePropertyType.Device => "设备资产",
MovablePropertyType.Vehicle => "载具资产",
_ => "未知资产"
};
}
public virtual void UpdateSkillInfo()
{
}
}
public enum MovablePropertyType
{
General,
OfficeAsset,
Device,
Vehicle
}
}

View File

@ -0,0 +1,22 @@
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Entity
{
public class OfficeAssetMovableProperty : MovableProperty
{
public override MovablePropertyType MovablePropertyType => MovablePropertyType.OfficeAsset;
public virtual string OfficeAssetType { get; set; } = "";
public virtual int ComfortBonus { get; set; } = 0;
public virtual double EfficiencyBonusPercent { get; set; } = 0;
public OfficeAssetMovableProperty()
{
UpdateSkillInfo();
}
public override void UpdateSkillInfo()
{
if (ComfortBonus > 0) SkillInfo["舒适度"] = $"{ComfortBonus}";
if (EfficiencyBonusPercent > 0) SkillInfo["办公效率"] = $"{EfficiencyBonusPercent * 100:0.##}%";
}
}
}

View File

@ -0,0 +1,101 @@
using System.Text;
using Milimoe.FunGame.Core.Entity;
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Entity
{
public class VehicleMovableProperty : MovableProperty
{
public override string Category => "载具";
public override MovablePropertyType MovablePropertyType => MovablePropertyType.Vehicle;
public virtual string VehicleType { get; set; } = "";
public virtual int PassengerCapacity { get; set; } = 0;
public virtual int CargoCapacity { get; set; } = 0;
public virtual double TransportTime { get; set; } = 5;
public virtual double TransportCost { get; set; } = 0;
public virtual double LoadingTime { get; set; } = 0;
public virtual List<RealEstate> ParkingAvailable { get; set; } = [];
public bool Enabled
{
get => !IsParked && field;
set;
}
public bool IsParked { get; set; } = false;
public PackingRequirement ParkingIn { get; set; } = new(Guid.Empty, "");
public List<TransportSchedule> TransportScheduleList { get; } = [];
public List<Character> VehicleOperations { get; } = [];
public VehicleMovableProperty()
{
UpdateSkillInfo();
}
public override void UpdateSkillInfo()
{
base.UpdateSkillInfo();
if (PassengerCapacity > 0) SkillInfo["乘客定员"] = $"{PassengerCapacity}";
if (CargoCapacity > 0) SkillInfo["载货容量"] = $"{CargoCapacity}";
if (TransportTime > 0) SkillInfo["运输时间"] = $"{TransportTime:0.##} 分钟 / 次";
if (TransportCost > 0) SkillInfo["运输成本"] = $"{TransportCost:0.##} {GameplayEquilibriumConstant.InGameCurrency} / 次";
if (LoadingTime > 0) SkillInfo["装载时间"] = $"{LoadingTime:0.##} 分钟 / 百件货物";
if (VehicleOperations.Count > 0) SkillInfo["车务担当"] = $"\r\n{string.Join("\r\n", VehicleOperations.Select(c => c.ToStringWithLevelWithOutUser()))}";
if (TransportScheduleList.Count > 0)
{
int count = 0;
SkillInfo["时间表"] = $"";
foreach (TransportSchedule ts in TransportScheduleList)
{
SkillInfo[$"第 {++count} 站"] = $"\r\n{ts}";
}
}
SkillInfo["停放于"] = $"\r\n{ParkingIn}";
}
}
public enum LoadingStrategy
{
Full = 1,
Half = 2,
AllowForTime = 3,
SpecifiedCapacity = 4
}
public class TransportSchedule(Guid esId, string esName)
{
public Guid TargetRealEstate { get; set; } = esId;
public string TargetRealEstateName { get; set; } = esName;
public LoadingStrategy LoadingStrategy { get; set; } = LoadingStrategy.Full;
public int WaitingTime { get; set; } = 0;
public double SpecifiedCapacity { get; set; } = 0;
public override string ToString()
{
StringBuilder sb = new();
sb.AppendLine($"目标地点:{TargetRealEstateName}");
sb.AppendLine($"装载策略:{GetLoadingStrategyName(LoadingStrategy)}");
return sb.ToString();
}
public string GetLoadingStrategyName(LoadingStrategy ls)
{
return ls switch
{
LoadingStrategy.Full => "满载",
LoadingStrategy.Half => "半载",
LoadingStrategy.AllowForTime => $"等待 {WaitingTime} 分钟",
LoadingStrategy.SpecifiedCapacity => $"装载容量达到 {SpecifiedCapacity} 件",
_ => "未知装载策略"
};
}
}
public class PackingRequirement(Guid esId, string esName)
{
public Guid TargetRealEstate { get; set; } = esId;
public string TargetRealEstateName { get; set; } = esName;
public override string ToString() => TargetRealEstateName;
}
}

View File

@ -0,0 +1,69 @@
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Entity
{
public class CommercialRealEstate : RealEstate
{
public override RealEstateType RealEstateType => RealEstateType.Commercial;
public virtual string CommercialType { get; set; } = "";
public virtual double Attractiveness { get; set; } = 0;
public CommercialRealEstate()
{
UpdateSkillInfo();
}
public override void UpdateSkillInfo()
{
if (Attractiveness > 0) SkillInfo["吸引力"] = $"{Attractiveness * 100:0.##}%";
}
}
public class Shop : CommercialRealEstate
{
public override string CommercialType => "商铺";
public virtual int ShelfCount { get; set; } = 0;
public virtual int InventoryCapacity { get; set; } = 0;
public virtual int TruckCount { get; set; } = 0;
public Shop()
{
UpdateSkillInfo();
}
public override void UpdateSkillInfo()
{
base.UpdateSkillInfo();
if (ShelfCount > 0) SkillInfo["货架数量"] = $"{ShelfCount}";
if (InventoryCapacity > 0) SkillInfo["库存容量"] = $"{InventoryCapacity}";
if (TruckCount > 0) SkillInfo["附赠货车"] = $"{TruckCount}";
}
}
public class ParkingLot : CommercialRealEstate
{
public override string CommercialType => "停车场";
public virtual int BonusTruckCount { get; set; } = 0;
public virtual int MaxTruckCapacity { get; set; } = 0;
public virtual int PublicSpots { get; set; } = 0;
public virtual double HourlyParkingFee { get; set; } = 0;
public int CurrentTruckCount { get; set; } = 0;
public int CurrentPublicVehicles { get; set; } = 0;
public ParkingLot()
{
UpdateSkillInfo();
}
public override void UpdateSkillInfo()
{
base.UpdateSkillInfo();
if (BonusTruckCount > 0) SkillInfo["附赠货车"] = $"{BonusTruckCount}";
if (MaxTruckCapacity > 0) SkillInfo["货车停放"] = $"{CurrentTruckCount} / {MaxTruckCapacity}";
if (PublicSpots > 0) SkillInfo["公共车位"] = $"{CurrentPublicVehicles} / {PublicSpots}";
if (HourlyParkingFee > 0) SkillInfo["公共停车费收入"] = $"{HourlyParkingFee:0.##} {GameplayEquilibriumConstant.InGameCurrency} / 辆每小时";
}
}
}

View File

@ -0,0 +1,70 @@
using System.Text;
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Entity
{
public class IndustrialRealEstate : RealEstate
{
public override RealEstateType RealEstateType => RealEstateType.Industrial;
public virtual string IndustryType { get; set; } = "";
}
public class Factory : IndustrialRealEstate
{
public override string IndustryType => "工厂";
public virtual int TruckCount { get; set; } = 0;
public virtual int MaxProductionLines { get; } = 1;
public virtual double CostPerLine { get; set; } = 0;
public Dictionary<int, ProductionLine> ProductionLines { get; set; } = [];
public int ActiveLines => ProductionLines.Values.Count(pl => pl.Active);
public override double MaintenanceCost => ActiveLines * CostPerLine;
public override string MaintenanceUnit => "每条有效流水线每日";
public Factory(int maxProductionLines = 1)
{
Category = "工厂";
MaxProductionLines = maxProductionLines;
// 注意:工厂一经创建,流水线数量便无法更改
for (int i = 0; i < MaxProductionLines; i++)
{
ProductionLines[i] = new();
}
UpdateSkillInfo();
}
public override void UpdateSkillInfo()
{
if (TruckCount > 0) SkillInfo["附赠货车"] = $"{TruckCount}";
if (MaxProductionLines > 0)
{
SkillInfo["流水线数量"] = $"{ActiveLines} / {MaxProductionLines}";
if (ProductionLines.Count > 0)
{
foreach (var line in ProductionLines)
{
SkillInfo[$"流水线 {line.Key}"] = $"\r\n{line.Value}";
}
}
}
}
}
public class ProductionLine
{
public bool Active { get; set; } = false;
public string ItemName { get; set; } = "";
public double CountPerMinute => 60.0 / UnitProductionTime;
public int UnitProductionTime { get; set; } = 0;
public override string ToString()
{
StringBuilder sb = new();
sb.AppendLine($"是否启用:{(Active ? "" : "")}");
sb.AppendLine($"生产货物:{ItemName}");
sb.AppendLine($"生产时间:{UnitProductionTime} 秒 / 件");
sb.AppendLine($"理论产量:{CountPerMinute:0.##} 件 / 分钟");
return sb.ToString();
}
}
}

View File

@ -0,0 +1,138 @@
using System.Text;
using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Interface.Entity;
using Milimoe.FunGame.Core.Library.Constant;
using Oshima.FunGame.OshimaModules.BusinessSimulation.Interface;
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Entity
{
public class RealEstate : BaseEntity, IDescription, IBusinessSimulationEntity
{
public virtual QualityType QualityType { get; set; } = QualityType.White;
public virtual RealEstateType RealEstateType { get; set; } = RealEstateType.General;
public virtual string Description { get; set; } = "";
public virtual string GeneralDescription { get; set; } = "";
public virtual string BackgroundStory { get; set; } = "";
public virtual double Price { get; set; } = 0;
public virtual string Category { get; set; } = "";
public virtual double MaintenanceCost { get; set; } = 0;
public virtual string MaintenanceUnit { get; set; } = "每日";
public bool Enable { get; set; } = true;
public bool IsPurchasable { get; set; } = true;
public double OriginalPrice { get; set; } = 0;
public bool IsSellable { get; set; } = true;
public DateTime NextSellableTime { get; set; } = DateTime.MinValue;
public Dictionary<string, string> SkillInfo { get; set; } = [];
public string ToString(bool isShowGeneralDescription, bool isShowInStore = false)
{
StringBuilder builder = new();
builder.AppendLine($"【{Name}】");
string itemquality = ItemSet.GetQualityTypeName(QualityType);
string itemtype = GetRealEstateTypeName(RealEstateType);
if (itemtype != "") itemtype = $" {itemtype}";
builder.AppendLine($"{itemquality + itemtype}");
if (!string.IsNullOrWhiteSpace(Category)) builder.AppendLine(Category);
if (isShowInStore && Price > 0)
{
builder.AppendLine($"售价:{Price:0.##} {GameplayEquilibriumConstant.InGameCurrency}");
}
else if (Price > 0)
{
builder.AppendLine($"回收价:{Price:0.##} {GameplayEquilibriumConstant.InGameCurrency}");
}
if (OriginalPrice > 0) builder.AppendLine($"在商店或市场中出售时,售价不得超过原价:{OriginalPrice:0.##} {GameplayEquilibriumConstant.InGameCurrency}");
if (isShowInStore)
{
if (IsSellable)
{
builder.AppendLine($"购买此资产后可立即出售");
}
else if (NextSellableTime != DateTime.MinValue)
{
builder.AppendLine($"购买此资产后,将在 {NextSellableTime.ToString(General.GeneralDateTimeFormatChinese)} 后可出售");
}
}
else
{
if (IsSellable)
{
builder.AppendLine("可出售");
}
else if (!IsSellable && NextSellableTime != DateTime.MinValue)
{
builder.AppendLine($"此资产将在 {NextSellableTime.ToString(General.GeneralDateTimeFormatChinese)} 后可出售");
}
else if (!IsSellable)
{
builder.AppendLine("不可出售");
}
}
if (MaintenanceCost > 0)
{
builder.AppendLine($"维护费:{(MaintenanceUnit != "" ? MaintenanceUnit : "")} {MaintenanceCost:0.##} {GameplayEquilibriumConstant.InGameCurrency}");
}
if (isShowGeneralDescription && GeneralDescription != "")
{
builder.AppendLine("资产描述:" + GeneralDescription);
}
else if (Description != "")
{
builder.AppendLine("资产描述:" + Description);
}
if (SkillInfo.Count > 0)
{
builder.AppendLine("=== 资产技能 ===");
foreach (var skill in SkillInfo)
{
builder.AppendLine($"{skill.Key}{(!string.IsNullOrWhiteSpace(skill.Value) ? $"{skill.Value}" : "")}");
}
}
if (BackgroundStory != "")
{
builder.AppendLine($"\"{BackgroundStory}\"");
}
return builder.ToString();
}
public override bool Equals(IBaseEntity? other) => other is RealEstate re && GetIdName() == re.GetIdName();
public static string GetRealEstateTypeName(RealEstateType type)
{
return type switch
{
RealEstateType.General => "通用资产",
RealEstateType.Residential => "住宅资产",
RealEstateType.Commercial => "商业资产",
RealEstateType.Industrial => "工业资产",
RealEstateType.Warehouse => "仓储资产",
_ => "未知资产"
};
}
public virtual void UpdateSkillInfo()
{
}
}
public enum RealEstateType
{
General,
Residential,
Commercial,
Industrial,
Warehouse
}
}

View File

@ -0,0 +1,36 @@
using Milimoe.FunGame.Core.Entity;
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Entity
{
public class ResidentialRealEstate : RealEstate
{
public override RealEstateType RealEstateType => RealEstateType.Residential;
public virtual int MaxResidents { get; set; } = 0;
public virtual int ExperiencePerMinute { get; set; } = 0;
public virtual double RegenerationBonus { get; set; } = 0;
public virtual int ExtraInventorySlots { get; set; } = 0;
public virtual int FreeReviveCount { get; set; } = 0;
public virtual double CostPerResident { get; set; } = 0;
public int CurrentUsedFreeReviveCount { get; set; } = 0;
public override double MaintenanceCost => Characters.Count * CostPerResident;
public override string MaintenanceUnit => "每人每日";
public HashSet<Character> Characters { get; } = [];
public ResidentialRealEstate()
{
UpdateSkillInfo();
}
public override void UpdateSkillInfo()
{
if (MaxResidents > 0) SkillInfo["可入住角色数量"] = $"{Characters.Count} / {MaxResidents}";
if (ExperiencePerMinute > 0) SkillInfo["入住角色经验加成"] = $"{ExperiencePerMinute} / 人每分钟";
if (RegenerationBonus > 0) SkillInfo["生命/魔法回复速度提升"] = $"{RegenerationBonus * 100:0.##}%";
if (ExtraInventorySlots > 0) SkillInfo["额外玩家库存容量"] = $"{ExtraInventorySlots}";
if (FreeReviveCount > 0) SkillInfo["免费复活次数"] = $"{CurrentUsedFreeReviveCount} / {FreeReviveCount}";
if (Characters.Count > 0) SkillInfo["已入住角色"] = $"\r\n{string.Join("\r\n", Characters.Select(c => c.ToStringWithLevelWithOutUser()))}";
}
}
}

View File

@ -0,0 +1,19 @@
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Entity
{
public class WarehouseRealEstate : RealEstate
{
public override RealEstateType RealEstateType => RealEstateType.Warehouse;
public virtual int InventoryCapacity { get; set; } = 0;
public WarehouseRealEstate()
{
UpdateSkillInfo();
}
public override void UpdateSkillInfo()
{
if (InventoryCapacity > 0) SkillInfo["库存容量"] = $"{InventoryCapacity}";
}
}
}

View File

@ -0,0 +1,11 @@
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Interface
{
public interface IBusinessSimulationEntity
{
public string Category { get; }
public bool Enable { get; }
public Dictionary<string, string> SkillInfo { get; }
public void UpdateSkillInfo();
}
}

View File

@ -0,0 +1,9 @@
namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Interface
{
public interface IDescription
{
public string Description { get; }
public string GeneralDescription { get; }
public string BackgroundStory { get; }
}
}

View File

@ -84,6 +84,15 @@ namespace Oshima.FunGame.WebAPI.Model
[JsonPropertyName("stage")]
public string? Stage { get; set; }
[JsonPropertyName("team1")]
public string? Team1 { get; set; }
[JsonPropertyName("team2")]
public string? Team2 { get; set; }
[JsonPropertyName("betting_enabled")]
public bool? BettingEnabled { get; set; }
}
public class BettingEvent
@ -165,6 +174,9 @@ namespace Oshima.FunGame.WebAPI.Model
[JsonPropertyName("updated_at")]
public DateTime UpdatedAt { get; set; }
[JsonPropertyName("betting_enabled")]
public bool BettingEnabled { get; set; } = true;
}
public class BettingBetRecord

View File

@ -48,7 +48,7 @@ namespace Oshima.FunGame.WebAPI.Services
int id = Convert.ToInt32(row["id"]);
string name = row["name"].ToString() ?? "";
int status = Convert.ToInt32(row["status"]);
string statusStr = status switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", _ => "未知" };
string statusStr = status switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", 3 => "已取消", _ => "未知" };
DateTime startTime = Convert.ToDateTime(row["start_time"]);
DateTime endTime = Convert.ToDateTime(row["end_time"]);
sb.AppendLine($"🏆 [{id}] {name.CreateCmdInput($" {id}")} ({statusStr}{startTime:yyyy/MM/dd} ~ {endTime:yyyy/MM/dd})");
@ -73,7 +73,7 @@ namespace Oshima.FunGame.WebAPI.Services
int status = Convert.ToInt32(evt["status"]);
DateTime start = Convert.ToDateTime(evt["start_time"]);
DateTime end = Convert.ToDateTime(evt["end_time"]);
string statusStr = status switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", _ => "未知" };
string statusStr = status switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", 3 => "已取消", _ => "未知" };
StringBuilder header = new();
header.AppendLine($"赛事:{name}");
@ -115,7 +115,7 @@ namespace Oshima.FunGame.WebAPI.Services
DateTime deadline = Convert.ToDateTime(row["bet_deadline"]);
string stage = row["stage"].ToString() ?? "";
string result = row["result"] != DBNull.Value ? row["result"].ToString() ?? "" : "";
string mStatusStr = mstatus switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", _ => "未知" };
string mStatusStr = mstatus switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", 3 => "已取消", _ => "未知" };
string matchLabel = $"{t1} vs {t2}";
string clickableMatch = matchLabel.CreateCmdInput($"比赛详情 {mid}");
matches.AppendLine($" [{mid}] {(stage != "" ? $"{stage} " : "")} {clickableMatch} (状态:{mStatusStr}{(result.Trim() != "" ? $", {result}" : "")}, 截止:{deadline:MM-dd HH:mm})");
@ -179,7 +179,7 @@ namespace Oshima.FunGame.WebAPI.Services
long eventId = Convert.ToInt64(row["event_id"]);
string result = row["result"] != DBNull.Value ? row["result"].ToString() ?? "" : "";
string statusStr = status switch { 0 => "未开始", 1 => "进行中", 2 => $"已结束 | {result}", _ => "未知" };
string statusStr = status switch { 0 => "未开始", 1 => "进行中", 2 => $"已结束 | {result}", 3 => "已取消", _ => "未知" };
string matchLabel = $"{t1} vs {t2}".CreateCmdInput($"比赛详情 {id}");
sb.Append($"[{id}] {matchLabel}");
@ -215,6 +215,7 @@ namespace Oshima.FunGame.WebAPI.Services
long winner = row["winner"] != DBNull.Value ? Convert.ToInt64(row["winner"]) : 0;
decimal team1Odds = Convert.ToDecimal(row["team1_win_odds"]);
decimal team2Odds = Convert.ToDecimal(row["team2_win_odds"]);
bool bettingEnabled = Convert.ToBoolean(row["betting_enabled"]);
string eventName = "";
sql.Parameters["@eid"] = eventId;
@ -240,7 +241,7 @@ namespace Oshima.FunGame.WebAPI.Services
}
}
string statusStr = status switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", _ => "未知" };
string statusStr = status switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", 3 => "已取消", _ => "未知" };
StringBuilder sb = new();
sb.AppendLine($"比赛 #{matchId}");
if (eventName.Trim() != "")
@ -260,9 +261,10 @@ namespace Oshima.FunGame.WebAPI.Services
}
DateTime now = DateTime.Now;
bool canBet = (status == 0 || status == 1) && now < deadline;
bool canBet = (status == 0 || status == 1) && now < deadline && bettingEnabled;
if (canBet) sb.AppendLine($"可用选项:");
else sb.AppendLine($"该比赛已截止预测。");
else if (!bettingEnabled) sb.AppendLine($"🔒 该比赛不开放预测。");
else sb.AppendLine($"🔒 预测已锁定。");
string GetStatString(int opt)
{
@ -334,9 +336,15 @@ namespace Oshima.FunGame.WebAPI.Services
DateTime deadline = Convert.ToDateTime(row["bet_deadline"]);
decimal team1Odds = Convert.ToDecimal(row["team1_win_odds"]);
decimal team2Odds = Convert.ToDecimal(row["team2_win_odds"]);
bool bettingEnabled = Convert.ToBoolean(row["betting_enabled"]);
if (!bettingEnabled)
{
error = "该比赛不开放预测。";
return false;
}
if (status == 2 || DateTime.Now > deadline)
{
error = "当前比赛已结束或非可预测阶段。";
error = "该比赛处于不可预测阶段。";
return false;
}
@ -429,7 +437,9 @@ namespace Oshima.FunGame.WebAPI.Services
bool isMvp = available.Contains("mvp", StringComparison.CurrentCultureIgnoreCase);
int status = Convert.ToInt32(row["status"]);
if (status == 2)
return "比赛已结算。";
return "比赛已结算,无法再次结算。";
if (status == 3)
return "比赛已取消,无法结算。";
int winTeam = 3;
if (!isMvp)
@ -679,11 +689,11 @@ namespace Oshima.FunGame.WebAPI.Services
// 根据时间,设置比赛状态为未开始
sql.Parameters["@now"] = now;
sql.Execute("UPDATE csbetting_matches SET status = 0 WHERE start_time >= @now");
sql.Execute("UPDATE csbetting_matches SET status = 0 WHERE start_time >= @now AND status != 3");
// 更新比赛状态0→1 (进行中)1→2 (已结束) - 注意不要覆盖已结算的比赛winner 为 null 时视为未结束)
sql.Parameters["@now"] = now;
sql.Execute("UPDATE csbetting_matches SET status = 1 WHERE status = 0 AND start_time <= @now AND bet_deadline < @now AND winner IS NULL");
sql.Execute("UPDATE csbetting_matches SET status = 1 WHERE status = 0 AND start_time <= @now AND bet_deadline < @now AND winner IS NULL AND status != 3");
// 对于已经过了开始时间但还没有 winner 且状态为 1 的,可保留为进行中;实际上只要 winner 为 null状态应为 1进行中
// 如果有结果但 winner 不为 null管理员应该已经手动结算状态会设为 2这里不做额外修改。
// 安全起见,只更新未开始的,以及当比赛时间已过且无 winner 时自动变成进行中。
@ -843,12 +853,12 @@ namespace Oshima.FunGame.WebAPI.Services
}
/// <summary>
/// 管理员提前结束预测(将未开始比赛标记为进行中)
/// 管理员提前结束预测
/// </summary>
public static bool CloseBetting(int matchId, out string message) => UpdateMatch(new()
{
MatchId = matchId,
StartTime = DateTime.Now
BetDeadline = DateTime.Now
}, out message);
public static bool UpdateMatch(UpdateMatchRequest request, out string error)
@ -946,19 +956,34 @@ namespace Oshima.FunGame.WebAPI.Services
}
if (request.Description != null)
{
sql.Parameters["@desc"] = (object?)request.Description ?? "";
sql.Parameters["@desc"] = request.Description ?? "";
setClause.Append("description = @desc, ");
}
if (request.Result != null)
{
sql.Parameters["@result"] = (object?)request.Result ?? "";
sql.Parameters["@result"] = request.Result ?? "";
setClause.Append("result = @result, ");
}
if (request.Stage != null)
{
sql.Parameters["@stage"] = (object?)request.Stage ?? "";
sql.Parameters["@stage"] = request.Stage ?? "";
setClause.Append("stage = @stage, ");
}
if (request.Team1 != null)
{
sql.Parameters["@t1"] = request.Team1 ?? "";
setClause.Append("team1_name = @t1, ");
}
if (request.Team2 != null)
{
sql.Parameters["@t2"] = request.Team2 ?? "";
setClause.Append("team2_name = @t2, ");
}
if (request.BettingEnabled.HasValue)
{
sql.Parameters["@betting_enabled"] = request.BettingEnabled.Value ? 1 : 0;
setClause.Append("betting_enabled = @betting_enabled, ");
}
if (setClause.Length == 0)
{
@ -980,6 +1005,95 @@ namespace Oshima.FunGame.WebAPI.Services
return true;
}
/// <summary>
/// 取消比赛(管理员操作),退还所有未结算投注的本金,标记比赛状态为已取消
/// </summary>
/// <param name="matchId">比赛ID</param>
/// <param name="error">错误信息</param>
/// <returns>是否成功</returns>
public static bool CancelMatch(int matchId, out string error)
{
error = "";
using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper();
if (sql == null)
{
error = "数据库连接失败。";
return false;
}
try
{
sql.NewTransaction();
// 1. 获取比赛信息
sql.Parameters["@mid"] = matchId;
sql.ExecuteDataSet("SELECT id, status FROM csbetting_matches WHERE id = @mid");
if (!sql.Success || sql.DataSet.Tables[0].Rows.Count == 0)
{
error = "比赛不存在。";
sql.Rollback();
return false;
}
DataRow row = sql.DataSet.Tables[0].Rows[0];
int status = Convert.ToInt32(row["status"]);
if (status == 2)
{
error = "比赛已结束,无法取消。";
sql.Rollback();
return false;
}
if (status == 3)
{
error = "比赛已经是取消状态。";
sql.Rollback();
return false;
}
// 2. 获取该比赛所有未结算的投注记录
sql.Parameters["@mid"] = matchId;
sql.ExecuteDataSet("SELECT id, amount FROM csbetting_bet_records WHERE match_id = @mid AND is_settled = 0");
if (sql.Success && sql.DataSet.Tables[0].Rows.Count > 0)
{
foreach (DataRow bet in sql.DataSet.Tables[0].Rows)
{
long betId = Convert.ToInt64(bet["id"]);
long amount = Convert.ToInt64(bet["amount"]);
// 退还本金payout = amount标记为已结算注明取消但不自动领取
sql.Parameters["@bid"] = betId;
sql.Parameters["@payout"] = amount;
sql.Execute("UPDATE csbetting_bet_records SET is_settled = 1, payout = @payout, result_note = '比赛取消,退还本金' WHERE id = @bid");
if (!sql.Success)
{
sql.Rollback();
error = "退还投注本金失败。";
return false;
}
}
}
// 3. 更新比赛状态为 3已取消并禁止继续投注
sql.Parameters["@mid"] = matchId;
sql.Execute("UPDATE csbetting_matches SET status = 3, betting_enabled = 0 WHERE id = @mid");
if (!sql.Success)
{
sql.Rollback();
error = "更新比赛状态失败。";
return false;
}
sql.Commit();
error = $"比赛 {matchId} 已取消,所有未结算投注的本金已退还,请用户通过【预测领奖】领取。";
return true;
}
catch (Exception ex)
{
sql.Rollback();
error = $"取消比赛异常:{ex.Message}";
return false;
}
}
public static (decimal oddsA, decimal oddsB) CalculateOdds(decimal team1WinProbability, decimal margin = 0.08m)
{
if (team1WinProbability <= 0 || team1WinProbability >= 1)

View File

@ -79,3 +79,8 @@ ALTER TABLE `csbetting_matches`
-- 为投注记录表增加投注时的赔率字段,确保结算公平
ALTER TABLE `csbetting_bet_records`
ADD COLUMN `odds_at_bet` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '投注时的赔率' AFTER `amount`;
-- 增加比赛是否允许预测字段,以及状态值 3 表示已取消
ALTER TABLE `csbetting_matches`
MODIFY COLUMN `status` tinyint NOT NULL DEFAULT 0 COMMENT '比赛状态0=未开始1=进行中2=已结束3=已取消',
ADD COLUMN `betting_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否允许预测1=允许0=禁止' AFTER `description`;

View File

@ -239,7 +239,8 @@ namespace Oshima.FunGame.WebAPI.Controllers
Team2WinOdds = Convert.ToDecimal(row["team2_win_odds"]),
Description = row["description"]?.ToString(),
CreatedAt = Convert.ToDateTime(row["created_at"]),
UpdatedAt = Convert.ToDateTime(row["updated_at"])
UpdatedAt = Convert.ToDateTime(row["updated_at"]),
BettingEnabled = Convert.ToInt32(row["betting_enabled"]) == 1
};
}

View File

@ -338,5 +338,32 @@ namespace Oshima.FunGame.WebAPI.Controllers
return reply;
}
}
[HttpPost("cancel-match")]
public BotReply CancelMatch([FromQuery] long uid, [FromQuery] int matchId)
{
MarkdownMessage md = new() { Content = "服务器繁忙,请稍后再试。" };
BotReply reply = new() { Markdown = md };
try
{
if (!FunGameConstant.UserIdAndUsername.TryGetValue(uid, out User? user) || (!user.IsAdmin && !user.IsOperator))
{
md.Content = "你没有权限执行此操作。";
return reply;
}
if (CSBettingService.CancelMatch(matchId, out string msg))
md.Content = msg;
else
md.Content = msg;
return reply;
}
catch (Exception e)
{
Logger.LogError(e, "CancelMatch 异常");
return reply;
}
}
}
}

View File

@ -1,5 +1,4 @@
using System.Security.Cryptography;
using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Library.Constant;
using Oshima.FunGame.OshimaModules.Models;
using Oshima.FunGame.OshimaServers.Model;
@ -295,11 +294,7 @@ namespace Oshima.FunGame.WebAPI.Services
return true;
}
// 指令:创建比赛 <赛事ID> <队伍1> <队伍2> <阶段> <开始时间> <预测截止时间> [选项列表(逗号分隔)]
// 示例(创建默认形式的比赛):创建比赛 1 NAVI FaZe Quarter-final 2026-03-05 14:00 2026-03-05 13:55
// 示例(提供比赛助力选项):创建比赛 1 G2 Vitality Semi-final 2026-04-02 18:00 2026-04-02 17:55 team1_win,team2_win
// 示例(提供自定义奖励率):创建比赛 1 G2 Vitality Semi-final 2026-04-02 18:00 2026-04-02 17:55 team1_win=2.5 team2_win=2.5 team1_win,team2_win
// 示例提供队伍1的胜率自动计算奖励率创建比赛 1 Vitality FaZe Final 2026-05-12 20:00 2026-05-12 19:55 prob=0.6
// 指令:创建比赛 <赛事ID> <队伍1> <队伍2> <阶段> <开始时间> [选项列表(key=value格式空格分隔)]
if (e.Detail.StartsWith("创建比赛"))
{
e.UseNotice = false;
@ -311,13 +306,15 @@ namespace Oshima.FunGame.WebAPI.Services
string detail = e.Detail.Replace("创建比赛", "").Trim();
string[] parts = detail.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 7) // 最少需要 eventId, team1, team2, stage, start date, start time, deadline date, deadline time
if (parts.Length < 6) // 最少需要 eventId, team1, team2, stage, start date, start time
{
await SendAsync(e, "创建比赛",
"格式:创建比赛 <赛事ID> <队伍1> <队伍2> <阶段> <开始时间> <预测截止时间> [选项列表(逗号分隔)]\r\n" +
"时间格式yyyy-MM-dd HH:mm开始/截止各占两段)\r\n" +
"示例:创建比赛 1 NAVI FaZe Quarter-final 2026-03-05 14:00 2026-03-05 13:55\r\n" +
"选项默认 team1_win,team2_win 比分和MVP选项只允许独立添加score,mvp");
"格式:创建比赛 <赛事ID> <队伍1> <队伍2> <阶段> <开始时间> [选项列表(key=value格式空格分隔)]\r\n" +
"开始时间格式yyyy-MM-dd HH:mm两段\r\n" +
"可选参数ddl=截止时间需要用双引号包围opts=选项列表逗号分隔默认team1_win,team2_winteam1_win=奖励率team2_win=奖励率prob=胜率\r\n" +
"示例:创建比赛 1 NAVI FaZe Quarter-final 2026-03-05 14:00\r\n" +
"示例:创建比赛 1 NAVI FaZe Quarter-final 2026-03-05 14:00 ddl=\"2026-03-05 13:55\" opts=team1_win,team2_win\r\n" +
"示例:创建比赛 1 G2 Vitality Semi-final 2026-04-02 18:00 ddl=\"2026-04-02 17:55\" opts=team1_win,team2_win team1_win=2.5 prob=0.6");
return true;
}
@ -334,54 +331,90 @@ namespace Oshima.FunGame.WebAPI.Services
// 开始时间parts[4] + parts[5]
string startDate = parts[4];
string startTime = parts[5];
// 截止时间parts[6] + parts[7] 如果存在)
if (parts.Length < 8)
if (!DateTime.TryParseExact(startDate + " " + startTime, "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out DateTime startDt))
{
await SendAsync(e, "创建比赛", "预测截止时间需要完整日期和时间示例2026-03-05 13:55");
return true;
}
string deadlineDate = parts[6];
string deadlineTime = parts[7];
if (!DateTime.TryParseExact(startDate + " " + startTime, "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out DateTime startDt) ||
!DateTime.TryParseExact(deadlineDate + " " + deadlineTime, "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out DateTime deadlineDt))
{
await SendAsync(e, "创建比赛", "时间格式错误,请使用 yyyy-MM-dd HH:mm开始时间和截止时间各两段。");
await SendAsync(e, "创建比赛", "开始时间格式错误,请使用 yyyy-MM-dd HH:mm日期和时间分为两段。");
return true;
}
// 解析剩余参数:选项和奖励率
List<string> optionParts = [];
// 解析剩余参数从第6段开始key=value格式value支持双引号包围
int spaceCount = 0, idx = 0;
for (; idx < detail.Length && spaceCount < 6; idx++)
{
if (detail[idx] == ' ') spaceCount++;
}
string paramString = detail[idx..].Trim();
System.Text.RegularExpressions.MatchCollection matches = GetParamValue().Matches(paramString);
// 解析剩余可选参数key=value 格式支持双引号包围含空格的value
Dictionary<string, string> paramDict = new(StringComparer.OrdinalIgnoreCase);
foreach (System.Text.RegularExpressions.Match m in matches)
{
string key = m.Groups[1].Value;
string value = m.Groups[2].Success ? m.Groups[2].Value : m.Groups[3].Value;
if (!paramDict.TryAdd(key, value))
{
await SendAsync(e, "创建比赛", $"参数 '{key}' 重复。");
return true;
}
}
// 解析截止时间 (ddl=),默认与开始时间相同
DateTime deadlineDt = startDt;
if (paramDict.TryGetValue("ddl", out string? ddlStr))
{
if (!DateTime.TryParseExact(ddlStr, "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out deadlineDt))
{
await SendAsync(e, "创建比赛", "截止时间格式错误,请使用 yyyy-MM-dd HH:mm。");
return true;
}
}
// 解析选项列表 (opts=)
string options = "team1_win,team2_win";
if (paramDict.TryGetValue("opts", out string? optsStr))
{
options = optsStr; // 预期逗号分隔的列表
}
// 解析奖励率与胜率
decimal? team1Odds = null, team2Odds = null, team1WinProbability = null;
for (int i = 8; i < parts.Length; i++)
if (paramDict.TryGetValue("team1_win", out string? t1OddsStr))
{
string segment = parts[i];
if (segment.StartsWith("team1_win="))
if (decimal.TryParse(t1OddsStr, out decimal o1))
{
if (decimal.TryParse(segment[11..], out decimal odds1))
team1Odds = odds1;
}
else if (segment.StartsWith("team2_win="))
{
if (decimal.TryParse(segment[11..], out decimal odds2))
team2Odds = odds2;
}
else if (segment.StartsWith("prob="))
{
if (decimal.TryParse(segment[5..], out decimal prob))
team1WinProbability = prob;
else
{
await SendAsync(e, "创建比赛", "胜率值无效应为0~1之间的小数。");
return true;
}
team1Odds = o1;
}
else
{
optionParts.Add(segment);
await SendAsync(e, "创建比赛", "team1_win 奖励率无效。");
return true;
}
}
if (paramDict.TryGetValue("team2_win", out string? t2OddsStr))
{
if (decimal.TryParse(t2OddsStr, out decimal o2))
{
team2Odds = o2;
}
else
{
await SendAsync(e, "创建比赛", "team2_win 奖励率无效。");
return true;
}
}
if (paramDict.TryGetValue("prob", out string? probStr))
{
if (decimal.TryParse(probStr, out decimal prob) && prob > 0 && prob < 1)
{
team1WinProbability = prob;
}
else
{
await SendAsync(e, "创建比赛", "胜率值无效应为0~1之间的小数。");
return true;
}
}
string options = optionParts.Count > 0 ? string.Join(",", optionParts) : "team1_win,team2_win";
BotReply reply = BettingController.CreateMatch(new CreateMatchRequest
{
@ -497,6 +530,24 @@ namespace Oshima.FunGame.WebAPI.Services
case "result":
request.Result = val;
break;
case "stage":
request.Stage = val;
break;
case "t1":
request.Team1 = val;
break;
case "t2":
request.Team2 = val;
break;
case "be":
if (val == "0" || val == "1")
request.BettingEnabled = val == "1";
else
{
await SendAsync(e, "修改比赛", "enabled 值必须为 0 或 1。");
return true;
}
break;
default:
await SendAsync(e, "修改比赛", $"未知参数:{key}");
return true;
@ -508,11 +559,34 @@ namespace Oshima.FunGame.WebAPI.Services
reply.Keyboard = new KeyboardMessage()
.AppendButtons(2,
Button.CreateCmdButton("📋 赛事列表", "赛事列表"),
Button.CreateCmdButton("📅 比赛列表", "比赛列表"),
Button.CreateCmdButton("🔍 比赛详情", $"比赛详情 {matchId}"));
await SendAsync(e, "修改比赛", reply);
return true;
}
// 取消比赛指令
if (e.Detail.StartsWith("取消比赛"))
{
e.UseNotice = false;
string detail = e.Detail.Replace("取消比赛", "").Trim();
if (!int.TryParse(detail, out int matchId))
{
await SendAsync(e, "取消比赛", "格式:取消比赛 <比赛ID>");
return true;
}
BotReply reply = BettingController.CancelMatch(uid, matchId);
reply.Keyboard = new KeyboardMessage()
.AppendButtons(2,
Button.CreateCmdButton("📋 赛事列表", "赛事列表"),
Button.CreateCmdButton("📅 比赛列表", "比赛列表"),
Button.CreateCmdButton("🔍 比赛详情", $"比赛详情 {matchId}"),
Button.CreateCmdButton("💰 预测领奖", "预测领奖"));
await SendAsync(e, "取消比赛", reply);
return true;
}
return false;
}