前言在这里插入图片描述

前言

三国杀中的克制关系是游戏策略的核心。一个好的攻略App需要清晰展示武将间的克制链条,帮助玩家做出更优的选择。本文将介绍如何用Flutter实现一个完整的克制关系系统,包括数据模型、网络图可视化和智能推荐功能。

克制关系数据模型

首先定义克制关系的核心数据结构。我们需要存储武将的克制信息、被克制信息以及相关的统计数据。

// lib/models/hero_counter_model.dart
class CounterRelation {
  final String targetHeroId;
  final String targetHeroName;
  final String targetHeroAvatar;
  final double counterStrength;
  final String reason;
  final List<String> counterMethods;
  final double winRate;
  final int sampleSize;

  CounterRelation({
    required this.targetHeroId,
    required this.targetHeroName,
    required this.targetHeroAvatar,
    required this.counterStrength,
    required this.reason,
    required this.counterMethods,
    required this.winRate,
    required this.sampleSize,
  });

  factory CounterRelation.fromJson(Map<String, dynamic> json) {
    return CounterRelation(
      targetHeroId: json['targetHeroId'] ?? '',
      targetHeroName: json['targetHeroName'] ?? '',
      targetHeroAvatar: json['targetHeroAvatar'] ?? '',
      counterStrength: (json['counterStrength'] ?? 0.0).toDouble(),
      reason: json['reason'] ?? '',
      counterMethods: List<String>.from(json['counterMethods'] ?? []),
      winRate: (json['winRate'] ?? 0.0).toDouble(),
      sampleSize: json['sampleSize'] ?? 0,
    );
  }

  Map<String, dynamic> toJson() => {
    'targetHeroId': targetHeroId,
    'targetHeroName': targetHeroName,
    'targetHeroAvatar': targetHeroAvatar,
    'counterStrength': counterStrength,
    'reason': reason,
    'counterMethods': counterMethods,
    'winRate': winRate,
    'sampleSize': sampleSize,
  };
}

这个模型包含了克制的目标武将信息、克制强度(0-100)、克制原因和具体方法。counterStrength 表示克制程度,winRate 是基于实际对战数据的胜率。

class HeroCounterModel {
  final String heroId;
  final String heroName;
  final String heroAvatar;
  final String faction;
  final List<CounterRelation> counters;
  final List<CounterRelation> counteredBy;
  final double overallCounterScore;

  HeroCounterModel({
    required this.heroId,
    required this.heroName,
    required this.heroAvatar,
    required this.faction,
    required this.counters,
    required this.counteredBy,
    required this.overallCounterScore,
  });

  factory HeroCounterModel.fromJson(Map<String, dynamic> json) {
    return HeroCounterModel(
      heroId: json['heroId'] ?? '',
      heroName: json['heroName'] ?? '',
      heroAvatar: json['heroAvatar'] ?? '',
      faction: json['faction'] ?? '',
      counters: (json['counters'] as List?)
          ?.map((e) => CounterRelation.fromJson(e))
          .toList() ?? [],
      counteredBy: (json['counteredBy'] as List?)
          ?.map((e) => CounterRelation.fromJson(e))
          .toList() ?? [],
      overallCounterScore: (json['overallCounterScore'] ?? 0.0).toDouble(),
    );
  }

  Map<String, dynamic> toJson() => {
    'heroId': heroId,
    'heroName': heroName,
    'heroAvatar': heroAvatar,
    'faction': faction,
    'counters': counters.map((e) => e.toJson()).toList(),
    'counteredBy': counteredBy.map((e) => e.toJson()).toList(),
    'overallCounterScore': overallCounterScore,
  };
}

HeroCounterModel 是主要的数据模型,包含了一个武将的所有克制关系。counters 列表存储该武将能克制的其他武将,counteredBy 存储克制该武将的武将。

克制关系控制器

使用GetX管理克制关系的状态和业务逻辑。控制器负责数据加载、筛选、搜索和推荐。

// lib/controllers/hero_counter_controller.dart
class HeroCounterController extends GetxController {
  final CounterService _counterService = Get.find<CounterService>();
  
  final RxList<HeroCounterModel> allCounters = <HeroCounterModel>[].obs;
  final Rx<HeroCounterModel?> selectedHero = Rx<HeroCounterModel?>(null);
  final RxBool isLoading = false.obs;
  final RxString searchQuery = ''.obs;
  final RxDouble minCounterStrength = 0.0.obs;

  
  void onInit() {
    super.onInit();
    loadCounterData();
  }

  Future<void> loadCounterData() async {
    try {
      isLoading.value = true;
      final data = await _counterService.getAllCounterRelations();
      allCounters.assignAll(data);
    } catch (e) {
      Get.snackbar('错误', '加载克制关系数据失败: $e');
    } finally {
      isLoading.value = false;
    }
  }

  void selectHero(String heroId) {
    final hero = allCounters.firstWhereOrNull((h) => h.heroId == heroId);
    selectedHero.value = hero;
  }

  void searchHero(String query) {
    searchQuery.value = query;
    if (query.isNotEmpty) {
      final hero = allCounters.firstWhereOrNull(
        (h) => h.heroName.toLowerCase().contains(query.toLowerCase()),
      );
      if (hero != null) {
        selectHero(hero.heroId);
      }
    }
  }

  List<CounterRelation> getRecommendedCounters(String targetHeroId) {
    final recommendations = <CounterRelation>[];
    
    for (final hero in allCounters) {
      final counter = hero.counters.firstWhereOrNull(
        (c) => c.targetHeroId == targetHeroId,
      );
      if (counter != null && counter.counterStrength >= 60) {
        recommendations.add(counter);
      }
    }
    
    recommendations.sort((a, b) => b.counterStrength.compareTo(a.counterStrength));
    return recommendations.take(5).toList();
  }

  Map<String, dynamic> analyzeHeroCounter(String heroId) {
    final hero = allCounters.firstWhereOrNull((h) => h.heroId == heroId);
    if (hero == null) return {};
    
    final strongCounters = hero.counters.where((c) => c.counterStrength >= 70).length;
    final strongCounteredBy = hero.counteredBy.where((c) => c.counterStrength >= 70).length;
    
    return {
      'strongCounters': strongCounters,
      'strongCounteredBy': strongCounteredBy,
      'overallScore': hero.overallCounterScore,
      'recommendation': _getRecommendation(hero),
    };
  }

  String _getRecommendation(HeroCounterModel hero) {
    if (hero.overallCounterScore >= 80) {
      return '强势武将,适合多数情况';
    } else if (hero.overallCounterScore >= 60) {
      return '均衡武将,需根据对手选择';
    } else {
      return '特定环境武将,谨慎选择';
    }
  }
}

控制器提供了loadCounterData() 方法从服务加载数据,getRecommendedCounters() 根据目标武将推荐克制选择,analyzeHeroCounter() 分析武将的克制能力。

克制关系列表卡片

创建一个卡片组件展示单个武将的克制信息。

// lib/widgets/counter_card.dart
class CounterCard extends StatelessWidget {
  final HeroCounterModel hero;
  final VoidCallback onTap;

  const CounterCard({
    Key? key,
    required this.hero,
    required this.onTap,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Card(
        margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
        child: Padding(
          padding: EdgeInsets.all(12.w),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  CircleAvatar(
                    radius: 24.r,
                    backgroundImage: NetworkImage(hero.heroAvatar),
                  ),
                  SizedBox(width: 12.w),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          hero.heroName,
                          style: TextStyle(
                            fontSize: 16.sp,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        Text(
                          '${hero.faction}国',
                          style: TextStyle(
                            fontSize: 12.sp,
                            color: Colors.grey[600],
                          ),
                        ),
                      ],
                    ),
                  ),
                  _buildScoreBadge(hero.overallCounterScore),
                ],
              ),
              SizedBox(height: 12.h),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  _buildStatItem('克制', hero.counters.length.toString()),
                  _buildStatItem('被克', hero.counteredBy.length.toString()),
                  _buildStatItem('评分', hero.overallCounterScore.toInt().toString()),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildScoreBadge(double score) {
    final color = score >= 80 ? Colors.green : score >= 60 ? Colors.orange : Colors.red;
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(12.r),
      ),
      child: Text(
        '${score.toInt()}',
        style: TextStyle(
          color: Colors.white,
          fontSize: 12.sp,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }

  Widget _buildStatItem(String label, String value) {
    return Column(
      children: [
        Text(
          value,
          style: TextStyle(
            fontSize: 16.sp,
            fontWeight: FontWeight.bold,
            color: Colors.red[700],
          ),
        ),
        Text(
          label,
          style: TextStyle(
            fontSize: 12.sp,
            color: Colors.grey[600],
          ),
        ),
      ],
    );
  }
}

这个卡片组件展示了武将的基本信息、克制数量和评分。_buildScoreBadge() 根据评分显示不同的颜色,直观表示武将的强度。

克制详情页面

创建详情页面展示武将的详细克制关系和推荐。

// lib/screens/hero_counter_detail_screen.dart
class HeroCounterDetailScreen extends StatelessWidget {
  final HeroCounterModel hero;

  const HeroCounterDetailScreen({
    Key? key,
    required this.hero,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    final controller = Get.find<HeroCounterController>();
    final analysis = controller.analyzeHeroCounter(hero.heroId);
    
    return Scaffold(
      appBar: AppBar(
        title: Text(hero.heroName),
        backgroundColor: Colors.red[700],
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildHeroHeader(hero),
            _buildAnalysisSection(analysis),
            _buildCountersSection('克制的武将', hero.counters),
            _buildCountersSection('被克制的武将', hero.counteredBy),
          ],
        ),
      ),
    );
  }

  Widget _buildHeroHeader(HeroCounterModel hero) {
    return Container(
      padding: EdgeInsets.all(16.w),
      color: Colors.red[700],
      child: Column(
        children: [
          CircleAvatar(
            radius: 40.r,
            backgroundImage: NetworkImage(hero.heroAvatar),
          ),
          SizedBox(height: 12.h),
          Text(
            hero.heroName,
            style: TextStyle(
              fontSize: 20.sp,
              fontWeight: FontWeight.bold,
              color: Colors.white,
            ),
          ),
          Text(
            '${hero.faction}国 | 克制评分: ${hero.overallCounterScore.toInt()}',
            style: TextStyle(
              fontSize: 14.sp,
              color: Colors.white70,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildAnalysisSection(Map<String, dynamic> analysis) {
    return Container(
      margin: EdgeInsets.all(16.w),
      padding: EdgeInsets.all(12.w),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.red[700]!),
        borderRadius: BorderRadius.circular(8.r),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '克制分析',
            style: TextStyle(
              fontSize: 16.sp,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 8.h),
          Text(
            '强势克制: ${analysis['strongCounters']} 个武将',
            style: TextStyle(fontSize: 14.sp),
          ),
          Text(
            '被强克: ${analysis['strongCounteredBy']} 个武将',
            style: TextStyle(fontSize: 14.sp),
          ),
          SizedBox(height: 8.h),
          Text(
            '建议: ${analysis['recommendation']}',
            style: TextStyle(
              fontSize: 14.sp,
              color: Colors.red[700],
              fontWeight: FontWeight.bold,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildCountersSection(String title, List<CounterRelation> relations) {
    return Container(
      margin: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            title,
            style: TextStyle(
              fontSize: 16.sp,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 8.h),
          if (relations.isEmpty)
            Text(
              '暂无数据',
              style: TextStyle(
                fontSize: 14.sp,
                color: Colors.grey[600],
              ),
            )
          else
            ...relations.take(5).map((relation) => _buildCounterItem(relation)),
        ],
      ),
    );
  }

  Widget _buildCounterItem(CounterRelation relation) {
    return Container(
      margin: EdgeInsets.only(bottom: 8.h),
      padding: EdgeInsets.all(8.w),
      decoration: BoxDecoration(
        color: Colors.grey[100],
        borderRadius: BorderRadius.circular(8.r),
      ),
      child: Row(
        children: [
          CircleAvatar(
            radius: 16.r,
            backgroundImage: NetworkImage(relation.targetHeroAvatar),
          ),
          SizedBox(width: 8.w),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  relation.targetHeroName,
                  style: TextStyle(
                    fontSize: 14.sp,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                Text(
                  relation.reason,
                  style: TextStyle(
                    fontSize: 12.sp,
                    color: Colors.grey[600],
                  ),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              ],
            ),
          ),
          Container(
            padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
            decoration: BoxDecoration(
              color: _getStrengthColor(relation.counterStrength),
              borderRadius: BorderRadius.circular(4.r),
            ),
            child: Text(
              '${relation.counterStrength.toInt()}%',
              style: TextStyle(
                fontSize: 12.sp,
                color: Colors.white,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Color _getStrengthColor(double strength) {
    if (strength >= 80) return Colors.red;
    if (strength >= 60) return Colors.orange;
    if (strength >= 40) return Colors.yellow;
    return Colors.grey;
  }
}

详情页面展示了武将的完整克制信息。_buildCounterItem() 组件以列表形式展示每个克制关系,包括克制强度的颜色编码。

克制关系列表页面

主页面展示所有武将的克制关系列表。

// lib/screens/hero_counter_screen.dart
class HeroCounterScreen extends StatelessWidget {
  const HeroCounterScreen({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final controller = Get.put(HeroCounterController());
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('武将克制关系'),
        backgroundColor: Colors.red[700],
        foregroundColor: Colors.white,
        actions: [
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () => _showSearchDialog(controller),
          ),
        ],
      ),
      body: Obx(() {
        if (controller.isLoading.value) {
          return const Center(child: CircularProgressIndicator());
        }
        
        return ListView.builder(
          itemCount: controller.allCounters.length,
          itemBuilder: (context, index) {
            final hero = controller.allCounters[index];
            return CounterCard(
              hero: hero,
              onTap: () {
                Get.to(() => HeroCounterDetailScreen(hero: hero));
              },
            );
          },
        );
      }),
    );
  }

  void _showSearchDialog(HeroCounterController controller) {
    Get.dialog(
      AlertDialog(
        title: const Text('搜索武将'),
        content: TextField(
          decoration: const InputDecoration(
            hintText: '输入武将名称',
            prefixIcon: Icon(Icons.search),
          ),
          onChanged: controller.searchHero,
        ),
        actions: [
          TextButton(
            onPressed: () => Get.back(),
            child: const Text('关闭'),
          ),
        ],
      ),
    );
  }
}

这个页面使用 Obx 响应式地显示武将列表。当用户点击卡片时,导航到详情页面查看完整的克制关系。

克制关系服务

服务层负责与后端API通信获取克制关系数据。

// lib/services/counter_service.dart
class CounterService extends GetxService {
  final ApiClient _apiClient = Get.find<ApiClient>();

  Future<List<HeroCounterModel>> getAllCounterRelations() async {
    try {
      final response = await _apiClient.get('/api/heroes/counters');
      final List<dynamic> data = response.data ?? [];
      return data.map((e) => HeroCounterModel.fromJson(e)).toList();
    } catch (e) {
      throw Exception('获取克制关系失败: $e');
    }
  }

  Future<HeroCounterModel> getHeroCounters(String heroId) async {
    try {
      final response = await _apiClient.get('/api/heroes/$heroId/counters');
      return HeroCounterModel.fromJson(response.data);
    } catch (e) {
      throw Exception('获取武将克制关系失败: $e');
    }
  }

  Future<List<CounterRelation>> getRecommendedCounters(String targetHeroId) async {
    try {
      final response = await _apiClient.get(
        '/api/heroes/counters/recommend',
        queryParameters: {'targetHeroId': targetHeroId},
      );
      final List<dynamic> data = response.data ?? [];
      return data.map((e) => CounterRelation.fromJson(e)).toList();
    } catch (e) {
      throw Exception('获取推荐克制失败: $e');
    }
  }
}

服务提供了三个主要方法:获取所有克制关系、获取特定武将的克制关系、获取推荐克制武将。

总结

本文实现了一个完整的武将克制关系系统,包括数据模型、状态管理、UI组件和服务层。通过清晰的数据结构和合理的组件划分,使得克制关系的展示和查询变得直观高效。玩家可以快速了解武将间的克制关系,做出更优的战术选择。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐