Compare commits
12
Commits
53b6b261dd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d785d5139 | ||
|
|
eec09a0bb4 | ||
|
|
1422db3b19 | ||
|
|
a38a8d4b39 | ||
|
|
55506eff9a | ||
|
|
2e54acbc85 | ||
|
|
dc2aa59d58 | ||
|
|
c76fd85a2c | ||
|
|
777c58b812 | ||
|
|
aa44170d56 | ||
|
|
0c92fd9886 | ||
|
|
26aaa945a9 |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(unzip -l \"Assets/GameFramework/Libraries/GameFramework.xml\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
# Unity generated folders
|
||||
Library/
|
||||
Temp/
|
||||
Obj/
|
||||
Logs/
|
||||
Build/
|
||||
Builds/
|
||||
UserSettings/
|
||||
MemoryCaptures/
|
||||
Recordings/
|
||||
|
||||
# IDE / tool caches
|
||||
.vs/
|
||||
.idea/
|
||||
.gradle/
|
||||
.cache/
|
||||
|
||||
# OS / misc
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Node / frontend caches if present
|
||||
node_modules/
|
||||
|
||||
# Test / coverage outputs
|
||||
coverage/
|
||||
TestResults/
|
||||
|
||||
# Large binary assets that usually don't help code editing
|
||||
*.apk
|
||||
*.aab
|
||||
*.unitypackage
|
||||
*.zip
|
||||
*.7z
|
||||
*.rar
|
||||
|
||||
# Optional: generated csproj/sln if you mainly use Unity-generated ones
|
||||
# *.csproj
|
||||
# *.sln
|
||||
@@ -98,10 +98,14 @@ InitTestScene*.unity*
|
||||
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
|
||||
|
||||
/[Aa]ssets/StreamingAssets/
|
||||
!/[Aa]ssets/StreamingAssets/.gitkeep
|
||||
|
||||
/.vscode
|
||||
/.dotnet-home
|
||||
|
||||
/[Aa]ssets/RawResources
|
||||
/[Aa]ssets/RawResources.meta
|
||||
|
||||
/.dotnet
|
||||
/.idea
|
||||
~$*.xlsx
|
||||
@@ -109,3 +113,5 @@ InitTestScene*.unity*
|
||||
/node_modules
|
||||
package.json
|
||||
package-lock.json
|
||||
/.omc
|
||||
/.omx
|
||||
Generated
-15
@@ -1,15 +0,0 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider 忽略的文件
|
||||
/modules.xml
|
||||
/.idea.GeometryTD.iml
|
||||
/projectSettingsUpdater.xml
|
||||
/contentModel.xml
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,355 +0,0 @@
|
||||
# Project Philosophy
|
||||
|
||||
This project prioritizes **clarity, correctness, and maintainability** over defensive boilerplate or unnecessary abstraction.
|
||||
|
||||
AI-generated code should:
|
||||
|
||||
* follow existing architecture
|
||||
* keep logic simple and explicit
|
||||
* avoid hiding bugs
|
||||
* avoid speculative abstractions
|
||||
|
||||
If uncertain, prefer **simple direct code** over generalized frameworks.
|
||||
|
||||
## Repository Guidelines
|
||||
|
||||
### Project Structure & Module Organization
|
||||
- `Assets/` contains all Unity assets and code. Game-specific content lives under `Assets/GameMain/`.
|
||||
- `Assets/GameMain/Scripts/` holds gameplay code organized by domain (`Procedure/`, `Entity/`, `UI/`, `Scene/`, `Sound/`, `Utility/`).
|
||||
- `Assets/GameMain/Scenes/` stores Unity scenes (start from `Assets/Launcher.unity`).
|
||||
- `Assets/GameMain/Configs/` and `Assets/GameMain/DataTables/` store runtime configuration and data tables.
|
||||
- `StreamingAssets/` is for runtime-loaded files that must be preserved on build.
|
||||
- `docs/` contains design notes (see `docs/GameDesign.md`).
|
||||
- `数据表/` is a top-level data table workspace; keep it in sync with `Assets/GameMain/DataTables/` when exporting.
|
||||
|
||||
### Build, Test, and Development Commands
|
||||
- Open the project with Unity Hub and load `GeometryTD` as a Unity project.
|
||||
- Play locally via the Unity Editor Play button (no custom CLI runner is defined in this repo).
|
||||
- Build via Unity: `File > Build Settings...` then choose target and build.
|
||||
- IDE support: open `GeometryTD.sln` or `Assembly-CSharp.csproj` for C# navigation and tooling.
|
||||
|
||||
### Coding Style & Naming Conventions
|
||||
- C# uses 4-space indentation and Allman braces (see `Assets/GameMain/Scripts/Procedure/ProcedureLaunch.cs`).
|
||||
- Types, methods, and public members use `PascalCase`; locals and parameters use `camelCase`.
|
||||
- Namespaces follow `GeometryTD.*` by feature area (example: `GeometryTD.Procedure`).
|
||||
- Keep Unity `.meta` files with their assets; do not delete or regenerate them manually.
|
||||
|
||||
---
|
||||
|
||||
## Code Design Principles
|
||||
|
||||
### 1. Prefer explicit logic over abstraction
|
||||
|
||||
Do not introduce abstractions unless they are clearly justified.
|
||||
|
||||
Avoid creating layers such as:
|
||||
|
||||
```
|
||||
Manager
|
||||
Service
|
||||
Provider
|
||||
Repository
|
||||
Coordinator
|
||||
Bridge
|
||||
```
|
||||
|
||||
unless the architecture explicitly requires them.
|
||||
|
||||
Simple gameplay logic should remain simple.
|
||||
|
||||
Bad example:
|
||||
|
||||
```
|
||||
EnemyService -> EnemyManager -> EnemyRepository
|
||||
```
|
||||
|
||||
Good example:
|
||||
|
||||
```
|
||||
EnemySpawner
|
||||
Enemy
|
||||
EnemyAI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Do not create abstractions for a single implementation
|
||||
|
||||
Avoid introducing interfaces unless multiple implementations are expected.
|
||||
|
||||
Bad:
|
||||
|
||||
```
|
||||
IEnemyService
|
||||
EnemyService
|
||||
```
|
||||
|
||||
Good:
|
||||
|
||||
```
|
||||
EnemySystem
|
||||
```
|
||||
|
||||
Interfaces are only appropriate when:
|
||||
|
||||
* multiple implementations are required
|
||||
* plugin/mod systems are involved
|
||||
* testing requires mocking
|
||||
* architecture explicitly defines extension points
|
||||
|
||||
---
|
||||
|
||||
### 3. Avoid speculative generalization
|
||||
|
||||
Do not design systems for hypothetical future use.
|
||||
|
||||
Implement only what the current feature requires.
|
||||
|
||||
Avoid:
|
||||
|
||||
* generic service layers
|
||||
* premature plugin systems
|
||||
* unnecessary configuration frameworks
|
||||
|
||||
---
|
||||
|
||||
## Defensive Programming Policy
|
||||
|
||||
### Boundary validation only
|
||||
|
||||
Validation is appropriate only at **true system boundaries**, such as:
|
||||
|
||||
* user input
|
||||
* network input
|
||||
* file/config loading
|
||||
* inspector/external data
|
||||
* public API boundaries
|
||||
|
||||
Internal gameplay logic should assume that invariants are already satisfied.
|
||||
|
||||
Do not add redundant validation inside internal code.
|
||||
|
||||
---
|
||||
|
||||
### Do not hide errors
|
||||
|
||||
Do not silently ignore invalid states.
|
||||
|
||||
Avoid code such as:
|
||||
|
||||
```
|
||||
if (enemy == null)
|
||||
return;
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
if (config == null)
|
||||
continue;
|
||||
```
|
||||
|
||||
unless the behavior is intentionally designed.
|
||||
|
||||
Unexpected states should be **visible**, not hidden.
|
||||
|
||||
---
|
||||
|
||||
### Prefer fail-fast over silent failure
|
||||
|
||||
If a condition should never happen in correct execution:
|
||||
|
||||
Use assertions.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
Debug.Assert(config != null);
|
||||
```
|
||||
|
||||
Do not convert invariant violations into no-op behavior.
|
||||
|
||||
Bad:
|
||||
|
||||
```
|
||||
if (config == null)
|
||||
return;
|
||||
```
|
||||
|
||||
Good:
|
||||
|
||||
```
|
||||
Debug.Assert(config != null);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Do not weaken required dependencies
|
||||
|
||||
Required references must remain required.
|
||||
|
||||
Do not turn required dependencies into optional ones by adding defensive checks.
|
||||
|
||||
Bad:
|
||||
|
||||
```
|
||||
if (enemy.Config == null)
|
||||
return;
|
||||
```
|
||||
|
||||
If a dependency is required by design, assume it exists.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
Do not introduce `try/catch` blocks unless there is a **clear recovery strategy**.
|
||||
|
||||
Avoid:
|
||||
|
||||
```
|
||||
try
|
||||
{
|
||||
DoSomething();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError(e);
|
||||
}
|
||||
```
|
||||
|
||||
Exceptions should generally propagate during development so bugs are visible.
|
||||
|
||||
---
|
||||
|
||||
## Early Return Policy
|
||||
|
||||
Early returns are acceptable for **normal control flow simplification**.
|
||||
|
||||
However, do not use early returns to hide invariant violations or missing required state.
|
||||
|
||||
Bad:
|
||||
|
||||
```
|
||||
if (enemy == null)
|
||||
return;
|
||||
```
|
||||
|
||||
Good:
|
||||
|
||||
```
|
||||
if (!enemy.IsAlive)
|
||||
return;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
Avoid excessive logging.
|
||||
|
||||
Do not log inside per-frame loops unless necessary.
|
||||
|
||||
Avoid logs like:
|
||||
|
||||
```
|
||||
Debug.Log("Updating enemy");
|
||||
```
|
||||
|
||||
Use logging only when it provides meaningful debugging value.
|
||||
|
||||
---
|
||||
|
||||
## Utility Classes
|
||||
|
||||
Avoid creating generic utility classes such as:
|
||||
|
||||
```
|
||||
GameUtils
|
||||
CommonHelper
|
||||
MathHelper
|
||||
```
|
||||
|
||||
Prefer placing behavior in the domain object responsible for it.
|
||||
|
||||
Example:
|
||||
|
||||
Bad:
|
||||
|
||||
```
|
||||
DamageHelper.CalculateDamage(...)
|
||||
```
|
||||
|
||||
Good:
|
||||
|
||||
```
|
||||
enemy.CalculateDamage(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Structures
|
||||
|
||||
Avoid large context objects with many loosely related fields.
|
||||
|
||||
Bad:
|
||||
|
||||
```
|
||||
EffectContext
|
||||
{
|
||||
int value;
|
||||
int count;
|
||||
float rate;
|
||||
object source;
|
||||
object target;
|
||||
}
|
||||
```
|
||||
|
||||
Prefer explicit parameters or strongly typed structures.
|
||||
|
||||
---
|
||||
|
||||
## Code Generation Rules for AI
|
||||
|
||||
Before adding any of the following:
|
||||
|
||||
* null check
|
||||
* range check
|
||||
* fallback default
|
||||
* try/catch
|
||||
* abstraction layer
|
||||
|
||||
First determine:
|
||||
|
||||
1. Is this a boundary input?
|
||||
2. Is the value optional by design?
|
||||
3. Is there a real recovery strategy?
|
||||
|
||||
If the answer is **no**, do not add defensive code.
|
||||
|
||||
---
|
||||
|
||||
## Preferred Coding Style
|
||||
|
||||
Prefer:
|
||||
|
||||
* simple classes
|
||||
* explicit logic
|
||||
* minimal indirection
|
||||
* clear ownership of behavior
|
||||
|
||||
Avoid:
|
||||
|
||||
* unnecessary patterns
|
||||
* speculative architecture
|
||||
* defensive boilerplate
|
||||
|
||||
---
|
||||
|
||||
## Guiding Principle
|
||||
|
||||
Prefer:
|
||||
|
||||
**clear failure over hidden corruption**
|
||||
|
||||
A bug should surface near its source rather than being silently ignored.
|
||||
@@ -4,3 +4,7 @@
|
||||
1 元素底座 [2,2.1,2.2,2.3,2.5] 0.2 Fire [Fire,BurnSpread,IgniteBurst,Inferno]
|
||||
2 控制底座 [4,4.2,4.4,4.6,4.8] 0.1 Ice [Ice,FreezeMask,Shatter,AbsoluteZero]
|
||||
3 穿透底座 [1,1,1,1,1] 0 Physics [Pierce,Crit,Overpenetrate,Execution]
|
||||
4 12底座 [1,1,1,1,1] 0.1 Poison [Fire,BurnSpread,IgniteBurst,Inferno,Ice,FreezeMask,Shatter,AbsoluteZero]
|
||||
5 23底座 [1,1,1,1,1] 0.1 Water [Ice,FreezeMask,Shatter,AbsoluteZero,Pierce,Crit,Overpenetrate,Execution]
|
||||
6 13底座 [1,1,1,1,1] 0.1 Earth [Fire,BurnSpread,IgniteBurst,Inferno,Pierce,Crit,Overpenetrate,Execution]
|
||||
7 123底座 [1,1,1,1,1] 0.1 Fire [Fire,BurnSpread,IgniteBurst,Inferno,Ice,FreezeMask,Shatter,AbsoluteZero,Pierce,Crit,Overpenetrate,Execution]
|
||||
|
||||
@@ -4,3 +4,7 @@
|
||||
1 元素轴承 [10,12,13,14,15] 15 [2,2,2,2,2] 0.3 [Fire,BurnSpread,IgniteBurst,Inferno]
|
||||
2 控制轴承 [20,25,30,32,35] 0 [6,6.5,7,8,8] 0.2 [Ice,FreezeMask,Shatter,AbsoluteZero]
|
||||
3 穿透轴承 [60,70,80,90,100] 20 [4,4.5,5,5.5,6] 0 [Pierce,Crit,Overpenetrate,Execution]
|
||||
4 12轴承 [60,70,80,90,100] 20 [4,4.5,5,5.5,6] 0 [Fire,BurnSpread,IgniteBurst,Inferno,Ice,FreezeMask,Shatter,AbsoluteZero]
|
||||
5 23轴承 [60,70,80,90,100] 20 [4,4.5,5,5.5,6] 0 [Ice,FreezeMask,Shatter,AbsoluteZero,Pierce,Crit,Overpenetrate,Execution]
|
||||
6 13轴承 [60,70,80,90,100] 20 [4,4.5,5,5.5,6] 0 [Fire,BurnSpread,IgniteBurst,Inferno,Pierce,Crit,Overpenetrate,Execution]
|
||||
7 123轴承 [60,70,80,90,100] 20 [4,4.5,5,5.5,6] 0 [Fire,BurnSpread,IgniteBurst,Inferno,Ice,FreezeMask,Shatter,AbsoluteZero,Pierce,Crit,Overpenetrate,Execution]
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
# Id 策划备注 AssetName
|
||||
# int string
|
||||
# 实体编号 资源名称
|
||||
101 测试枪口 TestMuzzle
|
||||
201 测试轴承 TestBearing
|
||||
301 测试底座 TestBase
|
||||
401 防御塔实体 TowerEntity
|
||||
501 普通子弹 NormalBullet
|
||||
1001 测试普通敌人 TestEnemy
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Id 列1 Title Description Options
|
||||
# int string string string
|
||||
# 事件编号 策划备注 事件题目 事件描述 事件选项
|
||||
1 赌马 一名商人邀请你下注。赢了就能赚一笔。 [{"optionText":"下注 100(金)- 稳健:70% 赢 150","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":150}}],"probability":0.7},{"optionText":"下注 100(金)- 激进:30% 赢 250","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":250}}],"probability":0.3}]
|
||||
2 工匠的熔炉 工匠以金币交换防御塔组件。 [{"optionText":"交出 3 个白色组件,获得 50 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":3,"Rarity":"White"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":3,"Rarity":"White"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":50}}]},{"optionText":"交出 2 个绿色组件,获得 70 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":2,"Rarity":"Green"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":2,"Rarity":"Green"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":70}}]},{"optionText":"交出 1 个蓝色组件,获得 80 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":1,"Rarity":"Blue"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":1,"Rarity":"Blue"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":80}}]},{"optionText":"拒绝","rewardEffects":[]}]
|
||||
3 代价与回报 某种黑暗力量向你索取代价。 [{"optionText":"展示你的防御塔","requirements":[{"type":"TowerCountAtLeast","param":{"Count":2}}],"rewardEffects":[{"type":"AddGold","param":{"Count":50}}]},{"optionText":"离开","rewardEffects":[]}]
|
||||
# Id 列1 Title Description Option1 Option2 Option3 Option4
|
||||
# int string string string string string string
|
||||
# 事件编号 策划备注 事件题目 事件描述 选项 1 选项 2 选项 3 选项 4
|
||||
1 赌马 一名商人邀请你下注。赢了就能赚一笔。 {"optionText":"下注 100(金)- 稳健:70% 赢 150","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":150}}],"probability":0.7} {"optionText":"下注 100(金)- 激进:30% 赢 250","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":250}}],"probability":0.3}
|
||||
2 工匠的熔炉 工匠愿意熔炼你的旧组件,换给你一件更有希望的新货。 {"optionText":"交出 2 个白色组件,获得 1 个白色或绿色组件","requirements":[{"type":"CompCountAtLeast","param":{"Count":2,"Rarity":"White"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":2,"Rarity":"White"}}],"rewardEffects":[{"type":"AddRandomComps","param":{"Count":1,"MinRarity":"White","MaxRarity":"Green"}}]} {"optionText":"交出 2 个绿色组件,获得 1 个绿色或蓝色组件","requirements":[{"type":"CompCountAtLeast","param":{"Count":2,"Rarity":"Green"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":2,"Rarity":"Green"}}],"rewardEffects":[{"type":"AddRandomComps","param":{"Count":1,"MinRarity":"Green","MaxRarity":"Blue"}}]} {"optionText":"交出 2 个蓝色组件,获得 1 个蓝色或紫色组件","requirements":[{"type":"CompCountAtLeast","param":{"Count":2,"Rarity":"Blue"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":2,"Rarity":"Blue"}}],"rewardEffects":[{"type":"AddRandomComps","param":{"Count":1,"MinRarity":"Blue","MaxRarity":"Purple"}}]} {"optionText":"拒绝","requirements":[],"rewardEffects":[]}
|
||||
3 代价与回报 某种黑暗力量承诺给你金币,但它索取的代价会落在你的一座防御塔上。 {"optionText":"让黑暗力量抽取一座防御塔 20 点耐久,获得 50 金币","requirements":[{"type":"TowerCountAtLeast","param":{"Count":1}}],"costEffects":[{"type":"DamageRandomTowersEndurance","param":{"Count":1,"Amount":20}}],"rewardEffects":[{"type":"AddGold","param":{"Count":50}}]} {"optionText":"离开","requirements":[],"rewardEffects":[]}
|
||||
|
||||
@@ -4,3 +4,7 @@
|
||||
1 元素枪口 [20,30,40,50,80] 10 0.05 NormalBullet [Fire,BurnSpread,IgniteBurst,Inferno]
|
||||
2 控制枪口 [30,50,70,90,100] 20 0.01 NormalBullet [Ice,FreezeMask,Shatter,AbsoluteZero]
|
||||
3 穿透枪口 [50,55,60,80,90] 30 0.02 NormalBullet [Pierce,Crit,Overpenetrate,Execution]
|
||||
4 12枪口 [50,55,60,80,90] 30 0.02 NormalBullet [Fire,BurnSpread,IgniteBurst,Inferno,Ice,FreezeMask,Shatter,AbsoluteZero]
|
||||
5 23枪口 [50,55,60,80,90] 30 0.02 NormalBullet [Ice,FreezeMask,Shatter,AbsoluteZero,Pierce,Crit,Overpenetrate,Execution]
|
||||
6 13枪口 [50,55,60,80,90] 30 0.02 NormalBullet [Fire,BurnSpread,IgniteBurst,Inferno,Pierce,Crit,Overpenetrate,Execution]
|
||||
7 123枪口 [50,55,60,80,90] 30 0.02 NormalBullet [Fire,BurnSpread,IgniteBurst,Inferno,Ice,FreezeMask,Shatter,AbsoluteZero,Pierce,Crit,Overpenetrate,Execution]
|
||||
|
||||
@@ -1,48 +1,24 @@
|
||||
# Id 列1 LevelThemeType Rarity ItemType ItemId Weight MinPhase MaxPhase
|
||||
# int LevelThemeType RarityType ItemType int int int int
|
||||
# 道具池号 策划备注 关卡所属主题类型 道具稀有度 道具类型 道具Id 权重 出现的最低波次 出现的最高波次
|
||||
1 平原战斗池 Plain White MuzzleComp 1 10 1 10
|
||||
2 Plain White MuzzleComp 2 10 1 10
|
||||
3 Plain White MuzzleComp 3 10 1 10
|
||||
4 Plain White BearingComp 1 10 1 10
|
||||
5 Plain White BearingComp 2 10 1 10
|
||||
6 Plain White BearingComp 3 10 1 10
|
||||
7 Plain White BaseComp 1 10 1 10
|
||||
8 Plain White BaseComp 2 10 1 10
|
||||
9 Plain White BaseComp 3 10 1 10
|
||||
10 Plain Green MuzzleComp 1 5 2 10
|
||||
11 Plain Green MuzzleComp 2 5 2 10
|
||||
12 Plain Green MuzzleComp 3 5 2 10
|
||||
13 Plain Green BearingComp 1 5 2 10
|
||||
14 Plain Green BearingComp 2 5 2 10
|
||||
15 Plain Green BearingComp 3 5 2 10
|
||||
16 Plain Green BaseComp 1 5 2 10
|
||||
17 Plain Green BaseComp 2 5 2 10
|
||||
18 Plain Green BaseComp 3 5 2 10
|
||||
19 Plain Blue MuzzleComp 1 3 3 15
|
||||
20 Plain Blue MuzzleComp 2 3 3 15
|
||||
21 Plain Blue MuzzleComp 3 3 3 15
|
||||
22 Plain Blue BearingComp 1 3 3 15
|
||||
23 Plain Blue BearingComp 2 3 3 15
|
||||
24 Plain Blue BearingComp 3 3 3 15
|
||||
25 Plain Blue BaseComp 1 3 3 15
|
||||
26 Plain Blue BaseComp 2 3 3 15
|
||||
27 Plain Blue BaseComp 3 3 3 15
|
||||
28 Plain Purple MuzzleComp 1 2 4 20
|
||||
29 Plain Purple MuzzleComp 2 2 4 20
|
||||
30 Plain Purple MuzzleComp 3 2 4 20
|
||||
31 Plain Purple BearingComp 1 2 4 20
|
||||
32 Plain Purple BearingComp 2 2 4 20
|
||||
33 Plain Purple BearingComp 3 2 4 20
|
||||
34 Plain Purple BaseComp 1 2 4 20
|
||||
35 Plain Purple BaseComp 2 2 4 20
|
||||
36 Plain Purple BaseComp 3 2 4 20
|
||||
37 Plain Red MuzzleComp 1 1 5 50
|
||||
38 Plain Red MuzzleComp 2 1 5 50
|
||||
39 Plain Red MuzzleComp 3 1 5 50
|
||||
40 Plain Red BearingComp 1 1 5 50
|
||||
41 Plain Red BearingComp 2 1 5 50
|
||||
42 Plain Red BearingComp 3 1 5 50
|
||||
43 Plain Red BaseComp 1 1 5 50
|
||||
44 Plain Red BaseComp 2 1 5 50
|
||||
45 Plain Red BaseComp 3 1 5 50
|
||||
# Id 列1 LevelThemeType ItemType ItemId Weights MinPhase MaxPhase
|
||||
# int LevelThemeType ItemType int int[] int[] int[]
|
||||
# 道具池号 策划备注 关卡所属主题类型 道具类型 道具Id 权重 出现的最低波次 出现的最高波次
|
||||
1 平原战斗池 Plain MuzzleComp 1 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
|
||||
2 Plain MuzzleComp 2 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
|
||||
3 Plain MuzzleComp 3 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
|
||||
4 Plain MuzzleComp 4 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
5 Plain MuzzleComp 5 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
6 Plain MuzzleComp 6 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
7 Plain MuzzleComp 7 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
8 Plain BearingComp 1 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
|
||||
9 Plain BearingComp 2 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
|
||||
10 Plain BearingComp 3 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
|
||||
11 Plain BearingComp 4 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
12 Plain BearingComp 5 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
13 Plain BearingComp 6 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
14 Plain BearingComp 7 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
15 Plain BaseComp 1 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
|
||||
16 Plain BaseComp 2 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
|
||||
17 Plain BaseComp 3 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
|
||||
18 Plain BaseComp 4 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
19 Plain BaseComp 5 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
20 Plain BaseComp 6 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
21 Plain BaseComp 7 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
|
||||
|
||||
@@ -26,6 +26,8 @@ public partial class GameEntry
|
||||
|
||||
public static InventoryGenerationComponent InventoryGeneration { get; private set; }
|
||||
|
||||
public static TagRegistryComponent TagRegistry { get; private set; }
|
||||
|
||||
private static void InitCustomComponents()
|
||||
{
|
||||
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
|
||||
@@ -38,5 +40,6 @@ public partial class GameEntry
|
||||
ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent<ResolutionAdapterComponent>();
|
||||
SpriteCache = UnityGameFramework.Runtime.GameEntry.GetComponent<SpriteCacheComponent>();
|
||||
InventoryGeneration = UnityGameFramework.Runtime.GameEntry.GetComponent<InventoryGenerationComponent>();
|
||||
TagRegistry = UnityGameFramework.Runtime.GameEntry.GetComponent<TagRegistryComponent>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,11 +177,7 @@ namespace GeometryTD.CustomComponent
|
||||
|
||||
public void StartCombat(
|
||||
int levelId = 0,
|
||||
string runId = null,
|
||||
int runSeed = 0,
|
||||
int nodeId = 0,
|
||||
RunNodeType nodeType = RunNodeType.None,
|
||||
int sequenceIndex = -1)
|
||||
RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!EnsureCombatRuntimeInitialized())
|
||||
{
|
||||
@@ -232,11 +228,7 @@ namespace GeometryTD.CustomComponent
|
||||
selectedLevel,
|
||||
phaseList,
|
||||
_selectedSpawnEntriesByPhaseId,
|
||||
runId,
|
||||
runSeed,
|
||||
nodeId,
|
||||
nodeType,
|
||||
sequenceIndex))
|
||||
context))
|
||||
{
|
||||
CurrentLevel = null;
|
||||
return;
|
||||
|
||||
+14
-18
@@ -1,11 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Procedure;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
@@ -69,11 +67,7 @@ namespace GeometryTD.CustomComponent
|
||||
DRLevel level,
|
||||
IReadOnlyList<DRLevelPhase> phases,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId,
|
||||
string runId = null,
|
||||
int runSeed = 0,
|
||||
int nodeId = 0,
|
||||
RunNodeType nodeType = RunNodeType.None,
|
||||
int sequenceIndex = -1)
|
||||
RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!_initialized || _runtime.Entity == null)
|
||||
{
|
||||
@@ -95,16 +89,12 @@ namespace GeometryTD.CustomComponent
|
||||
_runtime.DidCombatWin = true;
|
||||
|
||||
_runtime.CurrentLevel = level;
|
||||
_runtime.RunId = runId;
|
||||
_runtime.RunSeed = runSeed;
|
||||
_runtime.NodeId = nodeId;
|
||||
_runtime.NodeType = nodeType;
|
||||
_runtime.SequenceIndex = sequenceIndex;
|
||||
GameEntry.InventoryGeneration.ConfigureRunContext(runSeed, sequenceIndex);
|
||||
_runtime.NodeContext = context != null ? context.Clone() : null;
|
||||
_runtime.NextDropOrdinal = 0;
|
||||
_runtime.NextRewardOrdinal = 0;
|
||||
_runtime.CombatRunResourceStore.InitializeForCombat(level);
|
||||
for (int i = 0; i < phases.Count; i++)
|
||||
foreach (var phase in phases)
|
||||
{
|
||||
DRLevelPhase phase = phases[i];
|
||||
if (phase != null)
|
||||
{
|
||||
_runtime.PhaseBuffer.Add(phase);
|
||||
@@ -122,7 +112,8 @@ namespace GeometryTD.CustomComponent
|
||||
_runtime.PhaseLoopRuntime.SetPhases(_runtime.PhaseBuffer);
|
||||
if (_runtime.PhaseLoopRuntime.PhaseCount <= 0)
|
||||
{
|
||||
return _coordinator.HandleStartFailure($"CombatScheduler start failed. Level '{level.Id}' has no phase data.");
|
||||
return _coordinator.HandleStartFailure(
|
||||
$"CombatScheduler start failed. Level '{level.Id}' has no phase data.");
|
||||
}
|
||||
|
||||
ChangeState(new CombatLoadingState(_runtime, _coordinator));
|
||||
@@ -201,7 +192,13 @@ namespace GeometryTD.CustomComponent
|
||||
enemy,
|
||||
_runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
_coordinator.ResolveCurrentThemeType());
|
||||
EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(context);
|
||||
int nextDropOrdinal = _runtime.NextDropOrdinal;
|
||||
EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(
|
||||
context,
|
||||
_runtime.NodeContext != null ? _runtime.NodeContext.RunSeed : 0,
|
||||
_runtime.NodeContext != null ? _runtime.NodeContext.SequenceIndex : -1,
|
||||
ref nextDropOrdinal);
|
||||
_runtime.NextDropOrdinal = nextDropOrdinal;
|
||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedReward(result.Coin, result.Gold);
|
||||
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
|
||||
}
|
||||
@@ -326,6 +323,5 @@ namespace GeometryTD.CustomComponent
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
+21
-15
@@ -55,11 +55,9 @@ namespace GeometryTD.CustomComponent
|
||||
_runtime.DidCombatWin = true;
|
||||
_runtime.IsCompleted = false;
|
||||
_runtime.NodeEnterFired = false;
|
||||
_runtime.RunId = null;
|
||||
_runtime.RunSeed = 0;
|
||||
_runtime.NodeId = 0;
|
||||
_runtime.NodeType = RunNodeType.None;
|
||||
_runtime.SequenceIndex = -1;
|
||||
_runtime.NodeContext = null;
|
||||
_runtime.NextDropOrdinal = 0;
|
||||
_runtime.NextRewardOrdinal = 0;
|
||||
}
|
||||
|
||||
public void CleanupAllCombatEntities()
|
||||
@@ -201,16 +199,20 @@ namespace GeometryTD.CustomComponent
|
||||
public void CompleteNormalCombatAndNotify(bool didCombatWin)
|
||||
{
|
||||
CompleteCombat(didCombatWin);
|
||||
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_runtime.RunId,
|
||||
_runtime.NodeId,
|
||||
_runtime.NodeType,
|
||||
_runtime.SequenceIndex,
|
||||
nodeContext?.RunId,
|
||||
nodeContext?.NodeId ?? 0,
|
||||
nodeContext?.NodeType ?? RunNodeType.None,
|
||||
nodeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
didCombatWin,
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null));
|
||||
nodeContext != null
|
||||
? nodeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
}
|
||||
|
||||
public void CompleteFailureCombatAndNotify()
|
||||
@@ -220,16 +222,20 @@ namespace GeometryTD.CustomComponent
|
||||
CloseRewardSelectForm();
|
||||
CloseDialogForm();
|
||||
CompleteCombat(false);
|
||||
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_runtime.RunId,
|
||||
_runtime.NodeId,
|
||||
_runtime.NodeType,
|
||||
_runtime.SequenceIndex,
|
||||
nodeContext?.RunId,
|
||||
nodeContext?.NodeId ?? 0,
|
||||
nodeContext?.NodeType ?? RunNodeType.None,
|
||||
nodeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Exception,
|
||||
false,
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null));
|
||||
nodeContext != null
|
||||
? nodeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
}
|
||||
|
||||
public bool HandleStartFailure(string errorMessage)
|
||||
|
||||
+3
-5
@@ -29,10 +29,8 @@ namespace GeometryTD.CustomComponent
|
||||
public bool IsCompleted { get; set; }
|
||||
public bool NodeEnterFired { get; set; }
|
||||
public CombatSettlementContext SettlementContext { get; set; }
|
||||
public string RunId { get; set; }
|
||||
public int RunSeed { get; set; }
|
||||
public int NodeId { get; set; }
|
||||
public RunNodeType NodeType { get; set; }
|
||||
public int SequenceIndex { get; set; }
|
||||
public RunNodeExecutionContext NodeContext { get; set; }
|
||||
public int NextDropOrdinal { get; set; }
|
||||
public int NextRewardOrdinal { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
+17
-212
@@ -11,10 +11,8 @@ namespace GeometryTD.CustomComponent
|
||||
public sealed class CombatSettlementService
|
||||
{
|
||||
private const int RewardSelectDisplayCount = 3;
|
||||
private const float FullBaseHpGoldBonusRate = 0.3f;
|
||||
private const float HighBaseHpGoldBonusRate = 0.1f;
|
||||
private const float HighBaseHpThreshold = 0.8f;
|
||||
private const float SettlementTowerEnduranceLoss = 1f;
|
||||
private readonly CombatSettlementCalculator _calculator = new();
|
||||
private readonly CombatSettlementCommitter _committer = new();
|
||||
|
||||
public CombatSettlementContext BuildSettlementContext(
|
||||
bool didCombatWin,
|
||||
@@ -22,69 +20,25 @@ namespace GeometryTD.CustomComponent
|
||||
int defeatedEnemyCount,
|
||||
CombatRunResourceStore resourceStore)
|
||||
{
|
||||
bool shouldOpenFullBaseHpRewardSelect = false;
|
||||
ResolveSettlementByBaseHp(
|
||||
return _calculator.BuildSettlementContext(
|
||||
didCombatWin,
|
||||
currentLevel,
|
||||
resourceStore,
|
||||
out int currentBaseHp,
|
||||
out int maxBaseHp,
|
||||
out int levelRewardGold,
|
||||
out float bonusRate,
|
||||
out int bonusGold,
|
||||
out shouldOpenFullBaseHpRewardSelect);
|
||||
|
||||
CombatSettlementContext settlementContext = new CombatSettlementContext
|
||||
{
|
||||
};
|
||||
settlementContext.Result.DidCombatWin = didCombatWin;
|
||||
settlementContext.Result.FinalCoin = Mathf.Max(0, resourceStore != null ? resourceStore.CurrentCoin : 0);
|
||||
settlementContext.Result.FinalBaseHp = currentBaseHp;
|
||||
settlementContext.Result.MaxBaseHp = maxBaseHp;
|
||||
settlementContext.Result.DefeatedEnemyCount = Mathf.Max(0, defeatedEnemyCount);
|
||||
settlementContext.Result.GainedGold = Mathf.Max(0, resourceStore != null ? resourceStore.GainedGold : 0);
|
||||
settlementContext.Result.RewardInventory = resourceStore != null
|
||||
? resourceStore.GetRewardInventorySnapshot()
|
||||
: new BackpackInventoryData();
|
||||
PopulateEnduranceSettlement(settlementContext, resourceStore);
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = shouldOpenFullBaseHpRewardSelect;
|
||||
settlementContext.Flags.DidEnterRewardSelection = false;
|
||||
settlementContext.Summary.DefeatedEnemyCount = settlementContext.Result.DefeatedEnemyCount;
|
||||
settlementContext.Summary.GainedGold = settlementContext.Result.GainedGold;
|
||||
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
|
||||
|
||||
Log.Info(
|
||||
"Combat settlement resolved. Level={0}, BaseHp={1}/{2}, LevelReward={3}, BonusRate={4:P0}, BonusGold={5}, FullHpRewardSelect={6}, EnduranceTargets={7}.",
|
||||
currentLevel != null ? currentLevel.Id : 0,
|
||||
currentBaseHp,
|
||||
maxBaseHp,
|
||||
levelRewardGold,
|
||||
bonusRate,
|
||||
bonusGold,
|
||||
shouldOpenFullBaseHpRewardSelect,
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds.Count);
|
||||
return settlementContext;
|
||||
defeatedEnemyCount,
|
||||
resourceStore);
|
||||
}
|
||||
|
||||
public void CommitSettlementInventory(CombatSettlementContext settlementContext)
|
||||
{
|
||||
if (settlementContext == null || settlementContext.Flags.IsCommitted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BackpackInventoryData rewardInventory = settlementContext.Result.RewardInventory ?? new BackpackInventoryData();
|
||||
GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
|
||||
settlementContext.Result.RewardInventory = rewardInventory;
|
||||
settlementContext.Summary.RewardInventory = rewardInventory;
|
||||
settlementContext.Result.Endurance.AffectedTowerCount = ApplyDeferredSettlementEndurance(settlementContext);
|
||||
settlementContext.Flags.IsCommitted = true;
|
||||
_committer.CommitSettlementInventory(settlementContext);
|
||||
}
|
||||
|
||||
public bool TryPrepareRewardSelection(
|
||||
CombatSettlementContext settlementContext,
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextRewardOrdinal,
|
||||
RewardSelectFormUseCase rewardSelectFormUseCase,
|
||||
Action<RewardSelectItemRawData> onRewardSelected,
|
||||
Action onGiveUp)
|
||||
@@ -97,36 +51,22 @@ namespace GeometryTD.CustomComponent
|
||||
IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates(
|
||||
displayPhaseIndex,
|
||||
themeType,
|
||||
RewardSelectDisplayCount);
|
||||
RewardSelectDisplayCount,
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
ref nextRewardOrdinal);
|
||||
if (candidateItems == null || candidateItems.Count <= 0)
|
||||
{
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
List<RewardSelectItemRawData> rewardPool = new List<RewardSelectItemRawData>(candidateItems.Count);
|
||||
foreach (var item in candidateItems)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
rewardPool.Add(BuildRewardSelectRawData(item));
|
||||
}
|
||||
|
||||
if (rewardPool.Count <= 0)
|
||||
{
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
rewardSelectFormUseCase.SetCallbacks(onRewardSelected, onGiveUp);
|
||||
rewardSelectFormUseCase.ConfigureRewardPool(
|
||||
rewardPool,
|
||||
rewardSelectFormUseCase.ConfigureRewardCandidates(
|
||||
candidateItems,
|
||||
displayCount: RewardSelectDisplayCount,
|
||||
refreshCost: 0,
|
||||
allowRefreshOnce: false,
|
||||
allowRotateOnce: false,
|
||||
allowGiveUp: false,
|
||||
tipText: "基地满血奖励:请选择 1 个组件");
|
||||
|
||||
@@ -144,13 +84,7 @@ namespace GeometryTD.CustomComponent
|
||||
|
||||
public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward)
|
||||
{
|
||||
if (settlementContext?.Result.RewardInventory == null || selectedReward?.SourceItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TryAppendRewardComponent(settlementContext.Result.RewardInventory, selectedReward.SourceItem);
|
||||
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
|
||||
_committer.ApplySelectedReward(settlementContext, selectedReward);
|
||||
}
|
||||
|
||||
public void OpenCombatFinishForm(
|
||||
@@ -166,134 +100,5 @@ namespace GeometryTD.CustomComponent
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm);
|
||||
}
|
||||
|
||||
private static void ResolveSettlementByBaseHp(
|
||||
bool didCombatWin,
|
||||
DRLevel currentLevel,
|
||||
CombatRunResourceStore resourceStore,
|
||||
out int currentBaseHp,
|
||||
out int maxBaseHp,
|
||||
out int levelRewardGold,
|
||||
out float bonusRate,
|
||||
out int bonusGold,
|
||||
out bool shouldOpenFullBaseHpRewardSelect)
|
||||
{
|
||||
currentBaseHp = Mathf.Max(0, resourceStore != null ? resourceStore.CurrentBaseHp : 0);
|
||||
maxBaseHp = resourceStore != null ? Mathf.Max(0, resourceStore.MaxBaseHp) : 0;
|
||||
if (maxBaseHp > 0)
|
||||
{
|
||||
currentBaseHp = Mathf.Clamp(currentBaseHp, 0, maxBaseHp);
|
||||
}
|
||||
|
||||
levelRewardGold = currentLevel != null ? Mathf.Max(0, currentLevel.RewardGold) : 0;
|
||||
bonusRate = 0f;
|
||||
bonusGold = 0;
|
||||
shouldOpenFullBaseHpRewardSelect = false;
|
||||
|
||||
if (!didCombatWin || resourceStore == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxBaseHp > 0 && currentBaseHp >= maxBaseHp)
|
||||
{
|
||||
bonusRate = FullBaseHpGoldBonusRate;
|
||||
shouldOpenFullBaseHpRewardSelect = true;
|
||||
}
|
||||
else if (maxBaseHp > 0)
|
||||
{
|
||||
float hpRate = (float)currentBaseHp / maxBaseHp;
|
||||
if (hpRate >= HighBaseHpThreshold)
|
||||
{
|
||||
bonusRate = HighBaseHpGoldBonusRate;
|
||||
}
|
||||
}
|
||||
|
||||
int goldForBonusCalculation = Mathf.Max(0, resourceStore.GainedGold) + levelRewardGold;
|
||||
bonusGold = bonusRate > 0f ? Mathf.FloorToInt(goldForBonusCalculation * bonusRate) : 0;
|
||||
int settlementGold = levelRewardGold + bonusGold;
|
||||
resourceStore.AddSettlementGold(settlementGold);
|
||||
}
|
||||
|
||||
private static void PopulateEnduranceSettlement(
|
||||
CombatSettlementContext settlementContext,
|
||||
CombatRunResourceStore resourceStore)
|
||||
{
|
||||
if (settlementContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CombatSettlementEnduranceResult enduranceResult = settlementContext.Result.Endurance;
|
||||
enduranceResult.TargetTowerInstanceIds.Clear();
|
||||
enduranceResult.EnduranceLossPerComponent = SettlementTowerEnduranceLoss;
|
||||
|
||||
IReadOnlyList<long> participantTowerIds = resourceStore?.GetParticipantTowerInstanceIdSnapshot();
|
||||
if (participantTowerIds == null || participantTowerIds.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < participantTowerIds.Count; i++)
|
||||
{
|
||||
long towerId = participantTowerIds[i];
|
||||
if (towerId > 0)
|
||||
{
|
||||
enduranceResult.TargetTowerInstanceIds.Add(towerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int ApplyDeferredSettlementEndurance(CombatSettlementContext settlementContext)
|
||||
{
|
||||
if (settlementContext == null ||
|
||||
settlementContext.Result.Endurance.EnduranceLossPerComponent <= 0f ||
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds.Count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
PlayerInventoryComponent inventory = GameEntry.PlayerInventory;
|
||||
if (inventory == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return inventory.ReduceTowerEndurance(
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds,
|
||||
settlementContext.Result.Endurance.EnduranceLossPerComponent);
|
||||
}
|
||||
|
||||
private static bool TryAppendRewardComponent(BackpackInventoryData rewardInventory, TowerCompItemData selectedItem)
|
||||
{
|
||||
if (rewardInventory == null || selectedItem == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedItem is MuzzleCompItemData muzzleComp)
|
||||
{
|
||||
rewardInventory.MuzzleComponents.Add(muzzleComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedItem is BearingCompItemData bearingComp)
|
||||
{
|
||||
rewardInventory.BearingComponents.Add(bearingComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedItem is BaseCompItemData baseComp)
|
||||
{
|
||||
rewardInventory.BaseComponents.Add(baseComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static RewardSelectItemRawData BuildRewardSelectRawData(TowerCompItemData item)
|
||||
{
|
||||
return RewardSelectItemRawDataBuilder.Build(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -16,16 +16,23 @@ namespace GeometryTD.CustomComponent
|
||||
}
|
||||
|
||||
Coordinator.EnsureRewardSelectFormUseCaseBound();
|
||||
int nextRewardOrdinal = Runtime.NextRewardOrdinal;
|
||||
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
|
||||
Runtime.SettlementContext,
|
||||
Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
|
||||
Coordinator.ResolveCurrentThemeType(),
|
||||
Runtime.NodeContext != null ? Runtime.NodeContext.RunSeed : 0,
|
||||
Runtime.NodeContext != null ? Runtime.NodeContext.SequenceIndex : -1,
|
||||
ref nextRewardOrdinal,
|
||||
Runtime.RewardSelectFormUseCase,
|
||||
Coordinator.OnFullBaseHpRewardSelected,
|
||||
Coordinator.OnFullBaseHpRewardGiveUp))
|
||||
{
|
||||
Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator));
|
||||
return;
|
||||
}
|
||||
|
||||
Runtime.NextRewardOrdinal = nextRewardOrdinal;
|
||||
}
|
||||
|
||||
public override void OnExit()
|
||||
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
@@ -41,10 +42,10 @@ namespace GeometryTD.CustomComponent
|
||||
GameEntry.Event.Fire(
|
||||
Coordinator,
|
||||
NodeEnterEventArgs.Create(
|
||||
Runtime.RunId,
|
||||
Runtime.NodeId,
|
||||
Runtime.NodeType,
|
||||
Runtime.SequenceIndex));
|
||||
Runtime.NodeContext?.RunId,
|
||||
Runtime.NodeContext?.NodeId ?? 0,
|
||||
Runtime.NodeContext?.NodeType ?? RunNodeType.None,
|
||||
Runtime.NodeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatSettlementCalculator
|
||||
{
|
||||
private const float FullBaseHpGoldBonusRate = 0.3f;
|
||||
private const float HighBaseHpGoldBonusRate = 0.1f;
|
||||
private const float HighBaseHpThreshold = 0.8f;
|
||||
private const float SettlementTowerEnduranceLoss = 1f;
|
||||
|
||||
public CombatSettlementContext BuildSettlementContext(
|
||||
bool didCombatWin,
|
||||
DRLevel currentLevel,
|
||||
int defeatedEnemyCount,
|
||||
CombatRunResourceStore resourceStore)
|
||||
{
|
||||
bool shouldOpenFullBaseHpRewardSelect = false;
|
||||
ResolveSettlementByBaseHp(
|
||||
didCombatWin,
|
||||
currentLevel,
|
||||
resourceStore,
|
||||
out int currentBaseHp,
|
||||
out int maxBaseHp,
|
||||
out int levelRewardGold,
|
||||
out float bonusRate,
|
||||
out int bonusGold,
|
||||
out shouldOpenFullBaseHpRewardSelect);
|
||||
|
||||
CombatSettlementContext settlementContext = new CombatSettlementContext();
|
||||
settlementContext.Result.DidCombatWin = didCombatWin;
|
||||
settlementContext.Result.FinalCoin = Mathf.Max(0, resourceStore != null ? resourceStore.CurrentCoin : 0);
|
||||
settlementContext.Result.FinalBaseHp = currentBaseHp;
|
||||
settlementContext.Result.MaxBaseHp = maxBaseHp;
|
||||
settlementContext.Result.DefeatedEnemyCount = Mathf.Max(0, defeatedEnemyCount);
|
||||
settlementContext.Result.GainedGold = Mathf.Max(0, resourceStore != null ? resourceStore.GainedGold : 0);
|
||||
settlementContext.Result.RewardInventory = resourceStore != null
|
||||
? resourceStore.GetRewardInventorySnapshot()
|
||||
: new BackpackInventoryData();
|
||||
PopulateEnduranceSettlement(settlementContext, resourceStore);
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = shouldOpenFullBaseHpRewardSelect;
|
||||
settlementContext.Flags.DidEnterRewardSelection = false;
|
||||
settlementContext.Summary.DefeatedEnemyCount = settlementContext.Result.DefeatedEnemyCount;
|
||||
settlementContext.Summary.GainedGold = settlementContext.Result.GainedGold;
|
||||
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
|
||||
|
||||
Log.Info(
|
||||
"Combat settlement resolved. Level={0}, BaseHp={1}/{2}, LevelReward={3}, BonusRate={4:P0}, BonusGold={5}, FullHpRewardSelect={6}, EnduranceTargets={7}.",
|
||||
currentLevel != null ? currentLevel.Id : 0,
|
||||
currentBaseHp,
|
||||
maxBaseHp,
|
||||
levelRewardGold,
|
||||
bonusRate,
|
||||
bonusGold,
|
||||
shouldOpenFullBaseHpRewardSelect,
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds.Count);
|
||||
return settlementContext;
|
||||
}
|
||||
|
||||
private static void ResolveSettlementByBaseHp(
|
||||
bool didCombatWin,
|
||||
DRLevel currentLevel,
|
||||
CombatRunResourceStore resourceStore,
|
||||
out int currentBaseHp,
|
||||
out int maxBaseHp,
|
||||
out int levelRewardGold,
|
||||
out float bonusRate,
|
||||
out int bonusGold,
|
||||
out bool shouldOpenFullBaseHpRewardSelect)
|
||||
{
|
||||
currentBaseHp = Mathf.Max(0, resourceStore != null ? resourceStore.CurrentBaseHp : 0);
|
||||
maxBaseHp = resourceStore != null ? Mathf.Max(0, resourceStore.MaxBaseHp) : 0;
|
||||
if (maxBaseHp > 0)
|
||||
{
|
||||
currentBaseHp = Mathf.Clamp(currentBaseHp, 0, maxBaseHp);
|
||||
}
|
||||
|
||||
levelRewardGold = currentLevel != null ? Mathf.Max(0, currentLevel.RewardGold) : 0;
|
||||
bonusRate = 0f;
|
||||
bonusGold = 0;
|
||||
shouldOpenFullBaseHpRewardSelect = false;
|
||||
|
||||
if (!didCombatWin || resourceStore == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxBaseHp > 0 && currentBaseHp >= maxBaseHp)
|
||||
{
|
||||
bonusRate = FullBaseHpGoldBonusRate;
|
||||
shouldOpenFullBaseHpRewardSelect = true;
|
||||
}
|
||||
else if (maxBaseHp > 0)
|
||||
{
|
||||
float hpRate = (float)currentBaseHp / maxBaseHp;
|
||||
if (hpRate >= HighBaseHpThreshold)
|
||||
{
|
||||
bonusRate = HighBaseHpGoldBonusRate;
|
||||
}
|
||||
}
|
||||
|
||||
int goldForBonusCalculation = Mathf.Max(0, resourceStore.GainedGold) + levelRewardGold;
|
||||
bonusGold = bonusRate > 0f ? Mathf.FloorToInt(goldForBonusCalculation * bonusRate) : 0;
|
||||
int settlementGold = levelRewardGold + bonusGold;
|
||||
resourceStore.AddSettlementGold(settlementGold);
|
||||
}
|
||||
|
||||
private static void PopulateEnduranceSettlement(
|
||||
CombatSettlementContext settlementContext,
|
||||
CombatRunResourceStore resourceStore)
|
||||
{
|
||||
if (settlementContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CombatSettlementEnduranceResult enduranceResult = settlementContext.Result.Endurance;
|
||||
enduranceResult.TargetTowerInstanceIds.Clear();
|
||||
enduranceResult.EnduranceLossPerComponent = SettlementTowerEnduranceLoss;
|
||||
|
||||
IReadOnlyList<long> participantTowerIds = resourceStore?.GetParticipantTowerInstanceIdSnapshot();
|
||||
if (participantTowerIds == null || participantTowerIds.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < participantTowerIds.Count; i++)
|
||||
{
|
||||
long towerId = participantTowerIds[i];
|
||||
if (towerId > 0)
|
||||
{
|
||||
enduranceResult.TargetTowerInstanceIds.Add(towerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8aa78b2f9ff4eb4bf83b8c4f652ce24
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,83 @@
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class CombatSettlementCommitter
|
||||
{
|
||||
public void CommitSettlementInventory(CombatSettlementContext settlementContext)
|
||||
{
|
||||
if (settlementContext == null || settlementContext.Flags.IsCommitted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
BackpackInventoryData rewardInventory = settlementContext.Result.RewardInventory ?? new BackpackInventoryData();
|
||||
GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
|
||||
settlementContext.Result.RewardInventory = rewardInventory;
|
||||
settlementContext.Summary.RewardInventory = rewardInventory;
|
||||
settlementContext.Result.Endurance.AffectedTowerCount = ApplyDeferredSettlementEndurance(settlementContext);
|
||||
settlementContext.Flags.IsCommitted = true;
|
||||
}
|
||||
|
||||
public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward)
|
||||
{
|
||||
if (settlementContext?.Result.RewardInventory == null || selectedReward?.SourceItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TryAppendRewardComponent(settlementContext.Result.RewardInventory, selectedReward.SourceItem);
|
||||
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
|
||||
}
|
||||
|
||||
private static int ApplyDeferredSettlementEndurance(CombatSettlementContext settlementContext)
|
||||
{
|
||||
if (settlementContext == null ||
|
||||
settlementContext.Result.Endurance.EnduranceLossPerComponent <= 0f ||
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds.Count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
PlayerInventoryComponent inventory = GameEntry.PlayerInventory;
|
||||
if (inventory == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return inventory.ReduceTowerEndurance(
|
||||
settlementContext.Result.Endurance.TargetTowerInstanceIds,
|
||||
settlementContext.Result.Endurance.EnduranceLossPerComponent);
|
||||
}
|
||||
|
||||
private static bool TryAppendRewardComponent(BackpackInventoryData rewardInventory, TowerCompItemData selectedItem)
|
||||
{
|
||||
if (rewardInventory == null || selectedItem == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedItem is MuzzleCompItemData muzzleComp)
|
||||
{
|
||||
rewardInventory.MuzzleComponents.Add(muzzleComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedItem is BearingCompItemData bearingComp)
|
||||
{
|
||||
rewardInventory.BearingComponents.Add(bearingComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedItem is BaseCompItemData baseComp)
|
||||
{
|
||||
rewardInventory.BaseComponents.Add(baseComp);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e4e69cf76f34e5baec16562d765fb85
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -14,10 +14,8 @@ namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class EventNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
private string _activeRunId;
|
||||
private int _activeNodeId;
|
||||
private RunNodeType _activeNodeType = RunNodeType.None;
|
||||
private int _activeSequenceIndex = -1;
|
||||
private RunNodeExecutionContext _activeContext;
|
||||
private EventItem _activeEvent;
|
||||
|
||||
private readonly List<EventItem> _eventItems = new List<EventItem>();
|
||||
|
||||
@@ -54,7 +52,7 @@ namespace GeometryTD.CustomComponent
|
||||
Log.Info("EventNodeComponent initialized with {0} events.", _eventItems.Count);
|
||||
}
|
||||
|
||||
public void StartEvent(string runId = null, int nodeId = 0, RunNodeType nodeType = RunNodeType.None, int sequenceIndex = -1)
|
||||
public void StartEvent(RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
@@ -67,22 +65,29 @@ namespace GeometryTD.CustomComponent
|
||||
return;
|
||||
}
|
||||
|
||||
int index = Random.Range(0, _eventItems.Count);
|
||||
EventItem randomEvent = _eventItems[index];
|
||||
|
||||
if (_eventFormUseCase == null)
|
||||
{
|
||||
Log.Warning("EventNodeComponent StartEvent failed. Event form is not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
_activeRunId = runId;
|
||||
_activeNodeId = nodeId;
|
||||
_activeNodeType = nodeType;
|
||||
_activeSequenceIndex = sequenceIndex;
|
||||
_eventFormUseCase.SetCurrentEvent(randomEvent);
|
||||
_activeContext = context != null ? context.Clone() : null;
|
||||
_activeEvent = SelectActiveEvent(_activeContext);
|
||||
if (_activeEvent == null)
|
||||
{
|
||||
Log.Warning("EventNodeComponent StartEvent failed. No active event could be resolved.");
|
||||
return;
|
||||
}
|
||||
|
||||
_eventFormUseCase.BindEvent(_activeEvent, _activeContext);
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.EventForm);
|
||||
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create(runId, nodeId, nodeType, sequenceIndex));
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeEnterEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
public void EndEvent()
|
||||
@@ -91,22 +96,24 @@ namespace GeometryTD.CustomComponent
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_activeRunId,
|
||||
_activeNodeId,
|
||||
_activeNodeType,
|
||||
_activeSequenceIndex,
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null));
|
||||
_activeContext != null
|
||||
? _activeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
ClearActiveNodeContext();
|
||||
}
|
||||
|
||||
private void ClearActiveNodeContext()
|
||||
{
|
||||
_activeRunId = null;
|
||||
_activeNodeId = 0;
|
||||
_activeNodeType = RunNodeType.None;
|
||||
_activeSequenceIndex = -1;
|
||||
_activeContext = null;
|
||||
_activeEvent = null;
|
||||
_eventFormUseCase?.Clear();
|
||||
}
|
||||
|
||||
private static EventOption[] ParseOptions(string optionsRaw)
|
||||
@@ -200,5 +207,34 @@ namespace GeometryTD.CustomComponent
|
||||
|
||||
return effects.ToArray();
|
||||
}
|
||||
|
||||
private EventItem SelectActiveEvent(RunNodeExecutionContext context)
|
||||
{
|
||||
if (_eventItems.Count <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
int randomIndex = Random.Range(0, _eventItems.Count);
|
||||
return _eventItems[randomIndex];
|
||||
}
|
||||
|
||||
System.Random random = new System.Random(BuildSelectionSeed(context));
|
||||
return _eventItems[random.Next(0, _eventItems.Count)];
|
||||
}
|
||||
|
||||
private static int BuildSelectionSeed(RunNodeExecutionContext context)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int seed = 17;
|
||||
seed = seed * 31 + context.RunSeed;
|
||||
seed = seed * 31 + context.SequenceIndex;
|
||||
seed = seed * 31 + context.NodeId;
|
||||
return seed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,24 @@ using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class DropPoolRoller
|
||||
{
|
||||
private const float RarityCurveScalePhase = 30f;
|
||||
private static readonly RarityType[] OrderedRarities =
|
||||
{
|
||||
RarityType.White,
|
||||
RarityType.Green,
|
||||
RarityType.Blue,
|
||||
RarityType.Purple,
|
||||
RarityType.Red
|
||||
};
|
||||
|
||||
private readonly List<DROutGameDropPool> _eligibleRowBuffer = new();
|
||||
private readonly Dictionary<RarityType, float> _rarityWeightBuffer = new();
|
||||
private readonly float[] _rarityWeightBuffer = new float[OrderedRarities.Length];
|
||||
private readonly IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||
|
||||
public DropPoolRoller(IDataTable<DROutGameDropPool> dropPoolTable)
|
||||
@@ -19,9 +28,15 @@ namespace GeometryTD.CustomComponent
|
||||
_dropPoolTable = dropPoolTable;
|
||||
}
|
||||
|
||||
public bool TryRollRow(int displayPhaseIndex, LevelThemeType themeType, out DROutGameDropPool selectedRow)
|
||||
public bool TryRollRow(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
Random random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity)
|
||||
{
|
||||
selectedRow = null;
|
||||
selectedRarity = RarityType.None;
|
||||
|
||||
DROutGameDropPool[] allRows = _dropPoolTable.GetAllDataRows();
|
||||
if (allRows == null || allRows.Length <= 0)
|
||||
@@ -35,22 +50,29 @@ namespace GeometryTD.CustomComponent
|
||||
return false;
|
||||
}
|
||||
|
||||
RarityType selectedRarity = RollRarity(displayPhaseIndex);
|
||||
selectedRarity = RollRarity(displayPhaseIndex, random);
|
||||
if (selectedRarity == RarityType.None)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int totalWeight = 0;
|
||||
for (int i = 0; i < _eligibleRowBuffer.Count; i++)
|
||||
DROutGameDropPool fallbackRow = null;
|
||||
foreach (var row in _eligibleRowBuffer)
|
||||
{
|
||||
DROutGameDropPool row = _eligibleRowBuffer[i];
|
||||
if (row.Rarity != selectedRarity)
|
||||
if (!IsEligibleAtPhase(row, selectedRarity, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
totalWeight += Mathf.Max(1, row.Weight);
|
||||
int rowWeight = row.GetWeight(selectedRarity);
|
||||
if (rowWeight <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fallbackRow = row;
|
||||
totalWeight += rowWeight;
|
||||
}
|
||||
|
||||
if (totalWeight <= 0)
|
||||
@@ -58,16 +80,22 @@ namespace GeometryTD.CustomComponent
|
||||
return false;
|
||||
}
|
||||
|
||||
int randomWeight = Random.Range(1, totalWeight + 1);
|
||||
int randomWeight = random.Next(1, totalWeight + 1);
|
||||
int cumulativeWeight = 0;
|
||||
foreach (var row in _eligibleRowBuffer)
|
||||
{
|
||||
if (row.Rarity != selectedRarity)
|
||||
if (!IsEligibleAtPhase(row, selectedRarity, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cumulativeWeight += Mathf.Max(1, row.Weight);
|
||||
int rowWeight = row.GetWeight(selectedRarity);
|
||||
if (rowWeight <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cumulativeWeight += rowWeight;
|
||||
if (randomWeight <= cumulativeWeight)
|
||||
{
|
||||
selectedRow = row;
|
||||
@@ -75,7 +103,7 @@ namespace GeometryTD.CustomComponent
|
||||
}
|
||||
}
|
||||
|
||||
selectedRow = _eligibleRowBuffer[_eligibleRowBuffer.Count - 1];
|
||||
selectedRow = fallbackRow;
|
||||
return selectedRow != null;
|
||||
}
|
||||
|
||||
@@ -86,9 +114,8 @@ namespace GeometryTD.CustomComponent
|
||||
{
|
||||
_eligibleRowBuffer.Clear();
|
||||
|
||||
for (int i = 0; i < allRows.Length; i++)
|
||||
foreach (var row in allRows)
|
||||
{
|
||||
DROutGameDropPool row = allRows[i];
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
@@ -99,7 +126,7 @@ namespace GeometryTD.CustomComponent
|
||||
continue;
|
||||
}
|
||||
|
||||
if (displayPhaseIndex < row.MinPhase || displayPhaseIndex > row.MaxPhase)
|
||||
if (!IsEligibleAtPhase(row, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -108,35 +135,45 @@ namespace GeometryTD.CustomComponent
|
||||
}
|
||||
}
|
||||
|
||||
private RarityType RollRarity(int displayPhaseIndex)
|
||||
private RarityType RollRarity(int displayPhaseIndex, Random random)
|
||||
{
|
||||
_rarityWeightBuffer.Clear();
|
||||
for (int i = 0; i < _rarityWeightBuffer.Length; i++)
|
||||
{
|
||||
_rarityWeightBuffer[i] = 0f;
|
||||
}
|
||||
|
||||
float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase);
|
||||
|
||||
for (int i = 0; i < _eligibleRowBuffer.Count; i++)
|
||||
foreach (var row in _eligibleRowBuffer)
|
||||
{
|
||||
DROutGameDropPool row = _eligibleRowBuffer[i];
|
||||
float curveWeight = GetRarityCurveWeight(row.Rarity, phaseT);
|
||||
for (int rarityIndex = 0; rarityIndex < OrderedRarities.Length; rarityIndex++)
|
||||
{
|
||||
RarityType rarity = OrderedRarities[rarityIndex];
|
||||
if (!IsEligibleAtPhase(row, rarity, displayPhaseIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int rowWeight = row.GetWeight(rarity);
|
||||
if (rowWeight <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float curveWeight = GetRarityCurveWeight(rarity, phaseT);
|
||||
if (curveWeight <= 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float rowWeight = Mathf.Max(1, row.Weight) * curveWeight;
|
||||
if (_rarityWeightBuffer.TryGetValue(row.Rarity, out float existingWeight))
|
||||
{
|
||||
_rarityWeightBuffer[row.Rarity] = existingWeight + rowWeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
_rarityWeightBuffer[row.Rarity] = rowWeight;
|
||||
_rarityWeightBuffer[rarityIndex] += rowWeight * curveWeight;
|
||||
}
|
||||
}
|
||||
|
||||
float totalWeight = 0f;
|
||||
foreach (var pair in _rarityWeightBuffer)
|
||||
foreach (var weight in _rarityWeightBuffer)
|
||||
{
|
||||
totalWeight += Mathf.Max(0f, pair.Value);
|
||||
totalWeight += Mathf.Max(0f, weight);
|
||||
}
|
||||
|
||||
if (totalWeight <= 0f)
|
||||
@@ -144,25 +181,54 @@ namespace GeometryTD.CustomComponent
|
||||
return RarityType.None;
|
||||
}
|
||||
|
||||
float randomWeight = Random.value * totalWeight;
|
||||
float randomWeight = (float)(random.NextDouble() * totalWeight);
|
||||
float cumulativeWeight = 0f;
|
||||
foreach (var pair in _rarityWeightBuffer)
|
||||
for (int rarityIndex = 0; rarityIndex < _rarityWeightBuffer.Length; rarityIndex++)
|
||||
{
|
||||
cumulativeWeight += Mathf.Max(0f, pair.Value);
|
||||
cumulativeWeight += Mathf.Max(0f, _rarityWeightBuffer[rarityIndex]);
|
||||
if (randomWeight <= cumulativeWeight)
|
||||
{
|
||||
return pair.Key;
|
||||
return OrderedRarities[rarityIndex];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var pair in _rarityWeightBuffer)
|
||||
for (int rarityIndex = 0; rarityIndex < _rarityWeightBuffer.Length; rarityIndex++)
|
||||
{
|
||||
return pair.Key;
|
||||
if (_rarityWeightBuffer[rarityIndex] > 0f)
|
||||
{
|
||||
return OrderedRarities[rarityIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return RarityType.None;
|
||||
}
|
||||
|
||||
private static bool IsEligibleAtPhase(DROutGameDropPool row, int displayPhaseIndex)
|
||||
{
|
||||
for (int rarityIndex = 0; rarityIndex < OrderedRarities.Length; rarityIndex++)
|
||||
{
|
||||
RarityType rarity = OrderedRarities[rarityIndex];
|
||||
if (IsEligibleAtPhase(row, rarity, displayPhaseIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsEligibleAtPhase(DROutGameDropPool row, RarityType rarity, int displayPhaseIndex)
|
||||
{
|
||||
if (row.GetWeight(rarity) <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int minPhase = row.GetMinPhase(rarity);
|
||||
int maxPhase = row.GetMaxPhase(rarity);
|
||||
return displayPhaseIndex >= minPhase && displayPhaseIndex <= maxPhase;
|
||||
}
|
||||
|
||||
private static float GetRarityCurveWeight(RarityType rarityType, float phaseT)
|
||||
{
|
||||
float hump = Mathf.Exp(-Mathf.Pow((phaseT - 0.35f) / 0.28f, 2f));
|
||||
|
||||
+245
-136
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
@@ -7,22 +6,12 @@ using GeometryTD.Factory;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
using Random = UnityEngine.Random;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class InventoryGenerationComponent : GameFrameworkComponent
|
||||
{
|
||||
private const float DropChanceBase = 0.05f;
|
||||
private const float DropChancePerPhase = 0.2f;
|
||||
private const float DropChanceCap = 0.2f;
|
||||
|
||||
private long _nextTempInstanceId = 1000000;
|
||||
private int _runSeed;
|
||||
private int _nodeSequenceIndex = -1;
|
||||
private int _nextDropTagOrdinal;
|
||||
private int _nextRewardTagOrdinal;
|
||||
|
||||
private readonly List<DRShopPrice> _shopPriceRows = new();
|
||||
private IDataTable<DRShopPrice> _shopPriceTable;
|
||||
private IDataTable<DROutGameDropPool> _dropPoolTable;
|
||||
@@ -32,22 +21,19 @@ namespace GeometryTD.CustomComponent
|
||||
private ShopGoodsBuilder _shopGoodsBuilder;
|
||||
private DropPoolRoller _dropPoolRoller;
|
||||
private RewardCandidateBuilder _rewardCandidateBuilder;
|
||||
|
||||
public void ConfigureRunContext(int runSeed, int sequenceIndex)
|
||||
{
|
||||
_runSeed = runSeed;
|
||||
_nodeSequenceIndex = sequenceIndex;
|
||||
_nextDropTagOrdinal = 0;
|
||||
_nextRewardTagOrdinal = 0;
|
||||
}
|
||||
private OutGameDropItemBuilder _outGameDropItemBuilder;
|
||||
|
||||
public List<GoodsItemRawData> BuildShopGoods(int goodsCount, int runSeed = 0, int sequenceIndex = -1)
|
||||
{
|
||||
EnsureShopBuilder();
|
||||
return _shopGoodsBuilder.BuildGoods(goodsCount, runSeed, sequenceIndex, AllocateTempInstanceId);
|
||||
return _shopGoodsBuilder.BuildGoods(goodsCount, runSeed, sequenceIndex);
|
||||
}
|
||||
|
||||
public EnemyDropResult ResolveEnemyDrop(in EnemyDropContext context)
|
||||
public EnemyDropResult ResolveEnemyDrop(
|
||||
in EnemyDropContext context,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextDropOrdinal)
|
||||
{
|
||||
DREnemy enemy = context.Enemy;
|
||||
if (enemy == null)
|
||||
@@ -61,14 +47,25 @@ namespace GeometryTD.CustomComponent
|
||||
? Mathf.Clamp01(enemy.DropPercent * 0.01f)
|
||||
: Mathf.Clamp01(enemy.DropPercent);
|
||||
|
||||
if (enemy.DropGold > 0 && dropRate > 0f && Random.value <= dropRate)
|
||||
int dropOrdinal = nextDropOrdinal;
|
||||
nextDropOrdinal++;
|
||||
InventoryGenerationRandomContext randomContext =
|
||||
new(runSeed, sequenceIndex, InventoryTagSourceType.Drop, dropOrdinal);
|
||||
Random random = randomContext.CreateRandom();
|
||||
|
||||
if (enemy.DropGold > 0 && dropRate > 0f && random.NextDouble() <= dropRate)
|
||||
{
|
||||
gold = Mathf.Max(0, enemy.DropGold);
|
||||
}
|
||||
|
||||
TowerCompItemData lootItem = null;
|
||||
if (ShouldRollOutGameItem(context.DisplayPhaseIndex) &&
|
||||
TryRollOutGameItem(context.DisplayPhaseIndex, context.ThemeType, out TowerCompItemData droppedItem))
|
||||
if (OutGameDropRuleService.ShouldRollOutGameItem(context.DisplayPhaseIndex, random) &&
|
||||
TryRollOutGameItem(
|
||||
context.DisplayPhaseIndex,
|
||||
context.ThemeType,
|
||||
randomContext,
|
||||
random,
|
||||
out TowerCompItemData droppedItem))
|
||||
{
|
||||
lootItem = droppedItem;
|
||||
}
|
||||
@@ -79,28 +76,104 @@ namespace GeometryTD.CustomComponent
|
||||
public IReadOnlyList<TowerCompItemData> BuildRewardCandidates(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int candidateCount)
|
||||
int candidateCount,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
ref int nextRewardOrdinal)
|
||||
{
|
||||
RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder();
|
||||
return rewardCandidateBuilder.BuildCandidates(
|
||||
int rewardOrdinal = nextRewardOrdinal;
|
||||
IReadOnlyList<TowerCompItemData> candidates = rewardCandidateBuilder.BuildCandidates(
|
||||
displayPhaseIndex,
|
||||
themeType,
|
||||
candidateCount,
|
||||
CreateNextRewardRandomContext,
|
||||
BuildRewardCandidateItem);
|
||||
nextRewardOrdinal = rewardOrdinal;
|
||||
return candidates;
|
||||
|
||||
InventoryGenerationRandomContext CreateNextRewardRandomContext()
|
||||
{
|
||||
return new InventoryGenerationRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
InventoryTagSourceType.Reward,
|
||||
rewardOrdinal++);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildEventRewardComponents(
|
||||
int count,
|
||||
RarityType rarity,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex)
|
||||
{
|
||||
return BuildEventRewardComponents(
|
||||
count,
|
||||
rarity,
|
||||
rarity,
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
eventId,
|
||||
optionIndex,
|
||||
effectIndex);
|
||||
}
|
||||
|
||||
public IReadOnlyList<TowerCompItemData> BuildEventRewardComponents(
|
||||
int count,
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex)
|
||||
{
|
||||
EnsureComponentTables();
|
||||
|
||||
int resolvedCount = Mathf.Max(0, count);
|
||||
if (resolvedCount <= 0)
|
||||
{
|
||||
return System.Array.Empty<TowerCompItemData>();
|
||||
}
|
||||
|
||||
RarityType normalizedMinRarity = InventoryRarityRuleService.NormalizeComponentRarity(minRarity);
|
||||
RarityType normalizedMaxRarity = InventoryRarityRuleService.NormalizeComponentRarity(maxRarity);
|
||||
if (normalizedMinRarity > normalizedMaxRarity)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
$"Event reward rarity range is invalid: {normalizedMinRarity} > {normalizedMaxRarity}.");
|
||||
}
|
||||
|
||||
List<TowerCompItemData> result = new List<TowerCompItemData>(resolvedCount);
|
||||
for (int i = 0; i < resolvedCount; i++)
|
||||
{
|
||||
InventoryGenerationRandomContext randomContext = CreateEventRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
eventId,
|
||||
optionIndex,
|
||||
effectIndex,
|
||||
i);
|
||||
Random random = randomContext.CreateRandom();
|
||||
result.Add(BuildRandomEventComponentItem(normalizedMinRarity, normalizedMaxRarity, randomContext, random));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void EnsureShopTables()
|
||||
{
|
||||
_shopPriceTable ??= GameEntry.DataTable.GetDataTable<DRShopPrice>();
|
||||
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
||||
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||
EnsureComponentTables();
|
||||
|
||||
if (_shopPriceTable == null || _muzzleCompTable == null || _bearingCompTable == null ||
|
||||
_baseCompTable == null)
|
||||
if (_shopPriceTable == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"InventoryGenerationComponent requires ShopPrice, MuzzleComp, BearingComp, and BaseComp data tables.");
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires ShopPrice data table.");
|
||||
}
|
||||
|
||||
if (_shopPriceRows.Count > 0)
|
||||
@@ -111,7 +184,7 @@ namespace GeometryTD.CustomComponent
|
||||
DRShopPrice[] rows = _shopPriceTable.GetAllDataRows();
|
||||
if (rows == null || rows.Length <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires at least one shop price row.");
|
||||
}
|
||||
|
||||
@@ -125,7 +198,7 @@ namespace GeometryTD.CustomComponent
|
||||
|
||||
if (_shopPriceRows.Count <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("InventoryGenerationComponent requires non-null shop price rows.");
|
||||
throw new System.InvalidOperationException("InventoryGenerationComponent requires non-null shop price rows.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,22 +212,15 @@ namespace GeometryTD.CustomComponent
|
||||
_baseCompTable);
|
||||
}
|
||||
|
||||
private long AllocateTempInstanceId()
|
||||
{
|
||||
return _nextTempInstanceId++;
|
||||
}
|
||||
|
||||
private void EnsureDropTables()
|
||||
{
|
||||
_dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
|
||||
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
||||
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||
EnsureComponentTables();
|
||||
|
||||
if (_dropPoolTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
|
||||
if (_dropPoolTable == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"InventoryGenerationComponent requires OutGameDropPool, MuzzleComp, BearingComp, and BaseComp data tables.");
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires OutGameDropPool data table.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,103 +237,45 @@ namespace GeometryTD.CustomComponent
|
||||
return _rewardCandidateBuilder;
|
||||
}
|
||||
|
||||
private static bool ShouldRollOutGameItem(int displayPhaseIndex)
|
||||
private OutGameDropItemBuilder EnsureOutGameDropItemBuilder()
|
||||
{
|
||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
|
||||
return Random.value <= dropChance;
|
||||
EnsureDropTables();
|
||||
_outGameDropItemBuilder ??= new OutGameDropItemBuilder(
|
||||
_muzzleCompTable,
|
||||
_bearingCompTable,
|
||||
_baseCompTable);
|
||||
return _outGameDropItemBuilder;
|
||||
}
|
||||
|
||||
private bool TryRollOutGameItem(int displayPhaseIndex, LevelThemeType themeType, out TowerCompItemData droppedItem)
|
||||
private bool TryRollOutGameItem(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random,
|
||||
out TowerCompItemData droppedItem)
|
||||
{
|
||||
droppedItem = null;
|
||||
DropPoolRoller dropPoolRoller = EnsureDropPoolRoller();
|
||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||
if (!dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null)
|
||||
if (!dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryBuildDropItem(selectedRow, InventoryTagSourceType.Drop, AllocateDropTagOrdinal(), out droppedItem);
|
||||
return EnsureOutGameDropItemBuilder().TryBuildItem(selectedRow, selectedRarity, randomContext, out droppedItem);
|
||||
}
|
||||
|
||||
private bool TryBuildDropItem(
|
||||
private TowerCompItemData BuildRewardCandidateItem(
|
||||
DROutGameDropPool row,
|
||||
InventoryTagSourceType sourceType,
|
||||
int localOrdinal,
|
||||
out TowerCompItemData droppedItem)
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext)
|
||||
{
|
||||
droppedItem = null;
|
||||
if (row.ItemId <= 0 || string.IsNullOrWhiteSpace(row.ItemType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string itemType = row.ItemType.Trim();
|
||||
if (itemType.Equals("MuzzleComp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DRMuzzleComp config = _muzzleCompTable.GetDataRow(row.ItemId);
|
||||
if (config == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
droppedItem = ComponentItemFactory.CreateMuzzle(
|
||||
config,
|
||||
AllocateTempInstanceId(),
|
||||
row.Rarity,
|
||||
CreateRandomContext(sourceType, localOrdinal, config.Id));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DRBearingComp config = _bearingCompTable.GetDataRow(row.ItemId);
|
||||
if (config == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
droppedItem = ComponentItemFactory.CreateBearing(
|
||||
config,
|
||||
AllocateTempInstanceId(),
|
||||
row.Rarity,
|
||||
CreateRandomContext(sourceType, localOrdinal, config.Id));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DRBaseComp config = _baseCompTable.GetDataRow(row.ItemId);
|
||||
if (config == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
droppedItem = ComponentItemFactory.CreateBase(
|
||||
config,
|
||||
AllocateTempInstanceId(),
|
||||
row.Rarity,
|
||||
CreateRandomContext(sourceType, localOrdinal, config.Id));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private int AllocateDropTagOrdinal()
|
||||
{
|
||||
return _nextDropTagOrdinal++;
|
||||
}
|
||||
|
||||
private int AllocateRewardTagOrdinal()
|
||||
{
|
||||
return _nextRewardTagOrdinal++;
|
||||
}
|
||||
|
||||
private TowerCompItemData BuildRewardCandidateItem(DROutGameDropPool row)
|
||||
{
|
||||
if (!TryBuildDropItem(row, InventoryTagSourceType.Reward, AllocateRewardTagOrdinal(), out TowerCompItemData droppedItem))
|
||||
if (!EnsureOutGameDropItemBuilder().TryBuildItem(row, rarity, randomContext, out TowerCompItemData droppedItem))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -275,16 +283,117 @@ namespace GeometryTD.CustomComponent
|
||||
return droppedItem;
|
||||
}
|
||||
|
||||
private InventoryTagRandomContext CreateRandomContext(
|
||||
InventoryTagSourceType sourceType,
|
||||
int localOrdinal,
|
||||
int configId)
|
||||
private void EnsureComponentTables()
|
||||
{
|
||||
return sourceType switch
|
||||
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
|
||||
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||
|
||||
if (_muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
|
||||
{
|
||||
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(_runSeed, _nodeSequenceIndex, localOrdinal, configId),
|
||||
_ => InventoryTagRandomContext.CreateDrop(_runSeed, _nodeSequenceIndex, localOrdinal, configId)
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires MuzzleComp, BearingComp, and BaseComp data tables.");
|
||||
}
|
||||
}
|
||||
|
||||
private TowerCompItemData BuildRandomEventComponentItem(
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
RarityType rarity = ResolveEventRewardRarity(minRarity, maxRarity, random);
|
||||
int slotRoll = random.Next(0, 3);
|
||||
return slotRoll switch
|
||||
{
|
||||
0 => BuildRandomEventMuzzleItem(rarity, randomContext, random),
|
||||
1 => BuildRandomEventBearingItem(rarity, randomContext, random),
|
||||
_ => BuildRandomEventBaseItem(rarity, randomContext, random)
|
||||
};
|
||||
}
|
||||
|
||||
private static RarityType ResolveEventRewardRarity(
|
||||
RarityType minRarity,
|
||||
RarityType maxRarity,
|
||||
Random random)
|
||||
{
|
||||
if (minRarity >= maxRarity)
|
||||
{
|
||||
return minRarity;
|
||||
}
|
||||
|
||||
int rarityValue = random.Next((int)minRarity, (int)maxRarity + 1);
|
||||
return (RarityType)rarityValue;
|
||||
}
|
||||
|
||||
private MuzzleCompItemData BuildRandomEventMuzzleItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
|
||||
DRMuzzleComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateMuzzle(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BearingCompItemData BuildRandomEventBearingItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
|
||||
DRBearingComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateBearing(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BaseCompItemData BuildRandomEventBaseItem(
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
|
||||
DRBaseComp config = rows[random.Next(0, rows.Length)];
|
||||
return ComponentItemFactory.CreateBase(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private static InventoryGenerationRandomContext CreateEventRandomContext(
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex,
|
||||
int itemIndex)
|
||||
{
|
||||
return new InventoryGenerationRandomContext(
|
||||
runSeed,
|
||||
sequenceIndex,
|
||||
InventoryTagSourceType.Event,
|
||||
BuildEventLocalOrdinal(eventId, optionIndex, effectIndex, itemIndex));
|
||||
}
|
||||
|
||||
private static int BuildEventLocalOrdinal(int eventId, int optionIndex, int effectIndex, int itemIndex)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int value = 17;
|
||||
value = value * 31 + eventId;
|
||||
value = value * 31 + optionIndex;
|
||||
value = value * 31 + effectIndex;
|
||||
value = value * 31 + itemIndex;
|
||||
return value & int.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.Definition
|
||||
{
|
||||
public readonly struct InventoryGenerationRandomContext
|
||||
{
|
||||
public InventoryGenerationRandomContext(
|
||||
int runSeed,
|
||||
int nodeSequenceIndex,
|
||||
InventoryTagSourceType sourceType,
|
||||
int localOrdinal)
|
||||
{
|
||||
RunSeed = runSeed;
|
||||
NodeSequenceIndex = nodeSequenceIndex;
|
||||
SourceType = sourceType;
|
||||
LocalOrdinal = localOrdinal;
|
||||
}
|
||||
|
||||
public int RunSeed { get; }
|
||||
|
||||
public int NodeSequenceIndex { get; }
|
||||
|
||||
public InventoryTagSourceType SourceType { get; }
|
||||
|
||||
public int LocalOrdinal { get; }
|
||||
|
||||
public Random CreateRandom()
|
||||
{
|
||||
return new Random(BuildSeed());
|
||||
}
|
||||
|
||||
public long CreateStableItemInstanceId()
|
||||
{
|
||||
long normalizedSource = ((long)Math.Max(0, (int)SourceType) + 1L) << 48;
|
||||
long normalizedSequence = ((long)Math.Max(0, NodeSequenceIndex) + 1L) << 24;
|
||||
long normalizedOrdinal = (uint)(Math.Max(0, LocalOrdinal) + 1);
|
||||
return normalizedSource | normalizedSequence | normalizedOrdinal;
|
||||
}
|
||||
|
||||
public InventoryTagRandomContext CreateTagRandomContext(int configId)
|
||||
{
|
||||
return SourceType switch
|
||||
{
|
||||
InventoryTagSourceType.Shop => InventoryTagRandomContext.CreateShop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
InventoryTagSourceType.Event => InventoryTagRandomContext.CreateEvent(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
|
||||
_ => InventoryTagRandomContext.CreateDrop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId)
|
||||
};
|
||||
}
|
||||
|
||||
private int BuildSeed()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int seed = 17;
|
||||
seed = seed * 31 + RunSeed;
|
||||
seed = seed * 31 + NodeSequenceIndex;
|
||||
seed = seed * 31 + (int)SourceType;
|
||||
seed = seed * 31 + LocalOrdinal;
|
||||
return seed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95b2ab672a7049ef9356a90d87baca50
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public static class OutGameDropRuleService
|
||||
{
|
||||
private const float DropChanceBase = 0.05f;
|
||||
private const float DropChancePerPhase = 0.2f;
|
||||
private const float DropChanceCap = 0.2f;
|
||||
|
||||
public static bool ShouldRollOutGameItem(int displayPhaseIndex, Random random)
|
||||
{
|
||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
|
||||
return random.NextDouble() <= dropChance;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 519549c7542f40efabf83a2b1b2dff44
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+28
-7
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
@@ -19,7 +20,8 @@ namespace GeometryTD.CustomComponent
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int candidateCount,
|
||||
Func<DROutGameDropPool, TowerCompItemData> buildRewardItem)
|
||||
Func<InventoryGenerationRandomContext> createRandomContext,
|
||||
Func<DROutGameDropPool, RarityType, InventoryGenerationRandomContext, TowerCompItemData> buildRewardItem)
|
||||
{
|
||||
int resolvedCount = Mathf.Max(0, candidateCount);
|
||||
if (resolvedCount <= 0)
|
||||
@@ -28,7 +30,7 @@ namespace GeometryTD.CustomComponent
|
||||
}
|
||||
|
||||
List<TowerCompItemData> candidates = new List<TowerCompItemData>(resolvedCount);
|
||||
HashSet<int> selectedPoolRowIds = new HashSet<int>();
|
||||
HashSet<long> selectedPoolEntryKeys = new HashSet<long>();
|
||||
int maxAttempts = Mathf.Max(resolvedCount * 6, resolvedCount);
|
||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||
|
||||
@@ -36,17 +38,24 @@ namespace GeometryTD.CustomComponent
|
||||
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
||||
{
|
||||
attempts++;
|
||||
if (!_dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null)
|
||||
InventoryGenerationRandomContext randomContext = createRandomContext();
|
||||
Random random = randomContext.CreateRandom();
|
||||
if (!_dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!selectedPoolRowIds.Add(selectedRow.Id))
|
||||
if (!selectedPoolEntryKeys.Add(BuildSelectionKey(selectedRow.Id, selectedRarity)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow);
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
|
||||
if (candidate == null)
|
||||
{
|
||||
continue;
|
||||
@@ -59,12 +68,19 @@ namespace GeometryTD.CustomComponent
|
||||
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
||||
{
|
||||
attempts++;
|
||||
if (!_dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null)
|
||||
InventoryGenerationRandomContext randomContext = createRandomContext();
|
||||
Random random = randomContext.CreateRandom();
|
||||
if (!_dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow);
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
|
||||
if (candidate == null)
|
||||
{
|
||||
continue;
|
||||
@@ -75,5 +91,10 @@ namespace GeometryTD.CustomComponent
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static long BuildSelectionKey(int rowId, RarityType rarity)
|
||||
{
|
||||
return ((long)rowId << 32) | (uint)rarity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using GeometryTD.Definition;
|
||||
using GeometryTD.Factory;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
@@ -32,8 +33,7 @@ namespace GeometryTD.CustomComponent
|
||||
public List<GoodsItemRawData> BuildGoods(
|
||||
int goodsCount,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
int sequenceIndex)
|
||||
{
|
||||
if (goodsCount <= 0)
|
||||
{
|
||||
@@ -43,7 +43,9 @@ namespace GeometryTD.CustomComponent
|
||||
List<GoodsItemRawData> goodsItems = new(goodsCount);
|
||||
for (int i = 0; i < goodsCount; i++)
|
||||
{
|
||||
goodsItems.Add(BuildGoodsItem(i, runSeed, sequenceIndex, allocateInstanceId));
|
||||
InventoryGenerationRandomContext randomContext =
|
||||
new(runSeed, sequenceIndex, InventoryTagSourceType.Shop, i);
|
||||
goodsItems.Add(BuildGoodsItem(i, randomContext));
|
||||
}
|
||||
|
||||
return goodsItems;
|
||||
@@ -51,18 +53,17 @@ namespace GeometryTD.CustomComponent
|
||||
|
||||
private GoodsItemRawData BuildGoodsItem(
|
||||
int goodsIndex,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
InventoryGenerationRandomContext randomContext)
|
||||
{
|
||||
TowerCompItemData sourceItem = BuildRandomComponentItem(goodsIndex, runSeed, sequenceIndex, allocateInstanceId);
|
||||
Random random = randomContext.CreateRandom();
|
||||
TowerCompItemData sourceItem = BuildRandomComponentItem(randomContext, random);
|
||||
return new GoodsItemRawData
|
||||
{
|
||||
GoodsIndex = goodsIndex,
|
||||
Title = sourceItem.Name,
|
||||
TypeText = BuildTypeText(sourceItem.SlotType),
|
||||
Description = BuildDescription(sourceItem),
|
||||
Price = ResolveRandomPrice(sourceItem.Rarity),
|
||||
Price = ResolveRandomPrice(sourceItem.Rarity, random),
|
||||
Tags = sourceItem.Tags != null ? (TagType[])sourceItem.Tags.Clone() : Array.Empty<TagType>(),
|
||||
IconAreaContext = BuildIconAreaContext(sourceItem),
|
||||
SourceItem = sourceItem,
|
||||
@@ -71,83 +72,58 @@ namespace GeometryTD.CustomComponent
|
||||
}
|
||||
|
||||
private TowerCompItemData BuildRandomComponentItem(
|
||||
int goodsIndex,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
int slotRoll = UnityEngine.Random.Range(0, 3);
|
||||
DRShopPrice priceRow = _shopPriceRows[UnityEngine.Random.Range(0, _shopPriceRows.Count)];
|
||||
int slotRoll = random.Next(0, 3);
|
||||
DRShopPrice priceRow = _shopPriceRows[random.Next(0, _shopPriceRows.Count)];
|
||||
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(
|
||||
priceRow != null ? priceRow.Rarity : RarityType.White);
|
||||
|
||||
return slotRoll switch
|
||||
{
|
||||
0 => BuildRandomMuzzleItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId),
|
||||
1 => BuildRandomBearingItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId),
|
||||
_ => BuildRandomBaseItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId)
|
||||
0 => BuildRandomMuzzleItem(rarity, randomContext, random),
|
||||
1 => BuildRandomBearingItem(rarity, randomContext, random),
|
||||
_ => BuildRandomBaseItem(rarity, randomContext, random)
|
||||
};
|
||||
}
|
||||
|
||||
private MuzzleCompItemData BuildRandomMuzzleItem(
|
||||
RarityType rarity,
|
||||
int goodsIndex,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
|
||||
DRMuzzleComp config = rows[UnityEngine.Random.Range(0, rows.Length)];
|
||||
long instanceId = allocateInstanceId();
|
||||
InventoryTagRandomContext randomContext =
|
||||
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
|
||||
return ComponentItemFactory.CreateMuzzle(config, instanceId, rarity, randomContext);
|
||||
DRMuzzleComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateMuzzle(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BearingCompItemData BuildRandomBearingItem(
|
||||
RarityType rarity,
|
||||
int goodsIndex,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
|
||||
DRBearingComp config = rows[UnityEngine.Random.Range(0, rows.Length)];
|
||||
long instanceId = allocateInstanceId();
|
||||
InventoryTagRandomContext randomContext =
|
||||
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
|
||||
return ComponentItemFactory.CreateBearing(config, instanceId, rarity, randomContext);
|
||||
DRBearingComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateBearing(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BaseCompItemData BuildRandomBaseItem(
|
||||
RarityType rarity,
|
||||
int goodsIndex,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
|
||||
DRBaseComp config = rows[UnityEngine.Random.Range(0, rows.Length)];
|
||||
long instanceId = allocateInstanceId();
|
||||
InventoryTagRandomContext randomContext =
|
||||
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
|
||||
return ComponentItemFactory.CreateBase(config, instanceId, rarity, randomContext);
|
||||
DRBaseComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateBase(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private int ResolveRandomPrice(RarityType rarity)
|
||||
private int ResolveRandomPrice(RarityType rarity, Random random)
|
||||
{
|
||||
for (int i = 0; i < _shopPriceRows.Count; i++)
|
||||
{
|
||||
DRShopPrice row = _shopPriceRows[i];
|
||||
if (row != null && row.Rarity == rarity)
|
||||
{
|
||||
int min = Mathf.Max(0, row.MinPrice);
|
||||
int max = Mathf.Max(min, row.MaxPrice);
|
||||
return UnityEngine.Random.Range(min, max + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return ShopPriceRuleService.ResolveRandomBuyPrice(_shopPriceRows, rarity, random);
|
||||
}
|
||||
|
||||
private static IconAreaContext BuildIconAreaContext(TowerCompItemData item)
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace GeometryTD.CustomComponent
|
||||
private PlayerInventoryCommandModel _commandModel;
|
||||
private PlayerInventoryTowerRosterService _towerRosterService;
|
||||
private PlayerInventoryTowerAssemblyService _towerAssemblyService;
|
||||
private PlayerInventoryTradeService _tradeService;
|
||||
|
||||
public int Gold
|
||||
{
|
||||
@@ -100,6 +101,12 @@ namespace GeometryTD.CustomComponent
|
||||
return _commandModel.TryConsumeGold(costGold);
|
||||
}
|
||||
|
||||
public bool TryPurchaseComponent(TowerCompItemData item, int price)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TryPurchaseComponent(item, price);
|
||||
}
|
||||
|
||||
public void AddGold(int gainGold)
|
||||
{
|
||||
EnsureInitialized();
|
||||
@@ -138,6 +145,24 @@ namespace GeometryTD.CustomComponent
|
||||
return _towerRosterService.ReduceTowerEndurance(towerInstanceIds, enduranceLoss);
|
||||
}
|
||||
|
||||
public bool TryDisassembleTower(long towerInstanceId)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _towerAssemblyService.TryDisassembleTower(towerInstanceId);
|
||||
}
|
||||
|
||||
public bool TryGetSaleCandidate(long itemId, out PlayerInventorySaleCandidate candidate)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TryGetSaleCandidate(itemId, out candidate);
|
||||
}
|
||||
|
||||
public bool TrySellItems(IReadOnlyCollection<long> itemIds, out PlayerInventorySaleResult result)
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _tradeService.TrySellItems(itemIds, out result);
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_queryModel.IsInitialized)
|
||||
@@ -155,6 +180,7 @@ namespace GeometryTD.CustomComponent
|
||||
_commandModel ??= new PlayerInventoryCommandModel(_state);
|
||||
_towerRosterService ??= new PlayerInventoryTowerRosterService(_queryModel, MaxParticipantTowerCount);
|
||||
_towerAssemblyService ??= new PlayerInventoryTowerAssemblyService(_queryModel, _commandModel);
|
||||
_tradeService ??= new PlayerInventoryTradeService(_queryModel, _commandModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+34
-2
@@ -123,7 +123,7 @@ namespace GeometryTD.CustomComponent
|
||||
int[] values = new int[TowerLevelCount];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = baseValue + perLevel * i;
|
||||
values[i] = Mathf.Max(0, baseValue + perLevel * i);
|
||||
}
|
||||
|
||||
return values;
|
||||
@@ -135,7 +135,7 @@ namespace GeometryTD.CustomComponent
|
||||
float[] values = new float[TowerLevelCount];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i] = baseValue + perLevel * i;
|
||||
values[i] = Mathf.Max(0f, baseValue + perLevel * i);
|
||||
}
|
||||
|
||||
return values;
|
||||
@@ -180,5 +180,37 @@ namespace GeometryTD.CustomComponent
|
||||
_drBaseComp ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
|
||||
return _drBaseComp;
|
||||
}
|
||||
|
||||
public bool TryDisassembleTower(long towerInstanceId)
|
||||
{
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (towerInstanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_queryModel.TryGetTowerById(towerInstanceId, out TowerItemData tower) || tower == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_queryModel.TryGetComponentById(inventory.MuzzleComponents, tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComp) ||
|
||||
!_queryModel.TryGetComponentById(inventory.BearingComponents, tower.BearingComponentInstanceId, out BearingCompItemData bearingComp) ||
|
||||
!_queryModel.TryGetComponentById(inventory.BaseComponents, tower.BaseComponentInstanceId, out BaseCompItemData baseComp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
InventoryParticipantUtility.TryRemoveParticipantTower(inventory, towerInstanceId, MaxParticipantTowerCount);
|
||||
|
||||
muzzleComp.IsAssembledIntoTower = false;
|
||||
bearingComp.IsAssembledIntoTower = false;
|
||||
baseComp.IsAssembledIntoTower = false;
|
||||
|
||||
inventory.Towers.Remove(tower);
|
||||
return true;
|
||||
}
|
||||
|
||||
private const int MaxParticipantTowerCount = 4;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-5
@@ -35,14 +35,27 @@ namespace GeometryTD.CustomComponent
|
||||
}
|
||||
|
||||
public int ReduceTowerEndurance(IReadOnlyList<long> towerInstanceIds, float enduranceLoss)
|
||||
{
|
||||
return InventoryTowerEnduranceUtility.ReduceTowerEndurance(
|
||||
_queryModel.Inventory,
|
||||
towerInstanceIds,
|
||||
enduranceLoss);
|
||||
}
|
||||
}
|
||||
|
||||
public static class InventoryTowerEnduranceUtility
|
||||
{
|
||||
public static int ReduceTowerEndurance(
|
||||
BackpackInventoryData inventory,
|
||||
IReadOnlyList<long> towerInstanceIds,
|
||||
float enduranceLoss)
|
||||
{
|
||||
float resolvedLoss = Mathf.Max(0f, enduranceLoss);
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (resolvedLoss <= 0f ||
|
||||
if (inventory?.Towers == null ||
|
||||
inventory.Towers.Count <= 0 ||
|
||||
resolvedLoss <= 0f ||
|
||||
towerInstanceIds == null ||
|
||||
towerInstanceIds.Count <= 0 ||
|
||||
inventory.Towers == null ||
|
||||
inventory.Towers.Count <= 0)
|
||||
towerInstanceIds.Count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public enum PlayerInventorySaleFailureReason : byte
|
||||
{
|
||||
None = 0,
|
||||
InvalidSelection = 1,
|
||||
ItemNotFound = 2,
|
||||
AssembledComponent = 3,
|
||||
ParticipantTower = 4,
|
||||
MissingTowerComponent = 5
|
||||
}
|
||||
|
||||
public sealed class PlayerInventorySaleCandidate
|
||||
{
|
||||
public long ItemId;
|
||||
public bool IsSellable;
|
||||
public bool IsTower;
|
||||
public int Price;
|
||||
public PlayerInventorySaleFailureReason FailureReason;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventorySaleResult
|
||||
{
|
||||
public int GainedGold;
|
||||
public int SoldComponentCount;
|
||||
public int SoldTowerCount;
|
||||
public PlayerInventorySaleFailureReason FailureReason;
|
||||
|
||||
public bool IsSuccess => FailureReason == PlayerInventorySaleFailureReason.None;
|
||||
public int SoldItemCount => SoldComponentCount + SoldTowerCount;
|
||||
}
|
||||
|
||||
public sealed class PlayerInventoryTradeService
|
||||
{
|
||||
private readonly PlayerInventoryQueryModel _queryModel;
|
||||
private readonly PlayerInventoryCommandModel _commandModel;
|
||||
private IDataTable<DRShopPrice> _shopPriceTable;
|
||||
|
||||
public PlayerInventoryTradeService(
|
||||
PlayerInventoryQueryModel queryModel,
|
||||
PlayerInventoryCommandModel commandModel,
|
||||
IDataTable<DRShopPrice> shopPriceTable = null)
|
||||
{
|
||||
_queryModel = queryModel;
|
||||
_commandModel = commandModel;
|
||||
_shopPriceTable = shopPriceTable;
|
||||
}
|
||||
|
||||
public bool TryPurchaseComponent(TowerCompItemData item, int price)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_commandModel.TryConsumeGold(price))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData inventoryDelta = WrapSingleItem(item);
|
||||
PlayerInventoryMergeSummary summary = _commandModel.MergeInventory(inventoryDelta);
|
||||
return summary.HasAnyGain;
|
||||
}
|
||||
|
||||
public bool TryGetSaleCandidate(long itemId, out PlayerInventorySaleCandidate candidate)
|
||||
{
|
||||
candidate = BuildSaleCandidate(itemId);
|
||||
return candidate != null;
|
||||
}
|
||||
|
||||
public bool TrySellItems(IReadOnlyCollection<long> itemIds, out PlayerInventorySaleResult result)
|
||||
{
|
||||
result = new PlayerInventorySaleResult();
|
||||
if (itemIds == null || itemIds.Count <= 0)
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.InvalidSelection;
|
||||
return false;
|
||||
}
|
||||
|
||||
HashSet<long> uniqueIds = new HashSet<long>();
|
||||
List<PlayerInventorySaleCandidate> candidates = new List<PlayerInventorySaleCandidate>(itemIds.Count);
|
||||
foreach (long itemId in itemIds)
|
||||
{
|
||||
if (itemId <= 0 || !uniqueIds.Add(itemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PlayerInventorySaleCandidate candidate = BuildSaleCandidate(itemId);
|
||||
if (candidate == null || !candidate.IsSellable)
|
||||
{
|
||||
result.FailureReason = candidate?.FailureReason ?? PlayerInventorySaleFailureReason.ItemNotFound;
|
||||
return false;
|
||||
}
|
||||
|
||||
candidates.Add(candidate);
|
||||
}
|
||||
|
||||
if (candidates.Count <= 0)
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.InvalidSelection;
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
PlayerInventorySaleCandidate candidate = candidates[i];
|
||||
if (candidate.IsTower)
|
||||
{
|
||||
if (!TryRemoveTower(inventory, candidate.ItemId))
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.MissingTowerComponent;
|
||||
return false;
|
||||
}
|
||||
|
||||
result.SoldTowerCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryRemoveComponent(inventory, candidate.ItemId))
|
||||
{
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.ItemNotFound;
|
||||
return false;
|
||||
}
|
||||
|
||||
result.SoldComponentCount++;
|
||||
}
|
||||
|
||||
result.GainedGold += Mathf.Max(0, candidate.Price);
|
||||
}
|
||||
|
||||
_commandModel.AddGold(result.GainedGold);
|
||||
result.FailureReason = PlayerInventorySaleFailureReason.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
private PlayerInventorySaleCandidate BuildSaleCandidate(long itemId)
|
||||
{
|
||||
if (itemId <= 0)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.InvalidSelection
|
||||
};
|
||||
}
|
||||
|
||||
BackpackInventoryData inventory = _queryModel.Inventory;
|
||||
if (inventory == null)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
if (_queryModel.TryGetTowerById(itemId, out TowerItemData tower) && tower != null)
|
||||
{
|
||||
if (inventory.ParticipantTowerInstanceIds != null && inventory.ParticipantTowerInstanceIds.Contains(itemId))
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
IsTower = true,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ParticipantTower
|
||||
};
|
||||
}
|
||||
|
||||
if (!ShopPriceRuleService.TryResolveTowerSalePrice(tower, inventory, out int towerPrice, EnsureShopPriceTable()))
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
IsTower = true,
|
||||
FailureReason = PlayerInventorySaleFailureReason.MissingTowerComponent
|
||||
};
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = true,
|
||||
IsTower = true,
|
||||
Price = towerPrice,
|
||||
FailureReason = PlayerInventorySaleFailureReason.None
|
||||
};
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.MuzzleComponents, itemId, out MuzzleCompItemData muzzleComp))
|
||||
{
|
||||
return BuildComponentCandidate(muzzleComp);
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.BearingComponents, itemId, out BearingCompItemData bearingComp))
|
||||
{
|
||||
return BuildComponentCandidate(bearingComp);
|
||||
}
|
||||
|
||||
if (TryGetComponentById(inventory.BaseComponents, itemId, out BaseCompItemData baseComp))
|
||||
{
|
||||
return BuildComponentCandidate(baseComp);
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = itemId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
private PlayerInventorySaleCandidate BuildComponentCandidate(TowerCompItemData component)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.ItemNotFound
|
||||
};
|
||||
}
|
||||
|
||||
if (component.IsAssembledIntoTower)
|
||||
{
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = component.InstanceId,
|
||||
IsSellable = false,
|
||||
FailureReason = PlayerInventorySaleFailureReason.AssembledComponent
|
||||
};
|
||||
}
|
||||
|
||||
return new PlayerInventorySaleCandidate
|
||||
{
|
||||
ItemId = component.InstanceId,
|
||||
IsSellable = true,
|
||||
IsTower = false,
|
||||
Price = ShopPriceRuleService.ResolveComponentSalePrice(component, EnsureShopPriceTable()),
|
||||
FailureReason = PlayerInventorySaleFailureReason.None
|
||||
};
|
||||
}
|
||||
|
||||
private bool TryRemoveTower(BackpackInventoryData inventory, long towerId)
|
||||
{
|
||||
if (inventory?.Towers == null || towerId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TowerItemData targetTower = null;
|
||||
for (int i = 0; i < inventory.Towers.Count; i++)
|
||||
{
|
||||
TowerItemData tower = inventory.Towers[i];
|
||||
if (tower != null && tower.InstanceId == towerId)
|
||||
{
|
||||
targetTower = tower;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetTower == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ContainsInstanceId(inventory.MuzzleComponents, targetTower.MuzzleComponentInstanceId) ||
|
||||
!ContainsInstanceId(inventory.BearingComponents, targetTower.BearingComponentInstanceId) ||
|
||||
!ContainsInstanceId(inventory.BaseComponents, targetTower.BaseComponentInstanceId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool removedMuzzle = RemoveByInstanceId(inventory.MuzzleComponents, targetTower.MuzzleComponentInstanceId);
|
||||
bool removedBearing = RemoveByInstanceId(inventory.BearingComponents, targetTower.BearingComponentInstanceId);
|
||||
bool removedBase = RemoveByInstanceId(inventory.BaseComponents, targetTower.BaseComponentInstanceId);
|
||||
if (!removedMuzzle || !removedBearing || !removedBase)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
inventory.Towers.Remove(targetTower);
|
||||
inventory.ParticipantTowerInstanceIds?.Remove(towerId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryRemoveComponent(BackpackInventoryData inventory, long itemId)
|
||||
{
|
||||
return RemoveByInstanceId(inventory?.MuzzleComponents, itemId) ||
|
||||
RemoveByInstanceId(inventory?.BearingComponents, itemId) ||
|
||||
RemoveByInstanceId(inventory?.BaseComponents, itemId);
|
||||
}
|
||||
|
||||
private static bool RemoveByInstanceId<TItem>(List<TItem> items, long instanceId)
|
||||
where TItem : class
|
||||
{
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
switch (items[i])
|
||||
{
|
||||
case TowerCompItemData component when component.InstanceId == instanceId:
|
||||
items.RemoveAt(i);
|
||||
return true;
|
||||
case TowerItemData tower when tower.InstanceId == instanceId:
|
||||
items.RemoveAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ContainsInstanceId<TItem>(IReadOnlyList<TItem> items, long instanceId)
|
||||
where TItem : class
|
||||
{
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
switch (items[i])
|
||||
{
|
||||
case TowerCompItemData component when component.InstanceId == instanceId:
|
||||
return true;
|
||||
case TowerItemData tower when tower.InstanceId == instanceId:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetComponentById<TComp>(IReadOnlyList<TComp> items, long instanceId, out TComp result)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
result = null;
|
||||
if (items == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
TComp item = items[i];
|
||||
if (item != null && item.InstanceId == instanceId)
|
||||
{
|
||||
result = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static BackpackInventoryData WrapSingleItem(TowerCompItemData item)
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData();
|
||||
switch (item)
|
||||
{
|
||||
case MuzzleCompItemData muzzleComp:
|
||||
inventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp));
|
||||
break;
|
||||
case BearingCompItemData bearingComp:
|
||||
inventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
|
||||
break;
|
||||
case BaseCompItemData baseComp:
|
||||
inventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
|
||||
break;
|
||||
}
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private IDataTable<DRShopPrice> EnsureShopPriceTable()
|
||||
{
|
||||
_shopPriceTable ??= GameEntry.DataTable.GetDataTable<DRShopPrice>();
|
||||
return _shopPriceTable;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 298cf9214556453cb5e7ec7c0605c350
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -8,11 +8,7 @@ namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public class ShopNodeComponent : GameFrameworkComponent
|
||||
{
|
||||
private string _activeRunId;
|
||||
private int _activeRunSeed;
|
||||
private int _activeNodeId;
|
||||
private RunNodeType _activeNodeType = RunNodeType.None;
|
||||
private int _activeSequenceIndex = -1;
|
||||
private RunNodeExecutionContext _activeContext;
|
||||
|
||||
private ShopFormUseCase _shopFormUseCase;
|
||||
private bool _initialized;
|
||||
@@ -29,26 +25,30 @@ namespace GeometryTD.CustomComponent
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public void StartShop(string runId = null, int runSeed = 0, int nodeId = 0, RunNodeType nodeType = RunNodeType.None, int sequenceIndex = -1)
|
||||
public void StartShop(RunNodeExecutionContext context = null)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
OnInit();
|
||||
}
|
||||
|
||||
int runSeed = context?.RunSeed ?? 0;
|
||||
int sequenceIndex = context?.SequenceIndex ?? -1;
|
||||
if (_shopFormUseCase == null || !_shopFormUseCase.PrepareForOpen(runSeed, sequenceIndex))
|
||||
{
|
||||
Log.Warning("ShopNodeComponent.StartShop() failed. Shop use case is unavailable or goods generation failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
_activeRunId = runId;
|
||||
_activeRunSeed = runSeed;
|
||||
_activeNodeId = nodeId;
|
||||
_activeNodeType = nodeType;
|
||||
_activeSequenceIndex = sequenceIndex;
|
||||
_activeContext = context != null ? context.Clone() : null;
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.ShopForm);
|
||||
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create(runId, nodeId, nodeType, sequenceIndex));
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeEnterEventArgs.Create(
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1));
|
||||
}
|
||||
|
||||
public void EndShop()
|
||||
@@ -57,23 +57,22 @@ namespace GeometryTD.CustomComponent
|
||||
GameEntry.Event.Fire(
|
||||
this,
|
||||
NodeCompleteEventArgs.Create(
|
||||
_activeRunId,
|
||||
_activeNodeId,
|
||||
_activeNodeType,
|
||||
_activeSequenceIndex,
|
||||
_activeContext?.RunId,
|
||||
_activeContext?.NodeId ?? 0,
|
||||
_activeContext?.NodeType ?? RunNodeType.None,
|
||||
_activeContext?.SequenceIndex ?? -1,
|
||||
RunNodeCompletionStatus.Completed,
|
||||
true,
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null));
|
||||
_activeContext != null
|
||||
? _activeContext.CreateCompletionSnapshot(
|
||||
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
|
||||
: null));
|
||||
ClearActiveNodeContext();
|
||||
}
|
||||
|
||||
private void ClearActiveNodeContext()
|
||||
{
|
||||
_activeRunId = null;
|
||||
_activeRunSeed = 0;
|
||||
_activeNodeId = 0;
|
||||
_activeNodeType = RunNodeType.None;
|
||||
_activeSequenceIndex = -1;
|
||||
_activeContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 523d63397a9c41fdaaf238bfb0e95357
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
public sealed class TagRegistryComponent : GameFrameworkComponent
|
||||
{
|
||||
private IDataTable<DRTagConfig> _tagConfigTable;
|
||||
private IDataTable<DRTag> _tagTable;
|
||||
private IDataTable<DRRarityTagBudget> _rarityTagBudgetTable;
|
||||
|
||||
public void OnInit()
|
||||
{
|
||||
ReloadAllFromLoadedTables();
|
||||
}
|
||||
|
||||
public void ReloadAllFromLoadedTables()
|
||||
{
|
||||
ReloadTagDefinitionsAndGenerationFromLoadedTables();
|
||||
ReloadRarityTagBudgetFromLoadedTable();
|
||||
}
|
||||
|
||||
public void ReloadTagDefinitionsAndGenerationFromLoadedTables()
|
||||
{
|
||||
EnsureTagDefinitionTables();
|
||||
DRTagConfig[] tagConfigRows = _tagConfigTable.GetAllDataRows();
|
||||
DRTag[] tagRows = _tagTable.GetAllDataRows();
|
||||
|
||||
TagGenerationRuleRegistry.LoadFromRows(tagRows);
|
||||
TagDefinitionRegistry.ReloadFromRows(tagConfigRows, tagRows);
|
||||
}
|
||||
|
||||
public void ReloadRarityTagBudgetFromLoadedTable()
|
||||
{
|
||||
EnsureRarityTagBudgetTable();
|
||||
RarityTagBudgetRuleRegistry.LoadFromRows(_rarityTagBudgetTable.GetAllDataRows());
|
||||
}
|
||||
|
||||
private void EnsureTagDefinitionTables()
|
||||
{
|
||||
_tagConfigTable ??= GameEntry.DataTable.GetDataTable<DRTagConfig>();
|
||||
_tagTable ??= GameEntry.DataTable.GetDataTable<DRTag>();
|
||||
|
||||
if (_tagConfigTable == null || _tagTable == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"TagRegistryComponent requires TagConfig and Tag data tables to be loaded before initialization.");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureRarityTagBudgetTable()
|
||||
{
|
||||
_rarityTagBudgetTable ??= GameEntry.DataTable.GetDataTable<DRRarityTagBudget>();
|
||||
if (_rarityTagBudgetTable == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"TagRegistryComponent requires RarityTagBudget data table to be loaded before initialization.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a268c57d4373494ab9b4d7167099f170
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,4 +1,5 @@
|
||||
using UnityGameFramework.Runtime;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
{
|
||||
@@ -30,6 +31,14 @@ namespace GeometryTD.DataTable
|
||||
/// <remarks>原始字符串(如 JSON 文本),不在此处做解析。</remarks>
|
||||
public string OptionsRaw { get; private set; }
|
||||
|
||||
public string Option1Raw { get; private set; }
|
||||
|
||||
public string Option2Raw { get; private set; }
|
||||
|
||||
public string Option3Raw { get; private set; }
|
||||
|
||||
public string Option4Raw { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
|
||||
@@ -44,9 +53,30 @@ namespace GeometryTD.DataTable
|
||||
index++;
|
||||
Title = columnStrings[index++];
|
||||
Description = columnStrings[index++];
|
||||
OptionsRaw = columnStrings[index++];
|
||||
Option1Raw = columnStrings[index++];
|
||||
Option2Raw = columnStrings[index++];
|
||||
Option3Raw = columnStrings[index++];
|
||||
Option4Raw = columnStrings[index++];
|
||||
OptionsRaw = BuildOptionsRaw(Option1Raw, Option2Raw, Option3Raw, Option4Raw);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildOptionsRaw(params string[] optionColumns)
|
||||
{
|
||||
JArray array = new JArray();
|
||||
for (int i = 0; i < optionColumns.Length; i++)
|
||||
{
|
||||
string optionRaw = optionColumns[i];
|
||||
if (string.IsNullOrWhiteSpace(optionRaw))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
array.Add(JObject.Parse(optionRaw));
|
||||
}
|
||||
|
||||
return array.ToString(Newtonsoft.Json.Formatting.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.DataTable
|
||||
@@ -14,12 +16,11 @@ namespace GeometryTD.DataTable
|
||||
public override int Id => m_Id;
|
||||
|
||||
public LevelThemeType LevelThemeType { get; private set; }
|
||||
public RarityType Rarity { get; private set; }
|
||||
public string ItemType { get; private set; }
|
||||
public int ItemId { get; private set; }
|
||||
public int Weight { get; private set; }
|
||||
public int MinPhase { get; private set; }
|
||||
public int MaxPhase { get; private set; }
|
||||
public int[] Weights { get; private set; }
|
||||
public int[] MinPhase { get; private set; }
|
||||
public int[] MaxPhase { get; private set; }
|
||||
|
||||
public override bool ParseDataRow(string dataRowString, object userData)
|
||||
{
|
||||
@@ -34,31 +35,110 @@ namespace GeometryTD.DataTable
|
||||
m_Id = int.Parse(columnStrings[index++]);
|
||||
index++;
|
||||
LevelThemeType = EnumUtility<LevelThemeType>.Get(columnStrings[index++]);
|
||||
Rarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
|
||||
ItemType = columnStrings[index++];
|
||||
ItemId = ParseIntOrDefault(columnStrings[index++], 0);
|
||||
Weight = ParseIntOrDefault(columnStrings[index++], 1);
|
||||
MinPhase = ParseIntOrDefault(columnStrings[index++], 1);
|
||||
MaxPhase = ParseIntOrDefault(columnStrings[index++], int.MaxValue);
|
||||
|
||||
if (Weight <= 0)
|
||||
{
|
||||
Weight = 1;
|
||||
}
|
||||
|
||||
if (MinPhase <= 0)
|
||||
{
|
||||
MinPhase = 1;
|
||||
}
|
||||
|
||||
if (MaxPhase < MinPhase)
|
||||
{
|
||||
MaxPhase = MinPhase;
|
||||
}
|
||||
Weights = ParseWeights(columnStrings[index++]);
|
||||
MinPhase = ParsePhases(columnStrings[index++], 1);
|
||||
MaxPhase = ParsePhases(columnStrings[index++], int.MaxValue);
|
||||
NormalizePhaseRanges();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetWeight(RarityType rarity)
|
||||
{
|
||||
int index = Mathf.Clamp((int)rarity - 1, 0, RarityCount - 1);
|
||||
return Weights[index];
|
||||
}
|
||||
|
||||
public int GetMinPhase(RarityType rarity)
|
||||
{
|
||||
int index = Mathf.Clamp((int)rarity - 1, 0, RarityCount - 1);
|
||||
return MinPhase[index];
|
||||
}
|
||||
|
||||
public int GetMaxPhase(RarityType rarity)
|
||||
{
|
||||
int index = Mathf.Clamp((int)rarity - 1, 0, RarityCount - 1);
|
||||
return MaxPhase[index];
|
||||
}
|
||||
|
||||
private static int[] ParseWeights(string raw)
|
||||
{
|
||||
if (!raw.StartsWith('[') || !raw.EndsWith(']'))
|
||||
{
|
||||
throw new ArgumentException("Weights must be enclosed in square brackets.");
|
||||
}
|
||||
|
||||
if (raw.Length == 2)
|
||||
{
|
||||
return new int[RarityCount];
|
||||
}
|
||||
|
||||
string[] raws = raw.Substring(1, raw.Length - 2).Split(",");
|
||||
if (raws.Length != RarityCount)
|
||||
{
|
||||
throw new ArgumentException($"Weights must contain exactly {RarityCount} values.");
|
||||
}
|
||||
|
||||
int[] weights = new int[RarityCount];
|
||||
for (int i = 0; i < raws.Length; i++)
|
||||
{
|
||||
weights[i] = Mathf.Max(0, int.Parse(raws[i]));
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
private static int[] ParsePhases(string raw, int fallbackValue)
|
||||
{
|
||||
if (!raw.StartsWith('[') || !raw.EndsWith(']'))
|
||||
{
|
||||
throw new ArgumentException("Phase ranges must be enclosed in square brackets.");
|
||||
}
|
||||
|
||||
if (raw.Length == 2)
|
||||
{
|
||||
int[] fallbackValues = new int[RarityCount];
|
||||
for (int i = 0; i < fallbackValues.Length; i++)
|
||||
{
|
||||
fallbackValues[i] = fallbackValue;
|
||||
}
|
||||
|
||||
return fallbackValues;
|
||||
}
|
||||
|
||||
string[] raws = raw.Substring(1, raw.Length - 2).Split(",");
|
||||
if (raws.Length != RarityCount)
|
||||
{
|
||||
throw new ArgumentException($"Phase ranges must contain exactly {RarityCount} values.");
|
||||
}
|
||||
|
||||
int[] phases = new int[RarityCount];
|
||||
for (int i = 0; i < raws.Length; i++)
|
||||
{
|
||||
phases[i] = int.Parse(raws[i]);
|
||||
}
|
||||
|
||||
return phases;
|
||||
}
|
||||
|
||||
private void NormalizePhaseRanges()
|
||||
{
|
||||
for (int i = 0; i < RarityCount; i++)
|
||||
{
|
||||
if (MinPhase[i] <= 0)
|
||||
{
|
||||
MinPhase[i] = 1;
|
||||
}
|
||||
|
||||
if (MaxPhase[i] < MinPhase[i])
|
||||
{
|
||||
MaxPhase[i] = MinPhase[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int ParseIntOrDefault(string raw, int fallbackValue)
|
||||
{
|
||||
if (int.TryParse(raw, out int parsed))
|
||||
@@ -68,5 +148,7 @@ namespace GeometryTD.DataTable
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
private const int RarityCount = (int)RarityType.Red;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ namespace GeometryTD.Definition
|
||||
Shop = 2,
|
||||
Drop = 3,
|
||||
Reward = 4,
|
||||
Event = 5,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,31 @@ namespace GeometryTD.Definition
|
||||
{
|
||||
public int Count;
|
||||
public RarityType Rarity;
|
||||
public RarityType MinRarity;
|
||||
public RarityType MaxRarity;
|
||||
|
||||
public AddRandomCompsParam(int count, RarityType rarity)
|
||||
{
|
||||
Count = count;
|
||||
Rarity = rarity;
|
||||
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
|
||||
Rarity = normalizedRarity;
|
||||
MinRarity = normalizedRarity;
|
||||
MaxRarity = normalizedRarity;
|
||||
}
|
||||
|
||||
public AddRandomCompsParam(int count, RarityType minRarity, RarityType maxRarity)
|
||||
{
|
||||
Count = count;
|
||||
MinRarity = InventoryRarityRuleService.NormalizeComponentRarity(minRarity);
|
||||
MaxRarity = InventoryRarityRuleService.NormalizeComponentRarity(maxRarity);
|
||||
if (MinRarity > MaxRarity)
|
||||
{
|
||||
throw new System.ArgumentOutOfRangeException(
|
||||
nameof(minRarity),
|
||||
$"AddRandomComps rarity range is invalid: {MinRarity} > {MaxRarity}.");
|
||||
}
|
||||
|
||||
Rarity = MinRarity == MaxRarity ? MinRarity : RarityType.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomComponent;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.Definition
|
||||
{
|
||||
public sealed class EventOptionExecutor
|
||||
{
|
||||
public EventOptionAvailability EvaluateOption(EventOption option, BackpackInventoryData inventory)
|
||||
{
|
||||
if (option == null)
|
||||
{
|
||||
return EventOptionAvailability.Blocked("选项配置无效");
|
||||
}
|
||||
|
||||
EventRequirementBase[] requirements = option.Requirements ?? Array.Empty<EventRequirementBase>();
|
||||
for (int i = 0; i < requirements.Length; i++)
|
||||
{
|
||||
EventRequirementBase requirement = requirements[i];
|
||||
if (requirement == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsRequirementSatisfied(requirement, inventory))
|
||||
{
|
||||
return EventOptionAvailability.Blocked(BuildBlockedReason(requirement));
|
||||
}
|
||||
}
|
||||
|
||||
return EventOptionAvailability.Selectable();
|
||||
}
|
||||
|
||||
public EventOptionExecutionResult Execute(
|
||||
EventItem eventItem,
|
||||
int optionIndex,
|
||||
RunNodeExecutionContext context,
|
||||
BackpackInventoryData workingInventory)
|
||||
{
|
||||
if (eventItem == null || workingInventory == null)
|
||||
{
|
||||
return EventOptionExecutionResult.Rejected("事件数据无效");
|
||||
}
|
||||
|
||||
if (optionIndex < 0 || optionIndex >= eventItem.Options.Length)
|
||||
{
|
||||
return EventOptionExecutionResult.Rejected("事件选项索引无效");
|
||||
}
|
||||
|
||||
EventOption option = eventItem.Options[optionIndex];
|
||||
EventOptionAvailability availability = EvaluateOption(option, workingInventory);
|
||||
if (!availability.IsSelectable)
|
||||
{
|
||||
return EventOptionExecutionResult.Rejected(availability.BlockedReason);
|
||||
}
|
||||
|
||||
ApplyEffects(option.CostEffects, eventItem, optionIndex, context, workingInventory, isRewardPhase: false);
|
||||
|
||||
bool isProbabilitySuccess = RollProbability(eventItem.Id, optionIndex, context, option.Probability);
|
||||
if (isProbabilitySuccess)
|
||||
{
|
||||
ApplyEffects(option.RewardEffects, eventItem, optionIndex, context, workingInventory, isRewardPhase: true);
|
||||
}
|
||||
|
||||
return EventOptionExecutionResult.Accepted(isProbabilitySuccess);
|
||||
}
|
||||
|
||||
private void ApplyEffects(
|
||||
EventEffectBase[] effects,
|
||||
EventItem eventItem,
|
||||
int optionIndex,
|
||||
RunNodeExecutionContext context,
|
||||
BackpackInventoryData workingInventory,
|
||||
bool isRewardPhase)
|
||||
{
|
||||
if (effects == null || effects.Length <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < effects.Length; i++)
|
||||
{
|
||||
EventEffectBase effect = effects[i];
|
||||
if (effect == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (effect.EffectType)
|
||||
{
|
||||
case EventEffectType.AddGold:
|
||||
ApplyAddGoldEffect((AddGoldParam)effect.Param, workingInventory);
|
||||
break;
|
||||
case EventEffectType.RemoveRandomComps:
|
||||
ApplyRemoveRandomComponentsEffect(
|
||||
(RemoveRandomCompsParam)effect.Param,
|
||||
eventItem.Id,
|
||||
optionIndex,
|
||||
context,
|
||||
workingInventory,
|
||||
i);
|
||||
break;
|
||||
case EventEffectType.AddRandomComps:
|
||||
ApplyAddRandomComponentsEffect(
|
||||
(AddRandomCompsParam)effect.Param,
|
||||
eventItem.Id,
|
||||
optionIndex,
|
||||
context,
|
||||
workingInventory,
|
||||
i);
|
||||
break;
|
||||
case EventEffectType.DamageRandomTowersEndurance:
|
||||
ApplyDamageRandomTowerEnduranceEffect(
|
||||
(DamageRandomTowerEnduranceParam)effect.Param,
|
||||
eventItem.Id,
|
||||
optionIndex,
|
||||
context,
|
||||
workingInventory,
|
||||
i);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported event effect at runtime: {effect.EffectType} (rewardPhase={isRewardPhase}).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyAddGoldEffect(AddGoldParam param, BackpackInventoryData workingInventory)
|
||||
{
|
||||
int nextGold = workingInventory.Gold + (param?.Count ?? 0);
|
||||
if (nextGold < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Event gold effect would reduce gold below zero.");
|
||||
}
|
||||
|
||||
workingInventory.Gold = nextGold;
|
||||
}
|
||||
|
||||
private static void ApplyRemoveRandomComponentsEffect(
|
||||
RemoveRandomCompsParam param,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
RunNodeExecutionContext context,
|
||||
BackpackInventoryData workingInventory,
|
||||
int effectIndex)
|
||||
{
|
||||
int removeCount = Mathf.Max(0, param?.Count ?? 0);
|
||||
if (removeCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<TowerCompItemData> candidates = CollectLooseComponents(workingInventory, param.Rarity);
|
||||
if (candidates.Count < removeCount)
|
||||
{
|
||||
throw new InvalidOperationException("Event component removal effect does not have enough candidates.");
|
||||
}
|
||||
|
||||
System.Random random = CreateRandom(context, eventId, optionIndex, effectIndex, 101);
|
||||
Shuffle(candidates, random);
|
||||
for (int i = 0; i < removeCount; i++)
|
||||
{
|
||||
RemoveComponentByInstanceId(workingInventory, candidates[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyAddRandomComponentsEffect(
|
||||
AddRandomCompsParam param,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
RunNodeExecutionContext context,
|
||||
BackpackInventoryData workingInventory,
|
||||
int effectIndex)
|
||||
{
|
||||
int addCount = Mathf.Max(0, param?.Count ?? 0);
|
||||
if (addCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameEntry.InventoryGeneration == null)
|
||||
{
|
||||
throw new InvalidOperationException("Event component generation requires InventoryGenerationComponent.");
|
||||
}
|
||||
|
||||
IReadOnlyList<TowerCompItemData> generatedComponents = GameEntry.InventoryGeneration.BuildEventRewardComponents(
|
||||
addCount,
|
||||
param.MinRarity,
|
||||
param.MaxRarity,
|
||||
context?.RunSeed ?? 0,
|
||||
context?.SequenceIndex ?? -1,
|
||||
eventId,
|
||||
optionIndex,
|
||||
effectIndex);
|
||||
for (int i = 0; i < generatedComponents.Count; i++)
|
||||
{
|
||||
AddComponentToInventory(workingInventory, generatedComponents[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyDamageRandomTowerEnduranceEffect(
|
||||
DamageRandomTowerEnduranceParam param,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
RunNodeExecutionContext context,
|
||||
BackpackInventoryData workingInventory,
|
||||
int effectIndex)
|
||||
{
|
||||
int towerCount = Mathf.Max(0, param?.Count ?? 0);
|
||||
float enduranceLoss = Mathf.Max(0, param?.Amount ?? 0);
|
||||
if (towerCount <= 0 || enduranceLoss <= 0f || workingInventory.Towers == null || workingInventory.Towers.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<long> candidateTowerIds = new List<long>(workingInventory.Towers.Count);
|
||||
for (int i = 0; i < workingInventory.Towers.Count; i++)
|
||||
{
|
||||
TowerItemData tower = workingInventory.Towers[i];
|
||||
if (tower != null && tower.InstanceId > 0)
|
||||
{
|
||||
candidateTowerIds.Add(tower.InstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidateTowerIds.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
System.Random random = CreateRandom(context, eventId, optionIndex, effectIndex, 211);
|
||||
Shuffle(candidateTowerIds, random);
|
||||
int resolvedCount = Mathf.Min(towerCount, candidateTowerIds.Count);
|
||||
List<long> selectedTowerIds = candidateTowerIds.GetRange(0, resolvedCount);
|
||||
InventoryTowerEnduranceUtility.ReduceTowerEndurance(workingInventory, selectedTowerIds, enduranceLoss);
|
||||
}
|
||||
|
||||
private static bool IsRequirementSatisfied(EventRequirementBase requirement, BackpackInventoryData inventory)
|
||||
{
|
||||
switch (requirement.RequirementType)
|
||||
{
|
||||
case EventRequirementType.GoldAtLeast:
|
||||
return (inventory?.Gold ?? 0) >= ((GoldAtLeastParam)requirement.Param).Gold;
|
||||
case EventRequirementType.CompCountAtLeast:
|
||||
CompCountAtLeastParam compParam = (CompCountAtLeastParam)requirement.Param;
|
||||
return CollectLooseComponents(inventory, compParam.Rarity).Count >= compParam.Count;
|
||||
case EventRequirementType.TowerCountAtLeast:
|
||||
return (inventory?.Towers?.Count ?? 0) >= ((TowerCountAtLeastParam)requirement.Param).Count;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported event requirement at runtime: {requirement.RequirementType}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildBlockedReason(EventRequirementBase requirement)
|
||||
{
|
||||
switch (requirement.RequirementType)
|
||||
{
|
||||
case EventRequirementType.GoldAtLeast:
|
||||
return $"需要至少 {((GoldAtLeastParam)requirement.Param).Gold} 金币";
|
||||
case EventRequirementType.CompCountAtLeast:
|
||||
CompCountAtLeastParam compParam = (CompCountAtLeastParam)requirement.Param;
|
||||
return $"需要至少 {compParam.Count} 个未装配的{GetRarityText(compParam.Rarity)}组件";
|
||||
case EventRequirementType.TowerCountAtLeast:
|
||||
return $"需要至少 {((TowerCountAtLeastParam)requirement.Param).Count} 座防御塔";
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported event requirement at runtime: {requirement.RequirementType}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RollProbability(int eventId, int optionIndex, RunNodeExecutionContext context, float probability)
|
||||
{
|
||||
float clampedProbability = Mathf.Clamp01(probability);
|
||||
if (clampedProbability >= 1f)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (clampedProbability <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
System.Random random = CreateRandom(context, eventId, optionIndex, 0, 17);
|
||||
return random.NextDouble() <= clampedProbability;
|
||||
}
|
||||
|
||||
private static System.Random CreateRandom(
|
||||
RunNodeExecutionContext context,
|
||||
int eventId,
|
||||
int optionIndex,
|
||||
int effectIndex,
|
||||
int salt)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int seed = 17;
|
||||
seed = seed * 31 + (context?.RunSeed ?? 0);
|
||||
seed = seed * 31 + (context?.SequenceIndex ?? -1);
|
||||
seed = seed * 31 + eventId;
|
||||
seed = seed * 31 + optionIndex;
|
||||
seed = seed * 31 + effectIndex;
|
||||
seed = seed * 31 + salt;
|
||||
return new System.Random(seed);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<TowerCompItemData> CollectLooseComponents(
|
||||
BackpackInventoryData inventory,
|
||||
RarityType rarity)
|
||||
{
|
||||
List<TowerCompItemData> result = new List<TowerCompItemData>();
|
||||
if (inventory == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
|
||||
CollectFromList(result, inventory.MuzzleComponents, normalizedRarity);
|
||||
CollectFromList(result, inventory.BearingComponents, normalizedRarity);
|
||||
CollectFromList(result, inventory.BaseComponents, normalizedRarity);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void CollectFromList<TComp>(
|
||||
List<TowerCompItemData> destination,
|
||||
List<TComp> source,
|
||||
RarityType rarity)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
TComp component = source[i];
|
||||
if (component == null || component.IsAssembledIntoTower)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (InventoryRarityRuleService.NormalizeComponentRarity(component.Rarity) != rarity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
destination.Add(component);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveComponentByInstanceId(BackpackInventoryData inventory, TowerCompItemData component)
|
||||
{
|
||||
switch (component)
|
||||
{
|
||||
case MuzzleCompItemData muzzleComp:
|
||||
RemoveByInstanceId(inventory.MuzzleComponents, muzzleComp.InstanceId);
|
||||
break;
|
||||
case BearingCompItemData bearingComp:
|
||||
RemoveByInstanceId(inventory.BearingComponents, bearingComp.InstanceId);
|
||||
break;
|
||||
case BaseCompItemData baseComp:
|
||||
RemoveByInstanceId(inventory.BaseComponents, baseComp.InstanceId);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unsupported component type for event removal: {component?.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddComponentToInventory(BackpackInventoryData inventory, TowerCompItemData component)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (component)
|
||||
{
|
||||
case MuzzleCompItemData muzzleComp:
|
||||
inventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp));
|
||||
break;
|
||||
case BearingCompItemData bearingComp:
|
||||
inventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
|
||||
break;
|
||||
case BaseCompItemData baseComp:
|
||||
inventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unsupported component type for event addition: {component.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveByInstanceId<TComp>(List<TComp> components, long instanceId)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
if (components == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
{
|
||||
TComp component = components[i];
|
||||
if (component != null && component.InstanceId == instanceId)
|
||||
{
|
||||
components.RemoveAt(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Failed to remove component instance #{instanceId} from inventory.");
|
||||
}
|
||||
|
||||
private static void Shuffle<T>(IList<T> values, System.Random random)
|
||||
{
|
||||
for (int i = values.Count - 1; i > 0; i--)
|
||||
{
|
||||
int swapIndex = random.Next(0, i + 1);
|
||||
(values[i], values[swapIndex]) = (values[swapIndex], values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRarityText(RarityType rarity)
|
||||
{
|
||||
return InventoryRarityRuleService.NormalizeComponentRarity(rarity) switch
|
||||
{
|
||||
RarityType.White => "白色",
|
||||
RarityType.Green => "绿色",
|
||||
RarityType.Blue => "蓝色",
|
||||
RarityType.Purple => "紫色",
|
||||
RarityType.Red => "红色",
|
||||
_ => "未知"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EventOptionAvailability
|
||||
{
|
||||
private EventOptionAvailability(bool isSelectable, string blockedReason)
|
||||
{
|
||||
IsSelectable = isSelectable;
|
||||
BlockedReason = blockedReason ?? string.Empty;
|
||||
}
|
||||
|
||||
public bool IsSelectable { get; }
|
||||
|
||||
public string BlockedReason { get; }
|
||||
|
||||
public static EventOptionAvailability Selectable()
|
||||
{
|
||||
return new EventOptionAvailability(true, string.Empty);
|
||||
}
|
||||
|
||||
public static EventOptionAvailability Blocked(string blockedReason)
|
||||
{
|
||||
return new EventOptionAvailability(false, blockedReason);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EventOptionExecutionResult
|
||||
{
|
||||
private EventOptionExecutionResult(bool isAccepted, bool isProbabilitySuccess, string failureReason)
|
||||
{
|
||||
IsAccepted = isAccepted;
|
||||
IsProbabilitySuccess = isProbabilitySuccess;
|
||||
FailureReason = failureReason ?? string.Empty;
|
||||
}
|
||||
|
||||
public bool IsAccepted { get; }
|
||||
|
||||
public bool IsProbabilitySuccess { get; }
|
||||
|
||||
public string FailureReason { get; }
|
||||
|
||||
public static EventOptionExecutionResult Accepted(bool isProbabilitySuccess)
|
||||
{
|
||||
return new EventOptionExecutionResult(true, isProbabilitySuccess, string.Empty);
|
||||
}
|
||||
|
||||
public static EventOptionExecutionResult Rejected(string failureReason)
|
||||
{
|
||||
return new EventOptionExecutionResult(false, false, failureReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e4d7125aa7b4cb9aa62cb0bf9301d5b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -24,8 +24,8 @@ namespace GeometryTD.Definition
|
||||
int normalizedBase = (int)NormalizeComponentRarity(baseRarity);
|
||||
|
||||
float average = (normalizedMuzzle + normalizedBearing + normalizedBase) / 3f;
|
||||
int rounded = Mathf.RoundToInt(average);
|
||||
int clamped = Mathf.Clamp(rounded, (int)RarityType.White, (int)RarityType.Red);
|
||||
int floored = Mathf.FloorToInt(average);
|
||||
int clamped = Mathf.Clamp(floored, (int)RarityType.White, (int)RarityType.Red);
|
||||
return (RarityType)clamped;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,15 @@ namespace GeometryTD.Definition
|
||||
configId);
|
||||
}
|
||||
|
||||
public static InventoryTagRandomContext CreateEvent(int runSeed, int nodeSequenceIndex, int eventOrdinal, int configId)
|
||||
{
|
||||
return new InventoryTagRandomContext(
|
||||
runSeed,
|
||||
InventoryTagSourceType.Event,
|
||||
ComposeLocalItemInstanceId(nodeSequenceIndex, eventOrdinal),
|
||||
configId);
|
||||
}
|
||||
|
||||
public static long ComposeLocalItemInstanceId(int nodeSequenceIndex, int localOrdinal)
|
||||
{
|
||||
long normalizedSequence = Math.Max(0, nodeSequenceIndex) + 1L;
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.Tilemaps;
|
||||
|
||||
namespace GeometryTD.Entity
|
||||
@@ -70,7 +71,14 @@ namespace GeometryTD.Entity
|
||||
return false;
|
||||
}
|
||||
|
||||
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
|
||||
Mouse mouse = Mouse.current;
|
||||
if (mouse == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector2 pointerPosition = mouse.position.ReadValue();
|
||||
Ray ray = mainCamera.ScreenPointToRay(pointerPosition);
|
||||
float mapPlaneZ = tilemap != null
|
||||
? tilemap.transform.position.z
|
||||
: (mapTransform != null ? mapTransform.position.z : 0f);
|
||||
@@ -84,7 +92,7 @@ namespace GeometryTD.Entity
|
||||
}
|
||||
|
||||
worldPosition = ray.GetPoint(enterDistance);
|
||||
screenPosition = BuildScreenPosition(Input.mousePosition);
|
||||
screenPosition = BuildScreenPosition(pointerPosition);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using GeometryTD.Entity.EntityData;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.Tilemaps;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
@@ -272,7 +273,8 @@ namespace GeometryTD.Entity
|
||||
|
||||
private void HandleCombatSelectInput()
|
||||
{
|
||||
if (!_enableCombatSelectInput || !Input.GetMouseButtonDown(0))
|
||||
Mouse mouse = Mouse.current;
|
||||
if (!_enableCombatSelectInput || mouse == null || !mouse.leftButton.wasPressedThisFrame)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
|
||||
namespace GeometryTD.CustomEvent
|
||||
@@ -24,7 +22,7 @@ namespace GeometryTD.CustomEvent
|
||||
|
||||
public bool CombatWon { get; private set; }
|
||||
|
||||
public BackpackInventoryData InventorySnapshotAfterNode { get; private set; }
|
||||
public RunNodeCompletionSnapshot CompletionSnapshot { get; private set; }
|
||||
|
||||
public NodeCompleteEventArgs()
|
||||
{
|
||||
@@ -42,7 +40,7 @@ namespace GeometryTD.CustomEvent
|
||||
int sequenceIndex,
|
||||
RunNodeCompletionStatus completionStatus,
|
||||
bool combatWon,
|
||||
BackpackInventoryData inventorySnapshotAfterNode)
|
||||
RunNodeCompletionSnapshot completionSnapshot)
|
||||
{
|
||||
var args = ReferencePool.Acquire<NodeCompleteEventArgs>();
|
||||
args.RunId = runId;
|
||||
@@ -51,7 +49,7 @@ namespace GeometryTD.CustomEvent
|
||||
args.SequenceIndex = sequenceIndex;
|
||||
args.CompletionStatus = completionStatus;
|
||||
args.CombatWon = combatWon;
|
||||
args.InventorySnapshotAfterNode = InventoryCloneUtility.CloneInventory(inventorySnapshotAfterNode);
|
||||
args.CompletionSnapshot = completionSnapshot != null ? completionSnapshot.Clone() : null;
|
||||
|
||||
return args;
|
||||
}
|
||||
@@ -64,7 +62,7 @@ namespace GeometryTD.CustomEvent
|
||||
SequenceIndex = -1;
|
||||
CompletionStatus = RunNodeCompletionStatus.None;
|
||||
CombatWon = false;
|
||||
InventorySnapshotAfterNode = null;
|
||||
CompletionSnapshot = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
|
||||
namespace GeometryTD.CustomEvent
|
||||
{
|
||||
public sealed class RepoSellCancelRequestedEventArgs : GameEventArgs
|
||||
{
|
||||
public static int EventId => typeof(RepoSellCancelRequestedEventArgs).GetHashCode();
|
||||
|
||||
public override int Id => EventId;
|
||||
|
||||
public static RepoSellCancelRequestedEventArgs Create()
|
||||
{
|
||||
return ReferencePool.Acquire<RepoSellCancelRequestedEventArgs>();
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cc34a22ea6347619b065c5874e0b715
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,21 @@
|
||||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
|
||||
namespace GeometryTD.CustomEvent
|
||||
{
|
||||
public sealed class RepoSellConfirmRequestedEventArgs : GameEventArgs
|
||||
{
|
||||
public static int EventId => typeof(RepoSellConfirmRequestedEventArgs).GetHashCode();
|
||||
|
||||
public override int Id => EventId;
|
||||
|
||||
public static RepoSellConfirmRequestedEventArgs Create()
|
||||
{
|
||||
return ReferencePool.Acquire<RepoSellConfirmRequestedEventArgs>();
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 598b87b9cb8e4783bc73db5e255d31f6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,21 @@
|
||||
using GameFramework;
|
||||
using GameFramework.Event;
|
||||
|
||||
namespace GeometryTD.CustomEvent
|
||||
{
|
||||
public sealed class RepoSellModeToggleRequestedEventArgs : GameEventArgs
|
||||
{
|
||||
public static int EventId => typeof(RepoSellModeToggleRequestedEventArgs).GetHashCode();
|
||||
|
||||
public override int Id => EventId;
|
||||
|
||||
public static RepoSellModeToggleRequestedEventArgs Create()
|
||||
{
|
||||
return ReferencePool.Acquire<RepoSellModeToggleRequestedEventArgs>();
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44692273ce6a417a875dea9675663d82
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -31,6 +31,21 @@ namespace GeometryTD.Factory
|
||||
case EventEffectType.AddRandomComps:
|
||||
{
|
||||
int count = GetInt(param, "Count");
|
||||
bool hasMinRarity = TryGetString(param, "MinRarity", out string minRarityRaw);
|
||||
bool hasMaxRarity = TryGetString(param, "MaxRarity", out string maxRarityRaw);
|
||||
if (hasMinRarity || hasMaxRarity)
|
||||
{
|
||||
if (!hasMinRarity || !hasMaxRarity)
|
||||
{
|
||||
throw new System.InvalidOperationException(
|
||||
"AddRandomComps requires both MinRarity and MaxRarity when using ranged rarity config.");
|
||||
}
|
||||
|
||||
RarityType minRarity = EnumUtility<RarityType>.Get(minRarityRaw);
|
||||
RarityType maxRarity = EnumUtility<RarityType>.Get(maxRarityRaw);
|
||||
return new AddRandomCompsEffect(new AddRandomCompsParam(count, minRarity, maxRarity), probability);
|
||||
}
|
||||
|
||||
RarityType rarity = EnumUtility<RarityType>.Get(GetString(param, "Rarity"));
|
||||
return new AddRandomCompsEffect(new AddRandomCompsParam(count, rarity), probability);
|
||||
}
|
||||
@@ -73,5 +88,17 @@ namespace GeometryTD.Factory
|
||||
|
||||
return token.Value<string>();
|
||||
}
|
||||
|
||||
private static bool TryGetString(JObject param, string key, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (param == null || !param.TryGetValue(key, out JToken token) || token.Type == JTokenType.Null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = token.Value<string>();
|
||||
return !string.IsNullOrWhiteSpace(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.Factory
|
||||
{
|
||||
public sealed class OutGameDropItemBuilder
|
||||
{
|
||||
private readonly IDataTable<DRMuzzleComp> _muzzleCompTable;
|
||||
private readonly IDataTable<DRBearingComp> _bearingCompTable;
|
||||
private readonly IDataTable<DRBaseComp> _baseCompTable;
|
||||
|
||||
public OutGameDropItemBuilder(
|
||||
IDataTable<DRMuzzleComp> muzzleCompTable,
|
||||
IDataTable<DRBearingComp> bearingCompTable,
|
||||
IDataTable<DRBaseComp> baseCompTable)
|
||||
{
|
||||
_muzzleCompTable = muzzleCompTable;
|
||||
_bearingCompTable = bearingCompTable;
|
||||
_baseCompTable = baseCompTable;
|
||||
}
|
||||
|
||||
public bool TryBuildItem(
|
||||
DROutGameDropPool row,
|
||||
RarityType rarity,
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
out TowerCompItemData droppedItem)
|
||||
{
|
||||
droppedItem = null;
|
||||
if (row.ItemId <= 0 || string.IsNullOrWhiteSpace(row.ItemType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string itemType = row.ItemType.Trim();
|
||||
if (itemType.Equals("MuzzleComp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DRMuzzleComp config = _muzzleCompTable.GetDataRow(row.ItemId);
|
||||
if (config == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
droppedItem = ComponentItemFactory.CreateMuzzle(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DRBearingComp config = _bearingCompTable.GetDataRow(row.ItemId);
|
||||
if (config == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
droppedItem = ComponentItemFactory.CreateBearing(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
DRBaseComp config = _baseCompTable.GetDataRow(row.ItemId);
|
||||
if (config == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
droppedItem = ComponentItemFactory.CreateBase(
|
||||
config,
|
||||
randomContext.CreateStableItemInstanceId(),
|
||||
rarity,
|
||||
randomContext.CreateTagRandomContext(config.Id));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5915901f51c430db50725908113ae1a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -196,45 +196,6 @@ namespace GeometryTD.Procedure
|
||||
|
||||
_loadedFlag[ne.DataTableAssetName] = true;
|
||||
Log.Info("Load data table '{0}' OK.", ne.DataTableAssetName);
|
||||
|
||||
if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("TagConfig", false))
|
||||
{
|
||||
ReloadTagRegistriesFromLoadedTables();
|
||||
}
|
||||
|
||||
if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("Tag", false))
|
||||
{
|
||||
ReloadTagRegistriesFromLoadedTables();
|
||||
}
|
||||
|
||||
if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("RarityTagBudget", false))
|
||||
{
|
||||
var tagBudgetTable = GameEntry.DataTable.GetDataTable<DRRarityTagBudget>();
|
||||
if (tagBudgetTable != null)
|
||||
{
|
||||
RarityTagBudgetRuleRegistry.LoadFromRows(tagBudgetTable.GetAllDataRows());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReloadTagRegistriesFromLoadedTables()
|
||||
{
|
||||
DRTagConfig[] tagConfigRows = null;
|
||||
var tagConfigTable = GameEntry.DataTable.GetDataTable<DRTagConfig>();
|
||||
if (tagConfigTable != null)
|
||||
{
|
||||
tagConfigRows = tagConfigTable.GetAllDataRows();
|
||||
}
|
||||
|
||||
DRTag[] tagRows = null;
|
||||
var tagTable = GameEntry.DataTable.GetDataTable<DRTag>();
|
||||
if (tagTable != null)
|
||||
{
|
||||
tagRows = tagTable.GetAllDataRows();
|
||||
TagGenerationRuleRegistry.LoadFromRows(tagRows);
|
||||
}
|
||||
|
||||
TagDefinitionRegistry.ReloadFromRows(tagConfigRows, tagRows);
|
||||
}
|
||||
|
||||
private void OnLoadDataTableFailure(object sender, GameEventArgs e)
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace GeometryTD.Procedure
|
||||
GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
|
||||
GameEntry.Event.Subscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested);
|
||||
|
||||
GameEntry.TagRegistry.OnInit();
|
||||
GameEntry.EventNode.OnInit();
|
||||
GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
|
||||
GameEntry.ShopNode.OnInit();
|
||||
@@ -193,23 +194,36 @@ namespace GeometryTD.Procedure
|
||||
args.CompletionStatus,
|
||||
args.CombatWon);
|
||||
|
||||
BackpackInventoryData snapshot = args.InventorySnapshotAfterNode;
|
||||
if (snapshot == null && GameEntry.PlayerInventory != null)
|
||||
RunNodeCompletionSnapshot completionSnapshot = args.CompletionSnapshot != null
|
||||
? args.CompletionSnapshot.Clone()
|
||||
: _currentRunState?.CreateCurrentNodeContext()?.CreateCompletionSnapshot(null);
|
||||
BackpackInventoryData inventorySnapshot = completionSnapshot?.InventorySnapshot;
|
||||
if (inventorySnapshot == null && GameEntry.PlayerInventory != null)
|
||||
{
|
||||
snapshot = GameEntry.PlayerInventory.GetInventorySnapshot();
|
||||
inventorySnapshot = GameEntry.PlayerInventory.GetInventorySnapshot();
|
||||
}
|
||||
|
||||
if (completionSnapshot != null)
|
||||
{
|
||||
completionSnapshot.InventorySnapshot = inventorySnapshot;
|
||||
}
|
||||
|
||||
ProcedureMainParticipantTowerCleanupResult cleanupResult =
|
||||
ProcedureMainParticipantTowerCleanupService.RemoveBrokenParticipantTowers(
|
||||
snapshot,
|
||||
inventorySnapshot,
|
||||
MaxParticipantTowerCount);
|
||||
if (cleanupResult.HasAnyRemovedTower && GameEntry.PlayerInventory != null)
|
||||
{
|
||||
GameEntry.PlayerInventory.ReplaceInventorySnapshot(snapshot);
|
||||
GameEntry.PlayerInventory.ReplaceInventorySnapshot(inventorySnapshot);
|
||||
}
|
||||
|
||||
if (completionSnapshot != null)
|
||||
{
|
||||
completionSnapshot.InventorySnapshot = inventorySnapshot;
|
||||
}
|
||||
|
||||
ProcedureMainRunAdvanceResult advanceResult =
|
||||
ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.CompletionStatus, snapshot);
|
||||
ProcedureMainRunFlowService.TryAdvanceRun(_currentRunState, args.CompletionStatus, completionSnapshot);
|
||||
HandleRunAdvanceResult(advanceResult);
|
||||
if (cleanupResult.HasAnyRemovedTower &&
|
||||
advanceResult != ProcedureMainRunAdvanceResult.RunCompleted)
|
||||
@@ -265,28 +279,20 @@ namespace GeometryTD.Procedure
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameEntry.CombatNode.CurrentThemeType != currentNode.ThemeType)
|
||||
{
|
||||
GameEntry.CombatNode.OnInit(currentNode.ThemeType);
|
||||
}
|
||||
|
||||
GameEntry.CombatNode.StartCombat(
|
||||
currentNode.LinkedLevelId,
|
||||
_currentRunState.RunId,
|
||||
_currentRunState.RunSeed,
|
||||
currentNode.NodeId,
|
||||
currentNode.NodeType,
|
||||
currentNode.SequenceIndex);
|
||||
_currentRunState.CreateCurrentNodeContext());
|
||||
return;
|
||||
case RunNodeType.Event:
|
||||
GameEntry.EventNode.StartEvent(
|
||||
_currentRunState.RunId,
|
||||
currentNode.NodeId,
|
||||
currentNode.NodeType,
|
||||
currentNode.SequenceIndex);
|
||||
GameEntry.EventNode.StartEvent(_currentRunState.CreateCurrentNodeContext());
|
||||
return;
|
||||
case RunNodeType.Shop:
|
||||
GameEntry.ShopNode.StartShop(
|
||||
_currentRunState.RunId,
|
||||
_currentRunState.RunSeed,
|
||||
currentNode.NodeId,
|
||||
currentNode.NodeType,
|
||||
currentNode.SequenceIndex);
|
||||
GameEntry.ShopNode.StartShop(_currentRunState.CreateCurrentNodeContext());
|
||||
return;
|
||||
default:
|
||||
Log.Warning("ProcedureMain.OnNodeMapNodeEnterRequested() encountered unsupported node type: {0}.",
|
||||
|
||||
@@ -7,9 +7,9 @@ namespace GeometryTD.Procedure
|
||||
public static ProcedureMainRunAdvanceResult TryAdvanceRun(
|
||||
RunState runState,
|
||||
RunNodeCompletionStatus completionStatus,
|
||||
BackpackInventoryData snapshot)
|
||||
RunNodeCompletionSnapshot completionSnapshot)
|
||||
{
|
||||
if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, completionStatus, snapshot))
|
||||
if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, completionStatus, completionSnapshot))
|
||||
{
|
||||
return ProcedureMainRunAdvanceResult.NoChange;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,96 @@ namespace GeometryTD.Procedure
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunItemState
|
||||
{
|
||||
public int ItemId { get; set; }
|
||||
public int StackCount { get; set; }
|
||||
|
||||
internal RunItemState Clone()
|
||||
{
|
||||
return new RunItemState
|
||||
{
|
||||
ItemId = ItemId,
|
||||
StackCount = StackCount
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunNodeExecutionContext
|
||||
{
|
||||
public string RunId { get; set; }
|
||||
public int RunSeed { get; set; }
|
||||
public int NodeId { get; set; }
|
||||
public RunNodeType NodeType { get; set; }
|
||||
public int SequenceIndex { get; set; }
|
||||
public LevelThemeType ThemeType { get; set; }
|
||||
public int ThemeStageIndex { get; set; }
|
||||
public List<LevelThemeType> CurrentThemePool { get; set; } = new List<LevelThemeType>();
|
||||
public List<LevelThemeType> ThemeHistory { get; set; } = new List<LevelThemeType>();
|
||||
public int CurrentNodeContinueChallengeLayer { get; set; }
|
||||
public List<RunItemState> RunItems { get; set; } = new List<RunItemState>();
|
||||
|
||||
internal RunNodeExecutionContext Clone()
|
||||
{
|
||||
return new RunNodeExecutionContext
|
||||
{
|
||||
RunId = RunId,
|
||||
RunSeed = RunSeed,
|
||||
NodeId = NodeId,
|
||||
NodeType = NodeType,
|
||||
SequenceIndex = SequenceIndex,
|
||||
ThemeType = ThemeType,
|
||||
ThemeStageIndex = ThemeStageIndex,
|
||||
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
|
||||
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
|
||||
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
|
||||
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
|
||||
};
|
||||
}
|
||||
|
||||
public RunNodeCompletionSnapshot CreateCompletionSnapshot(BackpackInventoryData inventorySnapshot)
|
||||
{
|
||||
return new RunNodeCompletionSnapshot
|
||||
{
|
||||
InventorySnapshot = InventoryCloneUtility.CloneInventory(inventorySnapshot),
|
||||
ThemeType = ThemeType,
|
||||
ThemeStageIndex = ThemeStageIndex,
|
||||
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
|
||||
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
|
||||
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
|
||||
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunNodeCompletionSnapshot
|
||||
{
|
||||
public BackpackInventoryData InventorySnapshot { get; set; }
|
||||
public LevelThemeType ThemeType { get; set; }
|
||||
public int ThemeStageIndex { get; set; }
|
||||
public List<LevelThemeType> CurrentThemePool { get; set; } = new List<LevelThemeType>();
|
||||
public List<LevelThemeType> ThemeHistory { get; set; } = new List<LevelThemeType>();
|
||||
public int CurrentNodeContinueChallengeLayer { get; set; }
|
||||
public List<RunItemState> RunItems { get; set; } = new List<RunItemState>();
|
||||
|
||||
internal RunNodeCompletionSnapshot Clone()
|
||||
{
|
||||
return new RunNodeCompletionSnapshot
|
||||
{
|
||||
InventorySnapshot = InventoryCloneUtility.CloneInventory(InventorySnapshot),
|
||||
ThemeType = ThemeType,
|
||||
ThemeStageIndex = ThemeStageIndex,
|
||||
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
|
||||
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
|
||||
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
|
||||
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunState
|
||||
{
|
||||
@@ -82,6 +172,11 @@ namespace GeometryTD.Procedure
|
||||
ThemeType = themeType;
|
||||
_nodes = nodes ?? new List<RunNodeState>();
|
||||
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(runInventorySnapshot);
|
||||
ThemeStageIndex = 0;
|
||||
CurrentThemePool = CreateDefaultThemeList(themeType);
|
||||
ThemeHistory = CreateDefaultThemeList(themeType);
|
||||
CurrentNodeContinueChallengeLayer = 0;
|
||||
RunItems = new List<RunItemState>();
|
||||
CurrentNodeIndex = _nodes.Count > 0 ? 0 : -1;
|
||||
IsCompleted = _nodes.Count <= 0;
|
||||
}
|
||||
@@ -98,6 +193,16 @@ namespace GeometryTD.Procedure
|
||||
|
||||
public BackpackInventoryData RunInventorySnapshot { get; internal set; }
|
||||
|
||||
public int ThemeStageIndex { get; internal set; }
|
||||
|
||||
public List<LevelThemeType> CurrentThemePool { get; internal set; }
|
||||
|
||||
public List<LevelThemeType> ThemeHistory { get; internal set; }
|
||||
|
||||
public int CurrentNodeContinueChallengeLayer { get; internal set; }
|
||||
|
||||
public List<RunItemState> RunItems { get; internal set; }
|
||||
|
||||
public IReadOnlyList<RunNodeState> Nodes => _nodes;
|
||||
|
||||
public int NodeCount => _nodes.Count;
|
||||
@@ -161,5 +266,90 @@ namespace GeometryTD.Procedure
|
||||
{
|
||||
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(inventorySnapshot);
|
||||
}
|
||||
|
||||
public RunNodeExecutionContext CreateCurrentNodeContext()
|
||||
{
|
||||
RunNodeState currentNode = CurrentNode;
|
||||
if (currentNode == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RunNodeExecutionContext
|
||||
{
|
||||
RunId = RunId,
|
||||
RunSeed = RunSeed,
|
||||
NodeId = currentNode.NodeId,
|
||||
NodeType = currentNode.NodeType,
|
||||
SequenceIndex = currentNode.SequenceIndex,
|
||||
ThemeType = ThemeType,
|
||||
ThemeStageIndex = ThemeStageIndex,
|
||||
CurrentThemePool = RunStateCloneUtility.CloneThemeList(CurrentThemePool),
|
||||
ThemeHistory = RunStateCloneUtility.CloneThemeList(ThemeHistory),
|
||||
CurrentNodeContinueChallengeLayer = CurrentNodeContinueChallengeLayer,
|
||||
RunItems = RunStateCloneUtility.CloneRunItems(RunItems)
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyCompletionSnapshot(RunNodeCompletionSnapshot completionSnapshot)
|
||||
{
|
||||
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(completionSnapshot.InventorySnapshot);
|
||||
ThemeType = completionSnapshot.ThemeType;
|
||||
ThemeStageIndex = completionSnapshot.ThemeStageIndex;
|
||||
CurrentThemePool = RunStateCloneUtility.CloneThemeList(completionSnapshot.CurrentThemePool);
|
||||
ThemeHistory = RunStateCloneUtility.CloneThemeList(completionSnapshot.ThemeHistory);
|
||||
CurrentNodeContinueChallengeLayer = completionSnapshot.CurrentNodeContinueChallengeLayer;
|
||||
RunItems = RunStateCloneUtility.CloneRunItems(completionSnapshot.RunItems);
|
||||
}
|
||||
|
||||
private static List<LevelThemeType> CreateDefaultThemeList(LevelThemeType themeType)
|
||||
{
|
||||
List<LevelThemeType> themes = new List<LevelThemeType>();
|
||||
if (themeType != LevelThemeType.None)
|
||||
{
|
||||
themes.Add(themeType);
|
||||
}
|
||||
|
||||
return themes;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class RunStateCloneUtility
|
||||
{
|
||||
internal static List<LevelThemeType> CloneThemeList(IReadOnlyList<LevelThemeType> source)
|
||||
{
|
||||
List<LevelThemeType> cloned = new List<LevelThemeType>();
|
||||
if (source == null)
|
||||
{
|
||||
return cloned;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
cloned.Add(source[i]);
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
internal static List<RunItemState> CloneRunItems(IReadOnlyList<RunItemState> source)
|
||||
{
|
||||
List<RunItemState> cloned = new List<RunItemState>();
|
||||
if (source == null)
|
||||
{
|
||||
return cloned;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
RunItemState item = source[i];
|
||||
if (item != null)
|
||||
{
|
||||
cloned.Add(item.Clone());
|
||||
}
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.Procedure
|
||||
{
|
||||
public static class RunStateAdvanceService
|
||||
@@ -7,7 +5,7 @@ namespace GeometryTD.Procedure
|
||||
public static bool TryCompleteCurrentNode(
|
||||
RunState runState,
|
||||
RunNodeCompletionStatus completionStatus,
|
||||
BackpackInventoryData inventorySnapshotAfterNode)
|
||||
RunNodeCompletionSnapshot completionSnapshot)
|
||||
{
|
||||
if (runState == null || runState.IsCompleted)
|
||||
{
|
||||
@@ -20,7 +18,7 @@ namespace GeometryTD.Procedure
|
||||
return false;
|
||||
}
|
||||
|
||||
runState.ReplaceInventorySnapshot(inventorySnapshotAfterNode);
|
||||
runState.ApplyCompletionSnapshot(completionSnapshot);
|
||||
|
||||
if (completionStatus == RunNodeCompletionStatus.Exception)
|
||||
{
|
||||
@@ -38,12 +36,14 @@ namespace GeometryTD.Procedure
|
||||
int nextIndex = runState.CurrentNodeIndex + 1;
|
||||
if (nextIndex >= runState.Nodes.Count)
|
||||
{
|
||||
runState.CurrentNodeContinueChallengeLayer = 0;
|
||||
runState.CurrentNodeIndex = runState.Nodes.Count;
|
||||
runState.IsCompleted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
runState.CurrentNodeIndex = nextIndex;
|
||||
runState.CurrentNodeContinueChallengeLayer = 0;
|
||||
RunNodeState nextNode = runState.CurrentNode;
|
||||
if (nextNode != null && nextNode.Status == RunNodeStatus.Locked)
|
||||
{
|
||||
|
||||
@@ -4,5 +4,7 @@ namespace GeometryTD.UI
|
||||
{
|
||||
public int OptionIndex;
|
||||
public string OptionText;
|
||||
public bool IsSelectable;
|
||||
public string BlockedReason;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
using GeometryTD.UI;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
{
|
||||
public class RepoFormContext : UIContext
|
||||
{
|
||||
public string GoldText;
|
||||
public RepoFormState State;
|
||||
public bool ShowSellModeButton;
|
||||
public string SellModeButtonText;
|
||||
public bool ShowCombineArea;
|
||||
public bool ShowSellArea;
|
||||
public CombineAreaContext CombineAreaContext;
|
||||
public SellAreaContext SellAreaContext;
|
||||
public CompAreaContext CompAreaContext;
|
||||
public ParticipantAreaContext ParticipantAreaContext;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace GeometryTD.UI
|
||||
{
|
||||
public enum RepoFormState
|
||||
{
|
||||
Assemble = 0,
|
||||
Sell = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9110fc764f3044bda40df4a8c02f320b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -11,6 +11,9 @@ namespace GeometryTD.UI
|
||||
public float EnduranceRate01;
|
||||
public RepoItemClickActionType ClickActionType;
|
||||
public TowerCompSlotType ComponentSlotType;
|
||||
public bool IsSellMode;
|
||||
public bool IsSellable = true;
|
||||
public bool IsSellSelected;
|
||||
public IconAreaContext IconAreaContext;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace GeometryTD.UI
|
||||
{
|
||||
public class SellAreaContext
|
||||
{
|
||||
public string TotalPriceText;
|
||||
public bool CanConfirmSell;
|
||||
public RepoItemContext[] ComponentItems;
|
||||
public TowerRepoItemContext[] TowerItems;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f153d1a509df4bb69c6d231df5b60db9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+138
-37
@@ -23,21 +23,44 @@ namespace GeometryTD.UI
|
||||
Dictionary<long, BearingCompItemData> bearingMap = BuildComponentMap(rawData.Inventory.BearingComponents);
|
||||
Dictionary<long, BaseCompItemData> baseMap = BuildComponentMap(rawData.Inventory.BaseComponents);
|
||||
Dictionary<long, TowerItemData> towerMap = BuildTowerMap(rawData.Inventory.Towers);
|
||||
HashSet<long> selectedSellItemIds = BuildSelectedSellItemSet(rawData.SelectedSellItemIds);
|
||||
bool isSellState = rawData.State == RepoFormState.Sell;
|
||||
|
||||
List<RepoItemContext> componentItems = new List<RepoItemContext>();
|
||||
List<TowerRepoItemContext> towerItems = new List<TowerRepoItemContext>();
|
||||
List<RepoItemContext> selectedSellComponentItems = new List<RepoItemContext>();
|
||||
List<TowerRepoItemContext> selectedSellTowerItems = new List<TowerRepoItemContext>();
|
||||
|
||||
if (rawData.Inventory.Towers != null)
|
||||
{
|
||||
foreach (var tower in rawData.Inventory.Towers)
|
||||
foreach (TowerItemData tower in rawData.Inventory.Towers)
|
||||
{
|
||||
if (tower == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TowerRepoItemContext towerContext = BuildTowerRepoItemContext(tower, muzzleMap, bearingMap, baseMap,
|
||||
RepoItemClickActionType.OpenDetail, true);
|
||||
bool isParticipantTower = rawData.Inventory.ParticipantTowerInstanceIds != null &&
|
||||
rawData.Inventory.ParticipantTowerInstanceIds.Contains(tower.InstanceId);
|
||||
bool isSellSelected = isSellState && selectedSellItemIds.Contains(tower.InstanceId);
|
||||
|
||||
TowerRepoItemContext towerContext = BuildTowerRepoItemContext(
|
||||
tower,
|
||||
muzzleMap,
|
||||
bearingMap,
|
||||
baseMap,
|
||||
RepoItemClickActionType.OpenDetail,
|
||||
!isSellState,
|
||||
isSellState,
|
||||
!isParticipantTower,
|
||||
isSellSelected,
|
||||
false);
|
||||
AddTowerItemContext(towerItems, towerContext);
|
||||
if (isSellSelected)
|
||||
{
|
||||
selectedSellTowerItems.Add(towerContext);
|
||||
}
|
||||
|
||||
AddItemDescSeed(
|
||||
tower.InstanceId,
|
||||
tower.Name,
|
||||
@@ -50,23 +73,24 @@ namespace GeometryTD.UI
|
||||
|
||||
if (rawData.Inventory.MuzzleComponents != null)
|
||||
{
|
||||
foreach (var item in rawData.Inventory.MuzzleComponents)
|
||||
foreach (MuzzleCompItemData item in rawData.Inventory.MuzzleComponents)
|
||||
{
|
||||
if (item == null || item.IsAssembledIntoTower)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RepoItemContext componentContext = new RepoItemContext
|
||||
{
|
||||
InstanceId = item.InstanceId,
|
||||
CanDrag = true,
|
||||
EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item),
|
||||
ClickActionType = RepoItemClickActionType.OpenDetail,
|
||||
ComponentSlotType = TowerCompSlotType.Muzzle,
|
||||
IconAreaContext = BuildIconAreaContext(item)
|
||||
};
|
||||
RepoItemContext componentContext = BuildSellableComponentContext(
|
||||
item,
|
||||
TowerCompSlotType.Muzzle,
|
||||
isSellState,
|
||||
selectedSellItemIds.Contains(item.InstanceId));
|
||||
AddComponentItemContext(componentItems, componentContext);
|
||||
if (componentContext.IsSellSelected)
|
||||
{
|
||||
selectedSellComponentItems.Add(componentContext);
|
||||
}
|
||||
|
||||
AddItemDescSeed(
|
||||
item.InstanceId,
|
||||
item.Name,
|
||||
@@ -86,16 +110,17 @@ namespace GeometryTD.UI
|
||||
continue;
|
||||
}
|
||||
|
||||
RepoItemContext componentContext = new RepoItemContext
|
||||
{
|
||||
InstanceId = item.InstanceId,
|
||||
CanDrag = true,
|
||||
EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item),
|
||||
ClickActionType = RepoItemClickActionType.OpenDetail,
|
||||
ComponentSlotType = TowerCompSlotType.Bearing,
|
||||
IconAreaContext = BuildIconAreaContext(item)
|
||||
};
|
||||
RepoItemContext componentContext = BuildSellableComponentContext(
|
||||
item,
|
||||
TowerCompSlotType.Bearing,
|
||||
isSellState,
|
||||
selectedSellItemIds.Contains(item.InstanceId));
|
||||
AddComponentItemContext(componentItems, componentContext);
|
||||
if (componentContext.IsSellSelected)
|
||||
{
|
||||
selectedSellComponentItems.Add(componentContext);
|
||||
}
|
||||
|
||||
AddItemDescSeed(
|
||||
item.InstanceId,
|
||||
item.Name,
|
||||
@@ -115,16 +140,17 @@ namespace GeometryTD.UI
|
||||
continue;
|
||||
}
|
||||
|
||||
RepoItemContext componentContext = new RepoItemContext
|
||||
{
|
||||
InstanceId = item.InstanceId,
|
||||
CanDrag = true,
|
||||
EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item),
|
||||
ClickActionType = RepoItemClickActionType.OpenDetail,
|
||||
ComponentSlotType = TowerCompSlotType.Base,
|
||||
IconAreaContext = BuildIconAreaContext(item)
|
||||
};
|
||||
RepoItemContext componentContext = BuildSellableComponentContext(
|
||||
item,
|
||||
TowerCompSlotType.Base,
|
||||
isSellState,
|
||||
selectedSellItemIds.Contains(item.InstanceId));
|
||||
AddComponentItemContext(componentItems, componentContext);
|
||||
if (componentContext.IsSellSelected)
|
||||
{
|
||||
selectedSellComponentItems.Add(componentContext);
|
||||
}
|
||||
|
||||
AddItemDescSeed(
|
||||
item.InstanceId,
|
||||
item.Name,
|
||||
@@ -140,7 +166,20 @@ namespace GeometryTD.UI
|
||||
|
||||
return new RepoFormContext
|
||||
{
|
||||
GoldText = $"金币: {rawData.Inventory.Gold}",
|
||||
State = rawData.State,
|
||||
ShowSellModeButton = rawData.State == RepoFormState.Assemble,
|
||||
SellModeButtonText = "出售模式",
|
||||
ShowCombineArea = rawData.State == RepoFormState.Assemble,
|
||||
ShowSellArea = rawData.State == RepoFormState.Sell,
|
||||
CombineAreaContext = new CombineAreaContext(),
|
||||
SellAreaContext = new SellAreaContext
|
||||
{
|
||||
TotalPriceText = $"总价值: {rawData.SelectedSellTotalPrice}",
|
||||
CanConfirmSell = rawData.State == RepoFormState.Sell && rawData.SelectedSellItemCount > 0,
|
||||
ComponentItems = selectedSellComponentItems.ToArray(),
|
||||
TowerItems = selectedSellTowerItems.ToArray()
|
||||
},
|
||||
CompAreaContext = new CompAreaContext
|
||||
{
|
||||
ComponentItems = componentItems.ToArray(),
|
||||
@@ -150,6 +189,26 @@ namespace GeometryTD.UI
|
||||
};
|
||||
}
|
||||
|
||||
private static RepoItemContext BuildSellableComponentContext(
|
||||
TowerCompItemData item,
|
||||
TowerCompSlotType slotType,
|
||||
bool isSellState,
|
||||
bool isSellSelected)
|
||||
{
|
||||
return new RepoItemContext
|
||||
{
|
||||
InstanceId = item.InstanceId,
|
||||
CanDrag = !isSellState,
|
||||
EnduranceRate01 = ItemDescUtility.ResolveComponentEnduranceRate(item),
|
||||
ClickActionType = RepoItemClickActionType.OpenDetail,
|
||||
ComponentSlotType = slotType,
|
||||
IsSellMode = isSellState,
|
||||
IsSellable = true,
|
||||
IsSellSelected = isSellState && isSellSelected,
|
||||
IconAreaContext = BuildIconAreaContext(item)
|
||||
};
|
||||
}
|
||||
|
||||
private void AddComponentItemContext(List<RepoItemContext> items, RepoItemContext itemContext)
|
||||
{
|
||||
if (itemContext == null)
|
||||
@@ -217,7 +276,7 @@ namespace GeometryTD.UI
|
||||
return map;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
foreach (TComp item in items)
|
||||
{
|
||||
if (item == null || item.InstanceId <= 0)
|
||||
{
|
||||
@@ -230,6 +289,26 @@ namespace GeometryTD.UI
|
||||
return map;
|
||||
}
|
||||
|
||||
private static HashSet<long> BuildSelectedSellItemSet(IReadOnlyList<long> selectedSellItemIds)
|
||||
{
|
||||
HashSet<long> selectedIds = new HashSet<long>();
|
||||
if (selectedSellItemIds == null)
|
||||
{
|
||||
return selectedIds;
|
||||
}
|
||||
|
||||
for (int i = 0; i < selectedSellItemIds.Count; i++)
|
||||
{
|
||||
long itemId = selectedSellItemIds[i];
|
||||
if (itemId > 0)
|
||||
{
|
||||
selectedIds.Add(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
return selectedIds;
|
||||
}
|
||||
|
||||
private static Dictionary<long, TowerItemData> BuildTowerMap(IReadOnlyList<TowerItemData> towers)
|
||||
{
|
||||
Dictionary<long, TowerItemData> map = new Dictionary<long, TowerItemData>();
|
||||
@@ -238,7 +317,7 @@ namespace GeometryTD.UI
|
||||
return map;
|
||||
}
|
||||
|
||||
foreach (var tower in towers)
|
||||
foreach (TowerItemData tower in towers)
|
||||
{
|
||||
if (tower == null || tower.InstanceId <= 0)
|
||||
{
|
||||
@@ -262,7 +341,7 @@ namespace GeometryTD.UI
|
||||
List<TowerRepoItemContext> participantItems = new List<TowerRepoItemContext>();
|
||||
if (inventory?.ParticipantTowerInstanceIds != null && towerMap != null)
|
||||
{
|
||||
foreach (var towerId in inventory.ParticipantTowerInstanceIds)
|
||||
foreach (long towerId in inventory.ParticipantTowerInstanceIds)
|
||||
{
|
||||
if (towerId <= 0)
|
||||
{
|
||||
@@ -279,8 +358,17 @@ namespace GeometryTD.UI
|
||||
continue;
|
||||
}
|
||||
|
||||
TowerRepoItemContext towerContext = BuildTowerRepoItemContext(tower, muzzleMap, bearingMap, baseMap,
|
||||
RepoItemClickActionType.RemoveParticipant, false);
|
||||
TowerRepoItemContext towerContext = BuildTowerRepoItemContext(
|
||||
tower,
|
||||
muzzleMap,
|
||||
bearingMap,
|
||||
baseMap,
|
||||
RepoItemClickActionType.RemoveParticipant,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false);
|
||||
if (towerContext != null)
|
||||
{
|
||||
participantItems.Add(towerContext);
|
||||
@@ -303,6 +391,11 @@ namespace GeometryTD.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (Context != null && Context.State == RepoFormState.Sell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (long towerId in _compAreaTowerIds)
|
||||
{
|
||||
Form.SetRepoItemSelected(towerId, _participantTowerIds.Contains(towerId));
|
||||
@@ -322,6 +415,7 @@ namespace GeometryTD.UI
|
||||
return;
|
||||
}
|
||||
|
||||
Form.RefreshGoldText(latestContext.GoldText);
|
||||
Form.RefreshParticipantArea(latestContext.ParticipantAreaContext);
|
||||
ApplyParticipantSelection();
|
||||
}
|
||||
@@ -332,7 +426,11 @@ namespace GeometryTD.UI
|
||||
IReadOnlyDictionary<long, BearingCompItemData> bearingMap,
|
||||
IReadOnlyDictionary<long, BaseCompItemData> baseMap,
|
||||
RepoItemClickActionType clickActionType,
|
||||
bool canDrag)
|
||||
bool canDrag,
|
||||
bool isSellMode,
|
||||
bool isSellable,
|
||||
bool isSellSelected,
|
||||
bool highlightSelected)
|
||||
{
|
||||
if (tower == null)
|
||||
{
|
||||
@@ -346,6 +444,9 @@ namespace GeometryTD.UI
|
||||
EnduranceRate01 = ItemDescUtility.ResolveTowerEnduranceRate(tower, muzzleMap, bearingMap, baseMap),
|
||||
ClickActionType = clickActionType,
|
||||
ComponentSlotType = TowerCompSlotType.None,
|
||||
IsSellMode = isSellMode,
|
||||
IsSellable = isSellable,
|
||||
IsSellSelected = isSellSelected || highlightSelected,
|
||||
IconAreaContext = new TowerIconAreaContext
|
||||
{
|
||||
Rarity = tower.Rarity,
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace GeometryTD.UI
|
||||
|
||||
private static EventFormContext BuildContext(EventFormRawData rawData)
|
||||
{
|
||||
if (rawData?.EventItem == null)
|
||||
if (rawData == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -82,24 +82,30 @@ namespace GeometryTD.UI
|
||||
for (int i = 0; i < options.Length; i++)
|
||||
{
|
||||
string optionText = string.Empty;
|
||||
if (rawData.EventItem.Options != null && i < rawData.EventItem.Options.Length &&
|
||||
rawData.EventItem.Options[i] != null)
|
||||
bool isSelectable = false;
|
||||
string blockedReason = string.Empty;
|
||||
if (rawData.OptionItems != null && i < rawData.OptionItems.Length &&
|
||||
rawData.OptionItems[i] != null)
|
||||
{
|
||||
optionText = rawData.EventItem.Options[i].OptionText;
|
||||
optionText = rawData.OptionItems[i].OptionText;
|
||||
isSelectable = rawData.OptionItems[i].IsSelectable;
|
||||
blockedReason = rawData.OptionItems[i].BlockedReason;
|
||||
}
|
||||
|
||||
options[i] = new EventOptionItemContext
|
||||
{
|
||||
OptionIndex = i,
|
||||
OptionText = optionText
|
||||
OptionText = optionText,
|
||||
IsSelectable = isSelectable,
|
||||
BlockedReason = blockedReason
|
||||
};
|
||||
}
|
||||
|
||||
return new EventFormContext
|
||||
{
|
||||
EventId = rawData.EventItem.Id,
|
||||
Title = rawData.EventItem.Title,
|
||||
Description = rawData.EventItem.Description,
|
||||
EventId = rawData.EventId,
|
||||
Title = rawData.Title,
|
||||
Description = rawData.Description,
|
||||
OptionItems = options
|
||||
};
|
||||
}
|
||||
@@ -117,7 +123,7 @@ namespace GeometryTD.UI
|
||||
return;
|
||||
}
|
||||
|
||||
m_UseCase.SelectOption(args.SelectedItemId);
|
||||
m_UseCase.TrySelectOption(args.SelectedItemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using GeometryTD.CustomEvent;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using GameFramework.Event;
|
||||
using GeometryTD.CustomComponent;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
@@ -43,6 +44,9 @@ namespace GeometryTD.UI
|
||||
GameEntry.Event.Subscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested);
|
||||
GameEntry.Event.Subscribe(RepoParticipantAssignRequestedEventArgs.EventId, OnRepoParticipantAssignRequested);
|
||||
GameEntry.Event.Subscribe(RepoFormReturnEventArgs.EventId, OnRepoFormReturn);
|
||||
GameEntry.Event.Subscribe(RepoSellModeToggleRequestedEventArgs.EventId, OnRepoSellModeToggleRequested);
|
||||
GameEntry.Event.Subscribe(RepoSellCancelRequestedEventArgs.EventId, OnRepoSellCancelRequested);
|
||||
GameEntry.Event.Subscribe(RepoSellConfirmRequestedEventArgs.EventId, OnRepoSellConfirmRequested);
|
||||
}
|
||||
|
||||
protected override void UnsubscribeCustomEvents()
|
||||
@@ -53,6 +57,9 @@ namespace GeometryTD.UI
|
||||
GameEntry.Event.Unsubscribe(RepoCombineRequestedEventArgs.EventId, OnRepoCombineRequested);
|
||||
GameEntry.Event.Unsubscribe(RepoParticipantAssignRequestedEventArgs.EventId, OnRepoParticipantAssignRequested);
|
||||
GameEntry.Event.Unsubscribe(RepoFormReturnEventArgs.EventId, OnRepoFormReturn);
|
||||
GameEntry.Event.Unsubscribe(RepoSellModeToggleRequestedEventArgs.EventId, OnRepoSellModeToggleRequested);
|
||||
GameEntry.Event.Unsubscribe(RepoSellCancelRequestedEventArgs.EventId, OnRepoSellCancelRequested);
|
||||
GameEntry.Event.Unsubscribe(RepoSellConfirmRequestedEventArgs.EventId, OnRepoSellConfirmRequested);
|
||||
}
|
||||
|
||||
public override int? OpenUI(object userData = null)
|
||||
@@ -125,6 +132,24 @@ namespace GeometryTD.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (_useCase != null && _useCase.State == RepoFormState.Sell)
|
||||
{
|
||||
if (_useCase.TryToggleSellSelection(
|
||||
args.ItemId,
|
||||
out RepoFormRawData sellModeRawData,
|
||||
out PlayerInventorySaleFailureReason failureReason))
|
||||
{
|
||||
SetContext(BuildContext(sellModeRawData));
|
||||
RefreshCurrentUI();
|
||||
}
|
||||
else if (failureReason == PlayerInventorySaleFailureReason.ParticipantTower)
|
||||
{
|
||||
OpenSellBlockedDialog("参战防御塔不能直接出售,请先移出参战区。");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (clickActionType == RepoItemClickActionType.RemoveParticipant)
|
||||
{
|
||||
if (_useCase == null || Form == null)
|
||||
@@ -165,6 +190,11 @@ namespace GeometryTD.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (Context != null && Context.State == RepoFormState.Sell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e is RepoItemDragEndedEventArgs args))
|
||||
{
|
||||
return;
|
||||
@@ -188,6 +218,11 @@ namespace GeometryTD.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (Context != null && Context.State == RepoFormState.Sell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e is CombineSlotClickedEventArgs args))
|
||||
{
|
||||
return;
|
||||
@@ -245,8 +280,8 @@ namespace GeometryTD.UI
|
||||
RepoFormContext latestContext = BuildContext(latestRawData);
|
||||
if (latestContext != null)
|
||||
{
|
||||
Form.RefreshUI(latestContext);
|
||||
ApplyParticipantSelection();
|
||||
SetContext(latestContext);
|
||||
RefreshCurrentUI();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +292,11 @@ namespace GeometryTD.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (Context != null && Context.State == RepoFormState.Sell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e is RepoParticipantAssignRequestedEventArgs args))
|
||||
{
|
||||
return;
|
||||
@@ -288,6 +328,65 @@ namespace GeometryTD.UI
|
||||
RefreshParticipantAreaOnly();
|
||||
}
|
||||
|
||||
private void OnRepoSellModeToggleRequested(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!IsEventFromCurrentForm(sender) || !(e is RepoSellModeToggleRequestedEventArgs) || _useCase == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_useCase.State != RepoFormState.Assemble)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RepoFormRawData rawData = _useCase.EnterSellState();
|
||||
SetContext(BuildContext(rawData));
|
||||
RefreshCurrentUI();
|
||||
}
|
||||
|
||||
private void OnRepoSellCancelRequested(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!IsEventFromCurrentForm(sender) || !(e is RepoSellCancelRequestedEventArgs) || _useCase == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_useCase.State != RepoFormState.Sell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetContext(BuildContext(_useCase.ExitSellState()));
|
||||
RefreshCurrentUI();
|
||||
}
|
||||
|
||||
private void OnRepoSellConfirmRequested(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!IsEventFromCurrentForm(sender) || !(e is RepoSellConfirmRequestedEventArgs) || _useCase == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_useCase.TryConfirmSellSelection(out RepoFormRawData rawData, out PlayerInventorySaleResult result))
|
||||
{
|
||||
if (result != null && result.FailureReason == PlayerInventorySaleFailureReason.ParticipantTower)
|
||||
{
|
||||
OpenSellBlockedDialog("参战防御塔不能直接出售,请先移出参战区。");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SetContext(BuildContext(rawData));
|
||||
RefreshCurrentUI();
|
||||
|
||||
if (GameEntry.UI.GetUIForm(UIFormType.ShopForm) != null)
|
||||
{
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.ShopForm);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsEventFromCurrentForm(object sender)
|
||||
{
|
||||
if (Form == null)
|
||||
@@ -309,6 +408,18 @@ namespace GeometryTD.UI
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void OpenSellBlockedDialog(string message)
|
||||
{
|
||||
GameEntry.UIRouter.OpenUI(UIFormType.DialogForm, new DialogFormRawData
|
||||
{
|
||||
Mode = 1,
|
||||
Title = "无法出售",
|
||||
Message = message,
|
||||
PauseGame = false,
|
||||
ConfirmText = "知道了"
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
{
|
||||
public class EventFormRawData
|
||||
{
|
||||
public EventItem EventItem;
|
||||
public int EventId;
|
||||
public string Title;
|
||||
public string Description;
|
||||
public EventOptionRawData[] OptionItems;
|
||||
}
|
||||
|
||||
public sealed class EventOptionRawData
|
||||
{
|
||||
public int OptionIndex;
|
||||
public string OptionText;
|
||||
public bool IsSelectable;
|
||||
public string BlockedReason;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,9 @@ namespace GeometryTD.UI
|
||||
public class RepoFormRawData
|
||||
{
|
||||
public BackpackInventoryData Inventory;
|
||||
public RepoFormState State;
|
||||
public long[] SelectedSellItemIds;
|
||||
public int SelectedSellItemCount;
|
||||
public int SelectedSellTotalPrice;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using GeometryTD.CustomComponent;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Procedure;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
@@ -8,10 +7,21 @@ namespace GeometryTD.UI
|
||||
public class EventFormUseCase : IUIUseCase
|
||||
{
|
||||
private EventItem _currentEvent;
|
||||
private RunNodeExecutionContext _currentContext;
|
||||
private readonly EventOptionExecutor _executor = new EventOptionExecutor();
|
||||
|
||||
public void SetCurrentEvent(EventItem eventItem)
|
||||
public void BindEvent(
|
||||
EventItem eventItem,
|
||||
RunNodeExecutionContext context)
|
||||
{
|
||||
_currentEvent = eventItem;
|
||||
_currentContext = context != null ? context.Clone() : null;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_currentEvent = null;
|
||||
_currentContext = null;
|
||||
}
|
||||
|
||||
public EventFormRawData CreateInitialModel()
|
||||
@@ -23,26 +33,74 @@ namespace GeometryTD.UI
|
||||
|
||||
return new EventFormRawData
|
||||
{
|
||||
EventItem = _currentEvent
|
||||
EventId = _currentEvent.Id,
|
||||
Title = _currentEvent.Title,
|
||||
Description = _currentEvent.Description,
|
||||
OptionItems = BuildOptionItems()
|
||||
};
|
||||
}
|
||||
|
||||
public EventOption SelectOption(int optionIndex)
|
||||
public bool TrySelectOption(int optionIndex)
|
||||
{
|
||||
if (_currentEvent == null || _currentEvent.Options == null)
|
||||
{
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (optionIndex < 0 || optionIndex >= _currentEvent.Options.Length)
|
||||
{
|
||||
Log.Warning("EventFormUseCase.SelectOption() option index is invalid: {0}", optionIndex);
|
||||
return null;
|
||||
Log.Warning("EventFormUseCase.TrySelectOption() option index is invalid: {0}", optionIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData workingInventory = GameEntry.PlayerInventory != null
|
||||
? GameEntry.PlayerInventory.GetInventorySnapshot()
|
||||
: new BackpackInventoryData();
|
||||
EventOptionExecutionResult executionResult = _executor.Execute(
|
||||
_currentEvent,
|
||||
optionIndex,
|
||||
_currentContext,
|
||||
workingInventory);
|
||||
if (!executionResult.IsAccepted)
|
||||
{
|
||||
Log.Warning(
|
||||
"EventFormUseCase.TrySelectOption() rejected. EventId={0}, OptionIndex={1}, Reason={2}",
|
||||
_currentEvent.Id,
|
||||
optionIndex,
|
||||
executionResult.FailureReason);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GameEntry.PlayerInventory != null)
|
||||
{
|
||||
GameEntry.PlayerInventory.ReplaceInventorySnapshot(workingInventory);
|
||||
}
|
||||
|
||||
// TODO: 执行 Requirements 校验、CostEffects/RewardEffects 结算与后续节点推进。
|
||||
GameEntry.EventNode.EndEvent();
|
||||
return _currentEvent.Options[optionIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
private EventOptionRawData[] BuildOptionItems()
|
||||
{
|
||||
EventOption[] options = _currentEvent.Options ?? System.Array.Empty<EventOption>();
|
||||
EventOptionRawData[] result = new EventOptionRawData[options.Length];
|
||||
BackpackInventoryData inventory = GameEntry.PlayerInventory != null
|
||||
? GameEntry.PlayerInventory.GetInventorySnapshot()
|
||||
: new BackpackInventoryData();
|
||||
for (int i = 0; i < options.Length; i++)
|
||||
{
|
||||
EventOption option = options[i];
|
||||
EventOptionAvailability availability = _executor.EvaluateOption(option, inventory);
|
||||
result[i] = new EventOptionRawData
|
||||
{
|
||||
OptionIndex = i,
|
||||
OptionText = option?.OptionText ?? string.Empty,
|
||||
IsSelectable = availability.IsSelectable,
|
||||
BlockedReason = availability.BlockedReason
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using GeometryTD.CustomUtility;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomComponent;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
@@ -7,6 +9,8 @@ namespace GeometryTD.UI
|
||||
{
|
||||
private const int MaxParticipantCount = 4;
|
||||
private BackpackInventoryData _fallbackInventory;
|
||||
private readonly HashSet<long> _selectedSellItemIds = new HashSet<long>();
|
||||
private RepoFormState _state = RepoFormState.Assemble;
|
||||
|
||||
public RepoFormRawData CreateInitialModel()
|
||||
{
|
||||
@@ -15,12 +19,23 @@ namespace GeometryTD.UI
|
||||
: GetOrCreateFallbackInventory();
|
||||
return new RepoFormRawData
|
||||
{
|
||||
Inventory = sample
|
||||
Inventory = sample,
|
||||
State = _state,
|
||||
SelectedSellItemIds = BuildSelectedSellItemArray(),
|
||||
SelectedSellItemCount = _selectedSellItemIds.Count,
|
||||
SelectedSellTotalPrice = ResolveSelectedSellTotalPrice()
|
||||
};
|
||||
}
|
||||
|
||||
public RepoFormState State => _state;
|
||||
|
||||
public bool TryAssembleTower(long muzzleItemId, long bearingItemId, long baseItemId)
|
||||
{
|
||||
if (_state == RepoFormState.Sell)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GameEntry.PlayerInventory == null)
|
||||
{
|
||||
return false;
|
||||
@@ -35,6 +50,15 @@ namespace GeometryTD.UI
|
||||
|
||||
public ParticipantTowerAssignResult TryAddParticipantTower(long towerItemId)
|
||||
{
|
||||
if (_state == RepoFormState.Sell)
|
||||
{
|
||||
return new ParticipantTowerAssignResult
|
||||
{
|
||||
TowerInstanceId = towerItemId,
|
||||
FailureReason = ParticipantTowerAssignFailureReason.ParticipantAreaFull
|
||||
};
|
||||
}
|
||||
|
||||
if (GameEntry.PlayerInventory == null)
|
||||
{
|
||||
BackpackInventoryData fallbackInventory = GetOrCreateFallbackInventory();
|
||||
@@ -44,11 +68,16 @@ namespace GeometryTD.UI
|
||||
MaxParticipantCount);
|
||||
}
|
||||
|
||||
return GameEntry.PlayerInventory.TryAddParticipantTower(towerItemId, 4);
|
||||
return GameEntry.PlayerInventory.TryAddParticipantTower(towerItemId, MaxParticipantCount);
|
||||
}
|
||||
|
||||
public bool TryRemoveParticipantTower(long towerItemId)
|
||||
{
|
||||
if (_state == RepoFormState.Sell)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GameEntry.PlayerInventory == null)
|
||||
{
|
||||
BackpackInventoryData fallbackInventory = GetOrCreateFallbackInventory();
|
||||
@@ -61,12 +90,120 @@ namespace GeometryTD.UI
|
||||
return GameEntry.PlayerInventory.TryRemoveParticipantTower(towerItemId);
|
||||
}
|
||||
|
||||
public RepoFormRawData EnterSellState()
|
||||
{
|
||||
_state = RepoFormState.Sell;
|
||||
_selectedSellItemIds.Clear();
|
||||
return CreateInitialModel();
|
||||
}
|
||||
|
||||
public RepoFormRawData ExitSellState()
|
||||
{
|
||||
_state = RepoFormState.Assemble;
|
||||
_selectedSellItemIds.Clear();
|
||||
return CreateInitialModel();
|
||||
}
|
||||
|
||||
public bool TryToggleSellSelection(
|
||||
long itemId,
|
||||
out RepoFormRawData rawData,
|
||||
out PlayerInventorySaleFailureReason failureReason)
|
||||
{
|
||||
rawData = null;
|
||||
failureReason = PlayerInventorySaleFailureReason.None;
|
||||
if (_state != RepoFormState.Sell || itemId <= 0 || GameEntry.PlayerInventory == null)
|
||||
{
|
||||
failureReason = PlayerInventorySaleFailureReason.InvalidSelection;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_selectedSellItemIds.Contains(itemId))
|
||||
{
|
||||
_selectedSellItemIds.Remove(itemId);
|
||||
rawData = CreateInitialModel();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!GameEntry.PlayerInventory.TryGetSaleCandidate(itemId, out PlayerInventorySaleCandidate candidate) ||
|
||||
candidate == null)
|
||||
{
|
||||
failureReason = PlayerInventorySaleFailureReason.ItemNotFound;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!candidate.IsSellable)
|
||||
{
|
||||
failureReason = candidate.FailureReason;
|
||||
return false;
|
||||
}
|
||||
|
||||
_selectedSellItemIds.Add(itemId);
|
||||
rawData = CreateInitialModel();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryConfirmSellSelection(out RepoFormRawData rawData, out PlayerInventorySaleResult result)
|
||||
{
|
||||
rawData = null;
|
||||
result = null;
|
||||
if (_state != RepoFormState.Sell || _selectedSellItemIds.Count <= 0 || GameEntry.PlayerInventory == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!GameEntry.PlayerInventory.TrySellItems(_selectedSellItemIds, out PlayerInventorySaleResult saleResult) ||
|
||||
saleResult == null ||
|
||||
!saleResult.IsSuccess)
|
||||
{
|
||||
result = saleResult;
|
||||
rawData = CreateInitialModel();
|
||||
return false;
|
||||
}
|
||||
|
||||
_selectedSellItemIds.Clear();
|
||||
result = saleResult;
|
||||
rawData = CreateInitialModel();
|
||||
return true;
|
||||
}
|
||||
|
||||
private BackpackInventoryData GetOrCreateFallbackInventory()
|
||||
{
|
||||
_fallbackInventory ??= InventorySeedUtility.CreateSampleInventory();
|
||||
InventoryParticipantUtility.NormalizeParticipantState(_fallbackInventory, MaxParticipantCount);
|
||||
return _fallbackInventory;
|
||||
}
|
||||
|
||||
private long[] BuildSelectedSellItemArray()
|
||||
{
|
||||
if (_selectedSellItemIds.Count <= 0)
|
||||
{
|
||||
return System.Array.Empty<long>();
|
||||
}
|
||||
|
||||
long[] itemIds = new long[_selectedSellItemIds.Count];
|
||||
_selectedSellItemIds.CopyTo(itemIds);
|
||||
return itemIds;
|
||||
}
|
||||
|
||||
private int ResolveSelectedSellTotalPrice()
|
||||
{
|
||||
if (_state != RepoFormState.Sell || _selectedSellItemIds.Count <= 0 || GameEntry.PlayerInventory == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
foreach (long itemId in _selectedSellItemIds)
|
||||
{
|
||||
if (GameEntry.PlayerInventory.TryGetSaleCandidate(itemId, out PlayerInventorySaleCandidate candidate) &&
|
||||
candidate != null &&
|
||||
candidate.IsSellable)
|
||||
{
|
||||
total += candidate.Price;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace GeometryTD.UI
|
||||
public class CombineArea : MonoBehaviour, IDropHandler
|
||||
{
|
||||
[SerializeField] private CombineSlotItem[] _slots;
|
||||
private bool _isInteractionEnabled = true;
|
||||
|
||||
public void OnInit(CombineAreaContext context)
|
||||
{
|
||||
@@ -43,6 +44,11 @@ namespace GeometryTD.UI
|
||||
|
||||
public void OnCombineClick()
|
||||
{
|
||||
if (!_isInteractionEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryGetBoundItemId(TowerCompSlotType.Muzzle, out long muzzleItemId) ||
|
||||
!TryGetBoundItemId(TowerCompSlotType.Bearing, out long bearingItemId) ||
|
||||
!TryGetBoundItemId(TowerCompSlotType.Base, out long baseItemId))
|
||||
@@ -55,6 +61,11 @@ namespace GeometryTD.UI
|
||||
|
||||
public bool TryAssignItem(RepoItemContext itemContext)
|
||||
{
|
||||
if (!_isInteractionEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (itemContext == null || itemContext.ComponentSlotType == TowerCompSlotType.None)
|
||||
{
|
||||
return false;
|
||||
@@ -91,6 +102,11 @@ namespace GeometryTD.UI
|
||||
|
||||
public void OnDrop(PointerEventData eventData)
|
||||
{
|
||||
if (!_isInteractionEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventData == null)
|
||||
{
|
||||
return;
|
||||
@@ -113,6 +129,11 @@ namespace GeometryTD.UI
|
||||
dragItem.SetDropResult(assigned);
|
||||
}
|
||||
|
||||
public void SetInteractionEnabled(bool enabled)
|
||||
{
|
||||
_isInteractionEnabled = enabled;
|
||||
}
|
||||
|
||||
private CombineSlotItem FindSlot(TowerCompSlotType slotType)
|
||||
{
|
||||
if (_slots == null)
|
||||
|
||||
@@ -8,7 +8,17 @@ namespace GeometryTD.UI
|
||||
{
|
||||
[SerializeField] private TMP_Text _optionText;
|
||||
|
||||
private static readonly Color SelectableTextColor = Color.white;
|
||||
private static readonly Color BlockedTextColor = new Color(0.75f, 0.75f, 0.75f, 1f);
|
||||
|
||||
private int _optionIndex;
|
||||
private bool _isSelectable;
|
||||
private CommonButton _commonButton;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_commonButton = GetComponent<CommonButton>();
|
||||
}
|
||||
|
||||
public void OnInit(EventOptionItemContext context)
|
||||
{
|
||||
@@ -20,9 +30,16 @@ namespace GeometryTD.UI
|
||||
}
|
||||
|
||||
_optionIndex = context.OptionIndex;
|
||||
_isSelectable = context.IsSelectable;
|
||||
if (_optionText != null)
|
||||
{
|
||||
_optionText.text = context.OptionText ?? string.Empty;
|
||||
_optionText.text = BuildDisplayText(context);
|
||||
_optionText.color = _isSelectable ? SelectableTextColor : BlockedTextColor;
|
||||
}
|
||||
|
||||
if (_commonButton != null)
|
||||
{
|
||||
_commonButton.Interactive = _isSelectable;
|
||||
}
|
||||
|
||||
gameObject.SetActive(!string.IsNullOrEmpty(context.OptionText));
|
||||
@@ -31,20 +48,38 @@ namespace GeometryTD.UI
|
||||
public void OnReset()
|
||||
{
|
||||
_optionIndex = -1;
|
||||
_isSelectable = false;
|
||||
if (_optionText != null)
|
||||
{
|
||||
_optionText.text = string.Empty;
|
||||
_optionText.color = SelectableTextColor;
|
||||
}
|
||||
|
||||
if (_commonButton != null)
|
||||
{
|
||||
_commonButton.Interactive = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnClick()
|
||||
{
|
||||
if (_optionIndex < 0)
|
||||
if (_optionIndex < 0 || !_isSelectable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Fire(this, EventOptionItemSelectedEventArgs.Create(_optionIndex));
|
||||
}
|
||||
|
||||
private static string BuildDisplayText(EventOptionItemContext context)
|
||||
{
|
||||
string optionText = context.OptionText ?? string.Empty;
|
||||
if (context.IsSelectable || string.IsNullOrEmpty(context.BlockedReason))
|
||||
{
|
||||
return optionText;
|
||||
}
|
||||
|
||||
return $"{optionText}\n<size=75%>{context.BlockedReason}</size>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace GeometryTD.UI
|
||||
[SerializeField] private Transform _content;
|
||||
[SerializeField] private TowerRepoItem _towerItemTemplate;
|
||||
[SerializeField] private int _instancePoolCapacity = 8;
|
||||
private bool _isInteractionEnabled = true;
|
||||
|
||||
private readonly List<TowerRepoItem> _activeItems = new List<TowerRepoItem>();
|
||||
private readonly HashSet<long> _boundItemIds = new HashSet<long>();
|
||||
@@ -81,6 +82,11 @@ namespace GeometryTD.UI
|
||||
|
||||
public bool CanAssign(IRepoDragItemView dragItem)
|
||||
{
|
||||
if (!_isInteractionEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dragItem == null || dragItem.InstanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
@@ -106,6 +112,11 @@ namespace GeometryTD.UI
|
||||
|
||||
public void OnDrop(PointerEventData eventData)
|
||||
{
|
||||
if (!_isInteractionEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventData == null)
|
||||
{
|
||||
return;
|
||||
@@ -134,6 +145,11 @@ namespace GeometryTD.UI
|
||||
GameEntry.Event.Fire(this, RepoParticipantAssignRequestedEventArgs.Create(dragItem.InstanceId));
|
||||
}
|
||||
|
||||
public void SetInteractionEnabled(bool enabled)
|
||||
{
|
||||
_isInteractionEnabled = enabled;
|
||||
}
|
||||
|
||||
private void EnsurePool()
|
||||
{
|
||||
if (_itemPool != null)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using GeometryTD.CustomEvent;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
@@ -7,10 +9,12 @@ namespace GeometryTD.UI
|
||||
public class RepoForm : UGuiForm
|
||||
{
|
||||
[SerializeField] private CombineArea _combineArea;
|
||||
|
||||
[SerializeField] private CompArea _compArea;
|
||||
|
||||
[SerializeField] private SellArea _sellArea;
|
||||
[SerializeField] private ParticipantArea _participantArea;
|
||||
[SerializeField] private TMP_Text _goldText;
|
||||
[SerializeField] private CommonButton _sellModeButton;
|
||||
[SerializeField] private TMP_Text _sellModeButtonText;
|
||||
|
||||
public void RefreshUI(RepoFormContext context)
|
||||
{
|
||||
@@ -19,9 +23,34 @@ namespace GeometryTD.UI
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshGoldText(context.GoldText);
|
||||
RefreshStateUI(context);
|
||||
|
||||
_combineArea?.OnInit(context.CombineAreaContext);
|
||||
_compArea?.OnInit(context.CompAreaContext);
|
||||
_sellArea?.OnInit(context.SellAreaContext);
|
||||
_participantArea?.OnInit(context.ParticipantAreaContext);
|
||||
|
||||
if (_combineArea != null)
|
||||
{
|
||||
_combineArea.gameObject.SetActive(context.ShowCombineArea);
|
||||
_combineArea.SetInteractionEnabled(context.State == RepoFormState.Assemble);
|
||||
}
|
||||
|
||||
if (_sellArea != null)
|
||||
{
|
||||
_sellArea.gameObject.SetActive(context.ShowSellArea);
|
||||
}
|
||||
|
||||
_participantArea?.SetInteractionEnabled(context.State == RepoFormState.Assemble);
|
||||
}
|
||||
|
||||
public void RefreshGoldText(string goldText)
|
||||
{
|
||||
if (_goldText != null)
|
||||
{
|
||||
_goldText.text = goldText ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryAssignItemToCombineArea(RepoItemContext itemContext)
|
||||
@@ -69,8 +98,14 @@ namespace GeometryTD.UI
|
||||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
if (_goldText != null)
|
||||
{
|
||||
_goldText.text = string.Empty;
|
||||
}
|
||||
|
||||
_combineArea?.OnReset();
|
||||
_compArea?.OnReset();
|
||||
_sellArea?.OnReset();
|
||||
_participantArea?.OnReset();
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
@@ -79,5 +114,23 @@ namespace GeometryTD.UI
|
||||
{
|
||||
GameEntry.Event.Fire(this, RepoFormReturnEventArgs.Create());
|
||||
}
|
||||
|
||||
public void OnSellModeButtonClick()
|
||||
{
|
||||
GameEntry.Event.Fire(this, RepoSellModeToggleRequestedEventArgs.Create());
|
||||
}
|
||||
|
||||
private void RefreshStateUI(RepoFormContext context)
|
||||
{
|
||||
if (_sellModeButton != null)
|
||||
{
|
||||
_sellModeButton.gameObject.SetActive(context != null && context.ShowSellModeButton);
|
||||
}
|
||||
|
||||
if (_sellModeButtonText != null)
|
||||
{
|
||||
_sellModeButtonText.text = context?.SellModeButtonText ?? "出售模式";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace GeometryTD.UI
|
||||
[SerializeField] private IconArea _iconArea;
|
||||
|
||||
private static readonly Color SelectedColor = new Color32(255, 216, 102, 255);
|
||||
private static readonly Color DisabledSellColor = new Color32(96, 96, 96, 255);
|
||||
private static readonly Color EmptyEnduranceColor = new Color(0.8f, 0f, 0f, 1f);
|
||||
private static readonly Color FullEnduranceColor = new Color(0f, 0.8f, 0f, 1f);
|
||||
private static readonly Vector2 DefaultDragGhostSize = new Vector2(64f, 64f);
|
||||
@@ -72,7 +73,7 @@ namespace GeometryTD.UI
|
||||
_context = context;
|
||||
_iconArea.OnInit(context.IconAreaContext);
|
||||
|
||||
SetSelected(false);
|
||||
SetSelected(context.IsSellSelected);
|
||||
ResetDragState();
|
||||
}
|
||||
|
||||
@@ -104,6 +105,12 @@ namespace GeometryTD.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (_context != null && _context.IsSellMode && !_context.IsSellable)
|
||||
{
|
||||
_bgImage.color = DisabledSellColor;
|
||||
return;
|
||||
}
|
||||
|
||||
float enduranceRate = _context != null ? Mathf.Clamp01(_context.EnduranceRate01) : 1f;
|
||||
_bgImage.color = Color.Lerp(EmptyEnduranceColor, FullEnduranceColor, enduranceRate);
|
||||
}
|
||||
@@ -212,6 +219,11 @@ namespace GeometryTD.UI
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_context.IsSellMode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_isSelected && _context.ComponentSlotType != TowerCompSlotType.None)
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomEvent;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
{
|
||||
public class SellArea : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Transform _content;
|
||||
[SerializeField] private RepoItem _itemTemplate;
|
||||
[SerializeField] private TowerRepoItem _towerItemTemplate;
|
||||
[SerializeField] private TMP_Text _totalPrice;
|
||||
|
||||
private readonly List<RepoItem> _activeComponentItems = new List<RepoItem>();
|
||||
private readonly List<TowerRepoItem> _activeTowerItems = new List<TowerRepoItem>();
|
||||
private SellAreaContext _context;
|
||||
|
||||
public void OnInit(SellAreaContext context)
|
||||
{
|
||||
OnReset();
|
||||
_context = context;
|
||||
|
||||
if (_totalPrice != null)
|
||||
{
|
||||
_totalPrice.text = context?.TotalPriceText ?? string.Empty;
|
||||
}
|
||||
|
||||
if (context == null || _content == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.ComponentItems != null)
|
||||
{
|
||||
foreach (RepoItemContext itemContext in context.ComponentItems)
|
||||
{
|
||||
if (itemContext == null || _itemTemplate == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RepoItem item = Instantiate(_itemTemplate, _content);
|
||||
item.gameObject.SetActive(true);
|
||||
item.OnInit(itemContext);
|
||||
_activeComponentItems.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.TowerItems != null)
|
||||
{
|
||||
foreach (TowerRepoItemContext itemContext in context.TowerItems)
|
||||
{
|
||||
if (itemContext == null || _towerItemTemplate == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TowerRepoItem item = Instantiate(_towerItemTemplate, _content);
|
||||
item.gameObject.SetActive(true);
|
||||
item.OnInit(itemContext);
|
||||
_activeTowerItems.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnReset()
|
||||
{
|
||||
for (int i = _activeComponentItems.Count - 1; i >= 0; i--)
|
||||
{
|
||||
RepoItem item = _activeComponentItems[i];
|
||||
if (item != null)
|
||||
{
|
||||
Destroy(item.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = _activeTowerItems.Count - 1; i >= 0; i--)
|
||||
{
|
||||
TowerRepoItem item = _activeTowerItems[i];
|
||||
if (item != null)
|
||||
{
|
||||
Destroy(item.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
_activeComponentItems.Clear();
|
||||
_activeTowerItems.Clear();
|
||||
_context = null;
|
||||
|
||||
if (_totalPrice != null)
|
||||
{
|
||||
_totalPrice.text = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnCancelButtonClick()
|
||||
{
|
||||
GameEntry.Event.Fire(this, RepoSellCancelRequestedEventArgs.Create());
|
||||
}
|
||||
|
||||
public void OnConfirmButtonClick()
|
||||
{
|
||||
if (_context == null || !_context.CanConfirmSell)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Fire(this, RepoSellConfirmRequestedEventArgs.Create());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 548fcad4dc8202f41b67f48aab7bed13
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -110,7 +110,7 @@ namespace GeometryTD.UI
|
||||
return;
|
||||
}
|
||||
|
||||
RewardSelectFormRawData nextRawData = _useCase.TryRefresh();
|
||||
RewardSelectFormRawData nextRawData = _useCase.TryRotateSelection();
|
||||
if (nextRawData == null)
|
||||
{
|
||||
CloseUI();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
{
|
||||
@@ -15,16 +15,20 @@ namespace GeometryTD.UI
|
||||
|
||||
private int _displayCount = 3;
|
||||
private int _refreshCost;
|
||||
private bool _allowRefreshOnce = true;
|
||||
private bool _allowRotateOnce = true;
|
||||
private bool _allowGiveUp = true;
|
||||
private bool _hasRefreshed;
|
||||
private bool _hasRotated;
|
||||
private int _selectionOffset;
|
||||
private string _tipText = "Select one reward";
|
||||
|
||||
// RewardSelectForm keeps a fixed reward pool for the current node.
|
||||
// The "refresh" action only rotates which slice of that pool is shown so save/load
|
||||
// can reopen the same node with the exact same reward contents.
|
||||
public void ConfigureRewardPool(
|
||||
IReadOnlyList<RewardSelectItemRawData> rewardPool,
|
||||
int displayCount = 3,
|
||||
int refreshCost = 0,
|
||||
bool allowRefreshOnce = true,
|
||||
bool allowRotateOnce = true,
|
||||
bool allowGiveUp = true,
|
||||
string tipText = null)
|
||||
{
|
||||
@@ -44,10 +48,43 @@ namespace GeometryTD.UI
|
||||
|
||||
_displayCount = Mathf.Max(1, displayCount);
|
||||
_refreshCost = Mathf.Max(0, refreshCost);
|
||||
_allowRefreshOnce = allowRefreshOnce;
|
||||
_allowRotateOnce = allowRotateOnce;
|
||||
_allowGiveUp = allowGiveUp;
|
||||
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
|
||||
_hasRefreshed = false;
|
||||
_hasRotated = false;
|
||||
_selectionOffset = 0;
|
||||
_currentModel = null;
|
||||
}
|
||||
|
||||
public void ConfigureRewardCandidates(
|
||||
IReadOnlyList<TowerCompItemData> rewardCandidates,
|
||||
int displayCount = 3,
|
||||
int refreshCost = 0,
|
||||
bool allowRotateOnce = true,
|
||||
bool allowGiveUp = true,
|
||||
string tipText = null)
|
||||
{
|
||||
_rewardPool.Clear();
|
||||
if (rewardCandidates != null)
|
||||
{
|
||||
foreach (var item in rewardCandidates)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_rewardPool.Add(RewardSelectItemRawDataBuilder.Build(item));
|
||||
}
|
||||
}
|
||||
|
||||
_displayCount = Mathf.Max(1, displayCount);
|
||||
_refreshCost = Mathf.Max(0, refreshCost);
|
||||
_allowRotateOnce = allowRotateOnce;
|
||||
_allowGiveUp = allowGiveUp;
|
||||
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
|
||||
_hasRotated = false;
|
||||
_selectionOffset = 0;
|
||||
_currentModel = null;
|
||||
}
|
||||
|
||||
@@ -59,30 +96,35 @@ namespace GeometryTD.UI
|
||||
|
||||
public RewardSelectFormRawData CreateInitialModel()
|
||||
{
|
||||
_hasRefreshed = false;
|
||||
_hasRotated = false;
|
||||
_currentModel = BuildModel();
|
||||
return _currentModel;
|
||||
}
|
||||
|
||||
public RewardSelectFormRawData TryRefresh()
|
||||
public RewardSelectFormRawData TryRotateSelection()
|
||||
{
|
||||
if (_currentModel == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!CanRefreshInternal())
|
||||
if (!CanRotateSelectionInternal())
|
||||
{
|
||||
return _currentModel;
|
||||
}
|
||||
|
||||
if (!TryConsumeRefreshCost())
|
||||
if (!TryConsumeRotateCost())
|
||||
{
|
||||
_currentModel.CanRefresh = false;
|
||||
return _currentModel;
|
||||
}
|
||||
|
||||
_hasRefreshed = true;
|
||||
_hasRotated = true;
|
||||
if (_rewardPool.Count > 0)
|
||||
{
|
||||
_selectionOffset = (_selectionOffset + _displayCount) % _rewardPool.Count;
|
||||
}
|
||||
|
||||
_currentModel = BuildModel();
|
||||
return _currentModel;
|
||||
}
|
||||
@@ -127,7 +169,7 @@ namespace GeometryTD.UI
|
||||
TipText = _tipText,
|
||||
RewardItems = selectedRewards,
|
||||
RefreshCost = _refreshCost,
|
||||
CanRefresh = selectedRewards.Length > 0 && CanRefreshInternal(),
|
||||
CanRefresh = selectedRewards.Length > 0 && CanRotateSelectionInternal(),
|
||||
CanGiveUp = _allowGiveUp
|
||||
};
|
||||
}
|
||||
@@ -140,39 +182,28 @@ namespace GeometryTD.UI
|
||||
}
|
||||
|
||||
int finalCount = Mathf.Clamp(_displayCount, 1, _rewardPool.Count);
|
||||
int[] indexes = new int[_rewardPool.Count];
|
||||
for (int i = 0; i < indexes.Length; i++)
|
||||
{
|
||||
indexes[i] = i;
|
||||
}
|
||||
|
||||
for (int i = 0; i < finalCount; i++)
|
||||
{
|
||||
int randomIndex = Random.Range(i, indexes.Length);
|
||||
(indexes[i], indexes[randomIndex]) = (indexes[randomIndex], indexes[i]);
|
||||
}
|
||||
|
||||
RewardSelectItemRawData[] results = new RewardSelectItemRawData[finalCount];
|
||||
for (int i = 0; i < finalCount; i++)
|
||||
{
|
||||
RewardSelectItemRawData source = _rewardPool[indexes[i]];
|
||||
int sourceIndex = (_selectionOffset + i) % _rewardPool.Count;
|
||||
RewardSelectItemRawData source = _rewardPool[sourceIndex];
|
||||
results[i] = source;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private bool CanRefreshInternal()
|
||||
private bool CanRotateSelectionInternal()
|
||||
{
|
||||
if (!_allowRefreshOnce || _hasRefreshed)
|
||||
if (!_allowRotateOnce || _hasRotated)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return CanPayRefreshCost();
|
||||
return CanPayRotateCost();
|
||||
}
|
||||
|
||||
private bool CanPayRefreshCost()
|
||||
private bool CanPayRotateCost()
|
||||
{
|
||||
if (_refreshCost <= 0)
|
||||
{
|
||||
@@ -187,7 +218,7 @@ namespace GeometryTD.UI
|
||||
return GameEntry.PlayerInventory.Gold >= _refreshCost;
|
||||
}
|
||||
|
||||
private bool TryConsumeRefreshCost()
|
||||
private bool TryConsumeRotateCost()
|
||||
{
|
||||
if (_refreshCost <= 0)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
@@ -360,24 +361,27 @@ namespace GeometryTD.UI
|
||||
|
||||
private static bool TryGetPointerDownScreenPosition(out Vector2 screenPoint)
|
||||
{
|
||||
if (Input.touchCount > 0)
|
||||
Touchscreen touchscreen = Touchscreen.current;
|
||||
if (touchscreen != null)
|
||||
{
|
||||
for (int i = 0; i < Input.touchCount; i++)
|
||||
var touches = touchscreen.touches;
|
||||
for (int i = 0; i < touches.Count; i++)
|
||||
{
|
||||
Touch touch = Input.GetTouch(i);
|
||||
if (touch.phase != TouchPhase.Began)
|
||||
var touch = touches[i];
|
||||
if (!touch.press.wasPressedThisFrame)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
screenPoint = touch.position;
|
||||
screenPoint = touch.position.ReadValue();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
Mouse mouse = Mouse.current;
|
||||
if (mouse != null && mouse.leftButton.wasPressedThisFrame)
|
||||
{
|
||||
screenPoint = Input.mousePosition;
|
||||
screenPoint = mouse.position.ReadValue();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,37 +61,16 @@ namespace GeometryTD.UI
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!GameEntry.PlayerInventory.TryConsumeGold(goodsItem.Price))
|
||||
if (!GameEntry.PlayerInventory.TryPurchaseComponent(goodsItem.SourceItem, goodsItem.Price))
|
||||
{
|
||||
Log.Warning("ShopFormUseCase.TryPurchase() failed. Not enough gold for goods item {0}.", goodsIndex);
|
||||
Log.Warning("ShopFormUseCase.TryPurchase() failed. Purchase command was rejected for goods item {0}.", goodsIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
BackpackInventoryData inventoryDelta = WrapSingleItem(goodsItem.SourceItem);
|
||||
GameEntry.PlayerInventory.MergeInventory(inventoryDelta);
|
||||
goodsItem.IsPurchased = true;
|
||||
updatedRawData = CreateInitialModel();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BackpackInventoryData WrapSingleItem(TowerCompItemData item)
|
||||
{
|
||||
BackpackInventoryData inventory = new BackpackInventoryData();
|
||||
switch (item)
|
||||
{
|
||||
case MuzzleCompItemData muzzleComp:
|
||||
inventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp));
|
||||
break;
|
||||
case BearingCompItemData bearingComp:
|
||||
inventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
|
||||
break;
|
||||
case BaseCompItemData baseComp:
|
||||
inventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
|
||||
break;
|
||||
}
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ namespace GeometryTD.CustomUtility
|
||||
AttackPropertyType.None => "无效",
|
||||
AttackPropertyType.Physics => "<color=#ffff00ff>物理</color>", //yellow
|
||||
AttackPropertyType.Fire => "<color=#ff0000ff>火</color>", //red
|
||||
AttackPropertyType.Water => "<color=#00ffffff>水/color>", //cyan
|
||||
AttackPropertyType.Water => "<color=#00ffffff>水</color>", //cyan
|
||||
AttackPropertyType.Earth => "<color=#00ff00ff>自然</color>", //lime
|
||||
AttackPropertyType.Poison => "<color=#008000ff>毒</color>", //green
|
||||
AttackPropertyType.Ice => "<color=#0000a0ff>冰</color>", //darkblue
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomUtility
|
||||
{
|
||||
public static class ShopPriceRuleService
|
||||
{
|
||||
public static int ResolveRandomBuyPrice(IReadOnlyList<DRShopPrice> shopPriceRows, RarityType rarity, Random random)
|
||||
{
|
||||
if (!TryFindPriceRow(shopPriceRows, rarity, out DRShopPrice row) || row == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int min = Mathf.Max(0, row.MinPrice);
|
||||
int max = Mathf.Max(min, row.MaxPrice);
|
||||
return random != null ? random.Next(min, max + 1) : min;
|
||||
}
|
||||
|
||||
public static int ResolveComponentSalePrice(TowerCompItemData component, IDataTable<DRShopPrice> shopPriceTable = null)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ResolveBasePrice(component.Rarity, shopPriceTable);
|
||||
}
|
||||
|
||||
public static bool TryResolveTowerSalePrice(
|
||||
TowerItemData tower,
|
||||
BackpackInventoryData inventory,
|
||||
out int price,
|
||||
IDataTable<DRShopPrice> shopPriceTable = null)
|
||||
{
|
||||
price = 0;
|
||||
if (tower == null || inventory == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetComponentById(inventory.MuzzleComponents, tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComp) ||
|
||||
!TryGetComponentById(inventory.BearingComponents, tower.BearingComponentInstanceId, out BearingCompItemData bearingComp) ||
|
||||
!TryGetComponentById(inventory.BaseComponents, tower.BaseComponentInstanceId, out BaseCompItemData baseComp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
price = ResolveComponentSalePrice(muzzleComp, shopPriceTable) +
|
||||
ResolveComponentSalePrice(bearingComp, shopPriceTable) +
|
||||
ResolveComponentSalePrice(baseComp, shopPriceTable);
|
||||
return price > 0;
|
||||
}
|
||||
|
||||
public static int ResolveBasePrice(RarityType rarity, IDataTable<DRShopPrice> shopPriceTable = null)
|
||||
{
|
||||
IDataTable<DRShopPrice> resolvedTable = shopPriceTable ?? GameEntry.DataTable.GetDataTable<DRShopPrice>();
|
||||
if (resolvedTable == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
DRShopPrice[] rows = resolvedTable.GetAllDataRows();
|
||||
if (!TryFindPriceRow(rows, rarity, out DRShopPrice row) || row == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int min = Mathf.Max(0, row.MinPrice);
|
||||
int max = Mathf.Max(min, row.MaxPrice);
|
||||
return Mathf.RoundToInt((min + max) * 0.5f);
|
||||
}
|
||||
|
||||
private static bool TryFindPriceRow(IReadOnlyList<DRShopPrice> rows, RarityType rarity, out DRShopPrice result)
|
||||
{
|
||||
result = null;
|
||||
if (rows == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
{
|
||||
DRShopPrice row = rows[i];
|
||||
if (row != null && row.Rarity == rarity)
|
||||
{
|
||||
result = row;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetComponentById<TComp>(IReadOnlyList<TComp> components, long instanceId, out TComp result)
|
||||
where TComp : TowerCompItemData
|
||||
{
|
||||
result = null;
|
||||
if (components == null || instanceId <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < components.Count; i++)
|
||||
{
|
||||
TComp component = components[i];
|
||||
if (component != null && component.InstanceId == instanceId)
|
||||
{
|
||||
result = component;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1bcf5c75bea4faaacdb4ffc5adfaf36
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -158,6 +158,7 @@ Transform:
|
||||
- {fileID: 1322505022}
|
||||
- {fileID: 1062564689}
|
||||
- {fileID: 1554147129}
|
||||
- {fileID: 336799288}
|
||||
m_Father: {fileID: 1852670053}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &120093239
|
||||
@@ -294,6 +295,50 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0a0c1c547ca24c95819f5f62f0bd3ea3, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
--- !u!1 &336799287
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 336799288}
|
||||
- component: {fileID: 336799289}
|
||||
m_Layer: 0
|
||||
m_Name: TagRegistery
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &336799288
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 336799287}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 119167776}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &336799289
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 336799287}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: a268c57d4373494ab9b4d7167099f170, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
--- !u!1001 &343730742
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -684,6 +729,10 @@ PrefabInstance:
|
||||
propertyPath: m_FrameRate
|
||||
value: 60
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 11499388, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||
propertyPath: m_GameSpeed
|
||||
value: 2
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 11499388, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||
propertyPath: m_EditorLanguage
|
||||
value: 8
|
||||
|
||||
@@ -183,11 +183,15 @@ namespace GeometryTD.Tests.EditMode
|
||||
CreateBoundPlayerInventory(CreateInventory(100f, 100f, 100f));
|
||||
CombatSettlementContext settlementContext = new CombatSettlementContext();
|
||||
settlementContext.Flags.ShouldOpenRewardSelection = true;
|
||||
int nextRewardOrdinal = 0;
|
||||
|
||||
bool prepared = new CombatSettlementService().TryPrepareRewardSelection(
|
||||
settlementContext,
|
||||
displayPhaseIndex: 1,
|
||||
themeType: LevelThemeType.Plain,
|
||||
runSeed: 1001,
|
||||
sequenceIndex: 3,
|
||||
ref nextRewardOrdinal,
|
||||
rewardSelectFormUseCase: null,
|
||||
onRewardSelected: _ => { },
|
||||
onGiveUp: null);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.UI;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GeometryTD.Tests.EditMode
|
||||
{
|
||||
public sealed class ContinueChallengeMultiplierTests
|
||||
{
|
||||
[Test]
|
||||
public void BuildRawData_Uses_OneX_Multiplier_For_First_Loop_And_Invalid_Input()
|
||||
{
|
||||
CombatInfoFormRawData firstLoop = CombatInfoFormUseCase.BuildRawData(
|
||||
LevelThemeType.Plain,
|
||||
levelId: 1,
|
||||
currentPhaseIndex: 1,
|
||||
totalPhaseCount: 4,
|
||||
coin: 10,
|
||||
baseHp: 20,
|
||||
baseHpMax: 20,
|
||||
canEnd: true);
|
||||
CombatInfoFormRawData invalidInput = CombatInfoFormUseCase.BuildRawData(
|
||||
LevelThemeType.Plain,
|
||||
levelId: 1,
|
||||
currentPhaseIndex: 0,
|
||||
totalPhaseCount: 0,
|
||||
coin: 10,
|
||||
baseHp: 20,
|
||||
baseHpMax: 20,
|
||||
canEnd: false);
|
||||
|
||||
Assert.That(firstLoop.EnemyHpRateMultiplier, Is.EqualTo(1));
|
||||
Assert.That(invalidInput.EnemyHpRateMultiplier, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildRawData_Doubles_EnemyHp_After_Each_Full_Loop()
|
||||
{
|
||||
CombatInfoFormRawData secondLoop = CombatInfoFormUseCase.BuildRawData(
|
||||
LevelThemeType.Volcano,
|
||||
levelId: 2,
|
||||
currentPhaseIndex: 5,
|
||||
totalPhaseCount: 4,
|
||||
coin: 15,
|
||||
baseHp: 18,
|
||||
baseHpMax: 20,
|
||||
canEnd: true);
|
||||
CombatInfoFormRawData thirdLoop = CombatInfoFormUseCase.BuildRawData(
|
||||
LevelThemeType.Mountain,
|
||||
levelId: 3,
|
||||
currentPhaseIndex: 9,
|
||||
totalPhaseCount: 4,
|
||||
coin: 20,
|
||||
baseHp: 16,
|
||||
baseHpMax: 20,
|
||||
canEnd: true);
|
||||
|
||||
Assert.That(secondLoop.EnemyHpRateMultiplier, Is.EqualTo(2));
|
||||
Assert.That(thirdLoop.EnemyHpRateMultiplier, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildRawData_Clamps_EnemyHp_Multiplier_At_IntMaxValue()
|
||||
{
|
||||
CombatInfoFormRawData rawData = CombatInfoFormUseCase.BuildRawData(
|
||||
LevelThemeType.Plain,
|
||||
levelId: 1,
|
||||
currentPhaseIndex: 121,
|
||||
totalPhaseCount: 4,
|
||||
coin: 30,
|
||||
baseHp: 10,
|
||||
baseHpMax: 20,
|
||||
canEnd: false);
|
||||
|
||||
Assert.That(rawData.EnemyHpRateMultiplier, Is.EqualTo(int.MaxValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user