Compare commits

...
14 Commits
Author SHA1 Message Date
basil 7106e0adbe feat: Introduce new services and refactor existing code for better structure
- Added ISpriteCacheService, IStaticDataService, and ITextService interfaces for improved service management.
- Implemented RunContracts for managing run states and node execution contexts.
- Created EnemyTagStatusRuntime to handle enemy status effects and their application.
- Refactored ShopPriceRuleService to utilize static data service for shop prices.
- Enhanced InventoryTowerEnduranceUtility to manage tower endurance reduction.
- Updated EventFormUseCase to work with new EventOptionExecutionContext.
- Removed unused dictionary loading code from ProcedurePreload.
- Cleaned up various utility classes and ensured consistent naming conventions.
- Updated tests to reflect changes in context and service usage.
2026-05-13 22:18:47 +08:00
basil 9fc0ef2216 拆分程序集,统一修改 Core 区域类的命名空间,暂不修复编译问题 2026-05-13 16:48:09 +08:00
basil 3d785d5139 添加设置文件,更新项目名称和API兼容性级别 2026-05-13 15:18:02 +08:00
SepComet eec09a0bb4 remove 2026-04-30 15:35:50 +08:00
SepComet 1422db3b19 use new input system 2026-04-30 14:52:19 +08:00
basil a38a8d4b39 update design 2026-04-30 10:36:25 +08:00
SepComet 55506eff9a introduce ccgs 2026-04-30 00:09:30 +08:00
basil 2e54acbc85 为 RepoForm 添加出售功能,ShopNode 只承载玩家购买组件的逻辑 2026-03-16 17:52:23 +08:00
basil dc2aa59d58 补全部分 EventComponent 设计 2026-03-16 11:53:24 +08:00
basil c76fd85a2c A1 + A2 2026-03-13 17:58:09 +08:00
basil 777c58b812 修复已知问题 2026-03-13 14:55:17 +08:00
SepComet aa44170d56 InventoryGenerationComponent + TagRegistryComponent 收口 2026-03-13 11:06:14 +08:00
SepComet 0c92fd9886 调整 OutGameDropPool 表定义 + 添加部分组件定义 2026-03-13 10:08:12 +08:00
SepComet 26aaa945a9 G3 finish 2026-03-13 09:46:59 +08:00
342 changed files with 9094 additions and 2219 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(unzip -l \"Assets/GameFramework/Libraries/GameFramework.xml\")"
]
}
}
-39
View File
@@ -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
+6
View File
@@ -98,10 +98,14 @@ InitTestScene*.unity*
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity* /[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
/[Aa]ssets/StreamingAssets/ /[Aa]ssets/StreamingAssets/
!/[Aa]ssets/StreamingAssets/.gitkeep
/.vscode /.vscode
/.dotnet-home /.dotnet-home
/[Aa]ssets/RawResources /[Aa]ssets/RawResources
/[Aa]ssets/RawResources.meta /[Aa]ssets/RawResources.meta
/.dotnet /.dotnet
/.idea /.idea
~$*.xlsx ~$*.xlsx
@@ -109,3 +113,5 @@ InitTestScene*.unity*
/node_modules /node_modules
package.json package.json
package-lock.json package-lock.json
/.omc
/.omx
-15
View File
@@ -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
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>
-6
View File
@@ -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>
-355
View File
@@ -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
View File
@@ -4,3 +4,7 @@
1 元素底座 [2,2.1,2.2,2.3,2.5] 0.2 Fire [Fire,BurnSpread,IgniteBurst,Inferno] 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] 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] 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] 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] 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] 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]
-3
View File
@@ -1,9 +1,6 @@
# Id 策划备注 AssetName # Id 策划备注 AssetName
# int string # int string
# 实体编号 资源名称 # 实体编号 资源名称
101 测试枪口 TestMuzzle
201 测试轴承 TestBearing
301 测试底座 TestBase
401 防御塔实体 TowerEntity 401 防御塔实体 TowerEntity
501 普通子弹 NormalBullet 501 普通子弹 NormalBullet
1001 测试普通敌人 TestEnemy 1001 测试普通敌人 TestEnemy
+6 -6
View File
@@ -1,6 +1,6 @@
# Id 列1 Title Description Options # Id 列1 Title Description Option1 Option2 Option3 Option4
# int string string string # 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}] 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":[]}] 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":"展示你的防御塔","requirements":[{"type":"TowerCountAtLeast","param":{"Count":2}}],"rewardEffects":[{"type":"AddGold","param":{"Count":50}}]},{"optionText":"离开","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] 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] 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] 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]
+24 -48
View File
@@ -1,48 +1,24 @@
# Id 列1 LevelThemeType Rarity ItemType ItemId Weight MinPhase MaxPhase # Id 列1 LevelThemeType ItemType ItemId Weights MinPhase MaxPhase
# int LevelThemeType RarityType ItemType int int int int # int LevelThemeType ItemType int int[] int[] int[]
# 道具池号 策划备注 关卡所属主题类型 道具稀有度 道具类型 道具Id 权重 出现的最低波次 出现的最高波次 # 道具池号 策划备注 关卡所属主题类型 道具类型 道具Id 权重 出现的最低波次 出现的最高波次
1 平原战斗池 Plain White MuzzleComp 1 10 1 10 1 平原战斗池 Plain MuzzleComp 1 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
2 Plain White MuzzleComp 2 10 1 10 2 Plain MuzzleComp 2 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
3 Plain White MuzzleComp 3 10 1 10 3 Plain MuzzleComp 3 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
4 Plain White BearingComp 1 10 1 10 4 Plain MuzzleComp 4 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
5 Plain White BearingComp 2 10 1 10 5 Plain MuzzleComp 5 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
6 Plain White BearingComp 3 10 1 10 6 Plain MuzzleComp 6 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
7 Plain White BaseComp 1 10 1 10 7 Plain MuzzleComp 7 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
8 Plain White BaseComp 2 10 1 10 8 Plain BearingComp 1 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
9 Plain White BaseComp 3 10 1 10 9 Plain BearingComp 2 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
10 Plain Green MuzzleComp 1 5 2 10 10 Plain BearingComp 3 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
11 Plain Green MuzzleComp 2 5 2 10 11 Plain BearingComp 4 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
12 Plain Green MuzzleComp 3 5 2 10 12 Plain BearingComp 5 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
13 Plain Green BearingComp 1 5 2 10 13 Plain BearingComp 6 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
14 Plain Green BearingComp 2 5 2 10 14 Plain BearingComp 7 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
15 Plain Green BearingComp 3 5 2 10 15 Plain BaseComp 1 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
16 Plain Green BaseComp 1 5 2 10 16 Plain BaseComp 2 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
17 Plain Green BaseComp 2 5 2 10 17 Plain BaseComp 3 [20,10,6,4,2] [1,2,3,4,5] [10,10,15,20,50]
18 Plain Green BaseComp 3 5 2 10 18 Plain BaseComp 4 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
19 Plain Blue MuzzleComp 1 3 3 15 19 Plain BaseComp 5 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
20 Plain Blue MuzzleComp 2 3 3 15 20 Plain BaseComp 6 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
21 Plain Blue MuzzleComp 3 3 3 15 21 Plain BaseComp 7 [15,8,5,3,1] [1,2,3,4,5] [10,10,15,20,50]
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
@@ -26,6 +26,8 @@ public partial class GameEntry
public static InventoryGenerationComponent InventoryGeneration { get; private set; } public static InventoryGenerationComponent InventoryGeneration { get; private set; }
public static TagRegistryComponent TagRegistry { get; private set; }
private static void InitCustomComponents() private static void InitCustomComponents()
{ {
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>(); BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
@@ -38,5 +40,6 @@ public partial class GameEntry
ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent<ResolutionAdapterComponent>(); ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent<ResolutionAdapterComponent>();
SpriteCache = UnityGameFramework.Runtime.GameEntry.GetComponent<SpriteCacheComponent>(); SpriteCache = UnityGameFramework.Runtime.GameEntry.GetComponent<SpriteCacheComponent>();
InventoryGeneration = UnityGameFramework.Runtime.GameEntry.GetComponent<InventoryGenerationComponent>(); InventoryGeneration = UnityGameFramework.Runtime.GameEntry.GetComponent<InventoryGenerationComponent>();
TagRegistry = UnityGameFramework.Runtime.GameEntry.GetComponent<TagRegistryComponent>();
} }
} }
@@ -0,0 +1,23 @@
{
"name": "GeometryTD.Runtime",
"rootNamespace": "GeometryTD.Runtime",
"references": [
"GUID:363c5eb08ff8e6a439b85e37b8c20d96",
"GUID:75469ad4d38634e559750d17036d5f7c",
"GUID:6055be8ebefd69e48b49212b09b47b2f",
"GUID:771b79705393da244ae720aa28ae8a69",
"GUID:c53de6f5028489b4689382a86bfbd807",
"GUID:0f19320f1ba33834b896afc5a5aaae67"
],
"includePlatforms": [
"Android"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,18 @@
{
"name": "GeometryTD.Gameplay",
"rootNamespace": "GeometryTD.Gameplay",
"references": [
"GUID:771b79705393da244ae720aa28ae8a69"
],
"includePlatforms": [
"Android"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c53de6f5028489b4689382a86bfbd807
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -177,11 +177,7 @@ namespace GeometryTD.CustomComponent
public void StartCombat( public void StartCombat(
int levelId = 0, int levelId = 0,
string runId = null, RunNodeExecutionContext context = null)
int runSeed = 0,
int nodeId = 0,
RunNodeType nodeType = RunNodeType.None,
int sequenceIndex = -1)
{ {
if (!EnsureCombatRuntimeInitialized()) if (!EnsureCombatRuntimeInitialized())
{ {
@@ -232,11 +228,7 @@ namespace GeometryTD.CustomComponent
selectedLevel, selectedLevel,
phaseList, phaseList,
_selectedSpawnEntriesByPhaseId, _selectedSpawnEntriesByPhaseId,
runId, context))
runSeed,
nodeId,
nodeType,
sequenceIndex))
{ {
CurrentLevel = null; CurrentLevel = null;
return; return;
@@ -1,11 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using GeometryTD.CustomEvent;
using GeometryTD.DataTable; using GeometryTD.DataTable;
using GeometryTD.Definition; using GeometryTD.Definition;
using GeometryTD.Entity; using GeometryTD.Entity;
using GeometryTD.Procedure; using GeometryTD.Procedure;
using GeometryTD.UI;
using UnityEngine; using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
@@ -69,11 +67,7 @@ namespace GeometryTD.CustomComponent
DRLevel level, DRLevel level,
IReadOnlyList<DRLevelPhase> phases, IReadOnlyList<DRLevelPhase> phases,
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId, IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId,
string runId = null, RunNodeExecutionContext context = null)
int runSeed = 0,
int nodeId = 0,
RunNodeType nodeType = RunNodeType.None,
int sequenceIndex = -1)
{ {
if (!_initialized || _runtime.Entity == null) if (!_initialized || _runtime.Entity == null)
{ {
@@ -95,16 +89,12 @@ namespace GeometryTD.CustomComponent
_runtime.DidCombatWin = true; _runtime.DidCombatWin = true;
_runtime.CurrentLevel = level; _runtime.CurrentLevel = level;
_runtime.RunId = runId; _runtime.NodeContext = context != null ? context.Clone() : null;
_runtime.RunSeed = runSeed; _runtime.NextDropOrdinal = 0;
_runtime.NodeId = nodeId; _runtime.NextRewardOrdinal = 0;
_runtime.NodeType = nodeType;
_runtime.SequenceIndex = sequenceIndex;
GameEntry.InventoryGeneration.ConfigureRunContext(runSeed, sequenceIndex);
_runtime.CombatRunResourceStore.InitializeForCombat(level); _runtime.CombatRunResourceStore.InitializeForCombat(level);
for (int i = 0; i < phases.Count; i++) foreach (var phase in phases)
{ {
DRLevelPhase phase = phases[i];
if (phase != null) if (phase != null)
{ {
_runtime.PhaseBuffer.Add(phase); _runtime.PhaseBuffer.Add(phase);
@@ -122,7 +112,8 @@ namespace GeometryTD.CustomComponent
_runtime.PhaseLoopRuntime.SetPhases(_runtime.PhaseBuffer); _runtime.PhaseLoopRuntime.SetPhases(_runtime.PhaseBuffer);
if (_runtime.PhaseLoopRuntime.PhaseCount <= 0) 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)); ChangeState(new CombatLoadingState(_runtime, _coordinator));
@@ -201,7 +192,13 @@ namespace GeometryTD.CustomComponent
enemy, enemy,
_runtime.PhaseLoopRuntime.DisplayPhaseIndex, _runtime.PhaseLoopRuntime.DisplayPhaseIndex,
_coordinator.ResolveCurrentThemeType()); _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.AddEnemyDefeatedReward(result.Coin, result.Gold);
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem); _runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
} }
@@ -326,6 +323,5 @@ namespace GeometryTD.CustomComponent
} }
#endregion #endregion
} }
} }
@@ -55,11 +55,9 @@ namespace GeometryTD.CustomComponent
_runtime.DidCombatWin = true; _runtime.DidCombatWin = true;
_runtime.IsCompleted = false; _runtime.IsCompleted = false;
_runtime.NodeEnterFired = false; _runtime.NodeEnterFired = false;
_runtime.RunId = null; _runtime.NodeContext = null;
_runtime.RunSeed = 0; _runtime.NextDropOrdinal = 0;
_runtime.NodeId = 0; _runtime.NextRewardOrdinal = 0;
_runtime.NodeType = RunNodeType.None;
_runtime.SequenceIndex = -1;
} }
public void CleanupAllCombatEntities() public void CleanupAllCombatEntities()
@@ -201,16 +199,20 @@ namespace GeometryTD.CustomComponent
public void CompleteNormalCombatAndNotify(bool didCombatWin) public void CompleteNormalCombatAndNotify(bool didCombatWin)
{ {
CompleteCombat(didCombatWin); CompleteCombat(didCombatWin);
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
GameEntry.Event.Fire( GameEntry.Event.Fire(
this, this,
NodeCompleteEventArgs.Create( NodeCompleteEventArgs.Create(
_runtime.RunId, nodeContext?.RunId,
_runtime.NodeId, nodeContext?.NodeId ?? 0,
_runtime.NodeType, nodeContext?.NodeType ?? RunNodeType.None,
_runtime.SequenceIndex, nodeContext?.SequenceIndex ?? -1,
RunNodeCompletionStatus.Completed, RunNodeCompletionStatus.Completed,
didCombatWin, didCombatWin,
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)); nodeContext != null
? nodeContext.CreateCompletionSnapshot(
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
: null));
} }
public void CompleteFailureCombatAndNotify() public void CompleteFailureCombatAndNotify()
@@ -220,16 +222,20 @@ namespace GeometryTD.CustomComponent
CloseRewardSelectForm(); CloseRewardSelectForm();
CloseDialogForm(); CloseDialogForm();
CompleteCombat(false); CompleteCombat(false);
RunNodeExecutionContext nodeContext = _runtime.NodeContext;
GameEntry.Event.Fire( GameEntry.Event.Fire(
this, this,
NodeCompleteEventArgs.Create( NodeCompleteEventArgs.Create(
_runtime.RunId, nodeContext?.RunId,
_runtime.NodeId, nodeContext?.NodeId ?? 0,
_runtime.NodeType, nodeContext?.NodeType ?? RunNodeType.None,
_runtime.SequenceIndex, nodeContext?.SequenceIndex ?? -1,
RunNodeCompletionStatus.Exception, RunNodeCompletionStatus.Exception,
false, 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) public bool HandleStartFailure(string errorMessage)
@@ -29,10 +29,8 @@ namespace GeometryTD.CustomComponent
public bool IsCompleted { get; set; } public bool IsCompleted { get; set; }
public bool NodeEnterFired { get; set; } public bool NodeEnterFired { get; set; }
public CombatSettlementContext SettlementContext { get; set; } public CombatSettlementContext SettlementContext { get; set; }
public string RunId { get; set; } public RunNodeExecutionContext NodeContext { get; set; }
public int RunSeed { get; set; } public int NextDropOrdinal { get; set; }
public int NodeId { get; set; } public int NextRewardOrdinal { get; set; }
public RunNodeType NodeType { get; set; }
public int SequenceIndex { get; set; }
} }
} }
@@ -11,10 +11,8 @@ namespace GeometryTD.CustomComponent
public sealed class CombatSettlementService public sealed class CombatSettlementService
{ {
private const int RewardSelectDisplayCount = 3; private const int RewardSelectDisplayCount = 3;
private const float FullBaseHpGoldBonusRate = 0.3f; private readonly CombatSettlementCalculator _calculator = new();
private const float HighBaseHpGoldBonusRate = 0.1f; private readonly CombatSettlementCommitter _committer = new();
private const float HighBaseHpThreshold = 0.8f;
private const float SettlementTowerEnduranceLoss = 1f;
public CombatSettlementContext BuildSettlementContext( public CombatSettlementContext BuildSettlementContext(
bool didCombatWin, bool didCombatWin,
@@ -22,69 +20,25 @@ namespace GeometryTD.CustomComponent
int defeatedEnemyCount, int defeatedEnemyCount,
CombatRunResourceStore resourceStore) CombatRunResourceStore resourceStore)
{ {
bool shouldOpenFullBaseHpRewardSelect = false; return _calculator.BuildSettlementContext(
ResolveSettlementByBaseHp(
didCombatWin, didCombatWin,
currentLevel, currentLevel,
resourceStore, defeatedEnemyCount,
out int currentBaseHp, resourceStore);
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;
} }
public void CommitSettlementInventory(CombatSettlementContext settlementContext) public void CommitSettlementInventory(CombatSettlementContext settlementContext)
{ {
if (settlementContext == null || settlementContext.Flags.IsCommitted) _committer.CommitSettlementInventory(settlementContext);
{
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 bool TryPrepareRewardSelection( public bool TryPrepareRewardSelection(
CombatSettlementContext settlementContext, CombatSettlementContext settlementContext,
int displayPhaseIndex, int displayPhaseIndex,
LevelThemeType themeType, LevelThemeType themeType,
int runSeed,
int sequenceIndex,
ref int nextRewardOrdinal,
RewardSelectFormUseCase rewardSelectFormUseCase, RewardSelectFormUseCase rewardSelectFormUseCase,
Action<RewardSelectItemRawData> onRewardSelected, Action<RewardSelectItemRawData> onRewardSelected,
Action onGiveUp) Action onGiveUp)
@@ -97,36 +51,22 @@ namespace GeometryTD.CustomComponent
IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates( IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates(
displayPhaseIndex, displayPhaseIndex,
themeType, themeType,
RewardSelectDisplayCount); RewardSelectDisplayCount,
runSeed,
sequenceIndex,
ref nextRewardOrdinal);
if (candidateItems == null || candidateItems.Count <= 0) if (candidateItems == null || candidateItems.Count <= 0)
{ {
settlementContext.Flags.ShouldOpenRewardSelection = false; settlementContext.Flags.ShouldOpenRewardSelection = false;
return 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.SetCallbacks(onRewardSelected, onGiveUp);
rewardSelectFormUseCase.ConfigureRewardPool( rewardSelectFormUseCase.ConfigureRewardCandidates(
rewardPool, candidateItems,
displayCount: RewardSelectDisplayCount, displayCount: RewardSelectDisplayCount,
refreshCost: 0, refreshCost: 0,
allowRefreshOnce: false, allowRotateOnce: false,
allowGiveUp: false, allowGiveUp: false,
tipText: "基地满血奖励:请选择 1 个组件"); tipText: "基地满血奖励:请选择 1 个组件");
@@ -144,13 +84,7 @@ namespace GeometryTD.CustomComponent
public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward) public void ApplySelectedReward(CombatSettlementContext settlementContext, RewardSelectItemRawData selectedReward)
{ {
if (settlementContext?.Result.RewardInventory == null || selectedReward?.SourceItem == null) _committer.ApplySelectedReward(settlementContext, selectedReward);
{
return;
}
TryAppendRewardComponent(settlementContext.Result.RewardInventory, selectedReward.SourceItem);
settlementContext.Summary.RewardInventory = settlementContext.Result.RewardInventory;
} }
public void OpenCombatFinishForm( public void OpenCombatFinishForm(
@@ -166,134 +100,5 @@ namespace GeometryTD.CustomComponent
GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm); 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);
}
} }
} }
@@ -16,16 +16,23 @@ namespace GeometryTD.CustomComponent
} }
Coordinator.EnsureRewardSelectFormUseCaseBound(); Coordinator.EnsureRewardSelectFormUseCaseBound();
int nextRewardOrdinal = Runtime.NextRewardOrdinal;
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection( if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
Runtime.SettlementContext, Runtime.SettlementContext,
Runtime.PhaseLoopRuntime.DisplayPhaseIndex, Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
Coordinator.ResolveCurrentThemeType(), Coordinator.ResolveCurrentThemeType(),
Runtime.NodeContext != null ? Runtime.NodeContext.RunSeed : 0,
Runtime.NodeContext != null ? Runtime.NodeContext.SequenceIndex : -1,
ref nextRewardOrdinal,
Runtime.RewardSelectFormUseCase, Runtime.RewardSelectFormUseCase,
Coordinator.OnFullBaseHpRewardSelected, Coordinator.OnFullBaseHpRewardSelected,
Coordinator.OnFullBaseHpRewardGiveUp)) Coordinator.OnFullBaseHpRewardGiveUp))
{ {
Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator)); Coordinator.ChangeState(new CombatFinishFormState(Runtime, Coordinator));
return;
} }
Runtime.NextRewardOrdinal = nextRewardOrdinal;
} }
public override void OnExit() public override void OnExit()
@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using GeometryTD.CustomEvent; using GeometryTD.CustomEvent;
using GeometryTD.DataTable; using GeometryTD.DataTable;
using GeometryTD.Procedure;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
@@ -41,10 +42,10 @@ namespace GeometryTD.CustomComponent
GameEntry.Event.Fire( GameEntry.Event.Fire(
Coordinator, Coordinator,
NodeEnterEventArgs.Create( NodeEnterEventArgs.Create(
Runtime.RunId, Runtime.NodeContext?.RunId,
Runtime.NodeId, Runtime.NodeContext?.NodeId ?? 0,
Runtime.NodeType, Runtime.NodeContext?.NodeType ?? RunNodeType.None,
Runtime.SequenceIndex)); Runtime.NodeContext?.SequenceIndex ?? -1));
} }
Log.Info( 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:
@@ -0,0 +1,3 @@
{
"reference": "GUID:c53de6f5028489b4689382a86bfbd807"
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5304296ea426f4d4b8067b7b64a3ad48
AssemblyDefinitionReferenceImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using GameFramework.DataTable; using GameFramework.DataTable;
using GeometryTD.Core;
using GeometryTD.CustomEvent; using GeometryTD.CustomEvent;
using GeometryTD.DataTable; using GeometryTD.DataTable;
using GeometryTD.Definition; using GeometryTD.Definition;
@@ -14,10 +15,8 @@ namespace GeometryTD.CustomComponent
{ {
public class EventNodeComponent : GameFrameworkComponent public class EventNodeComponent : GameFrameworkComponent
{ {
private string _activeRunId; private RunNodeExecutionContext _activeContext;
private int _activeNodeId; private EventItem _activeEvent;
private RunNodeType _activeNodeType = RunNodeType.None;
private int _activeSequenceIndex = -1;
private readonly List<EventItem> _eventItems = new List<EventItem>(); private readonly List<EventItem> _eventItems = new List<EventItem>();
@@ -54,7 +53,7 @@ namespace GeometryTD.CustomComponent
Log.Info("EventNodeComponent initialized with {0} events.", _eventItems.Count); 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) if (!_initialized)
{ {
@@ -67,22 +66,29 @@ namespace GeometryTD.CustomComponent
return; return;
} }
int index = Random.Range(0, _eventItems.Count);
EventItem randomEvent = _eventItems[index];
if (_eventFormUseCase == null) if (_eventFormUseCase == null)
{ {
Log.Warning("EventNodeComponent StartEvent failed. Event form is not initialized."); Log.Warning("EventNodeComponent StartEvent failed. Event form is not initialized.");
return; return;
} }
_activeRunId = runId; _activeContext = context != null ? context.Clone() : null;
_activeNodeId = nodeId; _activeEvent = SelectActiveEvent(_activeContext);
_activeNodeType = nodeType; if (_activeEvent == null)
_activeSequenceIndex = sequenceIndex; {
_eventFormUseCase.SetCurrentEvent(randomEvent); Log.Warning("EventNodeComponent StartEvent failed. No active event could be resolved.");
return;
}
_eventFormUseCase.BindEvent(_activeEvent, CreateEventOptionExecutionContext(_activeContext));
GameEntry.UIRouter.OpenUI(UIFormType.EventForm); 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() public void EndEvent()
@@ -91,22 +97,24 @@ namespace GeometryTD.CustomComponent
GameEntry.Event.Fire( GameEntry.Event.Fire(
this, this,
NodeCompleteEventArgs.Create( NodeCompleteEventArgs.Create(
_activeRunId, _activeContext?.RunId,
_activeNodeId, _activeContext?.NodeId ?? 0,
_activeNodeType, _activeContext?.NodeType ?? RunNodeType.None,
_activeSequenceIndex, _activeContext?.SequenceIndex ?? -1,
RunNodeCompletionStatus.Completed, RunNodeCompletionStatus.Completed,
true, true,
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)); _activeContext != null
? _activeContext.CreateCompletionSnapshot(
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
: null));
ClearActiveNodeContext(); ClearActiveNodeContext();
} }
private void ClearActiveNodeContext() private void ClearActiveNodeContext()
{ {
_activeRunId = null; _activeContext = null;
_activeNodeId = 0; _activeEvent = null;
_activeNodeType = RunNodeType.None; _eventFormUseCase?.Clear();
_activeSequenceIndex = -1;
} }
private static EventOption[] ParseOptions(string optionsRaw) private static EventOption[] ParseOptions(string optionsRaw)
@@ -200,5 +208,41 @@ namespace GeometryTD.CustomComponent
return effects.ToArray(); 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;
}
}
private static EventOptionExecutionContext CreateEventOptionExecutionContext(RunNodeExecutionContext context)
{
return new EventOptionExecutionContext(
context?.RunSeed ?? 0,
context?.SequenceIndex ?? -1);
}
} }
} }
@@ -0,0 +1,3 @@
{
"reference": "GUID:e67bd2d1b65d48ce8120141996e55b2b"
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: bd7362132664f0c499e39663c24ade2d
AssemblyDefinitionReferenceImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
{
"reference": "GUID:0f19320f1ba33834b896afc5a5aaae67"
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4c1cfd0d064e8b3418adeca47c20ae1d
AssemblyDefinitionReferenceImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -3,15 +3,24 @@ using GameFramework.DataTable;
using GeometryTD.DataTable; using GeometryTD.DataTable;
using GeometryTD.Definition; using GeometryTD.Definition;
using UnityEngine; using UnityEngine;
using Random = System.Random;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
public sealed class DropPoolRoller public sealed class DropPoolRoller
{ {
private const float RarityCurveScalePhase = 30f; 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 List<DROutGameDropPool> _eligibleRowBuffer = new();
private readonly Dictionary<RarityType, float> _rarityWeightBuffer = new(); private readonly float[] _rarityWeightBuffer = new float[OrderedRarities.Length];
private readonly IDataTable<DROutGameDropPool> _dropPoolTable; private readonly IDataTable<DROutGameDropPool> _dropPoolTable;
public DropPoolRoller(IDataTable<DROutGameDropPool> dropPoolTable) public DropPoolRoller(IDataTable<DROutGameDropPool> dropPoolTable)
@@ -19,9 +28,15 @@ namespace GeometryTD.CustomComponent
_dropPoolTable = dropPoolTable; _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; selectedRow = null;
selectedRarity = RarityType.None;
DROutGameDropPool[] allRows = _dropPoolTable.GetAllDataRows(); DROutGameDropPool[] allRows = _dropPoolTable.GetAllDataRows();
if (allRows == null || allRows.Length <= 0) if (allRows == null || allRows.Length <= 0)
@@ -35,22 +50,29 @@ namespace GeometryTD.CustomComponent
return false; return false;
} }
RarityType selectedRarity = RollRarity(displayPhaseIndex); selectedRarity = RollRarity(displayPhaseIndex, random);
if (selectedRarity == RarityType.None) if (selectedRarity == RarityType.None)
{ {
return false; return false;
} }
int totalWeight = 0; int totalWeight = 0;
for (int i = 0; i < _eligibleRowBuffer.Count; i++) DROutGameDropPool fallbackRow = null;
foreach (var row in _eligibleRowBuffer)
{ {
DROutGameDropPool row = _eligibleRowBuffer[i]; if (!IsEligibleAtPhase(row, selectedRarity, displayPhaseIndex))
if (row.Rarity != selectedRarity)
{ {
continue; continue;
} }
totalWeight += Mathf.Max(1, row.Weight); int rowWeight = row.GetWeight(selectedRarity);
if (rowWeight <= 0)
{
continue;
}
fallbackRow = row;
totalWeight += rowWeight;
} }
if (totalWeight <= 0) if (totalWeight <= 0)
@@ -58,16 +80,22 @@ namespace GeometryTD.CustomComponent
return false; return false;
} }
int randomWeight = Random.Range(1, totalWeight + 1); int randomWeight = random.Next(1, totalWeight + 1);
int cumulativeWeight = 0; int cumulativeWeight = 0;
foreach (var row in _eligibleRowBuffer) foreach (var row in _eligibleRowBuffer)
{ {
if (row.Rarity != selectedRarity) if (!IsEligibleAtPhase(row, selectedRarity, displayPhaseIndex))
{ {
continue; continue;
} }
cumulativeWeight += Mathf.Max(1, row.Weight); int rowWeight = row.GetWeight(selectedRarity);
if (rowWeight <= 0)
{
continue;
}
cumulativeWeight += rowWeight;
if (randomWeight <= cumulativeWeight) if (randomWeight <= cumulativeWeight)
{ {
selectedRow = row; selectedRow = row;
@@ -75,7 +103,7 @@ namespace GeometryTD.CustomComponent
} }
} }
selectedRow = _eligibleRowBuffer[_eligibleRowBuffer.Count - 1]; selectedRow = fallbackRow;
return selectedRow != null; return selectedRow != null;
} }
@@ -86,9 +114,8 @@ namespace GeometryTD.CustomComponent
{ {
_eligibleRowBuffer.Clear(); _eligibleRowBuffer.Clear();
for (int i = 0; i < allRows.Length; i++) foreach (var row in allRows)
{ {
DROutGameDropPool row = allRows[i];
if (row == null) if (row == null)
{ {
continue; continue;
@@ -99,7 +126,7 @@ namespace GeometryTD.CustomComponent
continue; continue;
} }
if (displayPhaseIndex < row.MinPhase || displayPhaseIndex > row.MaxPhase) if (!IsEligibleAtPhase(row, displayPhaseIndex))
{ {
continue; 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); float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase);
for (int i = 0; i < _eligibleRowBuffer.Count; i++) foreach (var row in _eligibleRowBuffer)
{ {
DROutGameDropPool row = _eligibleRowBuffer[i]; for (int rarityIndex = 0; rarityIndex < OrderedRarities.Length; rarityIndex++)
float curveWeight = GetRarityCurveWeight(row.Rarity, phaseT); {
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) if (curveWeight <= 0f)
{ {
continue; continue;
} }
float rowWeight = Mathf.Max(1, row.Weight) * curveWeight; _rarityWeightBuffer[rarityIndex] += rowWeight * curveWeight;
if (_rarityWeightBuffer.TryGetValue(row.Rarity, out float existingWeight))
{
_rarityWeightBuffer[row.Rarity] = existingWeight + rowWeight;
}
else
{
_rarityWeightBuffer[row.Rarity] = rowWeight;
} }
} }
float totalWeight = 0f; 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) if (totalWeight <= 0f)
@@ -144,25 +181,54 @@ namespace GeometryTD.CustomComponent
return RarityType.None; return RarityType.None;
} }
float randomWeight = Random.value * totalWeight; float randomWeight = (float)(random.NextDouble() * totalWeight);
float cumulativeWeight = 0f; 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) 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; 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) private static float GetRarityCurveWeight(RarityType rarityType, float phaseT)
{ {
float hump = Mathf.Exp(-Mathf.Pow((phaseT - 0.35f) / 0.28f, 2f)); float hump = Mathf.Exp(-Mathf.Pow((phaseT - 0.35f) / 0.28f, 2f));
@@ -0,0 +1,3 @@
{
"reference": "GUID:c53de6f5028489b4689382a86bfbd807"
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6bc6ae941e9ae4c43a4f75e3405d0eaf
AssemblyDefinitionReferenceImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using GameFramework.DataTable; using GameFramework.DataTable;
using GeometryTD.DataTable; using GeometryTD.DataTable;
@@ -7,22 +6,12 @@ using GeometryTD.Factory;
using GeometryTD.UI; using GeometryTD.UI;
using UnityEngine; using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
using Random = UnityEngine.Random; using Random = System.Random;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
public sealed class InventoryGenerationComponent : GameFrameworkComponent 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 readonly List<DRShopPrice> _shopPriceRows = new();
private IDataTable<DRShopPrice> _shopPriceTable; private IDataTable<DRShopPrice> _shopPriceTable;
private IDataTable<DROutGameDropPool> _dropPoolTable; private IDataTable<DROutGameDropPool> _dropPoolTable;
@@ -32,22 +21,19 @@ namespace GeometryTD.CustomComponent
private ShopGoodsBuilder _shopGoodsBuilder; private ShopGoodsBuilder _shopGoodsBuilder;
private DropPoolRoller _dropPoolRoller; private DropPoolRoller _dropPoolRoller;
private RewardCandidateBuilder _rewardCandidateBuilder; private RewardCandidateBuilder _rewardCandidateBuilder;
private OutGameDropItemBuilder _outGameDropItemBuilder;
public void ConfigureRunContext(int runSeed, int sequenceIndex)
{
_runSeed = runSeed;
_nodeSequenceIndex = sequenceIndex;
_nextDropTagOrdinal = 0;
_nextRewardTagOrdinal = 0;
}
public List<GoodsItemRawData> BuildShopGoods(int goodsCount, int runSeed = 0, int sequenceIndex = -1) public List<GoodsItemRawData> BuildShopGoods(int goodsCount, int runSeed = 0, int sequenceIndex = -1)
{ {
EnsureShopBuilder(); 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; DREnemy enemy = context.Enemy;
if (enemy == null) if (enemy == null)
@@ -61,14 +47,25 @@ namespace GeometryTD.CustomComponent
? Mathf.Clamp01(enemy.DropPercent * 0.01f) ? Mathf.Clamp01(enemy.DropPercent * 0.01f)
: Mathf.Clamp01(enemy.DropPercent); : 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); gold = Mathf.Max(0, enemy.DropGold);
} }
TowerCompItemData lootItem = null; TowerCompItemData lootItem = null;
if (ShouldRollOutGameItem(context.DisplayPhaseIndex) && if (OutGameDropRuleService.ShouldRollOutGameItem(context.DisplayPhaseIndex, random) &&
TryRollOutGameItem(context.DisplayPhaseIndex, context.ThemeType, out TowerCompItemData droppedItem)) TryRollOutGameItem(
context.DisplayPhaseIndex,
context.ThemeType,
randomContext,
random,
out TowerCompItemData droppedItem))
{ {
lootItem = droppedItem; lootItem = droppedItem;
} }
@@ -79,28 +76,104 @@ namespace GeometryTD.CustomComponent
public IReadOnlyList<TowerCompItemData> BuildRewardCandidates( public IReadOnlyList<TowerCompItemData> BuildRewardCandidates(
int displayPhaseIndex, int displayPhaseIndex,
LevelThemeType themeType, LevelThemeType themeType,
int candidateCount) int candidateCount,
int runSeed,
int sequenceIndex,
ref int nextRewardOrdinal)
{ {
RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder(); RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder();
return rewardCandidateBuilder.BuildCandidates( int rewardOrdinal = nextRewardOrdinal;
IReadOnlyList<TowerCompItemData> candidates = rewardCandidateBuilder.BuildCandidates(
displayPhaseIndex, displayPhaseIndex,
themeType, themeType,
candidateCount, candidateCount,
CreateNextRewardRandomContext,
BuildRewardCandidateItem); 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() private void EnsureShopTables()
{ {
_shopPriceTable ??= GameEntry.DataTable.GetDataTable<DRShopPrice>(); _shopPriceTable ??= GameEntry.DataTable.GetDataTable<DRShopPrice>();
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>(); EnsureComponentTables();
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
if (_shopPriceTable == null || _muzzleCompTable == null || _bearingCompTable == null || if (_shopPriceTable == null)
_baseCompTable == null)
{ {
throw new InvalidOperationException( throw new System.InvalidOperationException(
"InventoryGenerationComponent requires ShopPrice, MuzzleComp, BearingComp, and BaseComp data tables."); "InventoryGenerationComponent requires ShopPrice data table.");
} }
if (_shopPriceRows.Count > 0) if (_shopPriceRows.Count > 0)
@@ -111,7 +184,7 @@ namespace GeometryTD.CustomComponent
DRShopPrice[] rows = _shopPriceTable.GetAllDataRows(); DRShopPrice[] rows = _shopPriceTable.GetAllDataRows();
if (rows == null || rows.Length <= 0) if (rows == null || rows.Length <= 0)
{ {
throw new InvalidOperationException( throw new System.InvalidOperationException(
"InventoryGenerationComponent requires at least one shop price row."); "InventoryGenerationComponent requires at least one shop price row.");
} }
@@ -125,7 +198,7 @@ namespace GeometryTD.CustomComponent
if (_shopPriceRows.Count <= 0) 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); _baseCompTable);
} }
private long AllocateTempInstanceId()
{
return _nextTempInstanceId++;
}
private void EnsureDropTables() private void EnsureDropTables()
{ {
_dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>(); _dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>(); EnsureComponentTables();
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
if (_dropPoolTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null) if (_dropPoolTable == null)
{ {
throw new InvalidOperationException( throw new System.InvalidOperationException(
"InventoryGenerationComponent requires OutGameDropPool, MuzzleComp, BearingComp, and BaseComp data tables."); "InventoryGenerationComponent requires OutGameDropPool data table.");
} }
} }
@@ -171,103 +237,45 @@ namespace GeometryTD.CustomComponent
return _rewardCandidateBuilder; return _rewardCandidateBuilder;
} }
private static bool ShouldRollOutGameItem(int displayPhaseIndex) private OutGameDropItemBuilder EnsureOutGameDropItemBuilder()
{ {
int phaseIndex = Mathf.Max(1, displayPhaseIndex); EnsureDropTables();
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap); _outGameDropItemBuilder ??= new OutGameDropItemBuilder(
return Random.value <= dropChance; _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; droppedItem = null;
DropPoolRoller dropPoolRoller = EnsureDropPoolRoller(); DropPoolRoller dropPoolRoller = EnsureDropPoolRoller();
int phaseIndex = Mathf.Max(1, displayPhaseIndex); 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 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, DROutGameDropPool row,
InventoryTagSourceType sourceType, RarityType rarity,
int localOrdinal, InventoryGenerationRandomContext randomContext)
out TowerCompItemData droppedItem)
{ {
droppedItem = null; if (!EnsureOutGameDropItemBuilder().TryBuildItem(row, rarity, randomContext, out TowerCompItemData droppedItem))
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))
{ {
return null; return null;
} }
@@ -275,16 +283,117 @@ namespace GeometryTD.CustomComponent
return droppedItem; return droppedItem;
} }
private InventoryTagRandomContext CreateRandomContext( private void EnsureComponentTables()
InventoryTagSourceType sourceType,
int localOrdinal,
int configId)
{ {
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), throw new System.InvalidOperationException(
_ => InventoryTagRandomContext.CreateDrop(_runSeed, _nodeSequenceIndex, localOrdinal, configId) "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;
}
}
} }
} }
@@ -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;
}
}
}
}
@@ -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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 519549c7542f40efabf83a2b1b2dff44
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using GeometryTD.DataTable; using GeometryTD.DataTable;
using GeometryTD.Definition; using GeometryTD.Definition;
using UnityEngine; using UnityEngine;
using Random = System.Random;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
@@ -19,7 +20,8 @@ namespace GeometryTD.CustomComponent
int displayPhaseIndex, int displayPhaseIndex,
LevelThemeType themeType, LevelThemeType themeType,
int candidateCount, int candidateCount,
Func<DROutGameDropPool, TowerCompItemData> buildRewardItem) Func<InventoryGenerationRandomContext> createRandomContext,
Func<DROutGameDropPool, RarityType, InventoryGenerationRandomContext, TowerCompItemData> buildRewardItem)
{ {
int resolvedCount = Mathf.Max(0, candidateCount); int resolvedCount = Mathf.Max(0, candidateCount);
if (resolvedCount <= 0) if (resolvedCount <= 0)
@@ -28,7 +30,7 @@ namespace GeometryTD.CustomComponent
} }
List<TowerCompItemData> candidates = new List<TowerCompItemData>(resolvedCount); 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 maxAttempts = Mathf.Max(resolvedCount * 6, resolvedCount);
int phaseIndex = Mathf.Max(1, displayPhaseIndex); int phaseIndex = Mathf.Max(1, displayPhaseIndex);
@@ -36,17 +38,24 @@ namespace GeometryTD.CustomComponent
while (candidates.Count < resolvedCount && attempts < maxAttempts) while (candidates.Count < resolvedCount && attempts < maxAttempts)
{ {
attempts++; 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; break;
} }
if (!selectedPoolRowIds.Add(selectedRow.Id)) if (!selectedPoolEntryKeys.Add(BuildSelectionKey(selectedRow.Id, selectedRarity)))
{ {
continue; continue;
} }
TowerCompItemData candidate = buildRewardItem(selectedRow); TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
if (candidate == null) if (candidate == null)
{ {
continue; continue;
@@ -59,12 +68,19 @@ namespace GeometryTD.CustomComponent
while (candidates.Count < resolvedCount && attempts < maxAttempts) while (candidates.Count < resolvedCount && attempts < maxAttempts)
{ {
attempts++; 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; break;
} }
TowerCompItemData candidate = buildRewardItem(selectedRow); TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
if (candidate == null) if (candidate == null)
{ {
continue; continue;
@@ -75,5 +91,10 @@ namespace GeometryTD.CustomComponent
return candidates; 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.Factory;
using GeometryTD.UI; using GeometryTD.UI;
using UnityEngine; using UnityEngine;
using Random = System.Random;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
@@ -32,8 +33,7 @@ namespace GeometryTD.CustomComponent
public List<GoodsItemRawData> BuildGoods( public List<GoodsItemRawData> BuildGoods(
int goodsCount, int goodsCount,
int runSeed, int runSeed,
int sequenceIndex, int sequenceIndex)
Func<long> allocateInstanceId)
{ {
if (goodsCount <= 0) if (goodsCount <= 0)
{ {
@@ -43,7 +43,9 @@ namespace GeometryTD.CustomComponent
List<GoodsItemRawData> goodsItems = new(goodsCount); List<GoodsItemRawData> goodsItems = new(goodsCount);
for (int i = 0; i < goodsCount; i++) 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; return goodsItems;
@@ -51,18 +53,17 @@ namespace GeometryTD.CustomComponent
private GoodsItemRawData BuildGoodsItem( private GoodsItemRawData BuildGoodsItem(
int goodsIndex, int goodsIndex,
int runSeed, InventoryGenerationRandomContext randomContext)
int sequenceIndex,
Func<long> allocateInstanceId)
{ {
TowerCompItemData sourceItem = BuildRandomComponentItem(goodsIndex, runSeed, sequenceIndex, allocateInstanceId); Random random = randomContext.CreateRandom();
TowerCompItemData sourceItem = BuildRandomComponentItem(randomContext, random);
return new GoodsItemRawData return new GoodsItemRawData
{ {
GoodsIndex = goodsIndex, GoodsIndex = goodsIndex,
Title = sourceItem.Name, Title = sourceItem.Name,
TypeText = BuildTypeText(sourceItem.SlotType), TypeText = BuildTypeText(sourceItem.SlotType),
Description = BuildDescription(sourceItem), Description = BuildDescription(sourceItem),
Price = ResolveRandomPrice(sourceItem.Rarity), Price = ResolveRandomPrice(sourceItem.Rarity, random),
Tags = sourceItem.Tags != null ? (TagType[])sourceItem.Tags.Clone() : Array.Empty<TagType>(), Tags = sourceItem.Tags != null ? (TagType[])sourceItem.Tags.Clone() : Array.Empty<TagType>(),
IconAreaContext = BuildIconAreaContext(sourceItem), IconAreaContext = BuildIconAreaContext(sourceItem),
SourceItem = sourceItem, SourceItem = sourceItem,
@@ -71,83 +72,58 @@ namespace GeometryTD.CustomComponent
} }
private TowerCompItemData BuildRandomComponentItem( private TowerCompItemData BuildRandomComponentItem(
int goodsIndex, InventoryGenerationRandomContext randomContext,
int runSeed, Random random)
int sequenceIndex,
Func<long> allocateInstanceId)
{ {
int slotRoll = UnityEngine.Random.Range(0, 3); int slotRoll = random.Next(0, 3);
DRShopPrice priceRow = _shopPriceRows[UnityEngine.Random.Range(0, _shopPriceRows.Count)]; DRShopPrice priceRow = _shopPriceRows[random.Next(0, _shopPriceRows.Count)];
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity( RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(
priceRow != null ? priceRow.Rarity : RarityType.White); priceRow != null ? priceRow.Rarity : RarityType.White);
return slotRoll switch return slotRoll switch
{ {
0 => BuildRandomMuzzleItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId), 0 => BuildRandomMuzzleItem(rarity, randomContext, random),
1 => BuildRandomBearingItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId), 1 => BuildRandomBearingItem(rarity, randomContext, random),
_ => BuildRandomBaseItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId) _ => BuildRandomBaseItem(rarity, randomContext, random)
}; };
} }
private MuzzleCompItemData BuildRandomMuzzleItem( private MuzzleCompItemData BuildRandomMuzzleItem(
RarityType rarity, RarityType rarity,
int goodsIndex, InventoryGenerationRandomContext randomContext,
int runSeed, Random random)
int sequenceIndex,
Func<long> allocateInstanceId)
{ {
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows(); DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
DRMuzzleComp config = rows[UnityEngine.Random.Range(0, rows.Length)]; DRMuzzleComp config = rows[random.Next(0, rows.Length)];
long instanceId = allocateInstanceId(); long instanceId = randomContext.CreateStableItemInstanceId();
InventoryTagRandomContext randomContext = return ComponentItemFactory.CreateMuzzle(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
return ComponentItemFactory.CreateMuzzle(config, instanceId, rarity, randomContext);
} }
private BearingCompItemData BuildRandomBearingItem( private BearingCompItemData BuildRandomBearingItem(
RarityType rarity, RarityType rarity,
int goodsIndex, InventoryGenerationRandomContext randomContext,
int runSeed, Random random)
int sequenceIndex,
Func<long> allocateInstanceId)
{ {
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows(); DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
DRBearingComp config = rows[UnityEngine.Random.Range(0, rows.Length)]; DRBearingComp config = rows[random.Next(0, rows.Length)];
long instanceId = allocateInstanceId(); long instanceId = randomContext.CreateStableItemInstanceId();
InventoryTagRandomContext randomContext = return ComponentItemFactory.CreateBearing(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
return ComponentItemFactory.CreateBearing(config, instanceId, rarity, randomContext);
} }
private BaseCompItemData BuildRandomBaseItem( private BaseCompItemData BuildRandomBaseItem(
RarityType rarity, RarityType rarity,
int goodsIndex, InventoryGenerationRandomContext randomContext,
int runSeed, Random random)
int sequenceIndex,
Func<long> allocateInstanceId)
{ {
DRBaseComp[] rows = _baseCompTable.GetAllDataRows(); DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
DRBaseComp config = rows[UnityEngine.Random.Range(0, rows.Length)]; DRBaseComp config = rows[random.Next(0, rows.Length)];
long instanceId = allocateInstanceId(); long instanceId = randomContext.CreateStableItemInstanceId();
InventoryTagRandomContext randomContext = return ComponentItemFactory.CreateBase(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
return ComponentItemFactory.CreateBase(config, instanceId, rarity, randomContext);
} }
private int ResolveRandomPrice(RarityType rarity) private int ResolveRandomPrice(RarityType rarity, Random random)
{ {
for (int i = 0; i < _shopPriceRows.Count; i++) return ShopPriceRuleService.ResolveRandomBuyPrice(rarity, random);
{
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;
} }
private static IconAreaContext BuildIconAreaContext(TowerCompItemData item) private static IconAreaContext BuildIconAreaContext(TowerCompItemData item)
@@ -0,0 +1,3 @@
{
"reference": "GUID:c53de6f5028489b4689382a86bfbd807"
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 93d673219f2a1a94599e67456ea33f7f
AssemblyDefinitionReferenceImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -14,6 +14,7 @@ namespace GeometryTD.CustomComponent
private PlayerInventoryCommandModel _commandModel; private PlayerInventoryCommandModel _commandModel;
private PlayerInventoryTowerRosterService _towerRosterService; private PlayerInventoryTowerRosterService _towerRosterService;
private PlayerInventoryTowerAssemblyService _towerAssemblyService; private PlayerInventoryTowerAssemblyService _towerAssemblyService;
private PlayerInventoryTradeService _tradeService;
public int Gold public int Gold
{ {
@@ -100,6 +101,12 @@ namespace GeometryTD.CustomComponent
return _commandModel.TryConsumeGold(costGold); return _commandModel.TryConsumeGold(costGold);
} }
public bool TryPurchaseComponent(TowerCompItemData item, int price)
{
EnsureInitialized();
return _tradeService.TryPurchaseComponent(item, price);
}
public void AddGold(int gainGold) public void AddGold(int gainGold)
{ {
EnsureInitialized(); EnsureInitialized();
@@ -138,6 +145,24 @@ namespace GeometryTD.CustomComponent
return _towerRosterService.ReduceTowerEndurance(towerInstanceIds, enduranceLoss); 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() private void EnsureInitialized()
{ {
if (_queryModel.IsInitialized) if (_queryModel.IsInitialized)
@@ -155,6 +180,7 @@ namespace GeometryTD.CustomComponent
_commandModel ??= new PlayerInventoryCommandModel(_state); _commandModel ??= new PlayerInventoryCommandModel(_state);
_towerRosterService ??= new PlayerInventoryTowerRosterService(_queryModel, MaxParticipantTowerCount); _towerRosterService ??= new PlayerInventoryTowerRosterService(_queryModel, MaxParticipantTowerCount);
_towerAssemblyService ??= new PlayerInventoryTowerAssemblyService(_queryModel, _commandModel); _towerAssemblyService ??= new PlayerInventoryTowerAssemblyService(_queryModel, _commandModel);
_tradeService ??= new PlayerInventoryTradeService(_queryModel, _commandModel);
} }
} }
} }
@@ -123,7 +123,7 @@ namespace GeometryTD.CustomComponent
int[] values = new int[TowerLevelCount]; int[] values = new int[TowerLevelCount];
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
values[i] = baseValue + perLevel * i; values[i] = Mathf.Max(0, baseValue + perLevel * i);
} }
return values; return values;
@@ -135,7 +135,7 @@ namespace GeometryTD.CustomComponent
float[] values = new float[TowerLevelCount]; float[] values = new float[TowerLevelCount];
for (int i = 0; i < values.Length; i++) for (int i = 0; i < values.Length; i++)
{ {
values[i] = baseValue + perLevel * i; values[i] = Mathf.Max(0f, baseValue + perLevel * i);
} }
return values; return values;
@@ -180,5 +180,37 @@ namespace GeometryTD.CustomComponent
_drBaseComp ??= GameEntry.DataTable.GetDataTable<DRBaseComp>(); _drBaseComp ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
return _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;
} }
} }
@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using GeometryTD.Core;
using GeometryTD.CustomUtility; using GeometryTD.CustomUtility;
using GeometryTD.Definition; using GeometryTD.Definition;
using UnityEngine; using UnityEngine;
@@ -36,101 +37,10 @@ namespace GeometryTD.CustomComponent
public int ReduceTowerEndurance(IReadOnlyList<long> towerInstanceIds, float enduranceLoss) public int ReduceTowerEndurance(IReadOnlyList<long> towerInstanceIds, float enduranceLoss)
{ {
float resolvedLoss = Mathf.Max(0f, enduranceLoss); return InventoryTowerEnduranceUtility.ReduceTowerEndurance(
BackpackInventoryData inventory = _queryModel.Inventory; _queryModel.Inventory,
if (resolvedLoss <= 0f || towerInstanceIds,
towerInstanceIds == null || enduranceLoss);
towerInstanceIds.Count <= 0 ||
inventory.Towers == null ||
inventory.Towers.Count <= 0)
{
return 0;
}
Dictionary<long, MuzzleCompItemData> muzzleMap = BuildComponentMap(inventory.MuzzleComponents);
Dictionary<long, BearingCompItemData> bearingMap = BuildComponentMap(inventory.BearingComponents);
Dictionary<long, BaseCompItemData> baseMap = BuildComponentMap(inventory.BaseComponents);
HashSet<long> processedTowerIds = new HashSet<long>();
int affectedCount = 0;
for (int i = 0; i < towerInstanceIds.Count; i++)
{
long towerInstanceId = towerInstanceIds[i];
if (towerInstanceId <= 0 || !processedTowerIds.Add(towerInstanceId))
{
continue;
}
if (!InventoryParticipantUtility.TryGetTowerById(inventory, towerInstanceId, out TowerItemData tower) ||
tower == null)
{
continue;
}
bool towerAffected = false;
if (muzzleMap.TryGetValue(tower.MuzzleComponentInstanceId, out MuzzleCompItemData muzzleComp))
{
towerAffected |= TryReduceComponentEndurance(muzzleComp, resolvedLoss);
}
if (bearingMap.TryGetValue(tower.BearingComponentInstanceId, out BearingCompItemData bearingComp))
{
towerAffected |= TryReduceComponentEndurance(bearingComp, resolvedLoss);
}
if (baseMap.TryGetValue(tower.BaseComponentInstanceId, out BaseCompItemData baseComp))
{
towerAffected |= TryReduceComponentEndurance(baseComp, resolvedLoss);
}
if (towerAffected)
{
affectedCount++;
}
}
return affectedCount;
}
private static bool TryReduceComponentEndurance(TowerCompItemData component, float enduranceLoss)
{
if (component == null)
{
return false;
}
float originalEndurance = component.Endurance;
float nextEndurance = Mathf.Clamp(originalEndurance - Mathf.Max(0f, enduranceLoss), 0f, 100f);
if (nextEndurance >= originalEndurance)
{
return false;
}
component.Endurance = nextEndurance;
return true;
}
private static Dictionary<long, TComp> BuildComponentMap<TComp>(List<TComp> components)
where TComp : TowerCompItemData
{
Dictionary<long, TComp> map = new Dictionary<long, TComp>();
if (components == null || components.Count <= 0)
{
return map;
}
for (int i = 0; i < components.Count; i++)
{
TComp component = components[i];
if (component == null || component.InstanceId <= 0)
{
continue;
}
map[component.InstanceId] = component;
}
return map;
} }
} }
} }
@@ -0,0 +1,389 @@
using System.Collections.Generic;
using GeometryTD.CustomUtility;
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;
public PlayerInventoryTradeService(
PlayerInventoryQueryModel queryModel,
PlayerInventoryCommandModel commandModel)
{
_queryModel = queryModel;
_commandModel = commandModel;
}
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))
{
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),
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;
}
}
}
@@ -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 public class ShopNodeComponent : GameFrameworkComponent
{ {
private string _activeRunId; private RunNodeExecutionContext _activeContext;
private int _activeRunSeed;
private int _activeNodeId;
private RunNodeType _activeNodeType = RunNodeType.None;
private int _activeSequenceIndex = -1;
private ShopFormUseCase _shopFormUseCase; private ShopFormUseCase _shopFormUseCase;
private bool _initialized; private bool _initialized;
@@ -29,26 +25,30 @@ namespace GeometryTD.CustomComponent
_initialized = true; _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) if (!_initialized)
{ {
OnInit(); OnInit();
} }
int runSeed = context?.RunSeed ?? 0;
int sequenceIndex = context?.SequenceIndex ?? -1;
if (_shopFormUseCase == null || !_shopFormUseCase.PrepareForOpen(runSeed, sequenceIndex)) if (_shopFormUseCase == null || !_shopFormUseCase.PrepareForOpen(runSeed, sequenceIndex))
{ {
Log.Warning("ShopNodeComponent.StartShop() failed. Shop use case is unavailable or goods generation failed."); Log.Warning("ShopNodeComponent.StartShop() failed. Shop use case is unavailable or goods generation failed.");
return; return;
} }
_activeRunId = runId; _activeContext = context != null ? context.Clone() : null;
_activeRunSeed = runSeed;
_activeNodeId = nodeId;
_activeNodeType = nodeType;
_activeSequenceIndex = sequenceIndex;
GameEntry.UIRouter.OpenUI(UIFormType.ShopForm); 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() public void EndShop()
@@ -57,23 +57,22 @@ namespace GeometryTD.CustomComponent
GameEntry.Event.Fire( GameEntry.Event.Fire(
this, this,
NodeCompleteEventArgs.Create( NodeCompleteEventArgs.Create(
_activeRunId, _activeContext?.RunId,
_activeNodeId, _activeContext?.NodeId ?? 0,
_activeNodeType, _activeContext?.NodeType ?? RunNodeType.None,
_activeSequenceIndex, _activeContext?.SequenceIndex ?? -1,
RunNodeCompletionStatus.Completed, RunNodeCompletionStatus.Completed,
true, true,
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)); _activeContext != null
? _activeContext.CreateCompletionSnapshot(
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null)
: null));
ClearActiveNodeContext(); ClearActiveNodeContext();
} }
private void ClearActiveNodeContext() private void ClearActiveNodeContext()
{ {
_activeRunId = null; _activeContext = null;
_activeRunSeed = 0;
_activeNodeId = 0;
_activeNodeType = RunNodeType.None;
_activeSequenceIndex = -1;
} }
} }
} }
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 523d63397a9c41fdaaf238bfb0e95357
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
{
"reference": "GUID:c53de6f5028489b4689382a86bfbd807"
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e09026219ecab5e44ab54879792e2b26
AssemblyDefinitionReferenceImporter:
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,15 +1,8 @@
//------------------------------------------------------------ using System;
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using System;
using System.IO; using System.IO;
using UnityEngine; using UnityEngine;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
public static class BinaryReaderExtension public static class BinaryReaderExtension
{ {
@@ -1,9 +1,8 @@
using System; using System;
using GeometryTD.Definition; using GeometryTD.Core;
using GeometryTD.CustomUtility;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 底座组件表 /// 底座组件表
@@ -1,9 +1,8 @@
using System; using System;
using GeometryTD.Definition; using GeometryTD.Core;
using GeometryTD.CustomUtility;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 轴承组件表 /// 轴承组件表
+1 -1
View File
@@ -1,6 +1,6 @@
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 敌人配置表 /// 敌人配置表
@@ -16,7 +16,7 @@ using System.Text;
using UnityEngine; using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 实体表。 /// 实体表。
+32 -2
View File
@@ -1,6 +1,7 @@
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
using Newtonsoft.Json.Linq;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 事件配置表 /// 事件配置表
@@ -30,6 +31,14 @@ namespace GeometryTD.DataTable
/// <remarks>原始字符串(如 JSON 文本),不在此处做解析。</remarks> /// <remarks>原始字符串(如 JSON 文本),不在此处做解析。</remarks>
public string OptionsRaw { get; private set; } 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) public override bool ParseDataRow(string dataRowString, object userData)
{ {
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators); string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
@@ -44,9 +53,30 @@ namespace GeometryTD.DataTable
index++; index++;
Title = columnStrings[index++]; Title = columnStrings[index++];
Description = 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; 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);
}
} }
} }
+2 -3
View File
@@ -1,9 +1,8 @@
using System; using System;
using GeometryTD.Definition; using GeometryTD.Core;
using GeometryTD.CustomUtility;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 关卡表 /// 关卡表
@@ -1,9 +1,8 @@
using System; using System;
using GeometryTD.CustomUtility; using GeometryTD.Core;
using GeometryTD.Definition;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 关卡阶段表 /// 关卡阶段表
@@ -1,9 +1,8 @@
using System; using System;
using GeometryTD.CustomUtility; using GeometryTD.Core;
using GeometryTD.Definition;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 关卡阶段出怪条目表 /// 关卡阶段出怪条目表
+1 -1
View File
@@ -16,7 +16,7 @@ using System.Text;
using UnityEngine; using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 音乐配置表。 /// 音乐配置表。
@@ -1,9 +1,8 @@
using System; using System;
using GeometryTD.Definition; using GeometryTD.Core;
using GeometryTD.CustomUtility;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 枪口组件表 /// 枪口组件表
@@ -1,8 +1,9 @@
using GeometryTD.CustomUtility; using System;
using GeometryTD.Definition; using GeometryTD.Core;
using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 局外道具掉落池配置表。 /// 局外道具掉落池配置表。
@@ -14,12 +15,11 @@ namespace GeometryTD.DataTable
public override int Id => m_Id; public override int Id => m_Id;
public LevelThemeType LevelThemeType { get; private set; } public LevelThemeType LevelThemeType { get; private set; }
public RarityType Rarity { get; private set; }
public string ItemType { get; private set; } public string ItemType { get; private set; }
public int ItemId { get; private set; } public int ItemId { get; private set; }
public int Weight { get; private set; } public int[] Weights { get; private set; }
public int MinPhase { get; private set; } public int[] MinPhase { get; private set; }
public int MaxPhase { get; private set; } public int[] MaxPhase { get; private set; }
public override bool ParseDataRow(string dataRowString, object userData) public override bool ParseDataRow(string dataRowString, object userData)
{ {
@@ -34,31 +34,110 @@ namespace GeometryTD.DataTable
m_Id = int.Parse(columnStrings[index++]); m_Id = int.Parse(columnStrings[index++]);
index++; index++;
LevelThemeType = EnumUtility<LevelThemeType>.Get(columnStrings[index++]); LevelThemeType = EnumUtility<LevelThemeType>.Get(columnStrings[index++]);
Rarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
ItemType = columnStrings[index++]; ItemType = columnStrings[index++];
ItemId = ParseIntOrDefault(columnStrings[index++], 0); ItemId = ParseIntOrDefault(columnStrings[index++], 0);
Weight = ParseIntOrDefault(columnStrings[index++], 1); Weights = ParseWeights(columnStrings[index++]);
MinPhase = ParseIntOrDefault(columnStrings[index++], 1); MinPhase = ParsePhases(columnStrings[index++], 1);
MaxPhase = ParseIntOrDefault(columnStrings[index++], int.MaxValue); MaxPhase = ParsePhases(columnStrings[index++], int.MaxValue);
NormalizePhaseRanges();
if (Weight <= 0)
{
Weight = 1;
}
if (MinPhase <= 0)
{
MinPhase = 1;
}
if (MaxPhase < MinPhase)
{
MaxPhase = MinPhase;
}
return true; 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) private static int ParseIntOrDefault(string raw, int fallbackValue)
{ {
if (int.TryParse(raw, out int parsed)) if (int.TryParse(raw, out int parsed))
@@ -68,5 +147,7 @@ namespace GeometryTD.DataTable
return fallbackValue; return fallbackValue;
} }
private const int RarityCount = (int)RarityType.Red;
} }
} }
@@ -1,8 +1,7 @@
using GeometryTD.CustomUtility; using GeometryTD.Core;
using GeometryTD.Definition;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
public sealed class DRRarityTagBudget : DataRowBase public sealed class DRRarityTagBudget : DataRowBase
{ {
+1 -1
View File
@@ -6,7 +6,7 @@ using System.Text;
using UnityEngine; using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 场景配置表。 /// 场景配置表。
@@ -1,8 +1,7 @@
using GeometryTD.CustomUtility; using GeometryTD.Core;
using GeometryTD.Definition;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
public class DRShopPrice : DataRowBase public class DRShopPrice : DataRowBase
{ {
+1 -1
View File
@@ -2,7 +2,7 @@
using System.Text; using System.Text;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 声音配置表。 /// 声音配置表。
+2 -3
View File
@@ -1,9 +1,8 @@
using System; using System;
using GeometryTD.Definition; using GeometryTD.Core;
using GeometryTD.CustomUtility;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// Tag 表 /// Tag 表
@@ -1,8 +1,7 @@
using GeometryTD.CustomUtility; using GeometryTD.Core;
using GeometryTD.Definition;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
public sealed class DRTagConfig : DataRowBase public sealed class DRTagConfig : DataRowBase
{ {
@@ -2,7 +2,7 @@
using System.Text; using System.Text;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 界面配置表。 /// 界面配置表。
@@ -16,7 +16,7 @@ using System.Text;
using UnityEngine; using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 声音配置表。 /// 声音配置表。
@@ -10,7 +10,7 @@ using System;
using UnityEngine; using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.Core
{ {
public static class DataTableExtension public static class DataTableExtension
{ {
@@ -1,11 +1,8 @@
{ {
"name": "GeometryTD.Runtime", "name": "GeometryTD.Core",
"rootNamespace": "GeometryTD", "rootNamespace": "GeometryTD.Core",
"references": [ "references": [
"UnityGameFramework.Editor", "GUID:363c5eb08ff8e6a439b85e37b8c20d96"
"UnityGameFramework.Runtime",
"Unity.InputSystem",
"Unity.TextMeshPro"
], ],
"includePlatforms": [], "includePlatforms": [],
"excludePlatforms": [], "excludePlatforms": [],
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 771b79705393da244ae720aa28ae8a69
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
public enum CombatParticipantTowerValidationFailureReason public enum CombatParticipantTowerValidationFailureReason
{ {
@@ -1,4 +1,4 @@
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
public static class CombatParticipantTowerValidationText public static class CombatParticipantTowerValidationText
{ {
@@ -1,8 +1,8 @@
using System; using System;
using GeometryTD.CustomUtility; using GeometryTD.Core;
using UnityEngine; using UnityEngine;
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
[Serializable] [Serializable]
public sealed class AttackPayload public sealed class AttackPayload
@@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 玩家背包聚合数据。 /// 玩家背包聚合数据。
@@ -5,7 +5,7 @@
// Feedback: mailto:ellan@gameframework.cn // Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------ //------------------------------------------------------------
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
public class BuildInfo public class BuildInfo
{ {
@@ -1,4 +1,4 @@
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
public class EventItem public class EventItem
{ {
@@ -1,4 +1,4 @@
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
public class EventOption public class EventOption
{ {
@@ -1,8 +1,7 @@
using System; using System;
using GeometryTD.Entity;
using UnityEngine; using UnityEngine;
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
[Serializable] [Serializable]
public sealed class HitStatusModifierContext public sealed class HitStatusModifierContext
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
[StructLayout(LayoutKind.Auto)] [StructLayout(LayoutKind.Auto)]
public struct ImpactData public struct ImpactData
@@ -1,6 +1,6 @@
using System; using System;
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 背包内组件实例基类(非 DataTable,表示玩家持有物)。 /// 背包内组件实例基类(非 DataTable,表示玩家持有物)。
@@ -2,7 +2,7 @@ using System;
using Newtonsoft.Json; using Newtonsoft.Json;
using UnityEngine; using UnityEngine;
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 背包内防御塔实例数据。 /// 背包内防御塔实例数据。
@@ -1,6 +1,6 @@
using System; using System;
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 防御塔独立属性快照。 /// 防御塔独立属性快照。
@@ -1,4 +1,4 @@
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
public class VersionInfo public class VersionInfo
{ {
@@ -1,4 +1,4 @@
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
public enum AttackMethodType : byte public enum AttackMethodType : byte
{ {
@@ -1,4 +1,4 @@
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
public enum AttackPropertyType : byte public enum AttackPropertyType : byte
{ {
@@ -1,4 +1,4 @@
namespace GeometryTD.Definition namespace GeometryTD.Core
{ {
/// <summary> /// <summary>
/// 阵营类型。 /// 阵营类型。
@@ -1,4 +1,4 @@
namespace GeometryTD.UI namespace GeometryTD.Core
{ {
public enum CombatSelectActionType : byte public enum CombatSelectActionType : byte
{ {

Some files were not shown because too many files have changed in this diff Show More