From b544f10b8aa9b0b27afeca00aa0e3816165817f0 Mon Sep 17 00:00:00 2001 From: matthewblackman Date: Thu, 6 Jul 2023 15:29:25 -0400 Subject: [PATCH] Create SoccerPlayerPhase and refactor kicking logic to use phase - see https://github.com/phetsims/center-and-variability/issues/327 --- js/common/model/CAVSoccerSceneModel.ts | 3 +- .../model/MeanAndMedianModel.ts | 1 + js/median/model/MedianModel.ts | 1 + js/soccer-common/model/SoccerPlayer.ts | 32 ++++++++++++------- js/soccer-common/model/SoccerPlayerPhase.ts | 21 ++++++++++++ js/soccer-common/model/SoccerSceneModel.ts | 20 ++++++------ js/soccer-common/view/SoccerPlayerNode.ts | 10 +++--- js/variability/model/VariabilitySceneModel.ts | 1 + 8 files changed, 61 insertions(+), 28 deletions(-) create mode 100644 js/soccer-common/model/SoccerPlayerPhase.ts diff --git a/js/common/model/CAVSoccerSceneModel.ts b/js/common/model/CAVSoccerSceneModel.ts index 2c6b5d0a..2b746c30 100644 --- a/js/common/model/CAVSoccerSceneModel.ts +++ b/js/common/model/CAVSoccerSceneModel.ts @@ -29,13 +29,14 @@ export default class CAVSoccerSceneModel, maxKicksChoices: number[], kickDistanceStrategy: TKickDistanceStrategy, + hidePlayersWhenDoneKicking: boolean, physicalRange: Range, kickDistanceStrategyFromStateObject: ( string: string ) => TKickDistanceStrategy, soccerBallFactory: ( isFirstSoccerBall: boolean, options: CAVSoccerBallOptions ) => T, providedOptions: CAVSoccerSceneModelOptions ) { const options = providedOptions; - super( maxKicksProperty, maxKicksChoices, kickDistanceStrategy, physicalRange, + super( maxKicksProperty, maxKicksChoices, kickDistanceStrategy, hidePlayersWhenDoneKicking, physicalRange, kickDistanceStrategyFromStateObject, soccerBallFactory, options ); this.medianValueProperty = new Property( null, { diff --git a/js/mean-and-median/model/MeanAndMedianModel.ts b/js/mean-and-median/model/MeanAndMedianModel.ts index a7d18fe1..c4f38bd7 100644 --- a/js/mean-and-median/model/MeanAndMedianModel.ts +++ b/js/mean-and-median/model/MeanAndMedianModel.ts @@ -53,6 +53,7 @@ export default class MeanAndMedianModel extends CAVModel { MAX_KICKS_PROPERTY, CAVConstants.MAX_KICKS_VALUES, new RandomSkewStrategy(), + true, CAVConstants.PHYSICAL_RANGE, kickDistanceStrategyFromStateObject, CAVSoccerBall.createSoccerBall, { diff --git a/js/median/model/MedianModel.ts b/js/median/model/MedianModel.ts index d1a421ba..3bf3f796 100644 --- a/js/median/model/MedianModel.ts +++ b/js/median/model/MedianModel.ts @@ -50,6 +50,7 @@ export default class MedianModel extends CAVModel { maxKicksProperty, maxKicksAllowed, new RandomSkewStrategy(), + true, CAVConstants.PHYSICAL_RANGE, kickDistanceStrategyFromStateObject, CAVSoccerBall.createSoccerBall, { diff --git a/js/soccer-common/model/SoccerPlayer.ts b/js/soccer-common/model/SoccerPlayer.ts index 152fcad4..3155242f 100644 --- a/js/soccer-common/model/SoccerPlayer.ts +++ b/js/soccer-common/model/SoccerPlayer.ts @@ -9,28 +9,37 @@ import soccerCommon from '../soccerCommon.js'; import Pose from './Pose.js'; -import BooleanProperty from '../../../../axon/js/BooleanProperty.js'; import EnumerationProperty from '../../../../axon/js/EnumerationProperty.js'; import Tandem from '../../../../tandem/js/Tandem.js'; import Property from '../../../../axon/js/Property.js'; import NullableIO from '../../../../tandem/js/types/NullableIO.js'; import NumberIO from '../../../../tandem/js/types/NumberIO.js'; +import { SoccerPlayerPhase } from './SoccerPlayerPhase.js'; +import DerivedProperty from '../../../../axon/js/DerivedProperty.js'; +import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js'; export default class SoccerPlayer { - public readonly poseProperty; + public readonly soccerPlayerPhaseProperty: Property; + public readonly poseProperty: TReadOnlyProperty; + public readonly timestampWhenPoisedBeganProperty: Property; - // Also used to determine the artwork for rendering the SoccerPlayerNode + // Used to determine the artwork for rendering the SoccerPlayerNode public readonly initialPlaceInLine: number; - public readonly timestampWhenPoisedBeganProperty: Property; - public readonly isActiveProperty: BooleanProperty; - public constructor( placeInLine: number, tandem: Tandem ) { - this.poseProperty = new EnumerationProperty( Pose.STANDING, { - tandem: tandem.createTandem( 'poseProperty' ) + this.soccerPlayerPhaseProperty = new EnumerationProperty( placeInLine === 0 ? SoccerPlayerPhase.READY : SoccerPlayerPhase.INACTIVE, { + tandem: tandem.createTandem( 'soccerPlayerPhaseProperty' ) } ); - this.isActiveProperty = new BooleanProperty( placeInLine === 0, { - tandem: tandem.createTandem( 'isActiveProperty' ) + this.poseProperty = new DerivedProperty( [ this.soccerPlayerPhaseProperty ], soccerPlayerPhase => { + if ( soccerPlayerPhase === SoccerPlayerPhase.POISED ) { + return Pose.POISED_TO_KICK; + } + else if ( soccerPlayerPhase === SoccerPlayerPhase.KICKING ) { + return Pose.KICKING; + } + else { + return Pose.STANDING; + } } ); this.timestampWhenPoisedBeganProperty = new Property( null, { tandem: tandem.createTandem( 'timestampWhenPoisedBeganProperty' ), @@ -40,9 +49,8 @@ export default class SoccerPlayer { } public reset(): void { - this.poseProperty.reset(); + this.soccerPlayerPhaseProperty.reset(); this.timestampWhenPoisedBeganProperty.reset(); - this.isActiveProperty.reset(); } } diff --git a/js/soccer-common/model/SoccerPlayerPhase.ts b/js/soccer-common/model/SoccerPlayerPhase.ts new file mode 100644 index 00000000..6a1a3084 --- /dev/null +++ b/js/soccer-common/model/SoccerPlayerPhase.ts @@ -0,0 +1,21 @@ +// Copyright 2023, University of Colorado Boulder + +import Enumeration from '../../../../phet-core/js/Enumeration.js'; +import EnumerationValue from '../../../../phet-core/js/EnumerationValue.js'; +import soccerCommon from '../soccerCommon.js'; + +/** + * SoccerPlayerPhase is used to identify what part of the kicking phase a SoccerPlayer is currently in + * + * @author Matthew Blackman (PhET Interactive Simulations) + */ + +export class SoccerPlayerPhase extends EnumerationValue { + public static readonly INACTIVE = new SoccerPlayerPhase(); + public static readonly READY = new SoccerPlayerPhase(); + public static readonly POISED = new SoccerPlayerPhase(); + public static readonly KICKING = new SoccerPlayerPhase(); + private static readonly enumeration = new Enumeration( SoccerPlayerPhase ); +} + +soccerCommon.register( 'SoccerPlayerPhase', SoccerPlayerPhase ); \ No newline at end of file diff --git a/js/soccer-common/model/SoccerSceneModel.ts b/js/soccer-common/model/SoccerSceneModel.ts index f3bb26b2..9b58e700 100644 --- a/js/soccer-common/model/SoccerSceneModel.ts +++ b/js/soccer-common/model/SoccerSceneModel.ts @@ -26,11 +26,9 @@ import SoccerPlayer from './SoccerPlayer.js'; import dotRandom from '../../../../dot/js/dotRandom.js'; import Animation from '../../../../twixt/js/Animation.js'; import Easing from '../../../../twixt/js/Easing.js'; -import Pose from './Pose.js'; import { SoccerBallPhase } from './SoccerBallPhase.js'; import BooleanProperty from '../../../../axon/js/BooleanProperty.js'; import PhetioObject, { PhetioObjectOptions } from '../../../../tandem/js/PhetioObject.js'; -import Multilink from '../../../../axon/js/Multilink.js'; import { TKickDistanceStrategy } from './TKickDistanceStrategy.js'; import IOType from '../../../../tandem/js/types/IOType.js'; import VoidIO from '../../../../tandem/js/types/VoidIO.js'; @@ -45,6 +43,8 @@ import SoccerCommonQueryParameters from '../SoccerCommonQueryParameters.js'; import Tandem from '../../../../tandem/js/Tandem.js'; import ArrayIO from '../../../../tandem/js/types/ArrayIO.js'; import KickDistanceStrategy from './KickDistanceStrategy.js'; +import { SoccerPlayerPhase } from './SoccerPlayerPhase.js'; +import Multilink from '../../../../axon/js/Multilink.js'; const kickSound = new SoundClip( basicKick_mp3, { initialOutputLevel: 0.2 } ); soundManager.addSoundGenerator( kickSound ); @@ -89,7 +89,7 @@ export default class SoccerSceneModel extends private readonly timeWhenLastBallWasKickedProperty: NumberProperty; - // Starting at 0, iterate through the index of the kickers. This updates the SoccerPlayer.isActiveProperty to show the current kicker + // Starting at 0, iterate through the index of the kickers. This updates the SoccerPlayer.soccerPlayerPhaseProperty to show the current kicker private readonly activeKickerIndexProperty: NumberProperty; // Called when the location of a ball changed within a stack, so the pointer areas can be updated @@ -110,6 +110,7 @@ export default class SoccerSceneModel extends public readonly maxKicksProperty: TReadOnlyProperty, maxKicksChoices: number[], initialKickDistanceStrategy: TKickDistanceStrategy, + hidePlayersWhenDoneKicking: boolean, public readonly physicalRange: Range, kickDistanceStrategyFromStateObject: ( string: string ) => TKickDistanceStrategy, createSoccerBall: ( isFirstSoccerBall: boolean, options: { tandem: Tandem } ) => T, @@ -263,7 +264,7 @@ export default class SoccerSceneModel extends Multilink.multilink( [ this.activeKickerIndexProperty, this.maxKicksProperty ], ( activeKickerIndex, maxKicks ) => { this.soccerPlayers.forEach( ( soccerPlayer, index ) => { - soccerPlayer.isActiveProperty.value = index === activeKickerIndex && index < maxKicks; + soccerPlayer.soccerPlayerPhaseProperty.value = index === activeKickerIndex && ( index < maxKicks || !hidePlayersWhenDoneKicking ) ? SoccerPlayerPhase.READY : SoccerPlayerPhase.INACTIVE; } ); } ); @@ -433,14 +434,14 @@ export default class SoccerSceneModel extends this.advanceLine(); - if ( frontPlayer.poseProperty.value === Pose.STANDING ) { - frontPlayer.poseProperty.value = Pose.POISED_TO_KICK; + if ( frontPlayer.soccerPlayerPhaseProperty.value === SoccerPlayerPhase.READY ) { + frontPlayer.soccerPlayerPhaseProperty.value = SoccerPlayerPhase.POISED; frontPlayer.timestampWhenPoisedBeganProperty.value = this.timeProperty.value; } } // How long has the front player been poised? - if ( frontPlayer.poseProperty.value === Pose.POISED_TO_KICK ) { + if ( frontPlayer.soccerPlayerPhaseProperty.value === SoccerPlayerPhase.POISED ) { assert && assert( typeof frontPlayer.timestampWhenPoisedBeganProperty.value === 'number', 'timestampWhenPoisedBegan should be a number' ); const elapsedTime = this.timeProperty.value - frontPlayer.timestampWhenPoisedBeganProperty.value!; if ( elapsedTime > 0.075 ) { @@ -488,8 +489,7 @@ export default class SoccerSceneModel extends // Allow kicking another ball while one is already in the air. // if the previous ball was still in the air, we need to move the line forward so the next player can kick - const kickers = this.soccerPlayers.filter( soccerPlayer => soccerPlayer.isActiveProperty.value && - soccerPlayer.poseProperty.value === Pose.KICKING ); + const kickers = this.soccerPlayers.filter( soccerPlayer => soccerPlayer.soccerPlayerPhaseProperty.value === SoccerPlayerPhase.KICKING ); if ( kickers.length > 0 ) { let nextIndex = this.activeKickerIndexProperty.value + 1; if ( nextIndex > this.maxKicksProperty.value ) { @@ -570,7 +570,7 @@ export default class SoccerSceneModel extends * Select a target location for the nextBallToKick, set its velocity and mark it for animation. */ private kickBall( soccerPlayer: SoccerPlayer, soccerBall: T, playAudio: boolean ): void { - soccerPlayer.poseProperty.value = Pose.KICKING; + soccerPlayer.soccerPlayerPhaseProperty.value = SoccerPlayerPhase.KICKING; const x1 = SoccerCommonQueryParameters.sameSpot ? 7 : this.kickDistanceStrategy.currentStrategy.getNextKickDistance( this.soccerBalls.indexOf( soccerBall ) ); diff --git a/js/soccer-common/view/SoccerPlayerNode.ts b/js/soccer-common/view/SoccerPlayerNode.ts index f09ef433..22eac19c 100644 --- a/js/soccer-common/view/SoccerPlayerNode.ts +++ b/js/soccer-common/view/SoccerPlayerNode.ts @@ -15,7 +15,7 @@ import optionize, { EmptySelfOptions } from '../../../../phet-core/js/optionize. import Vector2 from '../../../../dot/js/Vector2.js'; import ModelViewTransform2 from '../../../../phetcommon/js/view/ModelViewTransform2.js'; import Pose from '../model/Pose.js'; -import Multilink from '../../../../axon/js/Multilink.js'; +import { SoccerPlayerPhase } from '../model/SoccerPlayerPhase.js'; type SelfOptions = EmptySelfOptions; type SoccerPlayerNodeOptions = SelfOptions & NodeOptions; @@ -61,6 +61,10 @@ export default class SoccerPlayerNode extends Node { } ) ); } + soccerPlayer.soccerPlayerPhaseProperty.link( phase => { + this.visible = phase !== SoccerPlayerPhase.INACTIVE; + } ); + soccerPlayer.poseProperty.link( pose => { standingNode.visible = pose === Pose.STANDING; poisedToKickNode.visible = pose === Pose.POISED_TO_KICK; @@ -68,10 +72,6 @@ export default class SoccerPlayerNode extends Node { this.centerBottom = modelViewTransform.modelToViewPosition( new Vector2( 0, 0 ) ).plusXY( -28, 8.5 ); } ); - Multilink.multilink( [ soccerPlayer.isActiveProperty ], isActive => { - this.visible = isActive; - } ); - const options = optionize()( { excludeInvisibleChildrenFromBounds: false, phetioVisiblePropertyInstrumented: false diff --git a/js/variability/model/VariabilitySceneModel.ts b/js/variability/model/VariabilitySceneModel.ts index 6a489fac..461d6dd6 100644 --- a/js/variability/model/VariabilitySceneModel.ts +++ b/js/variability/model/VariabilitySceneModel.ts @@ -40,6 +40,7 @@ export default class VariabilitySceneModel extends CAVSoccerSceneModel