From 67187200ca003d5af2a20434af28d2e2808754ac Mon Sep 17 00:00:00 2001 From: milimoe Date: Thu, 14 May 2026 20:19:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Entity/HumanResource/HumanResource.cs | 89 +++++++++ .../MovableProperty/DeviceMovableProperty.cs | 28 +++ .../Entity/MovableProperty/MovableProperty.cs | 136 ++++++++++++++ .../OfficeAssetMovableProperty.cs | 22 +++ .../MovableProperty/VehicleMovableProperty.cs | 101 +++++++++++ .../Entity/RealEstate/CommercialRealEstate.cs | 69 +++++++ .../Entity/RealEstate/IndustrialRealEstate.cs | 70 ++++++++ .../Entity/RealEstate/RealEstate.cs | 138 ++++++++++++++ .../RealEstate/ResidentialRealEstate.cs | 36 ++++ .../Entity/RealEstate/WarehouseRealEstate.cs | 19 ++ .../Interface/IBusinessSimulationEntity.cs | 11 ++ .../Interface/IDescription.cs | 9 + OshimaServers/Model/CSBettingModels.cs | 12 ++ OshimaServers/Service/CSBettingService.cs | 146 +++++++++++++-- OshimaWebAPI/Constant/CSBetting.sql | 5 + OshimaWebAPI/Controllers/BettingController.cs | 3 +- .../Controllers/CSBettingController.cs | 27 +++ .../Services/CSBettingInputHandler.cs | 170 +++++++++++++----- 18 files changed, 1026 insertions(+), 65 deletions(-) create mode 100644 OshimaModules/BusinessSimulation/Entity/HumanResource/HumanResource.cs create mode 100644 OshimaModules/BusinessSimulation/Entity/MovableProperty/DeviceMovableProperty.cs create mode 100644 OshimaModules/BusinessSimulation/Entity/MovableProperty/MovableProperty.cs create mode 100644 OshimaModules/BusinessSimulation/Entity/MovableProperty/OfficeAssetMovableProperty.cs create mode 100644 OshimaModules/BusinessSimulation/Entity/MovableProperty/VehicleMovableProperty.cs create mode 100644 OshimaModules/BusinessSimulation/Entity/RealEstate/CommercialRealEstate.cs create mode 100644 OshimaModules/BusinessSimulation/Entity/RealEstate/IndustrialRealEstate.cs create mode 100644 OshimaModules/BusinessSimulation/Entity/RealEstate/RealEstate.cs create mode 100644 OshimaModules/BusinessSimulation/Entity/RealEstate/ResidentialRealEstate.cs create mode 100644 OshimaModules/BusinessSimulation/Entity/RealEstate/WarehouseRealEstate.cs create mode 100644 OshimaModules/BusinessSimulation/Interface/IBusinessSimulationEntity.cs create mode 100644 OshimaModules/BusinessSimulation/Interface/IDescription.cs diff --git a/OshimaModules/BusinessSimulation/Entity/HumanResource/HumanResource.cs b/OshimaModules/BusinessSimulation/Entity/HumanResource/HumanResource.cs new file mode 100644 index 0000000..96f69b5 --- /dev/null +++ b/OshimaModules/BusinessSimulation/Entity/HumanResource/HumanResource.cs @@ -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 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 + } +} diff --git a/OshimaModules/BusinessSimulation/Entity/MovableProperty/DeviceMovableProperty.cs b/OshimaModules/BusinessSimulation/Entity/MovableProperty/DeviceMovableProperty.cs new file mode 100644 index 0000000..aba4d1e --- /dev/null +++ b/OshimaModules/BusinessSimulation/Entity/MovableProperty/DeviceMovableProperty.cs @@ -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 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)}"; + } + } +} diff --git a/OshimaModules/BusinessSimulation/Entity/MovableProperty/MovableProperty.cs b/OshimaModules/BusinessSimulation/Entity/MovableProperty/MovableProperty.cs new file mode 100644 index 0000000..3e52fcf --- /dev/null +++ b/OshimaModules/BusinessSimulation/Entity/MovableProperty/MovableProperty.cs @@ -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 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 + } +} diff --git a/OshimaModules/BusinessSimulation/Entity/MovableProperty/OfficeAssetMovableProperty.cs b/OshimaModules/BusinessSimulation/Entity/MovableProperty/OfficeAssetMovableProperty.cs new file mode 100644 index 0000000..927923f --- /dev/null +++ b/OshimaModules/BusinessSimulation/Entity/MovableProperty/OfficeAssetMovableProperty.cs @@ -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.##}%"; + } + } +} diff --git a/OshimaModules/BusinessSimulation/Entity/MovableProperty/VehicleMovableProperty.cs b/OshimaModules/BusinessSimulation/Entity/MovableProperty/VehicleMovableProperty.cs new file mode 100644 index 0000000..58a1808 --- /dev/null +++ b/OshimaModules/BusinessSimulation/Entity/MovableProperty/VehicleMovableProperty.cs @@ -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 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 TransportScheduleList { get; } = []; + public List 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; + } +} diff --git a/OshimaModules/BusinessSimulation/Entity/RealEstate/CommercialRealEstate.cs b/OshimaModules/BusinessSimulation/Entity/RealEstate/CommercialRealEstate.cs new file mode 100644 index 0000000..7ebf4da --- /dev/null +++ b/OshimaModules/BusinessSimulation/Entity/RealEstate/CommercialRealEstate.cs @@ -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} / 辆每小时"; + } + } +} diff --git a/OshimaModules/BusinessSimulation/Entity/RealEstate/IndustrialRealEstate.cs b/OshimaModules/BusinessSimulation/Entity/RealEstate/IndustrialRealEstate.cs new file mode 100644 index 0000000..95c9f1b --- /dev/null +++ b/OshimaModules/BusinessSimulation/Entity/RealEstate/IndustrialRealEstate.cs @@ -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 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(); + } + } +} diff --git a/OshimaModules/BusinessSimulation/Entity/RealEstate/RealEstate.cs b/OshimaModules/BusinessSimulation/Entity/RealEstate/RealEstate.cs new file mode 100644 index 0000000..943aad0 --- /dev/null +++ b/OshimaModules/BusinessSimulation/Entity/RealEstate/RealEstate.cs @@ -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 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 + } +} diff --git a/OshimaModules/BusinessSimulation/Entity/RealEstate/ResidentialRealEstate.cs b/OshimaModules/BusinessSimulation/Entity/RealEstate/ResidentialRealEstate.cs new file mode 100644 index 0000000..4612db6 --- /dev/null +++ b/OshimaModules/BusinessSimulation/Entity/RealEstate/ResidentialRealEstate.cs @@ -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 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()))}"; + } + } +} diff --git a/OshimaModules/BusinessSimulation/Entity/RealEstate/WarehouseRealEstate.cs b/OshimaModules/BusinessSimulation/Entity/RealEstate/WarehouseRealEstate.cs new file mode 100644 index 0000000..a045889 --- /dev/null +++ b/OshimaModules/BusinessSimulation/Entity/RealEstate/WarehouseRealEstate.cs @@ -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}"; + } + } +} diff --git a/OshimaModules/BusinessSimulation/Interface/IBusinessSimulationEntity.cs b/OshimaModules/BusinessSimulation/Interface/IBusinessSimulationEntity.cs new file mode 100644 index 0000000..d384dc0 --- /dev/null +++ b/OshimaModules/BusinessSimulation/Interface/IBusinessSimulationEntity.cs @@ -0,0 +1,11 @@ +namespace Oshima.FunGame.OshimaModules.BusinessSimulation.Interface +{ + public interface IBusinessSimulationEntity + { + public string Category { get; } + public bool Enable { get; } + public Dictionary SkillInfo { get; } + + public void UpdateSkillInfo(); + } +} diff --git a/OshimaModules/BusinessSimulation/Interface/IDescription.cs b/OshimaModules/BusinessSimulation/Interface/IDescription.cs new file mode 100644 index 0000000..2d308b9 --- /dev/null +++ b/OshimaModules/BusinessSimulation/Interface/IDescription.cs @@ -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; } + } +} diff --git a/OshimaServers/Model/CSBettingModels.cs b/OshimaServers/Model/CSBettingModels.cs index 43b0dfe..63e2a37 100644 --- a/OshimaServers/Model/CSBettingModels.cs +++ b/OshimaServers/Model/CSBettingModels.cs @@ -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 diff --git a/OshimaServers/Service/CSBettingService.cs b/OshimaServers/Service/CSBettingService.cs index 51e9237..809e98e 100644 --- a/OshimaServers/Service/CSBettingService.cs +++ b/OshimaServers/Service/CSBettingService.cs @@ -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 } /// - /// 管理员提前结束预测(将未开始比赛标记为进行中) + /// 管理员提前结束预测 /// 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; } + /// + /// 取消比赛(管理员操作),退还所有未结算投注的本金,标记比赛状态为已取消 + /// + /// 比赛ID + /// 错误信息 + /// 是否成功 + 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) diff --git a/OshimaWebAPI/Constant/CSBetting.sql b/OshimaWebAPI/Constant/CSBetting.sql index 2cd81f2..f5c9c6f 100644 --- a/OshimaWebAPI/Constant/CSBetting.sql +++ b/OshimaWebAPI/Constant/CSBetting.sql @@ -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`; diff --git a/OshimaWebAPI/Controllers/BettingController.cs b/OshimaWebAPI/Controllers/BettingController.cs index 74913fa..99de07f 100644 --- a/OshimaWebAPI/Controllers/BettingController.cs +++ b/OshimaWebAPI/Controllers/BettingController.cs @@ -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 }; } diff --git a/OshimaWebAPI/Controllers/CSBettingController.cs b/OshimaWebAPI/Controllers/CSBettingController.cs index 6af01c9..1c4b03c 100644 --- a/OshimaWebAPI/Controllers/CSBettingController.cs +++ b/OshimaWebAPI/Controllers/CSBettingController.cs @@ -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; + } + } } } diff --git a/OshimaWebAPI/Services/CSBettingInputHandler.cs b/OshimaWebAPI/Services/CSBettingInputHandler.cs index c6611fa..2d99215 100644 --- a/OshimaWebAPI/Services/CSBettingInputHandler.cs +++ b/OshimaWebAPI/Services/CSBettingInputHandler.cs @@ -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_win),team1_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 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 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; }