Difficulty axes: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: hit (line 80), apply (line 100), list (line 112), compare (line 123).
1/// Four axes a genre applies where it decides: what the player is hurt by,
2/// what their own attacks are worth, how quickly the opposition reacts, and
3/// how much help is switched on.
4///
5/// Quoted by `difficulty.md` and shown whole in the Source tab.
6library;
8import 'dart:math' as math;
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:flutter3d_showcase/src/demo/scene_kit.dart';
13import 'package:flutter3d_sim/flutter3d_sim.dart';
14import 'package:vector_math/vector_math.dart';
16final class DifficultyDemo extends ShowcaseDemo {
17 int level = 0;
19 late final MeshNode _player;
20 late final MeshNode _enemy;
21 late final BarGauge _health;
22 double _hp = 100.0;
23 double _swing = 0.0;
24 bool _struck = false;
27 void configureView(DemoContext context) {
28 context.orbit
29 ..distance = 9.0
30 ..pitch = 0.45
31 ..yaw = 0.0;
32 context.orbit.target.setValues(0.0, 0.8, 0.0);
33 }
36 Scene build(DemoContext context) {
37 _player = blockNode(
38 context,
39 'player',
40 Vector3(0.9, 1.6, 0.9),
41 Vector4(0.45, 0.65, 0.95, 1.0),
42 at: Vector3(-2.5, 0.8, 0.0),
43 );
44 _enemy = ballNode(
45 context,
46 'opponent',
47 0.6,
48 Vector4(0.85, 0.35, 0.3, 1.0),
49 at: Vector3(2.5, 0.6, 0.0),
50 );
51 _health = BarGauge(
52 context,
53 'health',
54 Vector4(0.4, 0.85, 0.4, 1.0),
55 Vector3(-3.5, 2.6, 0.0),
56 height: 2.0,
57 );
58 return sceneOf(<SceneNode>[
59 floorNode(context, width: 12.0, depth: 6.0),
60 _player,
61 _enemy,
62 ..._health.nodes,
63 ]);
64 }
67 void update(DemoContext context, double dt) {
68 final Difficulty difficulty = Difficulty.offered[level];
69 // The opponent lunges once a swing; a quicker opponent swings sooner.
70 _swing += dt * difficulty.opponentReaction * 0.6;
71 if (_swing >= 1.0) {
72 _swing -= 1.0;
73 _struck = false;
74 }
75 // Out to the player and back, hitting at the far end.
76 final double reach = math.sin(math.pi * _swing.clamp(0.0, 1.0));
77 _enemy.setPosition(2.5 - 4.2 * reach, 0.6, 0.0);
78 if (!_struck && reach > 0.97) {
79 _struck = true;
80 // The same ten-point hit as ever, scaled by the level.
81 _hp -= _incomingDamage(difficulty, 10.0);
82 if (_hp <= 0.0) _hp = 100.0;
83 }
84 _health.set(_hp / 100.0);
85 }
88 List<DemoControl> controls(DemoContext context) => <DemoControl>[
89 ChoiceControl(
90 'Difficulty',
91 options: <String>[for (final Difficulty d in Difficulty.offered) d.name],
92 index: () => level,
93 onChanged: (int i) {
94 level = i;
95 _hp = 100.0;
96 },
97 ),
98 ];
100 /// A hit a genre would deal at `normal`, scaled by one difficulty.
101 static double _incomingDamage(Difficulty at, double baseAmount) =>
102 baseAmount * at.damageTaken;
104 static String _lineFor(Difficulty level) {
105 final hit = _incomingDamage(level, 10.0);
106 return '${level.name}: a ten-point hit costs $hit, opponents react at '
107 'x${level.opponentReaction} speed, assistance ${level.assistance}';
108 }
110 /// One line for each level on offer, as the settings screen would list them.
111 static List<String> lines() {
112 final lines = <String>[
113 for (final Difficulty level in Difficulty.offered) _lineFor(level),
114 ];
115 return lines;
116 }
119 void verify(Scene scene, FrameResult frame) {
120 if (frame.drawCalls < 1) {
121 throw StateError('the target marker was not drawn');
122 }
123 final onGentle = _incomingDamage(Difficulty.gentle, 10.0);
124 final onPunishing = _incomingDamage(Difficulty.punishing, 10.0);
125 if (!(onGentle < 10.0 && onPunishing > 10.0)) {
126 throw StateError(
127 'gentle should soften a hit and punishing should '
128 'sharpen it, against the same ten points',
129 );
130 }
131 }
132}