diff --git a/source/Behavior/Behavior.ts b/source/Behavior/Behavior.ts index addd747..40b72b5 100644 --- a/source/Behavior/Behavior.ts +++ b/source/Behavior/Behavior.ts @@ -1,14 +1,22 @@ import { BehaviorRecorder, IAnyBehaviorRecorder } from "@Model/Behavior"; import { Template } from "@Behavior/Template"; -import { Dynamics } from "@Behavior/Dynamics"; +import { PhysicsDynamics } from "@Behavior/PhysicsDynamics"; import { Brownian } from "@Behavior/Brownian"; import { BoundaryConstraint } from "@Behavior/BoundaryConstraint"; +import { Tracking } from "@Behavior/Tracking"; +import { ContactAttacking } from "@Behavior/ContactAttacking"; +import { ContactAssimilate } from "@Behavior/ContactAssimilate"; +import { DelayAssimilate } from "@Behavior/DelayAssimilate"; const AllBehaviors: IAnyBehaviorRecorder[] = [ new BehaviorRecorder(Template), - new BehaviorRecorder(Dynamics), + new BehaviorRecorder(PhysicsDynamics), new BehaviorRecorder(Brownian), new BehaviorRecorder(BoundaryConstraint), + new BehaviorRecorder(Tracking), + new BehaviorRecorder(ContactAttacking), + new BehaviorRecorder(ContactAssimilate), + new BehaviorRecorder(DelayAssimilate), ] /** @@ -54,4 +62,13 @@ function categoryBehaviors(behaviors: IAnyBehaviorRecorder[]): ICategory[] { return res; } -export { AllBehaviors, AllBehaviorsWithCategory, ICategory as ICategoryBehavior }; \ No newline at end of file +function getBehaviorById(id: string): IAnyBehaviorRecorder { + for (let i = 0; i < AllBehaviors.length; i++) { + if (AllBehaviors[i].behaviorId === id) { + return AllBehaviors[i]; + } + } + return getBehaviorById("Template"); +} + +export { AllBehaviors, AllBehaviorsWithCategory, getBehaviorById, ICategory as ICategoryBehavior }; \ No newline at end of file diff --git a/source/Behavior/BoundaryConstraint.ts b/source/Behavior/BoundaryConstraint.ts index befd8c5..d77b3d5 100644 --- a/source/Behavior/BoundaryConstraint.ts +++ b/source/Behavior/BoundaryConstraint.ts @@ -17,7 +17,7 @@ class BoundaryConstraint extends Behavior { let rangeList: Range[] = this.parameter.range.objects; let fx = 0; @@ -57,7 +57,6 @@ class BoundaryConstraint extends Behavior> = { diff --git a/source/Behavior/Brownian.ts b/source/Behavior/Brownian.ts index 62e04aa..5f6293a 100644 --- a/source/Behavior/Brownian.ts +++ b/source/Behavior/Brownian.ts @@ -18,7 +18,7 @@ class Brownian extends Behavior { const {maxFrequency, minFrequency, maxStrength, minStrength} = this.parameter; diff --git a/source/Behavior/ContactAssimilate.ts b/source/Behavior/ContactAssimilate.ts new file mode 100644 index 0000000..89da19a --- /dev/null +++ b/source/Behavior/ContactAssimilate.ts @@ -0,0 +1,88 @@ +import { Behavior } from "@Model/Behavior"; +import { Group } from "@Model/Group"; +import { Individual } from "@Model/Individual"; +import { Model } from "@Model/Model"; + +type IContactAssimilateBehaviorParameter = { + target: "CLG", + assimilate: "CG", + success: "number", + range: "number" +} + +type IContactAssimilateBehaviorEvent = {} + +class ContactAssimilate extends Behavior { + + public override behaviorId: string = "ContactAssimilate"; + + public override behaviorName: string = "$Title"; + + public override iconName: string = "SyncStatus"; + + public override describe: string = "$Intro"; + + public override category: string = "$Interactive"; + + public override parameterOption = { + target: { type: "CLG", name: "$Target" }, + assimilate: { type: "CG", name: "$Assimilate" }, + range: { type: "number", name: "$Range", defaultValue: .05, numberMin: 0, numberStep: .01 }, + success: { type: "number", name: "$Success", defaultValue: 90, numberMin: 0, numberMax: 100, numberStep: 5 } + }; + + public effect = (individual: Individual, group: Group, model: Model, t: number): void => { + + let assimilateGroup = this.parameter.assimilate.objects; + if (!assimilateGroup) return; + + for (let i = 0; i < this.parameter.target.objects.length; i++) { + const targetGroup = this.parameter.target.objects[i]; + + targetGroup.individuals.forEach((targetIndividual) => { + + // 排除自己 + if (targetIndividual === individual) return; + let dis = targetIndividual.distanceTo(individual); + + // 进入同化范围 + if (dis <= this.parameter.range) { + + // 成功判定 + if (Math.random() * 100 < this.parameter.success) { + targetIndividual.transfer(assimilateGroup!); + } + } + }); + } + } + + public override terms: Record> = { + "$Title": { + "ZH_CN": "接触同化", + "EN_US": "Contact Assimilate" + }, + "$Target": { + "ZH_CN": "从哪个群...", + "EN_US": "From group..." + }, + "$Assimilate": { + "ZH_CN": "到哪个群...", + "EN_US": "To group..." + }, + "$Range": { + "ZH_CN": "同化范围 (m)", + "EN_US": "Assimilate range (m)" + }, + "$Success": { + "ZH_CN": "成功率 (%)", + "EN_US": "Success rate (%)" + }, + "$Intro": { + "ZH_CN": "将进入同化范围内的个体同化至另一个群", + "EN_US": "Assimilate individuals who enter the assimilation range to another group" + } + }; +} + +export { ContactAssimilate }; \ No newline at end of file diff --git a/source/Behavior/ContactAttacking.ts b/source/Behavior/ContactAttacking.ts new file mode 100644 index 0000000..93e9e32 --- /dev/null +++ b/source/Behavior/ContactAttacking.ts @@ -0,0 +1,79 @@ +import { Behavior } from "@Model/Behavior"; +import { Group } from "@Model/Group"; +import { Individual } from "@Model/Individual"; +import { Model } from "@Model/Model"; + +type IContactAttackingBehaviorParameter = { + target: "CLG", + success: "number", + range: "number" +} + +type IContactAttackingBehaviorEvent = {} + +class ContactAttacking extends Behavior { + + public override behaviorId: string = "ContactAttacking"; + + public override behaviorName: string = "$Title"; + + public override iconName: string = "DefenderTVM"; + + public override describe: string = "$Intro"; + + public override category: string = "$Interactive"; + + public override parameterOption = { + target: { type: "CLG", name: "$Target" }, + range: { type: "number", name: "$Range", defaultValue: .05, numberMin: 0, numberStep: .01 }, + success: { type: "number", name: "$Success", defaultValue: 90, numberMin: 0, numberMax: 100, numberStep: 5 } + }; + + public effect = (individual: Individual, group: Group, model: Model, t: number): void => { + + for (let i = 0; i < this.parameter.target.objects.length; i++) { + const targetGroup = this.parameter.target.objects[i]; + + targetGroup.individuals.forEach((targetIndividual) => { + + // 排除自己 + if (targetIndividual === individual) return; + let dis = targetIndividual.distanceTo(individual); + + // 进入攻击范围 + if (dis <= this.parameter.range) { + + // 成功判定 + if (Math.random() * 100 < this.parameter.success) { + targetIndividual.die(); + } + } + }); + } + } + + public override terms: Record> = { + "$Title": { + "ZH_CN": "接触攻击", + "EN_US": "Contact Attacking" + }, + "$Target": { + "ZH_CN": "攻击目标", + "EN_US": "Attacking target" + }, + "$Range": { + "ZH_CN": "攻击范围 (m)", + "EN_US": "Attacking range (m)" + }, + "$Success": { + "ZH_CN": "成功率 (%)", + "EN_US": "Success rate (%)" + }, + "$Intro": { + "ZH_CN": "攻击进入共进范围的目标群个体", + "EN_US": "Attack the target group and individual entering the range" + } + }; +} + +export { ContactAttacking }; \ No newline at end of file diff --git a/source/Behavior/DelayAssimilate.ts b/source/Behavior/DelayAssimilate.ts new file mode 100644 index 0000000..2f43603 --- /dev/null +++ b/source/Behavior/DelayAssimilate.ts @@ -0,0 +1,96 @@ +import { Behavior } from "@Model/Behavior"; +import { Group } from "@Model/Group"; +import { Individual } from "@Model/Individual"; +import { Model } from "@Model/Model"; + +type IDelayAssimilateBehaviorParameter = { + target: "CG", + maxDelay: "number", + minDelay: "number", + success: "number" +} + +type IDelayAssimilateBehaviorEvent = {} + +class DelayAssimilate extends Behavior { + + public override behaviorId: string = "DelayAssimilate"; + + public override behaviorName: string = "$Title"; + + public override iconName: string = "FunctionalManagerDashboard"; + + public override describe: string = "$Intro"; + + public override category: string = "$Initiative"; + + public override parameterOption = { + target: { type: "CG", name: "$Target" }, + maxDelay: { type: "number", name: "$Max.Delay", defaultValue: 20, numberStep: 1, numberMin: 0 }, + minDelay: { type: "number", name: "$Min.Delay", defaultValue: 5, numberStep: 1, numberMin: 0 }, + success: { type: "number", name: "$Success", defaultValue: 90, numberStep: 5, numberMin: 0 } + }; + + public effect = (individual: Individual, group: Group, model: Model, t: number): void => { + + let assimilateGroup = this.parameter.target.objects; + if (!assimilateGroup) return; + + const {maxDelay, minDelay, success} = this.parameter; + + let nextTime = individual.getData("DelayAssimilate.nextTime") ?? + minDelay + Math.random() * (maxDelay - minDelay); + let currentTime = individual.getData("DelayAssimilate.currentTime") ?? 0; + + currentTime += t; + if (currentTime > nextTime) { + + // 成功判定 + if (Math.random() * 100 < success) { + individual.transfer(assimilateGroup); + nextTime = undefined; + currentTime = undefined; + } else { + + nextTime = minDelay + Math.random() * (maxDelay - minDelay); + currentTime = 0; + } + } + + individual.setData("DelayAssimilate.nextTime", nextTime); + individual.setData("DelayAssimilate.currentTime", currentTime); + } + + public override terms: Record> = { + "$Title": { + "ZH_CN": "延迟同化", + "EN_US": "Delayed assimilation" + }, + "$Intro": { + "ZH_CN": "随着时间的推移,个体逐渐向另一个群同化。", + "EN_US": "Over time, individuals gradually assimilate to another group." + }, + "$Target": { + "ZH_CN": "同化目标", + "EN_US": "Assimilation target" + }, + "$Max.Delay": { + "ZH_CN": "最长时间", + "EN_US": "Longest time" + }, + "$Min.Delay": { + "ZH_CN": "最短时间", + "EN_US": "Shortest time" + }, + "$Success": { + "ZH_CN": "成功率", + "EN_US": "Minimum strength" + }, + "$Initiative": { + "ZH_CN": "主动", + "EN_US": "Initiative" + } + }; +} + +export { DelayAssimilate }; \ No newline at end of file diff --git a/source/Behavior/Dynamics.ts b/source/Behavior/PhysicsDynamics.ts similarity index 53% rename from source/Behavior/Dynamics.ts rename to source/Behavior/PhysicsDynamics.ts index 34c122a..994b731 100644 --- a/source/Behavior/Dynamics.ts +++ b/source/Behavior/PhysicsDynamics.ts @@ -3,22 +3,23 @@ import Group from "@Model/Group"; import Individual from "@Model/Individual"; import { Model } from "@Model/Model"; -type IDynamicsBehaviorParameter = { +type IPhysicsDynamicsBehaviorParameter = { mass: "number", maxAcceleration: "number", maxVelocity: "number", - resistance: "number" + resistance: "number", + limit: "boolean" } -type IDynamicsBehaviorEvent = {} +type IPhysicsDynamicsBehaviorEvent = {} -class Dynamics extends Behavior { +class PhysicsDynamics extends Behavior { - public override behaviorId: string = "Dynamics"; + public override behaviorId: string = "PhysicsDynamics"; public override behaviorName: string = "$Title"; - public override iconName: string = "Running"; + public override iconName: string = "SliderHandleSize"; public override describe: string = "$Intro"; @@ -26,12 +27,19 @@ class Dynamics extends Behavior { // 计算当前速度 const currentV = individual.vectorLength(individual.velocity); @@ -54,24 +62,28 @@ class Dynamics extends Behavior this.parameter.maxAcceleration) { - individual.acceleration[0] = individual.acceleration[0] * this.parameter.maxAcceleration / lengthA; - individual.acceleration[1] = individual.acceleration[1] * this.parameter.maxAcceleration / lengthA; - individual.acceleration[2] = individual.acceleration[2] * this.parameter.maxAcceleration / lengthA; + if (this.parameter.limit) { + const lengthA = individual.vectorLength(individual.acceleration); + if (lengthA > this.parameter.maxAcceleration) { + individual.acceleration[0] = individual.acceleration[0] * this.parameter.maxAcceleration / lengthA; + individual.acceleration[1] = individual.acceleration[1] * this.parameter.maxAcceleration / lengthA; + individual.acceleration[2] = individual.acceleration[2] * this.parameter.maxAcceleration / lengthA; + } } - + // 计算速度 individual.velocity[0] = individual.velocity[0] + individual.acceleration[0] * t; individual.velocity[1] = individual.velocity[1] + individual.acceleration[1] * t; individual.velocity[2] = individual.velocity[2] + individual.acceleration[2] * t; // 速度约束 - const lengthV = individual.vectorLength(individual.velocity); - if (lengthV > this.parameter.maxVelocity) { - individual.velocity[0] = individual.velocity[0] * this.parameter.maxVelocity / lengthV; - individual.velocity[1] = individual.velocity[1] * this.parameter.maxVelocity / lengthV; - individual.velocity[2] = individual.velocity[2] * this.parameter.maxVelocity / lengthV; + if (this.parameter.limit) { + const lengthV = individual.vectorLength(individual.velocity); + if (lengthV > this.parameter.maxVelocity) { + individual.velocity[0] = individual.velocity[0] * this.parameter.maxVelocity / lengthV; + individual.velocity[1] = individual.velocity[1] * this.parameter.maxVelocity / lengthV; + individual.velocity[2] = individual.velocity[2] * this.parameter.maxVelocity / lengthV; + } } // 应用速度 @@ -87,13 +99,17 @@ class Dynamics extends Behavior> = { "$Title": { - "ZH_CN": "动力学", - "EN_US": "Dynamics" + "ZH_CN": "物理动力学", + "EN_US": "Physics dynamics" }, "$Intro": { - "ZH_CN": "一切可以运动物体的必要行为,执行物理法则。", - "EN_US": "All necessary behaviors that can move objects and implement the laws of physics." + "ZH_CN": "一切按照物理规则运动物体的行为, 按照牛顿经典物理运动公式执行。", + "EN_US": "The behavior of all moving objects according to physical rules is carried out according to Newton's classical physical motion formula." }, + "$Limit": { + "ZH_CN": "开启限制", + "EN_US": "Enable limit" + }, "$Mass": { "ZH_CN": "质量 (Kg)", "EN_US": "Mass (Kg)" @@ -117,4 +133,4 @@ class Dynamics extends Behavior { } diff --git a/source/Behavior/Tracking.ts b/source/Behavior/Tracking.ts new file mode 100644 index 0000000..3ea2ed8 --- /dev/null +++ b/source/Behavior/Tracking.ts @@ -0,0 +1,157 @@ +import { Behavior } from "@Model/Behavior"; +import { Group } from "@Model/Group"; +import { Individual } from "@Model/Individual"; +import { Model } from "@Model/Model"; + +type ITrackingBehaviorParameter = { + target: "CLG", + strength: "number", + range: "number", + lock: "boolean" +} + +type ITrackingBehaviorEvent = {} + +class Tracking extends Behavior { + + public override behaviorId: string = "Tracking"; + + public override behaviorName: string = "$Title"; + + public override iconName: string = "Bullseye"; + + public override describe: string = "$Intro"; + + public override category: string = "$Interactive"; + + public override parameterOption = { + target: { type: "CLG", name: "$Target" }, + lock: { type: "boolean", name: "$Lock", defaultValue: false }, + range: { type: "number", name: "$Range", defaultValue: 4, numberMin: 0, numberStep: .1 }, + strength: { type: "number", name: "$Strength", defaultValue: 1, numberMin: 0, numberStep: .1 } + }; + + private target: Individual | undefined = undefined; + private currentDistant: number = Infinity; + + private searchTarget(individual: Individual) { + + for (let i = 0; i < this.parameter.target.objects.length; i++) { + const targetGroup = this.parameter.target.objects[i]; + + targetGroup.individuals.forEach((targetIndividual) => { + + // 排除自己 + if (targetIndividual === individual) return; + let dis = targetIndividual.distanceTo(individual); + + if (dis < this.currentDistant && dis <= this.parameter.range) { + this.target = targetIndividual; + this.currentDistant = dis; + } + + }); + } + } + + private clearTarget() { + this.target = undefined as Individual | undefined; + this.currentDistant = Infinity; + } + + public effect = (individual: Individual, group: Group, model: Model, t: number): void => { + + this.clearTarget(); + + if (this.parameter.lock) { + + let isValidTarget = false; + this.target = individual.getData("Tracking.lock.target"); + + if (this.target) { + + // 校验目标所在的群是否仍是目标 + let isInTarget = false; + for (let i = 0; i < this.parameter.target.objects.length; i++) { + if (this.parameter.target.objects[i].equal(this.target.group)) { + isInTarget = true; + break; + } + } + + // 如果还在目标范围内,校验距离 + if (isInTarget) { + let dis = individual.distanceTo(this.target); + + // 校验成功 + if (dis <= this.parameter.range) { + this.currentDistant = dis; + isValidTarget = true; + } + } + } + + // 如果目标无效,尝试搜索新的目标 + if (!isValidTarget) { + + this.clearTarget(); + this.searchTarget(individual); + + // 如果成功搜索,缓存目标 + if (this.target && this.currentDistant && this.currentDistant !== Infinity) { + individual.setData("Tracking.lock.target", this.target); + } + + // 搜索失败,清除目标 + else { + individual.setData("Tracking.lock.target", undefined); + } + } + } + + else { + this.searchTarget(individual); + } + + if (this.target && this.currentDistant && this.currentDistant !== Infinity) { + individual.applyForce( + (this.target.position[0] - individual.position[0]) * this.parameter.strength / this.currentDistant, + (this.target.position[1] - individual.position[1]) * this.parameter.strength / this.currentDistant, + (this.target.position[2] - individual.position[2]) * this.parameter.strength / this.currentDistant + ); + } + } + + public override terms: Record> = { + "$Title": { + "ZH_CN": "追踪", + "EN_US": "Tracking" + }, + "$Target": { + "ZH_CN": "追踪目标", + "EN_US": "Tracking target" + }, + "$Lock": { + "ZH_CN": "追踪锁定", + "EN_US": "Tracking lock" + }, + "$Range": { + "ZH_CN": "追踪范围 (m)", + "EN_US": "Tracking range (m)" + }, + "$Strength": { + "ZH_CN": "追踪强度系数", + "EN_US": "Tracking intensity coefficient" + }, + "$Intro": { + "ZH_CN": "个体将主动向最近的目标群个体发起追踪", + "EN_US": "The individual will actively initiate tracking to the nearest target group individual" + }, + "$Interactive": { + "ZH_CN": "交互", + "EN_US": "Interactive" + } + }; +} + +export { Tracking }; \ No newline at end of file diff --git a/source/Component/BehaviorList/BehaviorList.scss b/source/Component/BehaviorList/BehaviorList.scss index 0de9edc..ddd617d 100644 --- a/source/Component/BehaviorList/BehaviorList.scss +++ b/source/Component/BehaviorList/BehaviorList.scss @@ -94,8 +94,8 @@ div.behavior-list { } div.behavior-content-view { - width: calc( 100% - 50px ); - padding-right: 5px; + width: calc( 100% - 55px ); + padding-right: 10px; max-width: 125px; height: $behavior-item-height; display: flex; diff --git a/source/Component/Container/Container.scss b/source/Component/Container/Container.scss index a2d4287..7ef06fd 100644 --- a/source/Component/Container/Container.scss +++ b/source/Component/Container/Container.scss @@ -119,6 +119,10 @@ div.app-container { flex-shrink: 1; } + div.app-panel.hide-scrollbar { + overflow: hidden; + } + div.app-panel.hide-scrollbar::-webkit-scrollbar { width : 0; /*高宽分别对应横竖滚动条的尺寸*/ height: 0; diff --git a/source/Input/ObjectPicker/ObjectPicker.tsx b/source/Input/ObjectPicker/ObjectPicker.tsx index 0fd0ff6..8bbc26f 100644 --- a/source/Input/ObjectPicker/ObjectPicker.tsx +++ b/source/Input/ObjectPicker/ObjectPicker.tsx @@ -59,6 +59,10 @@ class ObjectPicker extends Component extends Component & IMi private renderParameter (key: K, option: IParameterOptionItem, value: IParamValue): ReactNode { - const language = this.props.setting?.language ?? "EN_US"; const indexKey = `${this.props.key}-${key}`; + + // 条件检测 + if (option.condition && this.props.value[option.condition.key] !== option.condition.value) { + return ; + } + const type = option.type; + const language = this.props.setting?.language ?? "EN_US"; let keyI18n: string, keyI18nOption: Record | undefined; // Custom I18N diff --git a/source/Input/PickerList/PickerList.tsx b/source/Input/PickerList/PickerList.tsx index be63e88..2a278db 100644 --- a/source/Input/PickerList/PickerList.tsx +++ b/source/Input/PickerList/PickerList.tsx @@ -51,16 +51,25 @@ function getObjectDisplayInfo(item?: IPickerListItem): IDisplayInfo { if (item instanceof Label) { if (item.isBuildIn) { + internal = true; allLabel = true; color = "transparent"; + if (item.id === "AllRange") { icon = "ProductList"; name = "Build.In.Label.Name.All.Range"; - } else if (item.id === "AllGroup") { + } + + else if (item.id === "AllGroup") { icon = "SizeLegacy"; name = "Build.In.Label.Name.All.Group"; } + + else if (item.id === "CurrentGroupLabel") { + icon = "TriangleShape"; + name = "Build.In.Label.Name.Current.Group"; + } } else { diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index 4107788..a9edf6f 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -57,8 +57,11 @@ const EN_US = { "Popup.Action.No": "Cancel", "Popup.Action.Objects.Confirm.Title": "Confirm Delete", "Popup.Action.Objects.Confirm.Delete": "Delete", + "Popup.Action.Objects.Confirm.Restore.Title": "Confirm Restore", + "Popup.Action.Objects.Confirm.Restore": "Restore", "Popup.Delete.Objects.Confirm": "Are you sure you want to delete this object(s)? The object is deleted and cannot be recalled.", "Popup.Delete.Behavior.Confirm": "Are you sure you want to delete this behavior? The behavior is deleted and cannot be recalled.", + "Popup.Restore.Behavior.Confirm": "Are you sure you want to reset all parameters of this behavior? This operation cannot be recalled.", "Popup.Setting.Title": "Preferences setting", "Popup.Add.Behavior.Title": "Add behavior", "Popup.Add.Behavior.Action.Add": "Add all select behavior", @@ -68,6 +71,7 @@ const EN_US = { "Popup.Behavior.Info.Confirm": "OK, I know it", "Build.In.Label.Name.All.Group": "All group", "Build.In.Label.Name.All.Range": "All range", + "Build.In.Label.Name.Current.Group": "Current group", "Common.Search.Placeholder": "Search in here...", "Common.No.Data": "No Data", "Common.No.Unknown.Error": "Unknown error", @@ -107,6 +111,7 @@ const EN_US = { "Common.Attr.Key.Generation.Error.Invalid.Label": "The specified label has expired", "Common.Attr.Key.Kill.Random": "Random kill", "Common.Attr.Key.Kill.Count": "Kill count", + "Common.Attr.Key.Behavior.Restore": "Restore default parameters", "Common.Render.Attr.Key.Display.Shape": "Display Shape", "Common.Render.Attr.Key.Display.Shape.Square": "Square", "Common.Render.Attr.Key.Display.Shape.Hollow.Square": "Hollow square", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index 712f78b..11a40eb 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -57,17 +57,21 @@ const ZH_CN = { "Popup.Action.No": "取消", "Popup.Action.Objects.Confirm.Title": "删除确认", "Popup.Action.Objects.Confirm.Delete": "删除", + "Popup.Action.Objects.Confirm.Restore.Title": "重置确认", + "Popup.Action.Objects.Confirm.Restore": "重置", "Popup.Delete.Objects.Confirm": "你确定要删除这个(些)对象吗?对象被删除将无法撤回。", "Popup.Delete.Behavior.Confirm": "你确定要删除这个行为吗?行为被删除将无法撤回。", + "Popup.Restore.Behavior.Confirm": "你确定要重置此行为的全部参数吗?此操作无法撤回。", "Popup.Setting.Title": "首选项设置", "Popup.Add.Behavior.Title": "添加行为", "Popup.Add.Behavior.Action.Add": "添加全部选中行为", - "Popup.Add.Behavior.Select.Counter": "找不到名为 \"{name}\" 的行为", - "Popup.Add.Behavior.Select.Nodata": "Could not find behavior named \"{name}\"", + "Popup.Add.Behavior.Select.Counter": "已选择 {count} 个行为", + "Popup.Add.Behavior.Select.Nodata": "找不到名为 \"{name}\" 的行为", "Popup.Behavior.Info.Title": "行为详情: {behavior}", "Popup.Behavior.Info.Confirm": "好的, 我知道了", "Build.In.Label.Name.All.Group": "全部群", "Build.In.Label.Name.All.Range": "全部范围", + "Build.In.Label.Name.Current.Group": "当前群", "Common.Search.Placeholder": "在此处搜索...", "Common.No.Data": "暂无数据", "Common.No.Unknown.Error": "未知错误", @@ -107,6 +111,7 @@ const ZH_CN = { "Common.Attr.Key.Generation.Error.Invalid.Label": "指定的标签已失效", "Common.Attr.Key.Kill.Random": "随机消除", "Common.Attr.Key.Kill.Count": "消除数量", + "Common.Attr.Key.Behavior.Restore": "还原默认参数", "Common.Render.Attr.Key.Display.Shape": "显示形状", "Common.Render.Attr.Key.Display.Shape.Square": "方形", "Common.Render.Attr.Key.Display.Shape.Hollow.Square": "空心方形", diff --git a/source/Model/Behavior.ts b/source/Model/Behavior.ts index 1815f07..76563ee 100644 --- a/source/Model/Behavior.ts +++ b/source/Model/Behavior.ts @@ -158,6 +158,11 @@ class Behavior< */ public parameter: IParameterValue

; + /** + * 指定当前群的 Key + */ + public currentGroupKey: Array = []; + /** * 对象参数列表 */ @@ -222,7 +227,7 @@ class Behavior< * @param model 模型 * @param t 经过时间 */ - public effect(individual: Individual, group: Group, model: Model, t: number): void {}; + public effect?: (individual: Individual, group: Group, model: Model, t: number) => void; /** * 作用影响于个体 @@ -231,7 +236,7 @@ class Behavior< * @param model 模型 * @param t 经过时间 */ - public afterEffect(individual: Individual, group: Group, model: Model, t: number): void {}; + public afterEffect?: (individual: Individual, group: Group, model: Model, t: number) => void; /** * 全部影响作用后 @@ -240,7 +245,7 @@ class Behavior< * @param model 模型 * @param t 经过时间 */ - public finalEffect(individual: Individual, group: Group, model: Model, t: number): void {}; + public finalEffect?: (individual: Individual, group: Group, model: Model, t: number) => void; } diff --git a/source/Model/CtrlObject.ts b/source/Model/CtrlObject.ts index 3dca039..19a768f 100644 --- a/source/Model/CtrlObject.ts +++ b/source/Model/CtrlObject.ts @@ -61,8 +61,8 @@ class CtrlObject extends LabelObject { /** * 判断是否为相同对象 */ - public equal(obj: CtrlObject): boolean { - return this === obj || this.id === obj.id; + public equal(obj?: CtrlObject): boolean { + return this === obj || this.id === obj?.id; } /** diff --git a/source/Model/Group.ts b/source/Model/Group.ts index 79b0bd9..d38e9d5 100644 --- a/source/Model/Group.ts +++ b/source/Model/Group.ts @@ -1,6 +1,6 @@ import { Individual } from "@Model/Individual"; import { CtrlObject } from "@Model/CtrlObject"; -import type { Behavior } from "@Model/Behavior"; +import type { Behavior, IAnyBehavior } from "@Model/Behavior"; import { Label } from "@Model/Label"; import { Range } from "@Model/Range"; import { Model, ObjectID } from "@Model/Model"; @@ -39,7 +39,7 @@ class Group extends CtrlObject { /** * 生成个数 */ - public genCount: number = 1; + public genCount: number = 100; /** * 生成错误信息 @@ -54,7 +54,7 @@ class Group extends CtrlObject { /** * 删除个数 */ - public killCount: number = 1; + public killCount: number = 100; private genInSingleRange(count: number, range: Range) { for (let i = 0; i < count; i++) { @@ -272,9 +272,11 @@ class Group extends CtrlObject { public remove(individual: Individual[] | Individual): this { if (Array.isArray(individual)) { for (let i = 0; i < individual.length; i++) { + individual[i].group = undefined; this.individuals.delete(individual[i]); } } else { + individual.group = undefined; this.individuals.delete(individual); } return this; @@ -308,7 +310,7 @@ class Group extends CtrlObject { /** * 行为列表 */ - public behaviors: Behavior[] = []; + public behaviors: IAnyBehavior[] = []; /** * 添加行为 @@ -358,15 +360,37 @@ class Group extends CtrlObject { * @param */ public runner(t: number, effectType: "finalEffect" | "effect" | "afterEffect" ): void { - this.individuals.forEach((individual) => { - for(let j = 0; j < this.behaviors.length; j++) { - if (this.behaviors[j].isDeleted()) { - continue; + + for(let j = 0; j < this.behaviors.length; j++) { + + const behavior = this.behaviors[j]; + if (behavior.isDeleted()) { + continue; + } + + const runnerFunction = behavior[effectType]; + if (!runnerFunction) { + continue; + } + + for (let k = 0; k < behavior.currentGroupKey.length; k++) { + + let parameterCache = behavior.parameter[ + behavior.currentGroupKey[k] as string + ]; + + if (Array.isArray(parameterCache?.objects)) { + parameterCache.objects = [this]; + } else { - this.behaviors[j][effectType](individual, this, this.model, t); + parameterCache.objects = this; } - } - }); + } + + this.individuals.forEach((individual) => { + runnerFunction(individual, this, this.model, t); + }); + } } /** diff --git a/source/Model/Individual.ts b/source/Model/Individual.ts index 2685dfa..ba41bb9 100644 --- a/source/Model/Individual.ts +++ b/source/Model/Individual.ts @@ -88,7 +88,7 @@ class Individual { /** * 所属群组 */ - public group: Group; + public group: Group | undefined; /** * 初始化 @@ -97,11 +97,16 @@ class Individual { this.group = group; } + public isDie(): boolean { + return !!this.group; + } + /** * 死亡 */ public die(): this { - this.group.remove(this); + this.group?.remove(this); + this.group = undefined; return this; } @@ -110,7 +115,7 @@ class Individual { * @param newGroup 新群体 */ public transfer(newGroup: Group): this { - this.group.remove(this); + this.group?.remove(this); newGroup.add(this); this.group = newGroup; return this; diff --git a/source/Model/Model.ts b/source/Model/Model.ts index 2ea6f02..c73f707 100644 --- a/source/Model/Model.ts +++ b/source/Model/Model.ts @@ -66,6 +66,11 @@ class Model extends Emitter { */ public allGroupLabel = new Label(this, "AllGroup").setBuildInLabel(); + /** + * 内置标签-全部群标签 + */ + public currentGroupLabel = new Label(this, "CurrentGroupLabel").setBuildInLabel(); + /** * 添加标签 */ @@ -223,6 +228,7 @@ class Model extends Emitter { public updateBehaviorParameter() { for (let i = 0; i < this.behaviorPool.length; i++) { const behavior = this.behaviorPool[i]; + behavior.currentGroupKey = []; for (let key in behavior.parameterOption) { switch (behavior.parameterOption[key].type) { @@ -236,11 +242,17 @@ class Model extends Emitter { } break; + case "CG": case "G": - const dataG: IParamValue<"G"> = behavior.parameter[key]; + const dataG: IParamValue<"CG"> = behavior.parameter[key]; dataG.objects = undefined; + + if (dataG.picker instanceof Label && dataG.picker.id === this.currentGroupLabel.id) { + behavior.currentGroupKey.push(key); + dataG.objects = undefined; + } - if (dataG.picker instanceof Group && !dataG.picker.isDeleted()) { + else if (dataG.picker instanceof Group && !dataG.picker.isDeleted()) { dataG.objects = dataG.picker; } break; @@ -260,8 +272,9 @@ class Model extends Emitter { } break; + case "CLG": case "LG": - const dataLG: IParamValue<"LG"> = behavior.parameter[key]; + const dataLG: IParamValue<"CLG"> = behavior.parameter[key]; dataLG.objects = []; if (dataLG.picker instanceof Group && !dataLG.picker.isDeleted()) { @@ -269,9 +282,16 @@ class Model extends Emitter { } if (dataLG.picker instanceof Label && !dataLG.picker.isDeleted()) { - dataLG.objects = this.getObjectByLabel(dataLG.picker).filter((obj) => { - return obj instanceof Group; - }) as any; + + if (dataLG.picker.id === this.currentGroupLabel.id) { + behavior.currentGroupKey.push(key); + dataLG.objects = []; + + } else { + dataLG.objects = this.getObjectByLabel(dataLG.picker).filter((obj) => { + return obj instanceof Group; + }) as any; + } } break; } diff --git a/source/Model/Parameter.ts b/source/Model/Parameter.ts index 54bf988..a40eb8b 100644 --- a/source/Model/Parameter.ts +++ b/source/Model/Parameter.ts @@ -22,6 +22,8 @@ type IMapObjectParamTypeKeyToType = { "G": IObjectParamCacheType; "LR": IObjectParamCacheType

(option: IParameterOption

): IP defaultObj[key] = [0, 0, 0] as any; break; + case "CG": case "G": case "R": defaultObj[key] = { @@ -171,6 +179,7 @@ function getDefaultValue

(option: IParameterOption

): IP } as any; break; + case "CLG": case "LR": case "LG": defaultObj[key] = { diff --git a/source/Page/SimulatorWeb/SimulatorWeb.tsx b/source/Page/SimulatorWeb/SimulatorWeb.tsx index 2b53cfb..281cac4 100644 --- a/source/Page/SimulatorWeb/SimulatorWeb.tsx +++ b/source/Page/SimulatorWeb/SimulatorWeb.tsx @@ -6,11 +6,12 @@ import { ClassicRenderer } from "@GLRender/ClassicRenderer"; import { initializeIcons } from '@fluentui/font-icons-mdl2'; import { RootContainer } from "@Component/Container/RootContainer"; import { LayoutDirection } from "@Context/Layout"; -import { AllBehaviors } from "@Behavior/Behavior"; +import { AllBehaviors, getBehaviorById } from "@Behavior/Behavior"; import { CommandBar } from "@Component/CommandBar/CommandBar"; import { HeaderBar } from "@Component/HeaderBar/HeaderBar"; import { Popup } from "@Component/Popup/Popup"; import { Entry } from "../Entry/Entry"; +import { Group } from "@Model/Group"; import "./SimulatorWeb.scss"; initializeIcons("https://img.mrkbear.com/fabric-cdn-prod_20210407.001/"); @@ -40,18 +41,22 @@ class SimulatorWeb extends Component { this.status.bindRenderer(classicRender); this.status.setting = this.setting; - // 测试代码 - if (true) { - let group = this.status.newGroup(); - let range = this.status.newRange(); - range.color = [.1, .5, .9]; - group.new(100); - group.color = [.8, .1, .6]; + const randomPosition = (group: Group) => { group.individuals.forEach((individual) => { individual.position[0] = (Math.random() - .5) * 2; individual.position[1] = (Math.random() - .5) * 2; individual.position[2] = (Math.random() - .5) * 2; }) + }; + + // 测试代码 + if (false) { + let group = this.status.newGroup(); + let range = this.status.newRange(); + range.color = [.1, .5, .9]; + group.new(100); + group.color = [.8, .1, .6]; + randomPosition(group); this.status.model.update(0); this.status.newLabel().name = "New Label"; this.status.newLabel().name = "Test Label 01"; @@ -70,6 +75,84 @@ class SimulatorWeb extends Component { group.addBehavior(boundary); } + // 鱼群模型测试 + if (true) { + let fish1 = this.status.newGroup(); + let fish2 = this.status.newGroup(); + let shark = this.status.newGroup(); + let range = this.status.newRange(); + + range.displayName = "Experimental site"; + range.color = [.8, .1, .6]; + + fish1.new(100); + fish1.displayName = "Fish A"; + fish1.color = [.1, .5, .9]; + randomPosition(fish1); + + fish2.new(50); + fish2.displayName = "Fish B"; + fish2.color = [.3, .2, .9]; + randomPosition(fish2); + + shark.new(3); + shark.displayName = "Shark"; + shark.color = [.8, .2, .3]; + shark.renderParameter.size = 100; + shark.renderParameter.shape = "5"; + randomPosition(shark); + + this.status.model.update(0); + let fishLabel = this.status.newLabel(); + fishLabel.name = "Fish"; + fish1.addLabel(fishLabel); + fish2.addLabel(fishLabel); + + let template = this.status.model.addBehavior(getBehaviorById("Template")); + template.name = "Template"; template.color = [150, 20, 220]; + + let dynamicFish = this.status.model.addBehavior(getBehaviorById("PhysicsDynamics")); + dynamicFish.name = "Dynamic Fish"; dynamicFish.color = [250, 200, 80]; + + let dynamicShark = this.status.model.addBehavior(getBehaviorById("PhysicsDynamics")); + dynamicShark.name = "Dynamic Shark"; dynamicShark.color = [250, 200, 80]; + dynamicShark.parameter.maxAcceleration = 8.5; + dynamicShark.parameter.maxVelocity = 15.8; + dynamicShark.parameter.resistance = 3.6; + + let brownian = this.status.model.addBehavior(getBehaviorById("Brownian")); + brownian.name = "Brownian"; brownian.color = [200, 80, 250]; + + let boundary = this.status.model.addBehavior(getBehaviorById("BoundaryConstraint")); + boundary.name = "Boundary"; boundary.color = [80, 200, 250]; + boundary.parameter.range.picker = this.status.model.allRangeLabel; + + let tracking = this.status.model.addBehavior(getBehaviorById("Tracking")); + tracking.name = "Tracking"; tracking.color = [80, 200, 250]; + tracking.parameter.target.picker = fishLabel; + + let attacking = this.status.model.addBehavior(getBehaviorById("ContactAttacking")); + attacking.name = "Contact Attacking"; attacking.color = [120, 100, 250]; + attacking.parameter.target.picker = fishLabel; + + fish1.addBehavior(dynamicFish); + fish1.addBehavior(brownian); + fish1.addBehavior(boundary); + + fish2.addBehavior(dynamicFish); + fish2.addBehavior(brownian); + fish2.addBehavior(boundary); + + shark.addBehavior(dynamicShark); + shark.addBehavior(boundary); + shark.addBehavior(tracking); + shark.addBehavior(attacking); + + setTimeout(() => { + this.status.model.updateBehaviorParameter(); + }, 200) + } + (window as any).s = this; } diff --git a/source/Panel/BehaviorDetails/BehaviorDetails.tsx b/source/Panel/BehaviorDetails/BehaviorDetails.tsx index e3490e9..c9f69d2 100644 --- a/source/Panel/BehaviorDetails/BehaviorDetails.tsx +++ b/source/Panel/BehaviorDetails/BehaviorDetails.tsx @@ -1,6 +1,7 @@ import { Component, ReactNode} from "react"; import { useSettingWithEvent, IMixinSettingProps } from "@Context/Setting"; import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; +import { getDefaultValue } from "@Model/Parameter"; import { IAnyBehavior } from "@Model/Behavior"; import { Message } from "@Input/Message/Message"; import { AttrInput } from "@Input/AttrInput/AttrInput"; @@ -12,9 +13,17 @@ import "./BehaviorDetails.scss"; interface IBehaviorDetailsProps {} +interface IBehaviorDetailsState { + updateId: number; +} + @useSettingWithEvent("language") @useStatusWithEvent("focusBehaviorChange", "behaviorAttrChange") -class BehaviorDetails extends Component { +class BehaviorDetails extends Component { + + public state: Readonly = { + updateId: 1 + }; private handelDeleteBehavior = (behavior: IAnyBehavior) => { if (this.props.status) { @@ -32,6 +41,28 @@ class BehaviorDetails extends Component { + if (this.props.status) { + const status = this.props.status; + status.popup.showPopup(ConfirmPopup, { + infoI18n: "Popup.Restore.Behavior.Confirm", + titleI18N: "Popup.Action.Objects.Confirm.Restore.Title", + yesI18n: "Popup.Action.Objects.Confirm.Restore", + red: "yes", + yes: () => { + status.changeBehaviorAttrib( + behavior.id, "parameter", + getDefaultValue(behavior.parameterOption) as any, + true + ); + this.setState({ + updateId: this.state.updateId + 1 + }); + } + }) + } + } + private renderFrom(behavior: IAnyBehavior): ReactNode { return <> @@ -60,9 +91,17 @@ class BehaviorDetails extends Component + + { + this.handelRestoreBehavior(behavior) + }} + /> behavior.getTerms(name, language)}